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

Behaviour and API for actuators in the BB framework.

This module serves two purposes:

1. **Behaviour** - Defines callbacks for actuator implementations
2. **API** - Provides functions for sending commands to actuators

## Behaviour

Actuators receive position/velocity/effort commands and drive hardware.
They must implement the `init/1` and `disarm/1` callbacks.

## Usage

The `use BB.Actuator` macro sets up your module as an actuator callback module.
Your module is NOT a GenServer - the framework provides a wrapper GenServer
(`BB.Actuator.Server`) that delegates to your callbacks.

### Required Callbacks

- `init/1` - Initialise actuator state from resolved options
- `handle_command/2` - Act on an inbound command
- `disarm/1` - Make hardware safe (called without GenServer state)

### Optional Callbacks

- `handle_options/2` - React to parameter changes at runtime
- `handle_call/3`, `handle_cast/2`, `handle_info/2` - Standard GenServer-style
  callbacks, for the driver's own traffic
- `handle_continue/2`, `terminate/2` - Lifecycle callbacks
- `options_schema/0` - Define accepted configuration options

### Options Schema

If your actuator accepts configuration options, pass them via `:options_schema`:

    defmodule MyServoActuator do
      use BB.Actuator,
        options_schema: [
          channel: [type: {:in, 0..15}, required: true, doc: "PWM channel"],
          controller: [type: :atom, required: true, doc: "Controller name"]
        ]

      @impl BB.Actuator
      def init(opts) do
        channel = Keyword.fetch!(opts, :channel)
        bb = Keyword.fetch!(opts, :bb)
        {:ok, %{channel: channel, bb: bb}}
      end

      @impl BB.Actuator
      def disarm(opts) do
        MyHardware.disable(opts[:controller], opts[:channel])
        :ok
      end

      @impl BB.Actuator
      def handle_command(%BB.Message{payload: %Command.Position{} = cmd}, state) do
        MyHardware.write(state.channel, cmd.position)
        {:noreply, state}
      end
    end

For actuators that don't need configuration, omit `:options_schema`:

    defmodule SimpleActuator do
      use BB.Actuator

      @impl BB.Actuator
      def init(opts) do
        {:ok, %{bb: opts[:bb]}}
      end

      @impl BB.Actuator
      def handle_command(_message, state), do: {:noreply, state}

      @impl BB.Actuator
      def disarm(_opts), do: :ok
    end

### Parameter References

Options can reference parameters for runtime-adjustable configuration:

    actuator :motor, {MyMotor, max_effort: param([:motion, :max_effort])}

When the parameter changes, `handle_options/2` is called with the new resolved
options. Override it to update your state accordingly.

### Auto-injected Options

The `:bb` option is automatically provided and should NOT be included in your
`options_schema`. It contains `%{robot: module, path: [atom]}`.

### Safety Registration

Safety registration is automatic - the framework registers your module with
`BB.Safety` using the resolved options. You don't need to call `BB.Safety.register`
manually.

## API

Supports both pubsub delivery (for orchestration, logging, replay) and
direct GenServer delivery (for time-critical control paths).

### Delivery Methods

- **Pubsub** (`set_position/4`, etc.) - Commands published to `[:actuator | path]`.
  Enables logging, replay, and multi-subscriber patterns.

- **Direct** (`set_position!/4`, etc.) - Commands sent directly via `BB.Process.cast`.
  Lower latency for time-critical control.

- **Synchronous** (`set_position_sync/5`, etc.) - Commands sent via `BB.Process.call`.
  Returns acknowledgement or error.

All three converge on `c:handle_command/2`. Which transport a caller chose
is not something a driver has to know about, and choosing one cannot skip
the checks `BB.Actuator.Server` applies on the way in.

### Addressing

Every function accepts either the actuator's unique name or its full path
through the topology. Names are resolved against the robot with
`BB.Robot.actuator_path/2`, so the two are interchangeable:

    BB.Actuator.set_position(MyRobot, :shoulder_servo, 1.57)
    BB.Actuator.set_position(MyRobot, [:base_link, :shoulder, :shoulder_servo], 1.57)

Naming an actuator the robot doesn't have raises `ArgumentError` rather than
publishing to a topic nothing is listening on.

