Jelajahi Sumber

Add one-shot UF2-on-reboot update mode

copilot-swe-agent[bot] 3 bulan lalu
induk
melakukan
baf76e6cb9

+ 4 - 0
OpenPuck/OpenPuck.ino

@@ -112,6 +112,10 @@ void setup()
 	factoryResetOnce(OPK_GIT_HASH);
 #endif
 	loadCfg();
+	if (g_bootUf2ThisBoot) {
+		delay(40);
+		enterUf2Dfu();
+	}
 	loadBonds();
 	// regenerate per-slot session addresses from each bond UUID (deterministic, stable across reboots)
 	for (int s = 0; s < NSLOT; s++)

+ 30 - 3
OpenPuck/config.cpp

@@ -13,11 +13,15 @@ uint8_t g_chordBtn[3] = {
 }; // back4+B/X/Y -> these modes (A always STEAM); Y defaults to Switch Pro
 bool g_persistMode = false;
 uint8_t g_bootMode = 0xFF;
+uint8_t g_bootUf2 = 0;
+bool g_bootUf2ThisBoot = false;
 
 bool g_debugCdcThisBoot = false;
 
 // persisted one-shot arm, stored in Cfg.rsvd0 (1 = keep CDC for the next boot)
 static uint8_t g_debugCdc = 0;
+// one-shot UF2 arm token, stored in Cfg.rxWin10 (legacy field no longer used)
+static const uint8_t BOOT_UF2_ARM = 0xA5;
 
 int g_mDiv = 64, g_mFric = 94;
 
@@ -76,7 +80,8 @@ const uint32_t g_pollUs = POLL_US_DEFAULT;
 struct Cfg {
 	uint8_t magic, mode, mDiv, mFric, rsvd0, pollU100, persistMode,
 		bootMode, chordBtn[3], rumbleScale;
-	// rxWin10: legacy RF tunable slot (window now fixed; ignored). lizKeep: the id9=0 hold enable (see
+	// rxWin10: one-shot UF2 boot arm token (legacy RF tunable slot, window now fixed). lizKeep: the id9=0 hold
+	// enable (see
 	// haptics.h LIZKEEP_MS). landAll87: the verbatim-0x87-relay experiment toggle (haptics.h g_landAll87).
 	uint8_t rxWin10, lizKeep, landAll87;
 	TypeCfg type[ET_COUNT]; // per-emulated-type back/qam/abSwap/padHaptics
@@ -94,7 +99,7 @@ void saveCfg()
 		  g_bootMode,
 		  { g_chordBtn[0], g_chordBtn[1], g_chordBtn[2] },
 		  g_rumbleScale,
-		  (uint8_t)(g_rxWin / 10),
+		  g_bootUf2,
 		  g_lizKeep,
 		  g_landAll87,
 		  {} };
@@ -127,6 +132,12 @@ void loadCfg()
 				g_debugCdc = 0;
 				consume = true;
 			}
+			// one-shot UF2 boot (Cfg.rxWin10): honor once, then clear.
+			g_bootUf2ThisBoot = (c.rxWin10 == BOOT_UF2_ARM);
+			if (g_bootUf2ThisBoot) {
+				g_bootUf2 = 0;
+				consume = true;
+			}
 			// poll rate is fixed; rewrite cfg so the persisted byte matches the new default.
 			if (c.pollU100 != (uint8_t)(POLL_US_DEFAULT / 100))
 				consume = true;
@@ -158,7 +169,11 @@ void loadCfg()
 			// verbatim-0x87-relay experiment toggle (0/1; default off)
 			if (c.landAll87 <= 1)
 				g_landAll87 = c.landAll87;
-			// The poll RX window is now FIXED (g_rxWin is const) -- any persisted rxWin10 is ignored.
+			// Keep only our explicit arm token; old legacy values never arm UF2 by accident.
+			if (c.rxWin10 == BOOT_UF2_ARM)
+				g_bootUf2 = BOOT_UF2_ARM;
+			else
+				g_bootUf2 = 0;
 		}
 		f.close();
 	}
@@ -188,6 +203,18 @@ void armDebugCdcNextBoot()
 	saveCfg();
 } // next boot keeps CDC; loadCfg() consumes it after
 
+void armUf2NextBoot()
+{
+	g_bootUf2 = BOOT_UF2_ARM;
+	saveCfg();
+}
+
+void clearUf2NextBoot()
+{
+	g_bootUf2 = 0;
+	saveCfg();
+}
+
 // FULL factory wipe: reformat the internal LittleFS, erasing cfg.bin (modes/tunables/chords) AND bonds.bin
 // (paired-controller record). Caller reboots: next boot finds no files and falls back to clean defaults, and
 // the controller must be re-paired. Irreversible -- gated behind explicit confirmation at every call site.

+ 6 - 0
OpenPuck/config.h

