Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/dsls/DSL-BB.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,7 +1354,7 @@ bridge name, child_spec
A parameter protocol bridge for remote access.

Bridges expose robot parameters to remote clients (GCS, web UI, etc.)
and receive parameter updates from them. They implement `BB.Parameter.Protocol`.
and receive parameter updates from them. They implement `BB.Bridge`.

#### Example

Expand Down
11 changes: 6 additions & 5 deletions documentation/topics/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ SPDX-License-Identifier: Apache-2.0

## Overview

BeamBots provides a software safety system through the `BB.Safety` behaviour and
BeamBots provides a software safety system through the `BB.Safety` module and
`BB.Safety.Controller`. This document explains how the system works and its limitations.

## How Safety Works

The safety system has four key components:

1. **BB.Safety behaviour**: Actuators, sensors, and controllers can implement the
`disarm/1` callback to handle hardware shutdown
1. **Behaviour callbacks**: Actuators and controllers implement the `disarm/1` callback
via their behaviours (`BB.Actuator`, `BB.Controller`) to handle hardware shutdown.
Sensors can optionally implement `disarm/1` if they control hardware.
2. **Registration**: Processes register with the safety controller on startup,
providing hardware-specific options needed for stateless disarm
3. **State management**: The controller tracks safety state per robot (`:disarmed`,
Expand Down Expand Up @@ -54,9 +55,9 @@ hardware safety controls for critical applications.
```elixir
defmodule MyServo do
use GenServer
@behaviour BB.Safety
use BB.Actuator

@impl BB.Safety
@impl BB.Actuator
def disarm(opts) do
# This callback can be called even if the GenServer process is dead
pin = Keyword.fetch!(opts, :pin)
Expand Down
39 changes: 18 additions & 21 deletions documentation/tutorials/08-parameter-bridges.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Parameter bridges provide bidirectional access between BB and remote systems:

> **For Roboticists:** Bridges work like MAVLink's parameter protocol or ROS2's parameter services. They let you enumerate, read, write, and subscribe to parameters over any transport.

> **For Elixirists:** Bridges are GenServers that implement the `BB.Parameter.Protocol` behaviour. They're supervised by the robot and integrate with PubSub for change notifications.
> **For Elixirists:** Bridges are GenServers that implement the `BB.Bridge` behaviour. They're supervised by the robot and integrate with PubSub for change notifications.

## Defining Bridges in the DSL

Expand Down Expand Up @@ -49,9 +49,9 @@ Each bridge takes:

Bridges are started as part of the robot's supervision tree.

## The Protocol Behaviour
## The Bridge Behaviour

Bridges implement `BB.Parameter.Protocol`. There are two directions:
Bridges implement `BB.Bridge`. There are two directions:

### Outbound Callback

Expand Down Expand Up @@ -88,8 +88,7 @@ Here's a bridge that logs parameter changes:

```elixir
defmodule MyDebugBridge do
use GenServer
@behaviour BB.Parameter.Protocol
use BB.Bridge

def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
Expand All @@ -108,7 +107,7 @@ defmodule MyDebugBridge do
end

# Handle local parameter changes
@impl BB.Parameter.Protocol
@impl BB.Bridge
def handle_change(_robot, changed, state) do
IO.puts("[DEBUG] Parameter #{inspect(changed.path)} changed:")
IO.puts(" Old: #{inspect(changed.old_value)}")
Expand Down Expand Up @@ -151,8 +150,7 @@ Add the inbound callbacks to your bridge:

```elixir
defmodule MyFlightControllerBridge do
use GenServer
@behaviour BB.Parameter.Protocol
use BB.Bridge

# Define a message type for remote param changes
defmodule ParamValue do
Expand All @@ -177,15 +175,15 @@ defmodule MyFlightControllerBridge do
}}
end

@impl BB.Parameter.Protocol
@impl BB.Bridge
def handle_change(_robot, changed, state) do
# Optionally sync local changes to FC
send_param_to_fc(state.conn, changed)
{:ok, state}
end

# List all parameters on the flight controller
@impl BB.Parameter.Protocol
@impl BB.Bridge
def list_remote(state) do
params = fetch_all_fc_params(state.conn)
|> Enum.map(fn {id, value} ->
Expand All @@ -202,7 +200,7 @@ defmodule MyFlightControllerBridge do
end

# Get a specific parameter from the FC
@impl BB.Parameter.Protocol
@impl BB.Bridge
def get_remote(param_id, state) do
case fetch_fc_param(state.conn, param_id) do
{:ok, value} -> {:ok, value, state}
Expand All @@ -211,14 +209,14 @@ defmodule MyFlightControllerBridge do
end

# Set a parameter on the FC
@impl BB.Parameter.Protocol
@impl BB.Bridge
def set_remote(param_id, value, state) do
:ok = send_fc_param_set(state.conn, param_id, value)
{:ok, state}
end

# Subscribe to FC parameter changes
@impl BB.Parameter.Protocol
@impl BB.Bridge
def subscribe_remote(param_id, state) do
state = %{state | subscriptions: MapSet.put(state.subscriptions, param_id)}
{:ok, state}
Expand Down Expand Up @@ -334,8 +332,7 @@ Here's a complete example with a simulated flight controller:
defmodule MockFCBridge do
@moduledoc "Simulates a flight controller with tunable parameters."

use GenServer
@behaviour BB.Parameter.Protocol
use BB.Bridge

defmodule ParamValue do
defstruct [:value]
Expand Down Expand Up @@ -370,26 +367,26 @@ defmodule MockFCBridge do
}}
end

@impl BB.Parameter.Protocol
@impl BB.Bridge
def handle_change(_robot, _changed, state), do: {:ok, state}

@impl BB.Parameter.Protocol
@impl BB.Bridge
def list_remote(state) do
params = Enum.map(state.params, fn {id, value} ->
%{id: id, value: value, type: :float, doc: nil, path: id_to_path(id)}
end)
{:ok, params, state}
end

@impl BB.Parameter.Protocol
@impl BB.Bridge
def get_remote(param_id, state) do
case Map.fetch(state.params, param_id) do
{:ok, value} -> {:ok, value, state}
:error -> {:error, :not_found, state}
end
end

@impl BB.Parameter.Protocol
@impl BB.Bridge
def set_remote(param_id, value, state) do
if Map.has_key?(state.params, param_id) do
state = %{state | params: Map.put(state.params, param_id, value)}
Expand All @@ -407,7 +404,7 @@ defmodule MockFCBridge do
end
end

@impl BB.Parameter.Protocol
@impl BB.Bridge
def subscribe_remote(param_id, state) do
{:ok, %{state | subscriptions: MapSet.put(state.subscriptions, param_id)}}
end
Expand Down Expand Up @@ -472,7 +469,7 @@ Parameter bridges enable:
- **Bidirectional sync:** Keep parameters in sync across systems

Key points:
- Bridges implement `BB.Parameter.Protocol`
- Bridges implement `BB.Bridge`
- Use `init/2` and `handle_change/3` for outbound (local changes)
- Use `list_remote/1`, `get_remote/2`, `set_remote/3` for inbound (remote access)
- Each bridge is supervised independently for fault isolation
Expand Down
135 changes: 132 additions & 3 deletions lib/bb/actuator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,91 @@

defmodule BB.Actuator do
@moduledoc """
Interface for sending commands to actuators.
Behaviour and API for actuator GenServers 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 `disarm/1` callback for safety.

## Usage

The `use BB.Actuator` macro:
- Adds `use GenServer` (you must implement GenServer callbacks)
- Adds `@behaviour BB.Actuator`
- Optionally defines `options_schema/0` if you pass the `:options_schema` option

### Required Callbacks

- `disarm/1` - Called when the robot is disarmed or crashes. Must work without
GenServer state (the process may have crashed).

### 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 disarm(opts) do
# Make hardware safe - called without GenServer state
MyHardware.disable(opts[:controller], opts[:channel])
:ok
end

@impl GenServer
def init(opts) do
channel = Keyword.fetch!(opts, :channel)
bb = Keyword.fetch!(opts, :bb)

BB.Safety.register(__MODULE__,
robot: bb.robot,
path: bb.path,
opts: [channel: channel, controller: opts[:controller]]
)

{:ok, %{channel: channel, bb: bb}}
end
end

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

defmodule SimpleActuator do
use BB.Actuator

# Must be used as bare module in DSL: actuator :motor, SimpleActuator

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

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

### Auto-injected Options

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

## API

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

## Delivery Methods
### Delivery Methods

- **Pubsub** (`set_position/4`, etc.) - Commands published to `[:actuator | path]`.
Enables logging, replay, and multi-subscriber patterns. Actuators receive
Expand All @@ -21,7 +100,7 @@ defmodule BB.Actuator do
- **Synchronous** (`set_position_sync/5`, etc.) - Commands sent via `BB.Process.call`.
Returns acknowledgement or error. Actuators respond via `handle_call/3`.

## Examples
### Examples

# Pubsub delivery (for kinematics/orchestration)
BB.Actuator.set_position(MyRobot, [:base_link, :shoulder, :servo], 1.57)
Expand All @@ -33,6 +112,56 @@ defmodule BB.Actuator do
{:ok, :accepted} = BB.Actuator.set_position_sync(MyRobot, :shoulder_servo, 1.57)
"""

# ----------------------------------------------------------------------------
# Behaviour
# ----------------------------------------------------------------------------

@doc """
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).
"""
@callback options_schema() :: Spark.Options.t()

@doc """
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 disarm(opts :: keyword()) :: :ok | {:error, term()}

@optional_callbacks [options_schema: 0]

@doc false
defmacro __using__(opts) do
schema_opts = opts[:options_schema]

quote do
use GenServer
@behaviour BB.Actuator

unquote(
if schema_opts do
quote do
@__bb_options_schema Spark.Options.new!(unquote(schema_opts))

@impl BB.Actuator
def options_schema, do: @__bb_options_schema

defoverridable options_schema: 0
end
end
)
end
end

# ----------------------------------------------------------------------------
# API
# ----------------------------------------------------------------------------

alias BB.Message
alias BB.Message.Actuator.Command

Expand Down
Loading