Parcourir la source

Add a mode to do a full board wipe

Jari il y a 3 mois
Parent
commit
c6d256cac0

+ 4 - 0
OpenPuck/OpenPuck.ino

@@ -101,6 +101,10 @@ void usbReenumerate(uint8_t k)
 
 void setup()
 {
+	// full-board wipe (debug-only "erase everything"): if the panel armed one, this never returns -- it erases
+	// the app + config/bond + bootloader-settings flash from RAM and resets into the app-less UF2 bootloader.
+	// Checked before the staged-update apply (both use the meta page; only one can be armed at a time).
+	fwupWipeIfArmed();
 	// staged firmware update (WebUSB "flash on reboot"): if the panel committed one, this never returns --
 	// it copies staged->app from RAM and resets into the new firmware. MUST run before anything else touches
 	// hardware; the board looks dead for the ~5 s the copy takes.

+ 95 - 0
OpenPuck/fw_update.cpp

@@ -13,6 +13,10 @@
 #define FWUP_APP_BASE 0x26000UL
 #define FWUP_APP_END 0xED000UL
 #define FWUP_META 0xEC000UL
+// internal LittleFS (cfg.bin + bonds.bin) sits between the app region and the bootloader; wiped whole by the
+// full-board erase (config.cpp's factoryErase() only reformats it -- this leaves it raw-erased).
+#define FWUP_FS_BASE 0xED000UL
+#define FWUP_FS_END 0xF4000UL
 #define FWUP_BL_SETTINGS 0xFF000UL
 #define FWUP_PAGE 4096UL
 // Image cap. Also what makes the apply copy safe from self-overlap: dst ends at most at 0x26000+0x60000 =
@@ -26,6 +30,9 @@
 static const char FWUP_TAG[] = "OPK-FWUP-v1";
 
 #define FWUP_META_MAGIC 0x32465055UL // "UPF2"
+// Full-board-wipe arm marker, written to the meta page instead of a FwupMeta. Distinct magic + its bitwise
+// complement in the next word so neither an erased page (0xFFFFFFFF) nor a stray FwupMeta can spoof the arm.
+#define FWUP_WIPE_MAGIC 0x57495045UL // "WIPE"
 struct FwupMeta {
 	uint32_t magic, size, crc, staged;
 	uint32_t metaCrc; // CRC32 over the four fields above
@@ -202,6 +209,18 @@ void fwupAbort(void)
 	nvmcErasePage(FWUP_META);
 }
 
+// Arm a full-board wipe: erase the meta page and stamp the wipe marker (self-complementary pair). Runs at
+// RUNTIME on the meta page only -- the same NVMC path fwupBegin()/fwupChunk() already use during a live
+// transfer, never the app region we execute from. Any staged update is dropped (its bytes are about to be
+// erased anyway). The caller reboots; fwupWipeIfArmed() does the erase from RAM at the next boot.
+void fwupArmFullWipe(void)
+{
+	s_active = false;
+	uint32_t w[2] = { FWUP_WIPE_MAGIC, ~FWUP_WIPE_MAGIC };
+	nvmcErasePage(FWUP_META);
+	nvmcWriteWords(FWUP_META, w, 2);
+}
+
 // ---- boot-time apply ---------------------------------------------------------------------------------------
 // Runs entirely from RAM (.data section => copied out of flash by the startup code) with interrupts off: it
 // erases and rewrites the very flash the rest of the firmware executes from, so from the first erase to the
@@ -367,3 +386,79 @@ void fwupApplyIfArmed(void)
 	for (;;) {
 	} // unreachable (ramApply resets)
 }
