Files
ESP32-learning-materials/蓝牙通信协议.md
T

802 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 蓝牙数据传输协议与 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_PING0x01
**请求**b1):
| TLV | 类型 | 必选 | 说明 |
| --- | --- | --- | --- |
| `TLV_ECHO_CONTENT` | string | 否 | 回显内容,可携带任意字符串 |
**响应**b2):
| TLV | 类型 | 说明 |
| --- | --- | --- |
| `TLV_RESULT_CODE` | uint8 | `SUCCESS` |
| `TLV_ECHO_CONTENT` | string | 仅当请求携带内容时原样回显 |
---
### 5.2 CMD_QUERY_STATUS0x10
**请求**:无 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 | 是 | 是否保存过 WiFi0/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_RADAR0x12
**请求**:无 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_CONTINUOUS0x14
**请求**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_CONTINUOUS0x16
**请求**:无 PAYLOAD。
**响应**b2):`TLV_RESULT_CODE = SUCCESS`
**说明**:幂等操作,无论当前是否在推送都返回成功;BLE 断开时设备自动停止推送,重连后需重新下发 `CMD_START_CONTINUOUS`
---
### 5.6 CMD_RADAR_SLEEP_QUERY0x17
**请求**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_CONTROL0x30
**请求**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`
**持久化与推送**:开关状态保存到 FlashPreferences 命名空间 `radar_data`,键 `ledEnabled`),断电保持;状态变化时经 `b3` 推送 `LED_ENABLED (0x33)` / `LED_DISABLED (0x32)`
---
### 5.8 CMD_WIFI_SCAN0x20
**请求**:无 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_CONFIG0x22
**请求**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_WIFI0x24
**请求**:无 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_WIFI0x26
**请求**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_RESP0x7E
用于协议层/入口级、无法归属到具体业务命令的错误(未知命令、帧过大、队列满、队列未初始化等),设备通过 `b2` 返回:
| TLV | 类型 | 说明 |
| --- | --- | --- |
| `TLV_RESULT_CODE` | uint8 | 错误码 |
---
## 六、主动推送(a1 / a2 / b3
### 6.1 a1 — CMD_CONTINUOUS_PUSH0x18)连续雷达数据
- **触发**`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_PUSH0x1A)雷达状态
- **触发**`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_PUSH0x19)设备信息与状态
- **触发**: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 | 心率×10720 = 72.0 bpm |
| `TLV_BREATH_RATE_X10` | `0x11` | uint16 | 呼吸率×10180 = 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 WiFi0x20-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 | 是否保存过 WiFi0/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_CONTINUOUSinterval=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 语义 |