Reverse-engineered the Triumph Keihin map format from the TuneECU loader (l.java zc/sc/Nc) and data classes c/r/s/t.java, and turned the viewer's table tab into a working editor. extract_mapdefs.py pulls the table directory out of the decompiled app into mapdefs.json: r.a/s.a/t.a (calibration directory, 8-int records keyed by the map's offset-20 signature) -> c.a (48-int calibration metadata) -> c.b (32-int groups = 16 offset/length pairs each), yielding 413 candidate table offsets. FORMAT.md documents the header magic (0x18008060 masked), the directory chain, and the write-back checksum. The viewer now edits: pick a known table offset (or set it manually), toggle edit mode, click a cell to change its value, and the bytes are rewritten with the running 16-bit checksum patched by (old - new) exactly as TuneECU does, then Download the modified copy. Verified: editing 2016->9999 with the checksum word at 0 yields 57553 = (0 + 2016 - 9999) & 0xffff. Geometry offsets are read-confident but not yet validated against a real map binary; FORMAT.md flags this. Editing/checksum stay local to a downloaded copy; the flash write path remains out of the read-only tunie tool.
3.6 KiB
Triumph Keihin map-file format (reverse-engineered)
Recovered from the decompiled TuneECU: the loader com/tuneecu/l.java
(zc → sc → Nc) and the data classes c/r/s/t.java. This is what a
TunerPro XDF encodes, but pulled straight out of the app.
Confidence: the extracted numbers (
mapdefs.json) are exact. The decode of what each field means is read-confident but not yet verified against a real map binary — we don't have a dump. Treat table offsets as candidates until checked against a ROM read from the bike.
Header
zc(byte[]) validates and indexes a loaded map:
- Magic: the first 4 bytes, masked
& 0xFF00FF60, must equal0x18008060. - Family byte:
bArr[0]selects the table directory —(bArr[0] & 0x7B) == 0x69→r.a;bArr[0] == 0x68→t.a; elses.a. (0x67/0x68/0x69are the map "generation" markers.) - Calibration signature: 4 bytes at offset 20 (big-endian).
sc()searches the directory for the record whosefield[0]equals this value.
Directory → metadata → geometry
Three levels, all in mapdefs.json:
-
r.a/s.a/t.a— 8 ints per record (926 / 1048 / 36 records).field[0]= the offset-20 signature (the lookup key).field[1]= index intoc.a(the calibration-metadata record, "Qd").field[2]=%100→ group index intoc.b(geometry);/100→ flags.field[3..7]= sizes / addresses / flags (not fully decoded).
-
c.a— 48 ints per record (153 records). Per-calibration metadata: memory size and region, and the checksum location. Exact field map still being pinned down. -
c.b— 32 ints per record (76 groups) = 16(offset, length)pairs. This is the table geometry: each pair is a byte offset into the map and the table's byte length. Confirmed by the loader readingc.bin 32-int strides intoDd, then usingDd[i*2]/Dd[i*2+1]as offset / size.extract_mapdefs.pysurfaces 413 candidate tables this way.Example (group 0):
0x6000/535,0x6218/1778,0x8000/8890,0x10002/42992.
Table dimensions and axes come from the runtime (MainActivity.T8/U8 for
rows/cols) and the title_axis labels (Throttle %, MAP hPa, RPM, Load %, Temp,
Gear). Cells are 16-bit big-endian with per-table scaling.
Editing / write-back (this is the whole trick)
TuneECU keeps a running 16-bit checksum at a calibration-specific offset and
patches it incrementally on every edit (l.java:1418-1431):
oldWord = (map[off] << 8) | map[off+1]
map[off] = newWord >> 8
map[off+1] = newWord & 0xFF
checksum = (checksum + oldWord - newWord) & 0xFFFF # at the checksum offset
So editing a cell is: overwrite the 2 bytes, then add (oldWord - newWord) to
the checksum word. No full re-hash needed. (In the decompile the checksum sits
at byte 884744/0xD8048 for that ROM size; the offset is per-calibration and
lives in c.a.)
This is exactly what the viewer's editor does: edit cells → patch the checksum word → export the modified copy. It is the core of the TuneECU edit-and-save loop, reproduced locally and for free.
What's still needed to fully replace TuneECU's editor
- A real map/ROM binary to validate the geometry offsets above.
- The per-calibration checksum offset decoded from
c.a(currently a configurable field in the editor). - Per-table scaling factors and axis linkage (partly in
l.java's per-table-type read code; partly derivable by diffing known maps). - The flash write path to push an edited map to the bike — deliberately out
of scope for the read-only
tunietool; see../research/.