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.
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Download TuneECU Triumph map files into a local cache for the viewer.
|
|
|
|
The viewer can render any of these from a dropdown (when served over HTTP, since
|
|
browsers block fetch() on file://). Maps come from the same endpoint the TuneECU
|
|
app uses: https://www.tuneecu.fr/Maps/<path> (found in the decompile).
|
|
|
|
python3 download_maps.py # mechanical-odo Bonneville set
|
|
python3 download_maps.py --filter Bonneville # all Bonneville twin maps
|
|
python3 download_maps.py 20187 20188 20262 # specific map numbers
|
|
python3 download_maps.py --all-twins # every Triumph twin (~big)
|
|
|
|
Writes maps_cache/<n>Map.hex and maps_cache/index.json (id + description).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
BASE = "https://www.tuneecu.fr/Maps/"
|
|
LIST_URL = BASE + "mapList.dat"
|
|
CACHE = Path(__file__).parent / "maps_cache"
|
|
MAPS_JSON = Path(__file__).parent / "maps.json"
|
|
|
|
# Default: the 2010 mechanical-odometer Bonneville candidates.
|
|
DEFAULT_IDS = ["20187", "20188", "20191", "20192"]
|
|
|
|
|
|
def _get(url: str) -> bytes:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "tunie"})
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return r.read()
|
|
|
|
|
|
def _map_list() -> list[str]:
|
|
"""Return the '<path>/<n>Map.hex' entries from the server's map list."""
|
|
text = _get(LIST_URL).decode("latin1")
|
|
return [ln.strip() for ln in text.splitlines() if ln.strip().endswith("Map.hex")]
|
|
|
|
|
|
def _descriptions() -> dict[str, list[str]]:
|
|
if not MAPS_JSON.exists():
|
|
return {}
|
|
return {m["id"]: m["description"] for m in json.loads(MAPS_JSON.read_text())}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("ids", nargs="*", help="specific map numbers to fetch")
|
|
ap.add_argument("--filter", help="download every map whose path contains this substring")
|
|
ap.add_argument("--all-twins", action="store_true", help="all Triumph twin maps")
|
|
args = ap.parse_args()
|
|
|
|
entries = _map_list() # e.g. "Triumph/Bonneville/20187Map.hex"
|
|
if args.ids:
|
|
wanted = [e for e in entries if any(f"/{i}Map.hex" in e for i in args.ids)]
|
|
elif args.all_twins:
|
|
wanted = [e for e in entries if e.startswith("Triumph/")
|
|
and any(k in e for k in ("Bonneville", "Thruxton", "Scrambler",
|
|
"America", "Speedmaster"))]
|
|
elif args.filter:
|
|
wanted = [e for e in entries if args.filter.lower() in e.lower()]
|
|
else:
|
|
wanted = [e for e in entries if any(f"/{i}Map.hex" in e for i in DEFAULT_IDS)]
|
|
|
|
if not wanted:
|
|
print("Nothing matched.", file=sys.stderr)
|
|
return 1
|
|
|
|
CACHE.mkdir(exist_ok=True)
|
|
descs = _descriptions()
|
|
index = []
|
|
for path in wanted:
|
|
fname = path.rsplit("/", 1)[-1] # 20187Map.hex
|
|
num = fname.replace("Map.hex", "")
|
|
dest = CACHE / fname
|
|
if not dest.exists():
|
|
try:
|
|
data = _get(BASE + path)
|
|
except Exception as exc:
|
|
print(f" skip {fname}: {exc}", file=sys.stderr)
|
|
continue
|
|
dest.write_bytes(data)
|
|
print(f" got {fname} ({len(data)} bytes)")
|
|
index.append({"file": fname, "id": num,
|
|
"desc": descs.get(num, [num])})
|
|
|
|
index.sort(key=lambda x: x["id"])
|
|
(CACHE / "index.json").write_text(json.dumps(index, indent=2))
|
|
print(f"\n{len(index)} maps in {CACHE}/ (index.json written)")
|
|
print("Serve them with: python3 serve.py then use the catalogue dropdown.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|