Files
samplez/tunie/STATUS.md
uhryniuk 4aa5da53d2 Commit tune maps and research so tunes are reachable from the phone
Removes the *.hex/maps_cache gitignore rule (explicit user call, reversing
the earlier no-redistribution stance) so the official TuneECU catalogue
maps, derived SAI/O2-delete composites, and the checksum/composition
tooling are actually available to pull up on a phone browser when using
the real TuneECU app. Also folds in tonight's KWP2000 fixes (TesterPresent
keep-alive, connect-failure cleanup, slow-init StartCommunication fix) and
the accumulated research docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP2GaxS9HkUdL5sLBnjKje
2026-08-27 01:34:05 -05:00

12 KiB
Raw Blame History

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:

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 <arrays.xml>                # 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 <key>).
  • 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.