Microduck 系统架构
最后更新
Microduck 的机上软件是 Rust 写的,一个 workspace,没有框架。由 systemd 负责生命周期、崩溃重启、启动顺序和 watchdog。
理解这张图的价值在于排查:知道某个功能归谁管,就知道该看哪个进程的日志。
守护进程分工
robotd 转发——这也是为什么手柄原始流要在 padd 上看,而不是在机器人状态里找。| 服务 | 负责 | 说明 |
|---|---|---|
robotd |
电机控制、运动学、里程计、步态策略、传感器循环、安全 | 准实时核心,对一切可能伤到机器人的事情有最终权威 |
mediad |
摄像头/麦克风、编码、感知、WebRTC 与远程网关 | 最重的服务,同时是远程 API 的前门 |
updaterd |
更新引擎 | 安装签名过的发布版,机器人起来不健康时自动回滚 |
configd |
wifi、机器人身份、电源、手柄配对 | |
btd |
BLE GATT 服务端 | 纯传输适配器,不持有任何状态 |
padd |
读取手柄 | 刻意设计成非特权客户端 |
tofd |
头部 ToF 传感器(HAT I²C 总线上的 8×8 深度矩阵) | 只拥有一个传感器并发布帧,不读取任何东西 |
几个切分决策,以及为什么
这些不是随意划分的,每一条都有具体理由。
mediad 从 robotd 拆出去,是为了让媒体/感知的崩溃不会带走电机控制。
tofd 是同一条规则用在更小的传感器上,而具体细节让这个决定更站得住脚:
- 启动一个 VL53L5/8CX 需要通过 I²C 上传约 90 KB 固件,耗时数秒;
- 这条总线和音频编解码器共用;
- 大多数鸭子根本没装这个传感器。
为这种情况写一个重试循环,不该放在拥有电机的那个进程里。而且控制循环不读深度数据,所以移出去没有任何损失。
它也刻意不放进 mediad:深度是总线上的一个传感器,不是媒体管线;而且它在有摄像头可标注之前很久就有用了。
手柄配对放在 configd 而不是 padd,因为绑定一个设备需要 root 和 BlueZ,而 padd 被刻意设计成非特权客户端。
配置归 configd 自己管,因为它必须在 robotd 已经死掉时仍然可达,同时 btd 又不能拥有任何状态——所以它不归这两者中任何一个管。
进程间通信
控制平面:Unix socket 上的 JSON-RPC 2.0
所有守护进程通过 Unix socket 上的一套 JSON-RPC 契约通信。
这里有一条对使用者很重要的性质:
每一个客户端——手机 App、控制台、手柄、你自己写的脚本——发送的是完全相同的调用。
也就是说,你脚本能做的事和官方 App 能做的事,接口上没有区别。
控制平面与数据平面是分开的
数据平面(传感器流)不走控制平面。消费者订阅拥有该数据的那个守护进程自己的 socket——比如 ToF 数据订阅 tof.stream,手柄原始流订阅 padd——而不是绕经 robotd。
tofd 发布的是传感器视角的原始数据,不假装自己能算出它算不出的几何。要把一帧深度重投影到机器人自身坐标系,需要把它和 robot.state 里的关节状态、以及 kinematics crate 的头部正运动学结合起来。
版本与健康:两个不同的问题
官方设计文档里有一节专门讲这个,值得单独记住:
「正在运行的版本」和「已安装的版本」是两个不同的问题。 这就是为什么 robotctl version 会同时报告两者并在不一致时警告——更新之后某个守护进程仍在跑旧代码,看起来和你刚修的 bug 没修好一模一样。
健康是一个问题,所以它是一条命令。 robotctl health 把硬件和软件放在一份报告里,而不是让你去分别查七个进程。
里程计不是一个服务
里程计是控制循环里的一个结构体,不是独立服务。理由很直接:它的输入恰好就是循环已经读到的那一份采样。
这也解释了 robotctl monitor 里那张轨迹图的性质:它来自足底触地和 IMU,没有磁力计,所以是相对运动且会漂移。它能回答“它是不是走了一圈”,回答不了“它现在在哪”。
相关
- robotctl 命令速查
- 官方资料入口——包含设计文档目录,里面有每个部分的单独页面