第 3 章 进程间通信:Unix socket 上的 JSON-RPC 契约
本章代码走读:
duck-ipc-proto/src/lib.rs(约 6200 行)——Id、Call、Service三个类型撑起整个通信体系。
3.1 一个 crate 防一种死法
duck-ipc-proto 的存在理由写在 Call 枚举的文档注释里:
/// A method together with its parameters.
///
/// Every request is built from one of these and read back as one, so a method can never be
/// paired with another method's parameters — the drift this crate exists to prevent.
#[derive(Debug, Clone, PartialEq)]
pub enum Call {
/// Version handshake. The first call on a connection.
Hello(HelloParams),
...
方法与参数在类型层面绑定:不存在"方法名字符串对了、参数结构旧了"的请求。这是"契约即代码"的 Rust 表达——协议演化时,编译器替你找出所有没跟上的人。
3.2 代码走读:Call 枚举——整个系统的 API 表
约 100 个变体按命名空间分组,注释里直接标注了每个调用的节奏类型(这对理解系统时序至关重要):
// ── intents ──────────────────────────────────────────────────────────────
/// Continuous. Send as a notification.
RobotMove(MoveParams),
/// Continuous. Send as a notification.
RobotHead(HeadParams),
/// Discrete. Send as a request; the answer is [`LookResult`].
RobotLook(LookParams),
RobotStop,
RobotEnable(EnableParams),
/// Power the joints and ramp to the home pose. No policy needed.
RobotInit,
/// Cut power to the joints. The robot collapses if nothing holds it.
RobotRelax,
/// Reboot servos (all of them, or the ids named), then limp. See [`method::ROBOT_REBOOT_MOTORS`].
RobotRebootMotors(RebootMotorsParams),
/// Run a one-shot skill, or toggle sit↔stand.
RobotDo(DoParams),
/// Standing body pose. Continuous. Send as a notification.
RobotPose(PoseParams),
/// Mouth opening. Continuous. Send as a notification.
RobotMouth(MouthParams),
连续调用是通知(notification),离散调用才是请求(request)——50 Hz 的遥操作指令走 fire-and-forget,丢一帧无所谓(下一帧 20 ms 后就到);配对、安装、配置这类低频高语义操作才需要应答。这个区分直接决定了 Id 的设计:
/// Request identifier. `None` on a [`Request`] makes it a notification.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Id {
Number(u64),
Text(String),
}
命名空间的完整清单(每组的语义):
| 命名空间 | 职责 | 代表调用 |
|---|---|---|
update.* |
组件更新与回滚 | Check Apply Rollback ResetToGolden Select Pin Subscribe |
robot.* |
运动意图与生命周期 | RobotMove RobotDo RobotInit RobotRelax RobotPolicies |
policy.* |
策略库管理 | PolicyCheck PolicyInstall PolicyFetch PolicySearch |
detector.* |
视觉检测模型 | DetectorCheck DetectorInstall |
account.* |
设备账号 | AccountLogin AccountStatus AccountLogout |
net.* |
Wi-Fi | NetStatus NetScan NetConnect NetForget |
system.* |
系统信息与安全 | SystemInfo SystemLogs SystemPairingPin SystemAuthenticate |
pad.* |
手柄 | PadPair PadBindings PadInput |
| 流订阅 | 传感器/媒体流 | TofStream HeadImuStream |
注意 TofStream 的注释:"Subscribe to the ToF depth stream. Answered by tofd."——谁应答写在契约里,路由不是猜的。
3.3 代码走读:Service 枚举——没有中间人的路由表
/// The service that owns the answer to a call.
///
/// One socket per service, connected directly — there is no broker (`architecture.md` §2.2). A
/// transport adapter holds connections to the services whose calls it carries, and to no others:
/// `btd` holds three, and `padd` being absent from them is deliberate rather than incidental —
/// `padd` is the unprivileged client whose whole value is having no special access.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Service {
/// `updaterd`, at [`DEFAULT_SOCKET`].
...
三个决策一目了然:
- 每服务一个 socket,直连,无 broker。没有 DBus、没有消息中间件——每少一个组件就少一类故障与一个依赖。
- 传输适配器只持有它需要的连接。最小权限不是口号:
btd(蓝牙桥)只连三个服务。 padd的"无特权"是被设计出来的。手柄服务故意不持有任何特殊连接——它的全部价值就是"没有任何特殊访问"。攻击面管理进了类型系统。
3.4 代码走读:配对 PIN——一条无法按 BLE 规格实现的需求
配对认证是这个系统里最精巧的一段协议设计。SystemPairingPin:
/// Read the pairing PIN.
///
/// Exists so `btd` can answer a BLE passkey request without owning config. It must never be
/// routed to BLE — a PIN an unpaired peer can read authorises nothing — and `btd`'s routing
/// table has a test saying so.
SystemPairingPin,
SystemAuthenticate:
/// Prove knowledge of the robot's pairing PIN.
///
/// Answered by the **transport** rather than by any service, which makes it unlike every
/// other call here. BLE cannot express a fixed, printed-on-the-robot passkey — the spec has
/// the *displaying* side generate a random one, and a headless robot can display nothing — so
/// the PIN check moved from the link layer to this one, where we define the rules. See
/// `docs/design/app-path-design.md` §5.
SystemAuthenticate(AuthenticateParams),
拆开看:BLE 规格假定配对双方至少一方有屏幕(显示侧生成随机码);一台无头机器人既不能显示也不能遵循该模型,于是团队把 PIN 校验从链路层上移到应用协议层,并配套两条纪律——PIN 读取调用永不被路由回 BLE(否则未配对端读走 PIN,认证形同虚设),且这条禁令由 btd 路由表上的一个测试看守。又是第 2 章那招:"两处必须同步/禁止的事,用机制不用记忆。"
3.5 鸭群合唱:通知的三方协作
chorale(合唱)功能让多只鸭子互相听见、同步鸣叫,它的协议是三个调用的小型状态机,注释把方向讲得极清楚:
/// `btd` subscribing to what it should advertise. Answered, then a stream of
/// [`method::CHORALE_BEACON`] notifications.
ChoraleSubscribe,
/// `robotd` telling `btd` what to advertise. A notification.
ChoraleBeaconSet(ChoraleAdvertise),
/// `btd` telling `robotd` what it heard. A notification.
ChoraleHeard(ChoraleHeard),
robotd 决定广播什么 → btd 通过无线电发出去并听别的鸭子 → 听到的内容回流给 robotd。感知-决策-通信的闭环全部走同一套 JSON-RPC 通知机制,没有为"多机协作"新造任何基础设施。
3.6 为什么不是 gRPC / DDS
v0.1 讨论过候选方案的表格,现在可以补上实证:Call 枚举里连续调用是通知、Service 直连无 broker、PIN 认证在传输层应答——这三个需求在 gRPC/DDS 里都要绕着框架做。而 6200 行的 duck-ipc-proto 用纯类型就把"方法-参数绑定、服务归属、节奏类型、路由禁区"全部编码完毕,serde 序列化后即为线上格式,jq/nc 即可调试。当你的 IPC 语义已经复杂到需要"契约"时,把契约做成类型比做成配置更有利。
3.7 本章小结
duck-ipc-proto 用三个枚举(Id、Call、Service)回答了 IPC 的全部四个问题:谁能被调用(方法表)、带什么参数(类型绑定)、谁应答(服务归属)、以什么节奏(通知 vs 请求)。第 4 章进入这些调用的最大消费者——robotd 的 50 Hz 控制循环。