Behaviour and API for actuators in the BB framework.
This module serves two purposes:
- Behaviour - Defines callbacks for actuator implementations
- 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 optionshandle_command/2- Act on an inbound commanddisarm/1- Make hardware safe (called without GenServer state)
Optional Callbacks
handle_options/2- React to parameter changes at runtimehandle_call/3,handle_cast/2,handle_info/2- Standard GenServer-style callbacks, for the driver's own traffichandle_continue/2,terminate/2- Lifecycle callbacksoptions_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
endFor 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
endParameter 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 viaBB.Process.cast. Lower latency for time-critical control.Synchronous (
set_position_sync/5, etc.) - Commands sent viaBB.Process.call. Returns acknowledgement or error.
All three converge on 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)
Summary
Types
How to address an actuator: its unique name, or its full path through the topology. Every function below accepts either.
Callbacks
Make the hardware safe.
Handle synchronous calls other than commands.
Handle asynchronous casts other than commands.
Act on an inbound command.
Handle continue instructions.
Handle all other messages.
Handle parameter changes at runtime.
Initialise actuator state from resolved options.
Returns the options schema for this actuator.
Clean up before termination.
Functions
Send a trajectory command via pubsub.
Send a trajectory command directly to an actuator (bypasses pubsub).
Send a trajectory command and wait for acknowledgement.
Send a hold command via pubsub.
Send a hold command directly to an actuator (bypasses pubsub).
Send a hold command and wait for acknowledgement.
Publish a BeginMotion message for the actuator at path, converting
the supplied motor-space values into joint-space before publishing.
Send an effort (torque/force) command via pubsub.
Send an effort command directly to an actuator (bypasses pubsub).
Send an effort command and wait for acknowledgement.
Send a position command via pubsub.
Send a position command directly to an actuator (bypasses pubsub).
Send a position command and wait for acknowledgement.
Send a velocity command via pubsub.
Send a velocity command directly to an actuator (bypasses pubsub).
Send a velocity command and wait for acknowledgement.
Send a stop command via pubsub.
Send a stop command directly to an actuator (bypasses pubsub).
Send a stop command and wait for acknowledgement.
Translate a motor-space outbound message into joint-space using the
transmission of the joint above the actuator at actuator_path.
Types
Callbacks
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.
@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 GenServer.handle_call/3. Commands arrive at
handle_command/2 regardless of transport.
@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 GenServer.handle_cast/2. Commands arrive at
handle_command/2 regardless of transport.
@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}
endCommands 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.
@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 GenServer.handle_continue/2.
@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 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 handle_command/2 instead.
@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.
@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.
@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).
Clean up before termination.
Same semantics as GenServer.terminate/2.
Functions
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
Send a trajectory command directly to an actuator (bypasses pubsub).
@spec follow_trajectory_sync( module(), target(), [keyword() | map()], keyword(), timeout() ) :: {:ok, :accepted | {:accepted, map()}} | {:error, term()}
Send a trajectory command and wait for acknowledgement.
Send a hold command via pubsub.
Instructs the actuator to actively maintain its current position.
Options
:command_id- Correlation ID for feedback tracking
Send a hold command directly to an actuator (bypasses pubsub).
@spec hold_sync(module(), target(), keyword(), timeout()) :: {:ok, :accepted | {:accepted, map()}} | {:error, term()}
Send a hold command and wait for acknowledgement.
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.
Send an effort (torque/force) command via pubsub.
Options
:duration- Duration (milliseconds), nil = until stopped:command_id- Correlation ID for feedback tracking
Send an effort command directly to an actuator (bypasses pubsub).
@spec set_effort_sync(module(), target(), number(), keyword(), timeout()) :: {:ok, :accepted | {:accepted, map()}} | {:error, term()}
Send an effort command and wait for acknowledgement.
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)
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.
@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
Send a velocity command via pubsub.
Options
:duration- Duration (milliseconds), nil = until stopped:command_id- Correlation ID for feedback tracking
Send a velocity command directly to an actuator (bypasses pubsub).
@spec set_velocity_sync(module(), target(), number(), keyword(), timeout()) :: {:ok, :accepted | {:accepted, map()}} | {:error, term()}
Send a velocity command and wait for acknowledgement.
Send a stop command via pubsub.
Options
:mode-:immediate(default) or:decelerate:command_id- Correlation ID for feedback tracking
Send a stop command directly to an actuator (bypasses pubsub).
@spec stop_sync(module(), target(), keyword(), timeout()) :: {:ok, :accepted | {:accepted, map()}} | {:error, term()}
Send a stop command and wait for acknowledgement.
@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.