Reverse-engineered the Triumph Keihin map format from the TuneECU loader (l.java zc/sc/Nc) and data classes c/r/s/t.java, and turned the viewer's table tab into a working editor. extract_mapdefs.py pulls the table directory out of the decompiled app into mapdefs.json: r.a/s.a/t.a (calibration directory, 8-int records keyed by the map's offset-20 signature) -> c.a (48-int calibration metadata) -> c.b (32-int groups = 16 offset/length pairs each), yielding 413 candidate table offsets. FORMAT.md documents the header magic (0x18008060 masked), the directory chain, and the write-back checksum. The viewer now edits: pick a known table offset (or set it manually), toggle edit mode, click a cell to change its value, and the bytes are rewritten with the running 16-bit checksum patched by (old - new) exactly as TuneECU does, then Download the modified copy. Verified: editing 2016->9999 with the checksum word at 0 yields 57553 = (0 + 2016 - 9999) & 0xffff. Geometry offsets are read-confident but not yet validated against a real map binary; FORMAT.md flags this. Editing/checksum stay local to a downloaded copy; the flash write path remains out of the read-only tunie tool.
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""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"))
|
|
|
|
# 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,
|
|
"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())
|