• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

Microduck 的 OTA 更新系统:一只鸭子怎么做到永远刷不砖

Build in Public admin 20小时前 25次浏览 已收录 扫描二维码

Microduck 的 OTA 更新系统:一只鸭子怎么做到永远刷不砖

搞过嵌入式设备的人都知道,OTA 更新是真正的生死线。你可以在实验室里跑通一切,但一旦推到用户手里——网络断断续续、电池随时没电、用户中途拔电源——任何一个环节出错,设备就变砖。Microduck 是一只 shipped 到非开发者手里的机器人鸭子,它的更新系统设计目标很明确:永远不能砖。

为什么不用 A/B 分区

先回答一个自然的问题:RK3566 跑的是 Linux,为什么不用 RAUC 或 Mender 做 A/B 分区更新?

答案在于 Microduck 的更新粒度。这台机器人上真正会变的东西只有两样:daemon 二进制文件(Rust 编译的七个守护进程)和 ML 策略模型(ONNX 文件)。操作系统、内核、驱动都是静态的,不走 OTA 通道。为一个应用层更新搞双分区,杀鸡用牛刀。

Microduck 的选择是应用层更新:版本化目录加上原子符号链接切换。每个 release 作为一个完整目录落在 /opt/robot/daemon/releases// 下,current 符号链接指向当前活跃版本。切换就是改一个链接,原子操作,不存在半切换状态。

这个设计的关键假设是:OS 不变。如果 OS 本身需要更新,走的是带外通道——用 USB 线刷镜像,不走 daemon 更新系统。

签名验证:minisign,CI 签,机端验

更新包的签名用 minisign。选这个不选 GPG 或自研方案,原因很务实:minisign 的 verify crate 很小,密钥管理简单,格式经过实战检验。CI 流水线在构建完成后用私钥签名产物,机器人端用预置的公钥验证。签名不通过的 release 直接拒绝安装,连解压都不会做。

签名覆盖的是整个产物目录——daemon 二进制、systemd unit 文件、配置模板、ML 模型文件。这意味着攻击者即使篡改了 GitHub Release 上的产物,没有私钥也无法伪造签名。机器人拉取的是 CDN 上的文件,但信任链不依赖 CDN,只依赖 minisign 签名。

更新通道:daemon 和 model 各走各的

Microduck 不只有一个更新通道。daemon 和 ML 策略模型是独立版本化的——它们几乎不会同时发布。daemon 大约每两三周一个 release(前期频繁,后期放缓到两三个月一次),模型走自己的节奏。

每个通道独立更新、独立回滚。模型更新不需要重启 daemon——robotd 在运行时热加载新的 ONNX 策略文件,切换观测归一化器,继续跑 50Hz 控制循环。这意味着你可以给鸭子换一个新训练的步态策略,它走路时切换,不用停机。

通道的配置是声明式的:每个机器人声明自己有哪些组件,通用引擎根据声明驱动更新。同一个引擎可以复用到 Pollen Robotics 的其他机器人上,只需要改配置文件。

传输:机器人自己拉,不走蓝牙

一个容易被忽视但很关键的决策:更新 payload 从不经过蓝牙。

蓝牙的 BLE 实际持续吞吐量大约 10-30 kB/s。一个 daemon 产物几十 MB,走蓝牙要传十几分钟;一个模型 bundle 可能上百 MB,走蓝牙要传几小时。这个瓶颈不是协议层的,是链路层的——连接间隔和 iOS 的限流策略决定了,再怎么优化 framing 也突破不了。

所以 Microduck 的更新流程是:手机 App 通过 BLE 发触发指令,机器人用自己的 WiFi 从 GitHub Releases(daemon)或 Hugging Face Hub(模型)拉取产物。大文件走 WiFi,BLE 只传控制信令和进度上报。

这里有个前置条件:机器人必须先完成 WiFi 配网才能更新。引导顺序是 BLE 配对 → WiFi 配网 → 更新。一个没配过网的机器人不能更新,但它仍然可以回滚——回滚操作只操作本地已有文件,不需要网络。

原子切换:symlink swap 的具体实现

更新的核心操作是切换 current 符号链接。这个过程必须是原子的——要么完全切到新版本,要么留在旧版本,不能停在中间。

具体步骤是这样的。updaterd 验证签名通过后,把新 release 目录放到目标位置。然后创建一个临时符号链接指向新目录,用 rename 系统调用把临时链接重命名为 current。在 Linux 上,rename 对符号链接是原子的——内核用 VFS 锁保证这个操作要么完成要么没发生,不会有中间状态。

切换完成后,updaterd 重启除自己之外的 systemd 服务(robotd、mediad、padd 等)。注意 updaterd 和 btd 被排除在重启列表外——updaterd 不能杀自己,btd 还在传回复给手机 App。这两个进程的延迟重启通过 systemd transient timer 实现,在回复发出 5 秒后触发。

NEVER_RESTART 列表硬编码在代码里,不放在配置文件中。这是个有意的设计决策:这两个进程的特殊性是它们”是什么”决定的,不是操作员的选择。早期设计把这个列表放在 /etc/robot/updater.toml 里,结果一个配网时间早于某个 daemon 存在时间的板子,配置里没有那个 daemon,更新后它从不重启,robotctl 报”client speaks API v4, daemon speaks v3″——新客户端跟旧 daemon 说话。这个 bug 后来通过把排除列表移到代码里解决了。

