diff --git a/tunie/research/reference-maps/DEVICES.md b/tunie/research/reference-maps/DEVICES.md new file mode 100644 index 0000000..eb2af1d --- /dev/null +++ b/tunie/research/reference-maps/DEVICES.md @@ -0,0 +1,52 @@ +# Device-enable flags (SAI / O2 / lambda) — validated + +## How they were found + +Both stock reference maps (20187, 20188) have SAI and O2 **active**, so diffing +them can't reveal the delete flags. The confirmation came from a community map +that explicitly disables them: + +`20188Map2009AIRBOXBONNY.hex` — "Bonneville, aftermarket exhaust, mechanical +odometer, NO AIR BOX, K&N, British Custom mufflers, **NO SAI, NO O² SENSORS**". +Same base as stock 20188, so the diff isolates the deletes. + +Diff (flat ROM) of that map vs stock 20188 = 0.36%, split into: +- the fuel tables (airbox/K&N enrichment — expected), and +- a small cluster in the device-config region at **0x53801…0x53819**. + +## The flags + +They are a **byte-boolean array** at flat-ROM `base + fe[33]` (= `0x50000 + 0x3801 += 0x53801`), one byte per device, **1 = enabled, 0 = disabled**. + +The delete map changed exactly three bytes from 1 → 0: + +| Flat-ROM offset | Stock | Deleted | Device | +|---|---|---|---| +| `0x53801` | 1 | 0 | **SAI** (Secondary Air Injection) | +| `0x53818` | 1 | 0 | **O2 sensor** | +| `0x53819` | 1 | 0 | **O2 sensor (2)** | + +SAI is the first flag in the array; the two O2 sensors are the last two — +consistent with the `Devices` resource order (SAI = index 0; O2 Sensor / O2 +Sensor (2)). The three-byte change matching "NO SAI, NO O²" is unambiguous. + +To disable a device: set its byte to `0`. (The `0x5369C`/`0x536AB` bytes that +also changed are idle/open-loop trim that comes with removing the O2 feedback, +not device-enable flags.) + +## Editing / export + +The downloaded `.hex` format's integrity is the `dc` stream cipher + the unpack +directory; the `caXX` bytes in the header/tail are map-ID metadata, **not** a +calibration checksum. So a device toggle = flip the byte in the flat ROM, re-pack +to the decoded layout, and `dc`-encode back to `.hex`. (The separate *ECU flash* +checksum is computed at flash time and is out of scope for the read/edit tool.) + +## Confidence + +- Flag **locations** (0x53801 / 0x53818 / 0x53819) and semantics (1/0): **validated** + against a real NO-SAI-NO-O2 map. +- SAI-vs-O2 **labeling** of the three bytes: strong (order + delete semantics); + final SAI-only-vs-O2-only separation would need a single-delete reference map or + a DTC/bench check. diff --git a/tunie/viewer/build_viewer.py b/tunie/viewer/build_viewer.py index 6ca4859..380a9c7 100644 --- a/tunie/viewer/build_viewer.py +++ b/tunie/viewer/build_viewer.py @@ -99,6 +99,14 @@ def _extract_romdefs(args) -> dict: "sa": arr("s", "a"), "tables": [{"name": n, "fe": f, "kind": k} for n, f, k in _TABLE_DEFS], "rows": 32, "cols": 20, "rpmFe": 8, "throttleFe": 27, + # Device-enable flags: byte at base + fe[33] + rel (1=on, 0=off). + # Validated against a NO-SAI-NO-O2 reference map (see research DEVICES.md). + "deviceFe": 33, + "devices": [ + {"name": "SAI (Secondary Air Injection)", "rel": 0x00}, + {"name": "O2 sensor", "rel": 0x17}, + {"name": "O2 sensor (2)", "rel": 0x18}, + ], } diff --git a/tunie/viewer/template.html b/tunie/viewer/template.html index 33e3bfb..37a3fce 100644 --- a/tunie/viewer/template.html +++ b/tunie/viewer/template.html @@ -126,6 +126,16 @@
+
+

Device flags Map A

