"""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 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) args = ap.parse_args() defs = _extract_arrays(args.arrays) maps = json.loads(args.maps.read_text(encoding="utf-8")) payload = { "definitions": defs, "maps": maps, "modTargets": sorted(MOD_TARGETS), "meta": { "mapCount": len(maps), "source": "TuneECU APK resources (arrays.xml) + maps.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())