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

12.1 Pre-flight checklist

  • Workspace clear of people and objects within the arms’ full reach (arms can reach ~0.6 m+ from the shoulder in every direction).
  • Motor power switch reachable by the person at the keyboard. It’s the emergency stop; software isn’t.
  • Base clamped or bolted; if that’s impossible (zeus), weighted. The robot has fallen over once (under LeRobot, with a wrong zero).
  • Nothing else on the buses: no LeRobot script, no other ROS launch, only one container running (docker ps).
  • can0 and can1 up, 1M/5M FD (§10.4). Optionally discover shows 8 motors on each.
  • Zero verified (§11), always after any LeRobot calibration.
  • Both arms powered before launching, hanging straight down near zero (wrists straight), grippers closed.
  • Launch only with bash /root/scripts/openarm_launch.sh real (zeus mapping: left = can0, right = can1), then do the identification step (§12.2).
  • The local changes are in place: git diff --stat in /root/ros2_ws/src/openarm_ros2 shows the 3 files; all four trajectory controllers report splines (§4).
  • Container openarm (image openarm-jazzy:rt2): the launch log says Successful set up FIFO RT scheduling policy with priority 50.
  • Plan to stop: arms back to zero, bash /root/scripts/openarm_stop.sh, then power off (§12.4).

12.2 Launch

On zeus, in a container shell, with both arms powered and hanging:

bash /root/scripts/openarm_launch.sh real

What happens:

  1. Configuration: CAN=can0, arm_prefix=left_, hand=enabled, can_fd=enabled and CAN=can1, arm_prefix=right_ (check this mapping).
  2. The right arm, then the left, each: torque on (the arm goes firm), 7 jointN start: … lines with real values, a 10 s slow move to zero, Reached zero position, OpenArm V10 activated (§4).
  3. Successful set up FIFO RT scheduling policy with priority 50.
  4. Spawners (some retry once, which is normal), then Configured and activated … for the 5 controllers. Wait for joint_state_broadcaster, ~25–30 s after the launch; until then RViz shows no arms and list_controllers lists nothing.

Switch the power off if anything moves fast or far, jerks or buzzes. If the start-up check refuses (no reply… or … rad from zero), it has already switched the motors off; read which joint it names.

Then, in a second shell:

grep -E "start:|Reached|Not moving|no reply|rad from zero" /root/launch_real_*.log | tail -16   # real start values, 2x Reached
ros2 control list_hardware_components | grep -E "name:|plugin|state"    # both openarm_hardware/OpenArmHW, active
ros2 control list_controllers                                            # 5 controllers active
ros2 topic echo --once /joint_states                                     # real values near 0, non-zero efforts

Identify the arms after every real launch. The robot’s own left wrist must move (stand behind the robot: it’s the arm on your left):

python3 /root/scripts/openarm_jmove.py left 7 0.1     # robot's LEFT wrist turns ~6 deg
python3 /root/scripts/openarm_jmove.py left 7 0       # back

If the other wrist moves, the CAN mapping is swapped: stop (§12.4) and check the launch.

Without the wrapper (other machines, or the MoveIt demo), the zeus arguments are:

ros2 launch openarm_bringup openarm.bimanual.launch.py \
  arm_type:=v10 use_fake_hardware:=false \
  left_can_interface:=can0 right_can_interface:=can1 can_fd:=true
 
ros2 launch openarm_bimanual_moveit_config demo.launch.py \
  arm_type:=v10 use_fake_hardware:=false \
  left_can_interface:=can0 right_can_interface:=can1 can_fd:=true

One arm only

There’s no launch argument for it, and with the patched driver an unpowered arm makes the start-up refuse, so power both arms and command only one. (Pointing the unused arm at a virtual vcan bus worked before patch 0002; it no longer does, by design.)

12.3 First motions

On zeus, with the helper scripts (§8.7):

  1. One joint, small and slow, e.g. python3 /root/scripts/openarm_jmove.py left 4 0.3 (elbow, 10 s), then back with … left 4 0. Compare the real arm with RViz: same direction? Same magnitude?
  2. Every joint of each arm: python3 /root/scripts/openarm_jsweep.py left, then right. Each joint goes 0.3 rad in its safe direction and back, 5 s each way, and the script prints the expected direction. All 14 joints were checked this way on 2026-10-10.
  3. Grippers: 0.022 (half) first, then 0.0 / 0.044.
  4. The demo: sim first, then real with python3 /root/scripts/openarm_demo.py 1.5.
  5. Only then use MoveIt, with velocity/acceleration scaling at 0.1 at first.

The same with raw commands (left elbow, 0.3 rad over 5 s; all 7 joints must be listed):

