mothercup/docs/ble_protocol.md
evan.liu 520838ce65 V1.00.20: OTA_END ack guaranteed delivery via deferred reset
- app_ota.c: after END ok, wait until the RSP is consumed by the host
  (BLE e0005 read-out / UART TX done) plus 300ms grace before resetting,
  2s timeout fallback; polled from the BLE schedule task (rwip_schedule
  must keep running for the ATT read to be processed)
- main.c: app_ota_reset_poll() in the BLE schedule task loop
- docs: ble_protocol.md 6.6 note, dev log section 43, AGENTS.md sync
2026-09-04 17:35:01 +08:00

327 lines
20 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.

# CAIIC BLE 通信协议 V1.1
适用于 mcm-ddc-ble 固件(N32WB031,Cortex-M0 64MHz,板载 256KB Flash 硅片)。
手机 APP(自研)或通用 BLE 调试工具(如 nRF Connect)通过本文协议与设备通信,
支持三类业务:**CLI 命令透传**、**设备信息查询**(温度/风扇转速等)、**BLE OTA 固件升级**。
## 1. GATT 服务
广播名:`CAIIC-MCM-20260902`
使用 SDK rdtss(raw data transfer server)自定义 128-bit UUID 服务:
| 项 | UUID | 属性 |
|---|---|---|
| Service | `00002760-08C2-11E1-9073-0E8AC72E1001` | Primary Service |
| 下行特征(手机→设备) | `00002760-08C2-11E1-9073-0E8AC72EE001` | Write Without Response |
| 上行特征(设备→手机) | `00002760-08C2-11E1-9073-0E8AC72EE002` | Notify(需写 CCCD=0x0001 使能) |
| CLI 读写特征 | `00002760-08C2-11E1-9073-0E8AC72EE003` | Read + Write(带响应):写原始命令行(≤63B)→ 分包读回输出文本(见 §6.5) |
| 只读参数特征 | `00002760-08C2-11E1-9073-0E8AC72EE004` | Read:全部信息项 TLV(同 INFO_RSP(all),见 §5) |
| OTA 特征 | `00002760-08C2-11E1-9073-0E8AC72EE005` | Read + Write(带响应):OTA 传输帧下行(写)→ OTA_RSP 帧读回(见 §6.6) |
MTU:**设备在连接建立后主动发起 MTU 交换(请求 247)**;手机端也可自行发起。
一次 BLE 写入/通知的最大数据量 = att_mtu − 3(默认 MTU 23 时为 20B)。
**一个协议帧可以跨多次 BLE 写入传输,一次写入也可以携带多个帧**——设备侧按字节流重组。
设备上行每个 notify 固定 ≤20B(对端 MTU 未知时也能工作)。
## 2. 帧格式(小端)
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0 | SOF = 0xCA | 帧起始 |
| 1 | TYPE | 帧类型(见第 3 节) |
| 2 | SEQ | 序号(模 256,发送方自增;应答帧回显请求帧 SEQ) |
| 3–4 | LEN | payload 长度(0–480),小端 |
| 5.. | PAYLOAD | LEN 字节 |
| 末尾 2B | CRC16 | CRC16-CCITT(poly 0x1021,初值 0xFFFF),覆盖 TYPE..PAYLOAD,小端 |
帧开销 = 7 字节(头 5 + CRC 2)。
- 设备单帧 payload 上限 480B;超出或 CRC 错误的帧被静默丢弃并重新找 SOF。
- 设备→手机的 CLI_RSP 分块大小 = 20 − 7 = 13B(固定按默认 MTU 分包)。
## 3. 帧类型
| TYPE | 方向 | 名称 | payload |
|---|---|---|---|
| 0x01 | 手机→设备 | CLI_REQ | 一行命令文本(无结束符,≤63B) |
| 0x02 | 设备→手机 | CLI_RSP | 应答文本分块(可多帧) |
| 0x03 | 设备→手机 | CLI_RSP_END | {status u8}:0=ok,1=未知命令/行超长 |
| 0x10 | 手机→设备 | OTA_BEGIN | {total_size u32, image_crc32 u32, version u32} |
| 0x11 | 手机→设备 | OTA_DATA | {offset u32, data…}(offset 必须严格连续) |
| 0x12 | 手机→设备 | OTA_END | {crc32 u32}(与 OTA_BEGIN 中一致) |
| 0x13 | 手机→设备 | OTA_ABORT | 空 payload;中止会话(设备回 OTA_RSP(ok)) |
| 0x1F | 设备→手机 | OTA_RSP | {cmd_echo u8, status u8, offset u32} |
| 0x20 | 手机→设备 | INFO_QUERY | {item_id u8}×n;空 payload 或单个 0xFF = 查询全部 |
| 0x21 | 设备→手机 | INFO_RSP | TLV 序列 {id u8, len u8, value…}×n |
OTA_RSP status:0=ok,1=bad_frame,2=bad_state/offset 乱序(offset 字段=设备期望的下一字节偏移),
3=size_too_big,4=crc_fail,5=flash_fail,6=bank_mismatch(镜像链接地址与目标 bank 不符,见 §6 选包规则)。
## 4. CLI 透传流程
1. 手机:CLI_REQ,payload 如 `help`、`sysinfo`、`devinfo`、`led 1 toggle`(与 UART CLI 命令集一致)。
2. 设备:若干 CLI_RSP(命令输出的文本流分块)+ 最后一帧 CLI_RSP_END 带状态码。
3. 无命令执行中时可随时发下一条;命令是同步执行的,设备应答完毕前不要再发 CLI_REQ。
UART CLI 特有的交互功能(行编辑、Tab 补全、历史、Ctrl+Z)不适用于 BLE 通道。
自测示例(默认 MTU,写入下行特征):`help` 命令帧 =
`CA 01 00 04 00 68 65 6C 70 16 EC`
## 5. 设备信息查询流程
手机发 INFO_QUERY,设备回一帧 INFO_RSP,payload 为请求的各信息项的 TLV 序列
(每项 {id u8, len u8, value},数值均小端)。设备不认识的 id 直接跳过、不出现在应答中。
| item_id | 名称 | 类型 | 说明 |
|---|---|---|---|
| 0x01 | FW_VERSION | u32 | 固件版本号,如 0x00010001 = V1.00.01 |
| 0x02 | CHIP_TEMP | i16 | 芯片温度,单位 0.1°C(ADC 内置温度传感器 CH7,出厂 trim 校准) |
| 0x03 | FAN_RPM | u16 | 风扇转速 rpm;0xFFFF = 无此硬件/未接入 |
| 0x04 | VDD_MV | u16 | 电源电压,单位 mV(ADC CH6) |
| 0x05 | UPTIME_S | u32 | 系统运行时间,单位 s |
| 0x06 | FREE_HEAP | u32 | FreeRTOS 堆剩余,单位 B |
| 0x07 | CUR_BANK | u8 | 当前运行 bank:1=APP1,2=APP2(按链接基址判定;OTA 选包依据,见 §6) |
示例:查询温度+风扇 = INFO_QUERY payload `02 03`;
应答 INFO_RSP payload 形如 `02 02 0A 01 03 02 FF FF`(温度 26.6°C,风扇无硬件)。
数据来源约定:温度/电压由设备固件直接采 ADC;风扇转速由应用层注册的
数据源提供(`app_fan_get_rpm()`,弱符号默认返回 0xFFFF),产品板接上风扇
转速检测电路后重写该函数即可,协议不变。
## 6. OTA 升级流程(双 bank 直写,无中转区)
Flash 布局(256KB 实装硅片;Boot/bootsetting/APP_DATA 保持 16/8/8KB,余下 224KB 均分双 bank):
| 区域 | 地址 | 大小 |
|---|---|---|
| Bootloader | 0x01000000 | 16KB |
| bootsetting | 0x01004000 | 8KB(用 1 个 4KB 扇区) |
| APP_DATA(预留) | 0x01006000 | 8KB |
| APP1 | 0x01008000 | 112KB |
| APP2 | 0x01024000 | 112KB |
设备当前运行 APP1 则新固件写入 APP2,反之亦然。bank 上限 112KB(0x1C000)。
镜像 = Keil 产物 bin(裸二进制,从 bank 基址开始的镜像)。
flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM 缓存**:
数据首次落入某个未擦除的扇区时先擦除该扇区(惰性擦除),收到的数据立即编程写入,
仅保留 4 字节对齐暂存(Qflash 写要求 4 字节对齐),**不经过任何 flash 中转区**。
1. **OTA_BEGIN**:手机发送 {total_size, image_crc32, version}。
- image_crc32 = IEEE CRC32(poly 0xEDB88320,初值/异或出 0xFFFFFFFF,即 zlib crc32)
对整个镜像 bin 文件的校验值。
- version:u32,如 0x00010001 表示 V1.00.01。
- 设备回 OTA_RSP(0x10, ok, 0) 后开始接收。
2. **OTA_DATA**:从 offset=0 起严格顺序发送,每帧 {offset, data}。
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233);
MTU 23 时单帧总长度不得超过 20B。
- 设备收到即写入 flash;**每一帧都回 OTA_RSP(0x11, ok, 新 offset)**——
逐帧锁步流控(V1.00.19 起;此前为每 4KB 扇区一答)。ack 往返时间天然覆盖
新扇区的惰性擦除窗口(数十 ms 关中断)。
- 主机节奏:逐帧等 ack(锁步)。若应答 status=2(offset 乱序),
从应答中的 offset 处重发即可重新同步。
3. **OTA_END**:{crc32}。设备 flush 尾部(0xFF 补齐到 4 字节对齐后写最后一个扇区),
从 flash 回读整镜像复算 CRC32,与手机端比对:
- 失败 → OTA_RSP(0x12, crc_fail),会话中止,不复位;
- 成功 → 更新 bootsetting(active bank 指向新 bank,记录 size/crc32/version)
→ OTA_RSP(0x12, ok) → 200ms 后自动复位,bootloader 直接跳转到新固件
(跳转不做校验,校验已在 OTA 过程完成)。
4. 任何时刻 BLE 断连,OTA 会话中止,对侧 bank 数据作废(可重新 OTA_BEGIN 重来)。
注意:flash 擦写期间设备关中断数十 ms/扇区(Qflash 算法在 RAM 执行),BLE 链路靠
5s supervision timeout 维持,属正常现象;但请避免在 OTA 期间主动断开。
**OTA 载荷必须链接在目标 bank 地址**(Cortex-M0 代码位置相关)。发布件为单文件
`mothercup_ble_ota.bin`(52B 头含双载荷索引与全部 CRC,格式见 §7)。**客户端选包
规则**:先读信息项 0x07 CUR_BANK(当前运行 bank),OTA 目标 = 对侧 bank,从头里取
对应载荷。固件侧兜底:OTA_END 整镜像 CRC 通过后,还会校验镜像向量表 Reset 地址
落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换
(V1.00.17 起)。
## 6.6 OTA 传输层:通道无关设计(V1.00.19 起)
OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧格式与
字节流重组规则和 §2 完全一致,因此**同一份协议可以跑在不同物理通道上**;
设备侧 OTA 引擎与通道解耦(应答经通道注册的 sink 出口):
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|---|---|---|
| **BLE OTA 特征** `...e0005` | 每次"写带响应"携带一帧(≤ att_mtu−3) | 写完**读同一特征**取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,**读需轮询至非空**(擦扇区时数十 ms) |
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota` 进入二进制帧模式,随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;3s 无帧自动退出回 CLI |
| BLE 旧帧协议通道(e0001/e0002 notify) | 0xCA 帧写入 e0001 | notify 上行(V1.00.19 修复 CRC 后可用,见开发日志 §42) |
锁步规则:每发一帧必须等到对应 OTA_RSP 再发下一帧(ack 里 offset =
设备期望的下一字节;status≠0 时按其 offset 重发可重新同步)。
**CRC16 覆盖范围 = TYPE 到 PAYLOAD 末尾(含 SEQ 与 LEN 两字节,共
4+payLen 字节)**——V1.00.19 之前固件漏算最后一个 payload 字节导致全部
帧被丢弃(§42 根因),新旧固件与主机实现必须统一按 4+payLen 计算。
PC 参考实现:`tools\ble_ota_update.py`(`ble_ota.bat`;BLE 默认,
`--uart COMx` 走串口)。
**END 应答可靠送达(V1.00.20 起)**:OTA_END 校验通过后,设备先发出 ok
应答,**等主机取走该应答**(BLE:e0005 读取消耗;UART:TX 发完)并留
300ms 宽限后才复位进入新 bank,2s 超时兜底。因此主机在正常链路上必定能
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
与版本"兜底(PC 工具两种处理都保留)。
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo)
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
**只暴露结构化字段访问,无裸读写命令**。UART CLI 与 BLE CLI 读写特征
`...c72e0003`(写原始命令行 → 分包读回文本应答)命令集完全相同:
| 命令 | 说明 |
|---|---|
| `appget` | 打印 APP_DATA 参数记录与 valid/default 状态(led = LED1 闪烁半周期 ms;pwm = PWM 占空比 %);`appget json` 输出 JSON 对象 `{"led":500,"pwm":0,"valid":1}`(V1.00.14 起) |
| `appset <led\|pwm> <val>` | 修改参数并立即落盘(led 50~10000 即时生效于 LED1 闪烁;pwm 0~100 仅存储备用);`appset json {"led":500,"pwm":50}` JSON 入参(V1.00.14 起,未知键忽略、任一越界整体不生效) |
| `bsdump` | 打印 bootsetting 记录全字段 + magic/CRC 校验结果 |
| `bsset <field> <val>` | 写 bootsetting 单字段(`active 1\|2`、`b1addr/b2addr/b1size/b2size/b1crc/b2crc/b1ver/b2ver`),自动重算 CRC、擦除+编程+校验。**写错 active/addr 会导致 Boot 跳错,恢复靠 SWD 重烧整片包** |
| `appsw [1\|2]` | 切换运行 bank 并复位(V1.00.15 起):无参切对侧 bank;切前校验 bootsetting 记录、目标 bank 记录一致性,并复算目标镜像 CRC32 与记录比对,不匹配拒绝(坏 bank 需先跑 OTA 写入有效镜像) |
| `reset` | 复位单板:应答后延时 200ms 执行 `NVIC_SystemReset()`,Boot 按 active bank 跳转 |
| `factory` | 恢复出厂设置:APP_DATA 参数区恢复缺省值并落盘后复位(**不动 bootsetting**) |
| `uartrst` | 复位串口:USART1 按 115200 8N1 重新初始化并清空 RX 队列 |
| `uartinfo` | 查询串口参数:引脚(PB6/PB7 AF4)、波特率/数据位/校验/停止位、TX DMA/RX 中断形态 |
参数记录(APP_DATA 区头,52B):magic `0xCA12DA7A` + layout_ver + 参数字段 +
reserved[8] + CRC32;记录无效时固件以缺省值运行,`appset` 时落盘。
注意:flash 擦写关中断数十 ms/扇区,BLE 应答可能延迟;MTU 协商失败(20B/包)时
长应答会被截断到 19B,建议先协商 MTU 247。
读写特征应答读取(V1.00.13 起分包上报):命令输出捕获上限 1024B;每次读
`...e0003` 返回下一片(≤ att_mtu−2,单片恒为"短读",不会触发 Read Blob——
blob 偏移不下发应用层,协议栈会切错窗口),**读到 0 长度包为应答结束**;
写入新命令即重置分包游标。客户端必须循环读至空包。**需搭配 V1.00.13+ 固件**:
旧固件为单读 ≤512B 静态缓冲,重复读返回同一内容,循环读会重复拼接直至
客户端兜底次数上限。
## 7. 首次烧录与恢复
- 出厂/首次:SWD 烧录整包(bootloader + bootsetting 缺省配置 + APP1),
由 `tools\merge_image.py` / `make_package.bat` 生成。
- OTA 发布文件(**单文件发布**,V1.00.18 起):`tools\make_package.bat` 产出
`mothercup_ble_ota.bin` = **52B 头 + bank1 载荷 + bank2 载荷**,头已包含原清单
json 的全部信息(version/总大小/总 CRC),无需伴随 json。格式(全小端):
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0 | magic u32 | `0xCA10BA11` |
| 4 | hdr_len u32 | = 52(头部字节数,前向兼容扩展用) |
| 8 | version u32 | 固件版本号(`0x00MMmmpp`,写进 OTA_BEGIN 与新 bank 记录) |
| 12 | total_size u32 | 整文件字节数(自检) |
| 16 | payload_crc u32 | CRC32(hdr_len .. 文件尾),即两份载荷整体 |
| 20 | count u32 | = 2 |
| 24 | b1_off/b1_size/b1_crc u32×3 | bank1(APP1 链接,0x01008000)载荷索引 |
| 36 | b2_off/b2_size/b2_crc u32×3 | bank2(APP2 链接,0x01024000)载荷索引 |
| 48 | hdr_crc u32 | CRC32(头 0..47) |
CRC32 均为 IEEE(zlib 多项式,与固件 `caiic_crc32` 一致)。
**升级流程(APP/UART/PC 通用)**:① 校验 magic + hdr_len + hdr_crc +
total_size + payload_crc;② 读设备信息项 0x07 CUR_BANK 得当前运行 bank N;
③ 取对侧 bank(3−N)的载荷(offset/size/crc 取自头部);④ OTA_BEGIN
{size, crc32, version} → OTA_DATA 流 → OTA_END{crc32};⑤ 设备自动复位到
新 bank,重连后复核 CUR_BANK 与 FW_VERSION。PC 参考实现:
`tools\ble_ota_update.py`(`ble_ota.bat`)。
调试用的单 bank 载荷(`mothercup_ble_prod_ota.bin` / `_ota_app2.bin` + json)
仍在 tools/out/ 生成,但不作为发布件。
- **SWD 单 bank 升级**(不动运行中的 bank,V1.00.18 实测通过):
`NSpyocd load ... "mcm-ddc-ble.bin@0x01008000"`(bin 后用 `@基址` 指定写入
位置,只擦写涉及扇区)→ CLI `bsset` 修正该 bank 的 size/crc/ver →
`appsw` 切入。详见开发日志 §40。
- OTA 失败变砖恢复:SWD 重烧即可(bootloader 不做串口 DFU)。
- Keil 调试下载必须用**扇区擦除**,整片擦除会删掉 bootloader。
## 8. 手机端开发指南
### 8.1 连接与初始化步骤
1. 扫描:按广播名 `CAIIC-MCM-20260902` 过滤(或服务 UUID `00002760-08C2-11E1-9073-0E8AC72E1001`)。
2. 连接,发现服务,确认两个特征存在:下行 `...E0001`(Write Without Response)、上行 `...E0002`(Notify)。
3. **使能 notify**:往上行特征的 CCCD 写 `0x0001`(Android:`setCharacteristicNotification` + 写 descriptor;iOS:`setNotifyValue(true)`)。
4. MTU:设备连接后会主动发起 MTU=247 交换;手机端也可以自己 `requestMtu(247)`。以协商结果为准计算单次写入上限 = mtu − 3。
5. 完成以上步骤后即可开始三类业务。建议连接后发一条 INFO_QUERY(查全部)确认链路。
### 8.2 参考实现(Python,可直接对照移植到 Kotlin/Swift/JS)
```python
def crc16_ccitt(data: bytes) -> int:
"""poly 0x1021, init 0xFFFF, 覆盖 TYPE..PAYLOAD"""
crc = 0xFFFF
for b in data:
crc ^= b << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def build_frame(ftype: int, seq: int, payload: bytes) -> bytes:
body = bytes([ftype, seq, len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) + payload
c = crc16_ccitt(body)
return b'\xCA' + body + bytes([c & 0xFF, c >> 8])
def parse_stream(buf: bytearray):
"""把 notify 收到的字节 append 进 buf,循环调用本函数取完整帧。
返回 (ftype, seq, payload) 或 None(数据不足)。坏帧自动丢弃并重新找 0xCA。"""
while True:
try:
i = buf.index(0xCA)
except ValueError:
buf.clear(); return None
del buf[:i]
if len(buf) < 5: return None
plen = buf[3] | (buf[4] << 8)
if plen > 480:
del buf[0]; continue # 长度不可信,丢 SOF 重找
if len(buf) < 7 + plen: return None
body = bytes(buf[1:5 + plen])
crc = buf[5 + plen] | (buf[6 + plen] << 8)
del buf[:7 + plen]
if crc16_ccitt(body) == crc:
return body[0], body[1], body[3:]
# CRC 错:继续循环重找下一个 SOF
```
镜像 CRC32(OTA_BEGIN / OTA_END 用):标准 zlib/IEEE CRC32,
对 `mothercup_ble_ota.bin` 中取出的目标 bank 载荷计算
(`zlib.crc32(payload) & 0xFFFFFFFF`),也可直接读头部的 bN_crc 字段。
### 8.3 字节级完整示例(均含 SOF 与 CRC16,SEQ 自取)
| 用途 | 字节流(hex) |
|---|---|
| CLI_REQ "help" | `CA 01 00 04 00 68 65 6C 70 16 EC` |
| CLI_RSP_END(status=0) 应答样例 | `CA 03 00 01 00 00 EE C8` |
| INFO_QUERY 查全部 | `CA 20 00 00 00 8E B3` |
| INFO_QUERY 温度+风扇 | `CA 20 00 02 00 02 03 71 80` |
| INFO_RSP 样例(26.6°C + 无风扇) | `CA 21 00 08 00 02 02 0A 01 03 02 FF FF C2 88` |
| OTA_BEGIN(39692B 镜像) | `CA 10 00 0C 00 0C 9B 00 00 D8 90 57 F1 01 00 01 00 BB 93` |
| OTA_RSP(BEGIN ok) 应答样例 | `CA 1F 00 06 00 10 00 00 00 00 00 51 B9` |
| OTA_DATA(offset=0,4B 数据) | `CA 11 01 08 00 00 00 00 00 00 B1 00 20 73 CF` |
| OTA_END(crc32=0xF15790D8) | `CA 12 02 04 00 D8 90 57 F1 81 D8` |
### 8.4 时序与异常处理
CLI 透传:
- 命令串行执行:发出 CLI_REQ 后,**收到 CLI_RSP_END 之前不要再发下一条**。
- CLI_RSP_END.status:0=ok,1=未知命令或行超长(>63B)。
信息查询:
- INFO_RSP 的 TLV 按序解析:{id, len, value},遇到不认识的 id 按 len 跳过。
- CHIP_TEMP 为 i16(0.1°C),注意符号位;0x7FFF = 读取失败。
- FAN_RPM 0xFFFF = 无测速硬件;VDD_MV 0 = 读取失败。
OTA:
- 状态机:OTA_BEGIN(ok) → OTA_DATA×n → OTA_END → 设备自动复位。断连即会话作废,重来即可。
- 流控:设备每写满 4KB 回一帧 OTA_RSP(ok, offset)。**推荐节奏:滑动窗口连续发,
每收到一帧 ok 应答就核对 offset 与已发进度一致**;不需要逐帧等 ack。
- 乱序/丢帧:收到 status=2(bad_state)时,从应答 offset 处重发后续数据即可恢复,无需重来。
- status=4(crc_fail)/5(flash_fail):会话已中止,从 OTA_BEGIN 重来。
- OTA_END 成功后设备 200ms 内复位;手机端会收到断连,重连后可用 INFO_QUERY 查
FW_VERSION 确认新固件已运行。
- OTA 期间 flash 擦写会关中断数十 ms/扇区,notify 应答可能延迟,属正常;不要主动断开。
通用:
- 设备上行 notify 每包 ≤20B,**一帧可能跨多个 notify 到达,手机端必须按字节流重组**
(见 8.2 parse_stream),不能假设一次 notify = 一帧。
- 任何时刻收到 CRC 错误或无法解析的字节:丢弃并重新找 0xCA,链路无需重置。