# 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`.