Search documentation of bb

# BB.Command behaviour (bb v0.17.0)

Copy Markdown View Source

Behaviour 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
    end

## State 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}
    end

This 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
    end

ParamRefs are resolved before `init/1` is called. When parameters change, `handle_options/2` is called with the new resolved options.

## Summary

### Types

goal()
options()
result()
state()

### Callbacks

handle_call(request, from, state)

Handle synchronous calls.

handle_cast(request, state)

Handle asynchronous casts.

handle_command(goal, t, state)

Execute the command with the given goal.

handle_continue(continue, state)

Handle continue instructions.

handle_info(msg, state)

Handle other messages.

handle_options(new_opts, state)

Handle parameter changes.

handle_safety_state_change(new_state, state)

Handle safety state changes.

init(opts)

Initialise the command state.

options_schema()

Define the options schema for this command.

result(state)

Extract the result when the command completes.

terminate(reason, state)

Clean up when the command terminates.

### Functions

await(pid, timeout \\ 5000)

Await the command result, blocking until completion or timeout.

cancel(pid)

Cancel a running command.

transition_state(context, target_state)

Transition to a new operational state during command execution.

yield(pid, timeout \\ 0)

Non-blocking check for command completion.

## Types

# goal()

    @type goal() :: map()

# options()

    @type options() :: [{:next_state, BB.Robot.Runtime.robot_state()}]

# result()

    @type result() :: term()

# state()

    @type state() :: term()

## Callbacks

# handle_call(request, from, state)

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

# handle_cast(request, 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}`.

# handle_command(goal, t, 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.

# handle_continue(continue, 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}`.

# handle_info(msg, 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_options(new_opts, state)

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

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.

# handle_safety_state_change(new_state, state)

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

# init(opts)

    @callback init(opts :: keyword()) :: {:ok, state()} | {:stop, term()}

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

# options_schema()

(optional)

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

# result(state)

    @callback result(state()) ::
      {:ok, result()} | {:ok, result(), options()} | {:error, term()}

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`

# terminate(reason, state)

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

Clean up when the command terminates.

Standard GenServer callback. Called after the result has been extracted and sent to awaiting callers.

## Functions

# await(pid, timeout \\ 5000)

    @spec await(pid(), timeout()) ::
      {:ok, term()} | {:ok, term(), options()} | {:error, term()}

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)

# cancel(pid)

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

# transition_state(context, target_state)

    @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 to `handle_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

# yield(pid, timeout \\ 0)

    @spec yield(pid(), timeout()) ::
      {:ok, term()} | {:ok, term(), options()} | {:error, term()} | nil

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