6a7de03773
obd-poller asks 0x7DF for the nine obd2 signals, only while the bus is alive, and publishes envelopes stamped with the reply's kernel time. The recorder subscribes to yukart/vehicle/* and writes vehicle.ndjson beside the frames. Tested against a fake ECU on vcan0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
180 lines
9.1 KiB
Markdown
180 lines
9.1 KiB
Markdown
---
|
||
type: plan
|
||
status: active
|
||
updated: 2026-10-12
|
||
---
|
||
# Initial plan
|
||
|
||
## Order
|
||
|
||
1. **`recorder`** — raw CAN and GNSS to disk. Built first because every later
|
||
piece is developed against what it records.
|
||
2. `obd-poller` — standard OBD-II PIDs at a low rate, the cross-check for
|
||
reverse-engineered signals. The first transmitter: `can_pcan` leaves
|
||
listen-only for it, a deliberate step.
|
||
3. `can-bridge` and `gnss` — publish onto the broker once it exists (project-yukart
|
||
Phase 3; MQTT at ~890 frames/s is still unproven).
|
||
4. `decoder` — frames to named signals via the DBC in `contracts`.
|
||
|
||
Alongside: **`orin-status`** — in place (2026-10-12), the first publisher on the
|
||
bus. Below.
|
||
|
||
## Recorder
|
||
|
||
Reads SocketCAN and gpsd directly. It is tier 1, and the broker is a single point
|
||
of failure that raw capture should not share (decision 25). When `can-bridge`
|
||
exists, the recorder still owns raw capture; it subscribes to the bus only for
|
||
`yukart/vehicle/*` (`--broker`, `''` to disable), and a broker that is down costs
|
||
those envelopes, never a frame.
|
||
|
||
### Session triggers
|
||
|
||
| Trigger | Starts on | Ends on |
|
||
|---|---|---|
|
||
| auto | the first CAN frame | `--idle` (15 s) with no frames |
|
||
| manual | `ctl start [label]` | `ctl stop`, or shutdown |
|
||
|
||
- **No traffic, no session.** The PEAK attached at a desk, attached to a parked
|
||
car with the bus asleep, or unplugged, produces nothing — the socket is retried
|
||
every second in silence.
|
||
- **Cranking stays inside one session.** A bus dropout or adapter re-enumeration
|
||
shorter than the idle timeout is recorded as `can_link` events, not as a split.
|
||
- **Blips are discarded.** An auto session with under `--min-secs` (3 s) of
|
||
traffic and no marks is deleted at close — door unlocks and brief plug-ins.
|
||
- `ctl start` during an auto session promotes it to manual. After `ctl stop`, auto
|
||
start waits for the bus to go idle, so a stop sticks.
|
||
- `ctl auto off` (or `--no-auto`) leaves only manual sessions.
|
||
|
||
The power service planned in orin-nano-env-setup (`docs/impl/plans/POWER.md`)
|
||
is the intended second client: `ctl mark crank` for a recovered input loss, and
|
||
`systemctl stop yukart-recorder` before poweroff.
|
||
|
||
### Session layout
|
||
|
||
```
|
||
/var/lib/yukart/recordings/
|
||
.last-seq high-water mark; numbers are never reused
|
||
000042-20261009T163736/ sequence number orders; the local time is for humans
|
||
session.json status open → closed | truncated; trigger, label, host,
|
||
boot_id, start/end realtime + monotonic + clock sync,
|
||
frame and drop counts, end reason
|
||
can-0001.log.zst candump -L, closed chunks (5 min or 64 MB), zstd
|
||
can-0002.log the open chunk, plain
|
||
gnss.ndjson {"rx": <local receive time>, "msg": <gpsd report>}
|
||
vehicle.ndjson {"rx": …, "signal": …, "env": <envelope>} for every message on
|
||
yukart/vehicle/* (OBD-II now); "retained": true for the
|
||
broker's stored copy, which may predate the session
|
||
events.ndjson marks, can_link, gnss_link, clock_step, drops, storage_bound
|
||
```
|
||
|
||
`zstdcat can-*.log.zst can-*.log | canplayer` replays a session.
|
||
|
||
### Failsafes
|
||
|
||
| Failure | Behaviour |
|
||
|---|---|
|
||
| Power loss, crash, SIGKILL | open chunk is fdatasync'd every second; at the next start the session is marked `truncated` and its chunks compressed |
|
||
| Crash mid-compression | compression writes `.zst.tmp` then renames; the plain chunk is removed only after |
|
||
| Adapter unplugged, link down | reopen every second; recorded as `can_link` events |
|
||
| gpsd down | reconnect every 3 s; CAN capture unaffected |
|
||
| Writer slower than the bus | 100 000-frame queue (~110 s); overflow counted as `queue_drops` |
|
||
| Kernel socket overflow | `SO_RXQ_OVFL` counted as `kernel_drops` |
|
||
| Disk filling | oldest closed sessions deleted to stay under `--cap-gb` (50) and above `--min-free-gb` (20); the open session is never deleted |
|
||
| Control socket fails | logged; auto capture continues |
|
||
|
||
Measured on the first capture: ~33 KB/s of candump text, ~5.5 KB/s after zstd —
|
||
**about 20 MB per hour of driving**. The 50 GB default holds years of it; the cap
|
||
is there for the failure where something else fills the disk.
|
||
|
||
### Time
|
||
|
||
The car unit has no RTC battery yet and can start with a stale clock that chrony
|
||
steps later. CAN frames carry the kernel's receive timestamp (`SO_TIMESTAMP`,
|
||
CLOCK_REALTIME), and GNSS reports carry their local receive time — **the same
|
||
clock**, so CAN and GNSS line up within a session however wrong that clock is.
|
||
|
||
To get absolute time back afterwards:
|
||
|
||
- `session.json` records the kernel's sync state at start and end.
|
||
- `clock_step` events record every step over 50 ms with its size.
|
||
- Each TPV holds GNSS UTC beside its local receive time; their difference is the
|
||
clock error, plus the SIM7600's NMEA latency (no PPS: expect tens to hundreds
|
||
of milliseconds, constant enough to calibrate out).
|
||
|
||
At 1 Hz GNSS against ~890 frames/s, alignment to a few hundred milliseconds is
|
||
what this gives. It is enough to match CAN speed against GNSS speed; it is not
|
||
enough to time individual frames against position.
|
||
|
||
## Open
|
||
|
||
- **Session marking from the app.** `app-flutter`'s mark button reaches the
|
||
recorder through `svc-gw`, which will forward to the control socket.
|
||
- **Native builds.** `scripts/deploy-recorder.sh` cross-compiles from the
|
||
workstation while the development Orin is offline; `check-glibc.sh` gates it.
|
||
- **Copy-off.** Recordings stay on the car until the parked-at-home step in the
|
||
power plan, or a manual `rsync`.
|
||
|
||
## obd-poller
|
||
|
||
OBD-II mode 01 (SAE J1979) on `can_pcan`, the single CAN writer. Tier 2.
|
||
|
||
- **Requests** go to the functional address `0x7DF`, one at a time; the first
|
||
single-frame reply from `0x7E8`–`0x7EF` answers it (kernel filter: nothing else
|
||
reaches the process). 100 ms timeout, 10 ms between requests.
|
||
- **Discovery** walks the supported-PID bitmasks (`01 00`, `01 20`, …) whenever the
|
||
bus wakes; only supported PIDs are polled.
|
||
- **Schedule:** rpm, speed, throttle, load, MAP and MAF every round (~10–13 Hz each
|
||
with a 4 ms ECU); coolant, intake air and fuel level once a second.
|
||
- **Only while the bus is alive:** the interface's received-frame count moved in
|
||
the last second. A request into a sleeping bus gets no ACK, and the adapter would
|
||
resend it until error-passive.
|
||
- **Output:** envelopes on `yukart/vehicle/<signal>`, retained; names, units and the
|
||
`obd2` source checked against contracts at build. `timestamp` is the reply's
|
||
kernel receive time, identical to its line in the recorder's CAN log.
|
||
- **State** on `yukart/system/obd-poller`: `ready` with supported PIDs, achieved
|
||
rates and unanswered count every 5 s; `degraded` with the reason (bus idle, no
|
||
ECU answers, interface errors); last will `stopping`.
|
||
- `--once`: discover, read each supported PID once, print JSON with reply times.
|
||
- **Not yet:** multi-frame replies (ISO-TP flow control), so no DTC reading (modes
|
||
03/07/0A) and no MUT-III `21 xx` requests. The kernel has no `can-isotp`; flow
|
||
control will be done by hand.
|
||
|
||
Tested 2026-10-12 against a fake ECU on `vcan0`: 7 PIDs discovered and polled,
|
||
540 requests and 540 replies in the recorder's log, every envelope's timestamp
|
||
equal to its reply frame's.
|
||
|
||
## orin-status
|
||
|
||
The Orin's own health as envelopes on `yukart/host/<signal>`, retained, once a
|
||
second; names and units from contracts `signals.json` (source `host`). Tier 2,
|
||
ordered after the broker only.
|
||
|
||
| Group | Source |
|
||
|---|---|
|
||
| Temperatures | thermal zones `cpu`, `gpu`, `soc*` (hottest), `tj`; the `cv*` zones read EAGAIN while the vision accelerators are off and are skipped |
|
||
| Load and memory | `/proc/stat`, the GPU's `load` (tenths of a percent), `/proc/meminfo` (one pool for CPU and GPU) |
|
||
| Power and fan | onboard INA3221 `VDD_IN`; `pwm_tach` |
|
||
| Network | `/sys/class/net` counters → kbit/s; WiFi level from `/proc/net/wireless`; the modem from `mmcli -m any -J` every 5 s on its own thread; the tablet link is carrier on `usb0`/`usb1`, counted on `l4tbr0` |
|
||
| UPS | INA219 on `/dev/i2c-7` `0x41`, below |
|
||
|
||
- A reading that fails is not published. Its retained value ages and `svc-gw`
|
||
shows it stale.
|
||
- Service state on `yukart/system/orin-status`: `ready`, or `degraded` with the
|
||
failed sources; last will `stopping`.
|
||
|
||
### UPS battery
|
||
|
||
- **Read-only.** Only register pointers are ever written; the INA219 stays at its
|
||
power-on configuration. Current is shunt voltage (10 µV per bit) over 0.01 Ω
|
||
(Waveshare's demo code), positive when charging. Bus voltage is 4 mV per bit.
|
||
- **State:** `charging` above +50 mA, `discharging` below −50 mA (on battery:
|
||
input lost), `idle` between.
|
||
- **Charge** is an estimate from a 21700 resting-voltage table per cell, the
|
||
reading corrected by the pack's I·R drop (0.15 Ω, a rough figure) and smoothed.
|
||
Waveshare's demo maps 9.0–12.6 V linearly instead, which swings with load.
|
||
Steadiest at rest on input power, least reliable while charging.
|
||
- The ignition-off policy (orin-nano-env-setup `docs/impl/plans/POWER.md`) needs
|
||
the same readings. It should take them from here, or live here, rather than
|
||
open the INA219 a second time.
|
||
|