+
+// ---- full-board wipe (debug-only "erase everything") -------------------------------------------------------
+// Like ramApply, runs entirely from RAM with interrupts off -- it erases the very flash it would otherwise
+// execute from, so no calls/libc/flash constants, only registers + arguments + NVMC. It erases the ENTIRE app
+// region, the LittleFS config/bond region, and the bootloader settings page; the MBR, SoftDevice, and the
+// bootloader itself are left intact so the board still enumerates as the UF2 mass-storage drive. Afterwards no
+// valid app exists, so the Adafruit bootloader stays in UF2 mode on EVERY boot until firmware is flashed again.
+// Power-cut safe by the same ordering as ramApply: the app vector page is erased FIRST, so the board reads
+// app-less from that instant on -- an interrupted wipe still lands in the safe UF2-bootloader state.
+FWUP_RAMFUNC static void ramWipe(uint32_t appBase, uint32_t appEnd,
+				 uint32_t fsBase, uint32_t fsEnd,
+				 uint32_t blSettings)
+{
+	NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Een;
+	while (!NRF_NVMC->READY) {
+	}
+	// app vector page first (word 0 = the bootloader's app-validity marker)
+	NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+	NRF_NVMC->ERASEPAGE = appBase;
+	while (!NRF_NVMC->READY) {
+	}
+	// rest of the app region (code, rodata, .data-init, staged-image scratch, the wipe marker itself)
+	for (uint32_t a = appBase + FWUP_PAGE; a < appEnd; a += FWUP_PAGE) {
+		NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+		NRF_NVMC->ERASEPAGE = a;
+		while (!NRF_NVMC->READY) {
+		}
+	}
+	// LittleFS: cfg.bin (settings) + bonds.bin (pairings) -- no user data survives
+	for (uint32_t a = fsBase; a < fsEnd; a += FWUP_PAGE) {
+		NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+		NRF_NVMC->ERASEPAGE = a;
+		while (!NRF_NVMC->READY) {
+		}
+	}
+	// bootloader settings: drop any "valid app" record so the boot check relies only on the (now erased)
+	// app vector -> UF2 mode every boot.
+	NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+	NRF_NVMC->ERASEPAGE = blSettings;
+	while (!NRF_NVMC->READY) {
+	}
+	NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Ren;
+	while (!NRF_NVMC->READY) {
+	}
+	// reset -- raw AIRCR write (keep NVIC_SystemReset's helpers out of the now-erased flash)
+	__asm volatile("dsb 0xF" ::: "memory");
+	*(volatile uint32_t *)0xE000ED0CUL = (0x5FAUL << 16) | (1UL << 2);
+	for (;;) {
+	}
+}
+
+void fwupWipeIfArmed(void)
+{
+	const volatile uint32_t *m = (const volatile uint32_t *)FWUP_META;
+	if (m[0] != FWUP_WIPE_MAGIC || m[1] != (uint32_t)~FWUP_WIPE_MAGIC)
+		return; // not armed (the common path): erased page or a FwupMeta -> nothing to do
+	// Point of no return -- identical freeze discipline to fwupApplyIfArmed: IRQs off, WDT forced on so a
+	// fault mid-erase still resets into the safe app-less state. The erase (~200 pages) drops USB the instant
+	// it starts; the WDT is reloaded per page so the ~18 s run never trips it.
+	__disable_irq();
+	if (!NRF_WDT->RUNSTATUS) {
+		NRF_WDT->CONFIG =
+			(WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos) |
+			(WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos);
+		NRF_WDT->CRV = 8UL * 32768UL - 1; // ~8 s
+		NRF_WDT->RREN = WDT_RREN_RR0_Msk;
+		NRF_WDT->TASKS_START = 1;
+	}
+	NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+	void (*volatile wipe)(uint32_t, uint32_t, uint32_t, uint32_t,
+			      uint32_t) = ramWipe;
+	wipe(FWUP_APP_BASE, FWUP_APP_END, FWUP_FS_BASE, FWUP_FS_END,
+	     FWUP_BL_SETTINGS);
+	for (;;) {
+	} // unreachable (ramWipe resets)
+}

+ 10 - 0
OpenPuck/fw_update.h

