CM4 中继盒走 ASCII 命令 + ASCII / hex 遥测,与 legacy CM1/CM2/CM3 的二进制 0x55XX 协议根本不同。本页是 App 侧对 CM4 协议的全部已知内容;与 legacy 共有的字节级布局(15B 探针温度数据等)见 v4.2 协议规范。
范围说明:本页只讲 CM4。Legacy 在 v4.2 协议规范。两者的并排速查见 BLE 协议(概览)。
| 维度 | CM4 | Legacy(CM1/2/3 / MW3/4/5) |
|---|---|---|
| 命令编码 | ASCII(SET_F / SET_BL=...) |
二进制(0x55 AB 00 00) |
| 遥测编码 | ASCII:D=<30 hex>\r\n |
二进制:15 字节 raw |
| 遥测 payload | 解码后 = 同一 15-byte 布局 | 直接 15-byte raw |
| 状态通知 | PENON/PENOFF <id> |
0x55 AA + MAC(入仓) |
| 心跳 / 独占锁刷新 | CNT_0,每 45 秒(per Liang 2026-06-06:同机制、60 s TTL) |
0x55 B1(独占锁刷新),每 45 秒 |
| 远程通道 | BLE + MQTT/TLS | 仅 BLE |
| 探针数 | 最多 4 | 1 / 2 / 3(按型号) |
CM4↔Legacy 的家族判别由 BoosterFamily 收敛——上层代码只调 transport.setProbeTarget(...) 等家族无关签名。
GATT 服务 / 特征与 legacy 共用:
| UUID | 方向 | 用途 |
|---|---|---|
AE30 |
service | 容器(扫描时按此匹配) |
AE03 |
App → 中继盒 | write / writeWithoutResponse(命令) |
AE05 |
中继盒 → App | notify(数据 + 状态) |
CM4 用 write-with-response + notify(legacy 是 indicate)。BleService 自动按 advertised 属性选择路径。
WiFi 配通后中继盒会注册到 EMQX 区域 broker。详情与区域路由见 MQTT 与云端。
| Topic | 方向 | payload |
|---|---|---|
/CM4/<deviceId>/data |
中继盒 → App | 同 BLE 的遥测 frame(ASCII D=<hex> 或 PENON/PENOFF) |
/CM4/<deviceId>/sett |
中继盒 → App(028087a / V14 / Per Beta 2026-06-16 起单向 box→app——先前 860de52 / TAPD #1003167 Beta 2026-06-12 起 App 也在此 publish 命令、双向) | 中继盒主动 push 通道:物理按键改设置时的 SETT==<32hex>、WIFI_STS=N、MQTT_STS=N、<deviceId> online broker-login 公告、SET_RD 应答(详见 §SETT== 设置帧 / §unsolicited push 全集 与 MQTT 与云端 §Topic 结构) |
/CM4/<deviceId>/apps |
App → 中继盒(028087a / V14 / Per Beta 2026-06-16 新增命令 topic) | App publish 的 downlink 命令 ASCII 字符串(SET_xx=… / SET_RD / 其它 ASCII)。mqtt_service.dart:_publish 改写到这条;App 不订阅 /apps、自此再不会听见自己的命令 echo(先前两端共用 /sett、MQTT 3.1 无 no-local、App 自己的 SET_xx= 会被 broker 回弹、依赖 fall-through + 2 s _lastUserTargetWriteAt 双重 suppression;Liang 改前不考虑老设备直接切换) |
BLE 优先(App 默认消费 BLE 通道);MQTT 在 BLE 不可达时作为活跃通道——但 per Beta 2026-05-11(55d699a),中继盒在两边都可达时同时往两个通道推遥测,"fallback" 是 App-side 的 DeviceConnectionManager 过滤而非 wire-side 的切换。详见 MQTT 与云端 §双通道遥测。
App 每 45 秒发一次 CNT_0 刷新中继盒侧独占锁。BoosterFamilyExt.heartbeatIntervalSeconds 是常量来源。
Per Liang 2026-06-06(TAPD #1003176):CM4 的
CNT_0与 legacy 的0x55B1是同一独占锁机制——锁 TTL 60 秒、45 秒刷新;CM4 自有 ASCII 协议、不能接受0x55B1的二进制字节,只是 wire 不同、语义一致。c613148 之前 CM4 cadence 是 20 秒、无独占锁假设由此推翻。WiFi 模式下 BLE 链路同样要持续刷锁——否则中继盒释放锁、约 20–30 s 后主动 shed GATT 链路(即 5586d80 / TAPD #1003176 修的 "phantom Reconnecting banner" 根因)。同 spec:BLE 与 WiFi 都可达时,App 应优先 BLE(减少流量、降低延时);DeviceConnectionManager现 BLE-primary,cloud 仅 failover——除非用户在 Connection-Mode 上手动选 WiFi(_userForcedCloud标志保留此覆盖)。
所有命令都是 ASCII 字符串。多设备并发时,第一次周期写入按 (deviceId.hashCode % cadence×1000) ms 错峰。
⚠️ AE03 写入节奏与 D= 遥测节奏(per Beta 2026-05-11,8b47f3e):同一设备相邻 AE03 写之间至少 ≥200 ms——CM4 固件无接收缓冲、命令实时处理,过短的 burst 会被丢;跨设备 GATT 链路独立,跨设备并发写不受限(上面那条
(deviceId.hashCode % cadence×1000) ms相位错开是为了避免 GATT 队列饱和,与此 per-device 规则无关)。D=<hex>遥测固件按探针单独转发、不合并,每根约 3 秒一帧——4 根全活时 ~1.3 packets/s,与device_providers.dart15 s staleness 阈值之间留 5 帧裕度。App 当前无写节流代码(每个 transport 方法 / 烹饪页 tap 都是 one-shot,没有 burst loop),但BleService.sendCommandTo/sendRawBytesTo的 dartdoc 已警示:未来若出现连续命令流(on-connect settings sync 等)要插Future.delayed(Duration(milliseconds: 200))或加 per-device gate。
| 命令 | 作用 |
|---|---|
SET_RD |
读取当前所有设置;中继盒按设置回 SETT==<32hex> 帧(per 6337d24,详见 §SETT== 设置帧) |
VER |
查询固件版本 |
USUS=? |
查询当前 server region |
WIFI_STS=? |
查询 WiFi 状态(当前代码未定义对应命令常量 / query builder) |
BL_LVL=? |
查询当前背光级别(返回 BL_LVL=N,per Beta 2026-05-11 / 55d699a) |
RI_LVL=? |
查询当前响铃音量(返回 RI_LVL=N,per Beta 2026-05-11 / 55d699a) |
RI_CNT=? |
查询当前响铃时长档位——旧固件不应答(7c141d7 / TAPD #1003203 log 实证:曾每条 RI_CNT 都是 App→booster、零 reply);per Liang 2026-06-10 起固件回报 RI_CNT=N,App 经 on-connect RI_CNT=? + onRingCountReported bridge 回读到 buzzerRingModeProvider(0–3 档生效)。⚠️ RI_CNT=4(Mute)当前被 _parseRingCount(cap 0..3)丢弃、回读不到,已知待修(详见 §显示亮度 / 音量 / 响铃时长) |
| 命令 | 作用 |
|---|---|
SET_F |
中继盒物理屏切换到 °F |
SET_C |
中继盒物理屏切换到 °C |
注:与 legacy 不同,SET_F / SET_C 是全局的,不带探针参数。
| 命令 | 作用 |
|---|---|
RING_OFF |
静音当前响铃——⚠️ buzzer-only(Beta Q12:静音当前响铃,不影响后续告警)。不清除每探针 闹钟 icon + backlight flash;Mute(RI_CNT=4)/ timed ring(RI_CNT=2/3)已过期等 booster 不在响时本身就 no-op,Confirm 路径必须再发 SET_{COLOR}=000000 才能清掉视觉报警(3c40aa9 / TAPD #1003214,详见 §探针配置 字段 A) |
SET_<color>=000000 |
探针级报警 DISARM——字段 A=0、BCDEF 全 0(FW 关 A=0 后丢弃 tail,Beta 2026-05-25 cloud doc 表给出 SET_BL=000000 范本)。3c40aa9 / TAPD #1003214:App 在 CM4 confirm 路径上把它当作物理按键的 wire 等价用——muteAlarm 先发 RING_OFF、Future.delayed(250 ms)(per Beta 2026-05-11 的同设备 AE03 ≥200 ms 间距规则)后调 disarmProbeAlarm(probe) 关闭被确认的探针。其它探针不受影响(CM4 SET_<color> 无 legacy 0x55AD 的 all-probe collateral clear,无需 re-arm pass)。HW 验证 pending Liang:in-flight SET_<color>=000000 是否真能在中继盒侧清掉正在 firing 的 icon + backlight;若固件忽略 alarming 状态下的 A=0,fallback 是 FW 改让 RING_OFF 在 mute 模式下连同视觉一起清 |
SET_H=<n><0\|1> |
探针 <n> 的高温报警开关,<n> ∈ |
SET_L=<n><0\|1> |
探针 <n> 的低温报警开关 |
CLK=<n><0\|1> |
探针 <n> 的计时器显示开关 |
| 命令 | 作用 |
|---|---|
CNT_0 |
独占锁刷新(每 45 秒,per Liang 2026-06-06;与 legacy 0x55B1 同机制 / 60 s TTL) |
BT_STS=<0\|1> |
主动告知中继盒当前 BLE 连接状态 |
SET_<probe>=ABCDEF
| 字段 | 值 |
|---|---|
| A | 报警模式:0 = 关 / 1 = 响铃(armed)。仅二值,固件丢弃任何 A∉{0,1} 的整条命令(738fdeb / TAPD #1003233,Beta 2026-06-11:log 实证 SET_BU=214F25 Save 发出后无 SETT== ack 也无 LCD 更新,而同探针 A=1 写 247 ms 内 ack)。先前 065d153 / TAPD #1003138 引入的 2 = 静音仅背光(per-cook silent mirror,a5cf346 / TAPD #1003217 用作 Mute 镜像写入)已整体回退:cm4_protocol.configureProbe 删 silent 参数、A 改为 alarmEnabled ? 1 : 0,BoosterTransport / Cm4BoosterTransport / LegacyBoosterTransport / BleDeviceService.setProbeTarget 沿栈剥除 silent,cooking 页 _pushTargetToDevice 不再读 silentAlarm、#1003105 dedup signature 也不再随 Buzzer Mute 变化,booster_configurations_page buzzer pick 不再 re-push active cook target。Mute 现纯走 booster-level RI_CNT=4。SETT== 回报 A 仍读 0/1;parser 保留 bytes[5] != 0x00 而非 == 0x01(防御老固件 fallback 值仍被读作"armed"而不会被 #1003117 ack 检测误读成 device-MUTE 1→0 edge),见 §SETT== 设置帧;CM4SettingsAscii.silent 字段 + parser 一并删除。⚠️ DISARM 形 SET_<color>=000000(A=0 + BCDEF 全 0,Beta 2026-05-25 cloud doc 范本):FW 关 A=0 后丢弃 tail,3c40aa9 / TAPD #1003214 起 App Confirm 路径用它当物理按键的 wire 等价(pair 在 RING_OFF 后 250 ms)清掉每探针 闹钟 icon + backlight,详见 §报警 / 静音 |
| B | 单位(0 = °F, 1 = °C) |
| CD | 目标温度(两位大写十六进制,00–FF,per Beta 2026-05-11) |
| E | 肉种(1–9) |
| F | 熟度(1–5) |
<probe> 前缀按颜色(per cloud doc 2026-05-19,6e5dfb7 反向恢复色名):
| 探针 | 颜色(V5) | wire 前缀 |
|---|---|---|
| Probe 1 | Black | SET_BL |
| Probe 2 | White | SET_WH |
| Probe 3 | Blue | SET_BU |
| Probe 4 | Yellow | SET_YE |
其它 per-probe 命令(SET_H=XY / SET_L=XY / CLK=XY)的 X 字段当前代码使用数字 slot 0–3——这个不对称在协议本身。
⚠️ SET 前缀历史(per cloud doc 2026-05-19,6e5dfb7):原 spec / 早期 App 一直用色名前缀;Beta 2026-05-11 Q12 一度说"1-digit
SET_1..4canonical, 色名 retired"(55d699a 落地);Beta 2026-05-12 Q3 再改成 2-digitSET_01..04(052e2ce / ac25aec);Beta 2026-05-13 walk-back;2026-05-19 cloud doc 明确给出色名形式 +SET_BL=013211示例 + Liang 2026-05-16 HW test (culinatech_log_20260516201644.txt) 实测 numeric form booster 不应答 SETT==、目标不生效——6e5dfb7 反向恢复色名前缀。
例:把 Probe 2(White)设到 70°C,肉种 3,熟度 4 → SET_WH=014634(70 → hex 46);165°F 鸡 → SET_<color>=00A553(165 → hex A5)。整串字符串以大写 emit(hex 字母仅在 CD 槽位才会出现 A–F)。
| 命令 | 范围 | 作用 |
|---|---|---|
BL_LVL=N |
N ∈ | 背光级别 |
RI_LVL=N |
N ∈ | 响铃音量 |
RI_CNT=N |
N ∈ {0, 1, 2, 3, 4} | 报警响铃时长档位 / 静音:0=持续 / 1=3 声 / 2=1 分钟 / 3=5 分钟 / 4=静音(Mute,per Liang 2026-06-10:device-level Mute 是 RI_CNT=4,中继盒保存并对每次新 cook 自动应用——RI_CNT 自身承接 Mute、不再走「#1003138 A 字段」 那条路径)。a5cf346 / TAPD #1003217 曾让 App 在 pick Mute 时镜像写入 active + newly armed cook 的 per-probe SET_<color>= A 字段为 silent(A=2)让中继盒 LCD 在已 armed 的 cook 上也渲染静音/斜线报警 icon,但 738fdeb / TAPD #1003233(Beta 2026-06-11)整体取消——固件只接受 A∈{0,1}、任何 A=2 都让整条命令被丢弃(实证 SET_BU=214F25 Save 发出后无 SETT== ack 也无 LCD 更新),Mute 自此纯走 booster-level RI_CNT=4、不再下传 per-cook。先前 3be4358 把 RI_CNT 误绑成 #1003138 后 c8d5ed5 整体回退、Liang 2026-06-09 澄清 RI_CNT 是独立命令后 2f3e778 重新接入;2026-06-10 reframe 进一步把 Mute 归入 RI_CNT=4,2026-06-11 把 mirror sink 也一并撤除(详见 §探针配置 A 字段) |
⚠️ RI_CNT 是 App-only-editable 字段——物理按键不能改、SETT== / D=<hex> 遥测里不带、无 unsolicited push。7c141d7 / TAPD #1003203 起 App 侧 source-of-truth 改为本地持久化 buzzerRingModeProvider(StateNotifierProvider.family / SharedPreferences key buzzer_ring_mode_<deviceId>,默认 0=持续——booster 自身上电默认,per Liang 2026-06-09):2026-06-10 之前固件保存 RI_CNT=N 但不回报(log 实证:每条 RI_CNT 都是 App→booster、零 reply,含 RI_CNT=?),connect 末尾 RI_CNT=? + _handleRingCount → Booster.ringCount 回读链路因此一度 de-facto dead,Booster.ringCount 字段永不更新;Xiaomi checkmark 隐形、Pixel checkmark stale 的根因就是这个 missing reply。per Liang 2026-06-10 起固件回报 RI_CNT=N,回读链路经 onRingCountReported bridge 恢复(见下)。改后 BoosterConfigurations Buzzer 行 sheet 的 ✓ 选项与 row subtitle 直接读 buzzerRingModeProvider 即刻同步、各机型一致;每次 tap 同时 set() 持久化 + 发 RI_CNT=N 给中继盒。5-option sheet 已接入(Continuous / 3 Beeps / 1 Minute / 5 Minutes / Mute,per Liang 2026-06-10 Mute 由 RI_CNT=4 承接)。⚠️ 738fdeb / TAPD #1003233(Beta 2026-06-11)撤掉 a5cf346 的 SET A=2 镜像——pick Mute / 切回非 Mute mode 不再按 active CM4 cook 队列遍历调 transport.setProbeTarget(..., silent: …);_sendBuzzerModeSequence 缩回纯 RI_CNT=N + 400 ms 间距 + RI_CNT=? verify 的 set + paced verify 两步,_activeSessionsForBuzzerRefresh / CM4_BUZZER_MODE_TARGET_REFRESH 日志一并删。⚠️ c002576 / TAPD #1003273 起进一步缩到 RI_CNT=N 单条:booster 每收一条 RI_CNT 都 beep 一声,set + 400 ms 后 RI_CNT=? verify 让箱子响 "di----di"(WiFi 模式两条都 hit /sett,偶尔 MQTT message coalesce 只响一声 "di")。_sendBuzzerModeSequence helper 与 ble_device_service.dart import 整体删,pick 直接 sendBoosterSettingTo(deviceId, CM4Command.setRingCount(mode)) 一发一停;设备真相走 on-connect RI_CNT=? 查询 + onRingCountReported bridge(per Liang 2026-06-10),本地 optimistic pick 撑到那时;RI_CNT 是 App-only-editable 字段、刚发的值即权威。⚠️ 回读目前只覆盖 0–3 档:_parseRingCount(cm4_data_parser.dart)cap 在 0..3,RI_CNT=4(Mute)回报被丢弃、Mute 状态不经 readback 采纳——已知 bug、待把 parser 放宽到 0..4。理由:固件只接受 A∈{0,1},A=2 让整条 SET_<color>= 被丢弃(实证 SET_BU=214F25 无 ack);Mute 自此纯走 booster-level RI_CNT=4、不再下传 per-cook。详见 仪表盘 §配置齿轮。
| 命令 | 作用 |
|---|---|
TEMP_INT |
中继盒 LCD 大数字显示探针内温(LCD 角标 -INT) |
TEMP_EXT |
中继盒 LCD 大数字显示探针外温 / 环境温(LCD 角标 -EXT) |
TEMP_ALL |
在内 / 外温之间自动循环 |
⚠️ 暂 write-only:现版本固件还没有 ? 查询、SETT== / D=<hex> 遥测里也不带,App 无法从 wire 读回当前模式;FW 已在开发 TEMP 查询命令,落地后按 RI_CNT=? pattern 接 on-connect read-back + reply bridge 让设备真相赢。Per Liang 2026-06-10(97fe331 / TAPD #1003204 sub-correction):模式可以在中继盒物理按键改(先前 efd6ce8 wiki 写的「物理按键也改不了 / App 是 sole writer」是错的),且跨开关机保留,出厂默认 = TEMP_ALL(Cycle)。boosterDisplayModeProvider 因此从 StateNotifierProvider.family<…, String?, …> 改为 StateNotifierProvider.family<…, String, …>、initial state 由 null 改为 CM4Command.displayCycle 作为 factory-default seed——fresh install / 同一账号第二台手机首次 pick 前 sheet ✓ 与 row subtitle 也会先显示 Cycle 而非"未设"。Best-effort 兜底:用户在物理按键改了之后 App 侧记录就 stale,待 FW 查询命令上线才补完整真相。每次 tap 经 BleDeviceService.sendBoosterSettingTo(deviceId, CM4Command.displayInternal / External / Cycle) 发出 + boosterDisplayModeProvider.set(mode) 持久化 + snackbar 确认(⚠️ fc7e379 / TAPD #1003254 起:DISP 与 Buzzer RI_CNT 都从 raw sendCommandTo / sendCommandAsyncTo 改走新增的 sendBoosterSettingTo → BoosterTransport.sendSetting → CM4 _send 的 BLE-or-cloud fallback,BLE 链路不在时经 /CM4/<id>/sett 推送送达;先前 raw 路径是 BLE-only、WiFi 模式 + 蓝牙关时静默吞掉、中继盒永远收不到 buzzer / display 切换。WiFi-provisioning 写入 M_ID / M_PD / SSID / PSWD 故意留在 BLE-only 路径),详见 仪表盘 §配置齿轮。
| 命令 | 作用 |
|---|---|
M_ID=<deviceId> |
设置中继盒自己登录 MQTT broker 用的 username(per Beta/Liang 2026-06-12,88aaeaa)。不重启中继盒。⚠️ 不发该命令时固件会 fallback 到内置 rd broker-superuser 账号——server 端 topic / auth 错误都被吞掉。详见 §配网序言下方 callout |
M_PD=<password> |
设置中继盒登录 MQTT broker 用的密码(同上 88aaeaa)。不重启。值取 MqttConfig.devTestPassword 共享 dev secret;dev log 仅记长度、不记明文 |
SSID=<name> |
设置 WiFi 名 |
PSWD=<password> |
设置 WiFi 密码(收到后中继盒重启到 WiFi 模式) |
WIFI_STS=N |
设置 WiFi 状态指示,N ∈ |
⚠️ 配网序言:M_ID → M_PD → SSID → PSWD(per Beta/Liang 2026-06-12,88aaeaa):App 必须先按顺序写
M_ID=<deviceId>+M_PD=<MqttConfig.devTestPassword>、再写SSID=+PSWD=,每两条之间 ≥1 s 间距满足 AE03 同设备 write-pacing。前两条只设 broker 凭据、不重启中继盒;只有PSWD=触发 boot。M_ID=/M_PD=wire 格式假定与SSID=/PSWD=一致——未到位的 written 确认前,固件对未知写直接 ignore(与 today's behavior 等价),落地后会再校正。dev log 记WIFI_PAIR_MQTT_CRED_SENT(只带密码长度)。详见 WiFi 配置 §代码路径。
| 命令 | 作用 |
|---|---|
USUS=N |
设置 server region:0 = China (cn-hangzhou), 1 = US (us-east-1), 2 = EU (eu-central-1)(per cloud doc 2026-05-11 / 351430f) |
OTA |
进入 OTA 升级模式 |
D=<hex> 探针遥测ASCII 字符串:D=<30 hex chars>\r\n。30 个 hex 字符解码后是 15 字节 binary,与 legacy 15-byte 探针包字节布局完全一致。
| 字节 | 字段 | 解析 |
|---|---|---|
| 1 | 中继盒电量 | 0–10 等级;高 nibble = 8 表示充电中 |
| 2 | 中继盒固件版本 | hex → decimal,显示 V<n> |
| 3–4 | 探针内温 ADC | BE16 → tempIntArray 反查 °C |
| 5–6 | 探针环境温 ADC | BE16 → tempExtArray 反查 °C |
| 7 | 探针电量 | 0–10 等级 |
| 8 | 探针固件版本 | hex → decimal |
| 9–14 | 探针 MAC | 6 字节,on-wire 反序;字节 13(index 4)= 探针标识 |
| 15 | RSSI | -(0x100 - byte) dBm |
字节序细节与 NTC 查表见 v4.2 §三、字节序 与 §四、温度查表(CM4 与 legacy 共享 lib/core/protocol/temperature_lookup.dart)。
PENON <id> / PENOFF <id> 探针上线 / 下线格式:
PENON 1 / PENON 2 / PENON 3 / PENON 4(新协议)PENOFF 形式相同(当前固件仅发 PENOFF <N>,无 PENON push)。Parser 仅接受数字 1–4 映射到 ProbeNumber enum,不接受颜色名。WIFI_STS=N WiFi 状态广播 ⚠️ 已知断口固件可能在 AE05 上推 WIFI_STS=N(0–4 档),但 per 4ece91f:cm4_data_parser 当前对它返回 null 丢弃,UI 信号格数走 MQTT telemetry merge 而非该 ASCII 解析路径。push-on-change vs query-response-only 语义 Beta 还没回——记为 Q5 follow-up。若 Beta 确认是 unsolicited push,再加 Cm4WifiStatus message 类型接进 parser。✅ 闭合(Beta 2026-05-12 / 9f419a8 V13 整合):Beta 确认 WIFI_STS=N 是 unsolicited push(2 s 一次)、CM4WifiStatus 已进 parser、_handleWifiStatus → Booster.wifiStatus;860de52 / TAPD #1003167 起 MQTT /sett 通道也带、4f26473 起 dashboard WiFi glyph 无条件渲 wifiStatus.firmwareValue。V13 同时新增 MQTT_STS=N(10 s 一推、broker 健康)作为 Beta 指定的「配网成功」信号——WIFI_STS 只报 AP join、MQTT_STS=1 才报 broker 可达。
SETT==<32 hex> 设置帧(per cloud doc 2026-05-11,6337d24)32 hex chars 解码出 16 字节,是中继盒对 SET_RD 查询的响应;当前代码注释还表明,用户在物理按键改设置时会在 BLE 与 MQTT 两条通道上收到同类 unsolicited SETT== 帧。
| 字节(0-idx) | 字段 |
|---|---|
| 0 | 中继盒电量 raw(过 decodeBoosterBattery) |
| 1 | 中继盒固件版本 |
| 2 | 单位(00 = °F,01 = °C) |
| 3–4 | 预设报警 raw(LE:byte 3 低位 / byte 4 高位) |
| 5 | 报警启用(00 = off,01 = on) |
| 6 | 报警值 °F |
| 7 | 报警值 °C |
| 8–13 | 探针地址(6 字节 wire 序,addr[4] = 探针标识) |
| 14 | 肉种 E(1–9;1 = Custom,per cloud doc 编号见 §Q2) |
| 15 | 熟度 F(1–5;仅 E>1 时生效) |
App 侧解析为 CM4SettingsAscii 类型;handler 当前接入两路 adopt——9240a3b 起 target adopt(CM4_ALARM_SYNC_ADOPT,复用 legacy 的 diverge-then-adopt + echo-suppression on lastUserTargetWriteAt),40f3340 起 meat-type adopt(CM4_MEAT_SYNC_ADOPT,per-probe _DeviceState.cachedMeatTypeE 抗 echo + 共享同一个 2 s 用户写入窗口,TAPD #1003050 ①);doneness 与 alarm 配置其余字段仍 log 但不 adopt,详见 §Q3。
✅
MQTT 订阅尚未连通:App 只 publish 到860de52 / TAPD #1003167(Beta 2026-06-12)整体接通:/CM4/<id>/sett,并不 subscribe;要接 unsolicited 还得加 prefix-filter 避免把 App 自己 outbound 的命令被 broker echo 回来当成新设置吃。MqttService在 connect 与 auto-reconnect 时subscribe/CM4/<id>/sett,_handleSettingsAsciicloud-local 镜像沿用 BLE 路径同套 Liang 闸(alarm-inactive skip / factory-reset one-shot ADOPT / 2 s own-echo suppression / strict< 1.0 °Fmatched check),三回调onProbeAlarmConfigUpdated/onProbeTargetReconciled/onProbeMeatTypeReconciled在device_providers.dart接进与 BLE 同一 provider body;App 自发的SET_xx=echo 不解析成CM4Message、静默 fall through,无需 prefix-filter。WIFI_STS push 同走/sett、merge 到Booster.wifiStatus让 cloud 模式 dashboard WiFi glyph 实时(详见 MQTT 与云端 §Topic 结构 旁的 ✅ callout 与 仪表盘 §header WiFi glyph)。BLE 路径下的周期SET_RD轮询仍是独立 follow-up(与本次 MQTT 订阅修复正交)。
Ver=V<hex>+MAC<xx:xx:xx:xx:xx:xx> 版本 + WiFi MAC 应答(per Liang 2026-06-04,TAPD #1003141)Per Liang 2026-06-04:CM4 收到 VER 查询后回 Ver=V08+MAC00:1A:2B:3C:4D:5E —— V<hex> 段是固件版本字节(与 D= byte 2 同义),+MAC.. 段是中继盒WiFi 网卡硬件 MAC,与 OS 暴露的 BLE radio MAC(连接 remoteId,iOS 完全隐藏)不同;该 MAC 永远可用,与 WiFi 是否关联到具体网络无关。App 在 CM4 连接末尾延迟 300 ms 发 VER(BleDeviceService._finishConnect,间距 SET_RD ≥ 200 ms 守 AE03 同设备 write-pacing),解析进 CM4VersionInfo 后由 _handleVersionInfo 写到 Booster.wifiMac,仪表盘 ℹ 信息对话框由「—」placeholder 翻为实际 MAC(详见 仪表盘 §Action #5 ℹ 信息)。固件版本字段 redundancy 不用,主取 MAC。Ver= 是solicited 应答(与 SETT== 既 unsolicited push 又 SET_RD 应答的双语义不同——它仅作 VER 的应答),所以不进上方「unsolicited 全集」清单;老固件回复缺 +MAC 段时 wifiMac == null、对话框保留 placeholder。BoosterNotifier.snapshotsEqual 同时比 wifiMac 保证这条 ~300 ms 后的单次 emit 不被 dedup 吞。
Per Beta 2026-05-11(Q6 确认 + 6337d24 SETT== 接入):上述三种(D=<hex>、PENON/PENOFF <id>、SETT==<32hex>)曾是 CM4 固件 unsolicited ASCII 推送的全集。中继盒/探针低电量、过温与安全报警条件全部是 App 端从 D=<hex> 解出的派生量(中继盒电量看字节 1、探针电量看字节 7、内/环境温看字节 3–6 配合 SET_H / SET_L 阈值),没有专门的 FW push。⚠️ 9f419a8 / Beta 2026-06-15(V13)扩 unsolicited 集——上述三种之外新增 WIFI_STS=N(2 s 一推、AP join 状态,先前 4ece91f Q5 断口处理已闭合;⚠️ decf29b / TAPD #1003141 0613:V13 起 WIFI_STS=N 第二行追加 RSSI=<n>dB——典型 wire WIFI_STS=3\n RSSI=-62dB,0615 log 上 _parseWifiStatus 拿整尾 int.tryParse → null 把每条 push 静默丢,parser 改 regex 分别抓 bar bucket + 可选 RSSI=<n>dB,CM4WifiStatus.rssiDbm → Booster.wifiRssiDbm,info dialog 拿到真实 dBm;老固件无 RSSI 段时 rssiDbm == null fallback 到 bar 区间,详见 仪表盘 §Action #5 ℹ 信息 6458c32 callout 末尾的 decf29b 沿用)、MQTT_STS=N(10 s 一推,N∈{0,1}:1 = booster 自己 broker session 健康即 Beta 指定的「配网成功」信号,与 WIFI_STS 的 AP join 区分——盒子可能 WiFi 满格却到不了 broker、走 CM4MqttStatus 进 _handleMqttStatus 更新 Booster.mqttConnected、BoosterNotifier.snapshotsEqual 已加 mqttConnected 字段比较防 false→true 翻转被去重吞)、M_ID OK / M_PD OK(配网时 App 写 M_ID= / M_PD= 落 NVS 后的 echo ack,单次一帧、走 CM4CredAck 进 _handleCredAck 记 always-on WIFI_PAIR_CRED_ACK 日志确认凭据真持久化,仍是 diagnostic 而非配对 verdict 判据)。⚠️ 028087a / V14 / Per Beta 2026-06-16 再扩 unsolicited 集:新增 <deviceId> online broker-login 公告——中继盒一登录 broker 就向 /sett publish " online",Per Liang 这是最可靠的 online / 配网成功信号(与 MQTT_STS=1 双保险);parser 大小写不敏感 + 容忍 CRLF 尾,map 到 CM4MqttStatus(connected: true) 走与 MQTT_STS=1 同套 Booster.mqttConnected 路径,wifi_setup_page 配网 verify 与 dashboard 现有的 90 s 双信号或 verdict 窗自动接通、零下游改动。同 commit V14 命令 topic 拆分 /CM4/<id>/apps(详见上方 §通信通道 MQTT 表),先前 App 自己的 SET_xx= echo 回弹问题自此从根本消除。Liang 不做老固件兼容门控,"直接改、不考虑老设备"。其它形式的 ASCII 进 parser 直接静默丢弃(log PARSER unrecognised)今后均视为 routing bug 或固件回归。Solicited 应答有 SETT== (SET_RD 触发,9f419a8 起 cloud 路径也支持:MqttService 在首条 cloud 帧到达后 publish SET_RD 到 /sett、Beta 2026-06-15 确认中继盒 over MQTT 也回 SETT==、_handleSettingsAscii adopt——纯 cloud session 无 GATT 也能同步 target / alarm) 与 Ver=...+MAC... (VER 触发,per Liang 2026-06-04 / TAPD #1003141,详见上方) 两种。
App ↔ CM4 中继盒
SET_RD / VER / USUS=? / WIFI_STS=? ─────────►
SET_F / SET_C ─────────►
RING_OFF ─────────►
SET_H=<n><01> / SET_L=<n><01> ─────────►
CLK=<n><01> ─────────►
CNT_0(每 45s 独占锁刷新) ─────────►
BT_STS=<01> ─────────►
SET_<color>=ABCDEF ─────────►
BL_LVL=N / RI_LVL=N / RI_CNT=N ─────────►
TEMP_INT / TEMP_EXT / TEMP_ALL ─────────► (write-only,b4592c5)
SSID=... / PSWD=... / WIFI_STS=N ─────────►
USUS=N / OTA ─────────►
◄───────── D=<30hex>\r\n
◄───────── PENON / PENOFF <id>
◄───────── SETT==<32hex> (SET_RD 响应 + 物理按键改设置 unsolicited push,6337d24)
◄───────── WIFI_STS=N(2 s 一推,AP join 状态,Beta 2026-05-12 闭合 Q5)
◄───────── MQTT_STS=N(10 s 一推,broker 健康,Beta 2026-06-15 V13 = 配网成功信号)
◄───────── M_ID OK / M_PD OK(配网时 NVS 持久化 echo ack,Beta 2026-06-15 V13)
以下问题在新人触碰 CM4 协议层时容易踩坑。优先级按对当前用户体验的影响排。
Per Beta 2026-05-11 spec:<CD> 是两位大写十六进制(00–FF),不是十进制——0xFF=255 覆盖所有现实烹饪温度(鸡 165°F → A5、火鸡 175°F → AF、牛排全熟 160°F → A0、°C 槽 70 → 46),早先把 chunk 当十进制读才会得出"装不下 99°F+"的结论。CM4Command.configureProbe 改用 toRadixString(16).padLeft(2, '0') 编码并对整串走 .toUpperCase()(hex 字母仅 CD 出,但整串大写防御性 emit),Cm4BoosterTransport.setProbeTarget 的 skip 守卫从 > 99 放宽到 > 0xFF。Beta 同次确认 sub-answer (a)/(b)/(c) 全部不需要:booster 按 B flag 自动 reconcile 显示单位,CD 槽位 2 字符上限 0xFF 不变,也没有备用的高温命令路径。
meatType 1–9 / doneness 1–5 的具体映射 ✅ 已对齐(cloud doc 2026-05-11 / 351430f)Per cloud doc 2026-05-11(351430f 反向同步):<E> 按 cloud doc 的 (E,F)→temp 查表索引编号,与中继盒物理屏槽位无关——1=Custom("无 preset" sentinel,FW 不查表、用 App 发的 CD 原值)、2=Veal、3=Beef、4=Chicken、5=Lamb、6=Hamburger、7=Pork、8=Fish、9=Turkey;<F> ∈ {1..5} 自然顺序(Rare → Well Done),仅当 E>1 时生效(E=1 Custom 下 F 被 FW 忽略)。E>1 时 FW 用 (E,F) 表算 target、完全忽略 App 的 CD(这条由 e43ba81 + Beta 2026-05-12 Q5 sub-answer (a) 明确确认,详见下方 ⚠️ callout)。中继盒物理屏视觉上把 Custom 放在右下槽位(位置 9)只是 LCD UX,与 wire E 编号无关——把屏序号当 wire E 是 7356689 引入的 bug,351430f 已反向修正;Cm4BoosterTransport.setProbeTarget fallback 默认 (meatType=1, doneness=3),即"缺 meat ⇒ Custom + medium,让 App 的 CD 直接生效"。
⚠️ CD 字段语义 + 不合法 (E,F) 组合(per Beta 2026-05-12,e43ba81,Q5 follow-up):preset 激活(E=2..9)时 FW 直接读 cloud doc 的 (E,F) → target 查表,完全忽略 App 发的 CD——Beta 在 (a)/(b)/(c) 三个 sub-answer 里明确选 (a):FW 总是用 table、不是 App-CD-override、也不是"table 决报警 / CD 决屏显",所以 preset 场景下 App 不需要也不应该尝试"算个 smart CD",CD 对 E=2..9 是 don't-care;只有 E=1(Custom,sentinel)会把 App CD 当 target 直发、F 被 FW 忽略。cloud doc 的 (E,F) 表里有 "--" 单元(如 Chicken+Rare),Beta 明确指示"请不要发送"——booster 对未填组合行为 undefined;新加的
MeatTypeFwTable.validDonenessextension(probe.dart)按家族返回合法Doneness列表,约定 N 个合法 doneness 一定是 Rare→Well Done 末尾 N 个——Veal/Beef/Lamb (E=2/3/5) 全 5 个、Pork (E=7) 去 Rare 剩 4 个、Fish (E=8) 只 Medium/MW/WD 3 个、Chicken/Hamburger/Turkey (E=4/6/9) 只 Well Done 1 个、Custom (E=1) 豁免暴露全集。data 层已铺好,但 cooking page / preset page picker 的接入尚未做(pre-existing UI gap,不在 e43ba81 范围内);新增 8 个test/models/probe_test.dart测试覆盖 numbering round-trip、四种约束模式、"valid 集合是Doneness.values的连续后缀"不变式。
Legacy 通过 12B 设置响应(字节 6 = enabled flag、字节 7 = °F 参考、字节 8 = °C 参考)实现 ALARM_SYNC_ADOPT:用户在中继盒按键改目标后,App 收到下一帧 12B 时把目标拉回到 App 侧。Legacy 每 ~3 秒自动 push 一次 12B、无需 App 主动查询,所以同步是常态。
CM4 端的 wire 等价由 SETT==<32hex> 承担(详见上方 §SETT== 设置帧 / cloud doc 2026-05-11 / 6337d24)——/CM4/<id>/sett MQTT topic 在用户改设置时被中继盒主动 push,BLE 模式下也作为 SET_RD 查询的响应回。9240a3b 起 ALARM_SYNC_ADOPT 等价的 target diverge-then-adopt + echo-suppression(沿用 legacy lastUserTargetWriteAt 流程,记 CM4_ALARM_SYNC_ADOPT)已接通;40f3340(TAPD #1003050 ①)起 meat-type adopt 作为独立路径接通——_handleSettingsAscii 在 snapshot 的 meatTypeWireE 与 App 上次发送的 _DeviceState.cachedMeatTypeE 不一致、alarm enabled 且 2 s 用户写入窗口已过时,记 CM4_MEAT_SYNC_ADOPT 并通过 onProbeMeatTypeReconciled 回调把新 MeatType 同时写进 active CookingSession 与 pendingCookParamsProvider(cooking page 的 _meatType 通过对应 ref.listen 自动 resync);越界 E 不 adopt 而记 CM4_MEAT_SYNC_SKIP,防固件回归污染 session。两路 ADOPT 互相独立——用户在物理屏只改 meat 不改 temp、或反之,都能反向同步。仍未接:doneness 与 alarm 配置其余字段(860de52 / TAPD #1003167 起 MQTT 订阅 /CM4/<id>/sett 已接通——MqttService._handleSettingsAscii 与 BLE 路径同 Liang 闸、cloud-local gate state、出口三回调与 BLE 同套 provider body;App 自发的 SET_xx= echo 不解析成 CM4Message、静默 fall through,无需 prefix-filter。详见上方 §SETT== 设置帧 ✅ callout)。
ee6634c(TAPD #1003063)补 connect-time 触发:CM4 不像 legacy 周期 push 12B,SETT== 帧只在「用户按物理键改设置」或「App 显式 SET_RD」时到来——所以 8 秒 ON/OFF 复位(每根探针目标回到 50 °C / 122 °F 默认)后 App 没事件触发反向同步、dashboard 仍显示旧目标。修复在 BleDeviceService._finishConnect 末尾按 deviceId.boosterFamily.isCm4 gate 调一次 _transportFor(deviceId).queryStatus()(CM4 transport 的 queryStatus 是 SET_RD 命令),记 CM4_READ_SETTINGS_ON_CONNECT;中继盒回的 SETT== 走既有 _handleSettingsResponse → pendingCookParamsProvider 路径刷新 dashboard。Legacy 0x55AE 设置查询不重新启用——legacy 走 ~3 秒一帧的 unsolicited 12B 流就够(per Liang 2026-05-06 已下线 0x55AE 周期查询)。待 CM4 HW 验证:booster 是否在 connect 后立刻应答 SET_RD、是否 beep。
App 每 20 秒发 — Per Liang 2026-06-06 / TAPD #1003176 已澄清:60 秒是中继盒固件层 独占锁 TTL(与 legacy 0x55B1 同机制),App 每 45 秒发 CNT_0,注释说「不发的话约 60 秒后链路死掉」。但 60 秒是 BLE 连接监督超时还是 CM4 固件层的 idle disconnect?CNT_0 刷新锁;锁到期后中继盒主动 shed GATT 链路(c613148 起 cadence 由 20 s 改 45 s 跟 legacy 对齐)。
CM4 自己是否会主动断开空闲连接、有没有可配置的超时时间——unknown。
当前固件仅发 PENOFF <N>(N ∈ {1, 2, 3, 4}),无 PENON push(上线由 D=<hex> 遥测隐式表示)。App 侧 _parseProbeIdentifier 仅接受数字 1–4,遇到 legacy 颜色名会返回 null 丢弃。
USUS=2 = US2 还是 EU ✅ 已解决(cloud doc 2026-05-11 / 351430f)Per cloud doc 2026-05-11:USUS=2 是 EU broker(k211a27b.ala.eu-central-1.emqxsl.com:8883),与 App 的 MqttServerConfig.eu host 完全一致。Q6 原始矛盾(cm4_protocol.dart 旧 docstring 写 "US2"、MQTT 与云端 broker 表写 "EU")由 cloud doc 落定后者;旧 docstring 已重写,setServerRegion / ServerRegion / README 里 "pending cloud-doc" 的 hedge 全部去掉。完整 broker 映射(0=CN cn-hangzhou / 1=US us-east-1 / 2=EU eu-central-1,均 TLS 8883)见 MQTT 与云端 §区域 broker。
cm4_protocol.dart 列的是 App 当前发的命令——不是 CM4 固件支持的全集。Liang 可能在固件里实现了 timezone、language、factory reset、explicit alarm-state push 等,但 App 没接。下次 spec review 时一并问。
Topic 已知三个:/CM4/<deviceId>/data(中继盒 → App 遥测)、/CM4/<deviceId>/sett(中继盒 → App 推送:SETT== / WIFI_STS / MQTT_STS / <id> online / SET_RD 应答)、/CM4/<deviceId>/apps(028087a / V14 / Per Beta 2026-06-16 新增:App → 中继盒命令)。中继盒是否还订阅 / 发布其他 topic(status / OTA-progress / 离线遗嘱等)——unknown。
| 项 | v4.2(legacy) | CM4(本页) |
|---|---|---|
| 协议格式 | 二进制 0x55XX | ASCII |
| 命令通道 | AE03 | AE03 + MQTT |
| 数据通道 | AE05 indicate | AE05 notify + MQTT |
| 探针数 | 1–3 | 1–4 |
| 字节级权威 | v4.2 §一-四 | 本页 |
| 共享字节布局 | 15-byte 探针包、NTC 查表 | 同 v4.2 |
15-byte 探针包的字节级解析(NTC 查表、字节序、MAC 还原)只在 v4.2 描述。CM4 解码出 binary 后走同一逻辑——temperature_lookup.dart 是双家族共享文件。
lib/core/protocol/cm4_protocol.dart(208 行)— App 发出的所有命令常量与 builder;行 61-74 的 SET prefix 历史注释块是 flip-flop 时间线权威,改 prefix 前必读lib/core/protocol/cm4_data_parser.dart(343 行)— 解析 D= / PENON / PENOFF / SETT== / WIFI_STS= / MQTT_STS= / M_ID OK / M_PD OK(9f419a8 V13 起新增后三种,详见 §unsolicited push 全集)lib/core/transport/cm4_booster_transport.dart — 命令派发;CD hex 编码后守卫从 > 99 放宽到 > 0xFF(a3c9cf7)lib/core/services/mqtt_service.dart(585 行)— MQTT 路径(topic、TLS、broker)lib/core/protocol/temperature_lookup.dart(199 行)— 与 legacy 共享的 NTC 查表