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.
83 lines
3.4 KiB
Python
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")
|