# The write/reprogram protocol — traced from the shared routine (Aug 2026) Started by following the mode flags `af()` sets (`RECOVERY_MODE.md`) into the actual write routine both normal reprogram and recovery share. This turned into real protocol-level detail for `docs/ROADMAP.md` B4 (the future write/flash path) — this is that research, started early because the trace led here naturally, not because B4 is starting now. **Nothing here changes `tunie`'s current posture.** `safety.py` still refuses every service traced below. This is knowledge-gathering for when that changes deliberately, per `safety.py`'s own standing instruction. ## The chain, start to finish `fg = true` (set by `H8`'s `case 12:`, `RECOVERY_MODE.md`) is polled by several per-response-tick handlers in `m.java` (found at lines 2130, 3436, 5682, 10274 — all structurally identical: `if (fg) { sc(false); }` in place of the normal `se()` live-dashboard poll). So **entering write mode simply means: stop polling live sensor data, start driving the write sequence instead** — same tick loop, different branch, not a separate scheduler. ### `sc(boolean z)` (`m.java:9330`) — policy gate before any wire activity - Walbro ECUs (`nf`) branch off entirely to `MODE_WALBRO_BREAK` — a different family, different sequence, not traced here. - **License/authorization check**: compares `MainActivity.Zb` (loaded map's associated identifier) against `MainActivity.Lb`; mismatch (with `i7 > 0`) aborts with `UNAUTHORIZED_MAP` and resets to `MODE_NULL`. This is TuneECU's commercial licensing gate (paid maps tied to a registered bike) — not a protocol safety check, irrelevant to `tunie` (which has no such licensing model), but worth knowing it exists so it's not mistaken for something protocol-relevant if this code is read again later. - The `Of`-family branch (recall `Of = Rf | yf | wf | xf`, some ECU-family flag combination) does a `k7` classification check, then just resets to `MODE_NULL` and returns — **does not proceed to a write at all** via this function. Unclear whether `Of` families use a completely different write path elsewhere, or whether this is a genuine "not supported this way" bail. Not traced further. - Otherwise: if not `G5`/`H5` (a device-generation flag pair — plausibly CAN-era vs. K-line-era Triumphs, unconfirmed), calls **`Fc(213)`** — `213` = `0xD5`, **the Triumph K-line ECU address `triumph.py` already documents**. If `G5`/`H5`, takes a different path (`Jg.D8(false)`, not traced) — plausibly the CAN-bus-generation equivalent. ### `Fc(int address)` (`m.java:1271`) — connection/retry driver - Resets a batch of protocol-state flags, **sets baud to 10400** (`Ig.Yb(10400, true)`) — the normal K-line fast-init rate, matching `tunie`'s own `kline.py` default. - Retry counter `Ed`: gives up after 5 attempts with one of three error dialogs (`ERR_FAILED` / `ERR_BAD_DEVICE` / `ERR_NO_ECU`) depending on which flags are set, and resets `z.Td = 0`. - Computes a UI status code (`i3`: 13 for address `0x43`, 9 for `0xD5`, 8 otherwise) shown via `MainActivity.Hb`, then **spins up a background thread**: `new Thread(new a(address)).start()` — everything past this point runs off the UI thread. ### `a` / `RunnableC0059a` (`m.java:273`) — the actual 5-baud slow-init bit-bang This is the concrete protocol-level payoff of the trace. After a 100ms sleep, it runs (on the UI thread, via `runOnUiThread`): ```java int i2 = (address * 4) + 1025; int i3 = 0; byte b = 0; while (i3 < 11) { SystemClock.sleep(200L); byte b2 = (byte) (i2 & 1); if (b2 != b) { z.Zb(b2); // toggles the K-line } i2 /= 2; i3++; b = b2; } z.Wd = 25; z.ec(null, 3, false, false); // not traced further ``` **This is a standard ISO 14230 5-baud slow-init address transmission**, bit-banged in software: 200ms per bit = exactly 5 bps, 11 iterations covering a start bit + address byte + stop bit framing, encoded as `(address * 4) + 1025` (for `0xD5`: `1877`) and shifted out LSB-first via `z.Zb()` toggling the line directly. This is the same class of sequence `tunie`'s `ELM_INIT_SLOW`/`--init slow` already implements for a different trigger (fast-init timeout) — worth comparing bit-for-bit against `kline.py`'s existing slow-init once this gets picked up seriously, since they may already be equivalent or may reveal a discrepancy worth fixing. ## `z.ec()` traced — bottoms out in raw FTDI USB control transfers `z.ec(null, 3, false, false)` (`z.java:1427`) — with `bArr=null`, the only action is `Bd.v((byte)1)`. `Bd` (type `f.c.a.d`, `f.c.a` package) is **TuneECU's own hand-rolled FTDI driver, talking directly to the Android USB Host API** (`UsbDeviceConnection.controlTransfer` etc. — no external SDK, they wrote their own FTDI D2XX-equivalent). Traced `v()` → `w(boolean, boolean)`: ```java private boolean w(boolean z, boolean z2) { if (z) { for (int i = 0; i < 6; i++) { controlTransfer(0x40, 0, 1, ...); // FTDI SIO_RESET, purge RX -- x6 } ... } return z2 && controlTransfer(0x40, 0, 2, ...) == 0; // purge TX } ``` `0x40`/`bRequest=0`/`wValue=1|2` is **FTDI's standard SIO_RESET_REQUEST** with the documented RX-purge/TX-purge sub-codes (matches FTDI's own AN232B-04 application note). `v((byte)1)` (our call) purges RX only, repeated 6× (defensive retry against a known-flaky USB reset); recovery's earlier `v((byte)3)` purges both RX and TX. **So this step isn't KWP2000 protocol logic at all — it's low-level USB hygiene**: clear out whatever garbage may have accumulated on the wire during the slow-init bit-bang before starting to listen for the ECU's response. **Bonus, while in `f.c.a.d`:** found `A(byte, byte)` (`controlTransfer(0x40, 11, ...)`) — `bRequest=11` is **FTDI's SIO_SET_BITMODE_REQUEST**, i.e. FTDI bit-bang mode. This is almost certainly what `z.Zb()` (the line-toggle call inside the 5-baud bit-bang loop in `WRITE_PATH.md`'s earlier section) actually calls into — standard technique for software 5-baud init on FTDI hardware, since normal UART framing can't produce an arbitrary custom baud rate for just the wake-up byte, so the driver drops into raw GPIO bit-bang mode to toggle TXD by hand at the required timing. Worth checking whether `tunie`'s existing `kline.py` slow-init already does the equivalent, or uses a different technique (e.g. relying on the OS UART driver's own 5-baud support if the FTDI chip/driver combo permits it directly). ## The response handler — found it, and the loop closes cleanly Found what reads the response: `z.java`'s inner `Handler` class `a` (`z.java:57`, the standard Android USB-receive-callback pattern) — `handleMessage()` dispatches on a message type: `0` resets the `he` timestamp and calls `Sb()` (receive setup), `1` is the actual data-received case, logging the bytes then (for non-Walbro ECUs) calling **`m.Uc(ke, Nd, Ld, Zd)`** — the core frame parser. **`Uc()` (`m.java:4670`) is the ISO14230/KWP2000 response dispatcher, and its very first branch is exactly the slow-init handshake this trace has been building toward:** ```java if (Cd == MODE_NULL) { // via the ordinal lookup table if (bArr[i] == 0x55) { // ISO14230 sync byte int kb1 = bArr[i+1]; if (kb1 == 0x08 || kb1 == 0xD9) { // recognized key byte 1 values // ...reset ~20 session/ECU-family flags to false... Fd = bArr[i+2] ^ 0xFF; // complement of key byte 2 Ue(d.MODE_INIT); // advance the state machine } } } ``` This is the textbook ISO 14230-2 slow-init handshake, confirmed end-to-end: the ECU replies to the 5-baud wake-up with **sync (`0x55`) + key byte 1 + key byte 2**; the tester's required response is the **bitwise complement of key byte 2**, computed right here (`Fd = kb2 ^ 0xFF`) and presumably transmitted in whatever `MODE_INIT` does next (not opened — natural following link, but the interesting part — recognizing and correctly answering the handshake — is now confirmed). Full session flag reset happens at the same moment, consistent with this being a clean restart of connection state. **`Uc()` is also the general-purpose ongoing frame dispatcher, not just the wake-up handler** — later branches (`Cd` in other modes) pattern-match various header byte sequences (e.g. `0x18 0xDA 0xF1...`, the CAN/ISO15765 extended-addressing header for the newer CAN-based Triumphs `triumph.py` already notes) and route to a wide array of specific handler functions (`Ic`, `Mc`, `Hc`, `Xe`, `bc`, `cc`, `Gc`, `Kc`, ...) — meaning **one function handles both the K-line (`0xD5`/`0xF5`) and CAN-era (`0x18DAxx`) Triumph protocols** in a single cascade, differentiated purely by header pattern. ## Where the trace stops The full initial connection sequence is now traced end-to-end: mode-flag entry → policy gate → connection/retry driver → 5-baud bit-bang wake-up (FTDI bit-bang mode) → USB buffer purge (FTDI reset/purge) → Handler receives bytes → `Uc()` validates the sync+key-byte response and computes the required handshake reply → state machine advances to `MODE_INIT`. What `MODE_INIT` does with that computed complement byte (send it back, await the ECU's own complement-of-tester-address reply, per standard ISO14230 slow init) is the natural next thread, not yet pulled. **This closes the loop this trace set out to close** — from a UI menu click down to the literal handshake math — everything from here forward is deeper (session establishment past the handshake, then SecurityAccess, then the actual transfer) rather than a missing link in what's already traced. ## Why this matters beyond Triumph (cross-pollination, per the original ask) The `Of`-family bailout in `sc()` and the `nf` (Walbro)/`G5`/`H5` branches all being *different code paths from the same entry point* reinforces the same lesson `RECOVERY_MODE.md` already drew from the changelog: **the high-level architecture (tick-loop flag-polling, shared write driver, retry-with-backoff, slow-init wake-up) is generic across ECU brands/families, but the specific sequence diverges by family at multiple points along the way** (Walbro splits off immediately; `Of` families don't even reach the wire; `G5`/`H5` skip the slow-init path entirely). A future `tunie` write path built by generalizing from this trace should expect to need Keihin-specific validation at each of those fork points, not assume the Triumph/Keihin branch generalizes to other ECU families it hasn't been checked against — same caution as before, now with concrete fork points identified instead of just "expect divergence somewhere." ## Status Real protocol detail recovered, genuinely useful for `docs/ROADMAP.md` B4 when that work starts for real. Still gated on the same prerequisites `ROADMAP.md` already lists: validate the AES seed/key against a captured real pair, and do this against a spare ECU before the bike's only one. Nothing in this document changes that — it's ahead-of-time reconnaissance, not a green light.