BB.Safety (bb v0.19.0)

Copy Markdown View Source

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.

Summary

Functions

Arm the robot.

Check if a robot is armed.

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).

Report a hardware error from a component.

Get current safety state for a robot.

Functions

arm(robot_module)

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

Arm the robot.

If the robot's DSL declares a command with arm true (set explicitly, or implicitly when the handler is BB.Command.Arm), this function dispatches that command via BB.Robot.Runtime.execute/3 and awaits its result. The command is responsible for performing whatever work the user wants done on arming (e.g. moving joints to a home pose) and for flipping safety state via BB.Safety.Controller.arm/1.

If no arm-flagged command is defined, this function falls through to the safety controller's direct state-flip behaviour — the historical default.

Cannot arm if the robot is in :error state; call force_disarm/1 first.

Returns :ok or {:error, :already_armed | :in_error | :not_registered | term()}. When routed through a command, the error reason is whatever the command returned (typically a BB.Error.State exception or a :command_failed tuple).

armed?(robot_module)

Check if a robot is armed.

Fast ETS read - does not go through GenServer.

disarm(robot_module, opts \\ [])

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

Disarm the robot.

If the robot's DSL declares a command with disarm true (set explicitly, or implicitly when the handler is BB.Command.Disarm), this function dispatches that command via BB.Robot.Runtime.execute/3 and awaits its result. The command is responsible for any pre-disarm work and for flipping safety state via BB.Safety.Controller.disarm/2. If the command returns successfully, the robot is in whatever state the command left it in (typically :disarmed). If the command returns an error before the safety state has been flipped, the robot is escalated to :error — by the issue's failure semantics, an incomplete disarm sequence means hardware may not be in a safe state.

If no disarm-flagged command is defined, this function falls through to the safety controller's direct disarm behaviour — the historical default.

Options

  • :timeout - timeout in milliseconds for each disarm callback. Defaults to 5000ms. Only applicable when no disarm-flagged command is defined; otherwise the command's own :timeout is used.

Returns :ok or {:error, :already_disarmed | {:disarm_failed, failures} | {:disarm_command_failed, reason} | term()}.

disarming?(robot_module)

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)

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}.

in_error?(robot_module)

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)

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]
)

report_error(robot_module, path, error)

Report a hardware error from a component.

Publishes a BB.Safety.HardwareError message to [:safety, :error] for subscribers to handle. This is a pure notification - it does not disarm the robot or change safety state.

Components that detect an unrecoverable hardware fault should raise or exit instead of (or in addition to) calling this function. The supervisor will restart the offending process; if the restart budget on the topology supervisor is exhausted, the safety controller will force-disarm the robot. This is the OTP-native way to signal hardware failure: let it crash, and let supervision escalate.

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 - publish for observers,
# then crash so the supervisor decides whether to escalate:
BB.Safety.report_error(MyRobot, [:dynamixel, :servo_1], {:hardware_error, 0x04})
raise BB.Error.Hardware.Overheat, servo: 1

state(robot_module)

Get current safety state for a robot.

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