Compare commits

...

8 Commits

Author SHA1 Message Date
mikkeli b52a7dcedb Checklist v3: IMU calibration (level twice, straight pull-aways and stops); yukart/raw/imu
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-14 13:28:40 +09:00
mikkeli 4cbac36fbe IMU signals in the car's frame (attitude, gravity-free g, yaw rate) on yukart/imu/<signal>
/ crosscheck (push) Successful in 0s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 17s
The WT61's publisher owns mounting and gravity; displays get car-frame values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 23:23:31 +09:00
mikkeli 5d186bfd5d Captures plan: OBD-II through the ELM on the K-line, obd_* names, what each poller state means
/ 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 22:57:31 +09:00
mikkeli 229d796cfd OBD-II signals renamed obd_*, plus timing, trims, O2; GNSS signals; elm_connected; reboot command
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Failing after 6s
Decision 30: CAN feeds displays, OBD-II (K-line via the ELM) calibrates, so every
obd2 signal carries the obd_ prefix and the plain names are left for decoded ones.
yukart/gnss/<signal> gives displays the fix as single values; gnss/fix stays for
navigation. Boundary: reboot beside poweroff, same guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:50:03 +09:00
mikkeli 5feba8b354 Decode report: clutch and transmission not on CAN, stated once; no camera trigger plan
/ 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 21:48:07 +09:00
mikkeli 0eab0e660e Decode report: reverse rechecked, gear inference dropped for a recommended gear
/ 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 21:44:52 +09:00
mikkeli 34140cc666 Decoded Z27AG signals: layouts in signals.json can blocks, the only source
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Failing after 6s
engine_rpm moves to source z27ag (no OBD-II on this car's CAN). Ten more:
accelerator, brake, handbrake, indicators, side lights, dipped beam, doors,
A/C compressor. Guideline 6: no service writes a CAN ID of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 21:31:51 +09:00
mikkeli 77d6b79c6b Z27AG first decoding pass: rpm, pedal, brake, handbrake, indicators, lights, doors, wheel speeds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 20:47:20 +09:00
8 changed files with 545 additions and 47 deletions
+5 -2
View File
@@ -17,11 +17,14 @@ definitions from here.
|---|---| |---|---|
| [`schema/topics.json`](schema/topics.json) | Topic namespace: payload kind, retention, rate, publisher | | [`schema/topics.json`](schema/topics.json) | Topic namespace: payload kind, retention, rate, publisher |
| [`schema/envelope.json`](schema/envelope.json) | JSON Schema for the decoded-signal envelope | | [`schema/envelope.json`](schema/envelope.json) | JSON Schema for the decoded-signal envelope |
| [`schema/signals.json`](schema/signals.json) | Signal registry — the single source for name, unit, type and range | | [`schema/signals.json`](schema/signals.json) | Signal registry — the single source for name, unit, type and range, and for where each decoded Z27AG signal sits in its CAN frame (`can` block) |
| [`docs/BOUNDARY.md`](docs/BOUNDARY.md) | Display boundary protocol: what crosses between `svc-gw` and the app | | [`docs/BOUNDARY.md`](docs/BOUNDARY.md) | Display boundary protocol: what crosses between `svc-gw` and the app |
| [`checklists/`](checklists/) | Recording checklists as data; the app bundles exact copies (its **Captures** page) | | [`checklists/`](checklists/) | Recording checklists as data; the app bundles exact copies (its **Captures** page) |
The Z27AG DBC lands here too, once Phase 2 produces one. Decoded Z27AG signals (source `z27ag`) carry their frame layout in `signals.json` itself,
so name, unit and bits cannot drift apart. A DBC for tools like SavvyCAN or cantools,
if one is wanted, is generated from it, never edited by hand. Findings behind each
layout: [`docs/impl/reports/`](docs/impl/reports/).
- For implementation practice guidelines, refer to the [guidelines](docs/GUIDELINES.md); - For implementation practice guidelines, refer to the [guidelines](docs/GUIDELINES.md);
- For implementation plans and history, refer to [them](docs/impl/); - For implementation plans and history, refer to [them](docs/impl/);
+54 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "z27ag-captures", "id": "z27ag-captures",
"version": 2, "version": 3,
"title": "Z27AG decoding captures", "title": "Z27AG decoding captures",
"about": "docs/impl/plans/Z27AG-CAPTURES.md", "about": "docs/impl/plans/Z27AG-CAPTURES.md",
"mark_format": "<step> <action>[ #<n>] start|end for a span, <step> <action>[ #<n>] for a point", "mark_format": "<step> <action>[ #<n>] start|end for a span, <step> <action>[ #<n>] for a point",
@@ -447,6 +447,59 @@
] ]
} }
] ]
},
{
"title": "IMU calibration (module fixed in its final place)",
"steps": [
{
"id": "27",
"title": "IMU level, facing one way",
"do": "Parked on the flattest ground at hand, engine off or idling, nobody moving inside. Start, keep still 15 s, end.",
"finds": "which way is down for the IMU (with step 28, cancelling the ground's slope)",
"actions": [
{
"label": "imu level A",
"hold_s": 15
}
]
},
{
"id": "28",
"title": "IMU level, turned round",
"do": "The same spot, nose the other way: a rough 180° will do. Still again, 15 s.",
"finds": "the second half of the level: the slope's tilt reverses, the mounting's does not",
"actions": [
{
"label": "imu level B",
"hold_s": 15
}
]
},
{
"id": "29",
"title": "IMU pull-away, straight",
"do": "A straight, flat section. From standstill, a firm pull-away in 1st to about 30. Start as the clutch bites, end as you lift.",
"finds": "which way is forward for the IMU",
"actions": [
{
"label": "imu pull away",
"repeat": 3
}
]
},
{
"id": "30",
"title": "IMU braking, straight",
"do": "The same section. From about 40, a firm (not emergency) stop. Start as you brake, end when stopped.",
"finds": "forward again, from the other side; also braking g",
"actions": [
{
"label": "imu brake",
"repeat": 3
}
]
}
]
} }
] ]
} }
+9 -7
View File
@@ -37,7 +37,7 @@ The two channels from decision 24 share the socket but keep different guarantees
| `ack` | reply to a `cmd` | `id`, `ok`, `detail`, `dup` (true when answered from the cache) | | `ack` | reply to a `cmd` | `id`, `ok`, `detail`, `dup` (true when answered from the cache) |
With `source: bus`, `signals` is the newest envelope of every signal svc-gw has With `source: bus`, `signals` is the newest envelope of every signal svc-gw has
seen on `yukart/vehicle/+` and `yukart/host/+`. svc-gw marks one seen on `yukart/vehicle/+`, `yukart/host/+`, `yukart/gnss/+` and `yukart/imu/+`. svc-gw marks one
`quality: "stale"` when it has not been refreshed for 5 s, or is known only from `quality: "stale"` when it has not been refreshed for 5 s, or is known only from
the broker's retained copy (which may predate a reboot). A signal once seen is the broker's retained copy (which may predate a reboot). A signal once seen is
never removed. never removed.
@@ -66,18 +66,20 @@ tablet, or an Orin behind a replugged hub, looks connected at the link layer.
|---|---|---| |---|---|---|
| `ping` | — | `ack` with `detail: "pong"` | | `ping` | — | `ack` with `detail: "pong"` |
| `mark` | `text`, `delay_ms` (optional: how long the command waited before this send) | stamped into the open recording; fails with no session open | | `mark` | `text`, `delay_ms` (optional: how long the command waited before this send) | stamped into the open recording; fails with no session open |
| `poweroff` | `confirm: "poweroff"`, `boot_id` (from this connection's `hello`) | acks, then shuts the host down cleanly (`systemctl poweroff`) about 2 s later. Refused unless svc-gw runs with `--allow-poweroff` (the Orin's unit only), and when `boot_id` is not the current boot's | | `poweroff` | `confirm: "poweroff"`, `boot_id` (from this connection's `hello`) | acks, then shuts the host down cleanly (`systemctl poweroff`) about 2 s later. Refused unless svc-gw runs with `--allow-power` (the Orin's unit only), and when `boot_id` is not the current boot's |
| `reboot` | `confirm: "reboot"`, `boot_id` | the same, with `systemctl reboot`; the same flag and refusals |
A repeated `id` returns the original `ack` (last 256 kept, shared across A repeated `id` returns the original `ack` (last 256 kept, shared across
connections) and never re-runs the command. connections) and never re-runs the command.
`poweroff` is the exception to retry-until-acked: the ack cache does not survive `poweroff` and `reboot` are the exception to retry-until-acked: the ack cache does
the reboot it causes, so a resend could shut the next boot down too. The client not survive the restart they cause, so a resend could shut the next boot down (or
sends it once and does not resend it; `boot_id` makes svc-gw refuse one from a reboot it again). The client sends them once and does not resend; `boot_id` makes
previous boot regardless. svc-gw refuse one from a previous boot regardless.
## Not yet ## Not yet
- TTS utterances (id, chunk sequence, terminator, expiry) — when there is a TTS. - TTS utterances (id, chunk sequence, terminator, expiry) — when there is a TTS.
- `gnss`: its fix is an object, which the envelope's `value` does not allow yet. - `gnss/fix` as one object (navigation's input): the envelope's `value` does not allow
one yet. Displays get the fix as single values on `yukart/gnss/<signal>` instead.
- Any version beyond `proto: 0`. A breaking change bumps it; the client checks it. - Any version beyond `proto: 0`. A breaking change bumps it; the client checks it.
+12 -2
View File
@@ -9,7 +9,7 @@ type: guidelines
- ⚠ **The unit appears in both the topic name and the payload, so they can - ⚠ **The unit appears in both the topic name and the payload, so they can
contradict each other.** Generating both from one entry is what stops that; a contradict each other.** Generating both from one entry is what stops that; a
hand-written `"unit"` reintroduces the failure it was meant to prevent. hand-written `"unit"` reintroduces the failure it was meant to prevent.
- Signal names carry their unit (`vehicle_speed_kph`, `coolant_temp_c`), so a unit - Signal names carry their unit (`gnss_speed_kph`, `obd_coolant_temp_c`), so a unit
mismatch is visible at the call site rather than one registry lookup away. mismatch is visible at the call site rather than one registry lookup away.
## 2. Two clocks, and they answer different questions ## 2. Two clocks, and they answer different questions
@@ -33,4 +33,14 @@ type: guidelines
- There is no version in any topic or payload. A recorded session names the - There is no version in any topic or payload. A recorded session names the
contracts tag it was captured under, which is what keeps old captures readable. contracts tag it was captured under, which is what keeps old captures readable.
## 6. To be added ## 6. A decoded signal's layout lives only in its registry entry
- Every `source: z27ag` signal has a `can` block in `signals.json`: frame ID, byte,
bit or length, byte order, scale and offset. The decoder reads those blocks and
nothing else, so a correction lands in one file and every consumer sees it.
- ⚠ **No CAN ID or bit position is written in any service's code.** A layout
hard-coded downstream is a second source that silently disagrees the day the
registry is corrected.
- Each block names the report with its evidence (`evidence`). A layout without
evidence is a guess and does not go in.
## 7. To be added
+38 -15
View File
@@ -1,7 +1,7 @@
--- ---
type: plan type: plan
status: ready status: ready
updated: 2026-10-13 updated: 2026-10-14
--- ---
# Z27AG decoding captures: what to do in the car # Z27AG decoding captures: what to do in the car
@@ -13,10 +13,11 @@ ground truth. Prior art and first candidates: [the prior-art report](../reports/
| In the session directory | From | Use | | In the session directory | From | Use |
|---|---|---| |---|---|---|
| `can-*.log(.zst)` | the PEAK, every frame, kernel timestamps | what gets decoded; also holds every OBD request (`7DF`) and reply (`7E8`) | | `can-*.log(.zst)` | the PEAK, every frame, kernel timestamps | what gets decoded (nothing transmits on CAN: OBD-II is on the K-line) |
| `vehicle.ndjson` | `obd-poller` via the bus | the same OBD replies as values (rpm, speed, throttle, load, MAP, MAF ~10 Hz; coolant, intake air, fuel 1 Hz); `env.timestamp` equals the reply frame's timestamp | | `vehicle.ndjson` | `decoder` and `obd-poller` via the bus | decoded CAN signals, and the `obd_*` references from the K-line through the ELM327 (rpm, speed, throttle, load, MAF, timing, O2 every round; coolant, intake air, trims every 5 s); `env.timestamp` is the reply's arrival at the Orin |
| `events.ndjson` | marks from the app | when each step happened | | `events.ndjson` | marks from the app | when each step happened |
| `gnss.ndjson` | gpsd (SIM7600) | speed and position for the drive | | `gnss.ndjson` | gpsd (SIM7600) | speed and position for the drive |
| `imu.ndjson` | `imu` via the bus (`yukart/raw/imu`) | every IMU sample, raw module axes, Orin receive time: calibration (27–30) and, once calibrated, motion for the drive |
## Operating it from the app ## Operating it from the app
@@ -33,8 +34,9 @@ ground truth. Prior art and first candidates: [the prior-art report](../reports/
session` when there is no traffic yet (ignition on, PEAK plugged). A mark sent with session` when there is no traffic yet (ignition on, PEAK plugged). A mark sent with
the link down waits (`unacked`) and goes when it is back, with its delay noted. the link down waits (`unacked`) and goes when it is back, with its delay noted.
- The **Mark** button on the main screen is still there for anything off-plan. - The **Mark** button on the main screen is still there for anything off-plan.
- **OBD is working** when `engine_rpm`, `vehicle_speed_kph` and the rest appear in - **OBD is working** when `obd_engine_rpm`, `obd_coolant_temp_c` and the rest appear
the signal list and move. If they never appear, see *If OBD stays silent*. in the signal list and move (the ELM on the OBD port, through the Y harness beside
the PEAK). If they never appear, see *If OBD stays silent*.
## The steps ## The steps
@@ -52,14 +54,20 @@ recorder refuses marks with "no session" otherwise); engine off, doors shut.
- **Engine running, parked** (14–18): start and idle, held revs in neutral (no OBD - **Engine running, parked** (14–18): start and idle, held revs in neutral (no OBD
on this car's CAN: the revs are approximate, read off the tacho), blips, A/C load, radiator fan. on this car's CAN: the revs are approximate, read off the tacho), blips, A/C load, radiator fan.
- **Driving** (19–22): steady speeds, stops, corners, each gear. A passenger presses, - **Driving** (19–22): steady speeds, stops, corners, each gear. A passenger presses,
or skip the marks: GNSS speed is recorded continuously, and gear comes from or skip the marks: GNSS speed is recorded continuously. The gear is not on CAN and
rpm ÷ speed. is not inferred (decode report).
- **On jack stands** (23–26, since v2): front wheels up, rears chocked, handbrake - **On jack stands** (23–26, since v2): front wheels up, rears chocked, handbrake
off. Unpowered steering; speedometer readings held in 2nd and 3rd (fronts turn, off. Unpowered steering; speedometer readings held in 2nd and 3rd (fronts turn,
rears stay still, which tells the wheels apart); braking from speed with vacuum rears stay still, which tells the wheels apart); braking from speed with vacuum
assist; reverse. Stands replace the driving speeds where a drive is not practical. assist; reverse. Stands replace the driving speeds where a drive is not practical.
- **IMU calibration** (27–30, since v3): once the IMU is fixed in its final place. Level
twice on the same spot, facing one way and then turned round (a rough 180° is fine:
the ground's slope reverses and cancels, the mounting's tilt does not); then three
firm pull-aways and three firm stops on a straight, flat section (forward, signed by
which kind of stretch it is). Any session holds raw IMU samples (`imu.ndjson`), so
these can go in their own short session or with anything else.
Version 2 (2026-10-13) adds the stands section and writes down the conditions the Version 3 (2026-10-14) adds the IMU calibration section. Version 2 (2026-10-13) adds the stands section and writes down the conditions the
first run showed matter: engine off and handbrake on for 1–13, foot brake held for first run showed matter: engine off and handbrake on for 1–13, foot brake held for
the handbrake step, neutral with the clutch up for the revs. the handbrake step, neutral with the clutch up for the revs.
@@ -80,18 +88,33 @@ The parked steps can be split over several sessions; the drive is the important
## If OBD stays silent ## If OBD stays silent
`yukart/system/obd-poller` says why (visible in `mosquitto_sub -v -t 'yukart/system/#'` `yukart/system/obd-poller` says why (visible in `mosquitto_sub -v -t 'yukart/system/#'`
on the Orin): on the Orin), and the signal list's `elm_connected` shows the Bluetooth link:
- `bus idle: not polling`: no traffic: ignition off or PEAK unplugged. - `ELM …: … (unplugged, asleep or out of range)`: no Bluetooth link. The ELM in the
- `no ECU answers 01 00`: the engine ECU does not do standard mode 01 on this bus. port? Its light on? A phone app holding it (it takes one connection)? Retried
Then OBDb's MUT-III (`21 xx`) requests are the next thing to try. every minute.
- `ECU not answering (…): ignition off?`: the ELM is there, the car is not. The
adapter's own words are in the brackets (`BUS INIT: ...ERROR`, `NO DATA`).
- `ECU silent for 60 s: adapter released`: ignition off for a minute; the poller let
go so the ELM can sleep, and tries again every minute.
- To see what answers, on the Orin: `sudo systemctl stop yukart-obd-poller`, - To see what answers, on the Orin: `sudo systemctl stop yukart-obd-poller`,
`yukart-obd-poller --once` (supported PIDs and one value each, with reply times), `yukart-obd-poller --elm $(. /etc/default/yukart-elm; echo $ELM_ADDR) --once`
`sudo systemctl start yukart-obd-poller`. Never two pollers at once: their (adapter, protocol, supported PIDs and one value each, with reply times), then
replies would cross. `sudo systemctl start yukart-obd-poller`.
## Afterwards ## Afterwards
**IMU calibration**, on the car Orin, from the session that holds steps 27–30:
```bash
yukart-imu --calibrate /var/lib/yukart/recordings/<session> # print: down, forward, slope, spread
yukart-imu --calibrate /var/lib/yukart/recordings/<session> --write # and save /var/lib/yukart/imu/mount.json
```
The service picks the new mounting up within seconds; roll and pitch should then read
about 0 parked on level ground. Steps 27–28 alone also work: forward then stays the
`--forward` axis, laid flat, until a session with 29–30 is calibrated.
Sessions are in `/var/lib/yukart/recordings/` on the car Orin. Copy one off with Sessions are in `/var/lib/yukart/recordings/` on the car Orin. Copy one off with
`rsync -a mikkeli-orin-nano:/var/lib/yukart/recordings/<session>/ …` into `rsync -a mikkeli-orin-nano:/var/lib/yukart/recordings/<session>/ …` into
`yukart-data`, with a line in its README of what was done. `yukart-data`, with a line in its README of what was done.
+70
View File
@@ -0,0 +1,70 @@
---
type: report
date: 2026-10-13
---
# Z27AG CAN: first decoding pass
Session `z27ag-20261013-170050-checklist` (yukart-data `can-captures/`), checklist v1,
117 marks. Method: list every transition of every rarely-changing bit and read it
against the marks; for blinking signals, count toggles inside each marked range
against the seconds before it; for values, dump bytes across the marked range; for
speed, correlate every 16-bit word with GNSS speed over the crawls. Marks lag the
action by a fraction of a second to a few seconds (the operator presses after doing),
so a change shortly *before* a start mark is normal.
Bit numbering: `ID.byte.bit`, byte 0 first, bit 7 the MSB.
## Found
| Signal | Where | Evidence | Confidence |
|---|---|---|---|
| Engine speed | `210` bytes 4–5, BE, 1 rpm | idle 740–870; holds at ~1410 and ~1900 for "1500" / "2000" (operator: revs not exact); blip peak 2887 | High |
| Engine speed, finer | `13D` bytes 0–1, LE, ≈ rpm × 2.048 | moves in lockstep with `210` | High |
| Accelerator pedal | `210` byte 0, 0x00 released … 0xFA full (÷2.5 → %) | engine-off sweep: 0 → 0xFA → 0 | High |
| Accelerator sensors 1 and 2 | `139` bytes 0 and 1 (raw, 0x33 rest … 0xEE full, track each other) | same sweep | High |
| Throttle-related | `139` byte 2 (0x21 … 0xD5) | same sweep; plate or target | Medium |
| Pedal-pressed flag | `210` byte 3 bit 7 | set exactly while the pedal is off rest | Medium |
| Brake pedal switch | `231.4.1` (1 = pressed) | 3/3 presses, plus the foot-brake holds around the handbrake step and the crawls | High |
| Handbrake | `239.0.2` (1 = applied) | set through every engaged stretch, clear during the crawls and the handbrake releases | High |
| Left indicator | `423.2.6` blinking | 2/2 | High |
| Right indicator | `423.2.5` blinking | 2/2 | High |
| Hazards | `423.2.6` only blinks, 2/2 | odd: `2.5` stays still. Maybe `2.6` is "left or hazard", maybe the hazard has its own bit elsewhere at a rate this method missed | Low |
| Side lights | `423.2.2` and `570.7.3` (1 = on) | on/off and during dipped | High |
| Dipped beam | `423.3.1` (1 = on) | 1/1 | Medium (one sample) |
| Any door open | `423.2.1` | driver and passenger | Medium |
| Driver door | `423.0.4` | driver only, not passenger | Medium |
| Driver belt unbuckled | `573.5.7` (1 = unbuckled) | set at ignition on, cleared at buckle; no release seen | Low |
| A/C request / blower | `443` bytes 1–2 and `711` bytes 5–6, multi-bit | change with A/C and blower; not separated yet | Low |
| A/C compressor engaged | `13D.6.7` | engine running, A/C on; set ~0.5 s after the request, cleared with A/C off | Medium |
| Four wheel speeds | `235` four 16-bit words (bytes 0–1, 2–3, 4–5, 6–7) | zero parked, all four move together in the crawls | High (signal), **scale open** |
| Vehicle speed | `231` byte 6 (and probably byte 7) | tracks the wheel words | Medium |
**Wheel-speed scale.** Crawls topped out at ~6 km/h, and so far only the first byte of each word moved
(0x4C at GNSS 3.5 km/h). Read as little-endian ×0.05 km/h (the forfour's `200`
scale), that is 3.8 km/h, which is plausible given GNSS lag. Big-endian is not ruled out. The
jack-stands session and a GNSS-matched drive settle both the byte order and the scale.
## Not on this bus (or not seen)
- **Clutch and transmission are not on this bus.** Nothing changed for the clutch
(3 presses), any of the 5 gears, neutral or reverse: nothing on the car consumes
them, so no frame carries them. Reverse was rechecked against both reverse
stretches (engine off and engine on) versus every other position: no bit
separates them.
- **Gear is not inferred from rpm ÷ wheel speed** (owner's call, 2026-10-13): the
clutch is invisible, so the ratio lies whenever it is down or slipping, and the
gear matters most at a standstill, where the ratio says nothing. A *recommended*
gear from speed and rpm is the useful form, if any.
- **Steering angle:** not analysed yet (step 5 was done with the engine on, at the end).
## Not analysed yet
- `312` (four noisy 16-bit words that follow the throttle: MAP/load/air candidates)
- `449` temperatures (coolant candidate), `710`–`713` slow body/meter frames
- `0C0`, `119`, `149`, `156` (counters/checksums; `149` inertial candidate)
## Scripts
Scratch analysis for now: a session loader, a transition timeline, a blink counter and a
GNSS correlator, in Python with numpy. They become a tool once a DBC exists to check
against.
+335 -19
View File
@@ -1,17 +1,7 @@
{ {
"$comment": "Single source for signal identity. Topic name and envelope unit are both derived from these entries and are never authored separately. source=obd2 is standardised; source=z27ag is reverse-engineered and lives in the DBC. source=host is the Orin itself (orin-status), not the car.", "$comment": "Single source for signal identity. Topic name and envelope unit are both derived from these entries and are never authored separately. source=obd2 is standardised (OBD-II mode 01 over the K-line, through the ELM327): calibration references, every name prefixed obd_ (project-yukart decision 30); source=host is the Orin itself (orin-status), not the car. source=gnss is the receiver (svc-core gnss), on yukart/gnss/<signal>. source=imu is the WT61 IMU, already turned into the car's frame (mounting and gravity handled by its publisher), on yukart/imu/<signal>. source=z27ag is reverse-engineered from the car's own broadcast frames, and its 'can' block is the only place its layout is written down: id (11-bit, hex string), byte (first data byte, 0-7), length in bits (1, 8 or 16), bit (length 1 only: 0 = LSB of that byte), order (length 16: big = byte first is most significant, little = the reverse), scale and offset (value = raw * scale + offset; booleans are raw != 0). 'evidence' names the contracts report that found it.",
"signals": { "signals": {
"engine_rpm": { "obd_vehicle_speed_kph": {
"unit": "rpm",
"type": "number",
"source": "obd2",
"description": "Engine speed",
"range": [
0,
8000
]
},
"vehicle_speed_kph": {
"unit": "kph", "unit": "kph",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -21,7 +11,7 @@
255 255
] ]
}, },
"coolant_temp_c": { "obd_coolant_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -31,7 +21,7 @@
215 215
] ]
}, },
"intake_air_temp_c": { "obd_intake_air_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -41,7 +31,7 @@
215 215
] ]
}, },
"throttle_pct": { "obd_throttle_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -51,7 +41,7 @@
100 100
] ]
}, },
"engine_load_pct": { "obd_engine_load_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -61,7 +51,7 @@
100 100
] ]
}, },
"fuel_level_pct": { "obd_fuel_level_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -71,7 +61,7 @@
100 100
] ]
}, },
"intake_pressure_kpa": { "obd_intake_pressure_kpa": {
"unit": "kpa", "unit": "kpa",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -81,7 +71,7 @@
255 255
] ]
}, },
"maf_gps": { "obd_maf_gps": {
"unit": "g/s", "unit": "g/s",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -91,6 +81,210 @@
656 656
] ]
}, },
"obd_engine_rpm": {
"unit": "rpm",
"type": "number",
"source": "obd2",
"description": "Engine speed (OBD-II 0C): the reference for the decoded engine_rpm",
"range": [
0,
8000
]
},
"obd_timing_advance_deg": {
"unit": "degree",
"type": "number",
"source": "obd2",
"description": "Ignition timing advance before TDC, cylinder 1",
"range": [
-64,
64
]
},
"obd_stft_pct": {
"unit": "percent",
"type": "number",
"source": "obd2",
"description": "Short-term fuel trim, bank 1: positive adds fuel",
"range": [
-100,
100
]
},
"obd_ltft_pct": {
"unit": "percent",
"type": "number",
"source": "obd2",
"description": "Long-term fuel trim, bank 1: positive adds fuel",
"range": [
-100,
100
]
},
"obd_o2_b1s1_v": {
"unit": "V",
"type": "number",
"source": "obd2",
"description": "Upstream oxygen sensor voltage (narrowband: ~0.1 lean … ~0.9 rich)",
"range": [
0,
1.275
]
},
"engine_rpm": {
"unit": "rpm",
"type": "number",
"source": "z27ag",
"description": "Engine speed",
"range": [
0,
8000
],
"can": {
"id": "0x210",
"byte": 4,
"length": 16,
"order": "big",
"scale": 1,
"offset": 0,
"evidence": "Z27AG-DECODE-1"
}
},
"accel_pedal_pct": {
"unit": "percent",
"type": "number",
"source": "z27ag",
"description": "Accelerator pedal position (0x00 at rest, 0xFA flat)",
"range": [
0,
100
],
"can": {
"id": "0x210",
"byte": 0,
"length": 8,
"scale": 0.4,
"offset": 0,
"evidence": "Z27AG-DECODE-1"
}
},
"brake_pedal": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Brake pedal switch: pressed",
"can": {
"id": "0x231",
"byte": 4,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"handbrake": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Handbrake applied",
"can": {
"id": "0x239",
"byte": 0,
"bit": 2,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"indicator_left": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Left indicator lamp, as it flashes (on and off at the flasher's rate)",
"can": {
"id": "0x423",
"byte": 2,
"bit": 6,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"indicator_right": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Right indicator lamp, as it flashes. Hazards were seen on the left bit only: open",
"can": {
"id": "0x423",
"byte": 2,
"bit": 5,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"side_lights": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Side (position) lights on; also on with dipped beam",
"can": {
"id": "0x423",
"byte": 2,
"bit": 2,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"dipped_beam": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Dipped headlights on",
"can": {
"id": "0x423",
"byte": 3,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"door_open": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Any door open",
"can": {
"id": "0x423",
"byte": 2,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"driver_door_open": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Driver's door open",
"can": {
"id": "0x423",
"byte": 0,
"bit": 4,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"ac_compressor": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "A/C compressor engaged (engine running)",
"can": {
"id": "0x13D",
"byte": 6,
"bit": 7,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"orin_cpu_temp_c": { "orin_cpu_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",
@@ -333,6 +527,12 @@
10000 10000
] ]
}, },
"elm_connected": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "Bluetooth link to the OBD-II adapter (the ELM327 on the K-line) is up: obd-poller holds it while the ECU answers"
},
"ups_battery_v": { "ups_battery_v": {
"unit": "V", "unit": "V",
"type": "number", "type": "number",
@@ -368,6 +568,122 @@
"type": "string", "type": "string",
"source": "host", "source": "host",
"description": "charging, discharging (running on battery) or idle (on input power, not charging)" "description": "charging, discharging (running on battery) or idle (on input power, not charging)"
},
"gnss_fix": {
"unit": "",
"type": "string",
"source": "gnss",
"description": "Fix quality: none, 2d or 3d (gpsd TPV mode)"
},
"gnss_speed_kph": {
"unit": "kph",
"type": "number",
"source": "gnss",
"description": "Speed over ground; null without a fix",
"range": [
0,
300
]
},
"gnss_heading_deg": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Course over ground, true north; null without a fix or at a standstill",
"range": [
0,
360
]
},
"gnss_lat": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Latitude, WGS84",
"range": [
-90,
90
]
},
"gnss_lon": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Longitude, WGS84",
"range": [
-180,
180
]
},
"gnss_alt_m": {
"unit": "m",
"type": "number",
"source": "gnss",
"description": "Altitude above mean sea level; null without a 3D fix",
"range": [
-500,
9000
]
},
"gnss_satellites": {
"unit": "",
"type": "number",
"source": "gnss",
"description": "Satellites used in the fix",
"range": [
0,
64
]
},
"imu_roll_deg": {
"unit": "degree",
"type": "number",
"source": "imu",
"description": "Body roll in the car's frame: positive leaning right (right side down)",
"range": [
-180,
180
]
},
"imu_pitch_deg": {
"unit": "degree",
"type": "number",
"source": "imu",
"description": "Body pitch in the car's frame: positive nose up",
"range": [
-90,
90
]
},
"imu_accel_lon_g": {
"unit": "g",
"type": "number",
"source": "imu",
"description": "Longitudinal acceleration in the car's frame, gravity removed: positive forward (speeding up), negative braking",
"range": [
-4,
4
]
},
"imu_accel_lat_g": {
"unit": "g",
"type": "number",
"source": "imu",
"description": "Lateral acceleration in the car's frame, gravity removed: positive to the right (a right turn reads positive)",
"range": [
-4,
4
]
},
"imu_yaw_rate_dps": {
"unit": "degree/s",
"type": "number",
"source": "imu",
"description": "Yaw rate: positive turning left (counter-clockwise from above)",
"range": [
-500,
500
]
} }
} }
} }
+22 -1
View File
@@ -12,7 +12,7 @@
"payload": "envelope", "payload": "envelope",
"retained": true, "retained": true,
"rate": "1-50/s", "rate": "1-50/s",
"publisher": "decoder, obd-poller", "publisher": "decoder (source z27ag), obd-poller (source obd2)",
"description": "One topic per signal, named exactly as in signals.json." "description": "One topic per signal, named exactly as in signals.json."
}, },
"yukart/gnss/fix": { "yukart/gnss/fix": {
@@ -22,6 +22,27 @@
"publisher": "gnss", "publisher": "gnss",
"description": "value is an object: lat, lon, alt_m, speed_kph, heading_deg, fix, satellites." "description": "value is an object: lat, lon, alt_m, speed_kph, heading_deg, fix, satellites."
}, },
"yukart/gnss/<signal>": {
"payload": "envelope",
"retained": true,
"rate": "1-10/s",
"publisher": "gnss",
"description": "The receiver's fix as single values, one topic per signal, named exactly as in signals.json (source=gnss). For displays; yukart/gnss/fix stays the composite for navigation."
},
"yukart/imu/<signal>": {
"payload": "envelope",
"retained": true,
"rate": "10-50/s",
"publisher": "imu",
"description": "The IMU (WT61) in the car's frame: attitude, gravity-free acceleration, yaw rate; one topic per signal, named exactly as in signals.json (source=imu)."
},
"yukart/raw/imu": {
"payload": "json",
"retained": false,
"rate": "10-100/s",
"publisher": "imu",
"description": "Every IMU sample, raw: module axes, not calibrated. {mono (ns, CLOCK_MONOTONIC), t (µs, realtime), accel [x,y,z] g, gyro [x,y,z] deg/s, angle [roll,pitch,yaw] deg (the module's own)}. Not an envelope; for the recorder (imu.ndjson), so a later calibration can reprocess a session. Displays use yukart/imu/<signal>."
},
"yukart/host/<signal>": { "yukart/host/<signal>": {
"payload": "envelope", "payload": "envelope",
"retained": true, "retained": true,