Z27AG decode pass 3: wheel positions from corners, odometer (570, 24-bit); layouts can be 24 bits
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 25s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-15 22:13:34 +09:00
parent 642be2a5e1
commit e8ba581985
3 changed files with 157 additions and 3 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ ground truth. Prior art and first candidates: [the prior-art report](../reports/
| `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 | | `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 | | `imu.bin` (`imu.ndjson`) | `imu` via the bus (`yukart/raw/imu`) | every IMU sample, packed (svc-core `imu --export` prints it as JSON lines; sessions before 2026-10-16 and anything that does not pack are in `imu.ndjson`), 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
@@ -64,7 +64,7 @@ recorder refuses marks with "no session" otherwise); engine off, doors shut.
twice on the same spot, facing one way and then turned round (a rough 180° is fine: 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 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 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 which kind of stretch it is). Any session holds raw IMU samples (`imu.bin`), so
these can go in their own short session or with anything else. these can go in their own short session or with anything else.
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 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
+59
View File
@@ -0,0 +1,59 @@
---
type: report
date: 2026-10-15
---
# Z27AG CAN: third pass, the drive back
Sessions `000011-20261015T212904` and `000013-20261015T214544` (the drive back, about 13 minutes,
no marks) together with pass 2's `000008` and `000009`: 20 km of driving with GNSS and the calibrated
IMU, no OBD-II. Nothing was marked, so this pass uses what any drive has: corners and distance.
## Confirmed
| Signal | Where | Evidence | Registered as |
|---|---|---|---|
| Wheel speeds | `235` four words, little-endian (pass 2): bytes 0–1 **front right**, 2–3 **front left**, 4–5 **rear right**, 6–7 **rear left** | from corners, below | `wheel_speed_fr_kph`, `_fl_`, `_rr_`, `_rl_` |
| Odometer | `570` bytes 4–6, little-endian, **1 km** | steps once per kilometre: 12 steps over 11.9 km (`000009`), 3 over 3.0 km (`000011`); 167966 on the 13th, 167988 at the end of the drive back; valid with the engine off too | `odometer_km` (the layout format gained 24-bit fields) |
| Distance counter | `14D` bytes 6–7, little-endian, **about 0.40 m** per count | tracks distance from the wheel speeds with correlation 1.0000 in both drives (103.2 and 103.6 m per step of byte 7); wraps every ~26 km | not registered: the odometer covers whole kilometres; this is there if a trip meter needs finer steps |
### Which word is which wheel
In a corner the outer wheels run faster than the inner ones, by about half the track width times the
curvature; and the fronts run on a wider arc than the rears, by about half the wheelbase squared times
the curvature squared. Fitting each word's share of the four (its speed ÷ their mean) against the
curvature from the steering angle (`steering_angle_deg` + 2.7° for the straight-ahead offset, ÷ 16.3,
over a 2.5 m wheelbase), 74 476 frames above 15 km/h:
| Word | Linear term (track) | Square term (wheelbase) | So |
|---|---|---|---|
| 0 | +0.72 m | +1.4 m² | right, front |
| 1 | −0.72 m | +1.4 m² | left, front |
| 2 | +0.75 m | −0.9 m² | right, rear |
| 3 | −0.75 m | −1.9 m² | left, rear |
Curvature is positive turning left, so a positive linear term is a wheel on the right (outside in a left
turn). The size, ±0.72–0.75 m, is half the Colt's track (1.45–1.46 m). The square terms split the words
into a pair above the mean and a pair below it, ±1.5 m² against an expected ±1.6 m² (2.5² ÷ 4). Medians
by curvature agree: in left turns tighter than 20 m (`κ > 0.05`) the words read 1.068, 0.956, 1.044,
0.932 of their mean.
Straight ahead the fronts read 0.6 % more counts than the rears, matching pass 2's per-word scales
(0.0578 against 0.0581 km/h per count). Driven wheels or tyre sizes, either is ordinary; each pair is
registered with its own scale, so all four read GNSS speed when cruising.
Jack stands (checklist 23–26) would still confirm this, but are no longer needed to find it.
## Candidates
- **`570` bytes 2 and 3** move together (byte 3 ≈ byte 2 + 48) and fall slowly: byte 2 read 87 on the
13th and 76 at the end of the drive back, 22 km later. Fuel level fits the fall, but byte 2 also rose
by about 3 while parked between drives (78 → 81), which fuel would not do on level ground. It may
be outside temperature instead, falling through the evening. Both read `7F` with the engine off. A
refuel settles it: fuel jumps, temperature does not. Not registered.
## Notes on the recording
- `000012` is a 25-second parked session in between; `000013` ends with the PEAK unplugged.
- The recorder kept IMU samples as JSON lines (34 MB for `000009`). From this pass on it packs them
(`imu.bin`, svc-core `imu/packed.rs`): the same samples are 8.5 MB, and all of `000009`'s samples fit
the packed form exactly.
+96 -1
View File
@@ -1,5 +1,5 @@
{ {
"$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), signed (lengths 8 and 16: two's complement; default unsigned), scale and offset (value = raw * scale + offset; booleans are raw != 0). 'evidence' names the contracts report that found it.", "$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, 16 or 24), bit (length 1 only: 0 = LSB of that byte), order (lengths 16 and 24: big = byte first is most significant, little = the reverse), signed (lengths 8 and up: two's complement; default unsigned), scale and offset (value = raw * scale + offset; booleans are raw != 0). 'evidence' names the contracts report that found it.",
"signals": { "signals": {
"obd_vehicle_speed_kph": { "obd_vehicle_speed_kph": {
"unit": "kph", "unit": "kph",
@@ -350,6 +350,101 @@
"evidence": "Z27AG-DECODE-2" "evidence": "Z27AG-DECODE-2"
} }
}, },
"wheel_speed_fr_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Front right wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 0,
"length": 16,
"order": "little",
"scale": 0.0578,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_fl_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Front left wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 2,
"length": 16,
"order": "little",
"scale": 0.0578,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_rr_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Rear right wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 4,
"length": 16,
"order": "little",
"scale": 0.0581,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_rl_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Rear left wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 6,
"length": 16,
"order": "little",
"scale": 0.0581,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"odometer_km": {
"unit": "km",
"type": "number",
"source": "z27ag",
"description": "Total distance (CAN), whole kilometres: steps once per kilometre driven (12 steps over 11.9 km). Not yet compared with the cluster's display",
"range": [
0,
16777215
],
"can": {
"id": "0x570",
"byte": 4,
"length": 24,
"order": "little",
"scale": 1,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"coolant_temp_c": { "coolant_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",