- Upstream hardware + C++ reference: enactic/openarm_can
- How to integrate any new arm: adding_a_custom_arm.md
Why this integration is different
Every other arm in dimos wraps a vendor Python SDK:
OpenArm ships no Python SDK. The only interface is raw CAN frames on the wire, speaking the Damiao MIT-mode protocol. So dimos includes a from-scratch driver that encodes/decodes the protocol directly on a SocketCAN bus. The reference implementation is the Enactic C++ library at enactic/openarm_can — we port the frame layout from there.
Architecture
Quick start
You need:- 2× OpenArm v10 arms, wired to USB-CAN adapters
- 2× USB-CAN adapters (we used gs_usb family, VID:PID
1d50:606f, e.g. CANable 2.0). Classical CAN @ 1 Mbit is enough; CAN-FD not required - Python 3.12 venv with dimos installed plus
python-can >= 4.3andpinocchio - sudo on first run (to bring up the CAN interfaces)
1. Bring up the CAN buses
sudo ... openarm_can_up.sh can0.
Troubleshooting:
Operation not permitted→ you forgotsudo.Operation not supportedonfd on→ your adapter doesn’t support CAN-FD. The script defaults to classical, so this shouldn’t happen unless you setMODE=fd.- Only one
can*interface appears → the other adapter isn’t enumerating. On gs_usb boards, the blue LED indicates USB enumeration. If one adapter only shows red/green, swap the USB cable (many USB-C cables are charge-only).
2. Verify all 16 motors are alive
8/8 motors replied on each bus, with plausible joint positions and rotor temps around 25–30 °C.
3. (First time only) Put motors in MIT mode
Damiao motors have a persistentCTRL_MODE register. They ship in POS_VEL mode by default, which means they will reply to enable/state queries but silently ignore any MIT control frames — the “motor doesn’t move, error grows” failure. The adapter writes MIT on every connect() by default, so this step is usually automatic. If you want to set it explicitly once:
4. Run a blueprint
Safety before hot-plugging hardware: hold the arms before starting. On connect, the adapter enables all motors and sends gravity-comp holds — the arms go slightly stiff but don’t leap. Ctrl-C to cleanly disable and exit.
First-time recommendation: mock planner to verify everything wires up, then real single-arm, then bimanual.
5. Drive the arms from the manipulation client
Withopenarm-planner-coordinator running in one terminal, open a second terminal and start the REPL client:
Example session — simple joint moves
skip
plan() returns True on success, False if planning failed (check the coordinator terminal for COLLISION_AT_GOAL, INVALID_START, NO_SOLUTION, etc). The and chaining is an idiom — if any step fails, the next one is short-circuited.
If you ever get stuck in a FAULT state (e.g. an invalid plan was sent), reset the state machine:
skip
Example session — bimanual
skip
Example session — Cartesian target
skip
plan_pose will fail with NO_SOLUTION if the IK can’t find a configuration reaching the target.
Adding obstacles
skip
Configuration
Which CAN bus is which arm
Linux assignscan0/can1 in USB-enumeration order, which isn’t guaranteed stable across reboots or cable swaps. If the arms come up “swapped” (commanding left_arm moves the physical right arm), flip these two constants in config.py:
Gain tuning (MIT kp/kd)
Defaults live in adapter.py. Gains are per-joint because the shoulder motors (DM8006, 40 Nm) tolerate higher kp than the wrist motors (DM4310, 10 Nm):kp ∈ [0, 500]in MIT mode. Higher kp = stiffer position tracking; too high → oscillation.kd ∈ [0, 5]. Higher kd = more damping, but values above ~2 on these gearboxes cause high-frequency buzz/grinding.- Gravity compensation is on by default (
gravity_comp=True) — the adapter uses Pinocchio to computeG(q)and adds it as feedforward torque. This removes the need for very high kp to fight gravity, so prefer low kp + gravity comp over high kp.
Physical joint limits
The URDFs use the xacro-generated limits (which include per-side offsets for mirroring). The adapter’sget_limits() reports the same per-side limits. If you measure tighter physical limits and want to enforce them, edit the URDFs directly — the planner will respect them.
Disabling auto MIT-mode write
The adapter writesCTRL_MODE=MIT to every motor at connect(). It’s idempotent (writing the same value is a no-op), so this is safe to leave on. To verify that a previous write persisted across a power cycle, flip AUTO_SET_MIT_MODE = False in config.py and restart — the arms should still respond.
Motor mapping (OpenArm v10)
Derived from the URDF’sjoint_limits.yaml (effort column) cross-checked against the Damiao torque tables. Both arms are identical.
Convention:
recv_id = send_id | 0x10.
Damiao protocol essentials
Ported fromenactic/openarm_can/src/openarm/damiao_motor/dm_motor_control.cpp. You shouldn’t need these unless you’re modifying the driver.
Enable / disable / zero-position
Send to the motor’s send_id. 8-byte payload:MIT control frame (8 bytes)
Bit layout:q[16] | dq[12] | kp[12] | kd[12] | tau[12]. Each float quantized via:
kp ∈ [0, 500], kd ∈ [0, 5]. Position/velocity/torque ranges come from the motor-type table above.
Byte layout:
State reply (8 bytes, on recv_id)
Sameq | dq | tau layout + 2 temperature bytes:
CTRL_MODE register write
Broadcast frame on CAN ID0x7FF:
Known gotchas
ip link ... fd on→Operation not supported. gs_usb firmware doesn’t support CAN-FD. Use classical CAN @ 1 Mbit (our bringup script’s default).- Motors reply to probes but commands do nothing. CTRL_MODE is not MIT. The adapter now writes MIT on connect, but if you disabled that and motors got reset, run
openarm_set_mit_mode.py. COLLISION_AT_STARTduring planning.link5andlink7collision meshes overlap by 3 mm at every configuration. Handled byOPENARM_COLLISION_EXCLUSIONSin the OpenArm config module. If you see it anyway, the exclusion pairs may not be getting applied — check that the collision filter log line appears during world build.INVALID_STARTduring planning. Hardware encoder noise pushed a joint 1 mrad past a URDF limit. Joint4 used to be exactlylower=0.0which tripped this — it’s now-0.01to give breathing room. If you see it on a different joint, widen that limit by ~10 mrad.- “Transmit buffer full” (ENOBUFS) at 100 Hz. Kernel TX queue too small. The bringup script sets
txqueuelen 1000; the driver also retries on ENOBUFS. If you still see the error, checkip -details link show canX | grep qlen. - Arms swap sides. USB enumeration order flipped. Swap
LEFT_CAN/RIGHT_CANin config.py.
Design decisions
- Driver separate from adapter.
driver.pyhas zero dimos deps → unit-testable with a virtual CAN bus, reusable outside dimos. - MIT mode for everything. MIT can emulate position (high kp), velocity (kp=0, nonzero kd+dq), and torque (kp=kd=0, nonzero tau). One code path.
- Gravity compensation on by default. Eliminates steady-state position error without needing high kp. Needs Pinocchio + the per-side URDFs.
- One adapter per CAN bus, keyed by
address. Matches the Piper adapter pattern. Bimanual = two adapters with differentaddressvalues. - Per-side URDFs for Drake planning. Loading the full 14-DOF bimanual URDF twice (once per robot instance) creates phantom-arm collisions with the “other” arm frozen at zero. The per-side URDFs keep only one arm’s links + the torso, avoiding the phantom collisions while matching the bimanual kinematics exactly.
- URDF stays in-tree (
data/openarm_description/) for now. Can migrate to LFS later — only the path constants in the OpenArm blueprint module change. - CAN bringup stays manual (
sudo). Auto-bringup fromconnect()would need sudo-in-a-library or a systemd unit; the explicit script is clearer and testable. For production, add a oneshot systemd unit that runs the script at boot.
Workspace analysis
For figuring out which targets are reachable before planning, use the generic workspace tool:Testing
can.Bus(interface="virtual") loopback — no real hardware needed.