健康检查:更新不是装完就算

符号链接切换和服务重启完成后,updaterd 不直接宣布成功。它问 robotd:你健康吗?

robotd 的健康检查不是简单的”进程还活着”。它检查:控制循环是否在跑 50Hz(不是 43Hz,不是 0Hz)、舵机总线是否应答、IMU 是否在产出有效四元数、策略文件是否成功加载。如果任何一项不通过,updaterd 把 current 符号链接切回旧版本,重启服务。

这个健康门是自动回滚的触发条件。用户不需要判断”更新后鸭子走不动是不是因为更新坏了”——updaterd 自己判断,自己回滚。整个回滚过程不需要网络,因为旧 release 目录还在本地磁盘上。

如果 robotd 根本起不来——比如新版本有个 panic 在启动路径上——systemd 的 Restart=on-failure 会重试几次,然后放弃。这时候 boot counter 介入:连续启动失败的计数达到阈值后,系统自动回滚到上一个已知良好的 release。这个机制不依赖 updaterd 自己——如果 updaterd 也崩了,Engine::recover_on_start 在下次启动时检测到异常状态并执行回滚。

self-test:装新版本前先试跑

更新设计文档里有一个值得注意的机制:self_test_updaterd。

问题是这样的:不重启 updaterd 保护了正在执行的更新,但新版本的 updaterd 能不能正常工作?如果新版本有个启动时 crash,Restart=on-failure 重试几次后放弃,机器人就没有更新守护进程了——而此时更新已经提交,没有人在看。

self_test_updaterd 在服务重启后、健康门之前运行。它以只读模式启动新版本的 updaterd——加载配置、构造引擎、但在执行恢复逻辑之前退出。这个模式检查的是:架构对不对(ARM64 vs x86)、依赖库在不在、配置文件能不能解析、有没有立即 panic。如果 self-test 失败,更新回滚,成本很低,不需要重启。

这个 self-test 不是用现有的 --check-only 标志。--check-only 会执行真正的 recover_on_start,增加 boot counter,可能在更新过程中触发意外回滚。它是一个运维工具,不是更新流程里的探针。

staging 和 stable:双速通道

Microduck 内部迭代的速度远高于发布速度。原型阶段三天出了八个 tag。更新系统需要同时服务两个节奏:快速的内部 staging 通道和稳定的 stable 通道。

staging 通道面向开发板,接收每次构建的产物。stable 通道面向已发货的鸭子,只接收经过验证的 release。两个通道用同一个引擎,区别只在产物来源。

这意味着 scripted rollback(脚本化回滚)不是可选的锦上添花——它是日常工具。开发者在 staging 通道里频繁更新和回滚,测试新功能。确认稳定后,同一个产物进入 stable 通道,推到用户设备。回滚路径被每天走很多遍,所以它必须可靠。

最低版本地板:强制离开已知坏版本

有一个机制专门处理”已知坏版本”:minimum-version floor。

如果某个 release 发现了严重问题(比如会让舵机过热),Pollen Robotics 可以在 manifest 里提高最低版本要求。机器人在检查更新时,如果发现自己当前版本低于最低要求,会被强制升级——不是”建议更新”,是”必须更新”。

这个机制的存在意味着 Microduck 的更新不是纯粹的可选行为。严重安全问题的修复可以强制推送到所有设备。但这个权力只用于真正严重的问题——普通的 bug fix 不会触发强制升级。

对做硬件产品的人有什么启发

Microduck 的更新系统设计有几个点值得借鉴。

更新粒度要匹配实际变化频率。如果只有应用层在变,就不要搞全系统 A/B 分区。版本化目录加符号链接切换够用,简单且可靠。OS 更新走带外通道,不跟应用更新混在一起。

签名验证是不可妥协的。不管你的 CDN 多可信,产物在传输链路上必须被签名验证。minisign 是个务实的选择——比 GPG 简单,比自研可信。

健康门是自动回滚的前提。没有健康检查的更新系统等于没有回滚——因为你不知道什么时候该回滚。健康检查要检查具体的功能指标(50Hz 循环在跑?总线应答?策略加载?),不是”进程存在”。

更新守护进程必须独立于被更新的东西。updaterd 跟 robotd 是不同的 systemd unit,updaterd 不依赖 robotd。一个崩溃的 robotd 不应该阻止你回滚更新。

离线可恢复是底线。一个没有网络的机器人必须能回滚到本地已有的版本。回滚操作只动本地文件,不需要 manifest,不需要源站。这是”不能砖”的真正含义——不是”更新不会失败”,是”更新失败后设备仍然可恢复”。


数据来源:pollen-robotics/microduck GitHub 仓库 docs/design/updater-design.md(更新系统完整设计文档:A/B vs 应用层更新决策、minisign 签名、传输决策、原子 symlink swap、health gate 自动回滚、self_test_updaterd 机制、staging/stable 双通道、minimum-version floor、NEVER_RESTART 设计决策及历史 bug),docs/design/architecture.md(七守护进程架构与 updaterd 的职责边界),docs/robot/cheatsheet.md(robotctl version/health/update 命令)。

喜欢 (0)赏
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。