Files
samplez/tunie/docs/CONTEXT.md
uhryniuk 4debdfa518 Add tunie/docs (full context + open-source roadmap); refresh README
docs/CONTEXT.md captures the entire reverse-engineering effort in one place:
KWP2000 protocol, AES-128 seed/key, the map format (dc decrypt -> flat-ROM
unpack -> directory -> fe table pointers), the fuel/ignition table map, the
validated SAI/O2 device flags, checksums, hardware, and a per-finding confidence
table.

docs/ROADMAP.md covers open-sourcing (legal/IP posture on proprietary maps and
the AES keys, repo hygiene, packaging/CI, community) and the technical path to
tuning the bike (first contact -> ROM dump -> calibration model -> write path ->
editor), with immediate next steps and a risk register.

Top-level README refreshed to describe the whole project (tool + viewer +
research) and point at docs/.
2026-08-11 08:21:16 -05:00

10 KiB
Raw Permalink Blame History

tunie — full technical context

Everything we've reverse-engineered and built for an open-source Triumph Keihin ECU toolchain. This is the pick-it-up-cold reference; if you read one file, read this one. Companion docs: ROADMAP.md (open-source + next steps), and the deeper per-topic writeups linked throughout.


1. The target

  • Bike: 2010 Triumph Bonneville T100, 865cc air-cooled parallel twin, mechanical/analog odometer variant.
  • ECU: Keihin, on a Renesas SH7054 (32-bit SuperH RISC). ~384 KB internal flash (corroborated: the app has a 393216-byte code path; npkern lists SH7054).
  • Goal: an open, offline toolchain to read, view, edit, and (eventually) flash the calibration — replacing the closed TuneECU app.
  • Owner's end state for the bike: 2-1 exhaust, airbox removal, O2 delete, SAI delete. This matches the community map 20188Map2009AIRBOXBONNY almost exactly.

2. What exists today

Piece Path State
Read-only KWP2000 diagnostic tool src/tunie/ built, unit-tested; not yet run on the bike
Map catalogue (1811 maps) maps.json extracted from the APK
Seed/key (AES-128) reference research/keihin_seedkey.py reversed + FIPS-verified
Map decrypt / unpack / table map research/reference-maps/ reversed + validated on real maps
Self-contained viewer/editor viewer/tunie-viewer.html renders real fuel/ignition tables + device toggles

Nothing has electrically touched the ECU yet. Everything is software + RE against downloaded map files.

3. KWP2000 diagnostic protocol (read path)

Recovered from the TuneECU APK (m.java, MainActivity.java), validated byte-for-byte in tests/verify_protocol.py:

  • ECU address 0xD5, tester 0xF5 on K-Line (NOT the common 0xF1). CAN-era Triumphs use 0xF1; K-line uses 0xF5.
  • Framing: format byte 0x80 | length; long form (0x80 + separate length byte) when payload > 120 B; additive mod-256 checksum.
  • Init: fast init (ATTP5) or 5-baud (ATTP4 + ATIIAD5), address 0xD5.
  • ECU type char: 0 = Keihin, 1 = Sagem, P = Bosch (from ecu array).
  • Reference frames: 81 d5 f5 81 cc (StartComms), 82 d5 f5 1a 80 e6 (ReadEcuIdentification), 84 d5 f5 18 00 ff 00 65 (ReadDtcByStatus).

The tool (src/tunie/) is read-only by construction: safety.assert_read_only runs on every outbound request and refuses all write/flash services (0x27, 0x31, 0x34/0x36, 0x35, 0x37, 0x14, 0x11, 0x2E) and programming sessions. A bug can't brick the ECU because the write path isn't implemented. Details: top-level README.md, STATUS.md.

4. Security Access seed/key — AES-128

