固件:A12 命令处理器 + A13 USB 分帧 + A14 WiFi/MQTT(含 C1 配网)

- A12 单一命令处理器(cmd*.c):一行 JSON 进出、seq 全局判重(32 条按值)、
  限幅 / 空槽一律拒 / 拒绝记事件;命令号与错误码冻结(进 flash)。新增 power 命令,
  fan 改为只抬不压的下限(t_thermal_set_floor)。rail_set_mv 注明不拿锁,调用方包 xact。
- A13 USB-Serial-JTAG 分帧(ulink*.c):@C / @R 前缀 + CRC-16,日志行首 @ 垫空格,
  任意切块可切;关掉 USB 副控制台(它绕开驱动写 FIFO)。PC 端客户端 tools/prlink.py。
- A14 + C1(netcfg / mlink / cmd_net):topic phonerail/<mac>/{cmd,rsp,tele,state},
  mqtts(evse-ca 编进固件)、1 s 遥测、遗嘱离线;wifi / mqtt 配置只收 USB、存 NVS,
  net 永不回显密码。
- 协议定稿写进需求文档 §10.8(A12 / A13 / A14 三节);N7 撤销改 D7(机架管理前后端)。

宿主机 23 个 ctest / 400 例全绿;板 B 已烧 0.1.0-a14,经 WiFi → 公网 → 8883 TLS 真连网验过。
固件 0xE2220 / 1 MB 分区只剩 12%,C2 前要重排分区表。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 12:33:18 +08:00
共同撰写人 Claude Opus 5.5
父节点 cae567dfd8
当前提交 a32fa06c43
共修改 38 个文件,包含 4490 行新增和 85 行删除
+31 -66
查看文件
@@ -1,79 +1,44 @@
# 交接
更新:2026-09-16 完成 **R-0 ~ R-6**(整个 R2 迁移,计划 v2.0 §6.2)
更新:2026-09-27 完成 **A14 + C1**(WiFi/MQTT 链路 + 经 USB 配网,需求 §10.8「A14 定稿的 MQTT 链路」)
## 一句话现状
固件已经整体从 R1(单板 / 六片不同地址 INA226 / 74HC595 键链 / `VBUS_EN` 是 GPIO)
迁到 **R2**(底板 + 5 片同构刀片 / 全是 0x40 与 0x20 / TCA9548A 分通道 /
`VBUS_EN` 在刀片 PCA9555 的 P0.4 上)。**19 个 ctest、351 个用例全绿**,`idf.py build` 通过。
器件层与拓扑层全部换掉,**判据阈值与时间常量一个没动**(它们来自电路计算,R2 没改电路)。
## 2026-09-27 首次上板(底板,未插刀片)修的三处
- `app_main.c` **HUB_RST 只在上电复位(`ESP_RST_POWERON`)时脉冲**。原先每次重启都脉冲 ⇒
esptool 经 USB 复位即断口,只能走 J33 手动下载。现在 USB 复位(原因 11)不碰 Hub,可直接经 USB 反复烧。
- `t_safety.c` **空槽不走总线**:原先五路无条件读,空槽 NAK 每次十几 ms,槽 1 就吃光 10 ms 预算,
槽 2~5 与主干 0x43 一圈都没读到(输入侧监控全瞎)。新用例
`test_empty_slots_are_never_touched_and_the_input_still_reads`(去掉修复即红)。
- `app_main.c` 关掉 `i2c.master` 驱动日志:它的 NAK 日志在持锁期间同步打印,拖垮节拍。
- 上板实测:空机 `skipped=0 i2c_err=0 过载 0 最迟 0 ms`;I²C 扫描主干 0x22/0x23/0x43/0x4A/0x4B/0x70 全在,
另有 0x47 应答(推测 DAC63204 广播地址)。扫描固件在 `tools/i2c_scan/`(独立工程,只读、不写任何输出)。
板 B(`b81f3fc9c8bc`)经 WiFi `CANBOX-5G` → 公网 → home 服务器 mosquitto 8883(TLS)**真连网跑通**:
retained state、1 s 遥测、命令 / 判重 / `link` / `empty` / `parse` 回应全对。两条链路(USB + MQTT)都已上板。
**23 个 ctest、400 个用例全绿**,`idf.py build` 通过(**0xE2220,分区只剩 12%**)。板 B 烧的是 0.1.0-a14,配置已存 NVS。
## 下一步
**A12**:命令处理器(语义 + 序号判重 + 限幅)。R2 多一条验收:
**空槽(`PRSNT#` 为高)的命令一律拒**(`slot_present()` 已备好)。
另外 A15 开工前先把启动序列从 `app_main.c` 拆成宿主机可编的函数 —— 见下面「还没有拦网的」。
固件还欠 **A15**(启动顺序 + 降级矩阵)/ **A16**(风扇闭环接进 t_thermal 任务体)。
或者转**服务器侧前后端**(需求 D7):后端以 `phonerail-backend` 订阅 `phonerail/+/rsp|tele|state`,经 `<base>/cmd` 下发。
## 本次新增/修改的文件
- `main/i2cbus.{h,c}` **新** — mux 原子事务:拿锁 → 选通道 → 跑回调 → 放锁;通道影子 + 失败作废
- `main/slot.{h,c}` **新** — 一片刀片的 I/O 门面:路号 → mux 通道 → 影子下标只在这里换算一次
- `components/drv/tca9548a.{h,c}` / `ch224a.{h,c}` **新** — I²C 八路多路器 / PD 受电(只读)
- `main/t_thermal.{h,c}` **新** — 风扇闭环(一路 PWM 两只风扇 + `FAN_EN` 总开关 + 两路 TACH)
- `main/board_pins.h` — 整表重写(R2 的 GPIO 与 R1 **一个都不一样**)
- `components/drv/pca9555.{h,c}` — 重写:P0.0~4 是输出、五实例影子、初始化顺序是安全本身
- `components/drv/ina226.{h,c}` — 五路同址 0x40 + 输入侧 0x43
- `main/rail_cfg.{h,c}` — 换列:+`mux_ch` +`prsnt_n`,−`vbus_en`;新增 `RAIL_SELFTEST_MUX`
- `main/t_safety.{h,c}` — 一圈改成「五次 `xact` + 主干一次」;+整圈锁预算;+mux 失联/在位两条判据
- `main/t_actuator.{h,c}` — 595 链 → PCA9555 Port0;本文件现在**要拿 I²C 锁**
- `main/app_main.c` — R2 启动顺序(含 `HUB_RST` 只脉冲一次、`ina226_set_reversed(0x43,true)`)
- `main/evlog.h` — 末尾追加 `LOG_EV_MUX_LOST=14 / SLOT_EMPTY=15 / PD_LIMIT=16`
- sim:假 TCA9548A(关着的支路一律 NAK)、五实例假 PCA9555、假风扇
- `host/qtest.sh` **新** — 单个测试的快编快跑,迁移期并行开工用(DoD 仍认 `host/test.sh`)
- `components/drv/include/sr595.h` — 顶部加「⛔ R2 已退役」横幅,代码与测试保留不动
- `main/netcfg.{h,c}` **新** — 配置校验 / 持有 / 存储回调;网络状态;「密码永不外泄」
- `main/mlink.{h,c}` **新** — topic、入站消息校验、WiFi 退避、遥测节拍、上线消息
- `main/cmd_net.c` **新** — `wifi` / `mqtt`(只收 USB)/ `net` 三条命令;新增错误码 `link`(11)、命令号 10~12(冻结)
- `main/mlink_esp.{h,c}` **新**(只进 IDF)— NVS、WiFi STA、esp-mqtt(TLS,enqueue 不阻塞)、遥测
- `main/certs/evse_ca.pem` **新** — 服务器 mosquitto 的自签 CA(公钥证书),`EMBED_TXTFILES`
- `main/cmd_json.h` — 字符串上限 32 → 65(WPA2 口令 63)
- `main/app_main.c` / `CMakeLists.txt` — 接线;REQUIRES 加 wifi / netif / event / timer / nvs / mqtt
- `test/test_net.c`(10 例)**新**;`test_cmd.c` 冻结编号更新;版本号 `0.1.0-a14`
## 服务器上改了什么(home 服务器,mosquitto 1.6.9,evse 项目共用)
- `evse.passwd` 加用户 `b81f3fc9c8bc`、`phonerail-backend`;`evse.acl` 末尾追加 phonerail 段(`pattern … phonerail/%u/…`)。
- ⛔ **修了 evse 原有的漏洞**:8883 listener 缺 `cafile` ⇒ TLS 根本没生效、**公网明文**。补 `cafile /etc/mosquitto/certs/ca.crt` 后重启;
改前 8883 上零成功连接(日志自 09-24 起),evse-backend 走本机 1884,重启后 5 s 自动重连。
- 三个文件都有 `.bak-20260927-*` 备份。口令在 `/root/phonerail/mqtt.env`(700 目录,仅 root)。
## 验收命令与结果
```
$ bash host/test.sh
100% tests passed, 0 tests failed out of 19 # 351 个用例
$ .\build_idf.ps1
Project build complete. # phonerail.bin 0x3fa90,分区剩 75%
$ bash host/test.sh # 100% tests passed, 0 tests failed out of 23(400 例)
$ python tools/prlink.py COM5 net # "wifi":"up","ip":"192.168.8.108","mqtt":"up"
服务器:mosquitto_sub -h 127.0.0.1 -p 1884 -u phonerail-backend … -t 'phonerail/#' -v
```
注错验证过:放开「只收 USB」、去掉 JSON 字符串清洗 → 各自有用例变红。
## 踩到的坑(只写会再犯的)
- ⛔⛔ **PCA9555 的初始化顺序就是安全本身。** Output Port 复位值是 **0xFF**,
所以必须 `OUTPUT=0 → POLINV=0 → CONFIG1 → CONFIG0` ——**方向最后改**。
反过来的那一刻 P0.0~P0.4 一起输出 1 = 三个键全按下 + EDL 短接 + VBUS 上电。
注错实测:把顺序调过来,33 条用例里**只有 1 条变红**,终态完全正确 ——
这就是为什么它在真板上查不出来。拦网是 `sim_pca9555_unsafe_enable()`。
- ⛔ **`check_mux()` 的两段式不能拿「复位计数器和自己的快照比」做状态**:
复位之后把后者同步成前者,两个数永远相等 ⇒ 每圈都走「先复位」分支,
`MUX_RST#` 每 20 ms 脉冲一次而 `MUX_LOST` 一次也不出。状态要单独存(`mux_step`)。
- ⛔ **R2 一圈要拿六次锁**(R1 只拿一次)。每次 5 ms 超时看着没问题,
六次加起来 **30 ms > 20 ms 周期** ⇒ 总线繁忙时巡检成片丢拍,而每次 `xact` 都按时返回了。
现在有 `T_SAFETY_LOCK_BUDGET_MS`(10 ms)这条整圈的闸。
- ⚠️ **「断 VBUS 在锁内、关 eFuse 在锁外」这条,光断言「写的时候持着锁、通道对」拦不住**:
把它挪到锁外之后 `slot_vbus_enable()` 自己也会拿锁、也会选对通道,两条断言照样过。
真正的硬凭据是**「这一写发生在本圈第几次拿锁里」**(对 = 第 rail 次,错 = 第 7 次)。
- ⚠️ **`slot_present()` 读的是缓存**,`t_safety` 每圈要先 `slot_prsnt_poll()`。
不 poll 的现象是「拔掉刀片永远判不出来」,而快照里那五个 bool 看着完全正常。
- ⚠️ 假总线上 `hal_i2c_write_read()` 会让 `n_write` 和 `n_read` **各 +1**,
所以「巡检有没有写刀片」不能看 `n_write`,要看最后一次写的寄存器号。
- ⚠️ `i2cbus_init()` **必定留下一次 `MUX_RST#` 低脉冲**(先 config(OUTPUT) 再写 1),
测试里所有复位断言都要走增量。
## 还没有拦网的(不是遗漏,是已知边界)
- **启动序列一行也测不到**:`app_main.c` 只进 IDF 目标。像 `ina226_set_reversed(0x43,true)`
这种「漏了就把正常供电判成反灌」的单行,现在唯一的拦网是人眼。A15 先拆函数。
- **mux 通道整体循环错位**(0→1→2→3→4→0)自检全过:五路互不重复、都不越界、都不等于 5。
现象是「每一路读到的都是隔壁那台手机」。唯一判据是上板 **B1c 五槽逐一单插**。
- **选通道「写下去不报错但没生效」**:假器件写了就生效,宿主机造不出来。见计划 §11。
- **真并发**:sim 单线程,`i2cbus_xact` 把「拿锁 + 选通道」绑成原子的价值只能间接证明。
- ⛔ **`openssl s_client … | grep "Verify return code"` 不能证明 TLS 通了**:握手失败、没拿到证书时它也打 `0 (ok)`。要看 `Cipher is` / `Protocol`。
- ⛔ mosquitto 1.6:listener 只有 certfile/keyfile、没有 cafile/capath ⇒ **静默退化成明文**。TLS 选项改动要 restart,HUP 不生效。
- esp-tls 的「Certificate verified.」在握手失败时也会打,别被它带偏;服务器抓包(tcpdump 看 RST 时机)最快。
- 服务器 pid 文件不在 `/var/run/mosquitto.pid`,HUP 用 `pgrep -x mosquitto`。`mosquitto_passwd -b`(1.6.9)会把文件内容打到终端,不是出错。
- 本机(192.168.8.x)与服务器(10.5.5.x)不同网段,板子只能走公网 `home.example:8883`;**明文 mqtt:// 只许用在同一局域网**。
- 固件 0xE2220 / 1 MB:WiFi + MQTT + TLS ≈ 600 KB。**C2(OTA 双分区)前必须重排分区表**(模组是 16 MB,sdkconfig 只按 8 MB 用)。
- WiFi 断开原因码 3(AUTH_LEAVE)是重配网时我们主动断的,不是故障;201 = 找不到 AP(比如只有 5 GHz)。