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

Microduck 软件架构深度拆解:七守护进程设计、安全层与 A/B 更新系统

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

架构总览:七守护进程,一个核心原则

从官方架构文档可以看到完整的进程拓扑图。七个 daemon 分别是:

  • robotd:唯一碰硬件的进程。50Hz 控制循环独占 Dynamixel 总线,15 个舵机和 IMU 在单次 sync_read 里全部读完。安全层拦截危险指令。
  • configd:管理 WiFi、机器人名称、配对 PIN、手柄绑定。和 robotd 无 systemd 依赖关系。
  • updaterd:发布版管理。签名验证→安装→健康检查→不健康自动回滚。
  • btd:蓝牙传输层,把 BLE GATT 服务转成 JSON-RPC 调用。
  • padd:手柄输入层,读取游戏手柄并发送意图。
  • mediad:摄像头/音频/WebRTC 传输层,也是远程 API 的入口。
  • tofd:ToF 深度传感器守护进程,发布 8×8 深度矩阵,只发布不读取。

还有 robotctl——CLI 工具,连接所有 socket,是断网断服务时的最后操作入口。

核心设计原则只有一条:“如果 robotd 死了,机器人必须还能被救回来。”

第一个决策:控制循环必须是唯一的”碰硬件者”

在 microduck 的设计中,robotd 是唯一能命令舵机的进程。其他所有组件——手柄、手机 app、远程连接——都只能发”意图”(intent),不能直接发舵机指令。

  • 手柄按了”前进”键 → padd 发送 robot.move 通知
  • robotd 收到 → 经过安全层检查关节限位、摔倒检测、温度阈值 → 加载 ONNX 策略 → 计算 15 个关节目标 → 写入 Dynamixel 总线

客户端发的是”我想干什么”,robotd 决定”实际能干什么”。安全层在 robotd 内部,不走网络、不走 IPC,任何绕过尝试都会被拒绝。

这个设计不是偶然。从官方的 robotd-design.md 可以看到,robotd 的 crate 分层非常清晰:

  • duck-ipc-proto/:wire 契约,只有 serde,没有 tokio 和 http
  • duck-control/:机器人模型、总线通信、IMU、观测、策略、安全——一切在读总线和写总线之间的逻辑
  • robotd/:进程层,socket、JSON-RPC、systemd、健康报告

核心洞察:duck-control 不依赖任何网络库或 systemd。这意味着它可以独立测试、独立模拟、甚至独立部署在别的硬件上。Pollen 在设计中反复强调的 crate boundary 就是这个意思——把”控制”和”进程”拆开,前者不碰后者。

第二个决策:三只守护进程必须能在 robotd 挂了以后继续工作

官方架构文档专门用了一个章节讲这条规则。

configd、updaterd、btd 没有对 robotd 的 systemd 依赖。没有 ML 运行时,没有媒体栈。它们必须在一个控制循环已经崩掉的机器人上正常工作。

为什么?因为”控制循环起不来的机器人,恰好是需要配网、更新或回滚的机器人。”

所以 WiFi 配置在 configd 里而不在 robotd 里——当鸭子摔断了腿起不来,你还能通过蓝牙给它配 WiFi、下更新。如果配网功能绑定在 robotd 上,那控制循环崩了就连不上网,彻底变砖。

这条规则在 updaterd 的设计文档里也有体现:updaterd 是独立于它更新的目标的独立 unit。一个进程不能干净地替换自己正在运行的程序,而且更新程序必须能在守护进程崩溃后执行回滚。

第三个决策:更新系统用全目录原子交换,不搞 OTA 增量

Microduck 的更新系统设计值得单独拿出来讲。

不是传统的 OTA 分区方案(A/B image),而是应用级别的更新:

  • 每个版本是一个完整目录:/opt/robot/daemon/releases/v1.2.3/
  • current 是一个符号链接
  • updaterd 做”验证签名→解包→移动 current→重启 unit→健康检查”
  • 健康检查失败?把 current 指回旧版本

整个设计文档(updater-design.md)对为什么不做 A/B 分区有明确的理由:只有 daemon 和模型会变,OS 是静态的。RAUC/Mender 那套方案在这个场景下属于过度工程。

签名方案用的是 minisign——一个经过实战检验的格式,验证 crate 极小,密钥管理简单。CI 构建完自动签名,机器人端验证。

更新来源也不是自建后端:daemon 走 GitHub Releases,模型走 Hugging Face Hub。零后端维护。

