Part I: The ROS 2 stack · §4 of 43
Hardware components
Two components are declared in openarm_v1.0/urdf/ros2_control/openarm.bimanual.ros2_control.xacro:
| Component | Plugin when use_fake_hardware:=true | Plugin when false | CAN |
|---|---|---|---|
openarm_left_hardware_interface | mock_components/GenericSystem | openarm_hardware/OpenArmHW | left_can_interface (default can1; zeus can0) |
openarm_right_hardware_interface | mock_components/GenericSystem | openarm_hardware/OpenArmHW | right_can_interface (default can0; zeus can1) |
Each joint exports command interfaces position, velocity, effort and state interfaces position, velocity, effort.
What OpenArmHW actually does (real hardware)
From openarm_hardware/src/openarm_simple_hardware.cpp:
| Lifecycle step | Behaviour |
|---|---|
on_init | Opens SocketCAN on can_interface (CAN-FD if can_fd). Registers motors: joints 1–7 = send IDs 0x01…0x07, receive IDs 0x11…0x17, types DM8009, DM8009, DM4340, DM4340, DM4310, DM4310, DM4310. Gripper = send 0x08 / receive 0x18, DM4310. |
on_configure | Asks every motor for its state (refresh_all), reads replies. |
on_activate | Upstream: enables all motors (torque on), sends one full-stiffness command to 0 (a kick, before it even reads the positions), then drives every arm joint from wherever it is to 0 rad in a straight joint-space line over ~2 s (200 steps × 10 ms) with the full PD gains; the gripper is sent straight to its position. The robot moves on activation. Patched on zeus: see below. |
read (every cycle) | Requests and receives motor states. Arm joint state = motor position/velocity/torque directly (no gear or offset conversion). Gripper position is converted from motor radians to metres: joint = 0.044 × motor_rad / (−1.0472). Gripper velocity and effort are reported as 0. |
write (every cycle) | Sends MIT control to every arm motor: {kp_i, kd_i, q_cmd, dq_cmd, tau_cmd} from the position/velocity/effort command interfaces. Gripper: {kp_hand, kd_hand, motor_rad(q_cmd), 0, 0} with motor_rad = q_cmd / 0.044 × (−1.0472). |
on_deactivate | Disables all motors 3×. Torque off: the arms go limp and fall under gravity. |
The hardware plugin's lifecycle. Two steps move the arms without any command from you.
stateDiagram-v2 direction TB [*] --> Unconfigured: on_init · open SocketCAN, register motors Unconfigured --> Inactive: on_configure · read every motor Inactive --> Active: on_activate · ⚠ torque on, joints → 0 rad in ~2 s Active --> Active: read() + write() every cycle Active --> Inactive: on_deactivate · ⚠ torque off, arms go limp
MIT gains (v1.0 config/arm/control_gains.yaml; override by editing that file):
| j1 | j2 | j3 | j4 | j5 | j6 | j7 | hand | |
|---|---|---|---|---|---|---|---|---|
| kp | 70 | 70 | 70 | 60 | 10 | 10 | 10 | 5 |
| kd | 2.75 | 2.5 | 2.0 | 2.0 | 0.7 | 0.6 | 0.5 | 0.1 |
Consequences you need to know:
- No gravity compensation.
tau_ffis 0 unless a controller writes theeffortinterface, and none of the provided ones do. Expect steady-state sag, especially on joints 1–2 with the arm extended. - All three command interfaces feed one MIT command. Position-only controllers leave velocity and effort at 0, which is fine. A velocity-only controller does not give pure velocity control: the position term with its stiff
kpstill pulls toward the last position command, which starts at 0. See §16. - Joint zero = motor encoder zero. Zero calibration (§11) is what makes “0 rad” in ROS mean the right physical pose.
- Joints stop a little short of their targets. The torque is only
kp × (target − position)(no integral term), so once the error is small, the torque is less than the gearbox friction and the joint stops. On zeus, at the hanging pose: wrist joints (kp 10) stop 0.01–0.02 rad short, and the reported efforts matchkp × error(e.g. joint6: 10 × 0.022 ≈ 0.2 Nm). Shoulder and elbow moves that lift the arm stop further short. - The driver switches the torque on only once, at start-up. An arm powered after the launch stays limp while ROS believes it’s in control, with stored targets that may be far from the real pose. Power the arms before launching, never while ROS runs.
Start-up on zeus (patched driver)
OpenArmHW::return_to_zero() (called by on_activate) is patched on zeus by two patches in Prandium/openarm_patches/, applied to /root/ros2_ws/src/openarm_ros2 and built (log 2026-10-09):
| Upstream | zeus (patches 0001 + 0002) | |
|---|---|---|
| Reading the start position | first sends a full-stiffness command to 0 | state request only, no command |
| Move to zero | 2 s, linear | 10 s, eased (smoothstep: gentle start and stop), RETURN_TO_ZERO_SECONDS |
| Gripper | sent straight to its position | ramped from where it is (same target) |
| Every motor replied? | not checked (an unanswered motor reads 0.0) | each arm motor and the gripper must report enabled, no error code, else refuse |
| Far from zero? | not checked | any arm joint more than 0.5 rad (≈29°) from zero → refuse (MAX_START_OFFSET) |
| On refusal | — | logs which joint, disables the motors, activation fails |
| Logging | — | prints every joint’s start position |
| After the ramp | command buffers left as they were | set to the zero pose, so a re-activation can’t send stale targets |
Log lines to expect, per arm (the right arm is activated first, then the left):
[OpenArmHW]: Activating OpenArm V10...
[OpenArmHW]: Returning to zero position...
[OpenArmHW]: joint1 start: -0.009 rad (x7, real non-zero numbers)
[OpenArmHW]: Reached zero position (~10.4 s later)
[OpenArmHW]: OpenArm V10 activated
Refusals look like joint5: no reply, not enabled, or error (code 0x0) (arm not powered or not answering) or joint5 is +1.394 rad from zero (limit 0.50 rad) (wrong zero or badly placed arm), followed by Not moving: … Disabling motors.
Consequences:
- Start-up takes ~21 s (two 10 s ramps in turn) and the controller spawners wait for it. Some spawners print
Failed to acquire lock in 20 seconds. Attempt 1 of 5 failed. Retrying in 3 seconds..., which is normal: they retry. The controllers, and the arms in RViz, appear ~25–30 s after the launch. - Both arms must be powered. An unpowered arm now fails the check (by design), so “one arm on a virtual CAN bus” no longer works. To test one arm, power both and command only one.
- Apply / undo:
docker cpthe patch into the container,git apply --checkthengit applyin/root/ros2_ws/src/openarm_ros2,colcon build --symlink-install --packages-select openarm_hardware. Undo:git checkout -- openarm_hardware, rebuild.
Controllers (openarm_bringup/config/controllers/openarm_bimanual_controllers.yaml)
| Controller | Type | Joints | Interfaces | Started by default? |
|---|---|---|---|---|
joint_state_broadcaster | JointStateBroadcaster | all | reads all states → /joint_states, /dynamic_joint_states | ✅ |
left_joint_trajectory_controller | JointTrajectoryController | left joint1–7 | cmd: position, state: position | ✅ (default robot_controller) |
right_joint_trajectory_controller | JointTrajectoryController | right joint1–7 | same | ✅ |
left_gripper_controller | JointTrajectoryController | openarm_left_finger_joint1 | position | ✅ |
right_gripper_controller | JointTrajectoryController | openarm_right_finger_joint1 | position | ✅ |
left/right_forward_position_controller | ForwardCommandController | joint1–7 | position | only with robot_controller:=forward_position_controller |
left/right_forward_velocity_controller | ForwardCommandController | joint1–7 | velocity | declared but never spawned |
Controller manager update rate:
- 750 Hz for
openarm_bringup(openarm_bimanual_controllers.yaml). - 100 Hz for the MoveIt demo (
openarm_bimanual_moveit_controllers.yaml).
Trajectory controller settings worth knowing:
interpolation_method: upstreammainsets it tonone. Withnone, there is no motion between points: see the next subsection. On zeus it is changed tosplines.allow_partial_joints_goal: false. Every goal must contain all 7 joints of that arm.- All
constraints(goal/trajectory tolerances,goal_time) are 0.0, which means “not checked”. A goal always reportsSUCCEEDEDwhen time runs out, even if the real arm didn’t get there (blocked, sagging). Verify with/joint_statesorcontroller_state, not just the action result.
Interpolation (what it does, and the zeus setup)
The trajectory controller runs 750 times a second. Each cycle it asks: “where should each joint be right now?” and writes that to the hardware. interpolation_method decides the answer between the points of your trajectory (source: joint_trajectory_controller/src/trajectory.cpp, Trajectory::sample(), ros2_controllers 4.42 for Jazzy):
Before the first point’s time_from_start | Between point k and point k+1 | |
|---|---|---|
none (upstream main) | holds the position it had when the goal arrived | jumps straight to point k+1 at the start of the segment, then waits there |
splines (ROS default, zeus) | moves smoothly from the current position to the first point | moves smoothly from point k to point k+1 |
The same one-point goal (time_from_start = 5 s) under each setting. The dot traces the joint position; the arm shows the same motion.
⚠ verify on hardware)goal sent time_from_start
So with none, a one-point goal {positions: [...], time_from_start: {sec: 5}} does nothing for 5 s and then steps to the target. time_from_start only delays the jump; it doesn’t slow it down. On real motors, with the stiff MIT gains, a step is a full-speed move. Upstream disabled interpolation for streaming use (the config comment says the controller “is expected to receive high-frequency commands”), where every command is a tiny step anyway.
What splines does depends on what each point contains:
| The points have… | Interpolation | Motion |
|---|---|---|
positions only | linear | constant speed; starts and stops abruptly (a velocity step, far milder than a position step) |
positions + velocities | cubic | eases in and out; use velocities: [0, 0, 0, 0, 0, 0, 0] on the last point for a smooth stop ⚠ verify |
positions + velocities + accelerations | quintic | smoothest |
The zeus change. interpolation_method is read-only at runtime (ros2 param set can’t change it), so it’s changed in the controller config and the bringup is relaunched. Inside the container, with nothing running:
ros2 pkg prefix openarm_bringup # must print /root/ros2_ws/install/openarm_bringup, i.e. the file below is the one used
cd /root/ros2_ws/src/openarm_ros2
git status --short # note anything already modified
sed -i 's/interpolation_method: "none"/interpolation_method: "splines"/' openarm_bringup/config/controllers/openarm_bimanual_controllers.yaml
git diff --stat # expect: 1 file changed, 4 insertions(+), 4 deletions(-)
ls -l /root/ros2_ws/install/openarm_bringup/share/openarm_bringup/config/controllers/openarm_bimanual_controllers.yamlIf the last line shows an arrow (-> /root/ros2_ws/src/...), the install is a symlink to the file you edited and nothing needs rebuilding. If it’s a plain file, rebuild that one package: cd /root/ros2_ws && colcon build --symlink-install --packages-select openarm_bringup.
Check after every launch (all four should print splines):
for c in left_joint_trajectory_controller right_joint_trajectory_controller left_gripper_controller right_gripper_controller; do ros2 param get /$c interpolation_method; doneTo undo: git checkout -- openarm_bringup/config/controllers/openarm_bimanual_controllers.yaml. Note that a git pull, git stash or re-clone of openarm_ros2 also undoes it. The MoveIt demo uses a different file (openarm_bimanual_moveit_controllers.yaml) that doesn’t set interpolation_method at all, so it already uses the default, splines.