BB.TUI.App (BB.TUI v0.1.0)

Copy Markdown View Source

Main TUI application using the ExRatatui.App reducer runtime.

Renders the dashboard layout and handles every keyboard event, PubSub message, and side effect through a single update/2 arrow. Pure state transitions live in BB.TUI.State; this module's job is to wire input and async results to those transitions and return declarative ExRatatui.Command values for IO.

Layout

 Safety  Joint Control 
  ARMED         Joint       Type  Position  Range           
 Runtime: Idle   elbow       rev   -63.8°      
 [a] Arm         gripper SIM pri    30.6mm     
 [d] Disarm      ...                                         
 Commands                                              
  home   Ready                                              
   calibrate                                                 
 Events (47)  Parameters 
 18:23:12 sensor.sim   speed              100               
 18:23:11 state_m...   controller.kp      0.5               

 Robot |  Armed | idle | [q]Quit [Tab]Panel [?]Help

Reducer callbacks

  • init/1 — validates the robot module, subscribes to PubSub ({:bb, _, _} messages flow into update/2 as {:info, _} automatically), and snapshots ETS state. No Task.Supervisor is required: long-running command execution is owned by the runtime via ExRatatui.Command.async/2.
  • render/2 — composes panel functions into a flat [{widget, rect}] list (the events panel contributes two panes: the list and an overlay ExRatatui.Widgets.Scrollbar).
  • update/2 — the single dispatch arrow. Receives {:event, ev} for terminal input and {:info, msg} for everything else (PubSub, async results, subscription ticks, send_after messages). Returns {:noreply, state} for pure transitions or {:noreply, state, commands: [cmd]} when an effect should fire.
  • subscriptions/1 — declares the throbber tick interval whenever the dashboard has something animating (a :disarming safety state or a command currently executing), and a one-shot :sensor_flush tick whenever a sensor render is pending (see High-rate sensor handling). The runtime diffs the result against the previous one, so timers only run when needed. This replaces the previously-dormant Process.send_after-style throbber tick.

High-rate sensor handling

Robot sensor readings arrive on the [:sensor | _] path and can be very fast. Two mechanisms keep the UI responsive without dropping meaningful information:

  • BB.TUI.State.append_event/3 debounces the event log — a repeat of the same {path, payload-type} within throttle.debounce_ms (default 1s) is dropped, so one fast sensor cannot evict every other event from the 100-entry log.
  • Sensor messages update state but suppress their immediate render (render?: false); a one-shot :sensor_flush tick (throttle.flush_ms, default ~33ms / 30fps) armed by subscriptions/1 performs a single coalesced redraw. Every other message — key presses, command results, safety/param/state events — still renders immediately.

Both intervals live in the BB.TUI.State.Throttle substruct, so tests can shrink or disable them (a debounce window of 0 disables it).

Async commands

Pressing Enter on a Ready command executes it when the command has no arguments, or enters an inline argument-edit mode when the command declares arguments. From edit mode, Tab/Shift+Tab cycle fields, typing edits the focused field, Enter executes with the parsed values, and Esc exits without executing.

Execution returns a Command.async/2 that calls BB.Command.await/2, which waits on the spawned command via GenServer.call, falls back to bb's ResultCache if the handler finishes before we can await, and enforces the timeout internally. The result arrives as a single {:command_result, _} info message (success, error, or {:error, :timeout}).

Side-effect convention

Fast, fire-and-forget calls (Robot.arm/2, Robot.disarm/2, Robot.set_actuator/4, Robot.set_parameter/4, Robot.publish/4, Robot.force_disarm/2) are invoked inline from update/2 rather than wrapped in a Command.async/2. They are effectively constant-time PubSub publishes; the boilerplate of routing through a no-op result mapper would dwarf the call. Only Robot.execute_command/4, which monitors a spawned command process and waits for its :DOWN, goes through Command.async/2.

Configuration

The wait window for BB.Command.await/2 is compile-time configurable via Application.compile_env/3:

# config/config.exs
config :bb_tui, command_timeout: 30_000

Default is 30_000 ms. The test suite overrides this to 100 ms in config/test.exs to keep timeout assertions snappy. Because the value is read with compile_env, downstream apps need to recompile :bb_tui after changing the config (mix deps.compile bb_tui --force).