Browse Source

Add WebUSB UF2 file write workflow

copilot-swe-agent[bot] 3 tháng trước cách đây
mục cha
commit
c6ac6792b1
1 tập tin đã thay đổi với 45 bổ sung và 1 xóa
  1. 45 1
      docs/index.html

+ 45 - 1
docs/index.html

@@ -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>