Current section
Files
Jump to
Current section
Files
lib/bb/safety.ex
# SPDX-FileCopyrightText: 2025 James Harton## SPDX-License-Identifier: Apache-2.0defmodule 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.Controllerend