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:
2026-08-27 01:34:05 -05:00
parent 587f75eab4
commit 4aa5da53d2
98 changed files with 4148 additions and 49 deletions

View File

@@ -0,0 +1,219 @@
# 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.