Part I: The ROS 2 stack · §11 of 43

ROS joint angles are raw motor encoder positions, so the motor zero must equal the URDF zero pose. If it doesn’t, RViz won’t match reality, the limits in §3 are wrong, and the 2-second “return to zero” at activation (§4) drives the arm somewhere unexpected and possibly into itself or the torso.

Check first: with motors disabled, put the arm in the URDF zero pose (compare against RViz with all joints at 0) and run openarm-can-cli -i canX monitor. If every joint reads ≈ 0 (within a degree or two), you don’t need to recalibrate.

openarm-can-zero-position-calibration drives each joint gently into its mechanical stop, computes the offset to the ideal zero from the known stop angles (MECH_LIM_V1), and writes the zero to the motors.

openarm-can-zero-position-calibration --robot-version v1 --arm-side right_arm --canport can0
openarm-can-zero-position-calibration --robot-version v1 --arm-side left_arm  --canport can1
  • --robot-version defaults to v2, so pass v1 for your arms.
  • The arm moves on its own. Clear the space and stand by the power cut. Ctrl+C disables the motors (the arm drops).
  • It needs the openarm_can Python module. That’s not built in the container. Either install the PPA’s Python package on the host, if available, or build the bindings: cd /root/ros2_ws/src/openarm_can/python && pip install . (in a venv, after the C++ lib is installed).

Option B: manual

  1. Motors disabled. Physically place the arm precisely in the zero pose (a jig helps).
  2. openarm-can-cli -i can0 set_zero sets all 8 motors of that bus at once (it’s the default, --arm).
  3. Verify with monitor, then power-cycle the motors and verify again (the zero is stored in the motor).

Manual zeroing is only as precise as your pose placement. Option A is more repeatable.

Option C: re-zero a single motor

When only one joint is off (as right joint5 was on zeus: +1.394 rad in the hanging pose, log 2026-10-09):

openarm-can-cli -i can1 set_zero --no-arm --id 5     # ONLY motor 5 on can1 (right arm on zeus)
  • Place that joint in the zero pose first (e.g. match the other arm’s joint, which reads ≈ 0). The pose at the moment you press Enter becomes the zero.
  • The output must show only Processing Motor ID: 5. Without --id, set_zero re-zeroes all 8 motors of that bus at their current positions.
  • Verify with monitor (that ID ≈ 0.00, the others unchanged), power-cycle, verify again.
  • The zero is shared with LeRobot (§21), so this fixes it there too.

Checking the zero (every session)

Motors powered, torque off, arms hanging straight down, grippers closed, then openarm-can-cli -i canX monitor -d 60000:

Reading in the hanging poseMeaning
IDs 1–7 within about ±0.05 radOK. Joints 3, 6 and 7 aren’t held in one exact position by gravity, so a few hundredths more there is hand placement, not a bad zero.
any joint 0.05–0.5 radslightly off: re-hang the arm; if it persists, re-zero that joint
any joint beyond 0.5 radwrong zero; the patched start-up refuses to move (§4); re-zero
ID 8 (gripper), closed≈ 0

Follow the official calibration instructions at https://docs.openarm.dev for anything that differs from this summary. Calibration procedure details have changed between releases.