"""Build a self-contained HTML viewer for the Triumph tuning project. Extracts TuneECU's editable-surface definitions (tunable parameters, table axes, toggleable devices, live sensors, actuator tests) from the decompiled APK resources, combines them with the already-extracted map catalogue (maps.json), and embeds everything into a single double-click-to-open HTML file with no server and no external dependencies. Usage: python3 viewer/build_viewer.py \ --arrays ../work/apktool_out/res/values/arrays.xml \ --maps maps.json \ --template viewer/template.html \ --out viewer/tunie-viewer.html """ from __future__ import annotations import argparse import json import re from pathlib import Path # Resource arrays worth surfacing, with a friendly label and short note. DEF_ARRAYS = { "Parameters": ("Tunable parameters", "Single-value settings TuneECU can change."), "title_axis": ("Table axes", "The axes every fuel/ignition map is built on."), "Devices": ("Toggleable devices", "Emissions/hardware features that can be turned off."), "eSensor": ("Live sensors", "Channels the ECU streams while running."), "test_devices": ("Actuator tests", "Outputs TuneECU can command for bench testing."), "reset_devices": ("Reset routines", "Adaptations/adjustments that can be reset."), "ftrim_name": ("Fuel-trim tables", "Per-cylinder fuel trim table names."), "ecu": ("ECU families", "Supported ECUs; the code after ':' is the type char."), } # Devices/parameters directly relevant to this bike's planned mods. MOD_TARGETS = {"SAI", "O2 Sensor", "O2 Sensor (2)", "Exhaust Valve", "Air Flap"} def _extract_arrays(arrays_xml: Path) -> dict: s = arrays_xml.read_text(encoding="utf-8") def arr(name: str) -> list[str] | None: m = re.search( rf']*>(.*?)', s, re.S ) if not m: return None return [ re.sub(r"\s+", " ", i.strip().strip('"')) for i in re.findall(r"(.*?)", m.group(1), re.S) ] out = {} for name, (label, note) in DEF_ARRAYS.items(): items = arr(name) or [] out[name] = { "label": label, "note": note, "items": [{"i": i, "text": t} for i, t in enumerate(items) if t], } 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, # Device-enable flags: byte at base + fe[33] + rel (1=on, 0=off). # Validated against a NO-SAI-NO-O2 reference map (see research DEVICES.md). "deviceFe": 33, "devices": [ {"name": "SAI (Secondary Air Injection)", "rel": 0x00}, {"name": "O2 sensor", "rel": 0x17}, {"name": "O2 sensor (2)", "rel": 0x18}, ], } 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 = [] if mapdefs_path.exists(): geometry = json.loads(mapdefs_path.read_text(encoding="utf-8")).get("geometry", []) payload = { "definitions": defs, "maps": maps, "geometry": geometry, "romdefs": romdefs, "modTargets": sorted(MOD_TARGETS), "meta": { "mapCount": len(maps), "tableGroups": len(geometry), "source": "TuneECU APK resources (arrays.xml) + maps.json + mapdefs.json", }, } template = args.template.read_text(encoding="utf-8") blob = json.dumps(payload, separators=(",", ":")) html = template.replace("/*__DATA__*/", blob) args.out.write_text(html, encoding="utf-8") kb = len(html.encode("utf-8")) / 1024 print(f"wrote {args.out} ({kb:.0f} KB, {len(maps)} maps, " f"{sum(len(d['items']) for d in defs.values())} definition rows)") return 0 if __name__ == "__main__": raise SystemExit(main())