OpenArm Integration
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:- 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:| Arm | Transport | Python SDK |
|---|---|---|
| xArm | TCP/IP | xarm-python-sdk |
| Piper | CAN (via SDK) | piper_sdk |
| R1 Pro | Galaxea | Galaxea SDK |
| Go2 / G1 | WebRTC | Unitree SDK |
| Panda | FCI | panda-py |
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
| Blueprint | What it does |
|---|---|
coordinator-openarm-mock | Bimanual, mock adapters. No hardware. |
openarm-mock-planner-coordinator | Drake planner + bimanual mock, Meshcat viz. Great smoke test. |
coordinator-openarm-left / coordinator-openarm-right | Single arm, real hardware on can0 / can1. |
coordinator-openarm-bimanual | Both arms, real hardware, no planner. |
openarm-planner-coordinator | Main usable blueprint — Drake planner + both arms on real hardware. |
keyboard-teleop-openarm-mock / keyboard-teleop-openarm | Single-arm Cartesian IK + pygame keyboard, mock / real. |
5. Drive the arms from the manipulation client
Withopenarm-planner-coordinator running in one terminal, open a second terminal and start the REPL client:
| Function | Purpose |
|---|---|
robots() | List configured robots (here: ["left_arm", "right_arm"]) |
joints(robot_name) | Read current joint positions (7 floats) |
ee(robot_name) | Read current end-effector pose |
state() | Module state: IDLE, PLANNING, EXECUTING, FAULT, etc. |
plan([q1..q7], robot_name) | Plan a collision-free trajectory to a joint configuration |
plan_pose(x, y, z, robot_name=...) | Plan to a Cartesian EE pose (preserves current orientation) |
preview(robot_name) | Animate the planned path in Meshcat without executing |
execute(robot_name) | Send the planned trajectory to the coordinator |
home(robot_name) | Plan + execute to home joints |
commands() | Print all available 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
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.
| Send ID | Recv ID | Joint | Motor | vMax [rad/s] | tMax [Nm] |
|---|---|---|---|---|---|
| 0x01 | 0x11 | joint1 | DM8006 | 45 | 40 |
| 0x02 | 0x12 | joint2 | DM8006 | 45 | 40 |
| 0x03 | 0x13 | joint3 | DM4340 | 8 | 28 |
| 0x04 | 0x14 | joint4 | DM4340 | 8 | 28 |
| 0x05 | 0x15 | joint5 | DM4310 | 30 | 10 |
| 0x06 | 0x16 | joint6 | DM4310 | 30 | 10 |
| 0x07 | 0x17 | joint7 | DM4310 | 30 | 10 |
| 0x08 | 0x18 | gripper | DM4310 | 30 | 10 |
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.