Safety

Safety model

How nao-rs stops the motors: a latched death switch checked before every frame, a guard process that takes over a hung runtime, the triggers, and the chest board's own hard power-off.

nao-rs has one rule for the motors: when the death switch is tripped, every frame to the chest board is all-off, whatever the controller asked for. All-off means stiffness 0 on every joint. The switch is latched: once tripped, it stays tripped until a person re-arms it on purpose.

Principles

These rules bind every part of nao-rs:

  1. The board firmware is never touched. The chest board, motor board and foot board firmware stays Aldebaran's. nao-rs flashes nothing and writes no configuration or EEPROM data stored on the boards. It only reads what HAL reads and sends what HAL sends.
  2. Everything above the firmware is rewritten and proven offline first. Frames must be byte-identical to HAL's, against Aldebaran's original libraries and against recordings of the real robot, before they reach a board. See Verification.
  3. No motion without a person present during bring-up. The first movements happen with someone at the robot, the robot lying down, one joint at a time and at low stiffness.
  4. No command bypasses the death switch. No API, lease or controller can override a trip or re-arm the switch; only a person on the robot can.

Four layers

Four layers make the all-off rule hold even when parts of the software fail.

LayerWhat it doesWhen it acts
1. The latch in the runtimeOne atomic load right before every frame. Tripped means the frame is all-offEvery cycle, about every 12 ms
2. The guard processWatches the runtime's heartbeat. Kills a hung runtime, takes the chest board over and sends all-off itselfHeartbeat older than 60 ms, or a trip not confirmed within 40 ms
3. The triggersChest button, all three head pads, overtemperature, cycle overrun, the UDP stop, the API, the CLIWhen they fire, they trip the latch
4. The chest board's firmwareHolding the chest button for 8 seconds switches the whole robot offAlways: it doesn't depend on the head computer

All-off means limp

A tripped switch removes all stiffness at once. A standing robot falls. The death switch protects people and the robot from motors that keep pushing; it is not a way to sit the robot down.

Layer 1: the latch

The switch is a small shared memory page at /run/nao-rs/deathswitch. Every process on the robot that can stop the motors maps it: the runtime inside hulk, the guard and naoctl. The page holds only atomic values, so no reader ever sees half a write and a crashed writer can't leave a lock behind.

  • Fresh pages start tripped. /run is cleared at every boot, so every boot starts with the motors off until someone re-arms.
  • The first reason wins. A second trip doesn't overwrite why the switch was tripped.
  • Stopping the runtime trips it. When the runtime shuts down, it trips the switch before its cycle ends, so no motor stays stiff without a cycle.
  • A damaged page is a stop. If the page file is truncated, zeroed, overwritten, deleted or replaced, the runtime sends all-off from that frame on and the switch trips with PageLost (details).
  • Re-arming is a person's decision. Only naoctl rearm re-arms. No timeout, no controller and no API message does.
  • Re-arming never resumes motion. Whatever tripped the switch (a gesture, the app, an API, UDP, naoctl, heat or a fault), the robot program drops to its unstiff state while the switch is tripped. After a re-arm the robot stays limp until the app or an API wakes it.

HAL's safety layer in every frame

Below the death switch, the runtime applies HAL's own stiffness safety to every frame, ported 1:1 and checked against Aldebaran's original libraries. It only ever lowers stiffness:

  • Controller lost. When the motion code hasn't sent a command for 300 ms (25 cycles), the stiffness of every joint ramps down to 0 in 500 cycles (6 s at the runtime's 12 ms; HAL's 10 ms cycle made it 5 s), and back up in 200 cycles once commands arrive again.
  • Cycle stall. When more than 100 ms pass between two cycles, every stiffness goes to 0 at once and stays there for 1 s, then ramps back up.
  • Battery over-current. While the battery current is above 4.5 A, the stiffness is lowered step by step, down to 24 %, and recovers once the current is back under the limit.

The page is not in /dev/shm: logind deletes a user's files there when the user's last session ends, and a process opening the path afterwards would get a new page the running runtime never sees.

Layer 2: the guard

