Search documentation of bb

# Custom States and Command Categories

Copy Markdown View Source

In this tutorial, you'll learn how to define custom operational states and use command categories to run multiple commands concurrently.

## Prerequisites

Complete Commands and State Machine. You should understand the basic state machine and how to define commands.

## Beyond Idle

The default state machine has just two operational states: `:disarmed` and `:idle`. This works well for simple robots, but real applications often need more operational modes:

- A data collection robot might have a recording mode
- A reactive robot might have a reacting mode where it responds to stimuli
- A robot running learned behaviours might switch between inference and training modes

Beam Bots lets you define custom operational states that represent these modes.

## Defining Custom States

Add a `states` section to your robot:

    defmodule DataCollectionRobot do
      use BB

      states do
        initial_state :idle  # Default, can be omitted

        state :recording do
          doc "Recording sensor data for dataset collection"
        end

        state :processing do
          doc "Processing recorded data"
        end
      end

      commands do
        command :arm do
          handler BB.Command.Arm
          allowed_states [:disarmed]
        end

        command :disarm do
          handler BB.Command.Disarm
          allowed_states [:idle, :recording, :processing]
          cancel :*  # Can cancel any running commands
        end
      end

      topology do
        link :base_link
      end
    end

The built-in states (`:idle`, `:disarmed`) are always available. Your custom states extend what's possible.

## Transitioning Between States

States can only change via commands - there's no direct API to set the state. This ensures all state transitions are tracked, auditable, and follow the command lifecycle.

### Simple State Transitions with SetState

For straightforward state changes, use the built-in `BB.Command.SetState` handler:

    commands do
      command :enter_recording do
        handler {BB.Command.SetState, to: :recording}
        allowed_states [:idle]
      end

      command :exit_recording do
        handler {BB.Command.SetState, to: :idle}
        allowed_states [:recording]
      end

      command :start_processing do
        handler {BB.Command.SetState, to: :processing}
        allowed_states [:recording]  # Can only process after recording
      end
    end

Use these commands like any other:

    iex> {:ok, _} = BB.Supervisor.start_link(DataCollectionRobot)
    iex> {:ok, cmd} = DataCollectionRobot.arm()
    iex> {:ok, :armed, _} = BB.Command.await(cmd)

    iex> BB.Robot.Runtime.state(DataCollectionRobot)
    :idle

    iex> {:ok, cmd} = DataCollectionRobot.enter_recording()
    iex> {:ok, :recording, _} = BB.Command.await(cmd)

    iex> BB.Robot.Runtime.state(DataCollectionRobot)
    :recording

### State Transitions During Command Execution

Commands that do work over time can transition through multiple states using `BB.Command.transition_state/2`:

    defmodule DataPipelineCommand do
      use BB.Command

      @impl BB.Command
      def handle_command(_goal, context, state) do
        # Start in :recording state
        :ok = BB.Command.transition_state(context, :recording)

        # Begin recording
        send(self(), :start_recording)
        {:noreply, Map.put(state, :context, context)}
      end

      @impl BB.Command
      def handle_info(:start_recording, state) do
        # ... record data ...
        Process.send_after(self(), :finish_recording, 5000)
        {:noreply, state}
      end

      def handle_info(:finish_recording, state) do
        # Transition to processing
        :ok = BB.Command.transition_state(state.context, :processing)

        # Process the data
        send(self(), :process_data)
        {:noreply, state}
      end

      def handle_info(:process_data, state) do
        # ... process data ...
        {:stop, :normal, Map.put(state, :result, {:ok, :pipeline_complete})}
      end

      @impl BB.Command
      def result(%{result: result}) do
        # Return to :idle when complete
        {:ok, result, next_state: :idle}
      end

      def result(_state), do: {:error, :cancelled}
    end

## Querying State

Use `BB.Robot.Runtime` to query the current state:

    # Get the operational state (what mode the robot is in)
    BB.Robot.Runtime.operational_state(MyRobot.Robot)
    # => :idle | :recording | :processing | ...

    # Get the "classic" state (backwards compatible)
    BB.Robot.Runtime.state(MyRobot.Robot)
    # => :disarmed | :idle | :executing | :recording | ...

The difference between `state/1` and `operational_state/1`:

- `operational_state/1` returns the actual operational mode
- `state/1` returns `:executing` when in `:idle` with commands running (for backwards compatibility)

For custom states, both return the actual state regardless of whether commands are running.

## Command Categories

By default, only one command runs at a time. But some robots need concurrent operations:

- Move the arm while recording sensor data
- Blink an LED while executing a motion
- Run multiple sensing operations in parallel

Command categories let you define groups of commands with independent concurrency.

### Defining Categories

Add categories to your `commands` section:

    commands do
      category :motion do
        doc "Physical movement commands"
        concurrency_limit 1  # Only one motion at a time (default)
      end

      category :sensing do
        doc "Sensor and recording commands"
        concurrency_limit 2  # Up to 2 concurrent sensing operations
      end

      category :auxiliary do
        doc "LEDs, sounds, indicators"
        concurrency_limit 3  # Multiple concurrent auxiliary commands
      end

      # Commands specify their category
      command :move_to do
        handler MyMoveCommand
        category :motion
        allowed_states [:idle]
        cancel [:motion]  # Can cancel previous motion commands
      end

      command :record_frame do
        handler MyRecordCommand
        category :sensing
        allowed_states [:idle]
        # No cancel - concurrent sensing up to limit
      end

      command :set_led do
        handler MyLedCommand
        category :auxiliary
        allowed_states [:idle]
        # No cancel - concurrent auxiliary up to limit
      end
    end

### How Categories Work

