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.
This commit is contained in:
78
tunie/viewer/FORMAT.md
Normal file
78
tunie/viewer/FORMAT.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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/`.
|
||||
Reference in New Issue
Block a user