From b55ccba268ffa6ea5e67f9ab68c2e49891d7512a Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Fri, 9 Oct 2026 20:19:05 +0900 Subject: [PATCH] Define display boundary protocol v0 What crosses between svc-gw and the app: hello, heartbeats both ways, state snapshots of envelopes with mock flagged per message, and commands carrying an idempotency key. TTS utterances are left for when a TTS exists. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz --- README.md | 1 + docs/BOUNDARY.md | 66 ++++++++++++++++++++++++++++++++++++++ docs/impl/plans/INITIAL.md | 18 +++-------- 3 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 docs/BOUNDARY.md diff --git a/README.md b/README.md index 406c411..3bab73d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ definitions from here. | [`schema/topics.json`](schema/topics.json) | Topic namespace: payload kind, retention, rate, publisher | | [`schema/envelope.json`](schema/envelope.json) | JSON Schema for the decoded-signal envelope | | [`schema/signals.json`](schema/signals.json) | Signal registry — the single source for name, unit, type and range | +| [`docs/BOUNDARY.md`](docs/BOUNDARY.md) | Display boundary protocol: what crosses between `svc-gw` and the app | The Z27AG DBC lands here too, once Phase 2 produces one. diff --git a/docs/BOUNDARY.md b/docs/BOUNDARY.md new file mode 100644 index 0000000..77fd13e --- /dev/null +++ b/docs/BOUNDARY.md @@ -0,0 +1,66 @@ +--- +type: reference +--- +# Display boundary protocol — v0 + +Between a display client (`app-flutter`) and [svc-gw](gitea:yukart/svc-gw). The +bus topics in `schema/` stay inside the Orin; this is what crosses to the tablet. + +## Transport + +One WebSocket, `ws://:7700/`, JSON text messages, each with a `"t"` type. +The tablet reaches the car Orin at `192.168.55.1` over USB NCM; the app binds to +that network itself (see `app-flutter`'s initial plan). + +The client always connects and owns reconnection. svc-gw refuses peers outside +loopback, RFC 1918 and the tailnet's 100.64.0.0/10. + +The two channels from decision 24 share the socket but keep different guarantees: + +| | Down (state) | Up (commands) | +|---|---|---| +| Loss | tolerated; svc-gw drops the oldest when a client lags (64-message queue) | not tolerated; retry until acked | +| On reconnect | `hello`, then current state on the next tick | resend anything unacked, same `id` | + +## Down: svc-gw → client + +| `t` | When | Fields | +|---|---|---| +| `hello` | first message | `proto` (0), `server`, `host`, `source` (`mock` \| `none`), `mock`, `hb_ms`, `timeout_ms` | +| `hb` | every `hb_ms` | `ts` (Orin epoch ms), `clients` | +| `state` | 5 Hz, when there is a source | `seq`, `mock`, `signals`: name → envelope (`schema/envelope.json`) | +| `ack` | reply to a `cmd` | `id`, `ok`, `detail`, `dup` (true when answered from the cache) | + +`signals` holds only what is currently known. A missing signal is unknown, a +`null` value is known-without-value, `quality: "stale"` is an old value. All three +are normal and a display must render each distinctly. + +⚠ **`mock: true` must be visible on the display surface** whenever it appears in +`hello` or in any envelope. Not in a settings page, not in a log. + +## Up: client → svc-gw + +| `t` | When | Fields | +|---|---|---| +| `hb` | every `hb_ms` | — | +| `cmd` | on user action | `id` (unique, the idempotency key), `cmd`, `args` | + +svc-gw drops a client silent for `timeout_ms` (5 s). The client should treat +`timeout_ms` without any downstream message the same way and reconnect: an asleep +tablet, or an Orin behind a replugged hub, looks connected at the link layer. + +### Commands + +| `cmd` | `args` | Effect | +|---|---|---| +| `ping` | — | `ack` with `detail: "pong"` | +| `mark` | `text`, `delay_ms` (optional: how long the command waited before this send) | stamped into the open recording; fails with no session open | + +A repeated `id` returns the original `ack` (last 256 kept, shared across +connections) and never re-runs the command. + +## Not yet + +- TTS utterances (id, chunk sequence, terminator, expiry) — when there is a TTS. +- `gnss` and decoded vehicle state from the bus — when the bus exists. +- Any version beyond `proto: 0`. A breaking change bumps it; the client checks it. diff --git a/docs/impl/plans/INITIAL.md b/docs/impl/plans/INITIAL.md index a988891..514c4d6 100644 --- a/docs/impl/plans/INITIAL.md +++ b/docs/impl/plans/INITIAL.md @@ -20,20 +20,10 @@ observed traffic. They arrive as DBC entries plus registry rows with `system/` have their fields named in `topics.json` but no JSON Schema yet. They get one when a publisher exists to constrain. -**The app ↔ `svc-gw` boundary protocol.** Bus topics are defined here, but the -two channels across the boundary (latest-wins state down, acknowledged commands -up) have no wire format yet. Project-yukart decision 24 (proposed) already -fixes what it must carry, because the USB link drops for ~3 s at every power -change and a sleeping tablet looks alive at the link layer: - -- a **heartbeat** in both directions, with an agreed interval and timeout, so the - Orin reaps a silent client instead of trusting TCP or link state; -- an **idempotency key** on every upstream command, so retries across a - reconnect never act twice; -- for TTS, **utterance id + chunk sequence + terminator + an expiry**, so a - reconnect can resume or drop an utterance rather than replay it blindly. - -Defined when `svc-gw` and `app-flutter` first talk, not before. +**The app ↔ `svc-gw` boundary protocol** is at v0 in [BOUNDARY.md](../../BOUNDARY.md): +heartbeat both ways, idempotency key on every command, mock flagged per message. +Still missing from it: the TTS utterance shape (id, chunk sequence, terminator, +expiry), defined when a TTS exists to send one. ## Order