Compare commits

...

38 Commits

Author SHA1 Message Date
mikkeli b896e05552 Decision 31: DTC clearing allowed, driver-initiated and confirmed, codes recorded first
/ crosscheck (push) Successful in 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 23:37:14 +09:00
mikkeli b05eaeaf78 Decision 31: reads allowed (OBD-II, DTC reads), nothing that changes an ECU (no DTC clear, actuator tests, coding)
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 23:36:23 +09:00
mikkeli 2e87ac83cf Decision 31: Yukart informs the driver, never controls the car; the goal in OVERVIEW
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 23:35:11 +09:00
mikkeli c04d8aed28 Roadmap: pl2303 joins the out-of-tree modules
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-14 00:40:40 +09:00
mikkeli 514f34ca3d imu in the runtime and repo lists
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-14 00:23:53 +09:00
mikkeli 76539f4625 Nothing transmits on CAN: the single writer is obd-poller, on the K-line; gnss live
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:57:31 +09:00
mikkeli 7911fa3e7e Decision 30: OBD-only values may be shown under obd_ names; DTCs via the ELM; classic Bluetooth first
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:33:18 +09:00
mikkeli a814ef554e Decision 30: CAN is the display source; OBD-II over K-line calibrates; GNSS speed beside it
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:25:45 +09:00
mikkeli bb47ee5301 Roadmap and repos: decoder live; signal layouts in contracts' signals.json
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 21:31:52 +09:00
mikkeli 5398e32427 Roadmap: off-board session retention goal; CAN bring-up, OBD and capture status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 20:20:32 +09:00
mikkeli da40a56f1d CI: crosscheck runs on the halogen label
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 01:01:20 +09:00
mikkeli 147bea3e03 CI: crosscheck caller names its runner
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 00:38:00 +09:00
mikkeli 837375b7ac CI: cross-repo consistency check on push (yukart/tool-crosscheck)
/ crosscheck (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 00:36:39 +09:00
mikkeli d838531410 REPOS: svc-core row lists orin-status and obd-poller
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 00:10:34 +09:00
mikkeli 180ad16730 obd-poller done (mode 01); DTC/MUT-III and first decoding captures next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 23:25:04 +09:00
mikkeli 8a82cb1c78 Decision 25 accepted: the recorder reads SocketCAN and gpsd directly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 22:33:45 +09:00
mikkeli b7c958ed68 Decision 4: raw CAN rate confirmed on the car Orin
No loss at a saturated bus; 10 ms batches cost the broker ~1 % of a core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 20:48:41 +09:00
mikkeli b254bb2c55 The bus is up: decision 4 accepted; orin-status publishes yukart/host/*
Also: std::process::Command breaks the cross-compile fallback (GLIBC_2.39).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 16:37:11 +09:00
mikkeli 66b7269f3c svc-vision: new repo for the camera; decision 29 (Python for vision)
The live view moved from app-flutter's test tool to svc-vision, running as
yukart-vision.service on the car Orin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 16:15:04 +09:00
mikkeli a290a01f03 Camera in the plan: roadmap Phase 5 split into camera then inference; proposed decisions 27 (live view) and 28 (paced Orin -> tablet senders)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-11 17:21:41 +09:00
mikkeli e6ca835992 Phase 1 recorder running; svc-core current; boundary v0 started
The recorder runs on the car unit and reads SocketCAN and gpsd directly
(decision 25, proposed). svc-gw and app-flutter talk boundary v0, both
channels on one WebSocket with their own semantics (decision 26, proposed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-09 20:24:32 +09:00
mikkeli 9a1512793d Roadmap: link the car power plan and its open restart problem
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:10:52 +09:00
mikkeli e171d7ae99 Roadmap: note UPS holdover from the first bench test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:03:21 +09:00
mikkeli dfa692d763 Phase 0: GNSS fix achieved; moving map no longer blocked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:39:31 +09:00
mikkeli ccd5b54450 Record measured Z27AG CAN: 500 kbps, ungatewayed OBD port
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:38:45 +09:00
mikkeli 668b40f330 ORIN-RUNTIME: buffer-to-terminator receiver is an acceptable first step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:10:48 +09:00
mikkeli 16b1031b8c REPOS: remove the stale duplicate of Naming..Scaffolding
The front-matter commit (d234a2d) accidentally appended a second copy of
the Naming through Scaffolding sections. The current-state rewrite
(574df14) then updated only the first copy, leaving the tail with old
wording and the superseded `type: default` front-matter rule. The tail
held nothing the first copy and the Front matter section don't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:51:59 +09:00
mikkeli feb9564ce4 Link drops: restoring PD costs ~2.2 s with the tablet awake
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:46:34 +09:00
mikkeli 5c7c0677cb Link-drop section: attach the ignition remark to the PD reset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:43:52 +09:00
mikkeli 3422b084ac Link drops: PD change costs ~3.5 s; app must keep the tablet awake
Further USB tests showed the 5-15 s outages were Android suspending on
battery, not the hub. Kept awake, a PD unplug costs ~3.5 s. Adds the
requirement that the app keeps the tablet awake while it is the display,
and notes a sleeping tablet is invisible to link state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:43:38 +09:00
mikkeli 6300006859 Link-drop section: replug outage is invisible to link state
Measured: plugging the hub's PD back in leaves the Orin's USB interface
up while the tablet stops answering for ~5 s, which only a heartbeat
detects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:25:55 +09:00
mikkeli 5bff21265b Treat link drops as routine: Orin detects, redelivers or expires
PD changes through the tablet's hub reset the USB link for 5-12 s at
every ignition change. The app still re-dials, but the Orin must notice
dead clients by heartbeat (TCP alone leaves half-open sockets), keep
pending reliable traffic to redeliver or expire by age, and accept
idempotent commands so retries across reconnects never act twice.
Recorded as proposed decision 24; no implementation yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:18:50 +09:00
mikkeli ac4626640c USB display link is NCM; app must bind to the Ethernet network
Tested with the tablet: it picks NCM, not RNDIS, from the Orin's L4T
gadget. Android won't route app traffic over a network without internet
while WiFi or cellular is up, so app-flutter has to request and bind
the Ethernet network. The Orin's USB-C port has no PD, so the tablet is
charged through a PD hub in the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:12:45 +09:00
mikkeli 6fc187570c Orin is headless: drop local video out, ALSA instead of PipeWire
The tablet app is the display, so the diagram's hud (local video out)
branch goes; displays are all remote. The car unit now runs no desktop
and no sound server, so the host layer lists ALSA, and mic capture for
Whisper reads it directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:02:05 +09:00
mikkeli ce4eaa1aaf Note the LTE/GNSS hat is car-unit only
The hat sits in a custom enclosure on the car Orin and isn't swapped
to the development unit, so modem and gpsd testing happens on the car
unit and gnss development elsewhere runs on its mock input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:59:08 +09:00
mikkeli c395d4e3b0 Architecture: note the stale boot clock and the metered povo SIM
The car unit has no RTC battery, so before NTP the clock sits near the
last shutdown time; that is the case the two-clock envelope exists for.
The LTE SIM is povo, not an IoT SIM, and its connection now carries
the metered flag the map-sync guard relies on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:58:51 +09:00
mikkeli f05fda71ee Phase 0: record host bring-up progress on the car unit
LTE is up over QMI on povo, metered and ranked below Ethernet/WiFi, with
failover tested. CAN interfaces now have stable udev names (can_pcan,
can_onboard) since canN order raced at boot, so references to can1/can0
go. GNSS stays open: gpsd runs but has no fix until an open-sky test,
which is now what blocks the moving map. Kernel-update rebuild steps
are documented for both out-of-tree modules; time sync is pinned to NICT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:58:51 +09:00
mikkeli d8c40eb9ed Name the two Orin units by hostname
The hardware table listed mikkeli-orin-nano-2 as the main compute, but
that is the development unit; the car unit is mikkeli-orin-nano. Both
the README and the REPOS units table now say which is which.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:58:51 +09:00
7 changed files with 215 additions and 147 deletions
+17
View File
@@ -0,0 +1,17 @@
# Cross-repo consistency (yukart/tool-crosscheck): decisions, topics, doc references,
# service lists and front matter checked against their source in every yukart repo.
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
crosscheck:
# Gitea dispatches a reusable-workflow call to a runner chosen by the CALLER's runs-on;
# without one, any runner may take it (the instance's pi5-runner did, and failed).
runs-on: halogen
uses: yukart/tool-crosscheck/.gitea/workflows/crosscheck.yml@master
with:
repo: ${{ github.event.repository.name }}
sha: ${{ github.sha }}
secrets: inherit
+5 -1
View File
@@ -3,6 +3,10 @@ type: reference
---
# Project Yukart Overview
**The goal:** take the car's telemetry, add more of our own, and do the calculations off the car's
ECUs, so the results inform the **driver**. Yukart controls the driver's information, never the car
(architecture decision 31).
A Mitsubishi Z27AG Colt Ralliart, plus
A Jetson Orin Nano that can:
@@ -13,4 +17,4 @@ A Jetson Orin Nano that can:
- voice recognition with local `whisper`
- cloud LLM-based reasoning
- navigation capabilities using GCP APIs (or equivalent)
- vision via camera module (pending, low priority)
- vision via a USB camera (IMX662; on the car unit, roles and inference pending)
+5 -4
View File
@@ -32,10 +32,11 @@ Planned repositories and the reasoning behind the split are described in
| Part | Role |
|---|---|
| NVIDIA Jetson Orin Nano Super (`mikkeli-orin-nano-2`) | Main compute, JetPack r36.4 / Ubuntu 22.04 |
| PEAK PCAN-USB | Car CAN bus access (`can1`, 500 kbps) |
| Onboard MTTCan | Second CAN interface (`can0`) |
| Waveshare SIM7600G-H-M.2 | LTE Cat 4 + GNSS |
| NVIDIA Jetson Orin Nano Super (`mikkeli-orin-nano`) | Car unit: main compute, JetPack r36.4 / Ubuntu 22.04 |
| NVIDIA Jetson Orin Nano Super (`mikkeli-orin-nano-2`) | Development unit: builds, CI, bench (see [REPOS](docs/REPOS.md)) |
| PEAK PCAN-USB | Car CAN bus access (`can_pcan`, 500 kbps measured at the OBD port; nothing transmits on it; OBD-II is on the K-line, through a Bluetooth ELM327) |
| Onboard MTTCan | Second CAN interface (`can_onboard`) |
| Waveshare SIM7600G-H-M.2 | LTE Cat 4 (povo, fallback uplink below WiFi/Ethernet) + GNSS |
Details and rationale live in the environment repo's
[bill of materials](gitea:yukart/orin-nano-env-setup).
+65 -16
View File
@@ -16,12 +16,13 @@ rest down.
## Shape
```
CAN (can1) ──▶ can-bridge ──┐
CAN ──▶ can-bridge ──┐
OBD-II ──▶ obd-poller ─┤
GNSS ──▶ gnss ├──▶ message bus ──┬──▶ recorder (disk)
camera ──▶ vision* ─┘ ├──▶ hud (local video out)
└──▶ telemetry-gw ──▶ remote displays
camera ──▶ vision* ─┘ └──▶ telemetry-gw ──▶ remote displays (tablet app)
* later phase, separate task
The Orin runs headless: no local video out. The tablet app is the display.
```
## Services
@@ -29,12 +30,13 @@ rest down.
| Service | Responsibility | Transmits on CAN |
|---|---|---|
| `can-bridge` | Read SocketCAN frames, timestamp, publish raw | No |
| `obd-poller` | Active OBD-II PID request/response | **Yes** |
| `obd-poller` | Active OBD-II PID request/response, on the K-line through the Bluetooth ELM327 (decision 30); never on CAN | **Yes** |
| `gnss` | gpsd client → position, speed, time fix | No |
| `decoder` | Raw frames → named signals via DBC | No |
| `recorder` | Persist raw + decoded sessions to disk | No |
| `recorder` | Persist raw + decoded sessions to disk; raw CAN and GNSS read directly, not via the bus (decision 25) | No |
| `telemetry-gw` | Serve external display clients | No |
| `vision` | Camera capture and on-device inference (later) | No |
| `orin-status` | The Orin's own health: temperatures, CPU/GPU load, memory, network links, UPS battery → `yukart/host/*` | No |
| `vision` | Camera capture (any V4L2 camera; in practice USB UVC, MJPEG), live view, software H.264 recording; on-device inference later. Repo `svc-vision`, Python (decision 29) | No |
Each ships as a systemd unit. The host-level prerequisites they depend on
(kernel modules, device permissions, gpsd) are the environment repo's job, not
@@ -50,7 +52,8 @@ protocol, so it necessarily transmits onto a 2008 car's bus. Constraints that
follow:
- `can-bridge` runs listen-only and must never transmit.
- `obd-poller` is the **single writer**. No other service opens a TX-capable socket.
- `obd-poller` is the **single writer**: the only service that sends anything to the car, OBD-II
requests on the K-line through the ELM. No service transmits on CAN; none opens a TX-capable CAN socket.
- Transmission is rate-limited and restricted to standard OBD-II diagnostic IDs.
- TX is a distinct, explicitly enabled mode — not the default state on boot.
@@ -59,7 +62,7 @@ traffic; discovered signals are recorded as a DBC rather than as code.
## Message bus
**Proposed: MQTT (mosquitto) on the Jetson.** Rationale:
**MQTT (mosquitto) on the Jetson**, listening on localhost only: displays reach the bus through `svc-gw`. Rationale:
- First-class client libraries on every display target — ESP32 (Arduino
`PubSubClient` / ESP-IDF) and Android (Paho) — which is the deciding factor.
@@ -73,6 +76,7 @@ traffic; discovered signals are recorded as a DBC rather than as code.
yukart/can/raw high-rate raw frames, not retained
yukart/vehicle/<signal> decoded signals (rpm, speed, coolant, …), retained
yukart/gnss/fix position/speed/time, retained
yukart/host/<signal> the Orin itself: temperatures, load, links, UPS battery, retained
yukart/system/<service> health and status, retained
yukart/nav/guidance next manoeuvre, distance, street name, retained
yukart/nav/route active route geometry, retained
@@ -84,6 +88,8 @@ Payloads: a self-describing JSON envelope for decoded signals; compact binary fo
Every envelope carries **two clocks**. `monotonic` is authoritative for ordering
and intervals; `timestamp` is the absolute reference and may step, since the car
boots without network and NTP corrects the clock once LTE connects mid-drive.
The car unit has no RTC battery, so until then the clock sits at roughly the
last shutdown time — offset by however long the car was parked.
Topic names, payload schemas and the signal registry live in
[contracts](gitea:yukart/contracts).
@@ -140,7 +146,33 @@ pressure, upstream plain request/response.** That is decision 9 generalised from
route requests to everything the app sends.
The app always initiates. The Orin listens and never dials out to a display, so
reconnection is entirely the client's problem.
re-dialling is the client's job. Noticing the loss and redelivering is the Orin's.
### Link drops are routine, not exceptional
Measured on the USB path (tablet ↔ PD hub ↔ Orin, 2026-10-09): cutting PD power
resets the hub's data port, and the link is back within ~3.5 s with no
intervention; restoring it costs ~2.2 s (a gadget reconnect, no detach). In the car that happens at every ignition change, cranking
included. Longer outages measured early on were the tablet sleeping on battery,
not the link. Design for drops as normal operation. Requirements, not yet implemented:
- **Failure detection on the Orin.** An app-level heartbeat with a timeout of a
few seconds, not TCP alone. A connection on a link that went down can stay
half-open long after the app has re-dialled. Reap dead connections and their
per-client queues promptly. Link state doesn't help either: a tablet that has
fallen asleep on battery leaves the Orin's USB interface up while answering
nothing.
- **The app keeps the tablet awake while it is the display.** Off charger, Android
suspends about a minute after screen-off. Then the link is gone in every way that
matters. Kept awake, a PD change costs only 2–3.5 s (measured).
- **Downstream (latest-wins):** unchanged. On reconnect, push current state; no
resend, since stale samples are worthless.
- **Reliable channel (TTS chunks, acknowledgements):** pending items survive a
drop. Redeliver on reconnect, or expire by age (an utterance that is several
seconds late is better dropped than played). Within the window, a reconnect
should resume rather than start over.
- **Upstream retries cross reconnects**, so commands carry an idempotency key.
At-least-once delivery then never means acting twice.
### Cloud links are outbound-initiated
@@ -183,13 +215,21 @@ rewrite: the seam stays visible even while the process is one.
## Display contract
**Every display is data-only.** The Orin sends no video to anything. Each device
subscribes to the bus and renders itself.
subscribes to the bus and renders itself. The one proposed exception is the
camera's live view (decision 27): a paced video stream beside the boundary,
never on the bus.
| Path | Transport | Notes |
|---|---|---|
| `app-flutter` | USB (RNDIS), or WiFi | The display. Full client, richest UI, also the audio sink |
| `app-flutter` | USB (NCM: tablet is host, Orin at `192.168.55.1`), or WiFi | The display. Full client, richest UI, also the audio sink |
| `app-esp32` + RLCD/e-ink | WiFi, or USB/BT serial | Low-rate glanceable subset. Not in current scope |
Over USB, Android sends app traffic only via its *default* network, and the
USB link, having no internet, never becomes default while WiFi or cellular is
up. `app-flutter` must request the Ethernet network (`TRANSPORT_ETHERNET`) and
bind to it. The Orin's USB-C port can't charge the tablet (no USB PD), so in the
car the link runs through a PD hub that feeds the tablet from the car charger.
Displays reaching the bus over IP connect to the broker through `svc-gw`, which
owns the boundary. For transports without IP — BT SPP, plain USB serial —
`svc-gw` bridges serial ↔ bus rather than each device inventing its own protocol.
@@ -259,10 +299,11 @@ They are never committed. `tool-mapmint` holds the scripts that fetch and build
them, plus a report of each build's provenance; `svc-nav` consumes the output. The
artifacts themselves live outside git and never touch the workstation-side repo.
They reach the car by `rsync` **over the LAN only, never over LTE** — the IoT SIM
is not a sensible transport for a multi-gigabyte tile set. Gate the sync on a
They reach the car by `rsync` **over the LAN only, never over LTE** — the metered
povo SIM is not a sensible transport for a multi-gigabyte tile set. Gate the sync on a
reachable LAN target rather than on an SSID, so the check validates the thing it
actually needs, and treat NetworkManager's metered flag as a second guard.
actually needs, and treat NetworkManager's metered flag as a second guard (set on the `povo`
connection).
Cadence is quarterly or on annoyance; road geometry does not move fast enough to
justify anything more.
@@ -312,11 +353,14 @@ rewriting the document.
| 1 | Separate services over a message bus, not one process | Accepted | 2026-08-14 |
| 2 | Displays are bus subscribers; no display-specific logic upstream | Accepted | 2026-08-14 |
| 3 | Listen-only sniffing + active OBD-II, single writer | Accepted | 2026-08-14 |
| 4 | MQTT as the bus | **Proposed** | 2026-08-14 |
| 5 | Rust for on-Orin services, Python for off-car tooling, Dart for the display client | Accepted | 2026-09-04 |
| 4 | MQTT as the bus; raw CAN rate confirmed on the car Orin (svc-core `BUS-BENCH.md`) | Accepted | 2026-10-12 |
| 5 | Rust for on-Orin services, Python for off-car tooling, Dart for the display client. One exception: decision 29 | Accepted | 2026-09-04 |
| 21 | Two clocks per envelope; `monotonic` orders, `timestamp` references | Accepted | 2026-09-05 |
| 22 | `signals.json` is the single source for signal name and unit; both are derived, never authored twice | Accepted | 2026-09-05 |
| 23 | Services build natively on the development Orin; both units stay on the same L4T release | Accepted | 2026-09-05 |
| 24 | Link drops are routine: `svc-gw` detects dead clients by heartbeat and redelivers or expires pending reliable traffic; upstream commands are idempotent | **Proposed** | 2026-10-09 |
| 25 | The recorder reads SocketCAN and gpsd directly; raw capture never depends on the broker | Accepted | 2026-10-12 |
| 26 | Boundary v0 carries both channels of decision 15 on one WebSocket, each keeping its own semantics (drop-oldest down, acked and idempotent up) | **Proposed** | 2026-10-09 |
| 6 | Local logging first; off-board LTE telemetry deferred | Accepted | 2026-08-14 |
| 7 | Camera/AI inference is a later, separate task | Accepted | 2026-08-14 |
| 8 | Self-hosted offline navigation (MapLibre GL + Valhalla + PMTiles) over a cloud maps SDK | Accepted | 2026-09-03 |
@@ -332,3 +376,8 @@ rewriting the document.
| 18 | Stubs publish the real topic and schema, never a fixture-specific one | Accepted | 2026-09-03 |
| 19 | A service mocks itself behind its own input interface; there is one implementation of each contract | Accepted | 2026-09-04 |
| 20 | Every display is data-only. Directly-driven screens are for permanently installed, car-powered panels only | Accepted | 2026-09-04 |
| 27 | Camera live view is the one video stream to the tablet: its own connection beside the boundary, paced to the link (≤ ~4 MB/s), never on the bus. Exception to 20 | **Proposed** | 2026-10-11 |
| 28 | Every Orin → tablet sender caps its socket (`SO_MAX_PACING_RATE`, ~5 MB/s); unpaced bursts stall the USB link for seconds | **Proposed** | 2026-10-11 |
| 29 | `svc-vision` is Python: camera pipelines and inference live in that ecosystem. Exception to 5 | Accepted | 2026-10-12 |
| 30 | The car's CAN broadcast is the source for every displayed vehicle value. OBD-II (K-line, ISO 9141-2, through the Bluetooth ELM327) is calibration, recorded beside the frames; a value proven absent from CAN may be displayed from OBD-II, and keeps its `obd_` name either way. GNSS speed is shown beside the decoded speed, to show their error. The ELM is the Orin's only way onto the K-line, so DTC scans go through it too; it is reached over classic Bluetooth serial (SPP, no pairing), BLE only if it proves as easy | Accepted | 2026-10-13 |
| 31 | Yukart informs the driver; it never controls the car. It extracts the car's telemetry (CAN, OBD-II), adds its own (GNSS, IMU, camera), and does its calculations off the car's ECUs; the results go to the driver (display, voice). No actuation of any kind (locks, lights, throttle), by CAN transmit or by wiring into the car's switches. Asking is allowed: OBD-II requests over the K-line (live data, DTC reads) are active but leave the ECU as it was. Anything that changes an ECU's state is not (no actuator tests, no coding), with one exception: **DTC clearing** (mode 04) is allowed as a deliberate, driver-initiated action at the driver's own risk. It is never automatic, it asks for confirmation, and it reads and records the codes and freeze frame first, since mode 04 wipes them along with readiness. Decision 3's "no TX on CAN" follows from this rather than only from caution | Accepted | 2026-10-15 |
+17 -5
View File
@@ -17,10 +17,10 @@ repo, so it lives in the hub rather than in any one of them.
─────────────────────────────────────────────────────────
serving svc-gw, svc-nav
processing decoder, recorder
acquisition can-bridge, obd-poller, gnss
acquisition can-bridge, obd-poller, gnss, imu
bus mosquitto
─────────────────────────────────────────────────────────
host kernel modules, gpsd, PipeWire, network orin-nano-env-setup
host kernel modules, gpsd, ALSA, network orin-nano-env-setup
```
A layer may depend on the one below it and on the bus. Nothing depends sideways:
@@ -40,7 +40,7 @@ have taken their share.
| Valhalla server | small; tiles are **mmap'd page cache** | evictable — see below |
| Whisper | 273 MB (tiny) → 3.9 GB (large) | `small` is 244 M params; `large` does not fit |
| GPT-SoVITS | upstream guidance assumes an 8 GB VRAM card | the largest unknown; **measure before designing around it** |
| vision (Phase 5) | — | competes directly with voice for the same pool |
| vision (Phase 5) | capture and H.264: small; inference: — | capture + software H.264 costs ~1.3–2 of 6 CPU cores at 1080p (no NVENC on the Orin Nano); inference competes directly with voice for the same pool |
Four rules follow.
@@ -68,8 +68,8 @@ Ordered by what is lost if a tier is late.
|---|---|---|
| 0 | kernel modules, gpsd, network | Host layer; nothing of ours can precede it |
| 1 | `mosquitto`, `can-bridge`, `recorder` | **Data is lost if late.** Must reach ready fastest |
| 2 | `decoder`, `gnss`, `obd-poller` | Derive from tier 1; a late start loses nothing |
| 3 | `svc-gw`, `svc-nav` | Serving; a display can wait |
| 2 | `decoder`, `gnss`, `imu`, `obd-poller`, `orin-status` | Derive from tier 1; a late start loses nothing |
| 3 | `svc-gw`, `svc-nav`, `svc-vision` | Serving; a display can wait |
| 4 | voice — ASR, TTS | Interactive; seconds late is harmless |
**Capture first** is the whole principle. A CAN frame missed at tier 1 is gone
@@ -161,6 +161,11 @@ play" player.** It works, and it forecloses streaming permanently — the receiv
has no way to begin before the last byte. The chunk boundary is what keeps both
TTS modes available.
What *is* fine early on: a receiver that buffers chunks and plays on the
terminator. The wire format is already the streaming one, so moving to playback
on the first chunk is a change to the receiver alone. Streaming playback is the
end state.
### The transport is testable with no real TTS
Audio is the lowest-priority part of the system, but its transport does not have
@@ -180,6 +185,13 @@ Whisper runs on the Orin, so its input does too. Audio *in* on one box and audio
*out* on the other is asymmetric on purpose: each side does the simple thing, and
a microphone has no reason to reach the head unit.
The USB camera has a microphone (48 kHz stereo, ALSA card `U1080P`): a candidate
input, if its placement and quality suit speech. Untested.
The Orin runs headless with no sound server: capture reads the ALSA device
directly. If a service ever needs PipeWire, run it for that service's user
(lingering), not as a desktop session.
### Degrade when there is no sink
With no tablet connected the Orin can still hear — the mic is local — but cannot
+19 -102
View File
@@ -24,16 +24,18 @@ contracts repo, not copied into each consumer.
| [orin-nano-env-setup](gitea:yukart/orin-nano-env-setup) | Jetson host bring-up |
| [app-flutter](gitea:yukart/app-flutter) | Flutter display client |
| [contracts](gitea:yukart/contracts) | Topic namespace, payload schemas, signal registry |
| [svc-gw](gitea:yukart/svc-gw) | Telemetry gateway — aggregation and the HTTP surface |
| [svc-core](gitea:yukart/svc-core) | Acquisition + processing cluster: `recorder`, `decoder`, `gnss`, `imu`, `orin-status`, `obd-poller` now; `can-bridge` to come |
| [svc-gw](gitea:yukart/svc-gw) | Telemetry gateway — aggregation and the display boundary |
| [svc-nav](gitea:yukart/svc-nav) | Navigation service — routing and guidance |
| [svc-vision](gitea:yukart/svc-vision) | Camera: capture and the live view; recording and inference to come |
| [tool-mapmint](gitea:yukart/tool-mapmint) | Map artifact minting, workstation-side |
| [tool-mock](gitea:yukart/tool-mock) | Mocks and fixtures — a fake car on a workstation |
| [tool-crosscheck](gitea:yukart/tool-crosscheck) | Consistency checks across all yukart repos; a reusable workflow every repo runs on push |
## Planned
| Repository | Role | Why separate |
|---|---|---|
| `svc-core` | Acquisition + processing cluster: `can-bridge`, `obd-poller`, `gnss`, `decoder`, `recorder` | Developed and deployed as one unit on the Orin |
| `app-esp32` | ESP32 RLCD/e-ink display firmware (Class B) | Embedded toolchain, flashed not deployed |
| `tool-analysis` | Offline session analysis, reverse-engineering notebooks | Runs off-car, no runtime coupling |
@@ -133,7 +135,8 @@ docs/BOM.md only where the repo owns hardware
| Where | Language | Why it is there |
|---|---|---|
| `svc-*` — on the Orin | **Rust** | No GC, low resident memory on a shared 8 GB pool, and fast start for the tier-1 capture services |
| `svc-*` — on the Orin, except `svc-vision` | **Rust** | No GC, low resident memory on a shared 8 GB pool, and fast start for the tier-1 capture services |
| `svc-vision` — on the Orin | **Python** | Camera pipelines (GStreamer) and inference live in the Python ecosystem (decision 29). Standard library and host packages only, until a dependency earns its place |
| `tool-*` — off-car | **Python** | Batch work where development speed is the only constraint that binds |
| `app-flutter` | **Dart** | Flutter |
@@ -153,8 +156,12 @@ Two Orin Nanos, and they have different jobs:
| Unit | Role |
|---|---|
| Development Orin | Build, CI runner, bench testing. Always powered, on the LAN |
| Car Orin | Deployment target only |
| Development Orin (`mikkeli-orin-nano-2`) | Build, CI runner, bench testing. Always powered, on the LAN |
| Car Orin (`mikkeli-orin-nano`) | Deployment target only |
The SIM7600 LTE/GNSS hat lives in the car unit only — its custom enclosure doesn't
move between units. Anything touching the modem or gpsd is tested on the car unit;
on the development unit, `gnss` runs against its own mock input (decision 19).
⚠ **The guarantee is version parity, not architecture.** An aarch64 binary built
on a newer userland still fails to load on an older one. A JetPack or Ubuntu
@@ -209,6 +216,13 @@ because everything above 2.30 comes from one event: glibc 2.34 merged
`libpthread` and `libdl` into `libc` and re-versioned the moved symbols. Stable,
but not guaranteed the way a native build is.
⚠ **`std::process::Command` breaks it.** Any binary that spawns a process links
`pidfd_spawnp` and `pidfd_getpid` (GLIBC_2.39) from the workstation's sysroot.
They are weak references, but the version requirement is not, so the car Orin
refuses to load the binary. `check-glibc.sh` catches it; such binaries build
natively (svc-core's `orin-status` runs `mmcli`, so its deploy script builds on
the development Orin).
### Writing style
Documents describe **what is and what will be**. They are not changelogs — git
@@ -253,100 +267,3 @@ for any particular repo**. The `type` discriminates:
`status` and `updated` make a plan's staleness visible — plans go stale and
reports do not, and without them a plan from months ago reads identically to one
written this morning. `date` on a report records the moment it describes.
## Naming
Repositories are named `<group>-<thing>`, with **no project prefix**. The `yukart`
Gitea org already namespaces everything, so a `yukart-` prefix would be a constant
occupying the most significant characters of every name — pure noise in a sorted
index, and the reason the earlier scheme scattered.
| Group | Meaning |
|---|---|
| `svc-` | A service running on the Orin |
| `app-` | A display client, running off the Orin |
| `tool-` | Off-car tooling; never deployed to the car |
The goal is that **lexical order equals semantic grouping**: opening the org
listing puts every service together, every client together, every tool together.
The group segment does that work because it is the first thing that varies.
Ungrouped singletons — `contracts` — are fine where a category has exactly one
member and always will.
`project-yukart` and `orin-nano-env-setup` predate this and keep their names
rather than paying a rename for two repos that are stable and easy to find.
### Reusability
Nothing in a name marks a repo as reusable. A component that turns out to be
genuinely reusable outside Yukart **leaves the org** rather than being renamed —
location says it, not spelling.
## Helper and deployment repos
Arduino and ESP32 work implies flashing, build and deployment scaffolding that
is likely to be reused. Keep those generic and outside the org — e.g. an ESP32
project skeleton with the toolchain, flashing scripts and CI already wired up.
### Gitea template repositories
Gitea supports these: repo **Settings → Repository → Template**, after which the
repo offers **"Use this template"** to create new repos from it.
Use a template repo for **scaffolding** — a new ESP32 or Arduino project that
should start with the toolchain, directory layout and flashing scripts in place.
Do **not** use one for code you intend to keep updating in one place. Template
instantiation is a fork-at-create: the copy has no link back, so improvements to
the template never reach repos already created from it. For genuinely shared
code, use a git submodule or publish a package instead.
Rule of thumb: *template for the starting shape, submodule/package for living
code.*
## Linking convention
Cross-repo links use the `gitea:owner/repo` scheme, as established in the hub
README — e.g. `[Environment setup](gitea:yukart/orin-nano-env-setup)`.
## Per-repo document layout
Following the convention already set by `orin-nano-env-setup`:
```
README.md minimal portal, points everywhere else
docs/
GUIDELINES.md implementation practice for this repo
BOM.md hardware, where the repo owns hardware
impl/
plans/ what is intended
reports/ what actually happened, and why
```
The `impl/plans` ÷ `impl/reports` split is worth keeping: plans go stale,
reports do not. A report like `PCAN-SETUP.md` stays valuable precisely because
it records the investigation and the dead ends, not just the final commands.
### Scaffolding conventions
No template repository for this. Template instantiation is fork-at-create, so it
could not reach the repos that already exist, and the scaffolding is three
markdown files — lighter than the machinery to manage it. A template earns its
place where there is a real toolchain to carry, which is the ESP32 case above.
What every component repo has instead:
```
README.md frontmatter, the shared blurb, "What this repo is
about", then links to guidelines and impl/
docs/GUIDELINES.md frontmatter, numbered sections, ends "## N. To be added"
docs/impl/plans/ at least INITIAL.md
docs/impl/reports/
docs/BOM.md only where the repo owns hardware
```
**Frontmatter follows the document's nature, not its location:** living documents
carry `type: default`; historical records do not. So README, GUIDELINES, BOM and
plans have it, and reports do not. The hub carries none at all, being the portal
rather than a component.
+87 -19
View File
@@ -1,7 +1,7 @@
---
type: plan
status: active
updated: 2026-09-04
updated: 2026-10-13
---
# Roadmap
@@ -12,12 +12,29 @@ something demonstrable in the car.
Getting the Jetson to see the hardware at all.
- [x] PCAN-USB working as SocketCAN `can1` (out-of-tree `pcan.ko`, netdev build)
- [x] PCAN-USB working as SocketCAN (out-of-tree `pcan.ko`, netdev build)
- [x] Stable CAN names by udev: `can_pcan`, `can_onboard` — `canN` order raced at boot
- [x] Z27AG OBD-port CAN measured: 500 kbps, not gatewayed, 27 periodic IDs at
~890 frames/s; `can_pcan` comes up at 500k on hot-plug (verified 2026-10-13;
boot not yet seen), acknowledging rather than listen-only
- [x] SIM7600G-H-M.2 specified and documented
- [ ] SIM7600 brought up: LTE data path and GNSS via gpsd
- [ ] Kernel-update procedure for the out-of-tree PCAN module — it does not
survive kernel upgrades and will break silently
- [x] SIM7600 LTE data path: QMI (self-built `qmi_wwan`), povo, metered, ranked
below Ethernet/WiFi; failover tested, Tailscale stays reachable over LTE
- [x] SIM7600 GNSS via gpsd (`/dev/sim7600-gps`): 3D fix in the car
- [x] Kernel-update procedure for the out-of-tree modules (`pcan`, `qmi_wwan`, `pl2303`) —
documented, sources kept in `~/infra`; still manual, and breaks silently
if forgotten
- [x] Network time pinned to NICT; timezone `Asia/Tokyo`
- [x] USB camera (IMX662, Sunplus UVC bridge) recognised on the car unit with
in-tree drivers: MJPEG up to 1080p60, microphone; measured in
orin-nano-env-setup `docs/impl/reports/USB-CAMERA.md`
- [ ] Optional: RTC backup battery (CR2032) — without it the clock boots stale
until NTP or GPS corrects it
- [ ] Power and mounting in the car: clean shutdown on ignition off
(UPS module bench test 2026-10-09: carries the Orin ≥ 20 s across a PD
supply swap, enough to ride through cranking)
— plan in orin-nano-env-setup `docs/impl/plans/POWER.md`; open: the Orin
doesn't auto-start at ignition on while the UPS keeps it powered
Tracked in [orin-nano-env-setup](gitea:yukart/orin-nano-env-setup).
@@ -25,29 +42,57 @@ Tracked in [orin-nano-env-setup](gitea:yukart/orin-nano-env-setup).
Get real Z27AG traffic onto disk before designing anything around it.
- [x] `recorder` — raw CAN + GNSS sessions on the car unit (`yukart-recorder.service`):
starts on traffic or `ctl start`, ends after 15 s idle, marks via `ctl mark` or
the app; fsync'd every second, bounded at 50 GB, ~20 MB per hour of driving
- [ ] Off-board retention: sessions roll off the car unit once copied to dedicated
storage (halogen over tailnet/WiFi, or object storage such as S3), instead of
living on board until the cap. Not urgent: first checklist session was 6.2 MB
for 16 min, 423 GB free (2026-10-13)
- [ ] `can-bridge` — listen-only, timestamped frames onto the bus
- [ ] `recorder` — persist raw sessions
- [ ] Capture drive sessions across varied conditions (idle, cold start, moving,
lights/indicators/windows actuated) with a written log of what was done when
The manual event log matters more than it sounds: it is what makes the raw
capture decodable later.
capture decodable later. Marks go into the session itself (`yukart-recorder ctl
mark …`, or the app's Mark button), on the same clock as the frames.
## Phase 2 — Decode
- [ ] Correlate captures against the event log; identify recurring frame IDs
- [ ] Build the Z27AG DBC incrementally in `contracts`
- [ ] `decoder` — raw frames to named signals
- [ ] `obd-poller` — active OBD-II PIDs, single writer, rate-limited
- [ ] `gnss` — position/speed, useful as ground truth for validating decoded speed
- [ ] Correlate captures against the event log; identify recurring frame IDs —
prior art and first candidates (engine speed in `0x210` / `0x13D`) in contracts
`docs/impl/reports/Z27AG-CAN-PRIOR-ART.md`
- [ ] Build the Z27AG signal set incrementally in `contracts` — in `signals.json`'s
`can` blocks, not a hand-kept DBC (one is generated if a tool needs it)
- [x] `decoder` — raw frames to named signals, 2026-10-13: svc-core `decoder`, laid out
only by the `can` blocks in contracts `signals.json` (rpm, accelerator, brake,
handbrake, indicators, side lights, dipped beam, doors, A/C compressor)
- [x] `obd-poller` — active OBD-II PIDs, single writer, 2026-10-12: mode 01, the
nine `obd2` signals (~10 Hz fast, 1 Hz slow), only while the bus is alive;
`can_pcan` out of listen-only. Recorded beside the frames (`vehicle.ndjson`).
2026-10-13: the Z27AG answers OBD-II only on the K-line (ISO 9141-2), so the
poller now goes through the Bluetooth ELM327 (classic serial, no pairing):
the `obd_*` signals, calibration references per decision 30
- [ ] OBD-II beyond mode 01: DTC scan (modes 03/07/0A) and MUT-III `21 xx`
requests, both needing multi-frame (ISO-TP) replies
- [ ] First decoding captures in the car, per contracts
`docs/impl/plans/Z27AG-CAPTURES.md` — first checklist session 2026-10-13
(117 marks, yukart-data `can-captures/`); jack-stands section still to do
- [x] `gnss` — position/speed, useful as ground truth for validating decoded speed,
2026-10-13: svc-core `gnss`, the fix as `yukart/gnss/<signal>` (speed, heading,
position, altitude, satellites); `yukart/gnss/fix` for navigation still to come
OBD-II gives standardised signals quickly and is worth doing early; it also
provides a cross-check for reverse-engineered ones.
## Phase 3 — Bus and contracts
- [ ] Stand up the broker; confirm MQTT holds up at real CAN rates (decision 4
in [ARCHITECTURE.md](ARCHITECTURE.md) is still *proposed*)
- [x] Stand up the broker, 2026-10-12: mosquitto 2.0.11 on the car Orin,
localhost only. First publisher `orin-status` (svc-core,
`yukart/host/*`); `svc-gw` subscribes and serves it to the tablet
- [x] Confirm MQTT holds up at real CAN rates (`yukart/can/raw`), 2026-10-12:
no loss at a saturated bus, per frame or in 10 ms batches; batched costs
the broker ~1 % of a core (svc-core `BUS-BENCH.md`)
- [ ] Freeze v1 of topic names and payload schemas
- [ ] `telemetry-gw` — external client access, including the serial bridge
@@ -56,13 +101,36 @@ provides a cross-check for reverse-engineered ones.
Order chosen by feedback speed, not ambition.
- [ ] One display end-to-end first, proving the contract — `app-flutter` is the
quickest to iterate on, and is the only display in current scope
quickest to iterate on, and is the only display in current scope.
Started: boundary v0 (contracts `docs/BOUNDARY.md`) between `svc-gw` on the
car unit and a text-readout starter app; not yet run on the tablet
- [ ] `app-esp32` — RLCD/e-ink glanceable subset. Deferred, no hardware in scope
## Phase 5 — Camera and inference · *separate task*
## Phase 5 — Camera, then inference
Deliberately deferred. It is the reason the compute is a Jetson rather than a
Pi, but it shares nothing with the CAN work beyond the bus.
The camera is on the car unit (2026-10-11). Capturing and recording need no
model and no GPU, so they can come before inference, which stays deferred: it is
the reason the compute is a Jetson rather than a Pi, but it shares nothing with
the CAN work beyond the bus, and it competes with voice for memory.
What the hardware gives (USB camera report): MJPEG only, ~7.3 MB/s at 1080p30;
no hardware video encoder on the Orin Nano, so H.264 is software, ~1.3–2 of 6
cores at 1080p; frame rate falls with the light.
- [ ] Decide the camera's roles and mounting: dash recording, live view on the
tablet, inference input, or several
- [ ] Measure in the car: daylight and night frame rate and MJPEG size on a road
scene
- [ ] One capture process owns the camera (V4L2 is single-reader) and feeds
the others; sets UVC controls on open, since they reset on re-enumeration
- [ ] Recording: H.264 segments on the recorder's clock and session marks,
bounded like CAN sessions (~2.7 GB per hour at 6 Mbit/s)
- [x] Live view (decision 27, proposed), 2026-10-12: `svc-vision` serves 720p
MJPEG passthrough on `:7710` (`yukart-vision.service`, ~2% of a core),
paced per decision 28, camera-agnostic, says when there is no camera;
`app-flutter` shows it with exposure and gain controls
- [ ] Inference — deferred; plan for evicting voice, not for coexisting with it
([ORIN-RUNTIME.md](ORIN-RUNTIME.md))
## Navigation · *parallel workstream*
@@ -79,7 +147,7 @@ and `tool-mapmint` (artifact builds).
- [ ] Pull a Japan basemap extract from the Protomaps daily build with
`go-pmtiles`; static-serve it, with `localIdeographFontFamily` set for CJK
- [ ] MapLibre page rendering the basemap from a replayed GNSS trace
- [ ] Moving map live in the car — **blocked on gpsd in Phase 0**
- [ ] Moving map live in the car (GNSS fix available on the car unit)
- [ ] Meili map matching over recorded sessions, as Phase 2 ground truth
- [ ] Routing and turn-by-turn guidance onto `yukart/nav/guidance`