### Examples

    # Pubsub delivery (for kinematics/orchestration)
    BB.Actuator.set_position(MyRobot, :shoulder_servo, 1.57)

    # Direct delivery (for time-critical control)
    BB.Actuator.set_position!(MyRobot, :shoulder_servo, 1.57)

    # Synchronous with acknowledgement
    {:ok, :accepted} = BB.Actuator.set_position_sync(MyRobot, :shoulder_servo, 1.57)

# `target`

```elixir
@type target() :: atom() | [atom()]
```

How to address an actuator: its unique name, or its full path through the
topology. Every function below accepts either.

# `command_payloads`
*optional* 

```elixir
@callback command_payloads(opts :: keyword()) :: [module()]
```

The command payloads this actuator accepts.

Defaults to `default_command_payloads/0` — the six built-in
`BB.Message.Actuator.Command.*` types — which is right for almost every
driver. Override it to either end of the range:

- **Widen it.** A driver whose hardware speaks a command BB doesn't model can
  name its own payload module here, and it will arrive through the same gated
  pipeline as any other command. Without that it would have to subscribe to
  `[:actuator | path]` itself, and those messages reach the driver having
  skipped the arm check.
- **Narrow it.** A port that only ever accepts `Command.Effort` can say so,
  and the framework refuses everything else before the driver sees it.

Called once at `init`, with the resolved options, because the answer isn't
always known at compile time — it may depend on a port or channel named in
the driver's own options.

    @impl BB.Actuator
    def command_payloads(opts) do
      [opts |> Keyword.fetch!(:port) |> MyDriver.command_struct()]
    end

The result is used for both the actuator's pubsub subscription and its
dispatch guard, so narrowing holds across all three transports rather than
only the published one.

Nothing is admitted outside this list, `Stop` included. A driver is never
handed a payload it didn't declare, so it can't be crashed by one it has no
clause for.

# `disarm`

```elixir
@callback disarm(opts :: keyword()) :: :ok | {:error, term()}
```

Make the hardware safe.

Called with the opts provided at registration. Must work without GenServer state.
This callback is required for actuators since they control physical hardware.

# `handle_call`
*optional* 

```elixir
@callback handle_call(request :: term(), from :: GenServer.from(), state :: term()) ::
  {:reply, reply :: term(), new_state :: term()}
  | {:reply, reply :: term(), new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
  | {:stop, reason :: term(), reply :: term(), new_state :: term()}
```

Handle synchronous calls other than commands.

Same semantics as `c:GenServer.handle_call/3`. Commands arrive at
`c:handle_command/2` regardless of transport.

# `handle_cast`
*optional* 

