Commit tune maps and research so tunes are reachable from the phone

Removes the *.hex/maps_cache gitignore rule (explicit user call, reversing
the earlier no-redistribution stance) so the official TuneECU catalogue
maps, derived SAI/O2-delete composites, and the checksum/composition
tooling are actually available to pull up on a phone browser when using
the real TuneECU app. Also folds in tonight's KWP2000 fixes (TesterPresent
keep-alive, connect-failure cleanup, slow-init StartCommunication fix) and
the accumulated research docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP2GaxS9HkUdL5sLBnjKje
This commit is contained in:
2026-08-27 01:34:05 -05:00
parent 587f75eab4
commit 4aa5da53d2
98 changed files with 4148 additions and 49 deletions

View File

@@ -0,0 +1,65 @@
"""Flat-ROM checksum for the Triumph Keihin twin family -- reverse-engineered
and validated Aug 2026 (see ../WRITE_PATH.md for the full trace).
Source: `com.tuneecu.l.Ac(int, boolean)` in the decompiled TuneECU app,
called from MainActivity's map-info display ("Checksum : %04x", flagged
"*No-OEM"/"Error" on mismatch). This is a 16-bit sum-of-words checksum over
the calibration region of the flat ROM (0x50000-0x60000 for this ECU
family -- the exact same base/end this project's own `table_map.py` already
uses), stored in the last 2 bytes of that region.
Validated against 6 real, unmodified, official TuneECU downloads:
20187/20188/20191/20192/20262/20313 -- all computed == stored, exact.
One deliberate non-match, informative rather than a bug: the community
SAI/O2-delete file (20188Map2009AIRBOXBONNY.hex) has the exact same stored
checksum as its unmodified base (20188Map.hex) -- whoever built it edited
a few bytes by hand and never recomputed the checksum. TuneECU's own code
path for this looks like a *display/validity* check (flags "*No-OEM" /
"Error" in the UI) rather than a proven hard ECU-side write gate -- that
community file apparently worked for people despite the stale checksum,
which is consistent with this being an app-side sanity indicator rather
than (or in addition to) something the ECU's own bootloader independently
verifies during the real flash transfer. That ECU-side question is NOT
resolved by this -- see caveats below.
"""
from __future__ import annotations
CAL_BASE = 0x50000
CAL_END = 0x60000
def compute(rom: bytes, base: int = CAL_BASE, end: int = CAL_END) -> int:
"""16-bit sum-of-words checksum over rom[base:end-2], byte-swapped read
(matches `l.Ac()`'s `bArr[pos ^ 1] | (bArr[pos] << 8)` exactly)."""
length = (end - base) - 2
total = 0
i = 0
while i < length:
pos = base + i
total += rom[pos ^ 1] | (rom[pos] << 8)
i += 2
return total & 0xFFFF
def stored(rom: bytes, end: int = CAL_END) -> int:
return (rom[end - 2] << 8) | rom[end - 1]
def patch(rom: bytearray, base: int = CAL_BASE, end: int = CAL_END) -> None:
"""Recompute and write the checksum into rom[end-2:end] in place."""
value = compute(bytes(rom), base, end)
rom[end - 2] = (value >> 8) & 0xFF
rom[end - 1] = value & 0xFF
if __name__ == "__main__":
import sys
from reconstruct_rom import flat_rom
for path in sys.argv[1:] or ["20187Map.hex"]:
rom = flat_rom(open(path, "rb").read())
c, s = compute(rom), stored(rom)
print(f"{path:40s} computed={c:04x} stored={s:04x} {'MATCH' if c == s else 'MISMATCH'}")