@@ -103,6 +103,12 @@ extern uint8_t g_chordBtn[3]; // back4+B/X/Y -> these modes (A always STEAM)
 extern bool g_persistMode;
 // one-shot: boot into this mode once then clear (!persistMode + explicit switch)
 extern uint8_t g_bootMode;
+// one-shot: reboot into UF2 bootloader once (apply staged update), then clear
+extern uint8_t g_bootUf2;
+// decision for THIS boot: true => enter UF2 bootloader before normal init
+extern bool g_bootUf2ThisBoot;
+void armUf2NextBoot(); // persist the one-shot UF2 arm (caller reboots later)
+void clearUf2NextBoot(); // clear a previously-armed one-shot UF2 entry
 
 // One-shot debug CDC. Puck mode normally DROPS the CDC serial console to free the USB endpoint its wake-mouse
 // interface needs (to wake a sleeping Windows host). Arming this keeps CDC for the NEXT boot only -- dropping

+ 9 - 1
OpenPuck/serial_console.cpp

@@ -12,7 +12,7 @@
 #include <stdlib.h>
 #include <string.h>
 
-// CDC commands: l=listen, s=stop, cN=channel, p<hex>=prefix, a<8hex>=base addr, b=bonds, etc.
+// CDC commands: l=listen, s=stop, cN=channel, p<hex>=prefix, a<8hex>=base addr, b=bonds, UF2NEXT, etc.
 // Command order matters: else-if chain, FIRST matching letter wins. 'C' and 'H' appear twice; the second
 // occurrence is unreachable.
 void serialConsolePoll()
@@ -53,6 +53,14 @@ void serialConsolePoll()
 					"# feature-cmd capture %s (I45 %s)\n",
 					g_cmdCapture ? "ON" : "off",
 					g_cmdCapture ? "suppressed" : "on");
+			} else if (!strcmp(line, "UF2NEXT")) {
+				armUf2NextBoot();
+				Serial.println(
+					"# UF2 bootloader armed for NEXT reboot (firmware update applies on reboot)");
+			} else if (!strcmp(line, "UF2CLR")) {
+				clearUf2NextBoot();
+				Serial.println(
+					"# UF2 next-boot arm cleared (normal app boot on reboot)");
 			} else if (!strcmp(line, "L87")) {
 				// EXPERIMENT: land all relayed 0x87 config verbatim (real-puck relay) vs the discard-whitelist.
 				// Exact-match string (single letters are all taken; 'l' alone = rfListenStart).

+ 16 - 3
OpenPuck/webusb_config.cpp

@@ -39,7 +39,8 @@ static volatile bool g_bondExportRequest = false;
 //                [v13: per-slot link stats, 4x9B from p[141]: {pollsps u16, f1ps u16, newps u16, crc/s u8,
 //                 noRx/s u8, relay/s u8} -- each controller's own rates (the v4 aggregates are their sums)]
 //                [v14: p[177] landAll87 (verbatim-0x87-relay experiment toggle)]
-#define WB_PAYLEN 176
+//                [v15: p[178] uf2NextBoot armed (1=next reboot enters UF2 bootloader)]
+#define WB_PAYLEN 177
 // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob
 // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank
 // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without
@@ -64,8 +65,10 @@ static void webusbSendBlob()
 	p[0] = 0xA5;
 	p[1] = WB_PAYLEN;
 
-	// protocol version (14 = +landAll87 toggle; 13 = +per-slot link stats; 12 = +relay rate + clock fingerprint; 11 = +reset cause; 10 = +ledBright per type; 9 = +per-type cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
-	p[2] = 14;
+	// protocol version (15 = +UF2-next-boot arm state; 14 = +landAll87 toggle; 13 = +per-slot link stats; 12
+	// = +relay rate + clock fingerprint; 11 = +reset cause; 10 = +ledBright per type; 9 = +per-type cfg; 8 =
+	// +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
+	p[2] = 15;
 	p[3] = g_usbMode;
 	p[4] = (uint8_t)g_mDiv;
 	p[5] = (uint8_t)g_mFric;
@@ -231,6 +234,8 @@ static void webusbSendBlob()
 	}
 	// v14: verbatim-0x87-relay experiment toggle (panel reflects + toggles it)
 	p[177] = g_landAll87;
+	// v15: staged update arm -- next reboot goes straight to UF2 bootloader.
+	p[178] = g_bootUf2 ? 1 : 0;
 	// CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the
 	// panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a
 	// backgrounded tab, or the host briefly not servicing transferIn under load -- the FIFO never empties and
@@ -780,6 +785,14 @@ void webusbPoll()
 				case 29:
 					g_landAll87 = v ? 1 : 0;
 					break;
+				// one-shot UF2 updater mode: arm/clear "enter UF2 on NEXT reboot".
+				case 30:
+					if (v)
+						armUf2NextBoot();
+					else
+						clearUf2NextBoot();
+					persist = false;
+					break;
 				}
 				if (persist)
 					saveCfg();