+
Toggle emissions/hardware devices. Validated against a real + NO-SAI-NO-O2 map. Uncheck to delete; then Export the edited map.
+
+ + +
Export re-encodes the map's distribution format. + The separate ECU-flash checksum is applied by the flashing tool at write time.
+
@@ -254,16 +264,26 @@ else { } const le=(b,o,n)=>{let v=0;for(let i=0;i>>0;}; const be16=(b,o)=>((b[o]<<8)|b[o+1]); - function flatRom(dec){ + function unpackInfo(dec){ const i21=le(dec,28,2); if(le(dec,i21+31,2)!==0x6F66) throw new Error('bad unpack marker'); - const cnt=dec[i21+33]; const ents=[]; - for(let k=0;ksz=Math.max(sz,o+l)); - const rom=new Uint8Array(sz).fill(0xff); let p=i21+34+cnt*8; + const rom=new Uint8Array(sz).fill(0xff); let p=dstart; ents.forEach(([o,l])=>{ rom.set(dec.subarray(p,p+l), o); p+=l; }); return rom; } + // Map a flat-ROM offset back to its position in the packed/decoded map. + function flatToDecoded(off, ents, dstart){ + let p=dstart; + for(const [o,l] of ents){ if(off>=o && off>>0; if(dec[0]===0x67) sig=((sig&0xFFFF0000)|(((sig&0xFFFF)+dec[25])&0xFFFF))>>>0; @@ -278,11 +298,37 @@ else { } function loadMap(raw){ const dec=dcDecode(raw); const rom=flatRom(dec); const {fe,base}=resolve(dec); + const {ents,dstart}=unpackInfo(dec); const rpm=[],thr=[]; for(let i=0;i({...t, off:base+(fe[t.fe]&0x7FFFF)})); - return {rom, base, rpm, thr, tables, desc:new TextDecoder().decode(dec.subarray(30,30+le(dec,28,2)))}; + const devBase=base+(fe[RD.deviceFe]&0x7FFFF); + const devices=(RD.devices||[]).map(d=>({...d, off:devBase+d.rel})); + return {raw, dec, rom, base, ents, dstart, fe, rpm, thr, tables, devices, + desc:new TextDecoder().decode(dec.subarray(30,30+le(dec,28,2)))}; + } + // Flip a device byte in the flat ROM + packed map, and return a downloadable .hex. + function setDeviceByte(m, off, val){ + m.rom[off]=val; + const dpos=flatToDecoded(off, m.ents, m.dstart); + if(dpos>=0) m.dec[dpos]=val; + } + function exportEdited(m){ + // re-encode the (edited) decoded map back to the .hex cipher form + const out=dcEncode(m.dec); + const blob=new Blob([out],{type:'application/octet-stream'}); + const a=document.createElement('a'); a.href=URL.createObjectURL(blob); + a.download=(m.desc.split('\n')[0]||'map').replace(/\W+/g,'_')+'.edited.hex'; + a.click(); URL.revokeObjectURL(a.href); + } + function dcEncode(dec){ + const b=Uint8Array.from(dec); const i5=b[3]-24; + const i2=(i5 in I2)?I2[i5]:((i5+3)<<24)|((i5+1)<<8)|i5|((i5+2)<<16); + const key=((i2 | (b[0]|(b[1]<<8)|(b[2]<<16)|(b[3]<<24)))>>>0); + let prev=0; + for(let i=4;i>>(((i-4)%4)*8))&0xff; const o=(prev^b[i]^ks)&0xff; b[i]=o; prev=o; } + return b; } let TA=null, TB=null, tdiff=false; @@ -317,13 +363,35 @@ else { }).catch(()=>{}); } + let devDirty=0; function initTriumph(){ document.getElementById('tBody').style.display='block'; const sel=document.getElementById('tsel'); sel.innerHTML=''; TA.tables.forEach((t,i)=>{ const o=document.createElement('option'); o.value=i; o.textContent=`${t.name} (0x${t.off.toString(16)})`; sel.appendChild(o); }); sel.onchange=renderT; renderT(); + renderDevices(); } + function renderDevices(){ + const wrap=document.getElementById('tdevlist'); wrap.innerHTML=''; devDirty=0; + document.getElementById('tdevdirty').textContent=''; + if(!TA.devices||!TA.devices.length){ document.getElementById('tdevices').style.display='none'; return; } + document.getElementById('tdevices').style.display='block'; + TA.devices.forEach(d=>{ + const on=TA.rom[d.off]!==0; + const row=document.createElement('label'); + row.className='item'; row.style.cursor='pointer'; + row.innerHTML=` ${d.name} + · 0x${d.off.toString(16)} = ${TA.rom[d.off]}`; + row.querySelector('input').onchange=e=>{ + setDeviceByte(TA, d.off, e.target.checked?1:0); + row.querySelector('span span').textContent=`· 0x${d.off.toString(16)} = ${TA.rom[d.off]}`; + devDirty++; document.getElementById('tdevdirty').textContent=devDirty+' change'+(devDirty===1?'':'s'); + }; + wrap.appendChild(row); + }); + } + document.getElementById('tdevExport').onclick=()=>{ if(TA) exportEdited(TA); }; document.getElementById('tdiffChip').onclick=e=>{ tdiff=!tdiff; e.target.classList.toggle('on',tdiff); renderT(); }; function renderT(){ diff --git a/tunie/viewer/tunie-viewer.html b/tunie/viewer/tunie-viewer.html index 1de99f6..e6207a7 100644 --- a/tunie/viewer/tunie-viewer.html +++ b/tunie/viewer/tunie-viewer.html @@ -126,6 +126,16 @@
+
+

Device flags Map A

+
Toggle emissions/hardware devices. Validated against a real + NO-SAI-NO-O2 map. Uncheck to delete; then Export the edited map.
+
+ + +
Export re-encodes the map's distribution format. + The separate ECU-flash checksum is applied by the flashing tool at write time.
+
@@ -168,7 +178,7 @@ - +