BB.Safety.Controller (bb v0.15.0)

View Source

Global safety controller that owns arm/disarm state for all robots.

Part of BB's application supervision tree (not per-robot), so it survives robot crashes and maintains safety state. Runs at high scheduler priority.

Uses two ETS tables:

  1. Robots table (protected set) - safety state per robot, writes only via GenServer
  2. Handlers table (public bag) - direct writes for registration

Monitors robot supervisors and cleans up state when they terminate.

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.

Note: The executing/idle distinction is handled by Runtime as it's not safety-critical.

Summary

Functions

Arm the robot.

Check if a robot is armed.

Returns a specification to start this module under a supervisor.

Check if a robot is currently disarming.

Force disarm from error state.

Check if a robot is in error state.

Register a safety handler (actuator/sensor/controller).

Register a robot when it starts.

Get list of registered handler modules for a robot.

Report a hardware error from a component.

Get current safety state for a robot.

Types

safety_state()

@type safety_state() :: :disarmed | :armed | :disarming | :error

Functions

arm(robot_module)

@spec arm(module()) :: :ok | {:error, :already_armed | :in_error | :not_registered}

Arm the robot.

Goes through GenServer to ensure proper state transitions and event publishing. Cannot arm if robot is in :error state - must call force_disarm/1 first.

armed?(robot_module)

@spec armed?(module()) :: boolean()

Check if a robot is armed.

Fast ETS read - does not go through GenServer.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

disarm(robot_module, opts \\ [])

@spec disarm(
  module(),
  keyword()
) ::
  :ok | {:error, :already_disarmed | :not_registered | {:disarm_failed, list()}}

Disarm the robot.

Goes through GenServer. Calls all registered disarm/1 callbacks before updating state. If any callback fails, the robot transitions to :error state instead of :disarmed, and this function returns an error with details of the failures.

When in :error state, the robot cannot be armed until force_disarm/1 is called to acknowledge the failure and reset to :disarmed.

Options

  • :timeout - timeout in milliseconds for each disarm callback. Defaults to 5000ms. The GenServer call timeout is set to timeout + 5000 to allow for processing overhead.

disarming?(robot_module)

@spec disarming?(module()) :: boolean()

Check if a robot is currently disarming.

Returns true while disarm callbacks are running.

Fast ETS read - does not go through GenServer.

force_disarm(robot_module)

@spec force_disarm(module()) :: :ok | {:error, :not_in_error | :not_registered}

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.

in_error?(robot_module)

@spec in_error?(module()) :: boolean()

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.

register(module, opts)

@spec register(
  module(),
  keyword()
) :: :ok

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. Uses a cast to set up process monitoring for cleanup on handler restart.

register_robot(robot_module)

@spec register_robot(module()) :: :ok | {:error, term()}

Register a robot when it starts.

Called by BB.Supervisor during robot startup. Sets up monitoring of the robot's supervision tree for automatic cleanup on crash.

registered_handlers(robot_module)

@spec registered_handlers(module()) :: [module()]

Get list of registered handler modules for a robot.

Used by Runtime to verify all safety handlers have registered on startup.

report_error(robot_module, path, error)

@spec report_error(module(), [atom()], term()) :: :ok

Report a hardware error from a component.

Publishes a HardwareError message and optionally triggers disarm based on the robot's auto_disarm_on_error setting.

state(robot_module)

@spec state(module()) :: safety_state()

Get current safety state for a robot.

Fast ETS read - does not go through GenServer. Returns :armed, :disarmed, or :error.