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,110 @@
"""Flip SAI/O2 device-enable flags in a TuneECU map, in place.
Byte-boolean array in the flat ROM: 0x53801 = SAI, 0x53818 / 0x53819 = the two
O2 sensors (1 = enabled, 0 = disabled). Found by diffing stock 20188 against the
community "NO SAI, NO O2 SENSORS" reference map -- see DEVICES.md for the full
derivation and confidence levels (SAI ~90%, O2 ~80%).
This edits the *download-format* map (decode -> flip bytes in the flat ROM ->
repack into the decoded map's own layout -> re-encode). It does NOT touch the
ECU flash checksum (out of scope, unsolved -- see the "Editing / export" section
of DEVICES.md), so the output is for the viewer / further analysis only. It is
NOT known to be flashable as-is.
Usage:
python3 toggle_devices.py 20262Map.hex out/20262-noSAI-noO2.hex
python3 toggle_devices.py --only-sai 20262Map.hex out/20262-noSAI.hex
"""
from __future__ import annotations
import argparse
from decode_map import decode, encode
SAI = 0x53801
O2_1 = 0x53818
O2_2 = 0x53819
FLAT_MARKER = 0x6F66
_LABELS = {SAI: "SAI", O2_1: "O2 sensor 1", O2_2: "O2 sensor 2"}
def _le(buf: bytes, o: int, n: int) -> int:
return int.from_bytes(buf[o : o + n], "little")
def unpack(decoded: bytes) -> tuple[bytearray, list[tuple[int, int, int]]]:
"""Reconstruct the flat ROM from a decoded map; also return the (packed_pos,
dest_offset, length) triples needed to repack it afterwards."""
i21 = _le(decoded, 28, 2)
if _le(decoded, i21 + 31, 2) != FLAT_MARKER:
raise ValueError(f"bad unpack marker at 0x{i21 + 31:X}")
count = decoded[i21 + 33]
entries = [
(_le(decoded, i21 + 34 + k * 8, 4), _le(decoded, i21 + 38 + k * 8, 4))
for k in range(count)
]
p = i21 + 34 + count * 8
rom = bytearray(b"\xff" * max(off + ln for off, ln in entries))
positions = []
for off, ln in entries:
rom[off : off + ln] = decoded[p : p + ln]
positions.append((p, off, ln))
p += ln
return rom, positions
def repack(decoded: bytes, rom: bytes, positions: list[tuple[int, int, int]]) -> bytes:
"""Inverse of unpack(): copy the (possibly modified) flat ROM back into the
decoded map's packed layout."""
out = bytearray(decoded)
for p, off, ln in positions:
out[p : p + ln] = rom[off : off + ln]
return bytes(out)
def toggle(raw: bytes, addresses: tuple[int, ...]) -> tuple[bytes, list[tuple[int, str, int, int]]]:
"""Return (re-encoded .hex bytes, [(address, label, before, after), ...])."""
decoded = decode(raw)
rom, positions = unpack(decoded)
changes = []
for addr in addresses:
before = rom[addr]
rom[addr] = 0
changes.append((addr, _LABELS.get(addr, f"0x{addr:X}"), before, 0))
repacked = repack(decoded, bytes(rom), positions)
out = encode(repacked)
assert decode(out) == repacked, "round-trip failed after repack"
return out, changes
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("infile")
ap.add_argument("outfile")
ap.add_argument("--only-sai", action="store_true", help="disable SAI only")
ap.add_argument("--only-o2", action="store_true", help="disable both O2 sensors only")
args = ap.parse_args()
if args.only_sai:
addrs = (SAI,)
elif args.only_o2:
addrs = (O2_1, O2_2)
else:
addrs = (SAI, O2_1, O2_2)
raw = open(args.infile, "rb").read()
out, changes = toggle(raw, addrs)
open(args.outfile, "wb").write(out)
for addr, label, before, after in changes:
print(f" 0x{addr:05X} {label:<14} {before} -> {after}")
print(f"wrote {args.outfile} ({len(out)} bytes)")
print("NOTE: flash checksum not patched -- not known to be flashable as-is.")
return 0
if __name__ == "__main__":
raise SystemExit(main())