|
|
@@ -65,9 +65,12 @@
|
|
|
<span id="connState" class="pill dn">disconnected</span>
|
|
|
<button id="connectBtn" class="primary">Connect</button>
|
|
|
<span class="spacer"></span>
|
|
|
+ <input id="uf2File" type="file" accept=".uf2,application/octet-stream" style="display:none">
|
|
|
+ <button id="uf2Pick" title="Choose a local .uf2 firmware file">Pick UF2 file</button>
|
|
|
+ <button id="uf2Flash" disabled title="Copy the selected .uf2 onto the UF2BOOT/NRF52BOOT drive">Write UF2 file</button>
|
|
|
<button id="debugCdc" title="Reboot ONCE with the CDC serial console enabled (WebUSB goes away for that boot); auto-reverts on the next boot">Debug CDC</button>
|
|
|
<button id="dfuSerial" title="Reboot into serial DFU (adafruit-nrfutil), then replug">Serial DFU</button>
|
|
|
- <button id="dfuUf2" title="Reboot into the UF2 mass-storage bootloader; drop a .uf2 on the drive">UF2 DFU</button>
|
|
|
+ <button id="dfuUf2" title="Reboot into the UF2 mass-storage bootloader; then use Write UF2 file">UF2 DFU</button>
|
|
|
<button id="factoryErase" class="danger" title="Wipe ALL bonds + settings and reboot to defaults. You must re-pair. Cannot be undone.">⚠ Factory erase</button>
|
|
|
</div>
|
|
|
<div class="wrap">
|
|
|
@@ -249,6 +252,7 @@ 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;
|
|
|
+let uf2Picked=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;
|
|
|
@@ -263,6 +267,20 @@ function renderHangLog(){
|
|
|
+ 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 updateUf2UI(){
|
|
|
+ const pick=$("#uf2Pick"), flash=$("#uf2Flash");
|
|
|
+ if(!pick || !flash) return;
|
|
|
+ const okFs = typeof window.showDirectoryPicker === "function";
|
|
|
+ flash.disabled = !uf2Picked || !okFs;
|
|
|
+ pick.textContent = uf2Picked ? ("UF2: " + uf2Picked.name) : "Pick UF2 file";
|
|
|
+ if(!okFs){
|
|
|
+ flash.title = "Your browser does not support writing files from the panel";
|
|
|
+ } else if(!uf2Picked){
|
|
|
+ flash.title = "Pick a .uf2 file first";
|
|
|
+ } else {
|
|
|
+ flash.title = "Copy " + uf2Picked.name + " onto the UF2BOOT/NRF52BOOT drive";
|
|
|
+ }
|
|
|
+}
|
|
|
function updateStabUI(){
|
|
|
const btn=$("#stabBtn"), st=$("#stabStatus");
|
|
|
if(!btn) return;
|
|
|
@@ -1062,6 +1080,31 @@ async function startPolling(){
|
|
|
|
|
|
// UI wiring
|
|
|
$("#connectBtn").onclick=connect;
|
|
|
+$("#uf2Pick").onclick=()=>{ $("#uf2File").click(); };
|
|
|
+$("#uf2File").onchange=(e)=>{
|
|
|
+ uf2Picked = e.target.files && e.target.files[0] ? e.target.files[0] : null;
|
|
|
+ updateUf2UI();
|
|
|
+ if(uf2Picked) log("UF2 selected: " + uf2Picked.name);
|
|
|
+};
|
|
|
+$("#uf2Flash").onclick=async()=>{
|
|
|
+ if(!uf2Picked){ log("pick a UF2 file first"); return; }
|
|
|
+ if(typeof window.showDirectoryPicker !== "function"){
|
|
|
+ log("browser file-write API unavailable — use UF2 DFU then drag/drop manually");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if(!confirm("Copy " + uf2Picked.name + " to the UF2BOOT/NRF52BOOT drive?\n\nIf the puck is not already in UF2 bootloader mode, click UF2 DFU first.")) return;
|
|
|
+ try{
|
|
|
+ const dir = await window.showDirectoryPicker({mode:"readwrite"});
|
|
|
+ const buf = await uf2Picked.arrayBuffer();
|
|
|
+ const fh = await dir.getFileHandle(uf2Picked.name, {create:true});
|
|
|
+ const wr = await fh.createWritable();
|
|
|
+ await wr.write(buf);
|
|
|
+ await wr.close();
|
|
|
+ log("UF2 copied: " + uf2Picked.name + " (" + Math.round(buf.byteLength/1024) + " KiB). Device should reboot after copy.");
|
|
|
+ }catch(e){
|
|
|
+ log("UF2 copy failed: " + e.message);
|
|
|
+ }
|
|
|
+};
|
|
|
$("#backupExport").onclick=exportBackup;
|
|
|
$("#backupImport").onclick=()=>$("#backupFile").click();
|
|
|
$("#backupFile").onchange=async(e)=>{ const f=e.target.files[0]; if(f) await importBackup(f); e.target.value=""; };
|
|
|
@@ -1131,6 +1174,7 @@ for(const b of document.querySelectorAll(".modebtn")){
|
|
|
const msg="Switch to "+MODE_NAMES[m]+"? The copycat will reboot."+(clean?"\n\nNOTE: the game/clean PlayStation modes (and PS3) drop WebUSB + host-wake, so THIS PANEL WILL DISCONNECT and can't reach the device while it's in this mode. To get back, chord on the controller: hold back-paddle 4 + A to return to Steam mode.":"");
|
|
|
if(confirm(msg)){ send([0x03, m]); log("mode switch requested — device will reboot"); } };
|
|
|
}
|
|
|
+updateUf2UI();
|
|
|
if(!("usb" in navigator)) $("#connectBtn").outerHTML='<b style="color:var(--bad)">WebUSB not supported — use Chrome or Edge.</b>';
|
|
|
else autoConnect(); // reopen an already-authorized puck on load, no picker (first-ever connect still needs the button)
|
|
|
</script>
|