BB.Command behaviour (bb v0.15.0)
View SourceBehaviour for implementing robot commands.
Commands are short-lived GenServers that can react to safety state changes
and other messages during execution. The handle_command/3 callback is the
entry point, returning GenServer-style tuples.
Example
defmodule NavigateToPose do
use BB.Command
@impl BB.Command
def handle_command(%{target_pose: pose}, context, state) do
# Subscribe to position updates
BB.PubSub.subscribe(context.robot_module, [:sensor, :position])
# Start navigation
send_navigation_command(pose)
{:noreply, %{state | target: pose}}
end
@impl BB.Command
def handle_info({:bb, [:sensor, :position], msg}, state) do
if close_enough?(msg.payload.position, state.target) do
{:stop, :normal, %{state | final_pose: msg.payload.position}}
else
{:noreply, state}
end
end
@impl BB.Command
def result(state) do
{:ok, %{final_pose: state.final_pose}}
end
endState Transitions
By default, when a command completes successfully, the robot transitions to
:idle. Commands can override this by returning a next_state option from
result/1:
def result(state) do
{:ok, :armed, next_state: :idle}
endThis is useful for commands like Arm and Disarm that need to control
the robot's state machine.
Execution Model
Commands run as supervised GenServers spawned by the Runtime. The caller
receives the command's pid and can use BB.Command.await/2 or
BB.Command.yield/2 to get the result.
Safety Handling
Commands automatically subscribe to safety state changes. When the robot
begins disarming, handle_safety_state_change/2 is called. The default
implementation stops the command with :disarmed reason. Override this
callback to implement graceful shutdown or to continue execution during
safety transitions.
Parameterised Options
Commands can receive options via child_spec format in the DSL:
commands do
command :move_joint do
handler {MyMoveJointCommand, max_velocity: param([:motion, :max_velocity])}
end
endParamRefs are resolved before init/1 is called. When parameters change,
handle_options/2 is called with the new resolved options.
Summary
Callbacks
Handle synchronous calls.
Handle asynchronous casts.
Execute the command with the given goal.
Handle continue instructions.
Handle other messages.
Handle parameter changes.
Handle safety state changes.
Initialise the command state.
Define the options schema for this command.
Extract the result when the command completes.
Clean up when the command terminates.
Functions
Await the command result, blocking until completion or timeout.
Cancel a running command.
Transition to a new operational state during command execution.
Non-blocking check for command completion.
Types
@type goal() :: map()
@type options() :: [{:next_state, BB.Robot.Runtime.robot_state()}]
@type result() :: term()
@type state() :: term()
Callbacks
@callback handle_call(request :: term(), from :: GenServer.from(), state()) :: {:reply, term(), state()} | {:reply, term(), state(), timeout() | :hibernate | {:continue, term()}} | {:noreply, state()} | {:noreply, state(), timeout() | :hibernate | {:continue, term()}} | {:stop, term(), state()} | {:stop, term(), term(), state()}
Handle synchronous calls.
Standard GenServer callback. The default implementation returns
{:reply, {:error, :not_implemented}, state}.
@callback handle_cast(request :: term(), state()) :: {:noreply, state()} | {:noreply, state(), timeout() | :hibernate | {:continue, term()}} | {:stop, term(), state()}
Handle asynchronous casts.
Standard GenServer callback. The default implementation returns
{:noreply, state}.
@callback handle_command(goal(), BB.Command.Context.t(), state()) :: {:noreply, state()} | {:noreply, state(), timeout() | :hibernate | {:continue, term()}} | {:stop, term(), state()}
Execute the command with the given goal.
Called via handle_continue(:execute) immediately after init/1. This is
the main entry point for command execution.
The handler can:
- Return
{:noreply, state}to continue running (waiting for messages) - Return
{:stop, reason, state}to complete immediately
For commands that complete immediately, simply return {:stop, :normal, state}
with the result stored in state.
@callback handle_continue(continue :: term(), state()) :: {:noreply, state()} | {:noreply, state(), timeout() | :hibernate | {:continue, term()}} | {:stop, term(), state()}
Handle continue instructions.
Standard GenServer callback. The default implementation returns
{:noreply, state}.
@callback handle_info(msg :: term(), state()) :: {:noreply, state()} | {:noreply, state(), timeout() | :hibernate | {:continue, term()}} | {:stop, term(), state()}
Handle other messages.
Standard GenServer callback. The default implementation returns
{:noreply, state}.
Handle parameter changes.
Called when a parameter that this command depends on changes. The new
resolved options are passed in. The default implementation returns
{:ok, state} unchanged.
@callback handle_safety_state_change( new_state :: :disarming | :disarmed | :error, state() ) :: {:continue, state()} | {:stop, term(), state()}
Handle safety state changes.
Called when the robot's safety state transitions to :disarming, :disarmed,
or :error. The default implementation stops the command with :disarmed
reason.
Return {:continue, state} to keep the command running during safety
transitions (use with care).
Initialise the command state.
Called when the command server starts. Receives resolved options including:
:bb- Map with:robot(robot module):goal- The command goal (arguments):context- The command context
The default implementation returns {:ok, Map.new(opts)}.
@callback options_schema() :: Spark.Options.schema()
Define the options schema for this command.
Optional. If defined, options passed to the command handler will be validated against this schema.
Extract the result when the command completes.
Called in terminate/2 to get the result to return to awaiting callers.
Return Values
{:ok, result}- Command succeeded, robot transitions to:idle{:ok, result, options}- Command succeeded with options:next_state: state- Robot transitions to specified state instead of:idle
{:error, reason}- Command failed, robot transitions to:idle
Clean up when the command terminates.
Standard GenServer callback. Called after the result has been extracted and sent to awaiting callers.
Functions
Await the command result, blocking until completion or timeout.
Uses GenServer.call internally, so standard timeout semantics apply.
If the command crashes, returns {:error, {:command_failed, reason}}.
Examples
{:ok, cmd} = MyRobot.navigate(target: pose)
{:ok, result} = BB.Command.await(cmd)
# With custom timeout
{:ok, result} = BB.Command.await(cmd, 30_000)
@spec cancel(pid()) :: :ok
Cancel a running command.
Stops the command server with :cancelled reason. Awaiting callers will
receive {:error, :cancelled} (depending on how result/1 handles this).
@spec transition_state(BB.Command.Context.t(), atom()) :: :ok | {:error, term()}
Transition to a new operational state during command execution.
This function allows a command to change the robot's operational state mid-execution. This is useful for multi-phase commands where different phases require different contexts.
Arguments
context- The command context (passed tohandle_command/3)target_state- The state to transition to (must be defined in DSL)
Returns
:ok- Transition successful{:error, reason}- Transition failed
Example
def handle_command(_goal, context, state) do
# Start in processing state
:ok = BB.Command.transition_state(context, :processing)
# Do work...
send(self(), :start_phase_two)
{:noreply, state}
end
def handle_info(:start_phase_two, context, state) do
# Move to finalising state
:ok = BB.Command.transition_state(context, :finalising)
# Do more work...
{:stop, :normal, state}
end
Non-blocking check for command completion.
Returns nil if the command is still running (timeout), otherwise returns
the result. Use this for polling-style waiting.
Examples
{:ok, cmd} = MyRobot.navigate(target: pose)
case BB.Command.yield(cmd, 100) do
nil -> IO.puts("Still running...")
{:ok, result} -> IO.puts("Done!")
{:error, reason} -> IO.puts("Failed: #{inspect(reason)}")
end