Files
samplez/tunie/research/reference-maps/reconstruct_rom.py
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

83 lines
3.4 KiB
Python

"""Reconstruct the flat ECU ROM image from a decoded TuneECU map, and locate
calibration tables.
Chain (all reversed from the decompile, verified against real stock maps):
encrypted .hex --dc()--> decoded map --unpack directory--> flat 0x60000 ROM
The decoded map carries its own unpack directory (from MainActivity.p8):
desc_len = le16(dec[28]); base i21 = desc_len
marker le16(dec[i21+31]) == 0x6F66
count dec[i21+33]
entries count * (le32 dest_offset, le32 length) at i21+34
data packed chunks follow, copied verbatim to flat_rom[dest:dest+len]
Reconstructing into the flat ROM is what makes sibling maps comparable:
20187 (production) vs 20188 (aftermarket) diff drops from 96% (packed, misaligned)
to 1.4% (flat ROM) — the difference is real fuel enrichment, not noise.
Table pointers live in the calibration-metadata record fe = c.a[Qd*48], where Qd
comes from the directory lookup (s.a record for the map's signature). Table
address = fe[39] (base, 0x50000) + fe[k]. VERIFIED: fe[11]=0x6990 -> 0x56990 is a
main fuel table; 20188-minus-20187 there is uniformly positive (richer), exactly
as an aftermarket-exhaust tune should be.
"""
from __future__ import annotations
import struct
from pathlib import Path
from decode_map import decode
FLAT_MARKER = 0x6F66
def flat_rom(encrypted_or_decoded: bytes) -> bytes:
"""Return the reconstructed flat ROM. Accepts an encrypted .hex or a decoded map."""
d = encrypted_or_decoded
# Bytes 0..3 are plaintext in BOTH forms, so detect via the description block
# at offset 30 (readable ASCII once decoded).
if not all(c in (10, 13) or 32 <= c < 127 for c in d[30:45]):
d = decode(d)
le = lambda o, n: int.from_bytes(d[o:o + n], "little")
i21 = le(28, 2)
if le(i21 + 31, 2) != FLAT_MARKER:
raise ValueError(f"bad unpack marker 0x{le(i21+31,2):04X} (expected 0x6F66)")
count = d[i21 + 33]
entries = [(le(i21 + 34 + k * 8, 4), le(i21 + 38 + k * 8, 4)) for k in range(count)]
p = i21 + 34 + count * 8
rom = bytearray(b"\xff" * max(o + l for o, l in entries))
for off, ln in entries:
rom[off:off + ln] = d[p:p + ln]
p += ln
return bytes(rom)
def read_table_u16(rom: bytes, offset: int, cols: int, rows: int, be: bool = True) -> list[list[int]]:
fmt = ">H" if be else "<H"
return [
[struct.unpack_from(fmt, rom, offset + (r * cols + c) * 2)[0] for c in range(cols)]
for r in range(rows)
]
if __name__ == "__main__":
here = Path(__file__).parent
roms = {m: flat_rom((here / f"{m}Map.hex").read_bytes()) for m in ("20187", "20188")}
a, b = roms["20187"], roms["20188"]
n = min(len(a), len(b))
diff = sum(1 for i in range(n) if a[i] != b[i])
print(f"flat ROM 0x{len(a):X}; 20187 vs 20188 differ {100*diff/n:.2f}% ({diff} bytes)")
# Main fuel table at 0x56990 (fe[39]=0x50000 + fe[11]=0x6990), 20 cols.
off, cols, rows = 0x56990, 20, 16
ta = read_table_u16(a, off, cols, rows)
tb = read_table_u16(b, off, cols, rows)
print(f"\nfuel table @0x{off:X} (20187 production), first {rows}x{cols}:")
for row in ta:
print(" " + " ".join(f"{v:5d}" for v in row))
deltas = [tb[r][c] - ta[r][c] for r in range(rows) for c in range(cols)]
pos = sum(1 for x in deltas if x > 0)
print(f"\naftermarket-minus-production: {pos}/{len(deltas)} cells richer "
f"(mean {sum(deltas)/len(deltas):+.0f}) -> enrichment, as expected")