Files
samplez/tunie/research/reference-maps
uhryniuk 2ad5fab677 Add a layered low-throttle fuel correction on top of the Arrow delete tune
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
2026-09-10 15:39:10 -05:00
..

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