Locate Keihin tables + render real fuel maps in the viewer
Reversed the inner map format from l.java (Nc/Lb) and validated it against the stock reference maps: - reconstruct_rom.py: decoded map -> unpack directory -> 384KB flat ROM. In the flat ROM, production (20187) vs aftermarket (20188) differ only 1.4% (vs 96% packed), i.e. real fuel enrichment. - table_map.py + TABLES.md: fe = c.a[Qd*48]; base = (fe[0]&0x2F0)<<12 = 0x50000; table = base + fe[k]. Located 9 tables (main/low-throttle fuel per cylinder, base/idle fuel, ignition by gear x4), 32 RPM rows x 20 throttle cols, with real axes (RPM fe[8], throttle fe[27]) and the AFR curve (fe[2], 128=lambda 1.00). Validated: main-fuel delta is uniformly richer in the aftermarket map. Viewer now has a "Triumph tables" tab: drop a real .hex (or pick two) and it decodes, unpacks, and renders the fuel/ignition tables as heatmaps with real axes, plus an A->B difference view. build_viewer.py embeds the c.a/s.a directory so any map resolves in-browser. Catalogue dropdown: download_maps.py fetches map .hex files into maps_cache/; serve.py serves the viewer over http so the dropdown can fetch them (drag-and-drop still works on file://). Proprietary map binaries (*.hex, *.dec.bin, maps_cache/) are gitignored — code and docs only.
This commit is contained in:
131
tunie/research/reference-maps/table_map.py
Normal file
131
tunie/research/reference-maps/table_map.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Named calibration-table map for the Triumph Keihin SH7054 (865 twin, mechanical
|
||||
odo family), derived from Nc()/Lb() in l.java and validated against the stock
|
||||
reference maps.
|
||||
|
||||
Pipeline (all reversed, see reconstruct_rom.py and decode_map.py):
|
||||
.hex --decode--> packed map --unpack--> flat 0x60000 ROM
|
||||
signature @ decoded[20] --> s.a directory record --> Qd = field[1]
|
||||
fe = c.a[Qd*48] (per-calibration metadata)
|
||||
base = (fe[0] & 0x2F0) << 12 (= 0x50000 here)
|
||||
table offset (flat ROM) = base + fe[<field>]
|
||||
|
||||
Tables are 32 rows (RPM) x 20 cols (throttle), 16-bit big-endian. Axes:
|
||||
RPM axis = fe[8] (32 breakpoints, e.g. 0..10000)
|
||||
Throttle axis = fe[27] (20 breakpoints, 0..1000 = 0..100.0%)
|
||||
|
||||
fe-field -> table assignment (validated by production(20187) vs aftermarket(20188)
|
||||
diff and by value range/shape):
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from reconstruct_rom import flat_rom
|
||||
|
||||
SRC = "/Users/dylan/dojo/tuner/work/jadx_out/sources/com/tuneecu"
|
||||
ROWS, COLS = 32, 20 # RPM x throttle
|
||||
|
||||
# name, fe field, kind. Confidence: fuel/ignition are strong; base-fuel & AFR noted.
|
||||
TABLE_DEFS = [
|
||||
("Main fuel — cylinder 1", 11, "fuel"),
|
||||
("Main fuel — cylinder 2", 12, "fuel"),
|
||||
("Low-throttle fuel — cyl 1", 15, "fuel"),
|
||||
("Low-throttle fuel — cyl 2", 16, "fuel"),
|
||||
("Fuel — base/idle", 9, "fuel"),
|
||||
("Ignition advance — gear 1", 19, "ignition"),
|
||||
("Ignition advance — gears 2–5", 20, "ignition"),
|
||||
("Ignition advance — gear 6", 21, "ignition"),
|
||||
("Ignition advance — neutral", 22, "ignition"),
|
||||
]
|
||||
RPM_AXIS_FE, THR_AXIS_FE = 8, 27
|
||||
AFR_FE = 2 # interleaved (value,breakpoint); 128 == lambda 1.00
|
||||
|
||||
|
||||
def _load_int_array(java_path: str, field: str) -> list[int]:
|
||||
s = Path(java_path).read_text()
|
||||
m = re.search(rf"\b{field} = \{{(.*?)\}};", s, re.S)
|
||||
return [int(t.strip().rstrip("L")) for t in m.group(1).split(",") if t.strip()]
|
||||
|
||||
|
||||
def _u(x: int) -> int:
|
||||
return x & 0xFFFFFFFF
|
||||
|
||||
|
||||
def resolve(map_path: str) -> dict:
|
||||
"""Reconstruct the flat ROM and resolve every named table's absolute offset."""
|
||||
ca = _load_int_array(f"{SRC}/c.java", "a")
|
||||
sa = _load_int_array(f"{SRC}/s.java", "a")
|
||||
rom = flat_rom(Path(map_path).read_bytes())
|
||||
|
||||
# Resolve Qd by scanning s.a for the record whose field[0] matches the map's
|
||||
# signature (read from the DECODED header); field[1] of that record is Qd.
|
||||
dec_sig = _map_signature(map_path)
|
||||
qd = None
|
||||
for i in range(len(sa) // 8):
|
||||
if _u(sa[i * 8]) == _u(dec_sig):
|
||||
qd = sa[i * 8 + 1]
|
||||
break
|
||||
if qd is None:
|
||||
raise ValueError("signature not found in s.a directory")
|
||||
|
||||
fe = ca[qd * 48: qd * 48 + 48]
|
||||
base = (fe[0] & 0x2F0) << 12
|
||||
|
||||
def table(off):
|
||||
return [
|
||||
[struct.unpack_from(">H", rom, off + (r * COLS + c) * 2)[0] for c in range(COLS)]
|
||||
for r in range(ROWS)
|
||||
]
|
||||
|
||||
out = {
|
||||
"map": Path(map_path).stem,
|
||||
"qd": qd,
|
||||
"base": base,
|
||||
"rpm_axis": _axis(rom, base + fe[RPM_AXIS_FE], ROWS),
|
||||
"throttle_axis": _axis(rom, base + fe[THR_AXIS_FE], COLS),
|
||||
"tables": [],
|
||||
}
|
||||
for name, field, kind in TABLE_DEFS:
|
||||
off = base + (fe[field] & 0x7FFFF)
|
||||
out["tables"].append({
|
||||
"name": name, "kind": kind, "fe": field,
|
||||
"offset": off, "offset_hex": f"0x{off:X}",
|
||||
"rows": ROWS, "cols": COLS, "data": table(off),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _axis(rom: bytes, off: int, n: int) -> list[int]:
|
||||
return [struct.unpack_from(">H", rom, off + i * 2)[0] for i in range(n)]
|
||||
|
||||
|
||||
def _map_signature(map_path: str) -> int:
|
||||
from decode_map import decode
|
||||
d = decode(Path(map_path).read_bytes())
|
||||
sig = int.from_bytes(d[20:24], "big")
|
||||
if d[0] == 0x67:
|
||||
sig = (sig & 0xFFFF0000) | (((sig & 0xFFFF) + d[25]) & 0xFFFF)
|
||||
return sig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = resolve("20187Map.hex")
|
||||
print(f"map {r['map']} Qd={r['qd']} base=0x{r['base']:X}")
|
||||
print(f"RPM axis: {r['rpm_axis']}")
|
||||
print(f"throttle axis: {r['throttle_axis']}")
|
||||
for t in r["tables"]:
|
||||
flat = [v for row in t["data"] for v in row]
|
||||
print(f" {t['name']:32} {t['offset_hex']:>8} "
|
||||
f"range {min(flat)}..{max(flat)}")
|
||||
Path("table_map.json").write_text(json.dumps(
|
||||
{"defs": [{"name": n, "fe": f, "kind": k} for n, f, k in TABLE_DEFS],
|
||||
"rows": ROWS, "cols": COLS,
|
||||
"axes": {"rpm_fe": RPM_AXIS_FE, "throttle_fe": THR_AXIS_FE},
|
||||
"reference": r}, separators=(",", ":")))
|
||||
print("wrote table_map.json")
|
||||
Reference in New Issue
Block a user