Packages

Terminal-based dashboard for Beam Bots robots

Current section

Files

Jump to
bb_tui usage-rules.md
Raw

usage-rules.md

<!--
SPDX-FileCopyrightText: 2026 Mauricio Cassola
SPDX-License-Identifier: Apache-2.0
-->
# BB.TUI Usage Rules
`bb_tui` provides `BB.TUI.run/2` and friends — a terminal dashboard for a
running [Beam Bots](https://hexdocs.pm/bb) robot, with live joint state, a 3D
model, command execution, a PubSub event stream, and arm/disarm safety
controls. It is the terminal counterpart to
[`bb_liveview`](https://hexdocs.pm/bb_liveview).
For BB framework basics see `bb`'s rules (`mix usage_rules.sync <file> bb:all`);
for the rendering layer see `ex_ratatui`'s
(`mix usage_rules.sync <file> ex_ratatui:all`). This file covers only how to
start and extend the dashboard.
## Core principles
1. **The dashboard is an entry point, not a DSL component.** It is *not* a
`BB.Sensor`/`BB.Actuator`/`BB.Controller` and it does *not* belong in the
robot's `topology`. Start it from IEx, a mix task, a supervision tree, or an
SSH daemon.
2. **It observes a robot that is already running.** On start it subscribes to
the robot's PubSub and reads live state over the local node or `:rpc`. The
robot's supervision tree must be started separately — the dashboard neither
starts nor supervises the robot.
3. **Pick the entry point by lifecycle, not by transport.** `run/2` blocks and
owns the terminal until `q`; `start/2` returns a supervised pid; `start_ssh/2`
runs a daemon; `subsystem/1` registers under `nerves_ssh`. Reaching for
`run/2` inside a supervision tree is the usual mistake.
4. **Rendering is a pure projection of state.** Panels read `BB.TUI.State` and
return widgets; every transition lives in `BB.TUI.State` and every effect is
dispatched from `BB.TUI.App.update/2`. Extensions should follow the same
split rather than reaching for the runtime directly.
## Starting it
Local terminal, blocking — the common case from IEx or `mix bb.tui`:
```elixir
BB.TUI.run(MyApp.Robot)
```
Supervised, for an app that should serve the dashboard for its lifetime:
```elixir
children = [
{BB.Supervisor, MyApp.Robot},
{BB.TUI, robot: MyApp.Robot, transport: :ssh, port: 2222, auto_host_key: true}
]
```
Attached to a robot on another node — rendering happens locally, data comes from
there. The nodes must already be connected:
```elixir
Node.connect(:"robot@192.168.1.42")
BB.TUI.run(MyApp.Robot, node: :"robot@192.168.1.42")
```
On Nerves, plug into the existing `nerves_ssh` daemon rather than starting a
second one:
```elixir
# config/runtime.exs
config :nerves_ssh,
subsystems: [
:ssh_sftpd.subsystem_spec(cwd: ~c"/"),
BB.TUI.subsystem(MyApp.Robot)
]
```
The igniter installer wires up the mix task and dev entry points:
```bash
mix igniter.install bb_tui --robot MyApp.Robot
```
## Options
`run/2`, `start/2` and `start_ssh/2` share the same keyword list.
| Option | Default | Meaning |
|---|---|---|
| `:robot` || The robot module. Must `use BB` (passed positionally to `run/2`) |
| `:transport` | `:local` | `:local` for the OS terminal, `:ssh` for a daemon. `:ssh` also accepts every `ExRatatui.SSH.Daemon` option |
| `:node` | `nil` | Connected remote node. All robot data is fetched from it via `:rpc.call/4` and PubSub is relayed back |
| `:subscribe_paths` | control-plane set | PubSub paths to subscribe to. Narrow it (`[[:state_machine], [:command]]`) to avoid a high-rate sensor firehose |
| `:renderers` | `%{}` | `%{path_prefix => module}` map of `BB.TUI.Renderer` implementations |
| `:test_mode` | `nil` | `{width, height}` for headless testing |
## Rendering custom payloads
The dashboard summarises the message types it knows. For a payload on a path the
consumer owns, register a `BB.TUI.Renderer` instead of teaching bb_tui about the
struct — messages route by longest-matching prefix, like a routing table:
```elixir
defmodule MyApp.GripperRenderer do
@behaviour BB.TUI.Renderer
@impl true
def summarize([:sensor, :gripper], %{force_n: n}), do: "gripper #{n} N"
def summarize(_path, _payload), do: nil
@impl true
def observed([:sensor, :gripper], %{force_n: n}), do: {"grip", "#{n} N"}
def observed(_path, _payload), do: nil
end
BB.TUI.run(MyApp.Robot, renderers: %{[:sensor, :gripper] => MyApp.GripperRenderer})
```
`summarize/2` returns the event-log line, or `nil` to fall back to the generic
`inspect/2`. `observed/2` is optional and feeds an at-a-glance status-bar slot.
## Running commands
Commands are awaited with `:infinity`, so a continuous command — one that only
returns when it stops or is cancelled — is never reported as timed out while it
is still running. Runaways stay bounded by the command's own DSL `timeout`.
Press `c` in the commands panel to cancel the running command.
## Anti-patterns
- **Don't declare the dashboard in `topology`.** There is no component to
supervise — it is an entry point driven purely by the robot's PubSub.
- **Don't call `run/2` from a supervision tree.** It blocks and takes over the
terminal. Use `start/2`, which returns `{:ok, pid}`.
- **Don't start it against a robot that isn't running.** The module must `use BB`
*and* have its supervision tree started; otherwise the dashboard renders but
shows no live state and controls do nothing.
- **Don't set `:node` without connecting first.** `Node.connect/1` must succeed
before the remote node is usable, or every `:rpc` call fails.
- **Don't subscribe to the full firehose when only control-plane traffic
matters.** The event log debounces and renders are coalesced, but
`:subscribe_paths` avoids the traffic altogether.
- **Don't reach into the runtime from a panel.** Panels project state to
widgets; effects belong in `BB.TUI.App.update/2` and transitions in
`BB.TUI.State`.
## Further reading
- [bb_tui docs](https://hexdocs.pm/bb_tui) — including the
[Transports](https://hexdocs.pm/bb_tui/transports.html) and
[Keybindings](https://hexdocs.pm/bb_tui/keybindings.html) guides
- `bb`'s rules (`mix usage_rules.sync <file> bb:all`) and the
[bb docs](https://hexdocs.pm/bb)
- `ex_ratatui`'s rules (`mix usage_rules.sync <file> ex_ratatui:all`) for the
reducer runtime and widget layer