m.java Vb(). Standard AES-128, not the Honda XOR scheme the early brief guessed. Verified: all 256 T-table entries match AES Te0; the port passes the FIPS-197 known-answer test. Vb() AES-encrypts a 128-bit seed with one of three embedded keys (selected by an ECU-code index) and returns the first ciphertext word as the 4-byte key. Reference + keys: research/keihin_seedkey.py, research/FINDINGS.md. This is write-path material, kept out of the read-only tool.

Still open (framing, not crypto): which of the 3 keys for this ECU, and the exact seed-block padding. One captured seed/key pair resolves both.

5. Map file format (this was the big reverse)

The full chain, all reversed from l.java/MainActivity.java and validated against real stock maps. See viewer/FORMAT.md and research/reference-maps/README.md.

5a. Download & decryption

  • Maps download from https://www.tuneecu.fr/Maps/<path> (e.g. Triumph/Bonneville/20187Map.hex), ~300 KB, encrypted (entropy ~7.99).
  • Decryptor is l.dc(): a self-synchronising CBC-style XOR stream cipher seeded by the plaintext 4-byte header. Bytes 0–3 stay plaintext (the magic); from byte 4: plain = prev_cipher ^ cipher ^ ((key>>((i%4)*8))&0xFF), where key = i2 | le32(header) and i2 is a constant from header[3]-24. Reversed + round-trip-verified in research/reference-maps/decode_map.py.
  • Header magic: le32(bytes[0:4]) & 0xFF00FFE0 == 0x18001360, i.e. byte0 ∈ {0x67,0x68,0x69} (family), byte1==0x13, byte3==0x18.

5b. Unpack to the flat ROM

The decoded map is packed — which is why a naive diff of two decoded maps showed 96% difference (misaligned chunks). Each decoded map carries an unpack directory (base le16(dec[28]), a 0x6F66 marker, a count, then (dest_offset, length) entries) that scatters chunks into a 384 KB (0x60000) flat ROM = the real ECU address space. In the flat ROM, production (20187) vs aftermarket (20188) differ only 1.4% — the real fuel enrichment. Reversed in research/reference-maps/reconstruct_rom.py.

5c. Directory lookup → calibration metadata

Decoded signature at offset 20 → s.a directory record → Qd = field[1] → fe = c.a[Qd*48] (the 48-int per-calibration metadata that holds all the table pointers). Validated: sig 0x0187CA84 → s.a #541 → Qd 32.

6. Calibration tables

research/reference-maps/TABLES.md + table_map.py. Base = (fe[0]&0x2F0)<<12 = 0x50000; table offset = base + fe[k]. Tables are 32 RPM rows × 20 throttle cols, 16-bit big-endian.

Table fe Offset (20187) Confidence
Main fuel cyl 1 / 2 11 / 12 0x56990 / 0x56E90 high (75/67% aftermarket diff, smooth VE)
Low-throttle fuel cyl 1 / 2 15 / 16 0x55590 / 0x55A90 high
Fuel base/idle 9 0x55500 high
Ignition advance gear 1 / 2–5 / 6 / neutral 19–22 0x587D0 … 0x596D0 high
AFR / target lambda 2 0x52260 medium (128 = λ1.00 confirmed)

Axes: RPM = fe[8] (32 breakpoints, real RPM); throttle = fe[27] (20, 0–100%×10). Validated end-to-end: the main fuel table renders as a clean VE surface, and the aftermarket map is richer in 284/320 cells — physically correct enrichment.

Scaling (relative changes are exact; absolute units still approximate): fuel values are the ECU's internal VE/fuel-mass units (~0–10800); ignition 13–600 is an internal advance unit; AFR 128 = λ1.00. Exact fuel/ignition constants: TBD (an XDF cross-check or more RE).

7. Device-enable flags (SAI / O2)

research/reference-maps/DEVICES.md. Byte-boolean array at base + fe[33] (=0x53801), 1=on/0=off. Validated against the real NO-SAI-NO-O2 map 20188Map2009AIRBOXBONNY, which cleared exactly three bytes:

