--- type: plan status: active updated: 2026-10-09 --- # Initial plan ## Order 1. Text readout of the boundary stream — prove the contract before drawing anything. **Starter in place:** connects to `svc-gw` (protocol v0, contracts `docs/BOUNDARY.md`), shows link state, every signal with stale/null/missing told apart, the raw message log, the mock banner, and a Mark button. 2. Gauges. 3. Moving map: MapLibre against the PMTiles basemap served by the Orin, with the position from `yukart/gnss/fix`. No routing involved. 4. Guidance from `yukart/nav/guidance`, once [svc-nav](gitea:yukart/svc-nav) publishes it. A moving map with position, heading and trace is most of the value in a car and needs no routing engine at all. Routing is the expensive last fraction. ## Open - **Transport.** The app talks to `svc-gw` over one WebSocket (boundary v0). Whether high-rate topics later bypass it to the broker is decided in `svc-gw`. - **CJK labels.** MapLibre cannot ship Japanese glyph sets as SDF ranges. Local font rendering must be configured, or labels will be slow or absent. - **Sunlight.** A typical tablet is unreadable through a windscreen in daylight. This is a hardware problem, but the UI should not make it worse. ## The boundary, from this side Two channels with different guarantees, not one socket: | | Down (state) | Up (commands) | |---|---|---| | Transport | push stream | request/response | | Loss | tolerated; next sample supersedes | **not** tolerated; retry until acknowledged | | On reconnect | expect current state immediately | resend anything pending | This client always initiates and owns reconnection — the Orin never dials out. Assume the link drops constantly: cable knocked, screen off, power changes. ## The USB link, as measured (2026-10-09) Tested with the actual tablet (Redmi Pad SE 8.7 4G, Android 16) against the car Orin. Details: [USB tablet link report](gitea:yukart/orin-nano-env-setup). - **Physical:** tablet USB-C → PD hub (charging from the car) → Orin's USB-C port. The tablet is the USB host; the Orin is a USB network gadget. The tablet picks **NCM** and gets `192.168.55.100`; the Orin is **`192.168.55.1`**. - **Android won't route over it by default.** The link has no internet, so it never becomes the default network while WiFi or cellular is up, and ordinary sockets go elsewhere. Verified: the Orin was unreachable until WiFi and cellular were off. The app must request the Ethernet network (`ConnectivityManager.requestNetwork` with `TRANSPORT_ETHERNET`) and bind its sockets to it. - **About 3 s blank per power change.** Cutting the hub's PD costs ~3.5 s (full USB detach); restoring it ~2.2 s. Ignition and cranking produce these. - **An asleep tablet is a dead link.** Off charger and idle, Android suspends about a minute after screen-off and answers nothing, while the Orin still sees the USB link up. The app keeps the device awake while it is the display (screen-on flag in the full UI; the same for any overlay widget). - Reconnect without user action, and expect the Orin to drop a silent client by heartbeat (project-yukart decision 24). ## Session marking is worth building early A button that stamps an event (`indicator on`, `cold start`, `hard braking`) into the recording, on the same clock as the CAN frames, turns the Phase 1 manual event log from a notebook-and-wristwatch exercise into structured data. It is what makes a raw capture decodable later, and it is a Phase 1 capability that happens to live in this repo. ## Mock data must look like mock data `svc-gw` can synthesise vehicle state without any real readings, which is how this app is developed with no Jetson present. The stream carries a flag when it does. Rendering synthetic readings indistinguishable from real ones, on a screen in a moving car, is a safety problem. The indication belongs on the display surface itself, not in a settings page or a startup log. ## This app is the audio sink The tablet owns the car's aux input and is the music player, so the Orin's speech has to arrive here to be heard. - TTS arrives as an **utterance: a sequence of chunks with a terminator** over the reliable channel, never as one opaque file body. Streaming playback (start on the first chunk, append, finish on the terminator via `AudioTrack` streaming mode) is the target. - **Acceptable first version:** buffer the chunks and play on the terminator. With a non-streaming TTS that is a single chunk anyway. Swapping to streaming then changes only this receiver, not the protocol or the Orin. - Duck other audio with `AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK` for the duration of the utterance, then release. That is what makes Spotify drop under the voice without either side coordinating. - The microphone stays on the Orin. This app never captures audio; ASR input never crosses the boundary. ## Development workflow | Where | Device link | Gateway | |---|---|---| | Desk | tablet USB → halogen, ADB over USB | `cargo run -- --mock` in `svc-gw` on halogen; app built with `--dart-define=YUKART_GW=ws://:7700/`, or the address changed at runtime by tapping the link bar | | Car | tablet USB → Orin (tablet is USB host; no ADB over USB) | `yukart-gw.service` on the Orin, `ws://192.168.55.1:7700/`, the default | The app binds the process to the USB Ethernet network only when the gateway address is on `192.168.55.0/24`, so a desk build pointed at halogen over WiFi is not routed into a dead USB link. ### ADB in the car, without a hub The tablet's only USB port is the Orin link, and in it the tablet is the host — USB debugging is unavailable there. ADB over TCP is not, and it travels over the same NCM link: ```bash adb tcpip 5555 # at the desk, over USB, once per tablet boot # in the car, from halogen, through the Orin over the tailnet: ssh -N -L 5555:192.168.55.100:5555 mikkeli-orin-nano & adb connect localhost:5555 flutter run -d localhost:5555 ``` The tablet keeps the RSA authorisation from the USB pairing. `adb usb` turns TCP mode off again; a reboot does too. Android's *Wireless debugging* (pairing) is WiFi-only and does not help here. Untested in the car so far. ### CI `.gitea/workflows/ci.yaml` mirrors the android-stuff apps: analyze + test, then a debug APK artifact on push, on label `flutter-android`. A Gitea repo has one owner and an org-scoped runner serves one org, so halogen carries two registrations of the same runner: `gitea-runner.service` for android-stuff and `gitea-runner-yukart.service` for this org. They share the `gitea-runner` user, HOME, Gradle/pub caches and the `/opt/flutter` + `/opt/android-sdk` toolchain — a second registration, not a second toolchain. `flutter-android-preflight` runs in every job, so drift after a `flutter upgrade` fails here before it fails on a build.