仓库: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>"
}}
version:目标版本 x.y.z(不带 v 前缀),必须与包头
版本一致,否则即使验签通过也拒绝安装。pkg_url:共用包下载地址,由下载器解释(板上是 HTTPS
客户端,主机测试是模拟下载器)。key_package:80 字节包密钥包(临时公钥 32 + 包装后包密钥
48),长度不对在启动时就拒绝,不下载。start):cmd_id 非空且不超 128 字节、
版本串是合法 x.y.z、包密钥包正好 80 字节、下载器
begin 成功。任一项不过,下一轮 poll 直接出失败回执,
不下载、不写盘。校验不过也有回执(cmd_id 原样回显),
只有“ worker 正忙”(已有命令在跑)是启动返回值错误,
由应用层自己回“ worker busy”。poll 最多搬一个 1024 字节块;
下载器没数据就报 Pending,worker 让出,调用方继续干别
的事。攒够 312 字节包头立刻解析:型号/固件名不对、包
太大(超 max_package_size)、缓冲装不下,马上拒绝,
不再下载后半截。流提前结束(字节数与包头宣称不符)
报“截断”。verify_package 全
流水线——解包密钥(设备私钥 × 临时公钥)、解密包体、
流式验签(包头前 248 字节 + 固件明文)。任一步不过,
零写盘(测试里数写盘次数,验证失败用例全是 0)。
验签通过后再比对版本:包头版本与命令版本不一致,
拒绝安装(同样零写盘)。0x08180000–0x08200000,按扇区懒擦,
只擦本次写盘实际触及的扇区;写不进就等下一轮,不阻塞;
闪存忙也一样)。写完读回暂存区算 SHA-256,与明文哈希
比对,不一致拒绝切换(并清待切,半成品永不启动)。
比对通过才“切换”:读暂存镜像版本块得版本、算暂存镜像
SHA-256、写启动信息(待切有效、待切版本/长度/哈希)。
这一步才叫“安装”。注意拷贝模型语义(见 docs/flash-boot.md):
切换不是翻运行版本标志——主区未动,运行版本未变;新镜像
在下次复位时由启动程序拷到主区,健康确认后才成为运行版本。cmd_ack,QoS 1):cmd_id 原样回显、
ok、原因 message、版本 data。成功时 data.version
为新版本(已暂存),data.active_version 仍为旧版本
(仍在运行)——云端不得把“已安装”误读为“已在运行新版本”,
直至复位拷贝 + 健康确认后状态上报的版本才变新版本。
失败时 data 含仍是旧版本的当前版本(回滚可见),
message 写清原因。| 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 |
写完读回哈希不符 |
| 下载器/存储返回的原因串 | 断网、闪存写失败等(原样进回执) |
docs/flash-boot.md 断电安全表)。max_package_size 上限;
单命令在跑(忙时第二个命令被拒绝、不排队)。
测试:超限包拒绝(包头宣称超限早拒、流超送拒)、
忙时拒绝;soak 后备份区仍正好一包大小。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)。
cmd:收到 firmware_update 后解析
params(version、pkg_url、key_package hex→字节),
调 OtaWorker::start;start 报 busy 时应用层自己回
ok:false + worker busy(worker 不排队)。poll(下载器、存储、时钟由
应用层给),Pending 时让出(embassy 里 yield 或
定时器唤醒),Complete 时把 outcome 按上表拼成
cmd_ack JSON 发到 cmd_ack 话题(QoS 1)。max_package_size,明文缓冲 ≥
最大固件;板上放静态区,不要放栈上(固件上百 KB)。bootloader::flash_store::FlashImageStore
(暂存区 + 启动信息,见 docs/flash-boot.md)。
switch_to_backup 写待切记录(待切版本/长度/哈希),不翻
运行版本;verify_backup 读回暂存区算 SHA-256 比对;
write_backup 闪存忙时报 Busy(worker 下一轮重试,
仍在安装超时预算内)。应用在上线状态发布成功后调
confirm_boot() 写健康确认(仅待确认时写盘,否则无操作)。docs/provisioning.md)。