# tunie Open-source, **read-only** KWP2000 diagnostics for the Triumph Keihin ECU — built for a 2010 Bonneville T100 (865cc, mechanical/analog odometer). The long game is a full open tuning toolchain to replace the closed TuneECU app; this first piece safely reads the ECU without any risk of bricking it. > **This tool cannot write to the ECU.** The flash/write services are not > implemented, and `src/tunie/safety.py` refuses them before any byte leaves the > program. See [Safety](#safety). For the full project context, risk register, and pick-up-where-we-left-off notes, read **[STATUS.md](STATUS.md)**. The seed/key reverse engineering is in **[research/FINDINGS.md](research/FINDINGS.md)**. --- ## Install ```sh cd tunie python3 -m venv .venv ./.venv/bin/pip install -e . ``` Requires Python 3.10+ and `pyserial` (pulled in automatically). ## Use ```sh tunie ports # list serial devices tunie info --port /dev/cu.usbserial-XXXX # interrogate ECU (wired KKL cable) tunie info --port /dev/cu.OBDII --adapter elm327 # or a Bluetooth ELM327 tunie dtc --port /dev/cu.usbserial-XXXX # read fault codes only tunie -v info --port ... # verbose: log every KWP frame # Offline map database (1811 TuneECU maps, already extracted to maps.json): tunie maps Bonneville --ecu 0 --odometer mechanical tunie extract-maps # rebuild it ``` Ignition **on**, engine **off**. If fast init times out, try `--init slow` (5-baud address init). A first `tunie info` returns the ECU's identity, its currently-flashed map ID, and any stored DTCs — that's the immediate goal, and it resolves which stock map is actually on the bike. ## Protocol facts (recovered from the TuneECU APK, not guessed) | Parameter | Value | |---|---| | ECU address | `0xD5` | | Tester address (K-Line) | `0xF5` (**not** 0xF1) | | Format byte | `0x80 \| length` | | Checksum | additive sum mod 256 | | Init | fast (`ATTP5`) or 5-baud (`ATTP4` + `ATIIAD5`) | Verified frames (see `tests/verify_protocol.py`): ``` 81 d5 f5 81 cc StartCommunication 82 d5 f5 1a 80 e6 ReadEcuIdentification 0x80 84 d5 f5 18 00 ff 00 65 ReadDtcByStatus ``` Run the tests: ```sh ./.venv/bin/python tests/verify_protocol.py ``` ## Safety Bricking an ECU over KWP2000 requires reaching the memory-write services, and those sit behind Security Access (`0x27`). `safety.assert_read_only()` is called on **every** outbound request inside `Transport.request()`, before the transport sees it. It allows only query services and refuses all of: `0x11` EcuReset, `0x14` Clear, `0x27` SecurityAccess, `0x2E` Write, `0x34`/`0x36` flash, `0x35` upload, `0x37` — plus every programming diagnostic session. A bug can't send a write because the write path does not exist. **The real remaining risk is electrical, not software:** confirm the Triumph diagnostic connector pinout before plugging any cable in. The 2010 twins do not use a standard OBD-II socket. --- ## Where we left off (2026-08-10) Phase 1 (read-only comms) is **built but not yet run against the bike** — nothing has touched the ECU. Next physical steps, in order: 1. **Identify the wired cable's chip** — `tunie ports`. It's a *VAG* KKL 409.1: right cable class (K-Line), but wired for a VW OBD-II socket, which this bike does not have. FTDI shows as `/dev/cu.usbserial-*`; CH340 as `/dev/cu.wchusbserial*` (needs the CH340 macOS driver). 2. **Confirm the Triumph connector pinout** and build an adapter/re-pin (K-Line, switched +12V, ground) from the cable to the bike's diagnostic connector. *Do not plug in until this is confirmed against a wiring diagram.* 3. **First scan:** ignition on / engine off, `tunie info --port …`. Later phases (all deliberately out of the read-only tool): ROM dump for a recovery image → firmware checksum patching → the write/flash path. ### Big finding: the seed/key is AES-128 The ECU's Security Access algorithm was fully recovered from the TuneECU APK — it's **standard AES-128** (verified against FIPS-197 and the app's own T-table), with three embedded keys selected by an ECU-code index. Not the XOR scheme the original brief predicted. Working reference and details: - `research/keihin_seedkey.py` — pure-Python, self-testing (`python3 research/keihin_seedkey.py`) - `research/FINDINGS.md` — full writeup This is **write-path** material, kept in `research/` and never imported by the `tunie` package. Having the algorithm does not make flashing safe — it's one of several pieces (checksum patching, a verified stock dump, a recovery plan) that all have to line up first. ## Layout ``` tunie/ README.md this file STATUS.md full context, risk register, tuning theory RESEARCH.md original research brief (partly wrong; STATUS corrects it) pyproject.toml maps.json 1811 TuneECU maps, extracted src/tunie/ the read-only tool safety.py read-only allowlist, enforced before transmit kwp2000.py ISO 14230-3 framing / checksums / NRC decoding triumph.py Triumph constants from the APK identify.py the read-only interrogation sweep maps.py map catalogue extraction + search cli.py command-line entry point transport/ kline.py (KKL cable) + elm327.py (Bluetooth) tests/verify_protocol.py research/ WRITE-PATH work, outside the package keihin_seedkey.py AES-128 seed/key reference FINDINGS.md seed/key writeup ``` > The decompiled TuneECU sources used for this research are **not** included here > (third-party app, bulky). Findings and extracted constants are documented in > `research/` and `STATUS.md`. ## Legal Independent interoperability research on a bike I own. TuneECU is a separate third-party product; this repo contains no TuneECU source, only original code and documented findings.