Files
mikkeli 6a7de03773 obd-poller: OBD-II mode 01, the single CAN writer; recorder keeps its values
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
2026-10-12 23:25:03 +09:00

180 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.