Offset Device Confidence
0x53801 SAI ~90% (code lc() → Devices[0] via fe[33], and the delete map)
0x53818 / 0x53819 O2 sensor 1 / 2 ~80% (two O2 sensors ↔ two adjacent bytes; not individually isolated)

Critical caveat: correct flags ≠ a good tune. Disabling O2 forces open-loop — the bike runs on the base fuel map with no live trim; on a stock map that can run lean/rough. A proper delete pairs flags-off with fuel enrichment (which is why the reference map changed the fuel tables too). Flash a complete matching delete map, not hand-toggled flags on a stock one. The owner's target = the AIRBOXBONNY map (SAI/O2 off + airbox fueling done); only the 2-1-vs-2-2 midrange fuel needs fine-tuning.

8. Checksums

Two distinct ones:

  • Table write-back (edit) checksum: a running 16-bit word, chk = (chk + old - new) & 0xFFFF, used when a cell changes. Location is per-calibration.
  • Distribution .hex integrity: the dc cipher + unpack directory. The caXX header/tail bytes are map-ID metadata, not a cal checksum, so re-encoding an edit is complete for the file format.
  • ECU flash checksum: applied by the flashing tool at write time — out of scope for the read/edit tool, part of the write path.

9. Hardware / cable

Owner has a VAG KKL 409.1 cable (right class — K-Line) and a Bluetooth ELM327. The VAG cable is wired for a VW OBD-II socket; the 2010 Bonneville uses Triumph's proprietary diagnostic connector, so it needs an adapter/re-pin (K-Line + switched 12V + ground). Confirm the connector pinout before plugging in — that's the one genuine electrical risk. Identify the chip first (tunie ports: FTDI = /dev/cu.usbserial-*; CH340 = /dev/cu.wchusbserial*).

10. The viewer

viewer/tunie-viewer.html — one self-contained file, no server, no external refs. Tabs: Map catalogue (1811, filterable, "my bike" filter) · Tune capabilities (devices/params/sensors from the APK) · Triumph tables (drop a real .hex → decode → unpack → render real fuel/ignition tables with axes + A→B diff + device checkboxes) · Raw table viewer. Build with viewer/build_viewer.py (--tuneecu-src enables the Triumph tab by embedding the c.a/s.a directory). download_maps.py + serve.py add a catalogue dropdown of locally cached maps.

11. Validation status & confidence (honest)

Finding Confidence How validated
KWP2000 addressing/framing high byte-identical to TuneECU
Seed/key = AES-128 high FIPS-197 + T-table match
Map decrypt (dc) high round-trip + readable strings on real maps
Flat-ROM unpack high sibling diff 96%→1.4%, directory lookup resolves
Fuel/ignition table offsets high render as real tables; enrichment physically correct
Table scaling (absolute units) low–med relative exact; absolute TBD
SAI flag (0x53801) high (~90%) code + real delete map
O2 flags (0x53818/9) probable (~80%) group-validated, not isolated
Anything on the actual bike none yet no hardware connected

12. What is NOT done

  • No comms with the real ECU (blocked on the cable adapter/pinout).
  • No ROM dump from the bike.
  • No write/flash path (seed/key which-key + padding, flash kernel, ECU checksum).
  • Editor exports the distribution .hex but re-packing an edited flat ROM back through the unpack directory + .hex for arbitrary edits is only partly wired (device flags work because they live in one chunk).
  • Absolute fuel/ignition scaling constants.
  • Individual SAI-vs-O2 isolation and the other device flags (exhaust valve, etc.).

13. Key source references (in the decompile)

l.java: dc() decrypt, zc()/Nc()/Lb() map load + tables, sc() directory, lc() device list. m.java: Vb() seed/key, KWP framing (Ld/Ub), uc() ROM packer. MainActivity.java: p8() load pipeline, unpack directory build, a3/W2 init sequences, Maps/ URLs. c/r/s/t.java: directory + geometry arrays. x.java a(): per-ECU device layout (x.t/x.s/x.q) — not fully traced.