Files
HC_APTBS/docs/kline_eeprom_spec.md
LucianoDev 827b811b39 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>
2026-05-07 13:59:50 +02:00

9.6 KiB
Raw Permalink Blame History

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.