Files
samplez/tunie/viewer/FORMAT.md
uhryniuk 9ab9feb62f Add map-format reversing + in-browser table editor
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.
2026-08-10 15:36:23 -05:00

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 equal 0x18008060.
  • Family byte: bArr[0] selects the table directory — (bArr[0] & 0x7B) == 0x69 → r.a; bArr[0] == 0x68 → t.a; else s.a. (0x67/0x68/0x69 are the map "generation" markers.)
  • Calibration signature: 4 bytes at offset 20 (big-endian). sc() searches the directory for the record whose field[0] equals this value.

Directory → metadata → geometry

Three levels, all in mapdefs.json:

  1. 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 into c.a (the calibration-metadata record, "Qd").
    • field[2] = %100 → group index into c.b (geometry); /100 → flags.
    • field[3..7] = sizes / addresses / flags (not fully decoded).
  2. 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.

  3. 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 reading c.b in 32-int strides into Dd, then using Dd[i*2] / Dd[i*2+1] as offset / size. extract_mapdefs.py surfaces 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

  1. A real map/ROM binary to validate the geometry offsets above.
  2. The per-calibration checksum offset decoded from c.a (currently a configurable field in the editor).
  3. Per-table scaling factors and axis linkage (partly in l.java's per-table-type read code; partly derivable by diffing known maps).
  4. The flash write path to push an edited map to the bike — deliberately out of scope for the read-only tunie tool; see ../research/.