Current section

Files

Jump to
bb lib bb safety.ex
Raw

lib/bb/safety.ex

# SPDX-FileCopyrightText: 2025 James Harton
#
# SPDX-License-Identifier: Apache-2.0
defmodule BB.Safety do
@moduledoc """
Safety system API.
This module provides the API for arming/disarming robots and managing safety state.
The `disarm/1` callback that components implement is now defined in `BB.Controller`
and `BB.Actuator` behaviours.
## Safety States
- `: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 one or more callbacks failed; hardware may not be safe
When in `:error` state, the robot cannot be armed until `force_disarm/1` is called
to acknowledge the error and reset to `:disarmed`.
Disarm callbacks run concurrently with a timeout. If any callback fails or times out,
the robot transitions to `:error` state.
## Implementing Disarm Callbacks
Controllers and actuators implement the `disarm/1` callback via their behaviours:
defmodule MyActuator do
use GenServer
use BB.Actuator
@impl BB.Actuator
def disarm(opts) do
pin = Keyword.fetch!(opts, :pin)
MyHardware.disable(pin)
:ok
end
def init(opts) do
BB.Safety.register(__MODULE__,
robot: opts[:bb].robot,
path: opts[:bb].path,
opts: [pin: opts[:pin]]
)
# ...
end
end
If your actuator doesn't need special disarm logic, you can implement a no-op:
@impl BB.Actuator
def disarm(_opts), do: :ok
## Important Limitations
The BEAM virtual machine provides soft real-time guarantees, not hard real-time.
Disarm callbacks may be delayed by garbage collection, scheduler load, or other
system activity. For safety-critical applications, always implement hardware-level
safety controls as your primary protection.
See the Safety documentation topic for detailed recommendations.
"""
# --- API (delegates to Controller) ---
@doc """
Check if a robot is armed.
Fast ETS read - does not go through GenServer.
"""
defdelegate armed?(robot_module), to: BB.Safety.Controller
@doc """
Get current safety state for a robot.
Fast ETS read - does not go through GenServer.
Returns `:armed`, `:disarmed`, `:disarming`, or `:error`.
"""
defdelegate state(robot_module), to: BB.Safety.Controller
@doc """
Check if a robot is in error state.
Returns `true` if a disarm operation failed and the robot requires
manual intervention via `force_disarm/1`.
Fast ETS read - does not go through GenServer.
"""
defdelegate in_error?(robot_module), to: BB.Safety.Controller
@doc """
Check if a robot is currently disarming.
Returns `true` while disarm callbacks are running.
Fast ETS read - does not go through GenServer.
"""
defdelegate disarming?(robot_module), to: BB.Safety.Controller
@doc """
Arm the robot.
Goes through the safety controller GenServer to ensure proper state transitions.
Cannot arm if robot is in `:error` state - must call `force_disarm/1` first.
Returns `:ok` or `{:error, :already_armed | :in_error | :not_registered}`.
"""
defdelegate arm(robot_module), to: BB.Safety.Controller
@doc """
Disarm the robot.
Goes through the safety controller GenServer. Calls all registered `disarm/1`
callbacks before updating state. If any callback fails, the robot transitions
to `:error` state instead of `:disarmed`.
## Options
* `:timeout` - timeout in milliseconds for each disarm callback.
Defaults to 5000ms.
Returns `:ok` or `{:error, :already_disarmed | {:disarm_failed, failures}}`.
"""
defdelegate disarm(robot_module, opts \\ []), to: BB.Safety.Controller
@doc """
Force disarm from error state.
Use this function to acknowledge a failed disarm operation and reset the
robot to `:disarmed` state. This should only be called after manually
verifying that hardware is in a safe state.
**WARNING**: This bypasses safety checks. Only use when you have manually
verified that all actuators are disabled and the robot is safe.
Returns `:ok` or `{:error, :not_in_error | :not_registered}`.
"""
defdelegate force_disarm(robot_module), to: BB.Safety.Controller
@doc """
Register a safety handler (actuator/sensor/controller).
Called by processes in their `init/1`. The opts should contain all
hardware-specific parameters needed to call `disarm/1` without GenServer state.
Writes directly to ETS to avoid blocking on the Controller's mailbox.
## Options
- `:robot` (required) - The robot module
- `:path` (required) - The path to this component (for logging)
- `:opts` - Hardware-specific options passed to `disarm/1`
## Example
BB.Safety.register(__MODULE__,
robot: MyRobot,
path: [:arm, :shoulder_joint, :servo],
opts: [pin: 18]
)
"""
defdelegate register(module, opts), to: BB.Safety.Controller
@doc """
Report a hardware error from a component.
This function should be called by controllers, actuators, or sensors when
they detect a hardware error condition. The behaviour depends on the robot's
`auto_disarm_on_error` setting:
- If `true` (default): The robot is automatically disarmed
- If `false`: The error is published but no automatic action is taken
In both cases, a `BB.Safety.HardwareError` message is published to
`[:safety, :error]` for subscribers to handle.
## Parameters
- `robot_module` - The robot module
- `path` - Path to the component reporting the error (e.g., `[:dynamixel, :servo_1]`)
- `error` - Component-specific error details
## Example
# In a controller detecting servo overheating:
BB.Safety.report_error(MyRobot, [:dynamixel, :servo_1], {:hardware_error, 0x04})
## Customising Error Handling
To implement custom error handling instead of auto-disarm:
defmodule MyRobot do
use BB
settings do
auto_disarm_on_error false
end
end
# Then subscribe to error events:
BB.subscribe(MyRobot, [:safety, :error])
"""
defdelegate report_error(robot_module, path, error), to: BB.Safety.Controller
end