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:
82
tunie/research/reference-maps/reconstruct_rom.py
Normal file
82
tunie/research/reference-maps/reconstruct_rom.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user