Files
tines/docs/superpowers/specs/2026-10-24-tines-fleet-console-design.md
T

11 KiB

tines fleet console + simulated fleet — design

Date: 2026-10-24 Status: approved by Kyle (2026-10-24) — decision C: add NodeEvent::Contact/NodeEvent::Lost to core (see "The how")

Intent

A desktop GUI (new tines-gui crate) that operates tines nodes in two containerized modes:

  1. Bluetooth: a container with the host Bluetooth adapter passed through runs the full robot stack; its node talks to real tines robots over BLE.
  2. Simulated fleet: containers on the host run the full stack with a new IPC-bus transport (new tines-bus crate); they exchange tines frames with each other over a host bus.

The GUI is a console: a launcher screen plus one window per container. Each window exposes the whole node control surface — observe (including addressed), belief, store listing, events (contacts, drops, confirmed deliveries), status — so tines can be exercised without hand-rolled JSON.

Why

  • Testing tines today means hand-crafting length-prefixed JSON to a Unix socket; contacts and custody can only be watched in stderr.
  • The two-device BLE check (tines-bt/README.md) has never been recorded; a BT container plus a console makes it a routine manual test.
  • The only exchange transports are BLE (real radio) and tines-sim's in-process memory transport; nothing lets separate processes exchange frames. A simulated fleet on one machine needs a host IPC transport.
  • No runtime container image exists (Dockerfile.ros2 is a CI build-check).
  • Kyle wants to test tines functionality and interact with tines-enabled robots (hexapod on a Jetson running ROS2, Pi-class robots).

Goals

  • G1. tines-gui: an egui/eframe Wayland app following this desktop's conventions (the wayland-gui skill): a launcher, plus one GUI process per container.
  • G2. BT mode: a containerized full stack with host Bluetooth passthrough; the container's node is a real radio peer for robots in range.
  • G3. Sim mode: a new tines-bus host transport crate over a host IPC bus; containers plug in and exchange frames; fleet size is operator-chosen.
  • G4. The tines service config gains a transport choice (bt default, bus); existing config files keep working.
  • G5. Widened service IPC: addressed observe (to + lifetime_ms via Node::observe_to), contact/lost events, dropped-frame events, and a store listing; the existing four operations are unchanged.
  • G6. GUI windows expose every node operation; the docker lifecycle (create, start, stop, remove, logs) is managed from the GUI.
  • G7. A runtime container image in the repo (new Dockerfile); both modes run from the same image with different runtime flags.

Non-goals

  • tines-core changes limited to one additive pair (decision C, approved 2026-10-24): NodeEvent::Contact/NodeEvent::Lost variants, so the console's event feed can show live contacts. Nothing else in core changes: it stays no_std, and the 1.0 branch is unaffected (the enum is non-exhaustive). The bus is a host crate implementing the existing Transport trait; the IPC widening is in the tines service crate.
  • No tines-sim changes: the simulator remains the protocol test harness; the simulated fleet runs real services over the bus transport.
  • No ROS2 integration this round: containers run the service (Pi-class load). tines-ros2 is untouched; a ROS2-backed container is future work.
  • Not a fleet deployment tool: the console is for development, testing and interacting; systemd units and production deployment stay out of scope.
  • No node in the GUI process: the Node is not Send and the GUI never hosts one; it only speaks IPC to containerized services.

Anti-goals

  • No weakening of the trust boundary: the widened IPC keeps the existing posture — malformed input gets an Error response, never a panic; frame caps stay; hex-on-the-wire stays.
  • No silent config breakage: transport defaults to bt; existing configs parse unchanged; unknown keys are still rejected.
  • No radio in the GUI process; no BLE access outside the BT container.
  • No X11: eframe features glow/default_fonts/wayland only.
  • No docker SDK dependency: docker is driven as a subprocess (CLI).

Constraints

  • AGENTS.md applies unchanged: spec-first, TDD with sh scripts/ci.sh before commit, conventional commits. New crates go in the workspace and the AGENTS.md crate layout.
  • The Node is not Send; the service's inline poll architecture is unchanged. tines-bus is a Transport implementation and must not require Send of the node (same as BtTransport).
  • The bus carries the core's wire frames (MAX_FRAME = 1500 and friends are unchanged).
  • Docker is available on this desktop; the GUI shells out to it.
  • The console follows wayland-gui conventions: theme watch on ~/.local/state/omarchy/current, vsync = false, opaque window painted with the theme background.

The how (decided with Kyle)

Core change (decision C, approved 2026-10-24)

Add NodeEvent::Contact / NodeEvent::Lost variants to core, emitted from the exchange's on_event when the transport reports a contact starting or ending. The enum is non-exhaustive and the variants ride the existing drop-oldest event queue, so this is additive and changes no wire format. This is the only core change in this work.

