Files
uhryniuk 173f6fa0c4 Locate Keihin tables + render real fuel maps in the viewer
Reversed the inner map format from l.java (Nc/Lb) and validated it against the
stock reference maps:

- reconstruct_rom.py: decoded map -> unpack directory -> 384KB flat ROM. In the
  flat ROM, production (20187) vs aftermarket (20188) differ only 1.4% (vs 96%
  packed), i.e. real fuel enrichment.
- table_map.py + TABLES.md: fe = c.a[Qd*48]; base = (fe[0]&0x2F0)<<12 = 0x50000;
  table = base + fe[k]. Located 9 tables (main/low-throttle fuel per cylinder,
  base/idle fuel, ignition by gear x4), 32 RPM rows x 20 throttle cols, with real
  axes (RPM fe[8], throttle fe[27]) and the AFR curve (fe[2], 128=lambda 1.00).
  Validated: main-fuel delta is uniformly richer in the aftermarket map.

Viewer now has a "Triumph tables" tab: drop a real .hex (or pick two) and it
decodes, unpacks, and renders the fuel/ignition tables as heatmaps with real
axes, plus an A->B difference view. build_viewer.py embeds the c.a/s.a directory
so any map resolves in-browser.

Catalogue dropdown: download_maps.py fetches map .hex files into maps_cache/;
serve.py serves the viewer over http so the dropdown can fetch them (drag-and-drop
still works on file://).

Proprietary map binaries (*.hex, *.dec.bin, maps_cache/) are gitignored — code and
docs only.
2026-08-11 07:00:19 -05:00

100 lines
4.8 KiB
Markdown

# Reference stock maps (real, from TuneECU's server)
Four genuine factory Bonneville calibrations, downloaded from
`https://www.tuneecu.fr/Maps/Triumph/Bonneville/<n>Map.hex` (the endpoint the
TuneECU app itself uses; found in the decompile at `MainActivity.java:7407`).
| File | Map | Fits |
|---|---|---|
| `20187Map.hex` | 20187 | Bonneville, **production** silencers, mechanical odo |
| `20188Map.hex` | 20188 | Bonneville, **aftermarket** silencers, mechanical odo |
| `20191Map.hex` | 20191 | production, up to VIN 739050, E25 |
| `20192Map.hex` | 20192 | aftermarket, up to VIN 739050, E25 |
These are the candidate stock maps for the 2010 T100 (mechanical odometer). Once
we dump the bike, its ROM should correspond to one of these.
## What they validated
- **Header format is correct.** All four satisfy the reversed magic:
little-endian u32 of bytes[0:4] `& 0xFF00FFE0 == 0x18001360`, i.e.
`byte0=0x67, byte1=0x13, byte3=0x18`. This confirms the format reversing in
`../../viewer/FORMAT.md` and the little-endian correction.
- **`c.b` is the table directory.** `m.uc()` reassembles a map by copying tables
at the `c.b` (offset,length) pairs — independent confirmation of the geometry.
## What they revealed (the new blocker)
**The map body is encrypted.** Evidence:
- Uniform entropy ~7.91 bits/byte across the whole body (8.0 = random).
- Sibling maps that should differ only in fueling (20187 vs 20188) share just
**0.1%** of bytes, with no equal run ≥16 bytes.
- A low-entropy footer (last ~5 KB, `H≈3.0`) that holds structured key/signature
material — matching `p8()`, which reads its key from the file **tail**
(`length-42`, `length-35`, …), derives it via `m.Sb()`, and unpacks via
`m.uc()`.
So the table offsets in `mapdefs.json` describe the **decrypted** map, and can't
be validated against these files until the map decryption is reversed. The
signature lookup (`sc()` at header offset 20) does **not** match our clean `s.a`
directory for these files — consistent with the real directory key living in the
encrypted/footer region, not the header.
## DECRYPTION SOLVED (`decode_map.py`)
The distribution format is decrypted by `l.dc()` — a self-synchronising CBC-style
XOR stream cipher seeded by the (plaintext) 4-byte header. NOT AES; the earlier
`m.uc`/`m.Sb` path is for raw ROM dumps, not these downloads. `p8()` calls
`l.dc()` on any map that isn't a raw-ROM size.
Reversed, ported, and **verified**:
- `decode_map.py` decodes all four maps; encode(decode(x)) == x (round-trip).
- Decoded 20187 contains the plaintext strings `Bonneville` / `Production
silencers` / `Mechanical odometer` at 0x1E/0x29/0x3E — matching the catalogue.
- Entropy drops 7.99 → ~6.1 bit/byte.
- **Directory lookup now validates on the decoded map:** signature at offset 20
= `0x0187CA84`, which matches `s.a` record #541 (`field[0]=25676420`). So the
whole chain works: decode → signature@20 → `s.a` directory → `c.a`/`c.b`.
Decoded files: `*.dec.bin`.
## SOLVED: flat ROM reconstruction + table location (`reconstruct_rom.py`)
The decoded map is a **packed** container, which is why a naive byte-diff of two
decoded maps showed 96% difference — the packed chunks are misaligned. Each
decoded map carries its own **unpack directory** (from `MainActivity.p8`):
`base = le16(dec[28])`; a `0x6F66` marker at `base+31`; a count at `base+33`;
then `count` (le32 dest_offset, le32 length) entries, followed by the packed
data copied verbatim to `flat_rom[dest:dest+len]`. This yields a **384 KB
(0x60000) flat ROM** — the real ECU address space.
**In the flat ROM, 20187 (production) vs 20188 (aftermarket) differ by only
1.4%** (down from 96%), localised to 26 regions. That is the actual production→
aftermarket calibration change, not noise.
Table pointers are in `fe = c.a[Qd*48]` (Qd from the directory lookup). Table
address = `fe[39]` (base 0x50000) + `fe[k]`. **VERIFIED:** `fe[11]=0x6990` →
`0x56990` is a **main fuel table** — a smooth 20-wide VE surface, and the
aftermarket map is richer in 284/320 cells (mean +323), exactly as an
aftermarket-exhaust tune should be.
The two big high-entropy diff regions (`0x55599`, `0x56990`) are the two fuel
tables; the small isolated diffs (e.g. single bytes at `0x50C13`, `0x534AD`,
`0x59759`) are prime **SAI / O2 / lambda flag** candidates — now findable because
the flat-ROM diff is localised.
Still to finish: map the remaining `fe[k]` pointers to named tables (small-
throttle fuel, AFR, ignition-by-gear, idle, limiters) and confirm each table's
dimensions/axes/scaling from the `fe` metadata (or cross-check with an XDF). The
hard part — decrypt, reconstruct, locate — is done and validated.
## Provenance (sha256)
```
cd02cd2a…306c99 20187Map.hex
bed451a9…e1ebe4 20188Map.hex
7436b0f2…79ca3a6 20191Map.hex
f9b9c60d…ffa10f 20192Map.hex
```