设备端 firmware_update 处理

仓库:https://github.com/ffffxxxjjj/gateway

设备收到云端 firmware_update 命令后,下载共用包、解包密钥、 解固件、验签、安装、回执的整套处理。实现在 bootloader/src/ota.rs:no_std、无堆、传输无关的轮询状态机, 同一份代码跑主机测试和芯片;MQTT 订阅/发布接线是板上应用层 的工作,不在本模块内(见文末“板上接线”)。真实 Flash 存储 实现在 bootloader/src/flash_store.rs(暂存区 + 启动信息, 见 docs/flash-boot.md);ota.rs 的状态机本身不改,只换 存储实现。

命令格式

云端经 MQTT 发 cmd(QoS 1,话题 v1/tenants/{tenant}/devices/{device}/cmd,契约 v1.2):

{"cmd_id": "…", "action": "firmware_update", "params": {
  "version": "1.2.3",
  "pkg_url": "https://…/gateway-stm32h743vi-release-v1.2.3-20251031-171000.pkg",
  "key_package": "<80 字节包密钥包的 hex>"
}}

处理流程(按步骤)

  1. 启动校验(start):cmd_id 非空且不超 128 字节、 版本串是合法 x.y.z、包密钥包正好 80 字节、下载器 begin 成功。任一项不过,下一轮 poll 直接出失败回执, 不下载、不写盘。校验不过也有回执(cmd_id 原样回显), 只有“ worker 正忙”(已有命令在跑)是启动返回值错误, 由应用层自己回“ worker busy”。
  2. 下载(分块轮询):每轮 poll 最多搬一个 1024 字节块; 下载器没数据就报 Pending,worker 让出,调用方继续干别 的事。攒够 312 字节包头立刻解析:型号/固件名不对、包 太大(超 max_package_size)、缓冲装不下,马上拒绝, 不再下载后半截。流提前结束(字节数与包头宣称不符) 报“截断”。
  3. 验证(先验后装):整包到齐后跑 verify_package 全 流水线——解包密钥(设备私钥 × 临时公钥)、解密包体、 流式验签(包头前 248 字节 + 固件明文)。任一步不过, 零写盘(测试里数写盘次数,验证失败用例全是 0)。 验签通过后再比对版本:包头版本与命令版本不一致, 拒绝安装(同样零写盘)。
  4. 安装(分块写 + 读回 + 切换):明文分 1024 字节块写 暂存区(bank2 后半段 0x08180000–0x08200000,按扇区懒擦, 只擦本次写盘实际触及的扇区;写不进就等下一轮,不阻塞; 闪存忙也一样)。写完读回暂存区算 SHA-256,与明文哈希 比对,不一致拒绝切换(并清待切,半成品永不启动)。 比对通过才“切换”:读暂存镜像版本块得版本、算暂存镜像 SHA-256、写启动信息(待切有效、待切版本/长度/哈希)。 这一步才叫“安装”。注意拷贝模型语义(见 docs/flash-boot.md): 切换不是翻运行版本标志——主区未动,运行版本未变;新镜像 在下次复位时由启动程序拷到主区,健康确认后才成为运行版本。
  5. 回执(cmd_ack,QoS 1):cmd_id 原样回显、 ok、原因 message、版本 data。成功时 data.version 为新版本(已暂存),data.active_version 仍为旧版本 (仍在运行)——云端不得把“已安装”误读为“已在运行新版本”, 直至复位拷贝 + 健康确认后状态上报的版本才变新版本。 失败时 data 含仍是旧版本的当前版本(回滚可见), message 写清原因。

回执映射(OtaOutcome → cmd_ack JSON)

OtaOutcome 字段 cmd_ack 字段 说明
cmd_id(原样回显) cmd_id 严格配对:逐字节等于命令的 cmd_id,成功失败都一样
ok ok 仅安装并切换成功为 true
reason message 成功为 installed;失败为下表原因串
new_version data.version 成功时为新版本,失败时为空
active_version data.active_version 出回执时从存储读的当前版本:成功时仍为旧版本(新版本只是已暂存,待复位拷贝 + 确认后才变),失败为旧版本

失败原因表

原因串 含义
installed 成功(不是失败)
empty cmd_id / bad version / key package is not 80 bytes 命令校验不过(启动时拒绝)
worker busy 已有命令在跑(应用层回执,不经 worker)
download timeout / install timeout 下载/安装阶段超预算(默认各 60 秒,可配)
package too large 包超上限,或流送的比包头宣称的多
package buffer too small / output buffer too small 调用方缓冲装不下(配置错)
truncated package 流提前结束
downloader chunk too large 下载器实现错(报了比缓冲大的块)
各 VerifyError 文本 验证流水线失败(解包密钥/解密/验签/身份/长度等,见 bootloader 库文档)
version mismatch 验签通过但版本与命令不符
empty firmware 固件长度为 0
backup verification failed 写完读回哈希不符
下载器/存储返回的原因串 断网、闪存写失败等(原样进回执)

工业级卡点(逐项 + 测试证据)

主机测试

cd bootloader
cargo test

78 项全过:原 44 项(验证 12 + provisioning 13 + OTA 19: 好包全程、篡改包体/包头、错设备密钥、错签名、包密钥包 不匹配、版本不符、断网、下载中断、截断、下载超时、安装 超时、安装写失败回滚、读回失败回滚、忙时拒绝、超限包拒绝、 命令校验拒绝、非阻塞轮询、1000 轮 soak)+ 新增 34 项 (布局 10 + 假闪存 2 + 真实存储 10 + 启动决策与拷贝 12, 见 docs/flash-boot.md 主机测试节)。

测试数据在 bootloader/src/ota_data/,由真实 sign-tool v3 生成(见该目录 README.md,含再生成命令与 SHA-256); 固件文件叫 .fw 而不用 .bin(推仓工具跳过 .bin)。

板上接线(应用层要做的事,不在本模块内)

遗留(不在本批内)