"""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/ (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/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 '/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())