解读 69 · 阶段 E:舵机协议适配的代码地图
解读对象:
docs/HL-2915路线全流程教程.md(380 行)§6 代码地图,逐项核对duck-control/、robotd/、updater/真实源码
需要的前置:解读 01(官方总线怎么跑 50Hz)、解读 07(RobotIo 的接缝设计)
阶段 E 是全教程唯一"纯代码"的阶段(¥0,预估 3–5 天)。教程 §6 预想的工作是:新写一个 Feetech2915Io 替掉 DynamixelIo,让官方 50Hz 控制环原样跑在飞特舵机上。本篇逐项把代码地图放到仓库里核对时发现了一个比教程更有价值的事实:适配已经落地——duck-control/src/hl2915.rs(620 行)的 Hl2915RobotIo 已实现 RobotIo,robotd 已按 BusBackend::Hl2915 选择后端。教程停在"待写",代码已先行。
1. 接缝不动:RobotIo 的真实接口
duck-control/src/io.rs(351 行)定义了控制循环与硬件的全部契约:RobotIo trait 六个必选方法——read() -> Sensors(io.rs:115,关节+IMU 一次拿全)、write(&JointTargets)(:116)、set_gain(kp: u16)(:124)、set_torque(on: bool)(:135)、reboot(id: u8)(:143)、slow_sensors() -> SlowSensors(:151)——外加两个默认实现的自诊断 imu_stale()(:159)/imu_ready()(:164)。教程的批注:"不动——这是整条接缝"。
这个"不动"是有内容的:IoError::ShortRead { what, expected, got } 规定"少读一个块"必须报错而不是静默用旧值;set_torque 的文档写着启动时没有任何人调用它——robotd 被更新重启时,站着的鸭子必须还站着。hl2915.rs 能干净接入,正是因为它继承的是这套语义。
2. 代码地图逐项核对
| 教程 §6.1 说 | 仓库核实 |
|---|---|
bus.rs 现状:rustypot Xl330Controller,读块 124/12,慢速 144/3 |
✅ bus.rs(708 行):READ_ADDR=124/READ_LEN=12,SLOW_READ_ADDR=144/SLOW_READ_LEN=3 |
新增 Feetech2915Io:读块 56 起 14 字节,写目标 42(+46 限速) |
✅ 已落地:duck-control/src/hl2915.rs(620 行),Hl2915RobotIo(:200)在 :334 impl RobotIo;Hl2915Bus(:189)管半双工收发;ID 表 DEFAULT_HL2915_IDS 由 robotd 传入(main.rs:39)。寄存器号与 hl2915_bench.py 完全一致(REG_POS=56 起 14 字节遥测块,REG_GOAL=42,REG_SPEED=46)——台架脚本就是它的协议测试向量 |
imu.rs 换 I²C 读 LSM6DSV16X,ImuData 结构和朝向不变 |
✅ 现文件从总线 12 字节块解码(SflpDecoder);ImuData/DEFAULT_MOUNT 正是"不变"的部分 |
model.rs 电池窗 6.6–8.2V → 9–14V(62 号遥测 0.1V);ID 表沿用 |
✅ BATTERY_FULL_V=8.2/BATTERY_EMPTY_V=6.6 待随 3S 方案调整;JOINT_IDS 的 10–14/20–24/30–34 沿用 |
robotd/src/main.rs 的 DynamixelIo::open 按配置选 Io |
✅ 已有双后端:BusIo 枚举含 Hl2915 变体(:1160),open_bus_for 按 BusBackend::Hl2915 => Hl2915RobotIo::open(port, &DEFAULT_HL2915_IDS, model::IMU_DXL_ID, …) 分支(main.rs:1301–1306) |
策略清单 hw_rev=1/servos="xl330" 是显示+门控字段,校验只强制 obs 61/action 14/型号 |
✅ updater/src/policy.rs:702 的真实清单样例带这两个字段;incompatibility() 只查 obs_len/action_len/model_api/robot.model——换舵机不换型号字段,旧清单也不会被误拒 |
3. 每步可回退的五步,与真实落地的对照
§6.2 预想的顺序本身就是风险控制:①把 hl2915_bench.py 的 packet/parse/transact 翻译成 Rust(serialport crate 开 1Mbps 8N1);②单测——用协议手册的示例帧当测试向量(PING/READ/WRITE/SYNC 各一条),cargo test 不需要硬件;③只接 2 只舵机,50Hz 跑 10 分钟零 ShortRead;④HOME 缓动——robotd init 同款插值到 DEFAULT_POSITION(参照 DynamixelIo::interpolate_to);⑤IMU 的 I²C 路径接进 read(),过阶段 B 的五步验证。读 hl2915.rs 可以逐条对出这五步的落点:帧层、单测、遥测块解码、ID 注入、read() 聚合——教程的方法论和代码是一一对应的,这正是"代码地图"的正确用法。
一个要留神的真实差异:set_gain 签名是 kp: u16(XL330 的位置环 P 是 16 位),而 HL-2915 的 21 号 P 增益是一字节 0–255(hl2915_bench.py 课 6 明确拒绝超范围值)——映射要显式处理,不能隐式截断。
验收(§6.3):cargo test 全绿;2 舵机+IMU 50Hz 跑 10 分钟丢读 0;set_torque(false) 立刻失力、true 恢复;拔掉一只舵机的线,得到明确的 ShortRead 而非静默旧值。
你带走的收获
- 换硬件驱动先找"接缝":
RobotIo这类 trait 是现成的替换点;接缝不动,上游的控制循环、安全层全部复用——hl2915.rs620 行就吃下了整条新舵机路线。 - 代码地图先核实再动手:教程说"待写"、仓库里其实已经有了——文档滞后于代码是常态,以仓库为准是铁律。
- 枚举即开关:
BusBackend::Hl2915/Dynamixel两个变体让两条舵机路线共存于同一个守护进程,配置切换而不是改代码。 - 测试向量来自协议手册与台架脚本,不是想象:bench 的
packet/parse就是 Rust 实现的单测向量。 - 错误语义是接口的一部分:
ShortRead报错、启动不写扭矩——这些"看不见的行为"比方法签名更容易丢。