From e8ba5819859eb377731f4375dc9dc8791ea68f19 Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Thu, 15 Oct 2026 22:13:34 +0900 Subject: [PATCH] Z27AG decode pass 3: wheel positions from corners, odometer (570, 24-bit); layouts can be 24 bits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz --- docs/impl/plans/Z27AG-CAPTURES.md | 4 +- docs/impl/reports/Z27AG-DECODE-3.md | 59 ++++++++++++++++++ schema/signals.json | 97 ++++++++++++++++++++++++++++- 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 docs/impl/reports/Z27AG-DECODE-3.md diff --git a/docs/impl/plans/Z27AG-CAPTURES.md b/docs/impl/plans/Z27AG-CAPTURES.md index 3d16bb5..34d782d 100644 --- a/docs/impl/plans/Z27AG-CAPTURES.md +++ b/docs/impl/plans/Z27AG-CAPTURES.md @@ -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 | | `events.ndjson` | marks from the app | when each step happened | | `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 @@ -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: 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 + 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. 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 diff --git a/docs/impl/reports/Z27AG-DECODE-3.md b/docs/impl/reports/Z27AG-DECODE-3.md new file mode 100644 index 0000000..7b415a1 --- /dev/null +++ b/docs/impl/reports/Z27AG-DECODE-3.md @@ -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. diff --git a/schema/signals.json b/schema/signals.json index 7147adb..c00aa2f 100644 --- a/schema/signals.json +++ b/schema/signals.json @@ -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/. source=imu is the WT61 IMU, already turned into the car's frame (mounting and gravity handled by its publisher), on yukart/imu/. 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/. source=imu is the WT61 IMU, already turned into the car's frame (mounting and gravity handled by its publisher), on yukart/imu/. 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": { "obd_vehicle_speed_kph": { "unit": "kph", @@ -350,6 +350,101 @@ "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": { "unit": "celsius", "type": "number",