# Triumph Bonneville tuning project — status & context **Bike:** 2010 Triumph Bonneville T100, 865cc air-cooled parallel twin, Keihin ECU (Renesas SH7054), **mechanical/analog odometer**. **Goal:** an open-source Python toolchain to read and (eventually) tune the ECU, replacing the closed TuneECU app. Ultimate hardware path: SAI removal, O2 delete, airbox removal, full exhaust — each needs a matching recalibration. **Last updated:** 2026-08-10. Not getting to actual tuning this week — this doc is the cold-start reference to pick it back up. --- ## TL;DR — where we are - **Phase 1 (read-only comms) is built, not yet run against the bike.** Nothing has touched the ECU. No hardware connected yet. - **Next real step:** wire the cable to the Triumph connector correctly, then run `tunie info` to read the ECU's identity, current map ID, and fault codes. - **Blocker:** the wired cable is a *VAG* KKL — right cable type, wrong plug for this bike. Needs an adapter/re-pin to the Triumph diagnostic connector. This is the one genuine (electrical) risk and must be confirmed against a wiring diagram before plugging in. - **Big win:** the ECU's security-access seed/key algorithm was fully recovered from the TuneECU APK — it's **AES-128** with three embedded keys. Documented, reference-implemented, and verified. This is write-path work, quarantined outside the read-only tool. --- ## Directory map ``` /Users/dylan/dojo/tuner/ tunie/ the read-only Python tool (installed, working) src/tunie/ package source tests/ protocol verification (passes) maps.json 1811 extracted TuneECU maps README.md tool-level docs research/ WRITE-PATH reverse engineering (kept OUT of the tool) keihin_seedkey.py AES-128 seed/key reference impl (self-testing) FINDINGS.md seed/key writeup work/ decompiled TuneECU jadx_out/ Java decompile (read-only, best for reading logic) apktool_out/ smali decompile (rebuildable) TuneECU.apk original TuneECU-logging.apk a clean rebuild (no real instrumentation yet) samplez/ pristine original TuneECU.apk (git repo) STATUS.md this file RESEARCH.md the original (LLM-written, partly wrong) research brief ``` --- ## What has been built ### `tunie` — read-only KWP2000 diagnostic tool Installed and working. Commands: ```sh tunie ports # list serial devices tunie info --port /dev/cu.usbserial-XXXX # interrogate ECU (KKL cable) tunie info --port /dev/cu.OBDII --adapter elm327 tunie dtc --port ... # fault codes only tunie maps Bonneville --ecu 0 --odometer mechanical tunie extract-maps # rebuild maps.json tunie -v info --port ... # verbose: log every frame ``` Supports both a wired K-Line KKL cable (`--adapter kline`) and a Bluetooth ELM327 (`--adapter elm327`) behind one interface. **Read-only by construction.** `src/tunie/safety.py::assert_read_only()` runs on every outbound request inside `Transport.request()`, before any byte reaches the serial port. It allows only an allowlist of query services and refuses all write/flash services (`0x27` SecurityAccess, `0x31` erase, `0x34`/`0x36` write, `0x35` upload, `0x37`, `0x14`, `0x11`, `0x2E`) and all programming diagnostic sessions. A bug cannot brick the ECU because the write path is not implemented. Module layout: ``` safety.py read-only allowlist, enforced before transmit kwp2000.py ISO 14230-3 framing, checksums, negative-response decoding triumph.py Triumph constants recovered from the APK identify.py the read-only interrogation sweep maps.py TuneECU map catalogue extraction + search cli.py command-line entry point transport/ base.py Transport ABC; routes every request through safety kline.py FTDI/KKL cable, raw serial, break-condition fast init elm327.py ELM327 adapter, using TuneECU's own AT init sequence ``` `tests/verify_protocol.py` passes: it asserts our frames are byte-identical to TuneECU's and that every write service / programming session is blocked. --- ## What was learned (all recovered from the APK, not guessed) ### Protocol facts (source: `com/tuneecu/m.java` + `MainActivity.java`) | Parameter | Value | Notes | |---|---|---| | ECU address | `0xD5` | from `Ld()` framing + `ATSH81D5F5` | | Tester address (K-Line) | `0xF5` | **not** 0xF1 — that's another brand | | Format byte | `0x80 \| length` | long form (0x80 + length byte) when >120 B | | Checksum | additive sum mod 256 | `Ub()` in m.java | | Init | fast (`ATTP5`) or 5-baud (`ATTP4`+`ATIIAD5`) | address 0xD5 | | ECU type code | `0`=Keihin, `1`=Sagem, `P`=Bosch | `ecu` string-array | Reference frames (verified in tests): ``` 81 d5 f5 81 cc StartCommunication 82 d5 f5 1a 80 e6 ReadEcuIdentification 0x80 84 d5 f5 18 00 ff 00 65 ReadDtcByStatus ``` > RESEARCH.md guessed target `0x10` / source `0xF1` and a 16-pin OBD-II port. > Both wrong for this bike. ### Map database Extracted TuneECU's full catalogue: **1811 entries** in `tunie/maps.json`. TuneECU's own database distinguishes **"Mechanical odometer"** from **"LCD odometer"** Bonnevilles — they are not interchangeable, and this is the ECU generation split. For a 2010 mechanical-odo 865: - **Stock baselines (`:0:` Keihin, mechanical odo):** - `20187` production silencers, `20188` aftermarket silencers - `20191`/`20192` same, up to VIN 739050, E25 fuel - **Exhaust maps:** `20262`–`20265`, `20313`–`20316` (Arrow 2-in-1 / 2-in-2) > RESEARCH.md recommended `20498` as an "OEM Arrow, safe rich baseline." It is > actually a **Thruxton, LCD-odometer** map — wrong model AND wrong ECU > generation. Do not use it. ### Security Access seed/key — RECOVERED, it's AES-128 Source: `com/tuneecu/m.java` method `Vb()`. Full writeup in `research/FINDINGS.md`; working reference in `research/keihin_seedkey.py` (`python3 research/keihin_seedkey.py` → all self-tests pass). - The algorithm is **standard AES-128** (verified: all 256 T-table entries match AES Te0; reference passes the FIPS-197 known-answer vector). It is **not** the Honda XOR/bit-shift scheme RESEARCH.md predicted. - `Vb()` AES-encrypts a 128-bit seed block with one of **three** embedded keys (`iArr4`, m.java:5351), selected by `MainActivity.h7 ∈ {0,1,2}`, and returns the first ciphertext word as the 4-byte key (`Gd()` sends `27 02 `). - The three keys (little-endian): ``` h7=0: ef704ca051b800cc9287df6a3511a978 h7=1: d4b15ff4c92ab7f098316e7a5b11ac39 h7=2: dc9fdba46f2fad18a4b8e1123c7183c2 ``` - **Still open (framing, not crypto):** which h7 applies to the mechanical-odo 865, and the exact seed-block padding (`Ie[1..3]` from the response + fixed `0x03`/`0x01`; `Ie[0]` set on an earlier path). **One captured (seed, key) pair resolves both.** ### The logging APK `work/TuneECU-logging.apk` is a clean apktool rebuild of the original (~17 KB delta = re-signing/recompression). No custom instrumentation; all `Log` calls are TuneECU's own, gated behind its debug boolean; no `debuggable` flag. It does not currently capture seed/key at runtime — but it's the right vehicle for the validation step (see next steps). --- ## The cable (VAG KKL) — what to do A "VAG KKL" 409.1 cable is the **correct cable class** (K-Line). Confirmed in hand and working: `/dev/cu.usbserial-A50285BI`, genuine FTDI FT232R, no driver needed. **Step 1 — identify the chip.** `tunie ports`: - `/dev/cu.usbserial-XXXX` → FTDI, ideal, no driver. **(this is what we have)** - `/dev/cu.wchusbserialXXXX` → CH340; works but install the CH340 macOS driver. - nothing → driver missing. **Step 2 — the connector, revised (Aug 2026).** Originally assumed this bike uses a fully proprietary, non-OBD-II connector requiring a hand-built adapter. **Real-world evidence from actual TuneECU users on this exact ECU family says otherwise** — multiple independent accounts (triumphrat.net, "TuneECU For Dummies" thread) describe a standard **16-pin OBD-II-style plug** that a genuine-FTDI cable connects to directly, no re-pinning: > "The KL-1 interface has a 16 pin plug (OBD2 style) that will plug > directly into Triumph models." — "The cable connects to the dedicated > 16-pin diagnostic plug of the ECM." — a real install log on a **2010 > Speed Triple 1050 (same-era Keihin ECU family)**: *"Remove your seat and > the ECU cable connector is just sitting in front of the fusebox (behind > the tank). You don't need any tools."* **New, previously-undocumented, important step from the same source:** **pull the headlight AND taillight fuses before connecting** — both to protect the battery ("Keihin ECUs don't like a voltage drop") and as a built-in interlock (the bike won't start with the headlight fuse pulled). Fuse numbers are bike-specific (the Speed Triple account used #8; check this bike's own fusebox cover, which has the numbering printed on it) — don't assume the same number without checking. **Still not 100% settled** — this is strong, multi-source evidence, not a verified fact for this specific 2010 T100 mechanical-odo bike (the primary sources are Speed Triple owners, a related but different model in the same ECU family). **Before plugging in: visually confirm the connector really is 16-pin OBD-II-shaped** at the location described (front of fusebox, behind tank) — if it matches, plug in directly; if it looks different, the re-pinning caution below still applies. > **Confirm the connector shape and pull the relevant fuses before plugging > in.** If it turns out not to be OBD-II shaped after all, do not force a > connection — that's when the re-pinning risk below is real. Wrong pins > damage the transceiver. --- ## Next steps (in order) 1. **Identify the cable chip** — `tunie ports`, install CH340 driver if needed. 2. **Confirm the Triumph diagnostic connector pinout** — service manual / wiring diagram. Build the adapter (K-Line, switched +12V, ground). 3. **First scan (safe):** ignition on / engine off, `tunie info --port …`. Yields ECU ID, currently-flashed map ID, DTCs. If fast init times out, try `--init slow`. This resolves which stock map is actually on the bike. 4. *(Later, Phase 2)* ROM dump for a recovery image — requires deliberately enabling `0x35` upload; blocked in the read-only tool by design. 5. *(Later, Phase 3)* Checksum patching — not yet investigated; needed before any modified map can boot. 6. *(Later, Phase 4)* Write path — validate the AES seed/key against a captured pair first; ideally against a spare ECU, not the bike's only one. ### Optional now: capture a real seed/key pair Write a minimal smali patch to `work/apktool_out` that logs every KWP frame (patch the BT send/receive in `d.smali`) to logcat, rebuild, run one real TuneECU Security Access against the bike, and capture the `27 01` seed + `27 02` key. Then check `compute_key(seed, h7)` reproduces it — validates the whole AES port and pins down h7 + padding. --- ## Risk register | Area | Status | |---|---| | Software bricking | **Eliminated** — write services structurally unreachable in `tunie`. | | Electrical / wiring | **LIVE** — VAG cable ≠ Triumph connector; confirm pinout before plugging in. | | Seed/key correctness | Algorithm recovered + AES-verified; which-key + padding need one captured pair. Phase 4 only. | | Firmware checksum | Not yet investigated. Needed before flashing a modified map. Phase 3. | | Single ECU, no spare | Read-only-first mandatory; validate any write path against TuneECU before trusting ours. | --- ## Tuning theory (from RESEARCH.md §9 — this part is sound) - **SAI removal:** flip the software SAI flag or the ECU throws a DTC; SAI air also corrupts AFR readings on a dyno, so disable it before any fuel tuning. - **O2 delete / open loop:** removing narrowband O2 + disabling closed-loop lets you command a richer light-load AFR (~13.5–13.8:1 vs 14.7) — cools the air-cooled top end and fixes the snatchy off-idle response. - **Airbox removal & full exhaust:** both raise cylinder fill (VE / scavenging); fuel tables must be enriched or it runs lean under load. Community airbox maps derived from `20188` provide pre-calculated enrichment. XDF definition files (to edit the .bin in TunerPro) are sold by OldSkullTuning and Tuniverse (~€70) for the SH7054 — a later purchase, only once we're editing. ```