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.
This commit is contained in:
99
tunie/research/reference-maps/README.md
Normal file
99
tunie/research/reference-maps/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user