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
9.1 KiB
type, status, updated
| type | status | updated |
|---|---|---|
| plan | active | 2026-10-12 |
Initial plan
Order
recorder— raw CAN and GNSS to disk. Built first because every later piece is developed against what it records.obd-poller— standard OBD-II PIDs at a low rate, the cross-check for reverse-engineered signals. The first transmitter:can_pcanleaves listen-only for it, a deliberate step.can-bridgeandgnss— publish onto the broker once it exists (project-yukart Phase 3; MQTT at ~890 frames/s is still unproven).decoder— frames to named signals via the DBC incontracts.
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_linkevents, 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 startduring an auto session promotes it to manual. Afterctl 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.jsonrecords the kernel's sync state at start and end.clock_stepevents 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 throughsvc-gw, which will forward to the control socket. - Native builds.
scripts/deploy-recorder.shcross-compiles from the workstation while the development Orin is offline;check-glibc.shgates 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 from0x7E8–0x7EFanswers 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 theobd2source checked against contracts at build.timestampis the reply's kernel receive time, identical to its line in the recorder's CAN log. - State on
yukart/system/obd-poller:readywith supported PIDs, achieved rates and unanswered count every 5 s;degradedwith the reason (bus idle, no ECU answers, interface errors); last willstopping. --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 xxrequests. The kernel has nocan-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-gwshows it stale. - Service state on
yukart/system/orin-status:ready, ordegradedwith the failed sources; last willstopping.
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:
chargingabove +50 mA,dischargingbelow −50 mA (on battery: input lost),idlebetween. - 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.