Sfoglia il codice sorgente

panel: running hang log (per-reset table) + refute usbd-stack theory

Add a Hang log card to the WebUSB panel: one row per reset (time, uptime
before it, reason, stuck stage, captured PC, usbd stack free), accumulating
across resets in-page (survives the device reset; cleared by a page refresh).
Summary shows reset count + average uptime; CSV export + clear. Pairs with
the Test stability button for unattended runs.

Measured "usbd stack free" stays ~118 words (of 200), so the usbd-task stack
overflow hypothesis is refuted -- the hang is elsewhere. The log accumulates
reason/stage/PC across the frequent resets to expose the pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jari 3 mesi fa
parent
commit
fd54d19efc
1 ha cambiato i file con 44 aggiunte e 1 eliminazioni
  1. 44 1
      docs/index.html

+ 44 - 1
docs/index.html

@@ -186,6 +186,17 @@
       <p class="note">Import <strong>overwrites</strong> all pairings and settings on the connected puck, then reboots it (~2 s — reconnect after). Tip: pair every controller to one puck, export once, then import onto each spare.</p>
     </div>
 
+    <div class="card">
+      <h2>Hang log</h2>
+      <p class="note" style="margin-top:0">One row per reset: uptime before it, reason, the stuck loop stage and captured PC (when the watchdog ISR could run), and usbd stack free. Accumulates across resets in this tab — a page refresh clears it. Pair with <em>Test stability</em> above for unattended runs.</p>
+      <div class="row" style="margin:0 0 8px;flex-wrap:wrap">
+        <span class="v" id="hangSummary" style="flex:1">no resets logged yet</span>
+        <button id="hangCsv">Download CSV</button>
+        <button id="hangClear">Clear</button>
+      </div>
+      <pre id="hangOut" style="font:12px ui-monospace,Menlo,monospace;color:var(--fg);background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:10px;margin:0;max-height:260px;overflow:auto;white-space:pre"></pre>
+    </div>
+
     <div class="card logonly">
       <h2>Capture — host → controller commands</h2>
       <p class="note" style="margin-top:0">The firmware logs everything from boot into a big RAM ring: Steam's writes (<code>ifN</code>), the frames we transmit to the controller (<code>TX→ctlr</code>), and RF <code>LINK UP/DOWN</code> edges. For the reconnect buzz: trigger it (it appears moments after the puck boots / the controller reconnects), then connect here and click <b>Dump from boot</b> — the trigger is still in the ring. (Only present in a logging build, <code>-DOPK_LOG=1</code>.)</p>
@@ -210,6 +221,19 @@ let lastP=null; // most recent status-blob payload (config source for Export)
 // Stability test: armed = active across reconnects; stabStart = ms of the current run; stabLastRun = last
 // completed run {secs} (ended by a reset). Kept in-page so it survives the device reset (not a page refresh).
 let stabArmed=false, stabStart=0, stabLastRun=null;
+// Running hang log: one entry per reset (persists in-page across the device reset; a page refresh clears it).
+// pendingHang is set at disconnect and enriched with reason/PC/stack from the first blob after reconnect.
+let hangLog=[], pendingHang=null;
+function renderHangLog(){
+  const out=$("#hangOut"), sum=$("#hangSummary"); if(!out) return;
+  if(!hangLog.length){ out.textContent=""; sum.textContent="no resets logged yet"; return; }
+  const ups=hangLog.filter(e=>e.uptime!=null).map(e=>e.uptime);
+  const avg=ups.length?Math.round(ups.reduce((a,b)=>a+b,0)/ups.length):null;
+  sum.textContent = hangLog.length+" reset(s)"+(avg!=null?("  ·  avg uptime "+fmtDur(avg)):"");
+  const pad=(s,n)=>String(s).padEnd(n);
+  out.textContent = pad("time",11)+pad("uptime",9)+pad("reason",16)+pad("stage",10)+pad("PC",11)+"usbd\n"
+    + hangLog.map(e=>pad(e.time,11)+pad(e.uptime!=null?fmtDur(e.uptime):"-",9)+pad(e.reason,16)+pad(e.stage||"-",10)+pad(e.pc||"-",11)+(e.usbd!=null?e.usbd:"-")).join("\n");
+}
 function fmtDur(s){ s=Math.round(s); return s>=60 ? (Math.floor(s/60)+"m "+(s%60)+"s") : (s+"s"); }
 function updateStabUI(){
   const btn=$("#stabBtn"), st=$("#stabStatus");
@@ -401,7 +425,10 @@ async function autoConnect(){
 function onGone(){
   // if a stability test is running, this disconnect is the reset that ended the run -- record its uptime
   // (kept in-page so it survives the reset + reconnect, shown next to Last reset).
-  if(stabArmed && stabStart){ stabLastRun={secs:(Date.now()-stabStart)/1000}; log("⏱ stability: stayed up "+fmtDur(stabLastRun.secs)+", then reset"); stabStart=0; }
+  let up=null;
+  if(stabArmed && stabStart){ up=(Date.now()-stabStart)/1000; stabLastRun={secs:up}; log("⏱ stability: stayed up "+fmtDur(up)+", then reset"); stabStart=0; }
+  // queue a hang-log entry; the reason/PC/stack are filled from the first blob after reconnect
+  pendingHang = { t: Date.now(), uptimeSecs: up };
   polling=false; dev=null; tabInited=false; // re-default the config tab to the mode on next connect
   $("#connState").textContent="reconnecting…"; $("#connState").className="pill dn";
   $("#panel").classList.add("hide");
@@ -584,6 +611,14 @@ function applyBlob(p){
       + (hpc ? "  hang PC="+hx(hpc)+" LR="+hx(hlr) : "")
       + (stabLastRun ? "  ·  stability: stayed up "+fmtDur(stabLastRun.secs)+" before this reset" : "");
     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);
+      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 });
+      pendingHang=null; renderHangLog();
+    }
   }
 
   const gitDirty=(p.length>38?p[38]:0); let gitHash="";
@@ -811,6 +846,14 @@ $("#stabBtn").onclick=async()=>{
   else { await send([0x0F,0]); stabStart=0; log("stability test stopped"); }
   updateStabUI();
 };
+$("#hangClear").onclick=()=>{ hangLog=[]; renderHangLog(); };
+$("#hangCsv").onclick=()=>{
+  const rows=[["time","uptime_s","reason","stage","pc","lr","usbd_free_words"]].concat(
+    hangLog.map(e=>[e.time, e.uptime!=null?Math.round(e.uptime):"", e.reason, e.stage, e.pc, e.lr, e.usbd]));
+  const csv=rows.map(r=>r.map(x=>'"'+String(x).replace(/"/g,'""')+'"').join(",")).join("\n");
+  const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"}));
+  a.download="openpuck-hanglog.csv"; a.click();
+};
 $("#hapClear").onclick=async()=>{ if(dev){ await send([0x07]); log("haptic re-init sent (clear stuck buzz)"); } };
 $("#ctlrOff").onclick=async()=>{ if(dev){ await send([0x08]); log("controller power-off attempt sent — watch link status"); } };
 $("#dfuSerial").onclick=async()=>{