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
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""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())
|