---
name: reading-gcu-logs
description: Interpret and diagnose logs from the Rush SR GCU (Gear Control Unit) — the firmware that runs the pneumatic paddle shifter. Use this whenever someone shares a GCU log file (gcu_log_*.txt, session.N.log, DUMP_ROLLING / DUMP_EXCEPTIONS output), pastes heartbeat (HB) or AIM lines, asks why a shift failed, why the car "ended up in neutral" or "ended up in 1st", why the GCU rebooted, or asks any question about on-car shifter behavior, missed shifts, false neutrals, compressor cycling, paddle problems, or the car shifting "on its own" (auto-downshift). Self-contained — requires no access to the GCU source code.
---

# Reading GCU Logs

The GCU (Gear Control Unit) is the Rush SR's paddle-shift controller: an
Adafruit Feather M4 CAN board that fires pneumatic valves to rotate the
gearbox shift drum, cuts/blips the throttle through the Suzuki ECU, manages
tank air pressure, and reads gear position from an analog drum sensor. It
logs everything it does to USB serial and to onboard flash. This skill is a
complete guide to reading those logs.

A "GCU log" is usually a text capture from the flasher GUI or a serial
terminal. One file typically mixes **four kinds of lines**:

1. **Live serial telemetry** — 1 Hz `HB` (heartbeat) and `AIM` lines, plus
   per-shift `SHIFT #`/`VALVE #`/`COUNT #` trace lines, all prefixed with
   uptime `[HH:MM:SS.mmm]`.
2. **Flash log records** — dumped via `DUMP_ROLLING`/`DUMP_EXCEPTIONS`,
   prefixed with raw milliseconds `[1297638]`. These survive power cycles;
   one numbered file per boot, so a dump can contain **several sessions
   back-to-back**. The dump is framed by `=== ROLLING LOG START ===` /
   `=== ROLLING LOG END ===` and each boot's file opens with a
   `--- SESSION N ---` marker (oldest first). The `[ms]` counter also resets
   to a small number at each boot — a second cue if the markers are missing
   from an older capture. A session that hits its size cap rolls into
   session N+1 **without a reboot**, so a new marker alone doesn't prove a
   power cycle — confirm with a `BOOT` record or an `[ms]` reset.
3. **Command protocol responses** — `OK ...`, `ERR ...`, `MODE RACING`,
   `VERSION ...`, `CONST ...`, framed by `BEGIN`/`END`.
4. **Boot banner / warnings** — `RESET_CAUSE`, `FLASH_SAFE_MODE`, `WARN ...`.

## How to analyze a log (workflow)

1. **Read the header.** `VERSION` gives firmware version + git SHA. The
   `BEGIN GET_CONSTS ... END` block gives every runtime tunable in effect —
   record `neutral_button_mode`, `downshift_block_rpm`,
   `post_shift_validate_ms`, `misshift_retry_enabled`, and `auto_downshift`.
   `aim_throttle_closed`/`aim_throttle_full` only calibrate the reported
   throttle percent; `auto_downshift_coast_pct` exists only on firmware older
   than 2026-07-25.
   They change how you interpret everything below. Public firmware
   (`DUMP_STATUS` shows `TIER tier=public`) lists only the 18 public
   tunables — a shorter GET_CONSTS block is normal there, not truncation.
2. **Establish the timeline.** Note where `[ms]` counters reset (new boots)
   and which records carry ISO wall-clock stamps (see Timestamps below).
3. **Scan for failure-class events** — grep for `CL_TIMEOUT`, `MISSHIFT`,
   `GEAR_MISMATCH`, `BLOCK`, `FAIL`, `PRESSURE_CRIT`, `RESET_CAUSE`,
   `NEUTRAL_CL_TIMEOUT`, `RPM_STALE`. These are the story; OK events are
   the background. Also count same-gear shifts (`g:X->X`) as a share of all
   shifts — that rate has been the headline miss metric since 2026-06 and is
   easy to miss because those records are named `SHIFT_OK_*`.
   **Do not read a pattern out of matching totals.** Two counts being equal
   (e.g. 16 `CL_TIMEOUT` and 16 upshifts) is not evidence they correspond;
   pair each CL record to the `SHIFT_OK_*` that follows it before claiming a
   split by direction. That specific coincidence appeared in session 165 and
   was pure chance.
