# tunie — full technical context Everything we've reverse-engineered and built for an open-source Triumph Keihin ECU toolchain. This is the pick-it-up-cold reference; if you read one file, read this one. Companion docs: [ROADMAP.md](ROADMAP.md) (open-source + next steps), and the deeper per-topic writeups linked throughout. --- ## 1. The target - **Bike:** 2010 Triumph Bonneville T100, 865cc air-cooled parallel twin, **mechanical/analog odometer** variant. - **ECU:** Keihin, on a Renesas SH7054 (32-bit SuperH RISC). ~384 KB internal flash (corroborated: the app has a 393216-byte code path; npkern lists SH7054). - **Goal:** an open, offline toolchain to read, view, edit, and (eventually) flash the calibration — replacing the closed TuneECU app. - **Owner's end state for the bike:** 2-1 exhaust, airbox removal, O2 delete, SAI delete. This matches the community map `20188Map2009AIRBOXBONNY` almost exactly. ## 2. What exists today | Piece | Path | State | |---|---|---| | Read-only KWP2000 diagnostic tool | `src/tunie/` | built, unit-tested; **not yet run on the bike** | | Map catalogue (1811 maps) | `maps.json` | extracted from the APK | | Seed/key (AES-128) reference | `research/keihin_seedkey.py` | reversed + FIPS-verified | | Map decrypt / unpack / table map | `research/reference-maps/` | reversed + validated on real maps | | Self-contained viewer/editor | `viewer/tunie-viewer.html` | renders real fuel/ignition tables + device toggles | Nothing has electrically touched the ECU yet. Everything is software + RE against downloaded map files. ## 3. KWP2000 diagnostic protocol (read path) Recovered from the TuneECU APK (`m.java`, `MainActivity.java`), validated byte-for-byte in `tests/verify_protocol.py`: - **ECU address `0xD5`**, **tester `0xF5`** on K-Line (NOT the common `0xF1`). CAN-era Triumphs use `0xF1`; K-line uses `0xF5`. - **Framing:** format byte `0x80 | length`; long form (`0x80` + separate length byte) when payload > 120 B; **additive mod-256 checksum**. - **Init:** fast init (`ATTP5`) or 5-baud (`ATTP4` + `ATIIAD5`), address `0xD5`. - **ECU type char:** `0` = Keihin, `1` = Sagem, `P` = Bosch (from `ecu` array). - Reference frames: `81 d5 f5 81 cc` (StartComms), `82 d5 f5 1a 80 e6` (ReadEcuIdentification), `84 d5 f5 18 00 ff 00 65` (ReadDtcByStatus). The tool (`src/tunie/`) is **read-only by construction**: `safety.assert_read_only` runs on every outbound request and refuses all write/flash services (0x27, 0x31, 0x34/0x36, 0x35, 0x37, 0x14, 0x11, 0x2E) and programming sessions. A bug can't brick the ECU because the write path isn't implemented. Details: top-level `README.md`, `STATUS.md`. ## 4. Security Access seed/key — AES-128 `m.java` `Vb()`. **Standard AES-128**, not the Honda XOR scheme the early brief guessed. Verified: all 256 T-table entries match AES Te0; the port passes the FIPS-197 known-answer test. `Vb()` AES-encrypts a 128-bit seed with one of three embedded keys (selected by an ECU-code index) and returns the first ciphertext word as the 4-byte key. Reference + keys: `research/keihin_seedkey.py`, `research/FINDINGS.md`. This is **write-path** material, kept out of the read-only tool. Still open (framing, not crypto): which of the 3 keys for this ECU, and the exact seed-block padding. One captured seed/key pair resolves both. ## 5. Map file format (this was the big reverse) The full chain, all reversed from `l.java`/`MainActivity.java` and validated against real stock maps. See `viewer/FORMAT.md` and `research/reference-maps/README.md`. ### 5a. Download & decryption - Maps download from `https://www.tuneecu.fr/Maps/` (e.g. `Triumph/Bonneville/20187Map.hex`), ~300 KB, **encrypted** (entropy ~7.99). - Decryptor is `l.dc()`: a **self-synchronising CBC-style XOR stream cipher** seeded by the plaintext 4-byte header. Bytes 0–3 stay plaintext (the magic); from byte 4: `plain = prev_cipher ^ cipher ^ ((key>>((i%4)*8))&0xFF)`, where `key = i2 | le32(header)` and `i2` is a constant from `header[3]-24`. Reversed + round-trip-verified in `research/reference-maps/decode_map.py`. - Header magic: `le32(bytes[0:4]) & 0xFF00FFE0 == 0x18001360`, i.e. `byte0 ∈ {0x67,0x68,0x69}` (family), `byte1==0x13`, `byte3==0x18`. ### 5b. Unpack to the flat ROM The decoded map is **packed** — which is why a naive diff of two decoded maps showed 96% difference (misaligned chunks). Each decoded map carries an **unpack directory** (base `le16(dec[28])`, a `0x6F66` marker, a count, then `(dest_offset, length)` entries) that scatters chunks into a **384 KB (0x60000) flat ROM** = the real ECU address space. In the flat ROM, production (20187) vs aftermarket (20188) differ only **1.4%** — the real fuel enrichment. Reversed in `research/reference-maps/reconstruct_rom.py`. ### 5c. Directory lookup → calibration metadata Decoded signature at offset 20 → `s.a` directory record → `Qd = field[1]` → `fe = c.a[Qd*48]` (the 48-int per-calibration metadata that holds all the table pointers). Validated: sig `0x0187CA84` → `s.a` #541 → Qd 32. ## 6. Calibration tables `research/reference-maps/TABLES.md` + `table_map.py`. Base `= (fe[0]&0x2F0)<<12 = 0x50000`; **table offset = base + fe[k]**. Tables are **32 RPM rows × 20 throttle cols, 16-bit big-endian**. | Table | fe | Offset (20187) | Confidence | |---|---|---|---| | Main fuel cyl 1 / 2 | 11 / 12 | 0x56990 / 0x56E90 | high (75/67% aftermarket diff, smooth VE) | | Low-throttle fuel cyl 1 / 2 | 15 / 16 | 0x55590 / 0x55A90 | high | | Fuel base/idle | 9 | 0x55500 | high | | Ignition advance gear 1 / 2–5 / 6 / neutral | 19–22 | 0x587D0 … 0x596D0 | high | | AFR / target lambda | 2 | 0x52260 | medium (128 = λ1.00 confirmed) | Axes: RPM = `fe[8]` (32 breakpoints, real RPM); throttle = `fe[27]` (20, 0–100%×10). **Validated end-to-end:** the main fuel table renders as a clean VE surface, and the aftermarket map is richer in 284/320 cells — physically correct enrichment. **Scaling** (relative changes are exact; absolute units still approximate): fuel values are the ECU's internal VE/fuel-mass units (~0–10800); ignition 13–600 is an internal advance unit; AFR 128 = λ1.00. Exact fuel/ignition constants: TBD (an XDF cross-check or more RE). ## 7. Device-enable flags (SAI / O2) `research/reference-maps/DEVICES.md`. Byte-boolean array at `base + fe[33]` (=`0x53801`), 1=on/0=off. Validated against the real NO-SAI-NO-O2 map `20188Map2009AIRBOXBONNY`, which cleared exactly three bytes: | Offset | Device | Confidence | |---|---|---| | `0x53801` | **SAI** | ~90% (code `lc()` → Devices[0] via fe[33], **and** the delete map) | | `0x53818` / `0x53819` | **O2 sensor 1 / 2** | ~80% (two O2 sensors ↔ two adjacent bytes; not individually isolated) | **Critical caveat:** correct flags ≠ a good tune. Disabling O2 forces open-loop — the bike runs on the base fuel map with no live trim; on a stock map that can run lean/rough. A proper delete pairs flags-off with fuel enrichment (which is why the reference map changed the fuel tables too). **Flash a complete matching delete map, not hand-toggled flags on a stock one.** The owner's target = the AIRBOXBONNY map (SAI/O2 off + airbox fueling done); only the 2-1-vs-2-2 midrange fuel needs fine-tuning. ## 8. Checksums Two distinct ones: - **Table write-back (edit) checksum:** a running 16-bit word, `chk = (chk + old - new) & 0xFFFF`, used when a cell changes. Location is per-calibration. - **Distribution `.hex` integrity:** the `dc` cipher + unpack directory. The `caXX` header/tail bytes are **map-ID metadata, not a cal checksum**, so re-encoding an edit is complete for the file format. - **ECU flash checksum:** applied by the flashing tool at write time — out of scope for the read/edit tool, part of the write path. ## 9. Hardware / cable Owner has a **VAG KKL 409.1** cable (right class — K-Line) and a Bluetooth ELM327. The VAG cable is wired for a VW OBD-II socket; the 2010 Bonneville uses Triumph's proprietary diagnostic connector, so it needs an adapter/re-pin (K-Line + switched 12V + ground). **Confirm the connector pinout before plugging in — that's the one genuine electrical risk.** Identify the chip first (`tunie ports`: FTDI = `/dev/cu.usbserial-*`; CH340 = `/dev/cu.wchusbserial*`). ## 10. The viewer `viewer/tunie-viewer.html` — one self-contained file, no server, no external refs. Tabs: Map catalogue (1811, filterable, "my bike" filter) · Tune capabilities (devices/params/sensors from the APK) · **Triumph tables** (drop a real `.hex` → decode → unpack → render real fuel/ignition tables with axes + A→B diff + device checkboxes) · Raw table viewer. Build with `viewer/build_viewer.py` (`--tuneecu-src` enables the Triumph tab by embedding the `c.a`/`s.a` directory). `download_maps.py` + `serve.py` add a catalogue dropdown of locally cached maps. ## 11. Validation status & confidence (honest) | Finding | Confidence | How validated | |---|---|---| | KWP2000 addressing/framing | high | byte-identical to TuneECU | | Seed/key = AES-128 | high | FIPS-197 + T-table match | | Map decrypt (`dc`) | high | round-trip + readable strings on real maps | | Flat-ROM unpack | high | sibling diff 96%→1.4%, directory lookup resolves | | Fuel/ignition table offsets | high | render as real tables; enrichment physically correct | | Table scaling (absolute units) | low–med | relative exact; absolute TBD | | SAI flag (0x53801) | high (~90%) | code + real delete map | | O2 flags (0x53818/9) | probable (~80%) | group-validated, not isolated | | Anything on the actual bike | **none yet** | no hardware connected | ## 12. What is NOT done - No comms with the real ECU (blocked on the cable adapter/pinout). - No ROM dump from the bike. - No write/flash path (seed/key which-key + padding, flash kernel, ECU checksum). - Editor exports the distribution `.hex` but re-packing an edited **flat ROM** back through the unpack directory + `.hex` for arbitrary edits is only partly wired (device flags work because they live in one chunk). - Absolute fuel/ignition scaling constants. - Individual SAI-vs-O2 isolation and the other device flags (exhaust valve, etc.). ## 13. Key source references (in the decompile) `l.java`: `dc()` decrypt, `zc()`/`Nc()`/`Lb()` map load + tables, `sc()` directory, `lc()` device list. `m.java`: `Vb()` seed/key, KWP framing (`Ld`/`Ub`), `uc()` ROM packer. `MainActivity.java`: `p8()` load pipeline, unpack directory build, `a3`/`W2` init sequences, Maps/ URLs. `c/r/s/t.java`: directory + geometry arrays. `x.java` `a()`: per-ECU device layout (`x.t/x.s/x.q`) — not fully traced.