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:
BIN
tunie/research/reference-maps/20184dynoTuneSteveO2-Disable.hex
Normal file
BIN
tunie/research/reference-maps/20184dynoTuneSteveO2-Disable.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20185Map.hex
Normal file
BIN
tunie/research/reference-maps/20185Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20186Map.hex
Normal file
BIN
tunie/research/reference-maps/20186Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20187Map.hex
Normal file
BIN
tunie/research/reference-maps/20187Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20188Map.hex
Normal file
BIN
tunie/research/reference-maps/20188Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20188Map2009AIRBOXBONNY.hex
Normal file
BIN
tunie/research/reference-maps/20188Map2009AIRBOXBONNY.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20191Map.hex
Normal file
BIN
tunie/research/reference-maps/20191Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20192Map.hex
Normal file
BIN
tunie/research/reference-maps/20192Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20262Map.hex
Normal file
BIN
tunie/research/reference-maps/20262Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20313Map.hex
Normal file
BIN
tunie/research/reference-maps/20313Map.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/20500MapAirboxBonnie_LCD.hex
Normal file
BIN
tunie/research/reference-maps/20500MapAirboxBonnie_LCD.hex
Normal file
Binary file not shown.
@@ -1,5 +1,16 @@
|
||||
# Device-enable flags (SAI / O2 / lambda) — validated
|
||||
|
||||
**Aug 2026 update:** re-fetched `20188Map2009AIRBOXBONNY.hex` from its real
|
||||
source — it's in `tuneecu.net`'s own custom-map archive
|
||||
(`Twin_Custom_Tune_list.html`, not a forum attachment as previously assumed),
|
||||
not gitignore-lost this time. Re-ran the diff against a freshly downloaded
|
||||
stock `20188Map.hex` and **every claim below reproduced exactly**: same
|
||||
1428-byte diff (0.36%), same three device-flag bytes, same two trim bytes.
|
||||
The file now lives in this directory (`20188Map2009AIRBOXBONNY.hex`,
|
||||
`20187/88/91/92Map.hex`) rather than resting on memory of a prior session.
|
||||
See `../COMMUNITY_TUNING.md` for the new composability findings (device
|
||||
flags vs. Arrow exhaust delta) this re-verification enabled.
|
||||
|
||||
## How they were found
|
||||
|
||||
Both stock reference maps (20187, 20188) have SAI and O2 **active**, so diffing
|
||||
@@ -66,3 +77,80 @@ stock map can run lean/rough. A proper delete pairs flags-off with fuel enrichme
|
||||
**Recommendation:** flash a complete, known-good delete map matching the hardware
|
||||
(exhaust/filters), not hand-toggled flags on a stock map. The checkbox editor is
|
||||
for understanding/building a map, not a one-click safe delete.
|
||||
|
||||
## `0x53822` — the extra flag found on the America dyno tune (Aug 2026)
|
||||
|
||||
Pushed on identifying this (index 33 past the `0x53801` base, one slot
|
||||
past the two O2 sensors — see `COMMUNITY_TUNING.md`'s original finding on
|
||||
`20184dynoTuneSteveO2-Disable.hex`). Real, bounded progress, not a full
|
||||
resolution:
|
||||
|
||||
**Found the actual device name list** — `R.array.Devices` in
|
||||
`arrays.xml:3-18`, 15 entries: `SAI, Exhaust Valve, O2 Sensor, 2nd
|
||||
Throttle, Air Flap, EPC, O2 Sensor (2), Purge Valve, Idle Speed Control,
|
||||
Traction Control, Immobilizer, Instruments, ABS, Speed Sensor (non ABS),
|
||||
Race ECU`. This independently confirms and extends the existing SAI(0)/O2
|
||||
Sensor(23)/O2 Sensor(2)(24) findings above — matches exactly, real data.
|
||||
|
||||
**Found the mechanism**: `l.java`'s `lc()` (`l.java:5742`) resolves the
|
||||
per-device flat-ROM byte-boolean array from three packed nibble-encoded
|
||||
constants (`x.t`/`x.s`/`x.q` in the decompile referenced by
|
||||
`docs/ROADMAP.md`), looked up against `R.array.Devices` by index. **This
|
||||
confirms flat-ROM byte position and `Devices[]` array index are not the
|
||||
same thing** — SAI (array index 0) sits at byte 0, but O2 Sensor (array
|
||||
index 2) sits at byte 23, O2 Sensor (2) (array index 6) sits at byte 24 —
|
||||
a bike-specific sparse remapping, not a fixed formula.
|
||||
|
||||
**Where it stops:** the specific per-family constants that would decode
|
||||
byte 33 precisely require tracing which branch of `x.java`'s ~46
|
||||
assignments to the relevant classifier variable applies to Bonneville/T100
|
||||
specifically. `docs/ROADMAP.md`'s existing note cites `i27=72` as the
|
||||
anchor for this ECU family from a prior research pass, but that exact
|
||||
comparison doesn't appear in this decompile snapshot — likely version
|
||||
drift (this decompile's changelog shows APK updates through v6.4.36, June
|
||||
2026; the `i27=72` note may predate that). Didn't find a reliable
|
||||
replacement anchor without a much larger, low-confidence search across all
|
||||
46 assignment sites.
|
||||
|
||||
**Best-supported guess, reasoning not proof:** narrowing the 12 unassigned
|
||||
`Devices[]` names by plausibility for a 2010 mechanical-odo Bonneville/
|
||||
America-class twin — **Purge Valve** (this project's own `system1.html`
|
||||
research already documents a "Canister Purge Valve, California models
|
||||
only" on this ECU family — a real, independently-corroborated candidate),
|
||||
**Idle Speed Control** (the ISC stepper motor genuinely exists on this
|
||||
engine), and **Immobilizer** (period-correct fuel-injected Triumphs
|
||||
commonly used transponder immobilizers) are the most plausible of the 12;
|
||||
**Exhaust Valve**, **2nd Throttle**, **ABS**, **Traction Control**, **Race
|
||||
ECU** are implausible for this specific bike/era and can likely be ruled
|
||||
out. Not confirmed — a genuine guess ranked by plausibility, not a finding.
|
||||
|
||||
**Update (Aug 2026) — independently verified, raises confidence further:**
|
||||
checked `tests.html` in this project's own site mirror directly and found
|
||||
a distinct, real testable menu entry: **"Purge Control Valve (Only bikes
|
||||
with charcoal canister) — Activate the purge valve, listen for a very
|
||||
quiet noise,"** listed right alongside its own separate "SAI" test entry.
|
||||
This confirms Purge Valve is a real, independent device on this ECU
|
||||
family with its own dedicated test/enable path, not a guess — genuinely
|
||||
strengthens (does not yet prove) the byte-33 hypothesis. Also confirms
|
||||
from the same list: "Air Flap (675 Daytona)" is explicitly Daytona-675-only,
|
||||
matching this document's elimination reasoning above. (Surfaced by an
|
||||
external research pass, then independently confirmed here directly
|
||||
against this project's own source rather than trusted secondhand — see
|
||||
`../EXTERNAL_RESEARCH_NOTES.md`.)
|
||||
|
||||
**What would actually resolve this:** a second real-world reference map
|
||||
where the specific device at byte 33 changes state with a documented
|
||||
description (the same technique that resolved SAI/O2 originally) would
|
||||
settle it definitively without needing the `x.t`/`x.s`/`x.q` trace at all.
|
||||
|
||||
## Open question raised by community research (unresolved)
|
||||
|
||||
Multiple forum threads describe TuneECU's own SAI/O2 checkboxes (the same UI
|
||||
these bytes drive) as **only suppressing the fault-code/CEL logic**, not
|
||||
actually forcing open-loop fueling — see `../COMMUNITY_TUNING.md`. That's in
|
||||
tension with "disabling O2 forces open-loop" above. Possible reconciliation:
|
||||
the three device-enable bytes gate DTC logic only, and the *fueling* behavior
|
||||
change lives in the other bytes the real delete-map diff also touched
|
||||
(`0x5369C`, `0x536AB`) or in the lambda-target curve (`fe[2]`/`0x52260`,
|
||||
`TABLES.md`). Not yet isolated — needs a second independent delete-map diff
|
||||
to separate device flags from fuel/trim bytes.
|
||||
|
||||
100
tunie/research/reference-maps/PERMUTATIONS.md
Normal file
100
tunie/research/reference-maps/PERMUTATIONS.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Full mechanical-odo Bonneville permutation grid — cross-diff (Aug 2026)
|
||||
|
||||
Downloaded and cross-diffed every official mechanical-odo Bonneville
|
||||
calibration TuneECU ships for the exhaust x fuel grid (up to VIN 739050):
|
||||
stock production/aftermarket, Arrow 2-in-1, Arrow 2-in-2, each at E10 and
|
||||
E25, across both catalog batches (`t202`/`t203`). 12 files, all reconstructed
|
||||
to the flat 384 KB ROM with `reconstruct_rom.py` and diffed byte-for-byte.
|
||||
|
||||
IDs used: `20187 20188 20191 20192` (stock), `20262 20263 20264 20265`
|
||||
(`t202` Arrow), `20313 20314 20315 20316` (`t203` Arrow).
|
||||
|
||||
## t202 vs t203: same tune, reissued
|
||||
|
||||
Every `t202`/`t203` pair with an identical catalogue description (e.g.
|
||||
`20262` vs `20313`, both "Arrow 2 in 1, E10") differs by only **10 bytes**,
|
||||
in two groups:
|
||||
|
||||
- 8 bytes at `0x4FFFA-B/E-F` and `0x5FFFA-B/E-F` — right at the `0x50000`/
|
||||
`0x60000` block boundaries, almost certainly per-block ID/checksum
|
||||
metadata (consistent with `DEVICES.md`/`README.md`'s prior finding that
|
||||
the `caXX` footer bytes are map-ID metadata, not a calibration checksum).
|
||||
- 2 bytes at `0x53626-27` — one real 16-bit calibration value changed. Small,
|
||||
single-cell revision.
|
||||
|
||||
**Conclusion: `t202` and `t203` aren't different calibrations, just a
|
||||
catalog-ID reissue with one tiny tweak.** Resolves the earlier open question
|
||||
("which of 20262 vs 20313 is correct for us") — it doesn't matter, use
|
||||
either.
|
||||
|
||||
## Fuel delta (E10 -> E25) is real, consistent, and localized
|
||||
|
||||
Matched pairs (same exhaust, VIN range, everything else equal, only fuel
|
||||
spec differs):
|
||||
|
||||
| Pair | Exhaust | Diff |
|
||||
|---|---|---|
|
||||
| `20262` vs `20263` | Arrow 2-in-1 | 4809 bytes (1.22%) |
|
||||
| `20264` vs `20265` | Arrow 2-in-2 | 4562 bytes (1.16%) |
|
||||
| `20313` vs `20314` | Arrow 2-in-1 (t203) | 4809 bytes — **identical set** to `20262`/`20263` |
|
||||
| `20315` vs `20316` | Arrow 2-in-2 (t203) | 4562 bytes — **identical set** to `20264`/`20265` |
|
||||
| `20187` vs `20191` | stock production | only 60 bytes (0.02%) |
|
||||
|
||||
The E10->E25 delta for Arrow 2-in-1 vs Arrow 2-in-2 overlaps 93% (4455 of
|
||||
4562-4809 bytes) — same fuel-table region touched regardless of exhaust,
|
||||
which is exactly what you'd expect (ethanol compensation lives in the fuel
|
||||
tables, not the exhaust-specific cells). The stock-production E10->E25 delta
|
||||
being tiny (60 bytes) while the Arrow one is ~80x larger is odd at first
|
||||
glance, but `20187` carries no VIN-range/fuel spec in its description at
|
||||
all — it and `20191` likely aren't a clean matched pair the way the Arrow
|
||||
E10/E25 pairs are (`20191` adds a VIN-range qualifier `20187` lacks). Treat
|
||||
`20262`/`20263` as the trustworthy E10-vs-E25 reference, not `20187`/`20191`.
|
||||
|
||||
## Exhaust delta and fuel delta are NOT independent/disjoint
|
||||
|
||||
Checked whether the "stock -> Arrow 2-in-1" delta and the "E10 -> E25" delta
|
||||
touch separate bytes (which would let us compose them by simple
|
||||
copy/overlay):
|
||||
|
||||
- Exhaust delta (`20187` -> `20262`): 7548 bytes
|
||||
- Fuel delta (`20262` -> `20263`): 4809 bytes
|
||||
- **Overlap: 4149 bytes** — the large majority of the fuel delta's footprint
|
||||
is inside the exhaust delta's footprint too.
|
||||
|
||||
**This means naive delta composition (baseline + delta A's changed bytes +
|
||||
delta B's changed bytes) doesn't work cleanly** — both mods rewrite
|
||||
overlapping cells in the same VE/fuel tables (expected: richness-for-exhaust
|
||||
and richness-for-ethanol are both corrections to the same underlying fuel
|
||||
surface, so of course they land on the same cells). You can't just XOR two
|
||||
independent byte-level diffs onto a base map; you'd overwrite one mod's
|
||||
change with the other's rather than combining them. Real composition would
|
||||
need table-level math (read both as VE values, apply both corrections
|
||||
arithmetically), not byte-patching — a materially bigger undertaking than
|
||||
`toggle_devices.py`'s simple flag flip.
|
||||
|
||||
## What this means for "build our own Arrow + SAI/O2-delete tune"
|
||||
|
||||
**Good news:** the exhaust x fuel grid is *already solved* — TuneECU
|
||||
engineers already shipped the exact validated combination you want
|
||||
(`20262`/`20313`, Arrow 2-in-1, E10). No synthesis needed there; picking the
|
||||
catalog entry is strictly better than trying to reconstruct it from deltas.
|
||||
|
||||
**Bad news, unchanged from `COMMUNITY_TUNING.md`:** none of these 12 files
|
||||
vary the SAI/O2-delete axis — TuneECU never shipped an Arrow-exhaust variant
|
||||
with devices off. The grid we now have doesn't help fill that gap, because
|
||||
the gap isn't on this grid. We're still bottlenecked on the single external
|
||||
`2009AIRBOXBONNY.hex` reference (not currently in the repo, needs
|
||||
re-fetching) for that axis, and even that reference conflates "no airbox"
|
||||
with "no SAI/no O2" in one diff (per `DEVICES.md`), which the exhaust-vs-fuel
|
||||
overlap finding above suggests may be a *real* problem, not just a
|
||||
theoretical one — table-level effects don't separate cleanly by simple
|
||||
byte diffing when two mods land on the same cells.
|
||||
|
||||
**Revised recommendation:** don't try to hand-synthesize "Arrow 2-in-1 +
|
||||
real SAI/O2 delete" from byte deltas — the overlap finding shows that's not
|
||||
sound with the tooling we have (byte-diff, not semantic table-diff). Either:
|
||||
1. Decode the fuel tables to actual VE values (`table_map.py`/`TABLES.md`
|
||||
already locate them) and do the composition arithmetically, cell by cell,
|
||||
rather than at the byte level — a real project, not a script tweak.
|
||||
2. Or just use a real matched commercial tune (DNK, since TTP's storefront is
|
||||
effectively defunct) rather than DIY-composing one.
|
||||
@@ -48,6 +48,91 @@ these richer. Exact dimensions still being confirmed.
|
||||
giving ~1.3–60°; the low-RPM row 90→14→20 fits a retard-then-advance curve). TBD.
|
||||
- **AFR** 128 = λ1.00 is solid (standard Keihin convention).
|
||||
|
||||
## Fuel/ignition trim array — RPM-indexed, 32 entries, `0x53690`-`0x536AF`
|
||||
|
||||
**Located and axis-mapped (Aug 2026).** A 32-byte array sitting just before
|
||||
the device-flags region (`0x53801`, `TABLES.md` above / `DEVICES.md`), one
|
||||
byte per **RPM breakpoint on the same 32-point RPM axis** as the main
|
||||
tables (`fe[8]`: 0, 500, 900, 1000, ..., 10000). Not addressed via a `fe[]`
|
||||
pointer for the maps checked (no `fe` field in `c.a[Qd*48]` equals `0x3690`
|
||||
for `20187`'s `Qd=32`) — likely a fixed offset relative to the device-flags
|
||||
struct rather than an independently relocatable table, unconfirmed.
|
||||
|
||||
Evidence for the RPM-axis mapping: diffing `20188Map2009AIRBOXBONNY.hex`
|
||||
(the Bonneville SAI/O2 delete map) against stock `20188` shows **exactly 2
|
||||
of the 32 cells changed — at RPM=2400 and RPM=7000** — with all 30 other
|
||||
RPM-indexed cells byte-identical. That's `0x5369C` (index 12, RPM 2400) and
|
||||
`0x536AB` (index 27, RPM 7000) from earlier sessions, now understood as two
|
||||
specific points on this curve rather than free-floating bytes.
|
||||
|
||||
Cross-checked against `20184dynoTuneSteveO2-Disable.hex` (America,
|
||||
open-pipe + airbox-snorkel-removed + O2-disable, real dyno tune) vs. stock
|
||||
`20186`: **nearly every one of the 32 cells changed**, by large,
|
||||
non-monotonic swings (e.g. RPM=1000: 231→63, RPM=4400: 235→5, RPM=5200:
|
||||
25→204). Not a smooth curve shift — looks like cell-by-cell dyno-session
|
||||
adjustment (consistent with a human tuner nudging individual RPM points
|
||||
after successive pulls) rather than a single global correction. Confirms
|
||||
this is a real, independently-tunable-per-cell array, not a scalar.
|
||||
|
||||
**Identity: confirmed as "Idle Fuel Trim (CO)".** Crawled tuneecu.net's full
|
||||
docs site (`../tuneecu_site_mirror/`) and found the exact match in
|
||||
`TuneECU_En/tests.html`, TuneECU's live-diagnostics parameter list:
|
||||
|
||||
> "Idle Fuel Trim (CO): lets you adjust the fuel richness at idle."
|
||||
> Available for "**Triumph without O²-Sensor only**."
|
||||
> (Counterpart: "Long Term Fuel Trim... Triumph models **with** O²-Sensor
|
||||
> only" — the closed-loop equivalent for bikes that still have the sensor.)
|
||||
|
||||
This is a direct, official match to what we observed empirically: the table
|
||||
is specifically the O2-delete-relevant trim, exactly why it's what the real
|
||||
delete/dyno maps touch and the stock maps don't need to. `co_trim`/`ift_co`
|
||||
string resources (`strings.xml`: "CO Trim" / "Idle Fuel Trim (CO)") back
|
||||
this — "CO" = idle mixture richness, the classic idle-adjustment convention
|
||||
carried over from carbureted-era tuning terminology.
|
||||
|
||||
**Encoding, upgraded confidence (Aug 2026):** found TuneECU's actual "CO
|
||||
Trim" edit dialog (`MainActivity.java`'s `La()`) and its value-scaling
|
||||
logic. Two independent code paths converge on the same answer:
|
||||
|
||||
1. The dialog's clamp/display logic (`j8()`) treats the value as a plain
|
||||
signed range: `if (v < -128) v = -128; else if (v > 127) v = 127;`,
|
||||
displayed as `Integer.toString(v)` — no fractional scaling.
|
||||
2. A separate live-value dispatcher (`n.java`, a large switch handling
|
||||
various PID-style codes) has a case doing manual two's-complement
|
||||
conversion — `if (raw > 127) raw -= 256;` — immediately before storing
|
||||
into the same field (`MainActivity.Mc`) the CO Trim dialog reads.
|
||||
|
||||
**Conclusion: the raw byte is signed two's-complement (-128 to 127), used
|
||||
directly with no multiply/divide scaling factor** — not the unsigned 0-255
|
||||
interpretation this project used everywhere earlier. **This retroactively
|
||||
resolves the "255 looks like a sentinel" puzzle** from the exhaust-family
|
||||
composability analysis (`COMMUNITY_TUNING.md`/`PERMUTATIONS.md`): unsigned
|
||||
`255` is simply signed `-1` — an entirely ordinary, near-neutral value, not
|
||||
a special case. Re-reading the earlier data with correct signs: the stock
|
||||
aftermarket-silencer family sits near `-1` (neutral), production/Arrow
|
||||
sits around `+69` to `+125` (already noticeably rich), and the real
|
||||
SAI/O2 delete map pushes to `+117` — a large, deliberate enrichment,
|
||||
exactly the physically-sensible story this parameter's name would predict.
|
||||
The composability conclusion (can't cleanly merge Arrow's delta with the
|
||||
delete map's delta) still holds — redone with correct signs, the combined
|
||||
correction still saturates past the `+127` clamp — but now for an
|
||||
intuitive reason (the needed correction is genuinely large) instead of an
|
||||
unexplained sentinel.
|
||||
|
||||
**Not fully closed:** the *encoding* (signed int8, no scaling) is now
|
||||
well-supported by two independent sites; the exact real-world *unit* (is
|
||||
`+1` precisely "+1% CO", or a nearby-but-not-identical ECU-internal unit)
|
||||
isn't separately confirmed by either site. Raised from low to medium-high
|
||||
confidence — upgraded, not fully resolved.
|
||||
|
||||
**Practical implication for composing tunes:** don't treat this array as
|
||||
"2 conflicting bytes" the way `compose_arrow_delete.py` originally did —
|
||||
it's a full 32-cell curve. A conservative mod (mufflers only) may touch 2
|
||||
cells; a more aggressive one (open pipes + airbox) touches nearly all 32.
|
||||
Composing two independent tunes' curves here needs the same per-cell
|
||||
arithmetic treatment as the main VE tables (`PERMUTATIONS.md`), not a
|
||||
handful of spot fixes.
|
||||
|
||||
## SAI / O2 / lambda flags
|
||||
|
||||
Now findable via the localised flat-ROM diff (20187 vs 20188), which isolates small
|
||||
|
||||
65
tunie/research/reference-maps/checksum.py
Normal file
65
tunie/research/reference-maps/checksum.py
Normal 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'}")
|
||||
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())
|
||||
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20262-noO2-only.hex
Normal file
BIN
tunie/research/reference-maps/derived/20262-noO2-only.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20262-noSAI-noO2.hex
Normal file
BIN
tunie/research/reference-maps/derived/20262-noSAI-noO2.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20262-noSAI-only.hex
Normal file
BIN
tunie/research/reference-maps/derived/20262-noSAI-only.hex
Normal file
Binary file not shown.
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20313-noO2-only.hex
Normal file
BIN
tunie/research/reference-maps/derived/20313-noO2-only.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20313-noSAI-noO2.hex
Normal file
BIN
tunie/research/reference-maps/derived/20313-noSAI-noO2.hex
Normal file
Binary file not shown.
BIN
tunie/research/reference-maps/derived/20313-noSAI-only.hex
Normal file
BIN
tunie/research/reference-maps/derived/20313-noSAI-only.hex
Normal file
Binary file not shown.
110
tunie/research/reference-maps/toggle_devices.py
Normal file
110
tunie/research/reference-maps/toggle_devices.py
Normal 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())
|
||||
Reference in New Issue
Block a user