Real-world reports: stalling/choking under ~10% throttle and while blipping the throttle at low speed, after snorkel removal with no main-table correction for the extra airflow. Confirmed via a genuine percentage-delta pulled from 20184dynoTuneSteveO2-Disable.hex (also snorkel-removed) vs its own stock baseline 20186Map.hex, in the Low-throttle fuel tables, restricted to the throttle columns actually implicated (raw throttle <= 100, ~10%). compose_lowthrottle.py composes onto the already-composed 20262-arrow-delete-composed.hex (not raw stock 20262) -- additive on top of the existing SAI/O2-off + idle-trim layer, same "respect what's already there" principle as compose_arrow_delete.py. Percentage-based per-cell, not flat additive, since Arrow's own table already has its own exhaust-specific correction baked in. Output: derived/20262-arrow-delete-lowthrottle-composed.hex. Checksum patched and verified valid; confirmed layer-1's device flags and trim bytes survive unchanged underneath. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FP2GaxS9HkUdL5sLBnjKje
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.mdand the little-endian correction. c.bis the table directory.m.uc()reassembles a map by copying tables at thec.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 — matchingp8(), which reads its key from the file tail (length-42,length-35, …), derives it viam.Sb(), and unpacks viam.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.pydecodes all four maps; encode(decode(x)) == x (round-trip).- Decoded 20187 contains the plaintext strings
Bonneville/Production silencers/Mechanical odometerat 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 matchess.arecord #541 (field[0]=25676420). So the whole chain works: decode → signature@20 →s.adirectory →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