Add tunie: read-only KWP2000 diagnostics for Triumph Keihin ECU
A Python tool to safely read the Keihin ECU on a 2010 Bonneville T100 over K-Line (KKL cable) or a Bluetooth ELM327, plus the reverse-engineering research behind it. Phase 1 (read-only comms) of an open tuning toolchain to replace the closed TuneECU app. Read-only by construction: safety.assert_read_only() runs on every outbound request before it hits the wire and refuses all write/flash services (0x27, 0x31, 0x34/0x36, 0x35, 0x37, 0x14, 0x11, 0x2E) and programming sessions, so a bug cannot brick the ECU. Verified frames match TuneECU byte-for-byte in tests/verify_protocol.py. Protocol constants recovered from the TuneECU APK (not guessed): ECU address 0xD5, K-Line tester 0xF5, format byte 0x80|len, additive mod-256 checksum. Includes the full TuneECU map catalogue (1811 entries) extracted to maps.json, searchable and filterable by ECU type and mechanical-vs-LCD odometer. research/ documents the Security Access seed/key algorithm, recovered as standard AES-128 (three embedded keys), with a self-testing reference impl verified against FIPS-197. This is write-path material, kept outside the read-only package. STATUS.md and README.md capture full context, the risk register, and where we left off: comms built but not yet run against the bike; next step is wiring the VAG KKL cable to the Triumph connector and running the first scan.
This commit is contained in:
151
tunie/README.md
Normal file
151
tunie/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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 <path/to/apktool_out/res/values/arrays.xml> # 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.
|
||||
Reference in New Issue
Block a user