Modes

  • Launcher: two choices — Bluetooth (real robots) and Simulated fleet. From it: create containers (name them, choose or generate node ids), see the running fleet, open any node's window.

  • One GUI process per container (Kyle: "one GUI instance per container"): the launcher spawns a child process of the same binary in node-window mode, so a hung container can't freeze the fleet view.

  • BT mode: docker run privileged, host network, D-Bus access (bluer/BlueZ needs the system bus and the hci device). Service config: node id, peer (the service still takes one — F3 remainder; the GUI exposes exactly what the service supports), log path on a named volume, socket path on a mounted directory. The GUI connects to the socket on the host filesystem.

  • Sim mode: the launcher starts a bus daemon on the host, then N containers, each running the service with the bus transport and the socket directory mounted. GUI windows drive each node.

  • Bus access control (wntrmute's call): the bus daemon's socket lives in a per-fleet directory under the operator's XDG state directory, created 0700. Same-user local processes are already inside the trust boundary (THREAT_MODEL); the bus adds no new posture.

  • Status strip transport (wntrmute's call): the launcher knows the transport because it wrote the container's config; it passes it to the window process. Status on the wire is unchanged.

  • Addressed panel in BT mode (wntrmute's call): the panel stays in every window — observe_to is a valid node operation whatever the transport. BT windows show a hint that the transport currently exchanges with exactly one configured peer, so an addressed observation to anyone else will sit until its lifetime expires.

The bus transport (tines-bus, new crate)

  • Implements Transport with Addr = NodeId, exactly like tines-bt: frames are the core wire frames; a frame's declared sender is checked against the bound peer (same address-spoofing rule).
  • Topology (wntrmute's call; Kyle can override): hub-and-spoke. A small bus daemon (a bin in the crate) binds a Unix socket in a per-fleet directory on the host; each node connects once. The bus fans every frame out to all connected nodes — a shared medium, like radio — and reports joins/leaves so the transport can emit Contact/Lost.
  • Peers: the bus transport enforces core's MAX_PEERS (8), as tines-bt does; an empty allowlist means any bus peer (the bus is a test medium, so it drops bt's single-peer F3 limit).
  • Framing: length-prefixed core frames over the socket; no new wire format.
  • Contact model: any frame from a bus peer is an implicit contact (core already treats unknown addresses that way); daemon disconnect → Lost for every peer.

Service changes (tines crate)

  • Config: new optional transport key, "bt" (default) or "bus"; with bus, a bus_socket path key. peer stays required for bt; for bus it is an optional allowlist (empty = any peer).
  • IPC widening (backward-compatible: new tagged variants; the existing four operations are untouched):
    • Observe gains optional to (hex NodeId) and lifetime_ms (present together or not at all) → Node::observe_to; without them, the existing observe path.
    • Events also returns contact/lost events (peer hex + kind) and dropped-frame events (reason + count), alongside confirmed deliveries. This changes the current D3 surfacing (drops to stderr only) so the console can show them; the stderr line stays.
    • New List request → the stored observations (id, observer, topic, payload, addressed destination, age), capped so a response fits the 1 MiB frame cap.
    • Status unchanged.
  • Trusted-time behavior is unchanged; the console shows service stderr via docker logs.

The GUI (tines-gui, new crate)

  • Window layout: status strip (node id, next seq, stored count, transport, socket path); observe panel (topic, payload, optional recipient + lifetime, broadcast default; text mode with a hex toggle — the wire is hex, the operator should not have to hex-encode by hand); belief panel (query a topic; list known topics from List); store browser (List view with refresh); events feed (contacts, lost, delivered, dropped, live); container panel (docker logs tail, stop/remove/restart).
  • Config generation: the launcher writes each container's service config JSON (matching the deny_unknown_fields schema), generates random node ids unless the operator supplies them, and manages volume/socket paths under a per-fleet state directory.
  • IPC client: the GUI speaks the widened IPC through a client module implementing the length-prefixed JSON framing. The request/response types are public in the tines crate (lib + bin), so the GUI can depend on the lib and share the types (builder's call, leaning yes).

Docker image

  • New Dockerfile.tines: builds the workspace, installs BlueZ runtime dependencies; the entrypoint runs the tines service with a mounted config. Both modes use this image; BT mode adds the privileged/host network and D-Bus/hci plumbing at run time.

What's left to the builder

  • Exact JSON schemas for the new IPC variants (within the constraints above).
  • Bus daemon protocol details (framing, join/leave messages, fan-out).
  • egui layout specifics and the theme integration.
  • Docker run flags per mode (device passthrough, volume layout, image build/tag flow).
  • Launcher supervision of child processes and daemons (PID tracking, cleanup on exit).

Success looks like

  • Sim mode: N containers exchange over the bus; broadcast observations converge in every node; an addressed observation with custody relay is watchable in the events feeds.
  • BT mode: the container's node exchanges with a real tines robot over BLE — the two-device check from tines-bt/README.md is finally run and recorded from the console.
  • Every operation in the widened IPC is reachable from a window without hand-rolled JSON.