# `BB.Bridge`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/bridge.ex#L5)

Behaviour for parameter bridge GenServers in the BB framework.

Bridges provide bidirectional parameter access between BB and remote systems
(GCS, web UIs, flight controllers).

Bridges do NOT implement safety callbacks - they handle data transport,
not physical hardware control.

## Two Directions

**Outbound (local → remote):** Expose BB's parameters to remote clients
- Subscribe to `[:param]` via `BB.PubSub` in GenServer `init/1`
- Implement `handle_change/3` to push local changes to remote clients
- Remote clients query local params via bridge (calls `BB.Parameter.list/get/set`)

**Inbound (remote → local):** Access remote system's parameters from BB
- Implement `list_remote/1` to enumerate remote parameters
- Implement `get_remote/2` to read remote values
- Implement `set_remote/3` to write remote values
- Implement `subscribe_remote/2` to subscribe to remote changes
- Publish remote changes via PubSub (path structure up to bridge)

## Usage

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

## Options Schema

If your bridge accepts configuration options, pass them via `:options_schema`:

    defmodule MyMavlinkBridge do
      use BB.Bridge,
        options_schema: [
          port: [type: :string, required: true, doc: "Serial port path"],
          baud_rate: [type: :pos_integer, default: 57600, doc: "Baud rate"]
        ]

      @impl BB.Bridge
      def handle_change(_robot, changed, state) do
        send_to_gcs(state.conn, changed)
        {:ok, state}
      end

      # ... other BB.Bridge callbacks
    end

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

    defmodule SimpleBridge do
      use BB.Bridge

      # Must be used as bare module in DSL: bridge :simple, SimpleBridge
    end

## DSL Usage

    parameters do
      bridge :mavlink, {MyMavlinkBridge, port: "/dev/ttyACM0", baud_rate: 115200}
      bridge :phoenix, {PhoenixBridge, url: "ws://gcs.local/socket"}
    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]}`.

## IEx Usage

```elixir
# List remote parameters (e.g., ArduPilot's params)
{:ok, params} = BB.Parameter.list_remote(MyRobot, :mavlink)
# => [%{id: "PITCH_RATE_P", value: 0.1, path: [:mavlink, :pitch, :rate, :p], ...}, ...]

# Get a remote parameter
{:ok, value} = BB.Parameter.get_remote(MyRobot, :mavlink, "PITCH_RATE_P")
# => 0.1

# Set a remote parameter
:ok = BB.Parameter.set_remote(MyRobot, :mavlink, "PITCH_RATE_P", 0.15)

# Subscribe to remote parameter changes (tells bridge to track this param)
:ok = BB.Parameter.subscribe_remote(MyRobot, :mavlink, "PITCH_RATE_P")

# Then subscribe to PubSub using the path from list_remote
BB.PubSub.subscribe(MyRobot, [:mavlink, :pitch, :rate, :p])
```

## Example Implementation

```elixir
defmodule MyMavlinkBridge do
  use BB.Bridge

  # Define a payload type for remote param change messages
  defmodule ParamValue do
    defstruct [:value]

    use BB.Message,
      schema: [value: [type: :any, required: true]]
  end

  # GenServer init - extract robot from :bb metadata, subscribe to param changes
  @impl GenServer
  def init(opts) do
    %{robot: robot} = Keyword.fetch!(opts, :bb)
    BB.PubSub.subscribe(robot, [:param])
    conn = connect_to_mavlink(opts[:conn])
    {:ok, %{robot: robot, conn: conn, subscriptions: MapSet.new()}}
  end

  # Outbound: local param changed, notify remote
  @impl BB.Bridge
  def handle_change(_robot, changed, state) do
    send_param_to_gcs(state.conn, changed)
    {:ok, state}
  end

  # Inbound: list remote params
  @impl BB.Bridge
  def list_remote(state) do
    # Return params with path for PubSub subscriptions
    params = Enum.map(fetch_all_params_from_fc(state.conn), fn {id, value} ->
      %{id: id, value: value, type: nil, doc: nil, path: param_id_to_path(id)}
    end)
    {:ok, params, state}
  end

  # Inbound: get remote param
  @impl BB.Bridge
  def get_remote(param_id, state) do
    value = fetch_param_from_fc(state.conn, param_id)
    {:ok, value, state}
  end

  # Inbound: set remote param
  @impl BB.Bridge
  def set_remote(param_id, value, state) do
    :ok = send_param_set_to_fc(state.conn, param_id, value)
    {:ok, state}
  end

  # Inbound: subscribe to remote param changes
  @impl BB.Bridge
  def subscribe_remote(param_id, state) do
    {:ok, %{state | subscriptions: MapSet.put(state.subscriptions, param_id)}}
  end

  # When FC sends param update, publish via PubSub
  @impl GenServer
  def handle_info({:mavlink_param_value, 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

  # Convert "PITCH_RATE_P" to [:mavlink, :pitch, :rate, :p]
  defp param_id_to_path(param_id) do
    atoms = param_id |> String.downcase() |> String.split("_") |> Enum.map(&String.to_atom/1)
    [:mavlink | atoms]
  end
end
```

# `param_id`

```elixir
@type param_id() :: String.t() | atom()
```

# `remote_param`

```elixir
@type remote_param() :: %{
  id: param_id(),
  value: term(),
  type: atom() | nil,
  doc: String.t() | nil,
  path: [atom()] | nil
}
```

# `robot`

```elixir
@type robot() :: module()
```

# `state`

```elixir
@type state() :: term()
```

# `get_remote`
*optional* 

```elixir
@callback get_remote(param_id(), state()) ::
  {:ok, term(), state()} | {:error, term(), state()}
```

Get a parameter value from the remote system.

# `handle_change`

```elixir
@callback handle_change(robot(), changed :: BB.Parameter.Changed.t(), state()) ::
  {:ok, state()}
```

Handle a local parameter change.

Called when a BB parameter changes locally. The bridge should notify
any subscribed remote clients.

# `list_remote`
*optional* 

```elixir
@callback list_remote(state()) ::
  {:ok, [remote_param()], state()} | {:error, term(), state()}
```

List parameters available on the remote system.

Returns a list of parameter info from the remote (e.g., flight controller).

# `options_schema`
*optional* 

```elixir
@callback options_schema() :: Spark.Options.t()
```

Returns the options schema for this bridge.

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

# `set_remote`
*optional* 

```elixir
@callback set_remote(param_id(), value :: term(), state()) ::
  {:ok, state()} | {:error, term(), state()}
```

Set a parameter value on the remote system.

# `subscribe_remote`
*optional* 

```elixir
@callback subscribe_remote(param_id(), state()) ::
  {:ok, state()} | {:error, term(), state()}
```

Subscribe to changes for a remote parameter.

When the remote parameter changes, the bridge should publish via
`BB.PubSub`. The path structure is up to the bridge implementation.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
