# `BB.TUI.Robot`
[🔗](https://github.com/mcass19/bb_tui/blob/v0.3.0/lib/bb/tui/robot.ex#L1)

Routing layer for BB.* calls used by the TUI.

When the TUI is launched against a remote BEAM node (via
`BB.TUI.run(robot, node: :"robot@host")`) all robot data needs to come
from that node — but the rendering, keyboard input and process state
live locally on the developer's machine. This module is the boundary
that decides where each call goes:

  * `node == nil` — call the local `BB.*` module directly.
  * `node` is a connected remote node atom — call via `BB.TUI.Rpc`,
    a thin wrapper over `:rpc.call/4` that exists so the cross-node
    paths can be mocked in tests (`:rpc` itself is a sticky kernel
    module that cannot be replaced at runtime).

## PubSub across nodes

`BB.PubSub` is built on `Registry`, which is node-local, so we cannot
simply call `BB.subscribe/2` from the dev node and expect to receive
messages published on the robot node. Instead `subscribe/3` spawns a
small relay process on the remote node via `Node.spawn_link/2`. The
relay subscribes locally there and forwards every `{:bb, _, _}`
message back to the TUI process on the dev node.

This is the only "process with a runtime reason" introduced by the
remote path: it exists because we need (1) a place to receive PubSub
messages on the remote node and (2) fault isolation if the remote node
goes away (the link will tear it down on disconnect).

# `maybe_node`

```elixir
@type maybe_node() :: node() | nil
```

Either nil for local execution or a connected remote node.

# `arm`

```elixir
@spec arm(module(), maybe_node()) :: term()
```

Arms the robot.

# `disarm`

```elixir
@spec disarm(module(), maybe_node()) :: term()
```

Disarms the robot.

# `discover_commands`

```elixir
@spec discover_commands(module(), maybe_node()) :: [map()]
```

Returns the list of declared commands for the robot, normalized for the
UI. Returns `[]` if the command DSL is not available or raises.

Each command map has the shape:

    %{
      name: atom(),
      handler: term(),
      timeout: integer() | :infinity,
      allowed_states: [atom()],
      arguments: [%{name: atom(), type: String.t(), required: boolean(),
                    default: term(), doc: String.t() | nil}]
    }

Argument types are normalized to strings: `"boolean"`, `"integer"`,
`"float"`, `"atom"`, `"string"`, or `"enum:[a, b, c]"`. Mirrors
`BB.LiveView.Components.Command` so both UIs see the same shape.

# `execute_command`

```elixir
@spec execute_command(module(), atom(), map(), maybe_node()) ::
  {:ok, pid()} | {:error, term()}
```

Executes a command on the runtime.

Returns whatever the runtime returns — typically `{:ok, pid}` for the
command process, or `{:error, reason}`. Cross-node pids are tracked
transparently by the Erlang distribution layer.

# `force_disarm`

```elixir
@spec force_disarm(module(), maybe_node()) :: term()
```

Force-disarms the robot from an error state.

# `get_robot`

```elixir
@spec get_robot(module(), maybe_node()) :: term()
```

Returns the runtime robot struct (joints, actuators, etc.).

# `list_bridges`

```elixir
@spec list_bridges(module(), maybe_node()) :: [map()]
```

Returns the list of declared parameter bridges for the robot.

Each bridge is rendered down to `%{name: atom(), simulation: atom()}` for
the UI; the underlying `BB.Dsl.Bridge` struct is not exposed so callers
don't depend on Spark internals. Bridges where `:simulation` is `:omit`
while the robot is in simulation mode are filtered out (matching
`bb_liveview`'s discovery rules).

Returns `[]` when the DSL is unavailable or raises.

# `list_parameters`

```elixir
@spec list_parameters(module(), keyword(), maybe_node()) :: [{list(), term()}]
```

Returns the parameter list (with metadata maps) for the robot.

# `list_remote_parameters`

```elixir
@spec list_remote_parameters(module(), atom(), maybe_node()) ::
  {:ok, [map()]} | {:error, term()}
```

Lists parameters exposed by a remote bridge.

Returns the bridge's flat parameter list (each entry a map carrying
`:id`, `:value`, `:type`, optionally `:min`, `:max`, `:doc`). Returns
`{:error, reason}` when the bridge is unavailable or the call fails.

# `positions`

```elixir
@spec positions(module(), maybe_node()) :: %{required(atom()) =&gt; float()}
```

Returns the latest joint positions known by the runtime.

# `publish`

```elixir
@spec publish(module(), list(), term(), maybe_node()) :: term()
```

Publishes a PubSub message under the robot's topic.

# `runtime_state`

```elixir
@spec runtime_state(module(), maybe_node()) :: atom()
```

Returns the runtime state machine state.

# `safety_state`

```elixir
@spec safety_state(module(), maybe_node()) :: atom()
```

Returns the safety state of the robot.

# `set_actuator`

```elixir
@spec set_actuator(module(), atom(), number(), maybe_node()) :: term()
```

Commands an actuator to a position.

# `set_parameter`

```elixir
@spec set_parameter(module(), list(), term(), maybe_node()) :: term()
```

Sets a parameter value.

# `set_remote_parameter`

```elixir
@spec set_remote_parameter(module(), atom(), term(), term(), maybe_node()) ::
  :ok | {:error, term()}
```

Sets a parameter value on a remote bridge.

# `subscribe`

```elixir
@spec subscribe(module(), [list()], maybe_node()) :: :ok
```

Subscribes to one or more PubSub paths for the given robot.

Local node — calls `BB.subscribe/2` for each path directly so messages
arrive at `self()`.

Remote node — spawns a relay process on the remote node that subscribes
there and forwards every `{:bb, _, _}` message back to `self()`.

---

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