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). -
can0andcan1up, 1M/5M FD (§10.4). Optionallydiscovershows 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 --statin/root/ros2_ws/src/openarm_ros2shows the 3 files; all four trajectory controllers reportsplines(§4). - Container
openarm(imageopenarm-jazzy:rt2): the launch log saysSuccessful 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 realWhat happens:
Configuration: CAN=can0, arm_prefix=left_, hand=enabled, can_fd=enabledandCAN=can1, arm_prefix=right_(check this mapping).- 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). Successful set up FIFO RT scheduling policy with priority 50.- Spawners (some retry once, which is normal), then
Configured and activated …for the 5 controllers. Wait forjoint_state_broadcaster, ~25–30 s after the launch; until then RViz shows no arms andlist_controllerslists 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 effortsIdentify 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 # backIf 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:=trueOne 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
vcanbus worked before patch 0002; it no longer does, by design.)
12.3 First motions
On zeus, with the helper scripts (§8.7):
- 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? - Every joint of each arm:
python3 /root/scripts/openarm_jsweep.py left, thenright. 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. - Grippers:
0.022(half) first, then0.0/0.044. - The demo: sim first, then real with
python3 /root/scripts/openarm_demo.py 1.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_startsets the speed only because the controllers usesplineson zeus (§4); with upstream’snonethe 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
SUCCEEDEDeven 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 raisingkpwithout 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.shIt asks you to confirm the arms hang at zero, then:
- deactivates the four arm and gripper controllers;
- sets both hardware components
inactive:OpenArmHW::on_deactivatedisables the motors, so the arms go limp. ExpectDeactivating OpenArm V10...→OpenArm V10 deactivated, twice; - 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_nodecrash during shutdown (Segmentation faultinJointTrajectoryController::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 beforeOpenArm 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.shstops the controllers first, which avoids the race. (log 2026-10-10)
Other ways to stop:
- Stop a motion: Ctrl+C on the
send_goalcommand 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 itactiveagain 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 launchescalate to SIGTERM and then SIGKILL, and a killed controller manager never runs the motors-off step. ros2 launch … | tee file: Ctrl+C reachesteetoo, which exits at once, hiding (and possibly cutting short) the shutdown. Usetee -i(the wrapper does).
12.5 Real-time tuning (recommended)
- Run the container with
--cap-add SYS_NICE --ulimit rtprio=99 --ulimit memlock=-1so the controller manager’s RT thread gets SCHED_FIFO priority 50. Done on zeus (containeropenarm, §5); the launch log saysSuccessful 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/fulland 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.