docs: 新增蓝牙通信协议文档,训练任务书蓝牙协议改为引用

This commit is contained in:
Admin
2026-09-15 14:28:59 +08:00
parent 99d8f411be
commit 43a27c48d4
2 changed files with 804 additions and 251 deletions
+3 -251
View File
@@ -33,17 +33,6 @@
- [A.4 主控命令帧](#a4-主控命令帧)
- [A.5 常用命令示例](#a5-常用命令示例)
- [附录 B:蓝牙通信协议](#附录-b蓝牙通信协议)
- [B.1 总体设计](#b1-总体设计)
- [B.2 GATT 服务与特征](#b2-gatt-服务与特征)
- [B.3 帧格式](#b3-帧格式)
- [B.4 Notify 分包与重组](#b4-notify-分包与重组)
- [B.5 命令码](#b5-命令码)
- [B.6 TLV 编码](#b6-tlv-编码)
- [B.7 TLV 类型](#b7-tlv-类型)
- [B.8 结果码](#b8-结果码)
- [B.9 设备状态码](#b9-设备状态码)
- [B.10 主要命令交互](#b10-主要命令交互)
- [B.11 异步长命令模型](#b11-异步长命令模型)
---
@@ -323,245 +312,8 @@ CHECKSUM = (sum(frame[0] .. frame[帧长-4])) & 0xFF // 位于 frame[帧长
## 附录 B:蓝牙通信协议
### B.1 总体设计
蓝牙通信协议(GATT 服务与特征、帧格式、TLV 编码、命令码、结果码、状态码、主动推送、客户端接入要求等)统一见同目录文档:
1. 所有业务数据统一封装为 TLV 二进制帧,通过 GATT Notify 分包发送。
2. 客户端必须先按特征 UUID 分流,再按命令字 `CMD` 解析。
3. `b1/b2` 为一问一答通道:客户端写 `b1`,设备从 `b2` 通知响应。
4. `a1/a2/b3` 为主动推送通道,不参与请求响应匹配。
5. 命令结果只以 `TLV_RESULT_CODE` 为准。
6. 状态变化通过 `b3` 推送,与命令结果解耦。
### [`蓝牙通信协议.md`](蓝牙通信协议.md)
### B.2 GATT 服务与特征
**Radar Data Service**`a8c1e5c0-3d5d-4a9d-8d5e-7c8b6a4e2f1a`
| 名称 | UUID | 属性 | 方向 | 职责 |
| --- | --- | --- | --- | --- |
| a1 | `beb5483e-36e1-4688-b7f5-ea07361b26a1` | NOTIFY | 设备→客户端 | 连续雷达数据推送 |
| a2 | `beb5483e-36e1-4688-b7f5-ea07361b26a2` | NOTIFY | 设备→客户端 | 雷达状态推送 |
**Device Config Service**`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 | 设备→客户端 | 设备信息 / 状态推送 |
### B.3 帧格式
```
SOF1 SOF2 VERSION CMD SEQ LEN_H LEN_L PAYLOAD CRC_H CRC_L
AA 55 01 xx xx xx xx ... xx xx
```
| 字段 | 长度 | 说明 |
| --- | --- | --- |
| SOF1 / SOF2 | 2 | 固定 `0xAA 0x55` |
| VERSION | 1 | 协议版本,当前 `0x01` |
| CMD | 1 | 命令码 |
| SEQ | 1 | 命令请求序列号;主动推送由设备侧决定(`b3``a2` 固定为 `0``a1` 连续推送为设备侧自增),客户端对主动推送不按 `seq` 匹配 |
| LEN | 2 | PAYLOAD 长度,大端 |
| PAYLOAD | N | TLV 数据区 |
| CRC | 2 | CRC16-CCITT,大端 |
- **CRC 计算范围**:从 VERSION 到 PAYLOAD 末尾,不含 SOF1/SOF2 与 CRC 本身。
- **CRC 参数**:多项式 `0x1021`,初值 `0xFFFF`,输入/输出不反转,无最终异或(CRC16-CCITT-FALSE)。
- **最小帧长**:9 字节(空载荷)。
### B.4 Notify 分包与重组
Notify 单包可能小于整帧,设备按固定 **20 字节**分片发送。客户端必须为 `a1 / a2 / b2 / b3` **各维护一个重组缓冲区**,按帧头 + LEN + CRC 提取完整帧。
### B.5 命令码
| 命令 | 值 | 通道 | 说明 |
| --- | --- | --- | --- |
| 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_ERROR_RESP | `0x7E` | b2 | 协议层错误响应 |
### B.6 TLV 编码
```
TYPE(1) LEN_H(1) LEN_L(1) VALUE(N)
```
### B.7 TLV 类型
**设备信息**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_RESULT_CODE | `0x02` | uint8 | 结果码 |
| TLV_TIMESTAMP | `0x04` | uint32 | 时间戳(ms |
| 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 地址 |
**雷达数据**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_HEART_RATE_X10 | `0x10` | uint16 | 心率 ×10 |
| TLV_BREATH_RATE_X10 | `0x11` | uint16 | 呼吸率 ×10 |
| TLV_PRESENCE | `0x12` | uint8 | 人体存在 |
| 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 | 体动 |
**WiFi**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_SSID | `0x20` | string | WiFi 名称 |
| TLV_PASSWORD | `0x21` | string | WiFi 密码 |
| TLV_WIFI_COUNT | `0x22` | uint16 | WiFi 数量 |
| TLV_WIFI_ITEM | `0x23` | block | WiFi 列表项 |
| TLV_RSSI | `0x24` | int8 | 信号强度 |
| TLV_SECURITY | `0x25` | uint8 | 加密类型 |
**控制与状态**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_INTERVAL_MS | `0x31` | uint16 | 推送间隔(ms |
| TLV_RADAR_SLEEP_ENABLED | `0x32` | uint8 | 雷达睡眠查询开关 |
| 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 | 指示灯开关 |
**通用**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_IP_ADDRESS | `0x41` | string | IP 地址 |
| TLV_WIFI_CONFIGURED | `0x42` | uint8 | 是否已保存 WiFi |
| TLV_WIFI_CONNECTED | `0x43` | uint8 | WiFi 是否连接 |
| TLV_ECHO_CONTENT | `0x44` | string | 回显内容 |
**波形**
| TLV | 值 | 类型 | 说明 |
| --- | --- | --- | --- |
| TLV_HEART_WAVEFORM | `0x60` | uint8 | 心跳波形(原值 + 128 |
| TLV_BREATH_WAVEFORM | `0x61` | uint8 | 呼吸波形(原值 + 128 |
### B.8 结果码
| 结果码 | 值 | 说明 |
| --- | --- | --- |
| 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` | 帧过大 |
| ERR_WIFI_SCAN_TIMEOUT | `0x20` | 扫描超时 |
| ERR_WIFI_SSID_NOT_FOUND | `0x21` | 未找到 SSID |
| ERR_WIFI_WRONG_PASSWORD | `0x22` | 密码错误 |
| ERR_WIFI_SIGNAL_WEAK | `0x25` | 信号弱 |
| ERR_WIFI_BUSY | `0x26` | WiFi 忙 |
| ERR_DEV_STATE_INVALID | `0x40` | 状态不允许 |
| ERR_DEV_STORAGE_FAIL | `0x41` | 存储失败 |
| ERR_DEV_QUEUE_FULL | `0x42` | 队列已满 |
### B.9 设备状态码
用于 `TLV_DEVICE_STATUS` / `TLV_WIFI_STATUS` / `TLV_MQTT_STATUS` / `TLV_RADAR_SLEEP_STATUS`
| 状态 | 值 |
| --- | --- |
| WIFI_DISCONNECTED | `0x10` |
| WIFI_CONNECTING | `0x11` |
| WIFI_CONNECTED | `0x12` |
| WIFI_FAILED | `0x13` |
| DEV_MQTT_DISCONNECTED | `0x20` |
| DEV_MQTT_CONNECTING | `0x21` |
| DEV_MQTT_CONNECTED | `0x22` |
| DEV_MQTT_FAILED | `0x23` |
| RADAR_SLEEP_QUERY_DISABLED | `0x30` |
| RADAR_SLEEP_QUERY_ENABLED | `0x31` |
| LED_DISABLED | `0x32` |
| LED_ENABLED | `0x33` |
### B.10 主要命令交互
**CMD_QUERY_STATUS0x10**
- 请求:无载荷。
- 响应(b2):`TLV_RESULT_CODE` + 设备信息 TLV`TLV_PROTOCOL_VERSION``TLV_FIRMWARE_VERSION``TLV_DEVICE_TYPE``TLV_MAC_ADDRESS`,存在 SN 时附 `TLV_DEVICE_SN`+ `TLV_WIFI_CONFIGURED``TLV_WIFI_CONNECTED``TLV_IP_ADDRESS`(已连接时)、`TLV_SSID`(已连接时)、`TLV_WIFI_STATUS``TLV_MQTT_STATUS``TLV_RADAR_SLEEP_STATUS``TLV_LED_ENABLED`
**CMD_QUERY_RADAR0x12**
- 请求:无载荷。
- 响应(b2):`TLV_RESULT_CODE` + `TLV_PRESENCE``TLV_HEART_RATE_X10``TLV_BREATH_RATE_X10``TLV_MOTION``TLV_DISTANCE_CM``TLV_POS_X_MM``TLV_POS_Y_MM``TLV_POS_Z_MM``TLV_BODY_MOVEMENT``seq` 与请求一致。
**CMD_START_CONTINUOUS0x14**
- 请求:`TLV_INTERVAL_MS`(有效范围 100~10000 ms)。
- 响应:`TLV_RESULT_CODE`,成功时附 `TLV_INTERVAL_MS`;参数缺失/非法返回对应错误码。
- 启动后设备按间隔通过 `a1` 推送 `CMD_CONTINUOUS_PUSH(0x18)` 帧。
**CMD_STOP_CONTINUOUS0x16**
- 请求无载荷;停止推送,幂等操作,始终返回 `SUCCESS`
**CMD_CONTINUOUS_PUSH0x18a1 推送)**
- 载荷:`TLV_TIMESTAMP``TLV_PRESENCE``TLV_HEART_RATE_X10``TLV_BREATH_RATE_X10``TLV_MOTION``TLV_DISTANCE_CM` 等。
**CMD_WIFI_SCAN0x20**
- 请求无载荷。先回 `PROCESSING`;完成后回 `TLV_RESULT_CODE` + `TLV_WIFI_COUNT` + 多个 `TLV_WIFI_ITEM`(每项含 `TLV_SSID``TLV_RSSI``TLV_SECURITY`)。
**CMD_WIFI_CONFIG0x22**
- 请求:`TLV_SSID``TLV_PASSWORD`
- 先回 `PROCESSING`(可含 `TLV_SSID`);完成后回 `SUCCESS` + `TLV_SSID` + `TLV_IP_ADDRESS`,失败回对应错误码。
**CMD_GET_SAVED_WIFI0x24**
- 响应:`TLV_RESULT_CODE` + `TLV_WIFI_COUNT` + 多个 `TLV_WIFI_ITEM`
**CMD_DELETE_SAVED_WIFI0x26**
- 请求:`TLV_SSID`;响应:`TLV_RESULT_CODE`
**CMD_DEVICE_INFO_PUSH0x19b3 推送)**
- `seq = 0`;载荷为 `TLV_DEVICE_STATUS`,或设备信息 TLV`TLV_RESULT_CODE`、协议版本、固件版本、设备类型、MAC、SN 等)。仅在状态变化时推送。
### B.11 异步长命令模型
适用于 `CMD_WIFI_SCAN``CMD_WIFI_CONFIG`
```
客户端 --b1--> 请求
设备 --b2--> 原命令码, 原 seq, RESULT_CODE = PROCESSING(0x01)
... 后台处理 ...
设备 --b2--> 原命令码, 原 seq, RESULT_CODE = SUCCESS 或 ERR_XXX + 业务 TLV
```
运行状态(WiFi 等)通过 `b3``CMD_DEVICE_INFO_PUSH(0x19)` + `TLV_DEVICE_STATUS` 推送。
本文不再重复协议细节,实现时以上述文档为准。
+801
View File
@@ -0,0 +1,801 @@
# 蓝牙数据传输协议与 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 语义 |