Skip to main content
Guide for running the OpenArm — an open-source bimanual 7-DOF research arm built from Damiao DM-J quasi-direct-drive motors — under the dimos manipulation + control stack. If you’re standing in front of the hardware and just want to run it, skip to Quick start. Related:

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

Code layout:
Workspace analysis is generic and lives in dimos/utils/workspace.py — works for any URDF, not just OpenArm.

Quick start

You need:
  • OpenArm v10 arms, wired to USB-CAN adapters
  • 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.3 and pinocchio
  • sudo on first run (to bring up the CAN interfaces)

1. Bring up the CAN buses

This sets both interfaces to classical CAN @ 1 Mbit with a 1000-frame TX queue (enough headroom for the 100 Hz tick loop). If only one bus is present, pass just that one: sudo ... openarm_can_up.sh can0. Troubleshooting:
  • Operation not permitted → you forgot sudo.
  • Operation not supported on fd on → your adapter doesn’t support CAN-FD. The script defaults to classical, so this shouldn’t happen unless you set MODE=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

Expected: 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 persistent CTRL_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:
The register is persistent across power cycles, so you only need this once per motor (or after a firmware reset).

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.
Meshcat will appear at http://localhost:7000.

5. Drive the arms from the manipulation client

With openarm-planner-coordinator running in one terminal, open a second terminal and start the REPL client:
This gives you an interactive Python prompt with these functions:

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
Each arm plans and executes independently — the coordinator runs both trajectories simultaneously on separate tick-loop tasks.

Example session — Cartesian target

skip
If you don’t know which Cartesian targets are reachable, check first with the workspace tool — see Workspace analysis below. 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 assigns can0/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:
No other code changes are needed.

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):
Guidelines:
  • 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 compute G(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’s get_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 writes CTRL_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’s joint_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 from enactic/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:
Gain ranges: 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)

Same q | dq | tau layout + 2 temperature bytes:

CTRL_MODE register write

Broadcast frame on CAN ID 0x7FF:
Persistent across power cycles.

Known gotchas

  • ip link ... fd onOperation 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_START during planning. link5 and link7 collision meshes overlap by 3 mm at every configuration. Handled by OPENARM_COLLISION_EXCLUSIONS in 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_START during planning. Hardware encoder noise pushed a joint 1 mrad past a URDF limit. Joint4 used to be exactly lower=0.0 which tripped this — it’s now -0.01 to 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, check ip -details link show canX | grep qlen.
  • Arms swap sides. USB enumeration order flipped. Swap LEFT_CAN / RIGHT_CAN in config.py.

Design decisions

  • Driver separate from adapter. driver.py has 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 different address values.
  • 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 from connect() 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:
Points are colored by Yoshikawa manipulability index: green = dexterous, red = near singularity. Avoid planning targets in the red regions.

Testing

Expected: 24 passed (13 driver + 11 adapter). All tests use can.Bus(interface="virtual") loopback — no real hardware needed.