Files
app-flutter/docs/impl/plans/INITIAL.md
T
mikkeli 50081d5417
CI / Analyze & Test (push) Successful in 12s
CI / Debug APK (push) Successful in 57s
Starter display: boundary v0 client, readout, mark button, CI
Connects to svc-gw over one WebSocket, heartbeats, treats timeout_ms of
silence as a dead link, and retries commands under the same id until acked.
Shows link state, signals with stale/null/missing told apart, the raw stream,
a mock banner on the surface, and a Mark button that stamps the recording.
Android: keeps the screen on and binds the process to the USB NCM network
when the gateway is on it. CI mirrors android-stuff's host-mode runner setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-09 20:23:00 +09:00

6.5 KiB

type, status, updated
type status updated
plan active 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 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.

Tested with the actual tablet (Redmi Pad SE 8.7 4G, Android 16) against the car Orin. Details: USB tablet link report.

  • 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://<halogen LAN IP>: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:

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: host-mode runner on halogen, label flutter-android, analyze + test, then a debug APK artifact on push. The existing halogen-android runner is registered to the android-stuff org only, so this repo needs a runner registered to the yukart org (a second instance on halogen, the way gitea-runner-audio is set up) before jobs run.