nao-rs-guard is a separate process, started by systemd before hulk, at real-time priority 40 (the runtime's cycle runs at 30). It allocates nothing in its loop and looks at the page every 2 ms. It takes over when:

  • the heartbeat stops: the runtime's last beat is more than 60 ms old (hang, crash, deadlock); or
  • a trip is not obeyed: the switch is tripped and the runtime hasn't confirmed an all-off frame within 40 ms.

Taking over means, in this order:

  1. trip the switch, so a runtime that wakes up again sends all-off as well;
  2. kill the runtime's process with SIGKILL, which makes the kernel release the chest board;
  3. open the chest board itself and send stiffness 0 to both motor buses from the first frame, without the start-up handshake;
  4. keep sending all-off every cycle until a person re-arms, or until a restarted runtime registers.

Restarts need no re-arm. systemd restarts hulk one second after it fails, also after the guard killed it. The new runtime registers in the page and waits up to 5 s for the chest board; the guard hands the board back, forgets the old heartbeat and watches the new runtime as before. The switch stays tripped through the handover, so the new runtime sends all-off too until a person re-arms. After a re-arm with no runtime running, the guard releases the board as well.

On the robot, a frozen hulk was detected and taken over in 62 ms, and the guard's first all-off frame went out 9 ms after the kill. That the motor boards obey the guard's frames is checked in the motor session (planned), because nao-rs doesn't drive the motors yet.

The guard also receives the UDP stop (see below). It does nothing while no runtime has ever beaten since boot or since its last release: then nothing sends frames, so no motor can be stiff.

Layer 3: the triggers

TriggerWhere it is seenReason recorded
Chest button pressedRuntime, every cycle. It fires on the press, so a button held at start-up doesn't hide a later pressChestButton
All three head pads touched at onceRuntime, from the head bus. Two pads don't tripHeadTouch
A joint at 75 °C or moreRuntime, from the motor boards' temperaturesOvertemperature
CPU above 95 °CFan thread, before it halts the systemOvertemperature
Cycle overrun: more than 3 periods missedRuntimeDriverFault
A failed exchange with the chest boardRuntimeDriverFault
The runtime stopsRuntimeDriverFault
UDP datagram starting with NAO-RS STOP on port 7701GuardUdp
REST API POST /v1/safety/stop, the WebSocket stop, the MessagePack API's Stop and the brain's stop toolThe page directly, and the UDP stopApi (Udp only if the page could not be written)
naoctl stopThe page directlyCli
The page file truncated, zeroed, overwritten, deleted or replacedRuntime before every frame, guard before every pollPageLost
Heartbeat lostGuardGuardHeartbeatLost
Trip not confirmedGuardGuardNoConfirmation

Gestures

GestureEffect
Chest button, one pressStop: the death switch trips and latches, all motors limp. The robot then says its IP address and how to connect, in English or German (What the robot says)
Chest button, held 8 sHard power-off by the chest board's firmware (layer 4)
All three head pads at onceStop, like the chest button
Waking the robotOnly from the app or an API (Power wake), never by a gesture, and never by a re-arm
Re-arming after a stopOnly naoctl rearm on the robot, after a person has checked it. The robot stays limp until it is woken

The UDP stop has no authentication on purpose: anyone on the robot's network must be able to stop it, even when the API or the runtime is stuck. From a computer with a checkout of the nao-rs repository:

cargo nao-rs stop <robot-address>

It sends the datagram ten times, 5 ms apart; one is enough. Any tool that can send UDP works too, for example printf 'NAO-RS STOP' | nc -u -w1 <robot-address> 7701.

Layer 4: the hard power-off

Holding the chest button for 8 seconds switches the robot off completely: motors, LEDs, fan and head. The chest board's firmware does this, so it works whatever runs or hangs on the head computer. It was tested on 2026-11-06 with no software reading the chest board. Pressing the button also trips the death switch at once, so in practice the motors are off long before the 8 seconds are up.

Read and re-arm

On the robot:

naoctl status   # armed or tripped, why, since when; the runtime's heartbeat
naoctl stop     # trip it
naoctl rearm    # re-arm: the robot may be woken again

Re-arm only after you have checked the robot and its surroundings. The robot stays limp after the re-arm; wake it from the app or an API when you want it to move. A gesture never wakes it. The death switch in detail covers the page layout, the heartbeat and the guard's rules.

On this page