Safety in BeamBots
View SourceOverview
BeamBots provides a software safety system through the BB.Safety module and
BB.Safety.Controller. This document explains how the system works and its limitations.
How Safety Works
The safety system has four key components:
- Behaviour callbacks: Actuators and controllers implement the
disarm/1callback via their behaviours (BB.Actuator,BB.Controller) to handle hardware shutdown. Sensors can optionally implementdisarm/1if they control hardware. - Registration: Processes register with the safety controller on startup, providing hardware-specific options needed for stateless disarm
- State management: The controller tracks safety state per robot (
:disarmed,:armed, or:error) - Disarm callbacks: On disarm command or robot crash, all registered
disarm/1callbacks are invoked
Safety States
| State | Description |
|---|---|
:disarmed | Robot is safely disarmed, all disarm callbacks succeeded |
:armed | Robot is armed and ready to operate |
:disarming | Disarm in progress, callbacks running concurrently |
:error | Disarm attempted but callbacks failed; hardware may not be safe |
When disarm is called, the robot immediately transitions to :disarming state.
Commands are rejected while in this state. Disarm callbacks run concurrently with
a 5 second timeout per callback.
If all callbacks succeed, the robot transitions to :disarmed. If any callback
fails (returns an error, raises, throws, or times out), the robot transitions to
:error state instead. This prevents the robot from being armed again until an
operator manually acknowledges the failure using BB.Safety.force_disarm/1.
Shutdown Behaviour
When the safety controller terminates (e.g. during application shutdown), it attempts to disarm all armed robots. This is a best-effort operation - if the system is shutting down quickly, callbacks may not complete. Always rely on hardware safety controls for critical applications.
Example Implementation
defmodule MyServo do
use GenServer
use BB.Actuator
@impl BB.Actuator
def disarm(opts) do
# This callback can be called even if the GenServer process is dead
pin = Keyword.fetch!(opts, :pin)
Pigpio.set_servo_pulsewidth(opts[:gpio_ref], pin, 0)
:ok
end
def init(opts) do
# Register with safety controller, providing stateless disarm options
BB.Safety.register(__MODULE__,
robot: opts[:bb].robot,
path: opts[:bb].path,
opts: [pin: opts[:pin], gpio_ref: opts[:gpio_ref]]
)
{:ok, %{pin: opts[:pin]}}
end
endKey Design Decisions
Stateless disarm: The
disarm/1callback receives only the options provided at registration. It must work without access to GenServer state, enabling hardware to be made safe even when the process has crashed.Direct ETS registration: Handler registration writes directly to an ETS table, avoiding the controller's GenServer mailbox. This prevents registration failures during high load.
Protected state changes: Arm/disarm state changes go through the GenServer to ensure callbacks are invoked and events are published.
Hardware Error Reporting
Controllers and actuators can report hardware errors using BB.Safety.report_error/3.
By default, this automatically disarms the robot - a safe default for most applications.
Default Behaviour (Auto-Disarm)
When a hardware error is reported:
- A
BB.Safety.HardwareErrormessage is published to[:safety, :error] - If
auto_disarm_on_erroristrue(default) and the robot is armed, it is automatically disarmed - The error is logged for debugging
# In a controller detecting a hardware fault:
BB.Safety.report_error(MyRobot, [:dynamixel, :servo_1], {:overheating, 85.0})Custom Error Handling
For applications that need more nuanced error handling, disable auto-disarm and handle errors yourself:
defmodule MyRobot do
use BB
settings do
auto_disarm_on_error false
end
# ... rest of robot definition
endThen subscribe to error events and implement your own logic:
defmodule MyRobot.ErrorHandler do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
BB.subscribe(MyRobot, [:safety, :error])
{:ok, %{}}
end
def handle_info({:bb, [:safety, :error], message}, state) do
%{payload: %BB.Safety.HardwareError{path: path, error: error}} = message
case categorise_error(error) do
:critical ->
# Critical errors still trigger disarm
BB.Safety.disarm(MyRobot)
:warning ->
# Warnings are logged but operation continues
Logger.warning("Hardware warning at #{inspect(path)}: #{inspect(error)}")
:transient ->
# Transient errors are ignored (e.g., brief communication glitches)
:ok
end
{:noreply, state}
end
defp categorise_error({:hardware_error, flags}) when Bitwise.band(flags, 0x04) != 0 do
:critical # Overheating
end
defp categorise_error({:hardware_error, _flags}) do
:warning
end
defp categorise_error(_) do
:transient
end
endImplementing Error Reporting in Controllers
If you're writing a controller that monitors hardware for errors, call
BB.Safety.report_error/3 when you detect a problem:
defmodule MyController do
use GenServer
use BB.Controller
# ... init and other callbacks ...
def handle_info(:poll_status, state) do
case read_hardware_status(state.device) do
{:ok, status} when status.error_flags != 0 ->
# Report the error - safety system handles the rest
BB.Safety.report_error(
state.robot,
state.path,
{:hardware_error, status.error_flags}
)
{:noreply, state}
{:ok, _status} ->
{:noreply, state}
{:error, reason} ->
# Communication errors might also warrant reporting
BB.Safety.report_error(state.robot, state.path, {:comm_error, reason})
{:noreply, state}
end
end
endImportant Limitations
BEAM is Soft Real-Time
The BEAM virtual machine provides soft real-time guarantees, not hard real-time. This means:
- No guaranteed response times: Disarm callbacks may be delayed by garbage collection, scheduler load, or other system activity
- No guaranteed execution: If the BEAM VM crashes, disarm callbacks won't run
- Priority is best-effort: While the safety controller runs at high scheduler priority, this doesn't guarantee immediate execution
What This Means for Your Robot
The software safety system is suitable for:
- Hobby projects and prototypes
- Research platforms with human supervision
- Systems where delayed shutdown is acceptable
The software safety system is NOT sufficient for:
- Safety-critical applications
- Systems that could cause injury or property damage
- Unattended operation of potentially dangerous equipment
Recommendations for Safety-Critical Systems
For robots where safety is critical, we strongly recommend implementing a hardware safety system in addition to the software controls:
Hardware Kill Switch
Use a dedicated microcontroller (Arduino, ESP32, STM32, etc.) with relays or solid-state switches that can physically disconnect power to actuators:
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ BeamBots │────▶│ Kill Switch │────▶│ Actuators │
│ (software) │ │ (hardware) │ │ │
└─────────────────┘ └──────────────┘ └──────────────┘
│ │
│ Heartbeat signal │
└──────────────────────┘Implementation options:
- Watchdog heartbeat: BeamBots sends periodic pulses. If pulses stop, hardware cuts power automatically
- Manual E-stop: Physical button that immediately disconnects actuator power
- Dual-channel: Both software command AND heartbeat required to enable actuators
Example Hardware Setup
- ESP32 or Arduino Nano monitoring a GPIO heartbeat from the main computer
- Relay module controlling actuator power
- Physical emergency stop button
- LED indicators for system state
Heartbeat Example
# In your robot's controller
defmodule MyRobot.SafetyHeartbeat do
use GenServer
@heartbeat_interval_ms 100
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
def init(opts) do
pin = Keyword.fetch!(opts, :heartbeat_pin)
schedule_heartbeat()
{:ok, %{pin: pin, gpio: opts[:gpio]}}
end
def handle_info(:heartbeat, state) do
# Toggle the heartbeat pin
Pigpio.write(state.gpio, state.pin, 1)
Process.sleep(1)
Pigpio.write(state.gpio, state.pin, 0)
schedule_heartbeat()
{:noreply, state}
end
defp schedule_heartbeat do
Process.send_after(self(), :heartbeat, @heartbeat_interval_ms)
end
endThe Safety Hierarchy
When designing robot safety, think in layers:
Physical E-stop (fastest, most reliable)
- Manual button or switch
- Directly interrupts power
- No software dependency
Hardware watchdog (fails safe on software crash)
- Monitors heartbeat from BeamBots
- Automatic power cutoff if heartbeat stops
- Independent of BEAM VM
BB.Safety controller (software-managed, best effort)
- Centralised arm/disarm state
- Calls registered disarm callbacks
- Handles robot supervisor crashes
Individual process state (application-level)
- Per-actuator enable/disable
- Command validation
- Motion limits
Summary
BeamBots' software safety system provides convenient, centralised safety management for your robot. However, for any application where actuator failures could cause harm, always implement hardware-level safety controls as your primary protection. The software system should be considered a convenience layer, not a safety-critical component.
Quick Reference
| Question | Answer |
|---|---|
| Can BeamBots guarantee actuators stop within X ms? | No |
| Is the software safety system enough for hobby projects? | Yes |
| Should I use hardware safety for research robots? | Recommended |
| Is software safety enough for unattended operation? | No |
| Can disarm callbacks run if my actuator process crashed? | Yes |
| Will disarm callbacks run if the BEAM VM crashes? | No |
| What happens if a disarm callback fails? | Robot enters :error state |
Can I arm a robot in :error state? | No, use force_disarm/1 first |
| Do disarm callbacks run concurrently? | Yes, with 5 second timeout |
| Can commands execute while disarming? | No, rejected with :disarming error |
| Are robots disarmed on shutdown? | Yes, best-effort during controller terminate |
| What happens when a hardware error is reported? | Auto-disarm (default) or custom handling |
| How do I disable auto-disarm on error? | Set auto_disarm_on_error false in settings |