The NO-SAI-NO-O2 reference map bundles SAI + O2 + airbox deletes, so it validates the three flag bytes as a GROUP but doesn't isolate O2 individually; no single-mod map exists and the x.a() device-layout branch for this ECU is too nested to trace. - SAI @ 0x53801: confirmed (code lc() -> Devices[0] via fe[33], + the delete map). - O2 @ 0x53818/0x53819: probable (2 O2 sensors declared off <-> 2 adjacent bytes cleared; 865 has no air-flap so airbox removal is fuel-only). Viewer now shows confirmed/probable badges and a warning that flags alone are not a tune: O2 delete forces open-loop and needs fuel enrichment, so prefer flashing a complete matching delete map over hand-toggling a stock one. See DEVICES.md.
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.pyrefuses them before any byte leaves the program. See Safety.
For the full project context, risk register, and pick-up-where-we-left-off notes, read STATUS.md. The seed/key reverse engineering is in research/FINDINGS.md.
Install
cd tunie
python3 -m venv .venv
./.venv/bin/pip install -e .
Requires Python 3.10+ and pyserial (pulled in automatically).
Use
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:
./.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:
- 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). - 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.
- 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/andSTATUS.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.