# 蓝牙数据传输协议与 API 通信文档 本文档是固件与蓝牙客户端(微信小程序)之间 BLE 通信的唯一协议说明,包含 GATT 定义、二进制帧格式、命令清单、TLV 定义、结果码/状态码、主动推送、响应模型与客户端接入要求。 > 本文档**以当前工程代码为准**,对应源码: > `include/ble_tlv_protocol.h`、`src/ble_tlv_protocol.cpp`、`src/radar_manager.cpp`、`src/tasks_manager.cpp`、`src/wifi_manager.cpp`。 > 若代码与本文档不一致,以代码为准并同步更新本文档。 --- ## 概述 本协议用于雷达设备与蓝牙客户端(微信小程序)之间的双向通信。设备作为 BLE GATT Server(外设),客户端作为 GATT Client(中心);所有业务数据统一封装为 **TLV 二进制帧**,通过 GATT 特征的写入与 Notify 收发。 ``` ┌──────────────┐ b1 写入(命令) ┌────────────────┐ │ │ ──────────────────────▶ │ │ │ BLE 客户端 │ b2 通知(响应) │ 雷达设备 │ │ (小程序) │ ◀────────────────────── │ (GATT Server)│ │ │ a1/a2/b3 通知(推送) │ │ │ │ ◀────────────────────── │ │ └──────────────┘ └────────────────┘ ``` **通信模型** | 方向 | 特征 | 说明 | | --- | --- | --- | | 客户端 → 设备 | `b1`(Write) | 下发命令 | | 设备 → 客户端 | `b2`(Notify) | 命令响应,按 `seq` 与请求匹配 | | 设备 → 客户端 | `a1` / `a2` / `b3`(Notify) | 主动推送数据与状态,不做请求匹配 | **协议特点** 1. 统一帧格式 `AA 55 | VER | CMD | SEQ | LEN | PAYLOAD | CRC`,二进制紧凑,适合嵌入式设备。 2. 命令与数据分离:命令走 `b1/b2` 一问一答,数据与状态走 `a1/a2/b3` 主动推送。 3. 结果以 `TLV_RESULT_CODE` 判定,错误码/状态码的文案由客户端本地映射。 4. 支持 Notify 分包与客户端重组(当前固定 20 字节一包)。 5. 状态推送去重:仅在状态变化时经 `b3` 发送。 6. 配网等耗时命令采用"先 `PROCESSING`、后最终结果"的异步响应模型。 **当前实现参数** | 项目 | 值 | | --- | --- | | 协议版本 | `0x01` | | 帧头 | `0xAA 0x55` | | 校验 | CRC16-CCITT(覆盖 `VERSION` ~ `PAYLOAD`,大端) | | Notify 分包 | 固定 20 字节 | | 命令接收缓冲 | 256 字节(超限返回 `ERR_PROTO_FRAME_TOO_LARGE`) | | 默认连续推送间隔 | 500 ms(可通过 `TLV_INTERVAL_MS` 设置为 100~10000 ms) | --- ## 一、设计原则 1. 客户端必须先按 GATT 特征 UUID 分流,再按 `CMD` 命令码解析。 2. `b1/b2` 是命令请求/响应通道:客户端写 `b1`,设备通过 `b2` Notify 响应。 3. `a1/a2/b3` 是主动推送通道,不参与请求响应匹配。 4. Notify 单包可能小于整帧,客户端必须按特征分别维护重组 buffer。 5. 命令结果只以 `TLV_RESULT_CODE` 为事实来源,错误文案由客户端按错误码本地映射。 6. 运行状态变化通过 `b3` 推送,与命令结果解耦。 7. 协议层不再使用 `flags`、`ACK`、`TLV_STATE`、`TLV_STEP`、`TLV_MESSAGE`、`TLV_ERROR_MESSAGE`。 8. `READ` 不作为主协议能力,客户端不依赖读特征获取业务数据(只有 `a1/a2/b2/b3` 为 Notify,`b1` 为 Write)。 --- ## 二、GATT 服务与特征 ### 2.1 Radar Data Service(雷达数据服务) Service UUID:`a8c1e5c0-3d5d-4a9d-8d5e-7c8b6a4e2f1a` | 别名 | UUID | 属性 | 方向 | 职责 | | --- | --- | --- | --- | --- | | `a1` | `beb5483e-36e1-4688-b7f5-ea07361b26a1` | NOTIFY | 设备→客户端 | 连续雷达数据流推送 | | `a2` | `beb5483e-36e1-4688-b7f5-ea07361b26a2` | NOTIFY | 设备→客户端 | 雷达状态主动推送 | ### 2.2 Device Config Service(设备配置服务) Service UUID:`a8c1e5c0-3d5d-4a9d-8d5e-7c8b6a4e2f1b` | 别名 | UUID | 属性 | 方向 | 职责 | | --- | --- | --- | --- | --- | | `b1` | `beb5483e-36e1-4688-b7f5-ea07361b26b1` | WRITE | 客户端→设备 | 命令写入通道 | | `b2` | `beb5483e-36e1-4688-b7f5-ea07361b26b2` | NOTIFY | 设备→客户端 | 命令响应通道 | | `b3` | `beb5483e-36e1-4688-b7f5-ea07361b26b3` | NOTIFY | 设备→客户端 | 设备信息与状态主动推送 | ### 2.3 通道职责 | 通道 | 属性 | 匹配模式 | 说明 | | --- | --- | --- | --- | | `b1` | Write | 请求→响应 | 客户端发送命令 | | `b2` | Notify | 请求→响应 | 设备返回命令响应,按 `seq` 匹配 | | `a1` | Notify | 仅推送 | 连续雷达数据流,不做请求匹配 | | `a2` | Notify | 仅推送 | 雷达状态推送,不做请求匹配 | | `b3` | Notify | 仅推送 | 设备信息/状态推送,不做请求匹配 | --- ## 三、二进制帧格式 ### 3.1 帧结构 ``` ┌──────┬──────┬───────┬─────┬─────┬───────┬───────┬─────────┬───────┬───────┐ │ SOF1 │ SOF2 │ VER │ CMD │ SEQ │ LEN_H │ LEN_L │ PAYLOAD │ CRC_H │ CRC_L │ │ 1B │ 1B │ 1B │ 1B │ 1B │ 1B │ 1B │ N B │ 1B │ 1B │ └──────┴──────┴───────┴─────┴─────┴───────┴───────┴─────────┴───────┴───────┘ ``` | 字段 | 偏移 | 长度 | 说明 | | --- | --- | --- | --- | | `SOF1` | 0 | 1 | 帧头,固定 `0xAA` | | `SOF2` | 1 | 1 | 帧头,固定 `0x55` | | `VERSION` | 2 | 1 | 协议版本,当前 `0x01` | | `CMD` | 3 | 1 | 命令码 | | `SEQ` | 4 | 1 | 命令请求/响应序列号;主动推送见下表 | | `LEN` | 5-6 | 2 | PAYLOAD 长度,大端 | | `PAYLOAD` | 7 | N | TLV 编码数据区 | | `CRC` | 7+N | 2 | CRC16-CCITT,大端 | - 最小帧长:**9 字节**(空 PAYLOAD)。 - 所有多字节整数均为**大端**。 **SEQ 取值规则(与代码一致)** | 帧类型 | SEQ | | --- | --- | | 命令请求(b1)与响应(b2) | 请求的 `seq`,原样返回 | | `a1` 连续推送 | 设备侧自增(`bleSequenceCounter++`) | | `a2` 雷达状态推送 | 固定 `0x00` | | `b3` 设备信息/状态推送 | 固定 `0x00` | > 客户端只在 `b2` 上按 `seq` 匹配请求响应;`a1/a2/b3` 不参与匹配,因此 `a1` 的自增 `seq` 仅作参考。 ### 3.2 CRC16-CCITT - 算法:CRC16-CCITT(与代码 `crc16Ccitt()` 一致) - 多项式:`0x1021` - 初值:`0xFFFF` - 输入/输出反转:否 - 最终异或:无 - 计算范围:从 `VERSION` 到 `PAYLOAD` 末尾(**不含** `SOF1/SOF2` 与 `CRC` 本身) - 字节序:大端 ### 3.3 TLV 编码 ``` ┌────────┬───────┬───────┬─────────┐ │ TYPE │ LEN_H │ LEN_L │ VALUE │ │ 1B │ 1B │ 1B │ N B │ └────────┴───────┴───────┴─────────┘ ``` `LEN` 为大端,`VALUE` 长度由 `LEN` 指定。 ### 3.4 Notify 分包与重组 固件统一通过 `sendFrameToBLE()` 发送 Notify: - 当前为**固定 20 字节分片**模式(`BLE_FIXED_20_BYTE_MODE = 1`),单包最大 20 字节。 - 发送带互斥锁与流控(约 500 B/s,包间隔 ≥5ms)。 - 仅当对应特征已被客户端订阅(CCCD `0x2902`)且已连接时才发送。 客户端必须为每个 Notify 特征分别维护重组 buffer: | 通道 | Buffer | 说明 | | --- | --- | --- | | `a1` | `a1Buffer` | 连续数据流重组 | | `a2` | `a2Buffer` | 雷达状态重组 | | `b2` | `b2Buffer` | 命令响应重组 | | `b3` | `b3Buffer` | 设备信息/状态重组 | --- ## 四、命令码总览 | 命令 | 值 | 通道 | 说明 | | --- | --- | --- | --- | | `CMD_PING` | `0x01` | b1/b2 | Ping 请求/响应(可携带回显内容) | | `CMD_QUERY_STATUS` | `0x10` | b1/b2 | 查询设备概览 | | `CMD_QUERY_RADAR` | `0x12` | b1/b2 | 查询雷达数据快照 | | `CMD_START_CONTINUOUS` | `0x14` | b1/b2 | 启动连续推送 | | `CMD_STOP_CONTINUOUS` | `0x16` | b1/b2 | 停止连续推送 | | `CMD_RADAR_SLEEP_QUERY` | `0x17` | b1/b2 | 雷达睡眠/综合状态查询开关 | | `CMD_CONTINUOUS_PUSH` | `0x18` | a1 | 连续雷达数据主动推送 | | `CMD_DEVICE_INFO_PUSH` | `0x19` | b3 | 设备信息/状态主动推送 | | `CMD_RADAR_STATUS_PUSH` | `0x1A` | a2 | 雷达状态主动推送 | | `CMD_WIFI_SCAN` | `0x20` | b1/b2 | WiFi 扫描 | | `CMD_WIFI_CONFIG` | `0x22` | b1/b2 | WiFi 配网 | | `CMD_GET_SAVED_WIFI` | `0x24` | b1/b2 | 查询已保存 WiFi | | `CMD_DELETE_SAVED_WIFI` | `0x26` | b1/b2 | 删除已保存 WiFi | | `CMD_LED_CONTROL` | `0x30` | b1/b2 | 系统指示灯开关 | | `CMD_ERROR_RESP` | `0x7E` | b2 | 协议层/入口级错误响应 | --- ## 五、命令详解(b1 写入 / b2 响应) ### 5.1 CMD_PING(0x01) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_ECHO_CONTENT` | string | 否 | 回显内容,可携带任意字符串 | **响应**(b2): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | `SUCCESS` | | `TLV_ECHO_CONTENT` | string | 仅当请求携带内容时原样回显 | --- ### 5.2 CMD_QUERY_STATUS(0x10) **请求**:无 PAYLOAD。 **响应**(b2,按代码 `buildStatusPayload()` 的字段与顺序): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 是 | 结果码 | | `TLV_DEVICE_SN` | uint64 | 否 | 设备序列号,仅 `device_sn > 0` 时发送 | | `TLV_PROTOCOL_VERSION` | string | 是 | 协议版本(当前 `"1.0.0"`) | | `TLV_FIRMWARE_VERSION` | string | 是 | 固件版本 | | `TLV_DEVICE_TYPE` | string | 是 | 设备类型(当前 `"Radar"`) | | `TLV_MAC_ADDRESS` | string | 是 | MAC 地址 | | `TLV_WIFI_CONFIGURED` | uint8 | 是 | 是否保存过 WiFi(0/1) | | `TLV_WIFI_CONNECTED` | uint8 | 是 | WiFi 是否已连接(0/1) | | `TLV_IP_ADDRESS` | string | 否 | IP 地址,仅 WiFi 已连接时发送 | | `TLV_SSID` | string | 否 | 当前 WiFi 名称,仅 WiFi 已连接时发送 | | `TLV_WIFI_STATUS` | uint8 | 否 | WiFi 状态,当前状态非 0 时发送 | | `TLV_MQTT_STATUS` | uint8 | 否 | MQTT 状态,当前状态非 0 时发送 | | `TLV_RADAR_SLEEP_STATUS` | uint8 | 否 | 雷达睡眠查询状态,当前状态非 0 时发送 | | `TLV_LED_ENABLED` | uint8 | 是 | 系统指示灯开关(0/1),始终发送 | --- ### 5.3 CMD_QUERY_RADAR(0x12) **请求**:无 PAYLOAD。 **响应**(b2,按代码 `processQueryRadarData()`): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_TIMESTAMP` | uint32 | 设备运行时间戳(`millis()`,单位 ms) | | `TLV_PRESENCE` | uint8 | 是否有人(0=无人,1=有人) | | `TLV_HEART_RATE_X10` | uint16 | 心率×10 | | `TLV_BREATH_RATE_X10` | uint16 | 呼吸率×10 | | `TLV_MOTION` | uint8 | 运动状态 | | `TLV_DISTANCE_CM` | uint16 | 距离(cm) | | `TLV_POS_X_MM` | int16 | X 坐标(mm) | | `TLV_POS_Y_MM` | int16 | Y 坐标(mm) | | `TLV_POS_Z_MM` | int16 | Z 坐标(mm) | | `TLV_BODY_MOVEMENT` | uint8 | 体动 | > 快照即使无人也返回,字段为 0 属合法,客户端自行判断有效性。 --- ### 5.4 CMD_START_CONTINUOUS(0x14) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_INTERVAL_MS` | uint16 | 是 | 推送间隔,有效范围 **100~10000 ms** | **响应**(b2): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_INTERVAL_MS` | uint16 | 成功时回传实际生效的间隔 | **错误**:缺少参数 → `ERR_PROTO_PARAM_MISSING`;超出范围 → `ERR_PROTO_PARAM_INVALID`。 **效果**:启动后设备通过 `a1` 推送 `CMD_CONTINUOUS_PUSH`,并通过 `a2` 每 200ms 推送 `CMD_RADAR_STATUS_PUSH`。 --- ### 5.5 CMD_STOP_CONTINUOUS(0x16) **请求**:无 PAYLOAD。 **响应**(b2):`TLV_RESULT_CODE = SUCCESS`。 **说明**:幂等操作,无论当前是否在推送都返回成功;BLE 断开时设备自动停止推送,重连后需重新下发 `CMD_START_CONTINUOUS`。 --- ### 5.6 CMD_RADAR_SLEEP_QUERY(0x17) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_RADAR_SLEEP_ENABLED` | uint8 | 是 | `0`=关闭,`1`=开启 | **响应**(b2): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_RADAR_SLEEP_ENABLED` | uint8 | 当前开关状态 | **错误**:缺失 → `ERR_PROTO_PARAM_MISSING`;非 0/1 → `ERR_PROTO_PARAM_INVALID`。 **推送**:开关状态变化时经 `b3` 推送 `RADAR_SLEEP_QUERY_ENABLED (0x31)` / `RADAR_SLEEP_QUERY_DISABLED (0x30)`。 --- ### 5.7 CMD_LED_CONTROL(0x30) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_LED_ENABLED` | uint8 | 是 | `0`=关闭,`1`=开启 | **响应**(b2): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_LED_ENABLED` | uint8 | 当前开关状态 | **错误**:缺失 → `ERR_PROTO_PARAM_MISSING`;非 0/1 → `ERR_PROTO_PARAM_INVALID`。 **持久化与推送**:开关状态保存到 Flash(Preferences 命名空间 `radar_data`,键 `ledEnabled`),断电保持;状态变化时经 `b3` 推送 `LED_ENABLED (0x33)` / `LED_DISABLED (0x32)`。 --- ### 5.8 CMD_WIFI_SCAN(0x20) **请求**:无 PAYLOAD。 **异步响应**(b2): 第一阶段(立即): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | `PROCESSING (0x01)` | 第二阶段(扫描完成): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 是 | 结果码 | | `TLV_WIFI_COUNT` | uint16 | 否 | WiFi 数量,成功且非空时发送 | | `TLV_WIFI_ITEM` | block | 否 | WiFi 条目,可重复多个 | 每个 `TLV_WIFI_ITEM` 内为独立 TLV: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_SSID` | string | WiFi 名称 | | `TLV_RSSI` | int8 | 信号强度(dBm) | | `TLV_SECURITY` | uint8 | 加密类型,见第十节 | **错误**:启动扫描失败 → `ERR_WIFI_BUSY`;超时 → `ERR_WIFI_SCAN_TIMEOUT`。 --- ### 5.9 CMD_WIFI_CONFIG(0x22) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_SSID` | string | 是 | WiFi 名称 | | `TLV_PASSWORD` | string | 是 | WiFi 密码 | **异步响应**(b2): 第一阶段(立即): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | `PROCESSING (0x01)` | | `TLV_SSID` | string | 正在配置的 SSID(回显) | 第二阶段(完成): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 是 | 结果码 | | `TLV_SSID` | string | 否 | 成功时发送 | | `TLV_IP_ADDRESS` | string | 否 | 成功时发送 | **错误**:SSID 为空 → `ERR_PROTO_PARAM_MISSING`;其他常见错误 `ERR_WIFI_BUSY (0x26)`、`ERR_WIFI_SSID_NOT_FOUND (0x21)`、`ERR_WIFI_SIGNAL_WEAK (0x25)`、`ERR_WIFI_WRONG_PASSWORD (0x22)`、`ERR_WIFI_SCAN_TIMEOUT (0x20)`、`ERR_DEV_STORAGE_FAIL (0x41)`。 --- ### 5.10 CMD_GET_SAVED_WIFI(0x24) **请求**:无 PAYLOAD。 **响应**(b2,即时命令): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 是 | `SUCCESS` 或 `ERR_DEV_STORAGE_FAIL` | | `TLV_WIFI_COUNT` | uint16 | 否 | 成功时发送 | | `TLV_WIFI_ITEM` | block | 否 | 成功时发送,可重复多个 | 每个 `TLV_WIFI_ITEM` 内为独立 TLV: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_SSID` | string | WiFi 名称 | | `TLV_PASSWORD` | string | WiFi 密码 | > 注意:已保存网络回包中包含密码,仅用于本地配网管理。 --- ### 5.11 CMD_DELETE_SAVED_WIFI(0x26) **请求**(b1): | TLV | 类型 | 必选 | 说明 | | --- | --- | --- | --- | | `TLV_SSID` | string | 是 | 要删除的 WiFi 名称 | **响应**(b2,成功时): | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_SSID` | string | 被删除的 SSID | | `TLV_WIFI_COUNT` | uint16 | 删除后剩余数量 | **错误**:SSID 为空 → `ERR_PROTO_PARAM_MISSING`;网络不存在 → `ERR_WIFI_SSID_NOT_FOUND`;删除失败 → `ERR_DEV_STORAGE_FAIL`。 --- ### 5.12 CMD_ERROR_RESP(0x7E) 用于协议层/入口级、无法归属到具体业务命令的错误(未知命令、帧过大、队列满、队列未初始化等),设备通过 `b2` 返回: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 错误码 | --- ## 六、主动推送(a1 / a2 / b3) ### 6.1 a1 — CMD_CONTINUOUS_PUSH(0x18)连续雷达数据 - **触发**:`CMD_START_CONTINUOUS` 成功后,按 `TLV_INTERVAL_MS` 间隔推送(默认 500ms),且仅在数据发生变化时发送。 - **帧**:`cmd = 0x18`,`seq = 设备侧自增`。 - **TLV**: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_PRESENCE` | uint8 | 是否有人(0/1) | | `TLV_HEART_RATE_X10` | uint16 | 心率×10 | | `TLV_BREATH_RATE_X10` | uint16 | 呼吸率×10 | | `TLV_MOTION` | uint8 | 运动状态 | > 无人时上述字段均发 0。`TLV_SLEEP_STATE`、`TLV_HEART_WAVEFORM`、`TLV_BREATH_WAVEFORM` 当前代码未发送(已注释),客户端不应依赖。 ### 6.2 a2 — CMD_RADAR_STATUS_PUSH(0x1A)雷达状态 - **触发**:`CMD_START_CONTINUOUS` 成功后,每 200ms 推送。 - **帧**:`cmd = 0x1A`,`seq = 0x00`。 - **TLV**: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_DISTANCE_CM` | uint16 | 距离(cm) | | `TLV_POS_X_MM` | int16 | X 坐标(mm) | | `TLV_POS_Y_MM` | int16 | Y 坐标(mm) | | `TLV_POS_Z_MM` | int16 | Z 坐标(mm) | | `TLV_BODY_MOVEMENT` | uint8 | 体动 | ### 6.3 b3 — CMD_DEVICE_INFO_PUSH(0x19)设备信息与状态 - **触发**:BLE 连接建立时同步当前状态;WiFi/MQTT/雷达睡眠查询/指示灯状态变化时推送(去重,仅变化时发送)。 - **帧**:`cmd = 0x19`,`seq = 0x00`。 **状态推送 TLV**: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_DEVICE_STATUS` | uint8 | 设备状态码,见第九节 | **设备信息推送 TLV**: | TLV | 类型 | 说明 | | --- | --- | --- | | `TLV_RESULT_CODE` | uint8 | 结果码 | | `TLV_PROTOCOL_VERSION` | string | 协议版本 | | `TLV_FIRMWARE_VERSION` | string | 固件版本 | | `TLV_DEVICE_TYPE` | string | 设备类型 | | `TLV_MAC_ADDRESS` | string | MAC 地址 | | `TLV_DEVICE_SN` | uint64 | 设备序列号,仅存在时发送 | --- ## 七、TLV 类型码 ### 7.1 设备信息(0x01-0x0F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_RESULT_CODE` | `0x02` | uint8 | 结果码 | | `TLV_TIMESTAMP` | `0x04` | uint32 | 时间戳 | | `TLV_PROTOCOL_VERSION` | `0x05` | string | 协议版本 | | `TLV_DEVICE_SN` | `0x06` | uint64 | 设备序列号,仅存在时发送 | | `TLV_FIRMWARE_VERSION` | `0x07` | string | 固件版本 | | `TLV_DEVICE_TYPE` | `0x08` | string | 设备类型 | | `TLV_MAC_ADDRESS` | `0x09` | string | MAC 地址 | ### 7.2 雷达数据(0x10-0x1F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_HEART_RATE_X10` | `0x10` | uint16 | 心率×10(720 = 72.0 bpm) | | `TLV_BREATH_RATE_X10` | `0x11` | uint16 | 呼吸率×10(180 = 18.0 次/分) | | `TLV_PRESENCE` | `0x12` | uint8 | 存在检测(0=无人,1=有人) | | `TLV_MOTION` | `0x13` | uint8 | 运动状态 | | `TLV_SLEEP_STATE` | `0x14` | uint8 | 睡眠状态 | | `TLV_DISTANCE_CM` | `0x15` | uint16 | 距离(cm) | | `TLV_POS_X_MM` | `0x16` | int16 | X 坐标(mm) | | `TLV_POS_Y_MM` | `0x17` | int16 | Y 坐标(mm) | | `TLV_POS_Z_MM` | `0x18` | int16 | Z 坐标(mm) | | `TLV_BODY_MOVEMENT` | `0x19` | uint8 | 体动 | ### 7.3 WiFi(0x20-0x2F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_SSID` | `0x20` | string | WiFi 名称 | | `TLV_PASSWORD` | `0x21` | string | WiFi 密码 | | `TLV_WIFI_COUNT` | `0x22` | uint16 | WiFi 数量 | | `TLV_WIFI_ITEM` | `0x23` | block | WiFi 条目(嵌套 TLV) | | `TLV_RSSI` | `0x24` | int8 | 信号强度(dBm) | | `TLV_SECURITY` | `0x25` | uint8 | 加密类型,见第十节 | ### 7.4 控制与状态(0x30-0x3F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_INTERVAL_MS` | `0x31` | uint16 | 推送间隔(ms) | | `TLV_RADAR_SLEEP_ENABLED` | `0x32` | uint8 | 雷达睡眠查询开关(0=关,1=开) | | `TLV_DEVICE_STATUS` | `0x33` | uint8 | 设备状态码(b3 推送) | | `TLV_WIFI_STATUS` | `0x34` | uint8 | WiFi 状态 | | `TLV_MQTT_STATUS` | `0x35` | uint8 | MQTT 状态 | | `TLV_RADAR_SLEEP_STATUS` | `0x36` | uint8 | 雷达睡眠查询状态 | | `TLV_LED_ENABLED` | `0x37` | uint8 | 系统指示灯开关(0=关,1=开) | ### 7.5 通用消息(0x40-0x4F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_IP_ADDRESS` | `0x41` | string | IP 地址 | | `TLV_WIFI_CONFIGURED` | `0x42` | uint8 | 是否保存过 WiFi(0/1) | | `TLV_WIFI_CONNECTED` | `0x43` | uint8 | WiFi 是否已连接(0/1) | | `TLV_ECHO_CONTENT` | `0x44` | string | 回显内容 | ### 7.6 波形(0x60-0x6F) | 常量 | 值 | 类型 | 说明 | | --- | --- | --- | --- | | `TLV_HEART_WAVEFORM` | `0x60` | uint8 | 心跳波形(原始 int8 + 128) | | `TLV_BREATH_WAVEFORM` | `0x61` | uint8 | 呼吸波形(原始 int8 + 128) | --- ## 八、结果码 | 常量 | 值 | 说明 | | --- | --- | --- | | `SUCCESS` | `0x00` | 成功 | | `PROCESSING` | `0x01` | 命令已接收,处理中 | | `ERR_PROTO_CMD_UNKNOWN` | `0x13` | 未知命令 | | `ERR_PROTO_PARAM_MISSING` | `0x14` | 参数缺失 | | `ERR_PROTO_PARAM_INVALID` | `0x15` | 参数非法 | | `ERR_PROTO_BUSY` | `0x16` | 设备忙 | | `ERR_PROTO_FRAME_TOO_LARGE` | `0x18` | 请求帧过大(超过 256 字节) | | `ERR_WIFI_SCAN_TIMEOUT` | `0x20` | WiFi 扫描超时 | | `ERR_WIFI_SSID_NOT_FOUND` | `0x21` | 未找到 SSID | | `ERR_WIFI_WRONG_PASSWORD` | `0x22` | WiFi 密码错误 | | `ERR_WIFI_SIGNAL_WEAK` | `0x25` | WiFi 信号弱 | | `ERR_WIFI_BUSY` | `0x26` | WiFi 忙 | | `ERR_DEV_STATE_INVALID` | `0x40` | 当前设备状态不允许 | | `ERR_DEV_STORAGE_FAIL` | `0x41` | 存储失败 | | `ERR_DEV_QUEUE_FULL` | `0x42` | 命令队列已满 | --- ## 九、设备状态码 `TLV_DEVICE_STATUS` / `TLV_WIFI_STATUS` / `TLV_MQTT_STATUS` / `TLV_RADAR_SLEEP_STATUS` 共用同一组状态码。 ### 9.1 WiFi 状态 | 常量 | 值 | 说明 | | --- | --- | --- | | `WIFI_DISCONNECTED` | `0x10` | WiFi 断开 | | `WIFI_CONNECTING` | `0x11` | WiFi 连接中 | | `WIFI_CONNECTED` | `0x12` | WiFi 已连接 | | `WIFI_FAILED` | `0x13` | WiFi 连接失败 | ### 9.2 MQTT 状态 | 常量 | 值 | 说明 | | --- | --- | --- | | `DEV_MQTT_DISCONNECTED` | `0x20` | MQTT 断开 | | `DEV_MQTT_CONNECTING` | `0x21` | MQTT 连接中 | | `DEV_MQTT_CONNECTED` | `0x22` | MQTT 已连接 | | `DEV_MQTT_FAILED` | `0x23` | MQTT 连接失败 | ### 9.3 雷达睡眠查询状态 | 常量 | 值 | 说明 | | --- | --- | --- | | `RADAR_SLEEP_QUERY_DISABLED` | `0x30` | 雷达睡眠/综合状态查询已关闭 | | `RADAR_SLEEP_QUERY_ENABLED` | `0x31` | 雷达睡眠/综合状态查询已开启 | ### 9.4 系统指示灯状态 | 常量 | 值 | 说明 | | --- | --- | --- | | `LED_DISABLED` | `0x32` | 系统指示灯已关闭 | | `LED_ENABLED` | `0x33` | 系统指示灯已开启 | --- ## 十、WiFi 安全类型 | 常量 | 值 | 说明 | | --- | --- | --- | | `WIFI_SEC_OPEN` | `0` | 开放网络 | | `WIFI_SEC_WEP` | `1` | WEP 加密 | | `WIFI_SEC_WPA` | `2` | WPA 加密 | | `WIFI_SEC_WPA2` | `3` | WPA2 加密 | | `WIFI_SEC_WPA3` | `4` | WPA3 加密 | | `WIFI_SEC_UNKNOWN` | `255` | 未知类型 | --- ## 十一、响应模型 ### 11.1 同步命令 ``` 客户端写 b1 → 设备从 b2 通知响应 ``` 响应帧: ``` cmd = 原命令码 seq = 请求 seq payload = TLV_RESULT_CODE + 业务数据 TLV ``` 失败时同样使用原命令码响应,`TLV_RESULT_CODE` 为对应错误码;无法归属具体业务命令的入口级错误使用 `CMD_ERROR_RESP (0x7E)`。 ### 11.2 异步长命令 适用:`CMD_WIFI_SCAN`、`CMD_WIFI_CONFIG`。 ``` 阶段一(立即): cmd=原命令码, seq=原seq, TLV_RESULT_CODE = PROCESSING (0x01) 阶段二(完成): cmd=原命令码, seq=原seq, TLV_RESULT_CODE = SUCCESS / ERR_XXX + 业务 TLV ``` 客户端收到 `PROCESSING` 后**不要清除** pending 请求,继续等待最终结果。 --- ## 十二、客户端接入要求 ### 12.1 基本原则 1. 先按特征 UUID 分流,再按 `cmd` 解析。 2. 每个 Notify 特征维护独立重组 buffer。 3. 只在 `b2` 上按 `seq` 匹配请求响应。 4. `a1/a2/b3` 不做请求响应匹配。 5. 所有结果判断以 `TLV_RESULT_CODE` 为准,文案本地映射。 ### 12.2 伪代码 ```javascript const buffers = { a1: new Uint8Array(0), a2: new Uint8Array(0), b2: new Uint8Array(0), b3: new Uint8Array(0) } function onNotify(characteristicId, chunk) { const channel = mapCharacteristicToChannel(characteristicId) buffers[channel] = concatBytes(buffers[channel], chunk) while (true) { const frame = tryExtractFrame(buffers[channel]) if (!frame) break buffers[channel] = frame.remaining if (!verifyCrc(frame.bytes)) continue dispatchByChannel(channel, frame.decoded) } } const pending = new Map() function sendCommand(cmd, payload) { const seq = nextSeq() pending.set(seq, { cmd }) return writeB1(encodeFrame({ cmd, seq, payload })) } function handleCommandResponse(frame) { const req = pending.get(frame.seq) if (!req) return const resultCode = getTlvU8(frame.payload, TLV_RESULT_CODE) if (resultCode !== PROCESSING) pending.delete(frame.seq) routeCommandResult(req.cmd, resultCode, frame.payload) } function tryExtractFrame(buffer) { const sofIndex = findSOF(buffer) if (sofIndex < 0) return null const headerLen = 7 if (buffer.length < sofIndex + headerLen) return null const payloadLen = (buffer[sofIndex + 5] << 8) | buffer[sofIndex + 6] const totalLen = headerLen + payloadLen + 2 if (buffer.length < sofIndex + totalLen) return null return { bytes: buffer.slice(sofIndex, sofIndex + totalLen), remaining: buffer.slice(sofIndex + totalLen), } } ``` ### 12.3 典型交互流程 **连接初始化** ``` 1. 连接 BLE 设备 2. 订阅 b2、b3、a1、a2 的 Notify(未订阅时设备不会发送) 3. 等待 b3 推送设备信息 / 发送 CMD_QUERY_STATUS 获取完整状态 4. 按需发送 CMD_START_CONTINUOUS ``` **WiFi 配网** ``` 1. 发送 CMD_WIFI_SCAN → 收到 PROCESSING → 收到最终结果,展示列表 2. 用户选择 SSID 并输入密码 3. 发送 CMD_WIFI_CONFIG → 收到 PROCESSING → 收到 SUCCESS(含 IP) 4. b3 推送 WIFI_CONNECTED ``` **雷达数据监控** ``` 1. 发送 CMD_START_CONTINUOUS(interval=500ms) 2. a1 持续推送 CMD_CONTINUOUS_PUSH 3. a2 每 200ms 推送 CMD_RADAR_STATUS_PUSH 4. 发送 CMD_STOP_CONTINUOUS 停止 ``` --- ## 十三、数据解析注意事项 1. **心率/呼吸率**:`TLV_HEART_RATE_X10`、`TLV_BREATH_RATE_X10` 为 uint16,实际值 = 值 / 10.0。 2. **波形**:`TLV_HEART_WAVEFORM`、`TLV_BREATH_WAVEFORM` 原始为 int8,线上为 `原始值 + 128`,解析时减 128。(当前 a1 未发送波形。) 3. **坐标**:`TLV_POS_X_MM`/`Y`/`Z` 为 int16,单位 mm;米 = 值 / 1000.0。 4. **设备序列号**:`TLV_DEVICE_SN` 固定 uint64,无 SN 时不发送,客户端不应依赖。 5. **密码回包**:`CMD_GET_SAVED_WIFI` 的 `TLV_WIFI_ITEM` 含 `TLV_PASSWORD`,仅用于本地配网管理。 6. **时间戳**:`TLV_TIMESTAMP` 为设备 `millis()`,非绝对时间。 --- ## 十四、固件实现约束 1. 所有业务 Notify 必须通过 `sendFrameToBLE()` 发送,不允许直接 `setValue(整帧) + notify()`。 2. `a1/a2/b2/b3` 均为 NOTIFY;`b1` 为 WRITE。 3. 命令响应 `seq` 必须与请求一致;主动推送 `a1` 自增、`a2/b3` 为 `0`。 4. `b1` 单次写入超过 256 字节时返回 `ERR_PROTO_FRAME_TOO_LARGE`;命令队列(10 深度)满时返回 `ERR_DEV_QUEUE_FULL`。 5. 仅当客户端订阅了对应特征的 Notify 时才发送,避免无效通知。 6. 所有多字节字段与 CRC 均为大端。 7. 修改协议时需同步更新本文档(新增命令/字段须保持向后兼容)。 --- ## 十五、版本历史 | 版本 | 日期 | 说明 | | --- | --- | --- | | 1.0 | 2026-06 | 初始版本,纯 TLV 协议,移除 legacy JSON 兼容 | | 2.0 | 2026-09 | 合并 `ble_protocol.md` 与 `ble_api_communication.md`;以代码为准补齐 `CMD_LED_CONTROL (0x30)`、`TLV_LED_ENABLED (0x37)`、LED 状态码、`CMD_QUERY_RADAR` 的 `TLV_TIMESTAMP`、`CMD_QUERY_STATUS` 的 `TLV_SSID/TLV_LED_ENABLED`、已保存 WiFi 含密码、删除 WiFi 回包字段,并修正 SEQ 语义 |