+ 1 - 0
OpenPuck/webusb_config.h

@@ -2,6 +2,7 @@
 // read/set runtime tunables. Binary framed protocol:
 //   host->dev:  0x01                 GET  -> reply status blob
 //               0x02 <field> <value> SET one byte field (see the switch in webusb_config.cpp)
+//                                      field 30 arms/clears "enter UF2 on next reboot" (1/0)
 //               0x03 <mode>          switch USB mode (0..6): persist + reboot
 //               0x0B                 reboot into serial DFU (adafruit-nrfutil)
 //               0x0C                 reboot into UF2 bootloader (USB mass storage)

+ 1 - 1
README.md

@@ -50,7 +50,7 @@ Additionally it has some shortcuts that might be useful: Steam + L5/R5 will do v
 and Steam + L4 ls Ctrl + Alt + Delete.
 
 # How to install/use it
-You will need to flash OpenPuck.ino onto a compatible microcontroller. The easiest way to do this is to grab a uf2 file from the GitHub releases and drag and drop it onto the folder that the microcontroller mounts when in DFU mode. Fresh microcontrollers should already be in DFU mode and present like a flash drive. If they are not in DFU mode you'll need to short the RST and GND pints twice in quick succession. If you've already flashed openpuck you can go to the [webusb configurator](https://safijari.github.io/openpuck/), connect to the OpenPuck, and then press the `UF2 DFU` button on the top bar.
+You will need to flash OpenPuck.ino onto a compatible microcontroller. The easiest way to do this is to grab a uf2 file from the GitHub releases and drag and drop it onto the folder that the microcontroller mounts when in DFU mode. Fresh microcontrollers should already be in DFU mode and present like a flash drive. If they are not in DFU mode you'll need to short the RST and GND pints twice in quick succession. If you've already flashed openpuck you can go to the [webusb configurator](https://safijari.github.io/openpuck/), connect to the OpenPuck, and then press the `UF2 DFU` button on the top bar. If you want to stage it for later, `UF2 on reboot` arms a one-shot flag so the *next* reboot lands in UF2 bootloader automatically.
 
 See [build instructions document](./docs/BUILD_AND_DEPLOY.md) for  details on how to flash the MCU during development.
 

+ 4 - 0
docs/BUILD_AND_DEPLOY.md

@@ -135,6 +135,10 @@ If the board has the **Adafruit nRF52 UF2 bootloader** (common on Pro Micro nRF5
 
 3. **Copy the `.uf2` file** onto the UF2BOOT drive. The board auto-ejects and reboots with the new firmware.
 
+You can also arm a **deferred UF2 update** from the running firmware (no immediate reboot): in the WebUSB panel,
+click **UF2 on reboot** so it shows "armed". The device stores a one-shot flag in config; on the **next reboot**
+it jumps straight to UF2 bootloader before normal app init. Use the same button again to clear the arm.
+
 Alternatively, use the **adafruit-nrfutil DFU** Python tool with the board in DFU mode (bootloader LED pulsing):
 
 ```bash

+ 9 - 0
docs/index.html

@@ -71,6 +71,7 @@
   <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; then use Write UF2 file">UF2 DFU</button>
+  <button id="uf2Next" title="Arm/clear one-shot updater mode: the next reboot enters UF2 bootloader">UF2 on reboot</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">
@@ -766,6 +767,8 @@ function applyBlob(p){
   $("#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); }
+  // one-shot UF2 updater arm (blob v15, firmware p[178] -> JS p[176])
+  { const u=(p.length>176?p[176]:0); $("#uf2Next").textContent = u?"UF2 on reboot: armed":"UF2 on reboot"; $("#uf2Next").classList.toggle("active",!!u); }
   // per-type button config (blob v10): 4 types x 8 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){
@@ -1153,6 +1156,12 @@ $("#dfuUf2").onclick=async()=>{
   if(!confirm("Reboot into UF2 bootloader?\n\nThe puck will disconnect and mount as a USB drive. Drag the .uf2 file onto it to flash.")) return;
   await send([0x0C]); log("UF2 bootloader reboot sent — device disconnecting");
 };
+$("#uf2Next").onclick=async()=>{
+  if(!dev){ log("not connected"); return; }
+  const on=$("#uf2Next").classList.contains("active");
+  await setField(30, on?0:1);
+  log(on?"UF2-on-reboot cleared":"UF2-on-reboot armed — next reboot enters UF2 bootloader");
+};
 $("#factoryErase").onclick=async()=>{
   if(!dev){ log("not connected"); return; }
   if(!confirm("Factory erase?\n\nThis wipes ALL persistent storage on the copycat:\n  • the paired-controller bond (you'll have to re-pair)\n  • every saved setting (mode, chords, back paddles, sensitivity)\n\nThe copycat reboots to factory defaults. This CANNOT be undone.")) return;