```elixir
@callback handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle asynchronous casts other than commands.

Same semantics as `c:GenServer.handle_cast/2`. Commands arrive at
`c:handle_command/2` regardless of transport.

# `handle_command`

```elixir
@callback handle_command(command :: BB.Message.t(), state :: term()) ::
  {:reply, reply :: term(), new_state :: term()}
  | {:reply, reply :: term(), new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Act on an inbound command.

Called for every command that reaches this actuator, whichever transport
delivered it. By the time it arrives, `BB.Actuator.Server` has checked that
the robot is armed and translated the payload from joint-space into
motor-space, so the values are ready to write to hardware.

The reply is used only by the synchronous transport (`set_position_sync/5`
and friends); it is discarded for pubsub and direct delivery. Returning
`{:noreply, state}` replies `{:ok, :accepted}` to a synchronous caller.

    @impl BB.Actuator
    def handle_command(%BB.Message{payload: %Command.Position{} = cmd}, state) do
      MyHardware.write(state.channel, cmd.position)
      {:noreply, state}
    end

Commands the driver doesn't implement should fall through to a catch-all
clause rather than crashing the actuator - a `Command.Trajectory` sent to a
position-only servo is a caller error, not a hardware fault.

> #### `Command.Stop` is a motion command, not a safety one {: .info}
>
> `Stop` means *cease travelling and become passive* — it's the counterpart to
> `Command.Hold`, which maintains position and resists external force. Its
> `:decelerate` mode makes that plain: nothing that slows down smoothly is an
> emergency stop.
>
> Making hardware safe is `c:disarm/1`, which is robot-wide, runs without
> GenServer state, and leaves the robot unable to move until re-armed. Don't
> reach for `Stop` to do that job.
>
> If you declare `Stop` in `c:command_payloads/1` — and the default does —
> give it a clause that genuinely stops driving, rather than letting a
> catch-all swallow it and report success while the joint keeps moving:
>
> ```elixir
> def handle_command(%BB.Message{payload: %Command.Stop{}}, state) do
>   MyHardware.cut_drive(state.channel)
>   {:noreply, state}
> end
> ```
>

# `handle_continue`
*optional* 

```elixir
@callback handle_continue(continue_arg :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle continue instructions.

Same semantics as `c:GenServer.handle_continue/2`.

# `handle_info`
*optional* 

```elixir
@callback handle_info(msg :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle all other messages.

Same semantics as `c:GenServer.handle_info/2`. Messages from topics the
driver subscribed to itself arrive here untouched - the server neither
transforms nor intercepts them. Commands addressed to this actuator arrive
at `c:handle_command/2` instead.

# `handle_options`
*optional* 

```elixir
@callback handle_options(new_opts :: keyword(), state :: term()) ::
  {:ok, new_state :: term()} | {:stop, reason :: term()}
```

Handle parameter changes at runtime.

Called when a referenced parameter changes. The `new_opts` contain all options
with the updated parameter value(s) resolved.

Return `{:ok, new_state}` to update state, or `{:stop, reason}` to shut down.

# `init`

```elixir
@callback init(opts :: keyword()) ::
  {:ok, state :: term()}
  | {:ok, state :: term(), timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term()}
  | :ignore
```

Initialise actuator state from resolved options.

Called with options after parameter references have been resolved.
The `:bb` key contains `%{robot: module, path: [atom]}`.

Return `{:ok, state}` or `{:ok, state, timeout_or_continue}` on success,
`{:stop, reason}` to abort startup, or `:ignore` to skip this actuator.

# `options_schema`

```elixir
@callback options_schema() :: Spark.Options.t()
```

Returns the options schema for this actuator.

The schema should NOT include the `:bb` option - it is auto-injected.
If this callback is not implemented, the module cannot accept options
in the DSL (must be used as a bare module).

# `terminate`
*optional* 

```elixir
@callback terminate(reason :: term(), state :: term()) :: term()
```

Clean up before termination.

Same semantics as `c:GenServer.terminate/2`.

# `default_command_payloads`

```elixir
@spec default_command_payloads() :: [module()]
```

The command payloads an actuator accepts unless it says otherwise.

These are the payload types `BB.Actuator`'s own API can produce, so they are
the set every driver is expected to understand — or at least to ignore
gracefully.

# `follow_trajectory`

```elixir
@spec follow_trajectory(module(), target(), [keyword() | map()], keyword()) :: :ok
```

Send a trajectory command via pubsub.

## Waypoint Structure

Each waypoint should be a keyword list or map with:
- `position` - Position (radians or metres)
- `velocity` - Velocity (rad/s or m/s)
- `acceleration` - Acceleration (rad/s² or m/s²)
- `time_from_start` - Time from trajectory start (milliseconds)

## Options

- `:repeat` - Number of repetitions: positive integer or `:forever` (default 1)
- `:command_id` - Correlation ID for feedback tracking

# `follow_trajectory!`

```elixir
@spec follow_trajectory!(module(), target(), [keyword() | map()], keyword()) :: :ok
```

Send a trajectory command directly to an actuator (bypasses pubsub).

# `follow_trajectory_sync`

```elixir
@spec follow_trajectory_sync(
  module(),
  target(),
  [keyword() | map()],
  keyword(),
  timeout()
) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send a trajectory command and wait for acknowledgement.

# `hold`

```elixir
@spec hold(module(), target(), keyword()) :: :ok
```

Send a hold command via pubsub.

Instructs the actuator to actively maintain its current position.

## Options

- `:command_id` - Correlation ID for feedback tracking

# `hold!`

```elixir
@spec hold!(module(), target(), keyword()) :: :ok
```

Send a hold command directly to an actuator (bypasses pubsub).

# `hold_sync`

```elixir
@spec hold_sync(module(), target(), keyword(), timeout()) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send a hold command and wait for acknowledgement.

# `publish_begin_motion`

```elixir
@spec publish_begin_motion(module(), [atom()], keyword()) :: :ok
```

Publish a `BeginMotion` message for the actuator at `path`, converting
the supplied motor-space values into joint-space before publishing.

The driver builds the message in motor-space (the only coordinate space
it knows about); this helper looks up the joint above the actuator,
resolves its transmission against the current parameter store, applies
`BB.Transmission.unapply_to_payload/2`, and publishes the joint-space
message to `[:actuator | path]`.

`path` is the actuator's full path (i.e. the `:bb.path` injected into
driver opts). `opts` is the keyword list accepted by
`BB.Message.Actuator.BeginMotion`'s schema, with `:initial_position`,
`:target_position`, `:peak_velocity`, and `:acceleration` in
motor-space.

# `set_effort`

```elixir
@spec set_effort(module(), target(), number(), keyword()) :: :ok
```

Send an effort (torque/force) command via pubsub.

## Options

- `:duration` - Duration (milliseconds), nil = until stopped
- `:command_id` - Correlation ID for feedback tracking

# `set_effort!`

```elixir
@spec set_effort!(module(), target(), number(), keyword()) :: :ok
```

Send an effort command directly to an actuator (bypasses pubsub).

# `set_effort_sync`

```elixir
@spec set_effort_sync(module(), target(), number(), keyword(), timeout()) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send an effort command and wait for acknowledgement.

# `set_position`

```elixir
@spec set_position(module(), target(), number(), keyword()) :: :ok
```

Send a position command via pubsub.

The command is published to `[:actuator | path]` where subscribers can
receive it via `handle_info({:bb, path, message}, state)`.

## Options

- `:velocity` - Velocity hint (rad/s or m/s)
- `:duration` - Duration hint (milliseconds)
- `:command_id` - Correlation ID for feedback tracking

## Examples

    BB.Actuator.set_position(MyRobot, [:base_link, :shoulder, :servo], 1.57)
    BB.Actuator.set_position(MyRobot, [:shoulder, :servo], 1.57, velocity: 0.5)

# `set_position!`

```elixir
@spec set_position!(module(), target(), number(), keyword()) :: :ok
```

Send a position command directly to an actuator (bypasses pubsub).

Uses `BB.Process.cast` for fire-and-forget delivery. The actuator receives
the command via `handle_cast({:command, message}, state)`.

## Options

Same as `set_position/4`.

# `set_position_sync`

```elixir
@spec set_position_sync(module(), target(), number(), keyword(), timeout()) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send a position command and wait for acknowledgement.

Uses `BB.Process.call` for synchronous delivery. Returns the actuator's
response or raises on timeout.

## Options

Same as `set_position/4`, plus:
- Fifth argument is timeout in milliseconds (default 5000)

## Returns

- `{:ok, :accepted}` - Command accepted
- `{:ok, :accepted, map()}` - Command accepted with additional info
- `{:error, reason}` - Command rejected

# `set_velocity`

```elixir
@spec set_velocity(module(), target(), number(), keyword()) :: :ok
```

Send a velocity command via pubsub.

## Options

- `:duration` - Duration (milliseconds), nil = until stopped
- `:command_id` - Correlation ID for feedback tracking

# `set_velocity!`

```elixir
@spec set_velocity!(module(), target(), number(), keyword()) :: :ok
```

Send a velocity command directly to an actuator (bypasses pubsub).

# `set_velocity_sync`

```elixir
@spec set_velocity_sync(module(), target(), number(), keyword(), timeout()) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send a velocity command and wait for acknowledgement.

# `stop`

```elixir
@spec stop(module(), target(), keyword()) :: :ok
```

Send a stop command via pubsub.

## Options

- `:mode` - `:immediate` (default) or `:decelerate`
- `:command_id` - Correlation ID for feedback tracking

# `stop!`

```elixir
@spec stop!(module(), target(), keyword()) :: :ok
```

Send a stop command directly to an actuator (bypasses pubsub).

# `stop_sync`

```elixir
@spec stop_sync(module(), target(), keyword(), timeout()) ::
  {:ok, :accepted | {:accepted, map()}} | {:error, term()}
```

Send a stop command and wait for acknowledgement.

# `to_joint_space`

```elixir
@spec to_joint_space(module(), [atom()], BB.Message.t()) :: BB.Message.t()
```

Translate a motor-space outbound message into joint-space using the
transmission of the joint above the actuator at `actuator_path`.

Convenient for callers that build a message in motor-space and then
publish it on a topic of their own choosing — e.g. a controller
publishing `JointState` on a sensor topic. Performs a fresh
transmission resolution against the current parameter store on every
call, so it stays correct across runtime parameter changes without
the caller needing to subscribe.

Returns the message unchanged when the joint has no transmission.

---

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