Files
samplez/tunie
uhryniuk 173f6fa0c4 Locate Keihin tables + render real fuel maps in the viewer
Reversed the inner map format from l.java (Nc/Lb) and validated it against the
stock reference maps:

- reconstruct_rom.py: decoded map -> unpack directory -> 384KB flat ROM. In the
  flat ROM, production (20187) vs aftermarket (20188) differ only 1.4% (vs 96%
  packed), i.e. real fuel enrichment.
- table_map.py + TABLES.md: fe = c.a[Qd*48]; base = (fe[0]&0x2F0)<<12 = 0x50000;
  table = base + fe[k]. Located 9 tables (main/low-throttle fuel per cylinder,
  base/idle fuel, ignition by gear x4), 32 RPM rows x 20 throttle cols, with real
  axes (RPM fe[8], throttle fe[27]) and the AFR curve (fe[2], 128=lambda 1.00).
  Validated: main-fuel delta is uniformly richer in the aftermarket map.

Viewer now has a "Triumph tables" tab: drop a real .hex (or pick two) and it
decodes, unpacks, and renders the fuel/ignition tables as heatmaps with real
axes, plus an A->B difference view. build_viewer.py embeds the c.a/s.a directory
so any map resolves in-browser.

Catalogue dropdown: download_maps.py fetches map .hex files into maps_cache/;
serve.py serves the viewer over http so the dropdown can fetch them (drag-and-drop
still works on file://).

Proprietary map binaries (*.hex, *.dec.bin, maps_cache/) are gitignored — code and
docs only.
2026-08-11 07:00:19 -05:00
..

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.

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:

  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.

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.