# `BB.Safety`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/safety.ex#L5)

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.

# `arm`

```elixir
@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?`

Check if a robot is armed.

Fast ETS read - does not go through GenServer.

# `disarm`

```elixir
@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?`

Check if a robot is currently disarming.

Returns `true` while disarm callbacks are running.

Fast ETS read - does not go through GenServer.

# `epoch`

Get the current arm epoch for a robot.

Every successful arm allocates a new epoch, and it is discarded as soon as a
disarm begins, so an epoch identifies one arming session rather than the
robot. Outbound actuator commands are stamped with the epoch current at the
moment they are sent, and refused if they arrive under a different one — see
`BB.Actuator`.

Returns `{:ok, epoch}` while armed and `:error` otherwise. A long-lived
process holding state gathered under one arming session can compare the
epoch it last acted under against this one to find out whether a
disarm/re-arm cycle has happened underneath it:

    case BB.Safety.epoch(MyRobot) do
      {:ok, ^last_epoch} -> :same_session
      {:ok, _epoch} -> :rearmed_since
      :error -> :not_armed
    end

Fast ETS read - does not go through GenServer.

# `force_disarm`

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?`

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`

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`

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`

Get current safety state for a robot.

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
