# 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.