feat: developer tools page, auto-test orchestrator, BIP display, tests redesign

Bundles several feature streams that have been iterating on the working tree:

- Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the
  identification card, manual KWP write + transaction log, ROM/EEPROM dump
  card with progress banner and completion message, persisted custom-commands
  library, persisted EEPROM passwords library. New service primitives:
  IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync.
  Persistence mirrors the Clients XML pattern in two new files
  (custom_commands.xml, eeprom_passwords.xml).
- Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear
  K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and
  progress dialog VM, gated on dashboard alarms.
- BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at
  0x0106 via IKwpService.ReadBipStatusAsync; status definitions in
  BipStatusDefinition.
- Tests page redesign: TestSectionCard + PhaseTileView replacing the old
  TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/
  TestSectionView controls and their VMs.
- Pump command sliders: Fluent thick-track style with overhang thumb,
  click-anywhere-and-drag, mouse-wheel adjustment.
- Window startup: app.manifest declares PerMonitorV2 DPI awareness,
  MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and
  maximizes there (after the hook is in place) so the app fits the work
  area exactly on any display configuration.
- Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias
  importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and
  dump-functions reference docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:59:50 +02:00
parent da0581967b
commit 827b811b39
102 changed files with 7522 additions and 1798 deletions

207
docs/kline_eeprom_spec.md Normal file
View File

