本章讲 App 与中继盒从发现到断开的完整链路,对应协议层 v4.2 §BLE 通信通道 的 AE03 / AE05。CM4 与 legacy 的差异(family-specific 心跳 / MTU / 命令编码)由 lib/core/transport/ 抽象掉,本章除特别说明外按 family 分别给出。
lib/core/services/ble_service.dart(2122 行)—— BLE 底层 I/O:连接 / 特征发现 / R/W / 订阅 + _connectGate 串行化连接 + 锁刷新写按 deviceId.hashCode 相位错峰(ble_service.dart:1354)lib/core/services/ble_device_service.dart(4177 行)—— 多设备编排、心跳、重连、入仓关机处理、staleness 检查、readRssi() 周期采样lib/core/transport/ —— family 抽象:booster_transport.dart 接口 + booster_family.dart 心跳元数据 + legacy_booster_transport.dart / cm4_booster_transport.dart 两个实现lib/core/protocol/ —— 包编解码与 LUT
cm4_data_parser.dart(343 行)—— CM4 ASCII 推送解析(D=/PENON/PENOFF/SETT==/WIFI_STS=)legacy_data_parser.dart(429 行)—— legacy 二进制解析(12B/15B/2B/0x55AA)temperature_lookup.dart(199 行)—— tempIntArray / tempExtArray 共享 LUTcm4_protocol.dart(208 行)/ legacy_protocol.dart(157 行)—— 各自的命令常量booster_message.dart(389 行)—— 统一的消息类型层(CM4Message 家族)ScanPage 或 App 启动时 seedReconnectQueue 恢复已知设备CM / MW 识别BluetoothAdapterState.on,超时 5 秒
CBManagerStateUnknown,此时 connect 必失败_scanEntryTtl = 20s 内未再次广播的条目从列表清除;_scanReemitInterval = 2s 控制 re-emit 节奏deviceId 的 BluetoothDevice_connectGate 串行化(ble_service.dart:351)—— 7 台已知设备启动 seed reconnect 时不会并发抢 CCCD 写 / MTU 协商 / indicate 订阅device.connect(),超时 15 秒(directConnect 路径为 5 秒)BoosterFamilyExt.requestsLargeMtu 且 Platform.isAndroid,iOS 由系统自动协商);legacy 走默认 MTUAE30 → 解析特征 AE03(write)+ AE05(CM4 用 notify,legacy 用 indicate;BleService 按 advertised 属性自动选择)AE05BleService._sendHeartbeat 直接通过 BoosterFamilyExt.heartbeatBytes 获取字节并发送:
CNT_00x55B1(即锁刷新命令;同时充当初始心跳)_devices map,开始发射遥测BoosterFamilyExt.heartbeatIntervalSeconds 是单一来源:
| Family | 心跳字节 | 间隔 | 语义 |
|---|---|---|---|
| Legacy(CM1/2/3 / MW3/4/5) | 0x55B1 |
45 s | 独占锁刷新(60 s TTL,超时其他手机可抢) |
| CM4 | ASCII CNT_0 |
45 s | 独占锁刷新(60 s TTL)——per Liang 2026-06-06 / TAPD #1003176:CM4 与 legacy 同机制、wire 不同;c613148 之前文档为 20 s / 无独占锁,那是错的(CM4 协议 §心跳) |
锁刷新写按 deviceId.hashCode 相位错峰(ble_service.dart:1354)——多台中继盒同时连上时,第一次周期写不会撞在同一墙钟时刻,避开 Xiaomi/Huawei 浅 GATT 写队列饱和而丢写(per Liang 2026-04-30 audit reply)。Liang 原话:「不同的中继盒,按顺序发 0x55B1 就好,中继盒已经保留了微小的容错机制」。
CM4 与 legacy 的 RX payload 完全不同——CM4 是 ASCII,legacy 是 raw bytes。两个 parser 分文件处理,输出统一到 CM4Message 家族(booster_message.dart)供上层消费。
legacy_data_parser.dart)| 包类型 | 长度 | 频率 | 作用 |
|---|---|---|---|
| 设置响应 / 状态 | 12B | 每 3 秒 | 心跳 + 目标温度 + 报警状态 |
| 探针温度 | 15B | 每 3–6 秒(探针活跃时) | 内温 + 环境温 + 电量 + MAC |
入仓通知 0x55AA |
2/3/8B | 事件驱动 | 探针入仓 → 触发中继盒关机链 |
| 心跳(仅电量 + 版本) | 2B | 每 3 秒(无探针活跃时) | 保活 |
字节级布局见 v4.2 §一、App 接收。
cm4_data_parser.dart)| 形式 | 触发 | 作用 |
|---|---|---|
D=<30 hex>\r\n |
每根活跃探针 ~3 s 一帧(不合并,per Beta 2026-05-11 8b47f3e) | 探针遥测;解码后 = 同一 15-byte 布局,走 temperature_lookup.dart 共享 LUT |
PENON <id> / PENOFF <id> |
探针物理上线 / 下线 | id 既可能是数字(1..4)也可能是颜色名(BLACK/WHITE/BLUE/RED)——App 两种都吞 |
SETT==<32 hex>\r\n |
用户在中继盒物理按键改设置 → unsolicited push(BLE + MQTT);也是 SET_RD 查询的响应 |
16-byte 解码;当前 App 已实现按 probe 目标温度的同步采纳(CM4_ALARM_SYNC_ADOPT,回写缓存并触发 onProbeTargetReconciled)(详见 CM4 协议 §Q3) |
WIFI_STS=N |
固件可能 push | parser 会解析为 CM4WifiStatus,供上层更新 WiFi 状态/信号 |
VER=... / USUS=... / BL_LVL=... / RI_LVL=... |
查询响应 | 按 builder 命令对应回包解析 |
CM4 数据通道双轨:BLE 与 MQTT 都可达时固件同时往两个通道推(per Beta 2026-05-11,55d699a),App 通过 DeviceConnectionManager._mode 只消费其中一边。详见 MQTT 与云端 §双通道遥测。
temperature_lookup.dart)reverseLookup(intRaw, tempIntArray) 反查得到 °CreverseLookup(extRaw, tempExtArray) 反查得到 °CisInternalTempBelowRange / isInternalTempAboveRange),UI 显示 "LO" / "HI"详见 v4.2 §四、温度查表。
15B 包携带完整 6 字节 MAC(on-wire 反序),12B 包携带压缩 4 字节(去掉固定的 0x50 / 0x32)。legacy_protocol.dart 有 reconstructMacFromCompressed() 做还原。交叉比对:App 每次收到 12B 时对比重构 MAC 与 15B 反转 MAC,不一致打 SETTINGS_MAC_MISMATCH 警告——这是 v4.1 发现 MAC 字节序 bug 的关键手段,保留,不要删。
BoosterTransport 抽象了两个 family 的命令差异——上层调 transport.setProbeTarget(...) / setUnit(...) / muteAlarm(...) / queryStatus(),不感知具体 family。
legacy_protocol.dart)| 命令 | 字节 | 作用 |
|---|---|---|
querySettings |
0x55 AE 00 00 |
查询设置(立即回 12B) |
lockRefresh |
0x55 B1 |
锁刷新 + 初始心跳 |
muteAlarm |
0x55 AD 00 00 |
静音报警 |
setFahrenheit / setCelsius |
0x55 AB 00 [00/01] |
切换中继盒物理屏单位 |
setTarget |
[0x55, cmd, unit, tempByte, a0..a5] |
10 字节 SET_TARGET(详见 BLE 协议 §独占锁机制 的 2026-04-30 final clarification) |
颜色 → 命令字节映射(commandByteFromAddress):0x0A/0x0B → 0xAF(黑)、0x0D → 0xB0(白)、0x0E → 0xB2(蓝)。legacy 物理上从无 yellow probe——probeCommandByte(probe4) fallback 也返 0xB2 但实际 unreachable,详见 v4.2 §二 · 探针颜色 与 legacy_protocol.dart:72-87。
⚠️
0x55B1是锁刷新,不是蓝针命令。Beta 早期曾误说蓝针字节是0x55B1——Liang 已确认蓝针是0x55B2。
cm4_protocol.dart)ASCII 字符串:SET_RD / SET_F / SET_C / RING_OFF / CNT_0 / SET_<color>=ABCDEF / BL_LVL=N / RI_LVL=N / SSID=… / PSWD=… / USUS=N / OTA / BT_STS=N / SET_H=XY / SET_L=XY / CLK=XY。
探针配置 SET_<color>=ABCDEF:A=报警开关(1=开启并解析 B-F,0=关闭并丢弃后续字段)、B=单位、CD=目标温度(两位大写 hex)、E=肉种(1–9)、F=熟度(1–5)。<color> 当前是色名前缀 SET_BL/WH/BU/YE(per cloud doc 2026-05-19,6e5dfb7 反向恢复——本周内已 flip-flop 两次,新 agent 在改 prefix 之前请重读 CM4 协议 §探针配置)。同设备相邻 AE03 写之间至少 ≥200 ms——CM4 固件无接收缓冲,过短 burst 会丢;当前 App 无 burst loop 但 BleService.sendCommandTo dartdoc 已警示。
完整列表见 CM4 协议 §App → 中继盒命令。
disconnect():置标志位、清 grace timer、停心跳 / staleness / RSSI / no-probe-telemetry 各种 timer0x07 UNKNOWN 错误码)分两类处理(核心在 ble_device_service.dart 的 _onUnexpectedDisconnect):
Per Liang 2026-04-28:每个中继盒型号的
0x55AA → 0x07延迟是固定值(典型 ~70 ms,最高 1 秒以内属正常)。_dockShutoffWindow = 1s收紧后窗口尾部的无关0x07不会被误分类为入仓关机;旧 5 秒窗口太宽,会绕过 90 秒宽限期、直接给用户显示 "Booster off",而链路其实还能恢复。1 秒覆盖四个 CM 型号所有合理的固件延迟。
A. 入仓触发关机(_lastDockEventAt 在 1 秒内)
DOCK_TRIGGERED_SHUTOFFDeviceStatus.boosterShuttingDown_dockShutoffUiDelay)结束后发 DeviceStatus.boosterOff_dockShutoffReconnectDelay)初始延迟排队(中继盒大约 4 s 关机 + 4~8 s 启动)B. 非入仓断开(没有近期 0x55AA)
gracePeriod,原理:legacy 锁 TTL 60 s + 30 s 重连 margin)DeviceStatus.lostConnection + UI banner 翻 "Reconnecting…" + AlarmType.boosterDisconnected 单声+通知0x07 断开可以在 0x55AA 字节到达后 ~65 ms 就发生_onAe05Rx 钩子在字节层(parser 之前)就给 _lastDockEventAt[deviceId] 打戳(ble_device_service.dart:308-329)_DeviceState.stalenessTimer 每 3 s 跑一次:
| 触发 | 阈值 | 动作 |
|---|---|---|
| 任一类包到达 | — | lastBoosterData 重置;条件不命中 |
| 15B 沉默 | 20 s(_probeDropoutThreshold,旧 10 s 阈值因 12-15 s 正常 jitter 误报已上调) |
探针 Probe.isConnected = false、UI 卡片置灰、emit PROBE_DROPOUT |
| 12B & 15B & 2B 全沉默 | 60 s(v4 时 15 s,Liang 2026-04-30 audit 提高) | DeviceStatus.lostConnection、AlarmType.boosterDisconnected |
中继盒 RSSI 由 BluetoothDevice.readRssi() 每 10 s 采样一次(ble_device_service.dart:3516),写入 Booster.rssi。
所有 BLE 事件都通过 DevLogService 记录,带 packet seq #<seq> 关联。开发者模式下可导出全部日志给 Liang 诊断。详见开发与调试。