hex-motor
针对 HEX-MECHA / HexMeow CiA402 形态电机的 Rust 驱动库。
通信层基于 can-transport,与
具体的 CAN 后端(socketcan / pcan / vcan / 自定义)解耦。
[]
= "0.1"
= { = "0.1", = ["socketcan"] }
= { = "1", = ["full"] }
v0.1 状态:仅支持 CiA402 形态电机,仅覆盖 "上位机交互" 这一种使用 范式(动态发现 / SDO 配置 / 周期 TPDO 读状态 / SDO 控制)。不面向 1 kHz 硬实时 RPDO 控制环;如需后者请走 sans-IO 的下层 crate 自行实装。
这个库帮你做什么
- 被动发现 — 你不需要预先知道总线上有哪些电机,谁开机我们就识别谁
(读
0x1018Identity + 可选0x1008Manufacturer Device Name)。 - 生命周期管理 — 每个节点有 5 态
Unknown → Identified → Initializing → Initialized → NeedsReinit,加上正交的online,可以一眼分辨"还没 握上手"和"通了但掉线了"。 initialize()一把梭 —NMT PreOp → 配 TPDO1+TPDO2 → 配 0x1016 心跳消费者 → NMT Operational,失败自动回退 lifecycle,可重试。- CiA402 控制字 ramp —
set_mode(ProfileVelocity)这种调用会自动跑 完保险路径 ((可选 fault reset) → CW=0x06 → 0x6060=M → CW=0x06 → 0x07 → 0x0F),并等 TPDO 反馈状态字确认Operation Enabled之后才返回。 - 零阻塞的状态读取 —
status(nid)是无锁ArcSwapfast path;要 连续流则用subscribe_status(nid, opts)拿一个 mpsc 流,channel 满时 按OverflowPolicy::Lagged给丢弃计数,不会阻塞主循环。 - 出向心跳广播 — Manager 持续广播 NMT Operational 心跳,电机侧
0x1016监听超时会自动触发安全保护;上位机崩了电机自动短刹车。
30 秒上手
use Arc;
use Duration;
use ;
use ;
use ;
async
概念速览
生命周期
| Lifecycle | 含义 | 可控制? |
|---|---|---|
Unknown |
看到了心跳,0x1018 还没读到 |
否 |
Identified |
identity 已知,TPDO 还没配 | 否 |
Initializing |
initialize() 正在跑 |
否 |
Initialized |
TPDO 在流、心跳消费者已设、NMT Op | 是 |
NeedsReinit { reason } |
曾经 Initialized,电机离开了 Operational | 否,需 re-init |
online 是正交的布尔:最近一段时间内是否收到过 HB 或 TPDO。
MotorInfo::is_ready() 等价 lifecycle == Initialized && online。
两条数据通路
- 状态上行(电机 → 我们):HB / TPDO 帧 → 全局 listener 解码 → 写
MotorEntry内部字段 →publish原子刷新ArcSwap<LiveState>+ 推 给所有订阅者。 - 控制下行(我们 → 电机):用户调
set_mode/set_target/ ... → Manager 拼出Vec<SdoWrite>序列 → 顺序await canopen-sdo::download→ 必要时再轮询entry.logic等 TPDO 反馈 → 返回Result。
核心 API
// 构造(每条 CAN 线一个 manager)
let mgr = new?;
// 列表 / 事件
mgr.list ;
mgr.subscribe_events ; // NodeAppeared / Identified /
// Initialized / NodeOnline / NodeOffline /
// NeedsReinit / EnteredError / ...
// 显式触发
mgr.identify.await?; // 强制重读 0x1018
mgr.initialize.await?; // 完整 init 序列
mgr.initialize_all.await; // 对所有 Identified 节点并发
// 控制(必须 lifecycle == Initialized)
mgr.set_mode.await?;
mgr.set_target.await?;
mgr.disable.await?;
mgr.clear_error.await?;
// 状态读取
let snap: LiveState = mgr.status; // ArcSwap 无锁快照
let stream = mgr.subscribe_status?; // mpsc 流,per-subscriber 容量
v0.1 控制范围(HexMeow CiA402 电机约定)
| 模式 | set_mode |
set_target(...) 行为 |
|---|---|---|
ProfilePosition (1) |
✅ | 写 0x607A f32 Rev → CW=0x2F → CW=0x3F(CSI: Change Set Immediately 上升沿) |
ProfileVelocity (3) |
✅ | 写 0x60FF f32 Rev/s |
Torque (4) |
✅ | 写 0x6071 i16 = round(nm / peak * 1000),clamp 到 ±1000 ‰ |
Mit (5) |
✅ | 写 0x2003:01 pos (f32) / :02 vel (f32) / :03 tor (f32) / :04 kp_int (u16) / :05 kd_int (u16) —— uncompressed 形态,5 条 SDO |
任意 → Disable |
— | 0x6040 = 0x06,任意模式都合法 |
模式 / target enum variant 不匹配会得到 Error::TargetModeMismatch。
Torquetarget 需要initialize()时0x6076(Motor Peak Torque, REAL32 mNm) 能读到 —— 缺失会Error::Internal("peak_torque not cached")。Mittarget 需要0x2003:07(MIT KP/KD Factor, REAL32) 能读到 —— 缺失会Error::Internal("mit_kp_kd_factor not cached")。- 物理 Kp [Nm/Rev] 与 OD u16 值的关系:
kp_int = round(kp_phys / factor)。 initialize()还会写一次0x2003:06 = 1000,把 MIT 的 PD 输出力矩 上限放到 ±100% peak(不写默认 0 → kp/kd 无效)。
- 物理 Kp [Nm/Rev] 与 OD u16 值的关系:
ProfilePosition默认走 Change Set Immediately:每次set_target立刻替换当前目标,不等当前 motion profile 跑完。如果想要"打表"行为 (等当前完成再切下一个)请直接走 SDO 自行设置控制字 bit 5 = 0。
真机 examples
按里程碑顺序,每个都附 cargo run --example ... -- can0 0x10 0x21 的
用法(第一个参数是 CAN interface 名,第二个是我方心跳 nid,第三个是
目标电机 nid):
| Example | 验证内容 |
|---|---|
m1_heartbeat |
出向心跳广播 |
m1_sdo_identity |
单节点 SDO 读 0x1018 + 友好名称 |
m2_discover |
被动发现 + 自动 identify + online/offline 事件 |
m3_initialize_and_read |
完整 initialize() + 双路 TPDO 在流 |
m4_run_profile_velocity |
set_mode(PV) + set_target(Velocity) 真转 |
m5_subscribe_to_csv |
status() 无锁快照 + subscribe_status → CSV |
每个 example 的开头有详细的注释和参数说明。
CAN 后端:SocketCAN 或 gs_usb (CAN-FD)
第一个参数(interface 名)决定后端:
can0/vcan0等 —— Linux SocketCAN。gs_usb/gs_usb0/gs_usb1—— gs_usb / candleLight 适配器(USB 直连,CAN-FD,Windows / macOS / Linux 通用)。结尾数字选多路适配器 的通道(can0= 0)。
# 同一个 example,换成 gs_usb 适配器跑(电机在节点 0x01):
Linux 上 gs_usb 需要 usbfs 访问权限(sudo 或 udev 规则),后端会自动
卸载内核 gs_usb 驱动;详见 can-transport 的 README。
添加新电机型号
src/cia402/known_devices.rs 里的 KNOWN_DEVICES 是一个 const &[KnownDevice],
直接编辑追加一条即可:
KnownDevice ,
MotorInfo::friendly_name() 的优先级:
- 电机自己上报的
0x1008Manufacturer Device Name(非空且非纯空白) - 查
KNOWN_DEVICES:精确匹配(vendor, product)> 同 vendor 兜底 - 都没命中 →
"Unknown CiA402 device (vendor 0x..., product 0x...)"
默认 TPDO 映射
initialize() 默认配两路 TPDO(你也可以拿
default_tpdo1_recipe(nid) / default_tpdo2_recipe(nid) 自行调用底层 API):
- TPDO1(高速 1 ms,COB-ID
0x180 + nid,12 字节):0x6064(32) +0x1013(32, timestamp) +0x6077(16, torque) +0x603F(16, error) - TPDO2(低速 20 ms,COB-ID
0x280 + nid,10 字节):0x6041(16) +0x2204:01(16, drv_temp) +0x2204:02(16, motor_temp) +0x6040(16, ctrl readback) +0x603F(16, error)
两路加起来都 > 8 B,需要 CAN-FD。0x1013 / 0x2204:01-02 是 vendor-specific 字段,
标准 CiA402 不保证;其他厂家的电机请提供自定义 recipe。
限制与未支持
- 没有 RPDO:所有控制走 SDO。如需 1 kHz+ 闭环请用 sans-IO 层自己写。
- NMT 只切换一次:
initialize()里 PreOp → Op 之后就不再动 NMT。 - 没有自定义协议:HexMeow 的 0x3000 自定义 OD 形态在 v0.1 之外,由 独立 manager 在后续版本提供。
drop(manager)不发 NMT Stop —— 出向心跳一断,电机端0x1016监听 超时(默认 250 ms)后自动触发安全保护。
许可证
双协议:MIT OR Apache-2.0,任选其一。