Add tunie map/tune viewer: self-contained HTML, no deps

A single double-click-to-open HTML viewer (no server, no external refs) with
three tabs: browse/filter the 1811-map catalogue (with a "my bike" filter for
mechanical-odo Keihin Bonneville maps), see TuneECU's full editable surface
(tunable parameters, table axes, toggleable devices with SAI/O2/exhaust flagged
as mod targets, live sensors, actuator tests) extracted from the APK resources,
and a drag-drop .bin/.hex table viewer that renders bytes as a configurable
heatmap grid + hex dump for when a ROM dump lands.

build_viewer.py regenerates the HTML from arrays.xml + maps.json.
This commit is contained in:
2026-08-10 15:24:45 -05:00
parent 4c44933b5d
commit c8c10d8977
4 changed files with 656 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
"""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'<string-array name="{name}"[^>]*>(.*?)</string-array>', s, re.S
)
if not m:
return None
return [
re.sub(r"\s+", " ", i.strip().strip('"'))
for i in re.findall(r"<item>(.*?)</item>", 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())