4. **Reconstruct each incident** from the surrounding ±10 lines: what shift
   was requested, what gear it started from, what it landed in, what the
   driver did next.
5. **Classify** against the failure signatures in
   `references/event-catalog.md` (read it whenever a log shows a 2→1 problem,
   a false neutral, retries, or a crash/reboot).
6. **Report**: per-incident narrative with timestamps, then root cause,
   then a recommendation (tunable change, hardware check, or "working as
   designed").

## Timestamps — four clocks in one file

| Form             | Example                   | Meaning                                                        |
| ---------------- | ------------------------- | -------------------------------------------------------------- |
| `[HH:MM:SS.mmm]` | `[00:01:49.001]`          | Uptime since boot (live serial lines)                          |
| `[<ms>]`         | `[1297638]`               | Raw `millis()` since boot (flash records)                      |
| ISO local        | `2026-06-12T15:22:09.008` | Wall clock, **car-local** (AiM GPS time + `tz_offset` tunable) |
| ISO UTC          | `2026-06-12T18:54:37Z`    | Wall clock, UTC — `tz_offset` was unset when stamped           |

The wall clock comes from the AiM dash's GPS broadcast, so flash records
written **before the dash got a GPS fix have no ISO stamp** — just `[ms]`.
Failure-class records additionally carry `(MM:SS.mmm sess)` — AiM session
time — and `lap:N` when the dash was alive. Records within one boot are
ordered by `[ms]`; the dump itself may show a slightly out-of-order pair
because success records are queued before their trace record.

## The heartbeat (HB) line — 1 Hz health snapshot

```text
[00:01:49.001] HB phase=0 shifting=0 g=2 cg=2 src=GP rpm= 8190 psi=  81 nt_mode=MOTO drop=00000 pdrop=00000 ghosts=00000 doubles=00000 rpmv=1 gearv=1 proto=1 cmddrop=00000 bo=0 crst=0 txdrop=00000 tec=0 rec=0 emi=0 adc=389
```

| Field            | Meaning                                                                                                                                    | Healthy                 | Worry when                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | ----------------------------------------------------------------------- |
| `phase=`         | Shift state machine phase (0=idle, 1=valve open; full table in reference)                                                                  | 0 between shifts        | stuck non-zero                                                          |
| `shifting=`      | 1 while a shift is in progress                                                                                                             | 0                       | stuck 1                                                                 |
| `g=`             | Arbitrated current gear (0=N, 1–6, 255=unknown)                                                                                            | matches `cg=`           | 255, or ≠ `cg=`                                                         |
| `cg=`            | Gear as reported by the ECU over CAN                                                                                                       | matches `g=`            | disagreement → GP calibration or CAN issue                              |
| `src=`           | Active gear source: `GP` (drum sensor, closed-loop capable), `CAN` (ECU data, ~100 ms lag), `FALL` (no data — open-loop, safety gates OFF) | GP                      | `FALL`                                                                  |
| `rpm=`           | Engine RPM via CAN (0 when invalid)                                                                                                        | live value              | 0 with engine running                                                   |
| `psi=`           | Tank pressure                                                                                                                              | 70–90 (compressor band) | <65 during shifting                                                     |
| `nt_mode=`       | `MOTO` (down from 1 blocked, N via button) or `BTN ` (car mode: down from 1 → N)                                                           | per car config          | unexpected flips                                                        |
| `drop=`          | Serial lines dropped (TX buffer full)                                                                                                      | 0                       | growing                                                                 |
| `pdrop=`         | Paddle events dropped (ring buffer full)                                                                                                   | 0                       | any                                                                     |
| `ghosts=`        | Phantom paddle events suppressed                                                                                                           | 0                       | growing = EMI on paddle wiring                                          |
| `doubles=`       | Double-shift suppressions                                                                                                                  | 0                       | growing                                                                 |
| `rpmv=`/`gearv=` | CAN RPM / gear data currently valid                                                                                                        | 1/1                     | 0 with engine on                                                        |
| `cmddrop=`       | Serial commands dropped                                                                                                                    | 0                       | growing                                                                 |
| `bo=`            | CAN bus-off count                                                                                                                          | 0                       | any — EMI corrupted the bus                                             |
| `crst=`          | CAN controller resets                                                                                                                      | 0                       | growing                                                                 |
| `txdrop=`        | CAN TX frames dropped                                                                                                                      | 0 on healthy bus        | steady growth, bus healthy                                              |
| `tec=`/`rec=`    | CAN transmit/receive error counters                                                                                                        | 0                       | `tec` pinned at 128 + `bo=0` = ECU off / harness open (no-ACK), not EMI |
| `emi=`           | Paddle ISR rate-limiter rejections                                                                                                         | 0                       | growing = solenoid EMI hitting paddle pins                              |
| `adc=`           | Raw gear-position sensor ADC (0–1023)                                                                                                      | inside a gear band      | <50 or >980 = sensor fault                                              |

**GP sensor ADC bands** (what `adc=` should read per gear, settled):
1st 93–197 · N 219–319 · 2nd 334–434 · 3rd 475–555 · 4th 576–636 ·
5th 637–673 · 6th 674–727. Between-band readings = "unconfirmed" (gear 255).
HB snapshots catch mid-shift transients — only trust `adc=` for calibration
when the gear was held 5+ seconds.

## The AIM line — 1 Hz dash broadcast

```text
[00:01:48.501] AIM time=2026-06-12T16:30:31.796 spd=0 bf=-2 br=0 aimf=1 thr=78 latg=-12 long=310
```

`time=` wall clock as the firmware renders it (car-local; trailing `Z` = UTC
because `tz_offset` unset; `none` = no GPS fix yet). `spd=` raw GPS speed,
`bf=`/`br=` front/rear brake pressure in **signed psi**, broadcast directly by
the dash (no conversion). A released pedal reads slightly negative (`-2` at rest);
hard braking climbs into the tens/hundreds of psi. `-32768` = channel absent/stale
(`BRAKE_SENTINEL`). See `docs/CAN_BUS_SPEC.md`. Both fields on this line are dash channels.
On a car whose front sensor has been moved onto the GCU (`brake_front_source 1`), nothing is
wired to the dash's front input any more, so `bf=` here is meaningless: absent if RS3 stopped
broadcasting the channel, otherwise whatever an unconnected input floats to. The shift trace's
`bf:[...]` is the one to read on such a car. That is the wiring, not a fault — and it is the
one place these two identically-named fields disagree.
`aimf=` dash-data freshness (1 = fresh). `thr=` raw AiM throttle position —
**can exceed 100** (raw RS3 scaling, per-car calibration); `65535` = no
fresh dash data at that moment. `latg=`/`long=` raw signed lateral /
longitudinal G from the 0x712 AiM Dynamics broadcast (signed int16; + =
right for lat, + = acceleration for lon); `0` when the 0x712 frame has not
been seen or has gone stale. On this car (`tps:`/`tps_pre:` in flash records
reads 0 because ECU Type 4 broadcasts no TPS channel) — **use `thr=` from
the AIM line when you need throttle position**. No AIM lines at all = dash
off or CAN between dash and GCU is down.

## Flash log records

```text
[1297638] 2026-06-12T15:22:09.008 SHIFT_OK_DOWN rpm:8958 gear:1 psi:76 g:2->1 tps_pre:0 rpm_pre:7276 rpm:[...
[1968363] 2026-06-12T15:33:14.732 (23:55.889 sess) MISSHIFT_RETRY rpm:8308 gear:0 psi:75 lap:5 Misshift retry
```

Format: `[ms] [ISO] [(sess)] NAME rpm:R gear:G psi:P [lap:N] [message]`.
The `rpm:`/`gear:`/`psi:` triplet is sampled **at the moment the record was
written** — for a shift-OK record that is _after_ the shift, so `gear:` is
the landed gear. `gear:0` = neutral, `gear:255` = unknown/sensor
unconfirmed. The full event catalog — every event name, its meaning, and
its message fields — is in `references/event-catalog.md`.

**Shift trace format.** A `SHIFT_OK_DOWN` / `SHIFT_OK_UP` record shows
**one of two forms**:

1. **Full per-sample body** — for an _interesting_ shift (defined below).
   30 samples × 10 ms = 300 ms of post-shift telemetry. A downshift carries:

   ```text
   g:2->1 tps_pre:0 rpm_pre:8200 rpm:[8200,...] thr:[78,80,...] bf:[42,55,...] br:[18,20,...] adc:[140,142,...]
   ```

   An upshift carries the same five arrays, drum trace included. On firmware
   older than 2026-07-26 it instead ended in `gss:[..]` and carried **no**
   `adc:[..]`, so upshift traces from those logs have no drum position — and
   the `gss` values are all 0 regardless (see below).

2. **One-line summary** — for a _clean_ shift (or an interesting shift whose
   staged trace was evicted from the ring before flush):

   ```text
   g:2->1 rpm_pre:7276 dur:120ms
   ```

A shift is **interesting** (gets the full body) when it: landed off the
expected gear (computed from drum kinematics), is a 1↔2 shift (traverses the
N half-detent), fired a misshift retry, or landed in N/unknown. Everything
else gets the summary. Set the runtime tunable `trace_all_shifts 1` to force
the full body for **every** shift during a debug stint (default 0).

`tps_pre:` is a **single header value** (not an array), still present, and
reads 0 on this car (ECU Type 4 broadcasts no TPS) — use `thr:[...]` for
throttle context instead.
The `bf:[...]`/`br:[...]` arrays are front/rear brake pressure in **signed psi**
per 10 ms sample, sampled both directions (50 Hz source resolves real brake transitions
through the shift). No conversion — a released pedal reads near 0 (slightly negative at
rest). `br:[...]` is always the AiM rear channel, so `-32768` there means the dash was
stale or absent at that sample. `bf:[...]` records whichever front source the firmware has
selected. At `brake_front_source 1` (the default) that is the GCU's own analog sensor on
A4: the dash can be stale while `bf:[...]` is perfectly valid, and `-32768` means the
selected front source was unavailable or excluded. At `brake_front_source 0` the front
array is the AiM channel and `-32768` means dash stale/absent, as before. Both sources read
the same brake input, so the cross-check works either way. **Braking through a downshift is
a primary false-neutral trigger** — cross-check rising `bf`/`br` against an `adc[]` drum
trace that settles in the N band (see `docs/DEBUGGING_DOWNSHIFT_FAILURES.md` Mode E).
The `adc:[...]` array is the raw GP sensor ADC (0..1023) per 10 ms sample — the drum
position over time, mapped to gears via the ADC table. Present on **both directions**
since 2026-07-26. This is the array that separates a drum that never moved (flat to ±2
counts) from one that moved while the sensor missed it.
The `gss:[...]` array appears only on **upshifts in logs older than 2026-07-26**. It was meant
to show the GSS/quickshifter cut/blip sequence (0 = nominal, 1 = cut, 2 = blip), but
trace sampling starts after the valve closes and the throttle is back to nominal, so it
is **all zeros in every such log**. Do not read anything into it, and do not report the
absence of a cut from it.
The `thr:[...]` array is AiM raw throttle at each 10 ms sample; treat it as
a staircase (the AiM channel is 10 Hz so consecutive samples repeat). A
value of `65535` in the array means the AiM dash was stale or absent at that
sample.

**Older logs** may still carry the dropped `tps:[...]`, `psi:[...]`, and
`gear:[...]` arrays (removed because they were uninformative — `tps` always 0
on this ECU, `psi` flat within a shift, `gear` redundant once landed), and an
evicted-trace `SHIFT_OK` may show the pre-4.1.1 truncated stub (cut at 47
characters) instead of any body. Don't report the dropped or stubbed arrays as
"empty" — note the format/eviction.

**Failure-class records** (anything `logEventIsFailure()` matches: blocked
shifts, CL timeouts, gear mismatches, retries, CAN faults) additionally
carry `thr:N latg:N long:N` — throttle and signed G frozen **at event
time**, from 0x712. These appear after the standard `rpm:`/`gear:`/`psi:`
triplet. `thr:65535` = dash was absent; `latg:`/`long:` are 0 when the
0x712 frame had not arrived.

**Bench gotcha:** the racing loop only flushes the RAM log queue to flash
when it sees fresh low RPM. On a bench (engine off, CAN dead) send
`FLUSH_LOGS` before `DUMP_ROLLING` or the latest records won't appear — and
the full trace won't be there.

## What a normal shift looks like

A healthy closed-loop shift produces a **pair** of flash records,
success-confirmation first:

```text
[1001539] SHIFT_CL_OK   rpm:6347 gear:2 psi:75            ← drum reached target
[1001895] SHIFT_OK_DOWN rpm:7207 gear:2 psi:71 g:3->2 ... ← completion + trace
```

`SHIFT_CL_TIMEOUT ... Closed-loop shift timeout` followed by
`SHIFT_OK_UP/DOWN` with `g:X->X` (same gear!) means the closed-loop
validator never saw the target. **It does NOT mean the shift failed** — the
gearbox often shifts while detection times out. Session 165 (2026-07-26) had
three shifts log `CL_TIMEOUT` while the gear demonstrably did change (3→4,
3→4, 3→2). Scattered single `CL_TIMEOUT`s at high RPM are routine noise;
**clusters** of them, or ones followed by `GEAR_MISMATCH`/`gear:0`, are real.

### Diagnosing a `g:X->X`: read the drum trace, don't infer

Since 2026-07-26 **both** directions carry `adc:[...]`, so a same-gear shift
resolves into one of three cases without guesswork. Map first and last samples
onto the ADC band table:

| `adc[]` behavior                                     | What happened                              | Where to look                              |
| ---------------------------------------------------- | ------------------------------------------ | ------------------------------------------ |
| flat to ±2 counts for the full 300 ms                | drum never moved                           | air pressure, valve, solenoid, dog binding |
| moves tens of counts, stays inside the starting band | **drum moved partway and didn't complete** | pressure, pulse duration, drum detent      |
| reaches the target gear's band                       | drum DID shift, detection missed it        | GP calibration / band edges                |

The middle row is the case that was invisible before — old firmware gave
upshifts a `gss` array instead of `adc`, so partial motion and no motion looked
identical. Observed on 2026-07-26: two failed upshifts moved the drum 25 and 43
counts and never left the N band (219-319).

A cross-check when the drum trace is ambiguous: a completed shift steps RPM by
the gear ratio. For 4→5 predict `rpm × (1.360 / 1.500)` = 0.907. This is only
usable at real road RPM — at idle or engine-off it tells you nothing.

**Engine-off caveat.** A stationary paddock test has no relative rotation, so
the dogs cannot engage and most shifts will fail regardless. Do not compare a
`rpm:0` capture's miss rate to a track session's, and do not read a car fault
into it.

`GEAR_MISMATCH ... GP/CAN disagree` right after a shift is common and
benign in isolation (CAN gear lags the drum by ~100 ms). It matters when it
shows the drum somewhere it shouldn't be (`gear:0` after a 2→1 attempt).

## Shift drum layout — why "neutral" appears mid-sequence

The drum order is **1 – N – 2 – 3 – 4 – 5 – 6**: neutral is a _half-detent
between 1st and 2nd_, not below 1st. Every 2→1 or 1→2 shift must traverse
it, which is why ~1% of hard 2→1 downshifts can park in N ("false
neutral"). A shift that lands `gear:0` unexpectedly, retries
(`MISSHIFT_RETRY`), and a subsequent violent N→1 engagement at road speed is
the classic false-neutral incident — full signatures and the decision tree
are in `references/event-catalog.md` § Failure modes.

In `nt_mode=MOTO` a down-pull from 1st is blocked and N is reached only via
the NT button; in `BTN` (car) mode a down-pull from 1st hunts for N. Both
modes: down from 2 goes straight to 1 (never stops at N deliberately).

## Money-shift protection math

Downshifts are blocked if predicted post-shift RPM reaches or exceeds
`downshift_block_rpm` (default 11800).
Predicted RPM = current RPM × (ratio of target gear ÷ ratio of current
gear), with ratios 1st 2.562 · 2nd 2.052 · 3rd 1.714 · 4th 1.500 ·
5th 1.360 · 6th 1.269. Two blind spots to remember while reading logs:
the gate uses the last CAN RPM sample (`RPM_STALE_DOWNSHIFT` warns when
that sample was old); and a shift **from neutral** has no ratio to compute
from, so an N→1 engagement at road speed goes ungated. If a log shows an
N→1 shift where post-shift `rpm:` is several thousand RPM above
`rpm_pre:`, always call it out — it's the engine-damaging part of a
false-neutral incident.

## Auto-downshift ("Shiftomat") — shifts the driver didn't pull

Firmware 4.2+ has an opt-in **hold**-to-downshift feature (`auto_downshift`,
default 0). When enabled, one pull of the down paddle is one ordinary
downshift; **keep holding** (~200 ms past the hold's start, or past the last
completed downshift, whichever is later) and the box walks itself down toward
2nd. Letting go freezes it at the current gear. Multi-tap preselect existed
briefly and was removed in 2026-07 — logs from before that may show tap-armed
walks. Auto-fired shifts look like downshifts **without a matching paddle
pull**. They are tagged in both surfaces: live trace lines show
`SHIFT #... src=auto` (vs `src=user`), and since 2026-07 the flash record body
carries ` src:auto` too:

```text
[1600] ... SHIFT_OK_DOWN rpm:7800 gear:3 psi:79 g:4->3 rpm_pre:8000 dur:145ms src:auto
```

A plain driver pull has no `src:` tag at all (that keeps ordinary logs
unchanged), so absence means manual. **Logs from firmware older than 2026-07
have no tag on any flash record** — in those, an auto walk cannot be told apart
from repeated manual pulls, and you need live serial instead.

Every auto fire must pass ALL of these gates — this is the checklist for
"I held the paddle and nothing happened":

- **Money-shift gate** — same predicted-RPM math as a manual downshift;
  the fire simply waits until revs fall enough. This is the normal reason
  a walk fires seconds later, near redline each time.
- **RPM-rising veto** — RPM climbing ≥ 250 in 150 ms reads as "under power"
  and vetoes the fire _without_ disarming. Since 2026-07-25 this is the ONLY
  under-power test: there is no throttle gate, and auto-downshift will fire
  with the pedal down. On firmware older than that a coast gate also applied,
  and applying throttle disarmed the walk.
- **Fresh CAN RPM required** — in `src=FALL`/GP-only (`rpmv=0`) it never
  fires.
- **Spacing** — full shift-machine idle (the 300 ms trace window counts as
  busy) plus a 150 ms RPM-settle band between fires.
- **Hold bound** — a walk is bounded at 4 s of continuous hold (stuck-pin
  protection); a pin stuck LOW latches a lockout until a clean release.

### Throttle numbers, and what they no longer mean

The AiM `thr=` channel is **raw and per-car RS3-scaled with a pedal zero
offset** — it is not a percentage. On the fleet car, fully released reads
470-930 and WOT reads ~10100. `aim_throttle_closed` and `aim_throttle_full`
map it to a true 0-100% for the `thr_pct` field. Since 2026-07-25 that is all
they do: throttle does not gate a shift, so a wrong calibration makes the
number misleading but cannot stop the feature firing.

On **older firmware it was load-bearing**. That build compared the raw value
against a percent threshold of 2, so the coast gate could never pass, a hold
did nothing forever, and nothing was logged. An old log showing the feature
enabled with no `src=auto` shift ever fits that signature — but confirm it
against the veto tallies before concluding, since a hold that never escalated,
stale CAN RPM, or a persistent over-rev produce the same silence.

### Use the analyzer on anything long

For a full track session, don't read it by hand:

```bash
./tools/dev.sh analyze-log <logfile>            # full report
./tools/dev.sh analyze-log <logfile> --walks    # auto-downshift walks only
```

It reconstructs every walk (gear sequence, per-step spacing, whether it reached
the 2nd-gear floor) and flags one that stopped early, printing the surrounding
records with `--context N`. On current firmware an early stop points at
`over_rev`, `rpm_rising`, `settling`, or the RPM-freshness vetoes `no_rpm` /
`rpm_stale`; on pre-2026-07-25 logs it usually meant
the blip pushed throttle above the coast threshold and disarmed the walk. Works
on both live-serial and flash formats. Exit status is non-zero when something is
flagged.

### The AiM dash log is the other half of a shift diagnosis

The GCU log says what the controller decided; the dash's `.xrk` says what the car
did, at a higher rate and with GPS. When a shift complaint survives the GCU log —
the log looks clean and the driver still says it missed — the `.xrk` from the same
session is where the answer is. Ask for it by name; owners often have one and don't
think to send it.

Decode it with the existing analyzer rather than writing a parser:

```bash
cd ~/git/rush/promiscuous-slurp/xrk-analyzer
./scripts/export-parquet.sh /path/to/session.xrk \
  --channels Speed,RPM,Gear,Lateral_G,Longitudinal_G
```

That wraps Docker (`--platform linux/amd64`; the underlying xdrk library is Linux
x86_64 only). Then use the skills in that repo: **`parquet-introspection`** for
schema, columns, and samples, **`impound-run`** for fleet comparison across a race
event, **`ac-replay-export`** to replay a session in Assetto Corsa. They are
directory-scoped to `xrk-analyzer/`, so they do not auto-load in a GCU session — you
have to `cd` there.

Two properties of the dash data will mislead you otherwise. **`Gear` reads 6 while
the shift drum is between detents**, which is not 6th gear; confirm the real gear by
clustering `RPM / Speed`, since the ratio is flat and distinct per gear and a
whole-gear offset shows up immediately as inconsistency across gears. And **check
each channel's native sample rate before reading fine timing off it** — the exports
resample onto a uniform grid, so a channel logged at 10 Hz will happily hand you
50 Hz of forward-filled values and a 100 ms event looks like five samples when it
was one.

For a legacy (Mega 2560) car, use the `reading-gcu-legacy-logs` skill in the
GCU-legacy repo instead — and note that the legacy board puts nothing on the CAN
bus, so the `.xrk` is the only record of what the gearbox actually did.

### Diagnose from DUMP_STATUS, not by inference

`DUMP_STATUS` reports the state and a per-reason tally, so you no longer have
to infer a veto from surrounding HB/AIM lines:

```text
AUTO_DS en=1 target=2 hold=1 lockout=0 thr_pct=0 last=fire
AUTO_DS_VETO fire=3 over_rev=12 rpm_rising=1 settling=44 no_rpm=0 rpm_stale=0 at_target=1 at_floor=0
```

`thr_pct=` is the calibrated pedal percent, **reporting only** — throttle stopped
gating anything on 2026-07-25. `255` means no fresh throttle source. `last=` is the
reason the most recent armed tick declined. Counters are cumulative since boot.
Read them like this:

- `over_rev` climbing → working as designed, revs are still too high.
- `rpm_rising` climbing → the engine is under power. This is the only remaining
  power test, and it suppresses without cancelling the walk.
- `settling` climbing → normal, that's the inter-shift spacing.
- `no_rpm` climbing → CAN RPM isn't fresh; check `rpmv=`/`src=`.
- all zero with `en=1` → nothing ever armed; the hold never escalated, so
  check that the paddle is actually being held (pin LOW) and `lockout=0`.

**Firmware older than 2026-07-25** also prints `on_throttle=` and `no_throttle=`.
Those builds had a coast gate: a climbing `on_throttle` with the foot off meant
the throttle calibration was wrong, and that was the usual reason a walk died
mid-sequence. Neither counter exists on current firmware.

An upshift disarms the walk and releasing the paddle freezes it at the current
gear; both are by design. Throttle does **not** disarm — auto-downshift will fire
with the pedal down, held back only by the over-rev gate.

## Getting logs off the car

Read commands (always accepted over USB serial, newline-terminated):
`DUMP_STATUS` (reset cause, uptime, CAN/flash health, AiM clock, RAM queue),
`DUMP_ROLLING` (full session logs), `DUMP_EXCEPTIONS` (failure-only log),
`LOG_STATS`, `GET_CONSTS`, `GET_VERSION`. Write commands need
`UNLOCK_ACCESS CONFIRM` first. See the flash-records section above for the
`FLUSH_LOGS` bench requirement before `DUMP_ROLLING`.

### A live-serial capture has no trace bodies

The `adc:[...]`/`thr:[...]` arrays exist **only in flash records**. They are
composed at flush time, not emitted live. A live capture shows the shift as
`SHIFT #NNNNN start/fail/complete` and `VALVE #NNNNN` lines with no arrays at
all. If you are asked to check a trace and the file only has `SHIFT #` lines,
ask for `FLUSH_LOGS` then `DUMP_ROLLING` — the data is not in the file you have.

### Counters: read `SHIFT_COUNTS`, not the dash

`DUMP_STATUS` carries the six attempted-shift counters:

```text
SHIFT_COUNTS up_ok=31 up_fail=16 dn_ok=35 dn_fail=13 nt_ok=3 nt_fail=0
```

`fail` = the valve fired and closed-loop never **confirmed** the target within
the timeout (or a misshift retry chain ran out). That is a detection verdict,
not a physical one: the increment sits on the `else` of `gearChangeDetected`,
which only goes true after the GP sensor holds the target band for
`shift_target_confirm` consecutive samples. A drum that shifted while the
sensor missed it lands here too — the same `g:X->X` case the drum-trace table
above resolves. Read `adc:[...]` before calling a `fail` a mechanical failure.
Safety-blocked shifts never fire a valve and appear in `SHIFT_BLOCKS` instead.

The rate is `(up_fail + dn_fail) / (up_ok + up_fail + dn_ok + dn_fail)` —
neutral attempts are excluded, progressive-neutral being a different mechanism.
~12% was the 2026-06 baseline and ~31-39% the 2026-07 track figure. Counters
are **cumulative since boot**, so read them before a power cycle or they are
lost.

These same counters ride on CAN 0x702/0x703, but a dash reading of them has a
channel config in the path and has been wrong before. When firmware and dash
disagree, `SHIFT_COUNTS` is the authority.

### Check the build tier before recommending a tunable

`DUMP_STATUS` prints `TIER tier=dev` or `TIER tier=public`. The 18 dev-only
tunables — `trace_all_shifts` among them — are compiled out of a public build,
so telling someone to `SET trace_all_shifts 1` on a `tier=public` board sends
them out with a command that will be rejected. Check the tier first.

## Crash / reboot triage

`RESET_CAUSE` (in boot banner and `DUMP_STATUS`): `0x01 power-on` = supply
collapsed — on this car classically the shared-ground voltage sag when a
solenoid fires (hardware, not software); `0x20 WATCHDOG` = main loop hung

> 500 ms; `0x40 system` = clean commanded reboot; `0x10 external` = reset
> pin (button/programmer); `0x04/0x08 brownout` =
> supply sagged below threshold. A log that just _stops_ mid-shift then shows
> a boot banner: check the last HB's `bo=`/`tec=` (CAN death) and whether a
> downshift countershift had just fired (biggest current spike). Full
> checklist in the reference.

## Report format

Lead with a one-paragraph verdict (what happened, root cause class). Then
per-incident detail with wall-clock + lap when available. Quantify (RPM,
PSI, durations). End with concrete recommendations tied to evidence —
tunable `SET` commands, hardware checks, or "working as designed". Never
invent values for fields the log doesn't contain (truncated traces, absent
TPS).