@@ -0,0 +1,207 @@
## 5. EEPROM memory map (8-bit address, 0x000xFF)
The on-board serial EEPROM has 256 bytes addressed `0x000xFF`. The ECU's internal mirror after boot covers a sub-range only (`FUN_7568` loads EEPROM 0x400x8B into RAM 0x4000x44B); the upper area is read on demand.
Observed structure (from cross-dump comparison + disassembly):
| EEPROM range | Purpose | Notes |
|---|---|---|
| 0x000x3F | Pump cal record A — operating parameters | Read at boot via `FUN_4A79` (record-based, 8-byte chunks). Content is largely zero in current dumps. Erasable via cmd 0x05. |
| 0x400x8B | Pump cal record B — primary | Block-loaded into RAM 0x4000x44B at boot by `FUN_7568`. Contains: temp offset (0x42), dphi seed (0x44) + checksum (0x45), accel-comp seed (0x48), angle-table seed (0x4C), CKP loop seeds (0x500x56), redundant copies (0x790x7A), serial number ASCII (0x800x88) |
| 0x8C0xBF | Pump cal record C — extension | Not loaded into RAM at boot. Readable only via K-line cmd 0x19 after Zone 3 unlock. **Per user observation 2026-05-07: present in physical EEPROM, never consumed by running ECU code.** Likely OEM/factory metadata. |
| 0xC00xCF | Reserved / unused | Outside any zone bounds in observed configs |
| 0xD0 | Lockout backoff counter | Written by `FUN_29D4` failure path, read at boot by `FUN_3AD1`. Persists failed-auth state across power cycles. |
| 0xD70xDF | Zone 0 seed (9 bytes) | Read into RAM 0xB7+ as part of cmd 0x18 success response |
| 0xE10xE9 | Zone 1 seed (9 bytes) | Same |
| 0xEA0xF2 | Zone 2 seed / Zone 8 magic seed (9 bytes) | Same. Shared between Zone 2 and Zone 8. |
| 0xF30xFB | Zone 3 seed (9 bytes) | Same |
| 0xFC | Zone 0 use counter | Incremented on each cmd 0x18 + RB0=0x10 success |
| 0xFD | Zone 1 use counter | Same |
| 0xFE | Zone 2 / Zone 8 use counter | **Magic-zone access leaves this trace** |
| 0xFF | Zone 3 use counter | Same |
The "cal record" boundaries (A/B/C) are inferred from access patterns; the EEPROM does not have explicit headers separating them.
---
## 6. Security architecture
### 6.1 Zone descriptor table
A 4-entry × 12-byte table at **ROM 0x5FA0** drives zone authentication. Each entry has:
| Offset | Field | Type | Purpose |
|---|---|---|---|
| +0 | `alt_key` | u16-LE | Alternative key (used by cmd 0x18 with RB0 ≠ 0x10) |
| +2 | `exp_key` | u16-LE | Primary expected key |
| +4 | `zone_start` | u16-LE | First EEPROM address the zone covers |
| +6 | `zone_end` | u16-LE | Last EEPROM address (inclusive) |
| +8 | `flag_byte` | u8 | RE6/RE7 bit pattern set on success |
| +9 | `seed_off` | u8 | EEPROM offset of the 9-byte seed read on success |
| +10 | `cnt_off` | u8 | EEPROM offset of the per-zone use counter |
| +11 | reserved | u8 | usually 0xFF (unused) |
A **fifth implicit "magic zone 8"** is hard-coded in `FUN_29D4`:
- Key: **0x4453** (ASCII "DS")
- Range: 0x00000xFFFE (effectively the whole 64KB MCU address space)
- On success: `RE7 = 0xFF` (every gate-bit set, including the cmd 0x1A write gate)
- Seed/counter: shares Zone 2's offsets (0xEA / 0xFE)
### 6.3 Authentication flow (`FUN_29D4`)
Tester side:
1. **Build cmd 0x18 block** with:
- `RB3` = 0x03 (request length / sub-count)
- `RB4` = zone selector (0, 1, 2, 3, or 8)
- `RB5` = key high byte
- `RB6` = key low byte
- `RB7` = any non-zero byte (acts as continuation flag — without it, the ECU's `RWC4 = 0` reset short-circuits the success path)
- `RB0` = 0x10 if you also want write access enabled (sets the upper-nibble flag bits AND increments the use counter)
- Otherwise `RB0` = block length (4 + data bytes)
2. **TX block, observe response.** ECU returns success block (`RB2 = 0xF0`, payload = the 9-byte seed + zone bounds) or error (`RB2` = 0xE5/error pattern).
3. **On success**, internal flags are set:
- Zone 0 → `RE6 |= 0x01` (read) or `|= 0x11` if RB0=0x10 (read+write)
- Zone 1 → `RE6 |= 0x02` or `|= 0x22`
- Zone 2 → `RE6 |= 0x04` or `|= 0x44`
- Zone 3 → `RE7 |= 0x08`
- Zone 8 → `RE7 = 0xFF` (and cmd 0x1A becomes available)
4. The flags persist for the rest of the K-line session; they reset on session end / power cycle.
ECU-side details an implementer should be aware of:
- The 9-byte seed read at `EEPROM[seed_off..seed_off+8]` is **echoed back to the tester in the response**. It does not gate access — pure transport, presumably so the tool can fingerprint the unit.
- `RE8` (lockout state) must be 0 to attempt auth. If non-zero (set by previous failed attempts), the ECU silently fails until the lockout timer (`FUN_38BE`) decrements RE8 to 0.
- Wildcard match: if `exp_key == 0xFFFF` in any zone descriptor, ANY tester key is accepted. **None of the observed dumps use this**, but a future ROM variant might.
- Sentinel: if `exp_key == 0x5555`, the zone is permanently disabled (cannot be unlocked). None observed yet either.
### 6.4 Lockout mechanism
Implemented in `FUN_29D4` failure path + `FUN_38BE` timer.
```
fail #1 -> RE9 = 1, RE8 = 1, stored to EEPROM 0xD0
fail #2 -> RE9 = 2, RE8 = 2
fail #3 -> RE9 = 4
...
fail #8 -> RE9 = 128
fail #9+ -> RE9 = 240 (saturated)
```
`RE8` decrements once per ~30000 ticks (active session) or ~120000 ticks (idle). Each decrement re-writes EEPROM 0xD0 — the lockout therefore **persists across power cycles**. A wedged unit can require minutes to hours of waiting to retry.
**Recommendation for the reader software:** read EEPROM 0xD0 before any auth attempt (via cmd 0x19 after Zone 3 unlock — the only safe path that doesn't risk further lockout).
## 7. Read recipes
### 7.1 Recipe A — public read of 0x000xBF (Zone 3)
This is the safest operation: it requires a static key (0x00FF) that is identical across every variant we have dumps for, has no destructive side-effects, does not increment the magic-zone counter, and covers the full "general data" portion of the EEPROM.
```
# Step 1 — unlock Zone 3
TX: [ 06 NN 18 03 03 00 FF FF 03 ]
len=06, seq=NN, cmd=0x18, RB3=03, RB4=03 (Zone 3),
RB5=00, RB6=FF (key 0x00FF), RB7=FF (continuation flag),
end=03
ECU response (success): RB2 = 0xF0, plus 9-byte seed @ 0xF3..0xFB.
ECU now has RE7 |= 0x08.
# Step 2 — read EEPROM in 13-byte chunks
for offset in 0x00, 0x0D, 0x1A, 0x27, 0x34, 0x41, 0x4E, 0x5B,
0x68, 0x75, 0x82, 0x8F, 0x9C, 0xA9, 0xB6:
TX: [ 04 NN 19 0D 00 offset 03 ]
cmd=0x19 (read EEPROM), RB3=0x0D (13 bytes), RB5=offset
RX: 13 bytes from EEPROM[offset..offset+12] in the response payload
# 15 chunks * 13 bytes = 195 bytes, covering 0x00..0xC2
# Trim or adjust the last chunk's RB3 to 0x0A so it stops at 0xBF inclusive.
```
### 7.2 Recipe B — full read of 0x000xFF (Zone 0)
Reads bytes that Zone 3 cannot see (0xC00xFF — counters, seeds, lockout). Requires the OEM key 0x00A6.
```
# Step 1 — unlock Zone 0
TX: [ 06 NN 18 03 00 00 A6 01 03 ]
RB4=00 (Zone 0), RB5=00, RB6=A6, RB7=01
# Step 2 — read EEPROM via cmd 0x03 (per-zone path)
for offset in 0x00, 0x0A, 0x14, ..., 0xF6:
TX: [ 06 NN 03 0A 00 offset 03 ]
cmd=0x03, RB3=0x0A (10 bytes), RB4:RB5 = 0x00:offset (16-bit address)
# 26 chunks * 10 bytes = 260 bytes, slightly over-reads;
# adjust the last chunk's RB3 to stop at 0xFF.
```
### 7.3 Recipe C — magic full access (Zone 8) — read AND write
```
# Step 1 — unlock Zone 8 with the master key
TX: [ 06 NN 18 03 08 44 53 01 03 ]
RB4=08 (magic zone), RB5=44, RB6=53 (key "DS" = 0x4453), RB7=01
# After: RE7 = 0xFF (every gate-bit set).
# All read commands work; cmd 0x1A is also available for writing.
# WARNING: this access path increments EEPROM[0xFE] (the use counter).
# The increment is non-destructive but forensically observable — service
# tools at the OEM can read 0xFE to see how many times Zone 8 was accessed.
```
### 7.4 Recipe D — fast-path RAM-mirror dump (no auth)
For EEPROM 0x400x8B specifically, you don't need to talk to the EEPROM driver at all — the data is mirrored into RAM 0x4000x44B at boot and accessible via cmd 0x01 (read RAM, no auth required).
```
for offset in 0x400, 0x40D, 0x41A, ..., 0x444:
TX: [ 04 NN 01 0D high_addr low_addr 03 ]
cmd=0x01 (read RAM/ROM), RB3=0x0D, RB4=high, RB5=low
# 6 chunks covering 0x400..0x44B = exact mirror of EEPROM 0x40..0x8B.
```
This recipe is ideal for monitoring tools that want to observe the live EEPROM cache without authentication overhead.
---
## 8. Write recipes — DANGER ZONE
Writes require either Zone 0/1/2 unlock with `RB0 = 0x10` (cmd 0x0C path) or Zone 8 (cmd 0x1A path).
### 8.1 Cmd 0x0C — write via OEM zone (with RB0 = 0x10 unlock)
```
# Step 1 — unlock Zone 0 with write enabled
TX: [ 06 NN 18 03 00 00 A6 01 03 ] # NOTE: RB0 must be 0x10, not 0x06,
# to set the read+write flag pattern
# (RC8 << 4 | RC8) and increment 0xFC.
```
(Implementer task: verify the RB0=0x10 vs RB0=0x06 distinction by experiment — it's set in `FUN_29D4 @ 0x29D4` based on `RB0 == 0x10` test, but the call path that actually populates `RB0` differs between the dispatcher's path-A and path-B.)
```
# Step 2 — cmd 0x0C write with verify
TX: [ 0E NN 0C NN 00 offset b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 03 ]
cmd=0x0C, RB3=count<11, RB4:RB5 = address, RB6+ = bytes to write
```
### 8.2 Cmd 0x1A — write via Zone 8 (after master unlock)
After Zone 8 is unlocked (RE7 = 0xFF), cmd 0x1A becomes available:
```
TX: [ ... NN 1A count rb4 offset b1..bN 03 ]
cmd=0x1A, RB3=count<11, RB5=EEPROM offset (8-bit), data bytes from RAM 0xB6+
```
Each byte is written and immediately read-back-verified internally by `FUN_22EA`. On verify failure, the response indicates failure and the partial write is left in place.
---