但这里有个容易被忽视的前提:机器人必须有自己的网络。 BLE 带宽撑不起几百 KB 的模型更新(实测 iOS 下的 BLE 吞吐只有 10-30 KB/s),所以配网是更新的前置条件。没有网络的机器人,能回滚但不能升级——updaterd 设计文档第 3.1 节专门标注了这条边界。

第四个决策:IPC 用 JSON-RPC 而不是自定义协议

七个进程怎么通信?Pollen 团队的选择是 JSON-RPC 2.0 over Unix socket,每行一个 JSON 对象(NDJSON)。

为什么不用更高效的方案?官方设计文档记录了他们评估的选项:

  • JSON-RPC/NDJSON + tokio(30 依赖)→ 选中
  • jsonrpsee-types 仅类型(36 依赖)→ 合理但依赖 0.x 版本
  • varlink(24 依赖)→ 精神相近但不够熟悉
  • zbus(D-Bus p2p)(14 依赖)→ 非异步,ARM 上已知问题

关键参数:ARM Linux 目标、可维护性、团队熟悉度。JSON-RPC 是标准协议——标准请求/响应关联、标准错误对象、标准通知(Notification),正好适合推进度和事件流。

消息类型是 plain serde struct,用 tokio_util::codec::LinesCodec 做帧分割。所有 IPC 都带超时——任何对端都可能已经死了,关闭或静默的 socket 是正常情况,不是异常。

一个有趣的设计细节:两套流量路径完全分离。

  • 控制面:命令、配置、状态、感知事件,几十字节,≤100Hz → Unix socket RPC
  • 数据面:视频/音频帧,640×480 RGB @30fps 约 27 MB/s → 从不跨 socket

数据面走的是 mediad 的 TCP 端口(:8080 控制台,:8443 信令),不走 Unix socket。因为视频流一旦走 RPC,那个 50Hz 的控制循环就等着被阻塞了。

第五个决策:状态所有权不能模糊

架构文档里有一个表格专门标注每份状态的归属:

  • /etc/robot/robotd.toml → 每板配置,安装器写一次后永不覆盖 → 归属 robotd + mediad(只读 [media] 段)
  • /var/lib/robot/config/config.json → 机器名+配对PIN,文件+flock → 归属 configd
  • NetworkManager profiles → WiFi 凭据 → 归属 system(不存别处)
  • /opt/robot/daemon/releases/<ver>/ → 二进制+策略+默认配置 → 归属 updaterd
  • /opt/robot/daemon/current → 当前版本符号链接 → 归属 updaterd
  • /run/<service>/identity.json → 每个 daemon 实际运行的版本 → 归属各自

规则很简单:releases/<ver>/ 之外的一切在更新和回滚后都存活。所以每板配置不随发布版一起发布——否则回滚一个配置改了就是大麻烦。

对复刻的启示

如果正在复刻 Microduck,这套架构能直接借鉴什么?

1. 进程隔离原则:控制循环和配置管理必须分开。如果你用国产舵机,那协议适配层应该作为一个独立 crate,不影响上层控制逻辑。

2. JSON-RPC 作为 IPC 标准:不需要造轮子。在 RK3566 上用 JSON-RPC + Unix socket 是经过验证的方案,30 个 crate 依赖搞定。

3. A/B 回滚的最小实现:目录级原子交换 + 健康检查 + 启动计数器,代码量不大但救急能力强。

4. 安全层内嵌:不要让远程客户端有直接控制舵机的路径。所有意图经过安全层过滤——这对双足机器人尤其关键,一个错误的指令就能让它摔倒。

5. IMU 挂在 Dynamixel 总线上:不是所有机器人都这么做,但 Microduck 的做法——IMU 作为第 16 个设备挂在舵机总线——简化了同步问题。一次 sync_read 读回所有传感器和执行器状态,控制循环的观测空间天然一致。

从 duck.whatled.com 的复刻进度看,Rust 运行时已经在走协议适配,七守护进程的架构可以复用大部分设计,只是底层的 Dynamixel 协议需要替换成飞特协议。好消息是协议替换不会影响上层设计——robotd 的 duck-control crate 把总线通信抽象成了 DynamixelIo trait,换协议只需要实现这个 trait 就行了。

数据来源:pollen-robotics/microduck 官方仓库 architecture.md、robotd-design.md、updater-design.md(2026-07-22/08-20 版本)、pollen-robotics/microduck GitHub README、duck.whatled.com 复刻进度。

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