@@ -54,3 +54,13 @@ uint32_t fwupNextOff(
 // Apply a committed staged update. Call FIRST in setup(): if a valid meta page is present this never returns
 // (copies staged->app from RAM and resets). Invalid/stale meta is erased and ignored.
 void fwupApplyIfArmed(void);
+
+// Full-board wipe (debug-only "erase everything", 0x25). Unlike a factory reset -- which reformats LittleFS to
+// clean defaults but keeps the firmware -- this ERASES the entire app region, the LittleFS config/bond region,
+// and the bootloader settings page, leaving the board app-less. On every subsequent boot the Adafruit UF2
+// bootloader finds no valid app and stays in UF2 mass-storage (drag-and-drop) mode until firmware is flashed
+// again, so no trace of OpenPuck remains. The MBR/SoftDevice/bootloader are untouched (the board still mounts
+// as the UF2 drive). fwupArmFullWipe() stamps a marker + the caller reboots; fwupWipeIfArmed() does the erase
+// from RAM at the next boot (call BEFORE fwupApplyIfArmed) with the same power-cut-safe ordering as the applier.
+void fwupArmFullWipe(void);
+void fwupWipeIfArmed(void);

+ 17 - 1
OpenPuck/webusb_config.cpp

@@ -590,7 +590,7 @@ void webusbPoll()
 				break;
 			uint8_t op = buf[0];
 			if ((op < 0x01 || op > 0x15) &&
-			    (op < 0x20 || op > 0x24)) { // resync: drop one byte
+			    (op < 0x20 || op > 0x25)) { // resync: drop one byte
 				memmove(buf, buf + 1, --n);
 				continue;
 			}
@@ -615,6 +615,7 @@ void webusbPoll()
 				 op == 0x0F || op == 0x10 || op == 0x13) ?
 					       2 :
 				(op == 0x0A) ? 4 :
+				(op == 0x25) ? 5 :
 				(op == 0x20) ? 9 :
 				(op == 0x21) ? (uint8_t)(6 + (n >= 6 ? buf[5] :
 								       0)) :
@@ -692,6 +693,21 @@ void webusbPoll()
 				faultDiagArmIntentionalReset();
 				enterUf2Dfu();
 
