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:
2026-08-11 07:00:19 -05:00
parent 5af8825747
commit 173f6fa0c4
12 changed files with 959 additions and 10 deletions

View File

@@ -62,17 +62,62 @@ def _extract_arrays(arrays_xml: Path) -> dict:
return out
# fe-field -> named table map (from research/reference-maps/table_map.py, validated).
_TABLE_DEFS = [
("Main fuel — cylinder 1", 11, "fuel"),
("Main fuel — cylinder 2", 12, "fuel"),
("Low-throttle fuel — cyl 1", 15, "fuel"),
("Low-throttle fuel — cyl 2", 16, "fuel"),
("Fuel — base/idle", 9, "fuel"),
("Ignition advance — gear 1", 19, "ignition"),
("Ignition advance — gears 2–5", 20, "ignition"),
("Ignition advance — gear 6", 21, "ignition"),
("Ignition advance — neutral", 22, "ignition"),
]
def _extract_romdefs(args) -> dict:
"""Extract the c.a and s.a directory arrays needed to decode a real map in-browser.
Requires --tuneecu-src pointing at the decompiled com/tuneecu sources. If not
given or not found, returns {} and the viewer's Triumph-tables tab is disabled.
"""
src = getattr(args, "tuneecu_src", None)
if not src:
return {}
src = Path(src)
if not (src / "c.java").exists():
return {}
def arr(cls: str, field: str) -> list[int]:
s = (src / f"{cls}.java").read_text()
m = re.search(rf"\b{field} = \{{(.*?)\}};", s, re.S)
return [int(t.strip().rstrip("L")) for t in m.group(1).split(",") if t.strip()]
return {
"ca": arr("c", "a"),
"sa": arr("s", "a"),
"tables": [{"name": n, "fe": f, "kind": k} for n, f, k in _TABLE_DEFS],
"rows": 32, "cols": 20, "rpmFe": 8, "throttleFe": 27,
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--arrays", required=True, type=Path)
ap.add_argument("--maps", required=True, type=Path)
ap.add_argument("--template", required=True, type=Path)
ap.add_argument("--out", required=True, type=Path)
ap.add_argument("--tuneecu-src", default=None,
help="decompiled com/tuneecu dir (enables the Triumph-tables tab)")
args = ap.parse_args()
defs = _extract_arrays(args.arrays)
maps = json.loads(args.maps.read_text(encoding="utf-8"))
# Directory arrays + table map for decoding/rendering real Triumph maps.
romdefs = _extract_romdefs(args)
# Optional: table geometry from extract_mapdefs.py, if it has been run.
mapdefs_path = args.out.parent / "mapdefs.json"
geometry = []
@@ -83,6 +128,7 @@ def main() -> int:
"definitions": defs,
"maps": maps,
"geometry": geometry,
"romdefs": romdefs,
"modTargets": sorted(MOD_TARGETS),
"meta": {
"mapCount": len(maps),