Files

131 lines
6.3 KiB
Markdown

---
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://<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
USB debugging is unavailable there: on the Orin link the tablet is the USB host.
ADB runs over TCP on the NCM link instead, reached from halogen through one SSH
forward via the Orin. Mechanism, steps and troubleshooting:
[ADB in the car](../../ADB-IN-CAR.md).
### 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.