ros2 action send_goal /left_joint_trajectory_controller/follow_joint_trajectory control_msgs/action/FollowJointTrajectory \
  '{trajectory: {joint_names: [openarm_left_joint1, openarm_left_joint2, openarm_left_joint3, openarm_left_joint4, openarm_left_joint5, openarm_left_joint6, openarm_left_joint7],
    points: [{positions: [0, 0, 0, 0.3, 0, 0, 0], time_from_start: {sec: 5}}]}}'

Rules of thumb on hardware:

  • time_from_start sets the speed only because the controllers use splines on zeus (§4); with upstream’s none the joint waits, then jumps. Use ≥ 3–5 s per large move until you trust the setup.
  • Stay inside the per-arm limits (§3): joint2 has only 10° inward. The trajectory controller and mock don’t stop you; MoveIt respects limits, raw commands don’t.
  • ”+” isn’t “forward”: check the direction map (§3).
  • Goals report SUCCEEDED even if the arm didn’t arrive (tolerances disabled), and joints stop ~0.01–0.05 rad short (friction, no gravity compensation, §4). Don’t “fix” it by raising kp without understanding the motor limits.

12.4 Stopping

Normal stop (zeus): openarm_stop.sh, in a second shell, with both arms back at the hanging zero pose:

bash /root/scripts/openarm_stop.sh

It asks you to confirm the arms hang at zero, then:

  1. deactivates the four arm and gripper controllers;
  2. sets both hardware components inactive: OpenArmHW::on_deactivate disables the motors, so the arms go limp. Expect Deactivating OpenArm V10... → OpenArm V10 deactivated, twice;
  3. sends SIGINT to the launch’s whole process group (like Ctrl+C in its terminal).

Then switch the motor power off. Verified on the real robot on 2026-10-10: both arms deactivated, clean shutdown.

Why not a plain Ctrl+C on the real launch

On zeus, Ctrl+C made ros2_control_node crash during shutdown (Segmentation fault in JointTrajectoryController::update: the real-time loop still called a controller that had just been shut down). It died after starting to deactivate the right arm and before OpenArm V10 deactivated, and never deactivated the left arm, so the motors stayed enabled and holding until the power was cut. Harmless at the hanging pose, dangerous in a raised pose. openarm_stop.sh stops the controllers first, which avoids the race. (log 2026-10-10)

Other ways to stop:

  • Stop a motion: Ctrl+C on the send_goal command or helper script (it cancels the goal), or send a new goal to the current position. The arm holds where it is.
  • Torque off but keep ROS running: support the arm first (it will drop), then ros2 control set_hardware_component_state openarm_<side>_hardware_interface inactive. Setting it active again runs the start-up again (10 s ramp to zero and its checks).
  • Emergency: cut the motor power. Don’t rely on software.

Pitfalls:

  • Never power an arm on while ROS is running (also after an emergency power cut): stop ROS first, then power on, then relaunch. The torque-on command is sent only at start-up (§4).
  • Press Ctrl+C once and wait. Repeated Ctrl+C makes ros2 launch escalate to SIGTERM and then SIGKILL, and a killed controller manager never runs the motors-off step.
  • ros2 launch … | tee file: Ctrl+C reaches tee too, which exits at once, hiding (and possibly cutting short) the shutdown. Use tee -i (the wrapper does).
  • Run the container with --cap-add SYS_NICE --ulimit rtprio=99 --ulimit memlock=-1 so the controller manager’s RT thread gets SCHED_FIFO priority 50. Done on zeus (container openarm, §5); the launch log says Successful set up FIFO RT scheduling policy with priority 50.
  • Optional: a low-latency or PREEMPT_RT kernel on the host.
  • Watch for overruns: ros2 topic echo /controller_manager/statistics/full and the launch log. Persistent overruns at 750 Hz mean late CAN frames and jerky motion. A laptop on battery with GPU-heavy RViz is the worst case, so consider plugging in and closing RViz during demanding runs.

Measured on zeus (2026-10-10), both arms powered: loops of 1.34–1.58 ms against the 1.33 ms budget; read 0.53–0.81 ms, write 0.53–0.70 ms, update 0.10–0.16 ms. Almost all the time is CAN I/O over USB, and the PCAN adapter sits on a USB-C docking station, which adds latency and jitter. Harmless at slow test speeds. Next things to try, in order: the adapter in a USB port directly on the laptop; then lowering update_rate from 750 to 500 Hz in openarm_bimanual_controllers.yaml (a 2 ms budget). With an arm on a virtual bus (no replies), loops took 2.0–2.6 ms, because the driver waits up to 0.5 ms in read() and 0.1 ms in write() for replies.