+				// 0x25: FULL BOARD WIPE (debug panel only). Erase the app + config/bond + bootloader-settings
+				// flash and reboot app-less, so the board mounts as the UF2 drive on EVERY boot until it is
+				// flashed again -- no trace of the firmware left. Distinct from 0x0A (factory reset, which
+				// reformats settings but keeps the firmware). Guarded by the 4-byte magic "WIPE" so no stray
+				// byte can ever trigger it. Irreversible without re-flashing.
+			} else if (op == 0x25) {
+				if (buf[1] == 0x57 && buf[2] == 0x49 &&
+				    buf[3] == 0x50 && buf[4] == 0x45) {
+					usb_web.flush();
+					fwupArmFullWipe();
+					delay(40);
+					faultDiagArmIntentionalReset();
+					NVIC_SystemReset();
+				}
+
 				// 0x0D: write ONE bond slot into RAM -- the "clone onto this puck" side of Export/Import.
 				// [0x0D][slot][used][24 rec]. used=0 (or an empty record) clears the slot. The panel sends
 				// one of these per slot, then a 0x0E to persist + reboot. Kept to a single small command

+ 2 - 0
OpenPuck/webusb_config.h

@@ -10,6 +10,8 @@
 //               0x22                 firmware update: verify staged image + COMMIT (applies on next reboot)
 //               0x23                 clean reboot (after 0x22: boots into the freshly staged firmware)
 //               0x24                 firmware update: abort/disarm
+//               0x25 "WIPE"          FULL BOARD WIPE (debug): erase app+config+bond+bl-settings, reboot app-less
+//                                    -> mounts as UF2 every boot until re-flashed (see fwupArmFullWipe)
 //   dev->host:  0xA5 <len> <payload>  status blob (mode/tunables/link state/rates)
 //               0xAB 5 <status> <nextOff u32>  firmware-update ack, one per 0x20/0x21/0x22/0x24 (fw_update.h)
 // No setLandingPage() on purpose -- it would pop a Chrome "open <url>?" notification on every plug-in.

+ 4 - 0
docs/PROTOCOL.md

@@ -386,6 +386,10 @@ Messages:
   - `0x14`: commit the edited lizard map to flash (device echoes `0xAA`)
   - `0x15`: reset the lizard map to built-in defaults (device echoes `0xAA`)
   - `0x20`–`0x24`: staged firmware update (begin/data/end/reboot/abort), acked with `0xAB` frames
+  - `0x25 0x57 0x49 0x50 0x45`: **full board wipe** (`"WIPE"` magic, debug panel only). Erases the app
+    region + LittleFS (settings + bonds) + bootloader-settings page and reboots app-less, so the board mounts
+    as the UF2 bootloader drive on **every** boot until firmware is flashed again. Unlike `0x0A` (factory
+    reset, which keeps the firmware), this leaves no trace of OpenPuck. Irreversible without re-flashing.
 - Device to host:
   - `0xA5 <len> <payload>`: status blob
   - `0xA7 <len> <payload>`: bond export (§10.1)

+ 12 - 0
docs/index.html

@@ -106,6 +106,7 @@
   <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="factoryErase" class="danger" title="Wipe ALL bonds + settings and reboot to defaults. You must re-pair. Cannot be undone.">⚠ Factory erase</button>
+  <button id="wipeBoard" class="danger debugonly" title="ERASE THE ENTIRE BOARD: firmware + all settings + bonds. The board reboots app-less and mounts as the UF2 drive on every boot until you re-flash OpenPuck. Debug only. Cannot be undone.">☢ Wipe board</button>
 </div>
 <div class="wrap">
   <p class="sub">First connect uses the Chrome/Edge device picker; reconnects are automatic. If the picker is empty, quit any app holding the device or replug.</p>
@@ -1936,6 +1937,17 @@ $("#factoryErase").onclick=async()=>{
   await send([0x0A,0x45,0x52,0x53]);
   log("FACTORY ERASE sent — copycat is reformatting and rebooting to defaults. Re-pair the controller, then reconnect.");
 };
+// Debug-only: full board wipe. Erases the firmware itself (not just settings), leaving the board app-less so
+// it mounts as the UF2 bootloader drive on every boot until OpenPuck is flashed again. Sends 0x25 + "WIPE".
+$("#wipeBoard").onclick=async()=>{
+  if(!dev){ log("not connected"); return; }
+  if(!confirm("WIPE THE ENTIRE BOARD?\n\nThis is NOT a factory reset. It erases:\n  • the OpenPuck FIRMWARE itself\n  • every setting (mode, chords, back paddles, sensitivity)\n  • the paired-controller bond\n\nThe board reboots with NO firmware and mounts as the UF2 bootloader drive. It will do so on EVERY boot until you flash OpenPuck (a .uf2) back onto it. This panel will disconnect and NOT reconnect until then.\n\nThis CANNOT be undone from software.")) return;
+  if(!confirm("Are you absolutely sure?\n\nThe board will be blank. The ONLY way back is to drag a .uf2 firmware file onto the UF2 drive it mounts as.")) return;
+  const typed=prompt('Final confirmation — type  WIPE  (all caps) to erase the whole board:');
+  if(typed!=="WIPE"){ log("board wipe cancelled (confirmation text did not match)"); return; }
+  await send([0x25,0x57,0x49,0x50,0x45]);
+  log("FULL BOARD WIPE sent — the board is erasing firmware + all data (~15–20 s), then reboots as a blank UF2 drive. Flash OpenPuck (.uf2) to restore it.");
+};
 $("#persistMode").onclick=()=>{ const on=$("#persistMode").classList.contains("active"); setField(16, on?0:1); };
 $("#landAll87").onclick=async()=>{ const on=$("#landAll87").classList.contains("active"); await setField(29, on?0:1); log("land-all-0x87 "+(on?"disabled":"enabled")); };
 // Connection tuning: post-connect haptic block on (field 27) + seconds (field 28). (poll RX window is fixed.)