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:
110
tunie/research/reference-maps/compose_arrow_delete.py
Normal file
110
tunie/research/reference-maps/compose_arrow_delete.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Compose an Arrow 2-in-1 + SAI/O2-delete candidate map.
|
||||
|
||||
Builds on `toggle_devices.py`'s clean, conflict-free device-flag toggle
|
||||
(0x53801/0x53818/0x53819 -- verified disjoint from the Arrow exhaust delta,
|
||||
see COMMUNITY_TUNING.md) and additionally resolves the two "Idle Fuel Trim
|
||||
(CO)" bytes that conflict between the Arrow calibration and the real
|
||||
20188Map2009AIRBOXBONNY.hex delete map.
|
||||
|
||||
**Aug 2026 correction:** both bytes are now composed. The original version
|
||||
of this script left 0x5369C untouched, reasoning it was "exhaust-family
|
||||
dependent" (stock aftermarket-silencer maps held unsigned 255, which looked
|
||||
like a sentinel distinct from the ~104 held by production/Arrow maps). That
|
||||
reasoning was built on an **unsigned** byte interpretation. `TABLES.md`
|
||||
later established this whole table is **signed** two's-complement -128..127,
|
||||
confirmed from two independent code paths in the TuneECU app. Redone in
|
||||
signed space: unsigned 255 is simply signed -1 -- an entirely ordinary
|
||||
near-neutral value, not a sentinel at all. There is no regime conflict; the
|
||||
byte composes the same way 0x536AB always did, it just saturates.
|
||||
|
||||
0x5369C -- composed additively, signed. stock188=-1, delete=+117, delta
|
||||
= +118. arrow=+69 + 118 = +187, clamped to the signed max +127. The
|
||||
correction genuinely wants this cell pushed to its richest possible
|
||||
value at this RPM point -- clamping to +127 is the correct outcome, not
|
||||
a sign the byte is unusable, and it's a real, meaningfully different
|
||||
result from the prior version's "leave at Arrow's +69" default.
|
||||
|
||||
0x536AB -- composed additively, signed, same as before (the arithmetic
|
||||
is invariant to signed vs. unsigned interpretation as long as nothing
|
||||
overflows the representable range, which this one doesn't): stock188
|
||||
=-128, delete=-118, delta=+10, arrow=-123 + 10 = -113 -> byte 0x8F (143).
|
||||
|
||||
Usage:
|
||||
python3 compose_arrow_delete.py 20262Map.hex 20188Map.hex \\
|
||||
20188Map2009AIRBOXBONNY.hex out/20262-arrow-delete-composed.hex
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from decode_map import decode, encode
|
||||
from toggle_devices import SAI, O2_1, O2_2, repack, unpack
|
||||
|
||||
TRIM_BYTES = (0x536AB, 0x5369C) # both now composed, signed arithmetic
|
||||
|
||||
|
||||
def _s8(v: int) -> int:
|
||||
return v - 256 if v >= 128 else v
|
||||
|
||||
|
||||
def compose(arrow_raw: bytes, stock_after_raw: bytes, delete_raw: bytes) -> tuple[bytes, list[str]]:
|
||||
arrow_dec = decode(arrow_raw)
|
||||
arrow_rom, positions = unpack(arrow_dec)
|
||||
arrow_rom = bytearray(arrow_rom)
|
||||
|
||||
stock_rom, _ = unpack(decode(stock_after_raw))
|
||||
delete_rom, _ = unpack(decode(delete_raw))
|
||||
|
||||
notes = []
|
||||
|
||||
for addr, label in ((SAI, "SAI"), (O2_1, "O2 sensor 1"), (O2_2, "O2 sensor 2")):
|
||||
before = arrow_rom[addr]
|
||||
arrow_rom[addr] = 0
|
||||
notes.append(f"0x{addr:05X} {label:<14} {before} -> 0 (device flag, conflict-free)")
|
||||
|
||||
for addr in TRIM_BYTES:
|
||||
a_stock = _s8(stock_rom[addr])
|
||||
a_delete = _s8(delete_rom[addr])
|
||||
a_arrow = _s8(arrow_rom[addr])
|
||||
delta = a_delete - a_stock
|
||||
synth = max(-128, min(127, a_arrow + delta))
|
||||
before = arrow_rom[addr]
|
||||
after = synth & 0xFF
|
||||
arrow_rom[addr] = after
|
||||
notes.append(
|
||||
f"0x{addr:05X} trim (signed) {a_arrow:+d} + delta {delta:+d} = {synth:+d}"
|
||||
f" (byte {before} -> {after})"
|
||||
)
|
||||
|
||||
repacked = repack(arrow_dec, bytes(arrow_rom), positions)
|
||||
out = encode(repacked)
|
||||
assert decode(out) == repacked, "round-trip failed"
|
||||
return out, notes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 5:
|
||||
print(
|
||||
"usage: compose_arrow_delete.py <arrow.hex> <stock_aftermarket.hex> "
|
||||
"<delete_reference.hex> <outfile.hex>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
arrow_path, stock_path, delete_path, out_path = sys.argv[1:5]
|
||||
out, notes = compose(
|
||||
open(arrow_path, "rb").read(),
|
||||
open(stock_path, "rb").read(),
|
||||
open(delete_path, "rb").read(),
|
||||
)
|
||||
open(out_path, "wb").write(out)
|
||||
for n in notes:
|
||||
print(" ", n)
|
||||
print(f"wrote {out_path} ({len(out)} bytes)")
|
||||
print("NOTE: flash checksum not patched -- not known to be flashable as-is.")
|
||||
print("NOTE: main VE fuel-table overlap (~1340 bytes) still unresolved -- see COMMUNITY_TUNING.md.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user