|
@@ -60,7 +60,15 @@
|
|
|
<span>P→C: <b id="npc">0</b></span>
|
|
<span>P→C: <b id="npc">0</b></span>
|
|
|
<span>C→P: <b id="ncp">0</b></span>
|
|
<span>C→P: <b id="ncp">0</b></span>
|
|
|
<span>crc-bad: <b id="nbad">0</b></span>
|
|
<span>crc-bad: <b id="nbad">0</b></span>
|
|
|
- <span class="note">filter <input type="text" id="filter" placeholder="hex e.g. 9f or e3" style="width:90px;background:#0d1117;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:3px;font:inherit"></span>
|
|
|
|
|
|
|
+ <span>shown: <b id="nshown">0</b></span>
|
|
|
|
|
+</div>
|
|
|
|
|
+<div class="stats" style="border-top:none;padding-top:0">
|
|
|
|
|
+ <label class="note"><input type="checkbox" id="fHideRoutine"> hide input/polls (49-byte F1 + bare E3)</label>
|
|
|
|
|
+ <label class="note">dir <select id="fDir" style="background:#0d1117;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:3px;font:inherit"><option value="">all</option><option value="P→C">P→C</option><option value="C→P">C→P</option></select></label>
|
|
|
|
|
+ <label class="note">op <input type="text" id="fOp" placeholder="f1" style="width:48px;background:#0d1117;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:3px;font:inherit"></label>
|
|
|
|
|
+ <label class="note">len <select id="fLen" style="background:#0d1117;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:3px;font:inherit"><option value="">any</option><option value="ne49">≠ 49 (non-input)</option><option value="gt49">> 49</option><option value="gt1">> 1</option></select></label>
|
|
|
|
|
+ <label class="note">hex <input type="text" id="filter" placeholder="9f" style="width:90px;background:#0d1117;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:3px;font:inherit"></label>
|
|
|
|
|
+ <span class="note">↑ to find battery: <b>hide input/polls</b> + dir <b>C→P</b>, then read the controller's battery in Steam.</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div id="tbl"><table><thead><tr>
|
|
<div id="tbl"><table><thead><tr>
|
|
|
<th>#</th><th>t (ms)</th><th>dir</th><th>ch</th><th>pipe</th><th>op</th><th>crc</th><th>rssi</th><th>len</th><th>payload (from opcode)</th>
|
|
<th>#</th><th>t (ms)</th><th>dir</th><th>ch</th><th>pipe</th><th>op</th><th>crc</th><th>rssi</th><th>len</th><th>payload (from opcode)</th>
|
|
@@ -123,7 +131,7 @@ function parse(){
|
|
|
}
|
|
}
|
|
|
if(acc[i]===0xC0 && acc[i+1]===0xDE){ // packet
|
|
if(acc[i]===0xC0 && acc[i+1]===0xDE){ // packet
|
|
|
if(i+3>acc.length) break;
|
|
if(i+3>acc.length) break;
|
|
|
- const n=acc[i+2]; if(n>66){ i++; continue; } // bad len -> resync
|
|
|
|
|
|
|
+ const n=acc[i+2]; if(n>98){ i++; continue; } // bad len -> resync (frames up to MAXLEN+2)
|
|
|
if(i+11+n>acc.length) break;
|
|
if(i+11+n>acc.length) break;
|
|
|
const t=acc[i+3]|(acc[i+4]<<8)|(acc[i+5]<<16)|(acc[i+6]*16777216);
|
|
const t=acc[i+3]|(acc[i+4]<<8)|(acc[i+5]<<16)|(acc[i+6]*16777216);
|
|
|
const ch=acc[i+7], crc=acc[i+8]&1, rssi=acc[i+9], match=acc[i+10];
|
|
const ch=acc[i+7], crc=acc[i+8]&1, rssi=acc[i+9], match=acc[i+10];
|
|
@@ -135,27 +143,50 @@ function parse(){
|
|
|
}
|
|
}
|
|
|
acc=acc.slice(i);
|
|
acc=acc.slice(i);
|
|
|
}
|
|
}
|
|
|
|
|
+let allFrames=[], seqCtr=0, nShown=0; // ring of recent frames so filters can re-render; seq = stable # column
|
|
|
|
|
+const FRAMEBUF=8000;
|
|
|
function onPacket(t_us,ch,crc,rssi,match,raw){
|
|
function onPacket(t_us,ch,crc,rssi,match,raw){
|
|
|
// raw = [LENGTH, S1, payload...]; opcode = raw[2]; dir from opcode high nibble
|
|
// raw = [LENGTH, S1, payload...]; opcode = raw[2]; dir from opcode high nibble
|
|
|
const op=raw.length>2?raw[2]:0;
|
|
const op=raw.length>2?raw[2]:0;
|
|
|
const dir=(op&0xF0)===0xE0?"P→C":(op&0xF0)===0xF0?"C→P":"?";
|
|
const dir=(op&0xF0)===0xE0?"P→C":(op&0xF0)===0xF0?"C→P":"?";
|
|
|
if(dir==="P→C")nPC++; else if(dir==="C→P")nCP++; if(!crc)nBad++;
|
|
if(dir==="P→C")nPC++; else if(dir==="C→P")nCP++; if(!crc)nBad++;
|
|
|
const payload=raw.slice(2); // from opcode onward (what we care about)
|
|
const payload=raw.slice(2); // from opcode onward (what we care about)
|
|
|
- const ent={t_us,ms:(t_us/1000),ch,pipe:match,dir,op:op.toString(16).padStart(2,"0"),crc,rssi:-rssi,len:raw.length>0?raw[0]:0,raw:hex(raw),payload:hex(payload)};
|
|
|
|
|
|
|
+ const ent={seq:++seqCtr,t_us,ms:(t_us/1000),ch,pipe:match,dir,op:op.toString(16).padStart(2,"0"),crc,rssi:-rssi,len:raw.length>0?raw[0]:0,raw:hex(raw),payload:hex(payload)};
|
|
|
if(capturing) rec.push(ent);
|
|
if(capturing) rec.push(ent);
|
|
|
- addRow(ent);
|
|
|
|
|
|
|
+ allFrames.push(ent); if(allFrames.length>FRAMEBUF) allFrames.shift();
|
|
|
|
|
+ if(passesFilter(ent)) addRow(ent);
|
|
|
$("#npc").textContent=nPC; $("#ncp").textContent=nCP; $("#nbad").textContent=nBad; $("#nrec").textContent=rec.length;
|
|
$("#npc").textContent=nPC; $("#ncp").textContent=nCP; $("#nbad").textContent=nBad; $("#nrec").textContent=rec.length;
|
|
|
}
|
|
}
|
|
|
-const rows=$("#rows"); let rowi=0; const flt=()=>$("#filter").value.trim().toLowerCase();
|
|
|
|
|
|
|
+const rows=$("#rows");
|
|
|
|
|
+function passesFilter(e){
|
|
|
|
|
+ if($("#fHideRoutine").checked){ // drop the 99% spam: input replies + bare polls
|
|
|
|
|
+ if(e.dir==="C→P" && e.op==="f1" && e.len===49) return false;
|
|
|
|
|
+ if(e.dir==="P→C" && e.op==="e3" && e.len<=1) return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const d=$("#fDir").value; if(d && e.dir!==d) return false;
|
|
|
|
|
+ const op=$("#fOp").value.trim().toLowerCase(); if(op && e.op!==op) return false;
|
|
|
|
|
+ const lf=$("#fLen").value;
|
|
|
|
|
+ if(lf==="ne49" && e.len===49) return false;
|
|
|
|
|
+ if(lf==="gt49" && e.len<=49) return false;
|
|
|
|
|
+ if(lf==="gt1" && e.len<=1) return false;
|
|
|
|
|
+ const hx=$("#filter").value.trim().toLowerCase().replace(/ /g,""); if(hx && !e.payload.replace(/ /g,"").includes(hx)) return false;
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
function addRow(e){
|
|
function addRow(e){
|
|
|
- const f=flt(); if(f && !e.payload.replace(/ /g,"").includes(f.replace(/ /g,""))) return;
|
|
|
|
|
const tr=document.createElement("tr");
|
|
const tr=document.createElement("tr");
|
|
|
tr.className=(e.dir==="P→C"?"pc":e.dir==="C→P"?"cp":"")+(e.crc?"":" bad");
|
|
tr.className=(e.dir==="P→C"?"pc":e.dir==="C→P"?"cp":"")+(e.crc?"":" bad");
|
|
|
- tr.innerHTML=`<td>${++rowi}</td><td>${e.ms.toFixed(1)}</td><td class="dir">${e.dir}</td><td>${e.ch}</td><td>${e.pipe}</td><td>${e.op}</td><td>${e.crc?"ok":"BAD"}</td><td>${e.rssi}</td><td>${e.len}</td><td class="hex">${e.payload}</td>`;
|
|
|
|
|
|
|
+ tr.innerHTML=`<td>${e.seq}</td><td>${e.ms.toFixed(1)}</td><td class="dir">${e.dir}</td><td>${e.ch}</td><td>${e.pipe}</td><td>${e.op}</td><td>${e.crc?"ok":"BAD"}</td><td>${e.rssi}</td><td>${e.len}</td><td class="hex">${e.payload}</td>`;
|
|
|
rows.appendChild(tr);
|
|
rows.appendChild(tr);
|
|
|
- while(rows.childElementCount>MAXROWS) rows.removeChild(rows.firstChild);
|
|
|
|
|
|
|
+ nShown++;
|
|
|
|
|
+ while(rows.childElementCount>MAXROWS){ rows.removeChild(rows.firstChild); }
|
|
|
|
|
+ $("#nshown").textContent=nShown;
|
|
|
const w=$("#tbl"); if(w.scrollTop+w.clientHeight>=w.scrollHeight-40) w.scrollTop=w.scrollHeight;
|
|
const w=$("#tbl"); if(w.scrollTop+w.clientHeight>=w.scrollHeight-40) w.scrollTop=w.scrollHeight;
|
|
|
}
|
|
}
|
|
|
|
|
+function applyFilters(){ // re-render visible table from the buffer with current filters
|
|
|
|
|
+ rows.innerHTML=""; nShown=0;
|
|
|
|
|
+ for(const e of allFrames.slice(-MAXROWS)) if(passesFilter(e)) addRow(e);
|
|
|
|
|
+}
|
|
|
|
|
+for(const id of ["fHideRoutine","fDir","fOp","fLen","filter"]) $("#"+id).addEventListener("input",applyFilters);
|
|
|
async function readLoop(){
|
|
async function readLoop(){
|
|
|
while(reading && dev){
|
|
while(reading && dev){
|
|
|
try{
|
|
try{
|
|
@@ -180,7 +211,7 @@ $("#cap").onclick=async()=>{
|
|
|
else{ await cmd(0x02); log(`CAP ENDED — ${rec.length} packets recorded`); }
|
|
else{ await cmd(0x02); log(`CAP ENDED — ${rec.length} packets recorded`); }
|
|
|
$("#nrec").textContent=rec.length;
|
|
$("#nrec").textContent=rec.length;
|
|
|
};
|
|
};
|
|
|
-$("#clear").onclick=()=>{ rec=[]; rows.innerHTML=""; rowi=0; nPC=nCP=nBad=0; $("#nrec").textContent=0; $("#npc").textContent=0; $("#ncp").textContent=0; $("#nbad").textContent=0; };
|
|
|
|
|
|
|
+$("#clear").onclick=()=>{ rec=[]; allFrames=[]; seqCtr=0; rows.innerHTML=""; nPC=nCP=nBad=nShown=0; for(const id of ["nrec","npc","ncp","nbad","nshown"]) $("#"+id).textContent=0; };
|
|
|
$("#reacq").onclick=()=>{ $("#pinCh").value=""; cmd(0x03); log("re-acquire requested"); };
|
|
$("#reacq").onclick=()=>{ $("#pinCh").value=""; cmd(0x03); log("re-acquire requested"); };
|
|
|
let surveying=false;
|
|
let surveying=false;
|
|
|
$("#survey").onclick=()=>{ surveying=!surveying; $("#survey").classList.toggle("active",surveying); cmd(0x06); log(surveying?"SURVEY on — listing every E1 beacon on ch2 (no lock). Each distinct 'op e1' payload = one puck/slot session.":"survey off"); };
|
|
$("#survey").onclick=()=>{ surveying=!surveying; $("#survey").classList.toggle("active",surveying); cmd(0x06); log(surveying?"SURVEY on — listing every E1 beacon on ch2 (no lock). Each distinct 'op e1' payload = one puck/slot session.":"survey off"); };
|
|
@@ -199,7 +230,6 @@ $("#dltxt").onclick=()=>{
|
|
|
const lines=rec.map(e=>`${e.ms.toFixed(1)}ms ch${e.ch} ${e.dir} ${e.crc?"":"BAD "}rssi${e.rssi} op=${e.op} | ${e.payload}`);
|
|
const lines=rec.map(e=>`${e.ms.toFixed(1)}ms ch${e.ch} ${e.dir} ${e.crc?"":"BAD "}rssi${e.rssi} op=${e.op} | ${e.payload}`);
|
|
|
save("sniff-"+Date.now()+".txt", lines.join("\n"), "text/plain");
|
|
save("sniff-"+Date.now()+".txt", lines.join("\n"), "text/plain");
|
|
|
};
|
|
};
|
|
|
-$("#filter").oninput=()=>{}; // filter applies to new rows; Clear to re-filter the view
|
|
|
|
|
if(!navigator.usb) log("This browser has no WebUSB. Use Chrome/Edge over https or localhost.");
|
|
if(!navigator.usb) log("This browser has no WebUSB. Use Chrome/Edge over https or localhost.");
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|