Add validated SAI/O2 device-flag checkbox editor
Reversed the device-flag mechanism (l.java lc() + ee bit logic) and validated the flag locations against a real reference map: 20188Map2009AIRBOXBONNY (explicitly "NO SAI, NO O2 SENSORS"). Diffing its flat ROM vs stock 20188 isolated exactly three byte-boolean flags that flip 1->0: 0x53801 SAI (= base + fe[33] + 0x00) 0x53818 O2 sensor (+ 0x17) 0x53819 O2 sensor (2) (+ 0x18) 1 = enabled, 0 = disabled. Neither stock map could reveal these (both have SAI+O2 on); the delete map was the key. See research/reference-maps/DEVICES.md. Viewer: the Triumph-tables tab now has a Device flags panel — checkboxes reflect the loaded map's real state (stock: all on; delete map: all off), toggling flips the byte, and "Export edited .hex" re-encodes the distribution format. The decode->edit->encode round-trip is byte-exact (verified). The caXX header bytes are map-ID metadata, not a cal checksum; the ECU-flash checksum is applied at write time.
This commit is contained in:
@@ -126,6 +126,16 @@
|
||||
<span class="count" id="tinfo"></span>
|
||||
</div>
|
||||
<div style="overflow:auto"><table id="tgrid" class="mono" style="font-size:11px"></table></div>
|
||||
<div class="card" id="tdevices" style="margin-top:16px;max-width:520px">
|
||||
<h3>Device flags <span class="badge">Map A</span></h3>
|
||||
<div class="note">Toggle emissions/hardware devices. Validated against a real
|
||||
NO-SAI-NO-O2 map. Uncheck to delete; then Export the edited map.</div>
|
||||
<div id="tdevlist"></div>
|
||||
<button id="tdevExport" class="chip" style="border-color:var(--ok);color:var(--ok);margin-top:10px">⭳ Export edited .hex</button>
|
||||
<span class="count" id="tdevdirty"></span>
|
||||
<div class="hint" style="margin-top:8px">Export re-encodes the map's distribution format.
|
||||
The separate ECU-flash checksum is applied by the flashing tool at write time.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -254,16 +264,26 @@ else {
|
||||
}
|
||||
const le=(b,o,n)=>{let v=0;for(let i=0;i<n;i++)v|=b[o+i]<<(8*i);return v>>>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;k<cnt;k++) ents.push([le(dec,i21+34+k*8,4), le(dec,i21+38+k*8,4)]);
|
||||
const cnt=dec[i21+33]; const ents=[]; let p=i21+34+cnt*8; const dstart=p;
|
||||
for(let k=0;k<cnt;k++){ const o=le(dec,i21+34+k*8,4), l=le(dec,i21+38+k*8,4); ents.push([o,l]); }
|
||||
return {ents, dstart};
|
||||
}
|
||||
function flatRom(dec){
|
||||
const {ents,dstart}=unpackInfo(dec);
|
||||
let sz=0; ents.forEach(([o,l])=>sz=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<o+l) return p+(off-o); p+=l; }
|
||||
return -1;
|
||||
}
|
||||
function resolve(dec){
|
||||
let sig=(dec[20]<<24|dec[21]<<16|dec[22]<<8|dec[23])>>>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<RD.rows;i++) rpm.push(be16(rom, base+(fe[RD.rpmFe]&0x7FFFF)+i*2));
|
||||
for(let i=0;i<RD.cols;i++) thr.push(be16(rom, base+(fe[RD.throttleFe]&0x7FFFF)+i*2));
|
||||
const tables=RD.tables.map(t=>({...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<b.length;i++){ const ks=(key>>>(((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=`<input type="checkbox" ${on?'checked':''}> <span>${d.name}
|
||||
<span style="color:var(--dim)">· 0x${d.off.toString(16)} = ${TA.rom[d.off]}</span></span>`;
|
||||
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(){
|
||||
|
||||
Reference in New Issue
Block a user