Docs menu

Safety model

How the LB deadman and ackermann_mux each stop the car, which one protects you in each mode, and the rule every driving node must follow.

Two independent checks stand between driving code and the motor. The first is inside every node that can move the car: it publishes zero speed unless the driver is holding LB. The second is ackermann_mux, which lets manual commands override autonomy. In autonomy mode only the first one is active, which is why it is mandatory.

The two checks on every drive command An autonomy node checks LB before it publishes on /drive. ackermann_mux then passes /teleop instead of /drive whenever teleop is publishing. Only the winning command reaches the VESC. autonomy node CHECK 1: LB HELD? ackermann_mux CHECK 2: TELEOP WINS VESC /drive /ackermann_cmd /teleop LB RELEASED: SPEED 0 TELEOP RUNNING: /DRIVE IGNORED MOTOR AND SERVO
How to read it: a drive command travels left to right. Check 1 runs inside the autonomy node before it publishes on /drive. Check 2 runs in ackermann_mux, which receives /drive and /teleop and passes on only one of them as /ackermann_cmd. The label under each box is what that check does when it refuses.

Check 1: the LB deadman

Every node that can move the car subscribes to /joy itself and refuses to drive unless both of these are true:

  • LB (deadman_button, index 4) is held in the latest /joy message.
  • That message is newer than joy_timeout_sec, 0.5 s.

The node runs this check first on every tick, before any other logic. When it fails, the node publishes speed 0. gap_follow holds the steering where it is, and pure_pursuit returns it toward centre at the servo’s rate limit. The check is implemented separately in gap_follow_node, pure_pursuit_node, auto_map_race_node and neural_racer_node, so a bug in one node’s check does not disable the others.

State What the node does Log state in gap_follow
LB held, /joy fresh Drives gap_follow, corner_fallback
LB released Speed 0 deadman_released
No /joy received since start Speed 0 waiting_for_joy
Last /joy older than 0.5 s Speed 0 joy_stale
Never set enable_deadman to false on the car. The parameter exists so that tests can run a node without a gamepad, and those tests also move the node off the real /drive. Turning it off on the car removes the only check that protects an autonomy run. It is a team policy, not a tuning value.

Because every driving node depends on /joy, joy_node starts in bringup, not in the teleop launch. A car whose gamepad is missing or switched to DirectInput mode cannot drive in any mode.

Check 2: ackermann_mux

ackermann_mux receives commands on two topics and passes the winner on as /ackermann_cmd. The configuration is in mux.yamlGitHub.

Input Topic Priority Timeout
Manual driving /teleop 100 0.2 s
Autonomy /drive 10 0.2 s

The higher priority wins while it is publishing. An input that has been silent for longer than its timeout drops out. joy_teleop publishes on /teleop continuously while teleop_launch.py runs, even with LB released, so /teleop never times out. While teleop runs, no command on /drive reaches the motor.

What protects you in each mode

Mode LB deadman Mux override
Manual (teleop_launch.py) joy_teleop only publishes stick values while LB is held Not needed: manual is the only source
Autonomy (any autonomy node) Active in the node Inactive: teleop is not running

During autonomy, releasing LB is the only software stop. Keep a second person at the battery switch whenever the car drives itself.

What stops the car when something fails

Each failure is caught by a specific mechanism, and some by nothing in software. Knowing which is which is the reason for the person at the battery switch.

Failure What stops the car How fast
The driver releases LB The driving node publishes speed 0 The next tick, 25 ms
The gamepad switches off, or joy_node crashes Each driving node sees /joy go stale and publishes speed 0 0.5 s
The LiDAR stops pure_pursuit stops (lidar_scan_stale). gap_follow, driven by scans, stops publishing, and the VESC’s own command timeout stops the motor. 0.5 s, plus the VESC timeout for gap_follow
Localization freezes while the car moves pure_pursuit’s frozen-pose watchdog 0.5 s
Odometry stops gap_follow publishes speed 0 (odometry_stale), even with LB held. With fused speed on, either /odom or /odom/fused going stale stops it. 0.5 s
The car hits something, on car 1 with imu_impact_stop:=true gap_follow latches speed 0 until LB is released and pressed again. An IMU failure never clears the latch. Off by default; the threshold is provisional. The next scan after the spike
The IMU fails or goes stale Each gap_follow IMU feature falls back to its off behaviour on the same tick. The car keeps driving. The next tick
A driving node raises an error It publishes speed 0, then exits Immediately
A driving node hangs or is killed Nothing in ROS: the mux forwards nothing and sends no zero. The VESC’s command timeout stops the motor. The VESC timeout, which is not recorded
The Jetson loses power or the USB cable comes out The VESC’s command timeout The VESC timeout
The car is driving wrongly but every check passes The person holding LB lets go, or the person at the battery switch Human reaction time

The VESC’s command timeout is set in VESC Tool, on the VESC itself. It is the last line of defence for every row that ends in it, and its value has not yet been read off either car.

Rules for driving code

These rules apply to any node that publishes on /drive, /ackermann_cmd, /commands/motor/* or /commands/servo/*.

  1. Implement the LB check in the node itself, copied from gap_follow_node’s joy_callback and _deadman_status in gap_follow_node.pyGitHub. Another node’s check does not cover yours.
  2. Test all four rows of the table above, asserting the published command, not an internal flag.
  3. Run new code in this order, without skipping a step: with no driver stack running, then with the wheels off the ground, then on the floor at low speed in open space.

The full contract for a driving node is in Write a driving node.

Limitations

  • The deadman checks that the gamepad reports LB held. It cannot tell whether the person holding it is watching the car.
  • ackermann_mux only forwards the commands it receives. When every input goes quiet, it forwards nothing and publishes no stop of its own, so the motor then stops only through the VESC firmware’s command timeout, which is set in VESC Tool and cannot be read from ROS. This is why gap_follow and pure_pursuit publish an explicit zero before they exit after an error.

Checked against car 1 commit 8a88dcc and car 2 commit 80da6f5, before the two workspaces merged.