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:
201
tunie/research/RECOVERY_MODE.md
Normal file
201
tunie/research/RECOVERY_MODE.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# TuneECU's Recovery mode — traced (Aug 2026)
|
||||
|
||||
Started from `menu_recovery` in the decompile per `docs/ROADMAP.md` B2, to
|
||||
understand TuneECU's field-hardened recovery procedure (multi-year,
|
||||
multi-brand bug-fix history — Ducati, Aprilia Dorsoduro/Shiver, Walbro,
|
||||
5DM/7SM ECUs all had recovery-specific fixes 2019-2021, per
|
||||
`strings.xml`'s changelog) before this project ever attempts its own
|
||||
upload/write path (`docs/ROADMAP.md` B2/B4).
|
||||
|
||||
**Priority is Triumph/Keihin recovery specifically, but the trace so far
|
||||
is mostly generic app architecture — the useful cross-brand lessons below
|
||||
are a side effect of that, not a separate investigation.**
|
||||
|
||||
## What "Recovery" actually is: not a separate feature, a relabeled one
|
||||
|
||||
`MainActivity.java:30039`: a single menu item (`R.id.menu_prog`) swaps its
|
||||
own label between "Reprogram" (`menu_program`) and "Recovery"
|
||||
(`menu_recovery`) based on a flag `U9`. **Recovery is the same
|
||||
reprogram/write function as normal flashing** — same code path
|
||||
(`case R.id.menu_prog:` at `MainActivity.java:29129`), gated by the same
|
||||
`U9` condition that also swaps the label. There is no separate "Recovery"
|
||||
KWP2000 sequence sitting in its own function; whatever's different happens
|
||||
inside branches of the ordinary write path once `U9` is true.
|
||||
|
||||
## What decides `U9` — and it's not live ECU status, it's file validation
|
||||
|
||||
Traced `U9`'s assignment (`MainActivity.java:9433`, inside a large
|
||||
response/state-dispatch switch, `case 10`):
|
||||
|
||||
```java
|
||||
if (i8 == 0 && (k7 & 65280) == 2048) { // (k7 & 0xFF00) == 0x0800
|
||||
z11 = true;
|
||||
}
|
||||
U9 = z11;
|
||||
```
|
||||
|
||||
`k7` comes from `com.tuneecu.l.zc(bArrP8)` (`MainActivity.java:11011`),
|
||||
where `bArrP8` is the output of `p8(str, true)` — **`p8()` is the same map
|
||||
*file* decoder this project already reverse-engineered** (it's the
|
||||
function `reconstruct_rom.py`'s docstring already cites for the
|
||||
unpack-directory format). So `k7` isn't a live ECU status code at all —
|
||||
**it's a property of the currently-*loaded map file***.
|
||||
|
||||
Opened `l.zc()` itself (`l.java:6710`) and it's doing work this project's
|
||||
own tooling already replicates:
|
||||
|
||||
- Checks the same reversed magic-header pattern already documented in
|
||||
`reference-maps/README.md` (`bytes[0:4] & 0xFF00FFE0 == 0x18001360`) —
|
||||
returns an error code if it doesn't match.
|
||||
- Runs the **same signature/`Qd` directory lookup** `table_map.py`'s
|
||||
`resolve()` already implements (`sc()` → `s.a` directory → `Qd` →
|
||||
`c.a[Qd*48]` calibration metadata).
|
||||
|
||||
**So the practical meaning of `U9` (show "Recovery" instead of
|
||||
"Reprogram") is: does the currently-open map file decode successfully and
|
||||
resolve to a valid calibration signature, of a type/size in a particular
|
||||
range** (the `0x0800`-high-byte check on whatever `zc()` returns) — not
|
||||
"the app detected the ECU is stuck via a live diagnostic read." This lines
|
||||
up exactly with the community-documented procedure found earlier
|
||||
(`COMMUNITY_TUNING.md` and forum research): *"a matching map must
|
||||
definitely be opened, preferably a matching OEM map"* before recovery
|
||||
works. The map has to be there and valid; the app isn't sensing the ECU's
|
||||
internal fault state, it's checking that you're prepared with a complete
|
||||
known-good image before it'll offer to write one.
|
||||
|
||||
## The generalizable lesson (this is the actual cross-brand takeaway)
|
||||
|
||||
**The design isn't "cleverly detect exactly where a failed transfer left
|
||||
off and resume it." It's "if you have a complete, valid, known-good image,
|
||||
do a full rewrite."** That's a far more robust strategy than resume-logic
|
||||
— no need to reconstruct partial transfer state, no assumptions about
|
||||
where exactly a previous attempt died, just a full verified overwrite.
|
||||
This is the pattern worth carrying into any future `tunie` write-path work
|
||||
(`docs/ROADMAP.md` B4), regardless of ECU brand: **always have a complete,
|
||||
validated base image ready before attempting a write, and treat "recovery"
|
||||
as "do the same full write again," not as a distinct clever-resume
|
||||
code path.**
|
||||
|
||||
**Counter-lesson, equally important:** the changelog's multi-year,
|
||||
per-brand recovery bug list (5DM/7SM, Dorsoduro/Shiver 750, Walbro, Ducati
|
||||
— all separately broken and separately fixed, 2019-2021) shows that even
|
||||
with this simple, robust *design*, the *implementation details differ
|
||||
enough per ECU family that a shared strategy still needed years of
|
||||
per-brand empirical patching* to actually work reliably. "Full rewrite
|
||||
instead of resume" is the right architectural idea to borrow; assuming it
|
||||
works identically across ECU families without validating per-family is
|
||||
exactly the mistake that produced years of TuneECU's own bug list. Applies
|
||||
directly to this project: don't assume whatever works for Keihin
|
||||
generalizes to Sagem/Walbro/Bosch without separately checking each.
|
||||
|
||||
## What happens after the confirmation dialog — traced through to the fork
|
||||
|
||||
Followed dialog id 43's positive-button click all the way through:
|
||||
|
||||
`P9(...20, 3, 43)` → confirm → `s6.onClick()` (`MainActivity.java:6239`) →
|
||||
`H8(true, 43)` → `H8`'s `case 27: case 43:` (`MainActivity.java:9527`)
|
||||
shows a **second** confirmation dialog — the same standard `write_caution`
|
||||
warning a normal first-time reprogram shows, action code 12 → confirm
|
||||
again → `H8`'s `case 12:` (`MainActivity.java:9446`), **the actual fork**:
|
||||
|
||||
```java
|
||||
case 12:
|
||||
if (!U9) {
|
||||
com.tuneecu.m.fg = true; // normal path: flag read by the ordinary write routine
|
||||
} else {
|
||||
com.tuneecu.m.af(); // recovery path
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
`af()` (`m.java:6348`) is short and concrete:
|
||||
|
||||
```java
|
||||
public static void af() {
|
||||
Zf = true; Yf = true; nf = true; Vf = true; // mode flags for the write routine
|
||||
MainActivity.U9 = false; // clear the recovery flag
|
||||
Ue(d.MODE_NULL); // reset the connection state machine
|
||||
Jg.P9(null, null, Ig.ac(c.PLUG_SWITCH), 0, 20, 2, 15); // prompt: cycle the ignition
|
||||
Qe();
|
||||
Ig.Yb(9600, true); // reconnect at 9600 baud, with a control byte (3)
|
||||
}
|
||||
```
|
||||
|
||||
**This independently confirms, from the code, exactly what the community
|
||||
procedure described from experience** (`COMMUNITY_TUNING.md`/forum
|
||||
research): reset the connection, prompt the user to cycle the ignition
|
||||
switch (`PLUG_SWITCH`/`UNPLUG_SWITCH` are literal enum message keys for
|
||||
this), then reconnect — but **at 9600 baud, not the normal K-line rate**.
|
||||
That's a genuinely new, concrete, useful fact this project didn't have
|
||||
before: recovery mode reconnects at a different baud rate, strongly
|
||||
suggesting a **slow/5-baud-style re-init** rather than the normal fast
|
||||
init — which `tunie` already has support for (`--init slow`,
|
||||
`ELM_INIT_SLOW` in `triumph.py`), for the unrelated reason of "fast init
|
||||
timed out." The mechanism recovery leans on may be the same one `tunie`
|
||||
already implements for a different trigger condition.
|
||||
|
||||
**Where the trace stops:** what happens *after* the reconnect completes —
|
||||
the actual KWP2000 frames of the write/upload itself — isn't traced. The
|
||||
`Zf`/`Yf`/`nf`/`Vf` flags `af()` sets are presumably read by the same
|
||||
underlying write routine the normal `fg`-flag path uses, modifying its
|
||||
behavior (e.g. possibly skipping parts of SecurityAccess if a session is
|
||||
assumed already partially open) rather than being a wholly separate write
|
||||
implementation — consistent with the top-level finding that Recovery
|
||||
reuses the normal reprogram machinery rather than duplicating it. Tracing
|
||||
into that shared write routine itself is real further work, not attempted
|
||||
here.
|
||||
|
||||
## Independent real-world confirmation (Aug 2026)
|
||||
|
||||
The file-based `U9` detection this document traced — Recovery only becomes
|
||||
available when a valid map is loaded, not from a live ECU-fault check —
|
||||
is independently confirmed by real users, not just the static code trace.
|
||||
From the "TuneECU For Dummies" thread (triumphrat.net):
|
||||
|
||||
> "you will NEED to have a map opened up in the TuneECU program when you
|
||||
> go to reconnect to the bike or it will NOT initiate the recovery mode...
|
||||
> try to connect, and then click OK when the recovery option is offered."
|
||||
|
||||
Matches the traced mechanism exactly — recovery is gated on a loaded,
|
||||
valid file, confirmed from both directions (static code and real usage).
|
||||
Also from the same source, a disconnect-ordering caution worth carrying
|
||||
into any future `tunie` write-path work: **disconnect via the software
|
||||
menu before turning off ignition**, not the other way around — one user
|
||||
reported turning off ignition first "closes the program mode on the ECU"
|
||||
incorrectly and the bike wouldn't start afterward until sorted out. Not
|
||||
independently traced in the code here, but a real reported failure mode
|
||||
worth respecting.
|
||||
|
||||
## What's still untraced
|
||||
|
||||
- ~~The exact KWP2000 frames sent *during* the write itself~~ — **traced
|
||||
further, see `WRITE_PATH.md`**: the shared routine both paths feed into
|
||||
(`sc()` → `Fc()` → a 5-baud slow-init bit-bang) is now documented there.
|
||||
That trace stops at the post-slow-init handoff (`z.ec(...)`, unopened),
|
||||
which is the next link if this gets picked up again.
|
||||
- Whether there's *also* a live-ECU-side signal (e.g. a specific negative
|
||||
response during `StartCommunication`) that independently indicates a
|
||||
stuck programming session, separate from the file-based `U9` check
|
||||
found here. Plausible — the community procedure's "cycle ignition,
|
||||
reconnect" step suggests the ECU's live response does matter somehow —
|
||||
but not confirmed from what's traced so far. Real next step if this
|
||||
gets picked up again: trace what happens between "ignition cycled,
|
||||
reconnect" and the recovery menu becoming available, since that's where
|
||||
a live-status check would live if one exists.
|
||||
- Byte-level meaning of the `0x0800` high-byte check on `zc()`'s return
|
||||
value — confirmed it's a classification of some kind (map type/size
|
||||
family), not confirmed exactly what distinguishes it from a normal
|
||||
map's classification.
|
||||
|
||||
## Relevance to this project's own plans
|
||||
|
||||
- **Near-term** (`TUNING_IMPL_PLAN.md` step 2): unaffected — still use
|
||||
TuneECU's app for the actual ROM dump, this doesn't change that.
|
||||
- **Longer-term** (`docs/ROADMAP.md` B2/B4, if `tunie` ever implements its
|
||||
own upload/write path): the "full rewrite over clever resume" pattern
|
||||
found here is directly actionable design guidance, and cheap to adopt —
|
||||
it's simpler to implement than resume logic would have been anyway.
|
||||
The per-brand-patching counter-lesson argues for validating any write
|
||||
path thoroughly against Keihin specifically before assuming it's solved
|
||||
in general, exactly matching this project's existing "get a spare ECU
|
||||
first" caution in `safety.py`.
|
||||
Reference in New Issue
Block a user