All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.7.0 - 2026-09-15
Added
- The Visualization tab renders as a real bitmap in the browser. With
phoenix_ex_ratatui0.3 the hook reports the measured cell size with its resize event, the transport opens theCellSessionwith that font size, and ex_ratatui 0.14 rendersViewport3Din:auto(the default) or any explicit pixel mode as a pixel region — a PNG painted over the grid — instead of the braille fallback the browser used to get. Themcycle is unchanged: the cell-blit modes (:half_block,:braille,:ascii) still render as cells, so the comparison is one key away. Nothing changes on the terminal, SSH, or distribution transports.
Changed
- The optional
phoenix_ex_ratatuidependency is now~> 0.3. The installer's--web, theBB.TUI.Livedocs, and the README all pin 0.3, which is where pixel regions arrive; 0.2 still works but keeps the Visualization tab in braille. Consumer apps must rebuild their JS bundle after updating the dependency: an older hook reports no cell size, so the transport falls back to cells silently.
0.6.0 - 2026-09-04
Added
The browser is a fourth transport. With the optional
{:phoenix_ex_ratatui, "~> 0.2"}dependency present, the newBB.TUI.Livecompiles:use BB.TUI.Live, robot: MyApp.Robotdefines a Phoenix LiveView that serves the full dashboard — reducer runtime, subscriptions, throttling and all — through phoenix_ex_ratatui's CellSession transport, routable like any LiveView. Theuseoptions are the same mount keyword list as every other entry point, andtui_mount_opts/1is overridable for per-session options. Each browser tab is an isolated dashboard session over the shared robot, matching the SSH multi-client model. Without the dependency nothing changes — the module isn't compiled and no Phoenix code is pulled in. The dev application serves an npm-free reference wiring athttp://localhost:4040(dev/web/): prebuilt UMD bundles for phoenix and phoenix_live_view straight fromdeps/, the hook as the ES module it ships, no esbuild.The installer wires the browser transport with
--web.mix igniter.install bb_tui --webadds the optionalphoenix_ex_ratatuidependency, generates ause BB.TUI.LiveLiveView under the web namespace (MyAppWeb.RobotLiveforMyApp.Robot, atlib/my_app_web/robot_live.exwhere igniter places it), mounts it with aliveroute inside the router's:browserscope (/robotby default,--pathto choose), and registersPhoenixExRatatuiHookinassets/js/app.js— both the Phoenix 1.8 shape, wherehooks: {...colocatedHooks}already exists, and the 1.7 shape without ahooks:key. Anapp.jsit cannot recognise gets a warning with the snippet rather than a guess. The flag needs a Phoenix router; without one the installer prints the manual wiring and touches nothing. Idempotent like--sshand--nerves, and composable with both.Unit-typed parameters are readable and adjustable.
bb0.31.0 converts a unit-typed parameter into its declared unit on every write, so a{:unit, :meter}parameter's value is reliably aLocalize.Unitin a known unit — which the TUI previously rendered throughinspect(Localize.Unit.new!(0.5, "meter")in a 30%-wide table cell) and silently refused to adjust, because theh/lhandlers only matched plain numbers. The Value column now renders the magnitude the way a bare number would, followed by the canonical unit name (0.500 meter), the Type column rendersunit:meter(matchingbb_liveview's convention), andh/l/H/Lstep the magnitude with the same 1%/10%-of-range scaling numeric parameters get. Bounds need one extra move: the DSL deliberately keeps a bound in the unit it was declared in, so amaxof 100 centimeter can bound a meter parameter —BB.TUI.State.unit_bounds_in/2converts each bound into the value's own unit before step sizing and clamping, and opens a side it cannot convert rather than guessing (bb still validates the write server-side).String and atom parameters are editable, and fixed-set parameters cycle. Enter on a string or plain-atom parameter opens an inline editor in the value cell — typed characters append to the buffer (shown with a cursor mark), backspace deletes, enter commits, and esc cancels. A committed buffer with a leading
:reads as an atom, which is also how an atom value is prefilled, so it round-trips; bb validates the written value against the parameter's schema, so a wrong type comes back as a refused set rather than a crash. While the editor is open the TUI is modal: the edit keys are matched ahead of the global ones, so aq,a, ortlands in the buffer instead of quitting, arming, or switching tabs, and the status bar swaps to commit/cancel hints. A parameter declared with an{:in, values}type doesn't free-text edit at all —h/lcycles it through its allowed values with wrap-around, whatever their type. Bridge tabs are untouched: a bridge's parameter types are its own contract, and numeric stepping plus boolean toggling remain the remote surface.The dev robot demos every parameter shape. Every parameter now carries a
doc, and three joined so each panel feature has a live fixture:motion.cruise_speed({:unit, :meter_per_second}),controller.trim({:unit, :degree}whose bounds are declared in radians, exercising cross-unit clamping), andidentity.label(a string for the inline editor). A newDev.GaitSelectorcomponent registers a hand-writtenparam_schema/0with a{:in, [:walk, :trot, :crawl]}gait.patternparameter — the fixed-set shape the robot DSL cannot declare — soh/lcycling is demoable too.The selected parameter's
docshows in the panel.BB.Parameter.list/2has always carried each parameter'sdocstring and the TUI already stored it, but the panel's three columns never showed it. It now renders as a dim title on the panel's bottom border while the panel is focused — the table is too narrow for a fourth column, and the bottom border was unused. The doc follows the selection through the sorted row order; parameters without a doc, unfocused panels, and bridge tabs (whose parameters carry no schema doc) leave the border unadorned. Mirrorsbb_liveview, which renders the doc under each parameter name.
Fixed
The installer compiles inside consumers that have igniter.
igniterwas declaredonly: [:dev, :test], but Mix compiles dependencies in:prod, so in a consumer projectbb_tui's own dependency graph never contained igniter even when the consumer's did — andMix.Tasks.BbTui.Install'sCode.ensure_loaded?(Igniter)branch, compiled against a module outside the graph, tripped Elixir 1.20's "module is not available" checks (struct Igniter.Mix.Task.Info is undefined, a wall of undefined-function warnings). The dependency is nowoptional: true, the shapebbandex_ratatuialready use: consumers with igniter get it ordered ahead ofbb_tui, consumers without it get therequires igniterfallback task as before.Shift+tab works from browser transports. Terminal transports report shift+tab as crossterm's
back_tabcode, which is all the panel-cycling and argument-focus clauses matched;phoenix_ex_ratatuireports the same chord astabwith a"shift"modifier, so in a browser it fell through to the plain-tab clauses and cycled forward. The modifier form now normalizes toback_tabbefore any tab clause can match.Parameter bounds work again against
bb0.30+.bb0.30.0 changed whatBB.Parameter.list/2reports: the generated validator tuple ({:integer, [min: 0, max: 100]}) became the declared type (:integer) withmin/maxas separate metadata keys.BB.TUI.State.parameter_bounds/2still dug bounds out of the old tuple shape, which no longer occurs, so every bounded parameter lost its range-scaledh/lstep (falling back to 1 / 0.1) and its clamping — and sincebb0.30 also started rejecting out-of-range writes, stepping into a bound produced a silently refused set instead of a clamp. Bounds now come from themin/maxmetadata keys, whichBB.TUI.State.update_parameters/2previously discarded.
0.5.0 - 2026-08-25
Added
- Actuator refusals from jog keys now surface in the event log.
BB.TUI.Robot.set_actuator/4usesset_position/4's default:pubsubdelivery instead ofdelivery: :direct, so the actuator's answer comes back — and because that answer arrives via aGenServer.call, the call moves off the event loop into anExRatatui.Command.async/2whose result maps to{:actuator_result, actuator, result}. An:okis dropped without a render (there is one per key autorepeat); a{:error, reason}— including the{:exit, _}the async runner traps when an actuator is dead or wedged, which previously had nowhere to go — is appended to the event log asjoint ✗ reason, debounced per actuator like any other event. The call waits 250ms rather thanset_position/4's default five seconds — a jog is a stream of targets of which only the latest matters, and the long wait would let tasks pile up under key autorepeat against a wedged actuator (matchingBB.LiveView.Components.JointControl). The joint's target keeps the asked-for value; position still moves only on sensor feedback. As a side effect of:pubsubdelivery, jog commands themselves now appear in the event log, since the command is published for observers and the TUI already subscribes to[:actuator].
Changed
- Follows the removal of
BB.Actuator.set_position!/4inbbcore (breaking).bbmadeset_position/4synchronous and dropped bothset_position!/4andset_position_sync/5; the!cast is nowset_position(robot, target, position, delivery: :direct).BB.TUI.Robot.set_actuator/4sends that, locally and over:rpc, so behaviour is unchanged — a cast, no publication,:okregardless of whether the actuator accepted it. The synchronous default was considered and rejected for this caller:set_actuator/4is invoked inline fromBB.TUI.App.update/2on a keypress, and aGenServer.callthere would stall the process that owns the terminal for the round trip and exit it on timeout. Surfacing refusals would need the call moved off the event loop and somewhere in the UI to put the error, which is worth doing separately. Themix.exsrequirement moves from~> 0.28 and >= 0.28.1to~> 0.30to encode this:set_position/4also exists inbb0.28/0.29, as a publish that ignores options it doesn't recognise, so against an olderbbthis code compiles clean and quietly publishes where it means to cast. An ignored option fails silently at runtime where a removed function fails loudly at compile time, which makes the floor load-bearing rather than housekeeping.
0.4.0 - 2026-08-10
Added
Trajectory commands read properly in the event log. A
BB.Message.Actuator.Command.Trajectorypublished on[:actuator | joint]haswaypointsrather than aposition, so it missed the actuator summary clause and fell through to the genericinspect— a truncated blob of waypoint keyword lists. It now summarizes asshoulder ← trajectory 4 waypoints over 2400ms(with×5/×∞appended when the trajectory repeats), and the detail pane collapses each waypoint toposition@timeinstead of listing thevelocity: nil, acceleration: nilthatbb0.29 made optional. The dev robot gains atrajectorycommand that publishes a real trajectory per joint and then sweepsshoulderandelbowthrough its waypoints over ~2.4s, so the 3D view shows motion passing through the waypoints instead of snapping between targets.
Changed
Follows the
positions→configurationsrename inbbcore (breaking).bb0.27.0's multi-DoF joint work renamedBB.Robot.Runtime'spositions/1toconfigurations/1— a joint's configuration is only a float when it has one degree of freedom — so the TUI crashed with anUndefinedFunctionErrorduring dashboard init againstbb>= 0.27.BB.TUI.Robot'spositions/2is nowconfigurations/2and routes to the new accessor both locally and over:rpc. The joints panel and the visualization drive single-DoF joints, so values remain floats and behaviour is otherwise unchanged. Themix.exsrequirement moves from~> 0.20to~> 0.28 and >= 0.28.1to encode the new API — the floor is 0.28.1 rather than 0.27.0 becausebb0.27.0 and 0.28.0 close a compile-time cycle (BB.Message.Optionpattern-matches the geometry structs whose modules import it back) that deadlocks Elixir 1.19's parallel compiler;bbbroke that cycle in 0.28.1, so the~> 1.19floor and the two-cell CI matrix stay as they are. Mirrorsbb_liveviewv0.3.0's migration.Multi-DoF joints flow through the TUI (breaking). Joint configurations are stored verbatim, shaped to the joint's type — a float for single-DoF joints, a
BB.Math.Transform2Dfor planar, aBB.Math.Transformfor floating — soBB.TUI.State'supdate_positions/2is renamedupdate_configurations/2and dashboard init seeds missing joints with their type's identity. Planar and floating joints appear in the joints panel as read-only rows with a compact pose ((x, y, θ°)/ translation(x, y, z)), target-adjust keys skip them, and event details render their transform entries compactly. Driving them is not possible by design:BB.Actuator.set_position!/4takes a single number, and bb has no command API for a transform target.BB.TUI.Viz.RobotScenedelegates forward kinematics to bb core. The scene is built fromBB.Robot.Kinematics.all_link_transforms/2— one flat node per visual link carrying its base-frame transform — instead of a hand-rolled single-DoF FK walk, so planar and floating joints pose correctly in the 3D view and the local FK code is deleted. Requiresrobot.topology(every runtime-builtBB.Robothas it).
0.3.1 - 2026-07-31
Added
usage-rules.md, shipped in the package. Matches the conventionbbandbb_liveviewalready follow, so an agent working in a Beam Bots workspace picks up rules for the TUI layer alongside the framework's own (mix usage_rules.sync <file> bb_tui). Covers the entry points and how to pick between them, the full option table,BB.TUI.Rendererfor consumer-owned payloads, command cancellation, and the anti-patterns that actually bite — declaring the dashboard intopology, calling the blockingrun/2from a supervision tree, or setting:nodewithout connecting first. Added{:usage_rules, "~> 1.2", only: [:dev]}for the sync tooling.
Fixed
- Continuous commands are no longer reported as timed out while they are still running. Commands dispatched from the UI were awaited with a UI-side deadline (
:bb_tui, :command_timeout, default 30s), so a continuous command — one that only returns when it stops or is cancelled — surfaced{:error, :timeout}in the result panel after 30 seconds even though it was running normally, with no way to stop it.BB.Command.await/2is now called with:infinity; runaways stay bounded by the command's own DSLtimeout. MatchesBB.LiveView.Components.Command, which bb_tui mirrors.
Added
- Cancel a running command with
cin the commands panel. Command execution is now two-phase — one async starts the command and reports{:command_started, _}, a second awaits it and reports{:command_result, _}— so the running command's pid reaches state (BB.TUI.State.Commands.executing_pid) and can be cancelled.BB.TUI.Robot.cancel_command/2routes locally or over:rpclike every other runtime call. Cancelling resolves the pending await, so the result panel shows the cancellation instead of hanging on the throbber.
Removed
- The
:bb_tui, :command_timeoutconfig key. It no longer has anything to bound now that the await is:infinity— the DSLtimeoutand the new cancel key cover both cases. Setting it is now a no-op and can be deleted from consumer config.
0.3.0 - 2026-06-23
Added
- Configurable subscription paths.
BB.TUI.run/2(andstart/2/start_ssh/2) now accept a:subscribe_pathsoption that overrides the default control-plane set the dashboard subscribes to — e.g.[[:state_machine], [:command]]to narrow it, or a downsampled observability topic instead of the high-rate sensor firehose. Threaded through both the local and SSH transports; default behaviour is unchanged when the option is omitted. Thanks to @lostbean. - Consumer-supplied renderers.
BB.TUI.run/2(andstart/2/start_ssh/2) now accept a:renderersoption — a%{path_prefix => module}map that lets a consumer teach the dashboard how to render a payload on a PubSub path it owns, without bb_tui knowing the payload's struct. A module implements the newBB.TUI.Rendererbehaviour:summarize/2returns the event-log line (ornilto fall back to the genericinspect/2), and the optionalobserved/2feeds an at-a-glance status-bar slot. Messages route to a renderer by longest-matching prefix, like a routing table. Fully additive — with no:renderers, dispatch is unchanged. Threaded through both the local and SSH transports. Thanks to @lostbean.
0.2.0 - 2026-06-19
Added
- 3D visualization tab. A new top-level tab (
[/]to switch) renders the live robot in the terminal in 3D, built from its URDF topology and joint positions via forward kinematics. The camera orbits, tilts, zooms, and resets (←/→/h/l,↑/↓/k/j,+/-,r), and the arm reposes in real time as sensor data arrives. Built onExRatatui'sViewport3DandThreeD.Nodescene-graph. - Battery / power readout in the status bar. When the robot publishes
BB.Message.Sensor.BatteryStateorBB.Message.Sensor.PowerState, the status bar shows an at-a-glance segment — charge percentage (colored green / yellow / red by remaining charge, with a bolt while charging), falling back to bus voltage when percentage is unmeasured. Latest-reading-wins; the event log keeps the history. Especially useful when driving a headless robot over SSH. - Hardware-error and estimator events. The dashboard now also subscribes to
[:safety]and[:estimator], soBB.Safety.HardwareErrordetail (the component and reason behind an error badge) and estimator output (Odometry/Pose) surface in the event log. Safety state transitions already arrived via[:state_machine], so the badge was already accurate — this adds the missing diagnostic detail. - Dev demo commands.
Dev.TestRobotgainspower(drains a simulated battery so the status-bar readout shifts green → yellow → red) anddiagnostics(publishes a hardware-error report and an estimator pose so both surface in the event log).
0.1.0 - 2026-06-04
Initial release — a terminal dashboard for Beam Bots robots, built on ExRatatui.
Added
- Dashboard layout. A multi-panel terminal UI — title bar, Safety, Joint Control, Commands, Events, Parameters, and a status bar — composed through ExRatatui's reducer runtime. Pure state transitions live in
BB.TUI.State;BB.TUI.Appwires input and async results to those transitions. - Safety panel. Arm / disarm / force-disarm controls with a confirmation popup for force-disarm, plus an animated throbber while disarming. Reflects the robot's live safety state (
:armed/:disarmed/:disarming/:error). - Joint control panel. Position table showing joint type (revolute / prismatic / continuous), units (degrees / mm), visual range bars, last-commanded target markers, and simulated-joint tags. Direct keyboard position adjustment in 1%-of-range and 10%-of-range steps.
- Commands panel. Lists available robot commands with Ready / Blocked indicators based on runtime state. Argument-less commands execute on Enter; commands with declared arguments open an inline edit mode (Tab / Shift+Tab to cycle fields, type-to-edit, Enter to run, Esc to cancel). Argument types — boolean, integer, float, atom, enum (
{:in, [...]}), and string — are parsed before dispatch. Entered values are preserved per command across executions. - Parameters panel. Live parameter table grouped by path with real-time updates and schema-aware editing (min / max bounds drive 1%-of-range stepping). Bridge tabs surface remote-parameter lists fetched per bridge, editable through the same keys; press
tto cycle tabs. - Event stream. Scrollable, color-coded event log with summaries and timestamps taken from
BB.Message.wall_time(publish time, not arrival time). Pause / resume, clear, and Enter to open a detail popup showing the full payload. - High-rate sensor handling. The event log debounces repeats of the same
{path, payload-type}within a one-second window so a fast sensor can't flood it, and sensor-driven re-renders are coalesced to ~30fps — keeping the UI responsive under high-rate telemetry while key presses, command results, and safety / parameter / state changes still render immediately. Both windows are tunable. - Status bar, help overlay, and theme system. Status bar shows robot name, safety indicator, runtime state, and contextual key hints; a scrollable help overlay lists the full keybinding reference; a consistent color palette provides semantic styles (safety colors, focus borders, panel headers).
- Keyboard-driven navigation. Tab / Shift+Tab to cycle panels, number keys to jump directly to a panel, and vim-style
j/k/h/lwithin panels. - SSH transport. Serve the dashboard over SSH; multiple operators can connect simultaneously, each with an isolated session (built on ExRatatui's
:sshtransport). - Distribution attach. Run the TUI on the robot node and attach a thin renderer from any connected BEAM node (built on ExRatatui's
:distributedtransport). - Nerves support. Register the dashboard as a
nerves_sshsubsystem so operators can attach over SSH on-device. - Runtime inspection. Snapshot, trace, and inject events into a running TUI via
ExRatatui.Runtime— useful for debugging SSH sessions that aren't otherwise observable. mix bb.tuitask. Standalone launch —mix bb.tui --robot MyApp.Robot, with--sshand distribution options.mix bb_tui.installIgniter task. Addsbb_tuito a project, imports formatter rules, optionally scaffolds aBBrobot, and wires up launch for the default,--ssh, or--nervesinstall shapes.- Headless test suite. Full coverage using Mimic and ExRatatui's test backend, including end-to-end tests that drive a real server via
ExRatatui.Runtime.inject_event/2.