|
|
@@ -428,8 +428,8 @@ $("#trailClear").onclick=()=>{ loopTrail=[]; try{ localStorage.removeItem(TRAIL_
|
|
|
renderTrail();
|
|
|
|
|
|
// Per-emulated-type config (must match firmware ET_* order: Xbox=0, Switch=1, DS4=2, DS5=3). Each type lists
|
|
|
-// only the remap targets that exist on that controller. Field id sent to firmware = 40 + et*8 + k
|
|
|
-// (k: 0..3 back paddles, 4 QAM, 5 A/B-swap, 6 trackpad-haptics, 7 LED brightness). Blob v10 reads them back at p[73 + et*8 + ...].
|
|
|
+// only the remap targets that exist on that controller. Field id sent to firmware = 40 + et*9 + k
|
|
|
+// (k: 0..3 back paddles, 4 QAM, 5 A/B-swap, 6 trackpad-haptics, 7 LED brightness, 8 rumble). Blob v17 reads them back at p[73 + et*9 + ...].
|
|
|
const DPAD = {12:"D-pad Up",13:"D-pad Down",14:"D-pad Left",15:"D-pad Right"};
|
|
|
const TYPE_DEFS = [
|
|
|
{key:"XBOX", name:"Xbox", labels:{1:"A",2:"B",3:"X",4:"Y",5:"LB",6:"RB",7:"L3",8:"R3",9:"Back",10:"Start",11:"Guide",19:"LT",20:"RT",...DPAD}},
|
|
|
@@ -481,7 +481,7 @@ let lizardBindings = []; // [{outType, od:[7], trig, hold}]
|
|
|
let lizardBusy = false;
|
|
|
let lizardLoaded = false; // one-shot: the lizard map is fetched lazily, only once the connected puck proves it speaks v16+
|
|
|
const BACK_LABELS = ["L4 (back upper-left)","R4 (back upper-right)","L5 (back lower-left)","R5 (back lower-right)"];
|
|
|
-const typeEls = []; // one entry per TYPE_DEFS: {sec, tab, activeDot, back[], qam, abSwap, pad, led, ledV}
|
|
|
+const typeEls = []; // one entry per TYPE_DEFS: {sec, tab, activeDot, back[], qam, abSwap, pad, led, ledV, rumble}
|
|
|
function mkSelect(def, includeNone){
|
|
|
const sel=document.createElement("select");
|
|
|
if(includeNone){ const o=document.createElement("option"); o.value=0; o.textContent="— none —"; sel.appendChild(o); }
|
|
|
@@ -509,30 +509,33 @@ function setTab(et){
|
|
|
tab.onclick=()=>setTab(et); bar.appendChild(tab);
|
|
|
// panel
|
|
|
const sec=document.createElement("div"); sec.style.cssText="background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:12px 14px;display:none";
|
|
|
- const rec={sec,tab,activeDot:tab.querySelector(".active-dot"),back:[],qam:null,abSwap:null,pad:null,led:null,ledV:null};
|
|
|
- // A/B swap + trackpad haptics toggles
|
|
|
+ const rec={sec,tab,activeDot:tab.querySelector(".active-dot"),back:[],qam:null,abSwap:null,pad:null,led:null,ledV:null,rumble:null};
|
|
|
+ // A/B swap + trackpad haptics + rumble toggles
|
|
|
const tog=document.createElement("div"); tog.className="row";
|
|
|
tog.innerHTML='<label>A/B + X/Y swap</label>';
|
|
|
const ab=document.createElement("button"); ab.textContent="off"; tog.appendChild(ab);
|
|
|
const padLbl=document.createElement("label"); padLbl.textContent="Trackpad haptics"; padLbl.style.marginLeft="14px"; tog.appendChild(padLbl);
|
|
|
const pad=document.createElement("button"); pad.textContent="on"; tog.appendChild(pad);
|
|
|
+ const rumbleLbl=document.createElement("label"); rumbleLbl.textContent="Rumble"; rumbleLbl.style.marginLeft="14px"; tog.appendChild(rumbleLbl);
|
|
|
+ const rumble=document.createElement("button"); rumble.textContent="on"; tog.appendChild(rumble);
|
|
|
sec.appendChild(tog);
|
|
|
- rec.abSwap=ab; rec.pad=pad;
|
|
|
- ab.onclick=()=>{ const on=ab.classList.contains("active"); setField(40+et*8+5, on?0:1); };
|
|
|
- pad.onclick=()=>{ const on=pad.classList.contains("active"); setField(40+et*8+6, on?0:1); };
|
|
|
+ rec.abSwap=ab; rec.pad=pad; rec.rumble=rumble;
|
|
|
+ ab.onclick=()=>{ const on=ab.classList.contains("active"); setField(40+et*9+5, on?0:1); };
|
|
|
+ pad.onclick=()=>{ const on=pad.classList.contains("active"); setField(40+et*9+6, on?0:1); };
|
|
|
+ rumble.onclick=()=>{ const on=rumble.classList.contains("active"); setField(40+et*9+8, on?0:1); };
|
|
|
// back paddles
|
|
|
for(let i=0;i<4;i++){
|
|
|
const row=document.createElement("div"); row.className="row";
|
|
|
const lab=document.createElement("label"); lab.textContent=BACK_LABELS[i]; row.appendChild(lab);
|
|
|
const sel=mkSelect(def,true); row.appendChild(sel); sec.appendChild(row);
|
|
|
- sel.addEventListener("change",()=>setField(40+et*8+i, +sel.value));
|
|
|
+ sel.addEventListener("change",()=>setField(40+et*9+i, +sel.value));
|
|
|
rec.back.push(sel);
|
|
|
}
|
|
|
// QAM
|
|
|
{ const row=document.createElement("div"); row.className="row";
|
|
|
const lab=document.createElement("label"); lab.textContent="QAM (3 dots)"; row.appendChild(lab);
|
|
|
const sel=mkSelect(def,false); row.appendChild(sel); sec.appendChild(row);
|
|
|
- sel.addEventListener("change",()=>setField(40+et*8+4, +sel.value));
|
|
|
+ sel.addEventListener("change",()=>setField(40+et*9+4, +sel.value));
|
|
|
rec.qam=sel; }
|
|
|
// LED brightness
|
|
|
{ const row=document.createElement("div"); row.className="row";
|
|
|
@@ -541,7 +544,7 @@ function setTab(et){
|
|
|
const vspan=document.createElement("span"); vspan.className="val"; row.appendChild(vspan);
|
|
|
function fmtLed(v){ return (+v===0)?"Auto":v+"%"; }
|
|
|
sl.addEventListener("input",()=>{ vspan.textContent=fmtLed(sl.value); });
|
|
|
- sl.addEventListener("change",()=>setField(40+et*8+7, +sl.value));
|
|
|
+ sl.addEventListener("change",()=>setField(40+et*9+7, +sl.value));
|
|
|
sec.appendChild(row);
|
|
|
rec.led=sl; rec.ledV=vspan; }
|
|
|
host.appendChild(sec); typeEls.push(rec);
|
|
|
@@ -789,12 +792,12 @@ function applyBlob(p){
|
|
|
for (let s=0; s<4; s++) { if (!bondedSet.has(s)) g_slotData[s] = null; }
|
|
|
// clamp active slot to a valid bonded slot
|
|
|
if (!g_slotData[g_activeSlot]) g_activeSlot = bondedSlots[0] || 0;
|
|
|
- // v13: per-slot link stats (fw p[141..176] -> js index 139..174): each controller's own
|
|
|
+ // v13: per-slot link stats (fw p[145..180] -> js index 143..178): each controller's own
|
|
|
// polls/delivered/new/crc/noRx/relay, so the tabs no longer show every controller merged into one number
|
|
|
- if (p.length >= 175) {
|
|
|
+ if (p.length >= 179) {
|
|
|
for (let s=0; s<4; s++) {
|
|
|
if (!g_slotData[s]) continue;
|
|
|
- const b = 139 + s*9;
|
|
|
+ const b = 143 + s*9;
|
|
|
g_slotData[s].stats = { polls: p[b]|(p[b+1]<<8), f1: p[b+2]|(p[b+3]<<8),
|
|
|
newps: p[b+4]|(p[b+5]<<8), crc: p[b+6], norx: p[b+7], relay: p[b+8] };
|
|
|
}
|
|
|
@@ -814,29 +817,29 @@ function applyBlob(p){
|
|
|
// Poll TX + RF-fail counts: shown ALWAYS (even when the link reads down) so a wedge is diagnosable --
|
|
|
// polls>0 here while Delivered is "—" means the puck is still polling but getting no usable replies.
|
|
|
$("#stPolls").textContent = polls+" /s";
|
|
|
- { const crc=(p.length>113?p[113]:0), norx=(p.length>114?p[114]:0), heal=(p.length>115?p[115]:0);
|
|
|
- const rf=(p.length>126?(p[125]|(p[126]<<8)):0);
|
|
|
+ { const crc=(p.length>117?p[117]:0), norx=(p.length>118?p[118]:0), heal=(p.length>119?p[119]:0);
|
|
|
+ const rf=(p.length>130?(p[129]|(p[130]<<8)):0);
|
|
|
$("#stRfFail").innerHTML = crc+" · "+norx+" · "+heal + (rf?' · <span class="pill dn">ring '+rf+'</span>':"");
|
|
|
// ring fault = we caught+recovered a relay-ring corruption that would otherwise be an invisible IRQ-off
|
|
|
// watchdog hang. Log each new one (persists across the device reset + panel reconnect).
|
|
|
if(rf>(window._ringFault||0)) log("⚠ RING FAULT #"+rf+" recovered — caught a relay-ring desync/corruption (would have hung loop with IRQs off)");
|
|
|
window._ringFault=rf; }
|
|
|
// v12: relay-frame rate + clock fingerprint (browser index = firmware index − 2)
|
|
|
- { const relay=(p.length>117?(p[116]|(p[117]<<8)):null);
|
|
|
+ { const relay=(p.length>121?(p[120]|(p[121]<<8)):null);
|
|
|
$("#stRelay").textContent = relay==null ? "—" : (relay+" /s"); }
|
|
|
- if(p.length>121){
|
|
|
+ if(p.length>125){
|
|
|
const LF=["stopped","RC","xtal","synth"], HF=["RC","?","xtal","?"];
|
|
|
- const lf=p[118], hf=p[119], upm=(p[120]|(p[121]<<8));
|
|
|
+ const lf=p[122], hf=p[123], upm=(p[124]|(p[125]<<8));
|
|
|
const lfBad=(lf!==2), hfBad=(hf!==2), upmBad=(upm && (upm<985||upm>1015));
|
|
|
const tag=(t,bad)=> bad ? '<span class="pill dn">'+t+'</span>' : t;
|
|
|
$("#stClock").innerHTML = tag(LF[lf]||lf, lfBad)+" / "+tag(HF[hf]||hf, hfBad);
|
|
|
$("#stUsPerMs").innerHTML = upm ? tag(""+upm, upmBad) : "—";
|
|
|
}
|
|
|
- // v12: LIVE loop heartbeat — curStage (firmware p[125]) + ms-since-beat (p[126], 40ms units). Updates even
|
|
|
+ // v12: LIVE loop heartbeat — curStage (firmware p[129]) + ms-since-beat (p[130], 40ms units). Updates even
|
|
|
// while loop() is wedged (blob is pushed from the SOF interrupt), so a hang shows "STALLED @ <stage>" live.
|
|
|
- if(p.length>124){
|
|
|
+ if(p.length>128){
|
|
|
const STAGE=["webusb","ctrl.task","serial","rfdiag","rflink","haptic","led","usbmount","usbtx"];
|
|
|
- const cur=p[123], stallMs=p[124]*40, name=(STAGE[cur]||("stage "+cur));
|
|
|
+ const cur=p[127], stallMs=p[128]*40, name=(STAGE[cur]||("stage "+cur));
|
|
|
if(stallMs>=200){
|
|
|
$("#stLoopState").innerHTML='<span class="pill dn">STALLED @ '+name+' '+stallMs+'ms</span>';
|
|
|
if(!window._stallEpisode) trailAdd("STALLED @ "+name+" ("+stallMs+"ms, soft wedge — usbd still alive)");
|
|
|
@@ -853,8 +856,8 @@ function applyBlob(p){
|
|
|
}
|
|
|
}
|
|
|
// v12: least-ever free stack on the usbd task (words). Low = overflow risk (multi-controller haptic-relay
|
|
|
- // hang hypothesis). Firmware p[137] -> browser p[135]. Red under 24 words (~96B), amber under 48.
|
|
|
- if(p.length>136){ const usbdW=(p[135]|(p[136]<<8));
|
|
|
+ // hang hypothesis). Firmware p[141] -> browser p[139]. Red under 24 words (~96B), amber under 48.
|
|
|
+ if(p.length>140){ const usbdW=(p[139]|(p[140]<<8));
|
|
|
$("#stUsbdStack").innerHTML = usbdW<24 ? ('<span class="pill dn">'+usbdW+'</span>')
|
|
|
: (usbdW<48 ? ('<span class="pill" style="background:#3a3216;color:#e5c76b">'+usbdW+'</span>') : (""+usbdW));
|
|
|
}
|
|
|
@@ -867,19 +870,19 @@ function applyBlob(p){
|
|
|
const RR_NAMES = ["unknown","power-on","pin/replug","watchdog (hang)","CPU lockup",
|
|
|
"HARDFAULT","reboot","soft reset","wake-from-off"];
|
|
|
const RR_FAULT = new Set([3,4,5]); // watchdog / lockup / hardfault = a real crash worth reporting (#72)
|
|
|
- if (p.length > 109) {
|
|
|
- const code = p[105];
|
|
|
- const raw = ((p[106]|(p[107]<<8)|(p[108]<<16)|(p[109]<<24))>>>0);
|
|
|
+ if (p.length > 113) {
|
|
|
+ const code = p[109];
|
|
|
+ const raw = ((p[110]|(p[111]<<8)|(p[112]<<16)|(p[113]<<24))>>>0);
|
|
|
const name = RR_NAMES[code] || ("code "+code);
|
|
|
// v12: which loop stage was stuck when the last watchdog/lockup hang fired (0xFF = last boot wasn't a hang)
|
|
|
const STAGE_NAMES = ["webusb","ctrl.task","serial","rfdiag","rflink","haptic","led","usbmount","usbtx"];
|
|
|
- const hangStage = (p.length>122 ? p[122] : 0xFF);
|
|
|
+ const hangStage = (p.length>126 ? p[126] : 0xFF);
|
|
|
let label = name;
|
|
|
if (hangStage!==0xFF) label += " @ "+(STAGE_NAMES[hangStage]||("stage "+hangStage));
|
|
|
$("#stReset").innerHTML = RR_FAULT.has(code) ? ('<span class="pill dn">'+label+'</span>') : label;
|
|
|
- // v12: PC/LR captured by the WDT pre-reset ISR (browser idx = firmware−2: hangPC@127, hangLR@131)
|
|
|
- const hpc=(p.length>130?((p[127]|(p[128]<<8)|(p[129]<<16)|(p[130]<<24))>>>0):0);
|
|
|
- const hlr=(p.length>134?((p[131]|(p[132]<<8)|(p[133]<<16)|(p[134]<<24))>>>0):0);
|
|
|
+ // v12: PC/LR captured by the WDT pre-reset ISR (browser idx = firmware−2: hangPC@131, hangLR@135)
|
|
|
+ const hpc=(p.length>134?((p[131]|(p[132]<<8)|(p[133]<<16)|(p[134]<<24))>>>0):0);
|
|
|
+ const hlr=(p.length>138?((p[135]|(p[136]<<8)|(p[137]<<16)|(p[138]<<24))>>>0):0);
|
|
|
const hx=v=>"0x"+v.toString(16).padStart(8,"0");
|
|
|
const el = $("#resetHist");
|
|
|
if (el) el.textContent = "RESETREAS=0x"+raw.toString(16).padStart(8,"0")
|
|
|
@@ -889,7 +892,7 @@ function applyBlob(p){
|
|
|
if (hpc && hpc!==window._hangPC){ window._hangPC=hpc; log("⚑ HANG PC="+hx(hpc)+" LR="+hx(hlr)+" — send me this; I'll map it to the stuck function"); }
|
|
|
// append the queued reset to the running hang log, now that we have this boot's reason/PC/stack
|
|
|
if (pendingHang){
|
|
|
- const usbdW2=(p.length>136?(p[135]|(p[136]<<8)):0);
|
|
|
+ const usbdW2=(p.length>140?(p[139]|(p[140]<<8)):0);
|
|
|
hangLog.unshift({ time:new Date(pendingHang.t).toLocaleTimeString(), uptime:pendingHang.uptimeSecs,
|
|
|
reason:name, stage:(hangStage!==0xFF)?(STAGE_NAMES[hangStage]||("stage"+hangStage)):"",
|
|
|
pc:hpc?hx(hpc):"", lr:hlr?hx(hlr):"", usbd:usbdW2 });
|
|
|
@@ -925,19 +928,20 @@ function applyBlob(p){
|
|
|
checkUpdateNotice(); // needs BOTH the installed version (this blob) and relCache (fetched on connect)
|
|
|
setSlider("mDiv",mDiv); setSlider("mFric",mFric); setSlider("rumble",rumbleScale);
|
|
|
$("#persistMode").textContent = persistMode? "on":"off"; $("#persistMode").classList.toggle("active",!!persistMode);
|
|
|
- // land-all-0x87 experiment toggle (blob v14, firmware p[177] -> JS p[175])
|
|
|
- { const la=(p.length>175?p[175]:0); $("#landAll87").textContent = la?"on":"off"; $("#landAll87").classList.toggle("active",!!la); }
|
|
|
- // per-type button config (blob v10): 4 types x 8 bytes. readBlob() strips the 2-byte [0xA5][len] header, so
|
|
|
+ // land-all-0x87 experiment toggle (blob v14/v17, firmware p[181] -> JS p[179])
|
|
|
+ { const la=(p.length>179?p[179]:0); $("#landAll87").textContent = la?"on":"off"; $("#landAll87").classList.toggle("active",!!la); }
|
|
|
+ // per-type button config (blob v10/v17): 4 types x 9 bytes. readBlob() strips the 2-byte [0xA5][len] header, so
|
|
|
// the firmware's buffer index 75 lands at JS index 73 here.
|
|
|
- if(p.length>=73+TYPE_DEFS.length*8){
|
|
|
+ if(p.length>=73+TYPE_DEFS.length*9){
|
|
|
const activeEt=etypeForMode(mode);
|
|
|
typeEls.forEach((rec,et)=>{
|
|
|
- const q=73+et*8;
|
|
|
- const back=[p[q],p[q+1],p[q+2],p[q+3]], qam=p[q+4], ab=p[q+5], pad=p[q+6], led=p[q+7];
|
|
|
+ const q=73+et*9;
|
|
|
+ const back=[p[q],p[q+1],p[q+2],p[q+3]], qam=p[q+4], ab=p[q+5], pad=p[q+6], led=p[q+7], rum=p[q+8];
|
|
|
rec.back.forEach((sel,i)=>{ if(document.activeElement!==sel) sel.value=back[i]; });
|
|
|
if(document.activeElement!==rec.qam) rec.qam.value=qam;
|
|
|
rec.abSwap.textContent=ab?"on":"off"; rec.abSwap.classList.toggle("active",!!ab);
|
|
|
rec.pad.textContent=pad?"on":"off"; rec.pad.classList.toggle("active",!!pad);
|
|
|
+ rec.rumble.textContent=rum?"on":"off"; rec.rumble.classList.toggle("active",!!rum);
|
|
|
if(document.activeElement!==rec.led){ rec.led.value=led; rec.ledV.textContent=(led===0)?"Auto":led+"%"; }
|
|
|
// dot marks the type matching the current puck mode
|
|
|
rec.activeDot.style.display = (et===activeEt) ? "" : "none";
|
|
|
@@ -978,8 +982,8 @@ function buildBackup(p, bp){
|
|
|
for(let s=0;s<4;s++){ const rec=bp.slice(2+s*24, 2+s*24+24);
|
|
|
bonds.push({slot:s, used:!!((mask>>s)&1), rec:hexEnc(rec)}); }
|
|
|
const types=[];
|
|
|
- for(let et=0; et<TYPE_DEFS.length; et++){ const q=73+et*8;
|
|
|
- types.push({back:[p[q],p[q+1],p[q+2],p[q+3]], qam:p[q+4], abSwap:p[q+5], pad:p[q+6], led:p[q+7]}); }
|
|
|
+ for(let et=0; et<TYPE_DEFS.length; et++){ const q=73+et*9;
|
|
|
+ types.push({back:[p[q],p[q+1],p[q+2],p[q+3]], qam:p[q+4], abSwap:p[q+5], pad:p[q+6], led:p[q+7], rumble:p[q+8]}); }
|
|
|
const cfg={ mode:p[1], mDiv:p[2], mFric:p[3], persistMode:p[22], chord:[p[23],p[24],p[25]],
|
|
|
rumble:p[51], swProRate:p[52], swGyro10:p[53], types };
|
|
|
// version 2: include the lizard map so it survives a backup/restore cycle.
|
|
|
@@ -1029,10 +1033,10 @@ async function importBackup(file){
|
|
|
modalOpen("Restore backup");
|
|
|
const c=obj.config;
|
|
|
// Pre-count every write so the bar tracks real progress: settings (9 base fields), per-type config
|
|
|
- // (8 each), the lizard bindings, 4 bond slots, and the final commit.
|
|
|
+ // (9 each), the lizard bindings, 4 bond slots, and the final commit.
|
|
|
const typeN=Array.isArray(c.types)?Math.min(c.types.length,4):0;
|
|
|
const lizardN=(Array.isArray(c.lizardMap) && lizardCapable())?Math.min(c.lizardMap.length,LZ_MAX):0;
|
|
|
- const total=9 + typeN*8 + lizardN + 4 + 1;
|
|
|
+ const total=9 + typeN*9 + lizardN + 4 + 1;
|
|
|
let step=0, stage="";
|
|
|
const tick=()=>modalStage(stage, Math.min(99,Math.floor(step*100/total)));
|
|
|
try{
|
|
|
@@ -1061,10 +1065,11 @@ async function importBackup(file){
|
|
|
// was added to the panel export; guard so a missing field never crashes the import mid-flight.
|
|
|
if(typeN){
|
|
|
for(let et=0; et<typeN; et++){ const t=c.types[et];
|
|
|
- for(let k=0;k<4;k++) await sf(40+et*8+k, t.back[k]);
|
|
|
- await sf(40+et*8+4, t.qam); await sf(40+et*8+5, t.abSwap?1:0);
|
|
|
- await sf(40+et*8+6, t.pad?1:0); await sf(40+et*8+7, t.led); }
|
|
|
- log("settings replayed ("+(9+typeN*8)+" fields)");
|
|
|
+ for(let k=0;k<4;k++) await sf(40+et*9+k, t.back[k]);
|
|
|
+ await sf(40+et*9+4, t.qam); await sf(40+et*9+5, t.abSwap?1:0);
|
|
|
+ await sf(40+et*9+6, t.pad?1:0); await sf(40+et*9+7, t.led);
|
|
|
+ await sf(40+et*9+8, t.rumble!==undefined?t.rumble:1); }
|
|
|
+ log("settings replayed ("+(9+typeN*9)+" fields)");
|
|
|
} else { log("settings replayed (9 fields — no per-type config in this backup)"); }
|
|
|
// restore the lizard map if the backup includes it (version 2+) and the puck speaks v16+.
|
|
|
// pre-lizard-map backups simply lack c.lizardMap; skip silently so they still import cleanly.
|