Parameter Bridges
View SourceIn this tutorial, you'll learn how to connect your robot's parameters to remote systems using parameter bridges.
Prerequisites
Complete Parameters. You should understand how to define and access parameters at runtime.
What Are Parameter Bridges?
Parameter bridges provide bidirectional access between BB and remote systems:
- Outbound (local → remote): Expose BB's parameters to ground control stations, web UIs, or debugging tools
- Inbound (remote → local): Access parameters from flight controllers, external sensors, or other 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.Protocolbehaviour. They're supervised by the robot and integrate with PubSub for change notifications.
Defining Bridges in the DSL
Add bridges to your parameters section:
defmodule MyRobot do
use BB
parameters do
param :max_speed, type: :float, default: 1.0
bridge :debug, {MyDebugBridge, port: 4000}
end
topology do
link :base
end
endEach bridge takes:
- A name (atom) - used to identify the bridge
- A child spec - module or
{module, options}tuple
Bridges are started as part of the robot's supervision tree.
The Protocol Behaviour
Bridges implement BB.Parameter.Protocol. There are two directions:
Outbound Callback
Handle local parameter changes and notify remote clients:
@callback handle_change(robot :: module(), changed :: BB.Parameter.Changed.t(), state) ::
{:ok, state}Bridges should also subscribe to [:param] via BB.PubSub in their GenServer init/1.
Inbound Callbacks (Optional)
Access parameters on a remote system:
@callback list_remote(state) ::
{:ok, [remote_param()], state} | {:error, term(), state}
@callback get_remote(param_id, state) ::
{:ok, term(), state} | {:error, term(), state}
@callback set_remote(param_id, value :: term(), state) ::
{:ok, state} | {:error, term(), state}
@callback subscribe_remote(param_id, state) ::
{:ok, state} | {:error, term(), state}Implementing a Simple Bridge
Here's a bridge that logs parameter changes:
defmodule MyDebugBridge do
use GenServer
@behaviour BB.Parameter.Protocol
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
# GenServer init - extract robot, subscribe to param changes
@impl GenServer
def init(opts) do
%{robot: robot} = Keyword.fetch!(opts, :bb)
BB.PubSub.subscribe(robot, [:param])
{:ok, %{
robot: robot,
port: Keyword.get(opts, :port, 4000)
}}
end
# Handle local parameter changes
@impl BB.Parameter.Protocol
def handle_change(_robot, changed, state) do
IO.puts("[DEBUG] Parameter #{inspect(changed.path)} changed:")
IO.puts(" Old: #{inspect(changed.old_value)}")
IO.puts(" New: #{inspect(changed.new_value)}")
{:ok, state}
end
# Receive PubSub messages and dispatch to handle_change
@impl GenServer
def handle_info({:bb, [:param | _path], message}, state) do
{:ok, new_state} = handle_change(state.robot, message.payload, state)
{:noreply, new_state}
end
def handle_info(_msg, state) do
{:noreply, state}
end
endNow when parameters change, you'll see debug output:
iex> {:ok, _} = BB.Supervisor.start_link(MyRobot)
iex> BB.Parameter.set(MyRobot, [:max_speed], 2.0)
[DEBUG] Parameter [:max_speed] changed:
Old: 1.0
New: 2.0
:okAccessing Remote Parameters
Bridges can also expose parameters from remote systems. This is useful when your robot communicates with a flight controller that has its own parameters.
Implementing Inbound Access
Add the inbound callbacks to your bridge:
defmodule MyFlightControllerBridge do
use GenServer
@behaviour BB.Parameter.Protocol
# Define a message type for remote param changes
defmodule ParamValue do
defstruct [:value]
use BB.Message,
schema: [value: [type: :any, required: true]]
end
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
@impl GenServer
def init(opts) do
%{robot: robot} = Keyword.fetch!(opts, :bb)
BB.PubSub.subscribe(robot, [:param])
conn = connect_to_fc(opts[:device])
{:ok, %{
robot: robot,
conn: conn,
subscriptions: MapSet.new()
}}
end
@impl BB.Parameter.Protocol
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
def list_remote(state) do
params = fetch_all_fc_params(state.conn)
|> Enum.map(fn {id, value} ->
%{
id: id,
value: value,
type: nil,
doc: nil,
path: param_id_to_path(id)
}
end)
{:ok, params, state}
end
# Get a specific parameter from the FC
@impl BB.Parameter.Protocol
def get_remote(param_id, state) do
case fetch_fc_param(state.conn, param_id) do
{:ok, value} -> {:ok, value, state}
:error -> {:error, :not_found, state}
end
end
# Set a parameter on the FC
@impl BB.Parameter.Protocol
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
def subscribe_remote(param_id, state) do
state = %{state | subscriptions: MapSet.put(state.subscriptions, param_id)}
{:ok, state}
end
# When FC sends a param update, publish via PubSub
@impl GenServer
def handle_info({:fc_param_changed, param_id, value}, state) do
if MapSet.member?(state.subscriptions, param_id) do
path = param_id_to_path(param_id)
message = BB.Message.new!(ParamValue, :remote, value: value)
BB.PubSub.publish(state.robot, path, message)
end
{:noreply, state}
end
def handle_info({:bb, [:param | _], message}, state) do
{:ok, new_state} = handle_change(state.robot, message.payload, state)
{:noreply, new_state}
end
def handle_info(_msg, state), do: {:noreply, state}
# Convert "PITCH_RATE_P" to [:fc, :pitch, :rate, :p]
defp param_id_to_path(param_id) do
atoms = param_id
|> String.downcase()
|> String.split("_")
|> Enum.map(&String.to_atom/1)
[:fc | atoms]
end
# Placeholder - implement actual FC communication
defp connect_to_fc(_device), do: :connected
defp fetch_all_fc_params(_conn), do: [{"PITCH_RATE_P", 0.1}, {"ROLL_RATE_P", 0.15}]
defp fetch_fc_param(_conn, _id), do: {:ok, 0.1}
defp send_fc_param_set(_conn, _id, _value), do: :ok
defp send_param_to_fc(_conn, _changed), do: :ok
endUsing Remote Parameters from IEx
Access remote parameters through the BB.Parameter API:
iex> {:ok, _} = BB.Supervisor.start_link(MyRobot)
# List parameters on the flight controller
iex> {:ok, params} = BB.Parameter.list_remote(MyRobot, :fc)
{:ok, [
%{id: "PITCH_RATE_P", value: 0.1, path: [:fc, :pitch, :rate, :p], ...},
%{id: "ROLL_RATE_P", value: 0.15, path: [:fc, :roll, :rate, :p], ...}
]}
# Get a specific parameter
iex> {:ok, value} = BB.Parameter.get_remote(MyRobot, :fc, "PITCH_RATE_P")
{:ok, 0.1}
# Set a parameter on the FC
iex> :ok = BB.Parameter.set_remote(MyRobot, :fc, "PITCH_RATE_P", 0.12)
:ok
# Subscribe to changes
iex> :ok = BB.Parameter.subscribe_remote(MyRobot, :fc, "PITCH_RATE_P")
:ok
# Subscribe to PubSub using the path from list_remote
iex> BB.PubSub.subscribe(MyRobot, [:fc, :pitch, :rate, :p])
{:ok, #PID<0.234.0>}Multiple Bridges
A robot can have multiple bridges for different purposes:
parameters do
group :motion do
param :max_speed, type: :float, default: 1.0
end
# Expose params to web UI
bridge :web, {MyPhoenixBridge, url: "ws://localhost:4000/socket"}
# Connect to flight controller
bridge :fc, {MyMavlinkBridge, device: "/dev/ttyACM0"}
# Debug logging
bridge :debug, MyDebugBridge
endEach bridge operates independently:
- Changes to local params notify all bridges
- Remote params are accessed by bridge name
Bridge Supervision
Bridges are supervised with fault isolation. If a bridge crashes:
- Other bridges continue operating
- The crashed bridge is restarted
- Local parameters remain accessible
This is handled by BB.BridgeSupervisor, which is separate from sensor and controller supervisors.
Complete Example: Mock Flight Controller
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
defmodule ParamValue do
defstruct [:value]
use BB.Message,
schema: [value: [type: :any, required: true]]
end
# Simulated FC parameters
@fc_params %{
"PITCH_RATE_P" => 0.1,
"PITCH_RATE_I" => 0.01,
"PITCH_RATE_D" => 0.005,
"ROLL_RATE_P" => 0.1,
"ROLL_RATE_I" => 0.01,
"ROLL_RATE_D" => 0.005,
"YAW_RATE_P" => 0.15,
"THR_HOVER" => 0.5
}
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
@impl GenServer
def init(opts) do
%{robot: robot} = Keyword.fetch!(opts, :bb)
BB.PubSub.subscribe(robot, [:param])
{:ok, %{
robot: robot,
params: @fc_params,
subscriptions: MapSet.new()
}}
end
@impl BB.Parameter.Protocol
def handle_change(_robot, _changed, state), do: {:ok, state}
@impl BB.Parameter.Protocol
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
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
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)}
# Notify subscribers
if MapSet.member?(state.subscriptions, param_id) do
path = id_to_path(param_id)
message = BB.Message.new!(ParamValue, :fc, value: value)
BB.PubSub.publish(state.robot, path, message)
end
{:ok, state}
else
{:error, :not_found, state}
end
end
@impl BB.Parameter.Protocol
def subscribe_remote(param_id, state) do
{:ok, %{state | subscriptions: MapSet.put(state.subscriptions, param_id)}}
end
@impl GenServer
def handle_info({:bb, [:param | _], message}, state) do
{:ok, new_state} = handle_change(state.robot, message.payload, state)
{:noreply, new_state}
end
def handle_info(_msg, state), do: {:noreply, state}
defp id_to_path(param_id) do
atoms = param_id |> String.downcase() |> String.split("_") |> Enum.map(&String.to_atom/1)
[:fc | atoms]
end
endUse it in your robot:
defmodule TestRobot do
use BB
parameters do
param :armed, type: :boolean, default: false
bridge :fc, MockFCBridge
end
topology do
link :base
end
endTry it out:
iex> {:ok, _} = BB.Supervisor.start_link(TestRobot)
iex> {:ok, params} = BB.Parameter.list_remote(TestRobot, :fc)
iex> Enum.map(params, & &1.id)
["PITCH_RATE_P", "PITCH_RATE_I", "PITCH_RATE_D", "ROLL_RATE_P", ...]
iex> BB.Parameter.get_remote(TestRobot, :fc, "PITCH_RATE_P")
{:ok, 0.1}
iex> BB.Parameter.set_remote(TestRobot, :fc, "PITCH_RATE_P", 0.15)
:ok
iex> BB.Parameter.get_remote(TestRobot, :fc, "PITCH_RATE_P")
{:ok, 0.15}Summary
Parameter bridges enable:
- Local → Remote: Expose BB parameters to external tools
- Remote → Local: Access parameters from connected systems
- Bidirectional sync: Keep parameters in sync across systems
Key points:
- Bridges implement
BB.Parameter.Protocol - Use
init/2andhandle_change/3for outbound (local changes) - Use
list_remote/1,get_remote/2,set_remote/3for inbound (remote access) - Each bridge is supervised independently for fault isolation
- Access remote params via
BB.Parameter.{list,get,set}_remote
What's Next?
You've now learned the complete parameter system. You can:
- Define parameters in the DSL
- Read and write them at runtime
- Subscribe to changes via PubSub
- Connect to remote systems via bridges
For reference documentation on all parameter options, see the DSL Reference.