- Each category has a `concurrency_limit` (default: 1)
- Commands in a category run concurrently up to that limit
- Commands in different categories can run concurrently
- Commands without an explicit category use the `:default` category (limit: 1)

    # Start a motion command
    {:ok, move_cmd} = MyRobot.Robot.move_to(target: position)

    # While moving, start recording (different category - runs concurrently)
    {:ok, record_cmd} = MyRobot.Robot.record_frame(sensor: :camera)

    # Both commands are now running
    BB.Robot.Runtime.executing_commands(MyRobot.Robot)
    # => [
    #   %{name: :move_to, category: :motion, pid: #PID<...>},
    #   %{name: :record_frame, category: :sensing, pid: #PID<...>}
    # ]

### Category Full Behaviour

When a category is at capacity, the behaviour depends on the `cancel` option:

1. If the command has `cancel` that includes the full category, it cancels commands to make room
2. Otherwise, the new command is rejected with `{:error, %BB.Error.Category.Full{}}`

    # Start a motion command
    {:ok, cmd1} = MyRobot.Robot.move_to(target: pos1)

    # Start another motion (same category, at limit)
    {:ok, cmd2} = MyRobot.Robot.move_to(target: pos2)

    # cmd1 is cancelled, cmd2 runs
    # Because :move_to has cancel: [:motion]

The `cancel` option accepts:

- `:*` - cancels all categories
- `[:motion, :sensing]` - cancels specific categories
- `[]` (default) - cannot cancel, errors if category is full

## Introspection APIs

Query the execution state:

    # Is anything executing?
    BB.Robot.Runtime.executing?(MyRobot.Robot)
    # => true | false

    # Is a specific category occupied?
    BB.Robot.Runtime.executing?(MyRobot.Robot, :motion)
    # => true | false

    # List all running commands
    BB.Robot.Runtime.executing_commands(MyRobot.Robot)
    # => [%{name: :move_to, category: :motion, pid: #PID<...>, ...}]

    # Get category availability
    BB.Robot.Runtime.category_availability(MyRobot.Robot)
    # => %{motion: {1, 1}, sensing: {0, 2}, default: {0, 1}}
    #    Format: {current_count, limit}

## Compile-Time Validation

The DSL validates your state and category references at compile time:

    # This will produce a warning:
    command :bad_cmd do
      handler MyHandler
      allowed_states [:nonexistent_state]  # Warning: undefined state
    end

    # This will also produce a warning:
    command :bad_cmd do
      handler MyHandler
      category :nonexistent_category  # Warning: undefined category
    end

## A Complete Example

Here's a robot that collects data while moving:

    defmodule DataCollectorArm do
      use BB

      states do
        state :recording do
          doc "Actively recording sensor data"
        end
      end

      commands do
        category :motion do
          concurrency_limit 1
        end

        category :data do
          concurrency_limit 1
        end

        command :arm do
          handler BB.Command.Arm
          allowed_states [:disarmed]
        end

        command :disarm do
          handler BB.Command.Disarm
          allowed_states [:idle, :recording]
          cancel :*  # Can cancel any running commands
        end

        command :enter_recording do
          handler {BB.Command.SetState, to: :recording}
          allowed_states [:idle]
        end

        command :exit_recording do
          handler {BB.Command.SetState, to: :idle}
          allowed_states [:recording]
        end

        command :move_to do
          handler MoveToCommand
          category :motion
          allowed_states [:idle, :recording]
          cancel [:motion]  # Can cancel previous motion commands
        end

        command :capture_frame do
          handler CaptureFrameCommand
          category :data
          allowed_states [:recording]
          cancel [:data]  # Can cancel previous capture commands
        end
      end

      topology do
        link :base do
          joint :shoulder do
            type :revolute
            axis do
            end
            limit do
              effort(~u(50 newton_meter))
              velocity(~u(2 radian_per_second))
            end
            link :arm
          end
        end
      end
    end

Using it:

    # Start and arm
    {:ok, _} = BB.Supervisor.start_link(DataCollectorArm)
    {:ok, cmd} = DataCollectorArm.arm()
    {:ok, :armed, _} = BB.Command.await(cmd)

    # Enter recording mode
    {:ok, cmd} = DataCollectorArm.enter_recording()
    {:ok, :recording, _} = BB.Command.await(cmd)

    # Now we can move AND capture frames concurrently
    {:ok, move_cmd} = DataCollectorArm.move_to(position: 0.5)
    {:ok, capture_cmd} = DataCollectorArm.capture_frame(sensor: :camera)

    # Both commands run in parallel (different categories)
    BB.Robot.Runtime.executing_commands(DataCollectorArm)
    # => [%{name: :move_to, category: :motion}, %{name: :capture_frame, category: :data}]

    # Wait for both
    BB.Command.await(move_cmd)
    BB.Command.await(capture_cmd)

    # Exit recording mode
    {:ok, cmd} = DataCollectorArm.exit_recording()
    {:ok, :idle, _} = BB.Command.await(cmd)

## Best Practices

1. Use states for operational modes, not for tracking progress. A state like `:recording` is good; a state like `:step_3_of_5` is probably better handled inside a command.

2. Keep category limits low. High concurrency limits can make reasoning about robot behaviour difficult. Most categories should have limit 1.

3. Validate state transitions. Use `allowed_states` to ensure commands can only run in appropriate modes.

4. Consider safety implications. Can your robot safely run concurrent motions? Usually not - keep motion commands in a single category with limit 1.

5. Use SetState for simple transitions. Only implement custom command handlers when you need to do work during the transition.

## What's Next?

You now understand custom states and command categories. Continue exploring:

- Parameters for runtime-adjustable configuration
- Safety for implementing safe hardware control

← Previous Page Simulation Mode
Next Page → How to Add a Custom Command