Explorar o código

Possibly better

Jari hai 4 meses
pai
achega
1a968ef48b

+ 4 - 0
OpenPuck/OpenPuck.ino

@@ -36,6 +36,7 @@ using namespace Adafruit_LittleFS_Namespace;
 #include "status_led.h"
 #include "usb_mount.h"
 #include "identity.h"
+#include "fault_diag.h"
 #include <stdio.h>
 
 #if CFG_TUD_HID < 4
@@ -209,6 +210,9 @@ void setup()
 	Serial.printf("# copycat up: unit=%s board=%s, mode=%s\n", g_unit,
 		      g_board,
 		      MODE_NAME[g_usbMode <= MODE_MAX ? g_usbMode : 0]);
+	// Classify why we (re)booted: distinguishes a watchdog hang from a HardFault from an intentional reboot
+	// (issue #72 -- those are conflated in the field). Surfaced on the WebUSB panel too.
+	faultDiagBoot();
 	if (puckMode)
 		Serial.printf(
 			"# puck USB: %s\n",

+ 22 - 2
OpenPuck/config.cpp

@@ -1,4 +1,6 @@
 #include "config.h"
+#include "rf_link.h" // g_rxWin (poll RX window persisted here)
+#include "haptics.h" // g_hapticBlockOn, g_hapticBlockMs
 #include <Adafruit_LittleFS.h>
 #include <InternalFileSystem.h>
 #include <string.h>
@@ -68,11 +70,14 @@ uint8_t g_rumbleScale = 200;
 const uint32_t g_pollUs = POLL_US_DEFAULT;
 
 #define CFG_FILE "/cfg.bin"
-// TypeCfg size changed; old flash format is incompatible -> clean defaults on first boot after upgrade
-#define CFG_MAGIC 0xCB
+// Struct layout/semantics changed (RF tunables); bump so old flash format is discarded -> clean defaults once.
+#define CFG_MAGIC 0xCD
 struct Cfg {
 	uint8_t magic, mode, mDiv, mFric, rsvd0, pollU100, persistMode,
 		bootMode, chordBtn[3], rumbleScale;
+	// Connection tunables: the connected-poll RX window in 10us units (g_rxWin/10), the post-connect haptic
+	// block enable, and its duration in seconds. 0 in rxWin10 => use the boot default.
+	uint8_t rxWin10, hapticBlockOn, hapticBlockS;
 	TypeCfg type[ET_COUNT]; // per-emulated-type back/qam/abSwap/padHaptics
 }; // rsvd0 = ex-padSmooth, now the one-shot debug-CDC arm
 
@@ -88,6 +93,9 @@ void saveCfg()
 		  g_bootMode,
 		  { g_chordBtn[0], g_chordBtn[1], g_chordBtn[2] },
 		  g_rumbleScale,
+		  (uint8_t)(g_rxWin / 10),
+		  g_hapticBlockOn,
+		  (uint8_t)(g_hapticBlockMs / 1000),
 		  {} };
 	for (int i = 0; i < ET_COUNT; i++)
 		c.type[i] = g_type[i];
@@ -142,6 +150,18 @@ void loadCfg()
 
 			// 0 is a valid setting (rumble off)
 			g_rumbleScale = c.rumbleScale;
+			// Connection tunables: restore the poll RX window (0 = unset/old cfg -> keep boot default) and
+			// the post-connect haptic block (enable + duration in seconds).
+			if (c.rxWin10) {
+				uint32_t w = (uint32_t)c.rxWin10 * 10;
+				g_rxWin = w < 600 ? 600 : (w > 3000 ? 3000 : w);
+			}
+			g_hapticBlockOn = c.hapticBlockOn ? 1 : 0;
+			// clamp seconds so s*1000 fits the uint16 ms field (max 60s)
+			g_hapticBlockMs =
+				(uint16_t)((c.hapticBlockS > 60 ? 60 :
+							      c.hapticBlockS) *
+					   1000u);
 		}
 		f.close();
 	}

+ 81 - 0
OpenPuck/fault_diag.cpp

@@ -0,0 +1,81 @@
+#include "fault_diag.h"
+#include <Arduino.h> // readResetReason(), NRF_POWER, NVIC_SystemReset, POWER_RESETREAS_*_Msk
+
+// GPREGRET2 markers. GPREGRET (id 0) is reserved by the Adafruit bootloader for the DFU magic; GPREGRET2 is
+// free for the application and is retained across soft/watchdog/pin reset, cleared only on power-on/brownout.
+// Arbitrary non-zero tags -- any value distinct from each other and from the power-on default (0) works.
+#define G2_INTENT 0xB1u
+#define G2_FAULT 0xFAu
+
+static uint8_t g_reason = RR_UNKNOWN;
+static uint32_t g_resetReas = 0;
+
+static const char *const REASON_STR[RR_COUNT] = {
+	"unknown",	   "power-on",	   "pin/replug",
+	"WATCHDOG (hang)", "CPU lockup",   "HARDFAULT",
+	"reboot",	   "soft reset",   "wake-from-off",
+};
+
+// HardFault override. The Adafruit core's default handler (cores/.../debug.cpp) already does NVIC_SystemReset();
+// we replace it to first stamp the fault marker so the NEXT boot classifies this SREQ as RR_HARDFAULT rather
+// than an intentional reboot. The core's strong symbol is only linked to satisfy the vector table, so our own
+// strong definition takes its place. Keep this MINIMAL: we are in fault context on a possibly-corrupt stack --
+// no Serial, no allocations, just stamp and reset.
+extern "C" void HardFault_Handler(void)
+{
+	NRF_POWER->GPREGRET2 = G2_FAULT;
+	NVIC_SystemReset();
+	while (1) {
+	}
+}
+
+void faultDiagArmIntentionalReset()
+{
+	NRF_POWER->GPREGRET2 = G2_INTENT;
+}
+
+void faultDiagBoot()
+{
+	uint32_t rr = readResetReason(); // latched + cleared by the core's init()
+	uint8_t g2 = (uint8_t)NRF_POWER->GPREGRET2;
+	NRF_POWER->GPREGRET2 = 0; // consume the marker for this boot cycle
+	g_resetReas = rr;
+
+	uint8_t reason;
+	// Precedence: a physical pin reset / watchdog / lockup is unambiguous from RESETREAS; only a SREQ needs the
+	// GPREGRET2 marker to split intentional reboot vs HardFault.
+	if (rr & POWER_RESETREAS_RESETPIN_Msk)
+		reason = RR_PIN;
+	else if (rr & POWER_RESETREAS_DOG_Msk)
+		reason = RR_WATCHDOG;
+	else if (rr & POWER_RESETREAS_LOCKUP_Msk)
+		reason = RR_LOCKUP;
+	else if (rr & POWER_RESETREAS_SREQ_Msk)
+		reason = (g2 == G2_FAULT) ? RR_HARDFAULT :
+			 (g2 == G2_INTENT) ? RR_REBOOT :
+					     RR_SOFT;
+	else if (rr & POWER_RESETREAS_OFF_Msk)
+		reason = RR_WAKE;
+	else if (rr == 0)
+		reason = RR_POWERON; // all bits clear == power-on / brownout
+	else
+		reason = RR_UNKNOWN;
+	g_reason = reason;
+
+	Serial.printf(
+		"# reset cause: %s (RESETREAS=0x%08lX gpregret2=0x%02X)\n",
+		REASON_STR[reason], (unsigned long)rr, g2);
+}
+
+uint8_t faultDiagReason()
+{
+	return g_reason;
+}
+uint32_t faultDiagResetReas()
+{
+	return g_resetReas;
+}
+const char *faultDiagReasonStr()
+{
+	return REASON_STR[g_reason < RR_COUNT ? g_reason : 0];
+}

+ 42 - 0
OpenPuck/fault_diag.h

@@ -0,0 +1,42 @@
+// fault_diag.h -- "why did we reboot?" diagnostic for issue #72 (intermittent disconnects).
+//
+// Field reports conflate THREE different events that all look identical from the host (USB re-enumerates, the
+// WebUSB panel shows "RF Link down"):
+//   1. a genuine RF link drop -- NO reboot, the link self-recovers in seconds (handled in rf_link, not here);
+//   2. a loop() hang -- the ~8s hardware watchdog fires and resets the MCU (red LED);
+//   3. an MCU fault -- a HardFault triggers an immediate reset (the Adafruit core's default handler resets).
+// Without recording which one happened, (2) and (3) are indistinguishable in the field, so the thread keeps
+// guessing. This module classifies the cause of each boot and surfaces it (boot banner + WebUSB panel).
+//
+// How: the nRF52 RESETREAS register already distinguishes watchdog (DOG) / software (SREQ) / pin (RESETPIN) /
+// lockup resets; the Adafruit core latches it at boot (readResetReason()). A SREQ alone can't tell "we rebooted
+// on purpose" (mode-switch chord, config reboot, DFU) from "a HardFault reset us" -- so we stamp the GPREGRET2
+// retention register (retained across soft/watchdog/pin reset, cleared only on power-on; GPREGRET itself is
+// reserved by the bootloader for the DFU magic): faultDiagArmIntentionalReset() before every deliberate
+// NVIC_SystemReset, and the HardFault handler stamps a distinct fault marker.
+#pragma once
+#include <stdint.h>
+
+// Boot-cause classification (kept in sync with REASON_STR[] in fault_diag.cpp).
+enum {
+	RR_UNKNOWN = 0,
+	RR_POWERON, // cold power-on / brownout
+	RR_PIN, // reset pin / cable replug
+	RR_WATCHDOG, // WDT fired -> loop() stopped feeding it (a hang)
+	RR_LOCKUP, // CPU lockup
+	RR_HARDFAULT, // HardFault -> our handler reset us
+	RR_REBOOT, // intentional NVIC_SystemReset (mode switch / config / DFU)
+	RR_SOFT, // software reset, unattributed
+	RR_WAKE, // wake from System OFF
+	RR_COUNT,
+};
+
+// Classify + log the cause of THIS boot. Call once, early in setup() (after Serial.begin so the line lands).
+void faultDiagBoot();
+// Stamp "the reset about to happen is intentional" -- call immediately before a deliberate NVIC_SystemReset so
+// the next boot classifies it as RR_REBOOT, not RR_HARDFAULT.
+void faultDiagArmIntentionalReset();
+// Last-boot classification (RR_*) + the raw RESETREAS, for the WebUSB panel / console.
+uint8_t faultDiagReason();
+uint32_t faultDiagResetReas();
+const char *faultDiagReasonStr();

+ 13 - 5
OpenPuck/haptics.cpp

@@ -14,6 +14,10 @@ uint8_t g_relayOp = 0xE3; // E3 poll
 uint8_t g_relaySub = 0x05;
 volatile uint8_t g_testHaptic = 0;
 volatile uint8_t g_hapticStop = 0;
+// Post-connect haptic block (see haptics.h): enabled + duration, both persisted and panel-adjustable.
+// Off by default -- opt in from the WebUSB panel if a controller's haptics come up degraded after connect.
+uint8_t g_hapticBlockOn = 0;
+uint16_t g_hapticBlockMs = HAPTIC_BLOCK_MS_DEFAULT;
 // Per-slot reconnect block. 0 = idle; non-zero = drop haptics aimed at this slot until millis() catches up.
 unsigned long g_hapticBlockUntil[NSLOT] = { 0 };
 
@@ -140,8 +144,10 @@ void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n)
 	e.n = (uint8_t)(n > 255 ? 255 : n);
 	for (int i = 0; i < 16; i++)
 		e.b[i] = (i < (int)n) ? b[i] : 0;
-	g_hapHead = (uint8_t)((g_hapHead + 1) %
-			      (sizeof g_hapLog / sizeof g_hapLog[0]));
+	// uint16_t, NOT uint8_t: HAPLOG_N is 4096, so a uint8_t cast truncated the head to 0..255 and only the
+	// first 256 ring slots were ever used (capture lost ~94% of its depth).
+	g_hapHead = (uint16_t)((g_hapHead + 1) %
+			       (sizeof g_hapLog / sizeof g_hapLog[0]));
 	__set_PRIMASK(pm);
 }
 void hapticDumpLog()
@@ -448,8 +454,9 @@ void hapticInit()
 		g_rqHead[s] = g_rqTail[s] = 0;
 		g_rumble80On[s] = false;
 		g_rumble80Ms[s] = 0;
-		// block stale Steam 0x82 until the link is stable after boot
-		g_hapticBlockUntil[s] = millis() + HAPTIC_RECONNECT_BLOCK_MS;
+		// block stale Steam 0x82 until the link is stable after boot (when the block is enabled)
+		g_hapticBlockUntil[s] =
+			g_hapticBlockOn ? (millis() + g_hapticBlockMs) : 0;
 	}
 }
 // Arm the post-(re)connect haptic block + schedule the clearing re-init. Called on the reliable first-reply
@@ -459,7 +466,8 @@ void hapticOnReconnect(int slot)
 {
 	if (slot < 0 || slot >= NSLOT)
 		return;
-	g_hapticBlockUntil[slot] = millis() + HAPTIC_RECONNECT_BLOCK_MS;
+	g_hapticBlockUntil[slot] =
+		g_hapticBlockOn ? (millis() + g_hapticBlockMs) : 0;
 	g_haptic82On = false;
 	g_rumble80On[slot] = false;
 	g_rumble80Ms[slot] = 0;

+ 12 - 6
OpenPuck/haptics.h

@@ -20,10 +20,11 @@
 
 // after this much host silence, consider the current 0x82 haptic stream inactive
 #define HAPTIC_QUIET_MS 300u
-// Block ALL haptic relays for this long after a (re)connect: a just-powered-on controller's haptic engine
-// isn't ready, and feeding it haptics in that window leaves it in a degraded/latched state (slow/stuck/missing
-// haptics until a re-init). 3s gives it time to initialize.
-#define HAPTIC_RECONNECT_BLOCK_MS 3000u
+// Post-(re)connect haptic block default. While armed, ALL Steam haptic relays to that slot are dropped: a
+// just-powered-on controller's haptic engine isn't ready, and feeding it haptics in that window leaves it in a
+// degraded/latched state (slow/stuck/missing haptics until a re-init). Runtime-adjustable (g_hapticBlockMs) and
+// toggleable (g_hapticBlockOn) from the WebUSB panel; this is the boot default.
+#define HAPTIC_BLOCK_MS_DEFAULT 10000u
 // 0x82-zero relays per stop event (sent at poll cadence -- not loop rate)
 #define HAPTIC_STOP_BURST 4u
 // max relayed payload bytes per entry: RF frame = [E3][len][05][rid][payload] and MAXLEN=64 -> 60
@@ -46,6 +47,11 @@
 bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
 		  uint8_t slot = 0xFF);
 
+// Post-connect haptic block (persisted, panel-controlled): when g_hapticBlockOn, Steam haptics are dropped for
+// g_hapticBlockMs after a (re)connect so the controller's haptic engine settles before the first real haptic.
+extern uint8_t g_hapticBlockOn; // 1 = block enabled, 0 = relay haptics immediately on connect (default)
+extern uint16_t g_hapticBlockMs; // block duration in ms (default HAPTIC_BLOCK_MS_DEFAULT)
+
 // anything still queued (xinput uses it to pace rumble re-queues)
 bool relayPending();
 extern uint8_t g_relayOp; // relay frame opcode (E3 poll)
@@ -53,7 +59,7 @@ extern uint8_t g_relaySub; // relay sub-TLV type byte = SET
 extern volatile uint8_t g_testHaptic; // 't<n>' injects n test haptics
 // pending haptic-STOP frames to relay (kill a latched whine)
 extern volatile uint8_t g_hapticStop;
-// Per-slot block: arm after a (re)connect, drop haptics aimed at the slot for HAPTIC_RECONNECT_BLOCK_MS.
+// Per-slot block: arm after a (re)connect, drop haptics aimed at the slot for g_hapticBlockMs (when g_hapticBlockOn).
 extern unsigned long g_hapticBlockUntil[NSLOT];
 
 // relay the controller power-off (0x9F "off!"), burst x3 (Steam 0x9F / host-suspend / test button)
@@ -112,7 +118,7 @@ void hapticTask();
 // harmless on healthy controllers, so it's worth re-initializing every slot the firmware knows about.
 void hapticReinit(uint8_t slot = 0xFF);
 // Called from rf_link the instant a controller (re)connect is detected (an F-reply after a gap): blocks haptic
-// relays for HAPTIC_RECONNECT_BLOCK_MS and schedules a re-init just after, to keep the freshly-booted
+// relays for g_hapticBlockMs and schedules a re-init just after, to keep the freshly-booted
 // controller out of the degraded/latched haptic state. Reliable -- independent of hapticTask's link heuristic.
 // Per-slot: only the slot that just reconnected is blocked, the others keep relaying.
 void hapticOnReconnect(int slot);

+ 21 - 0
OpenPuck/puck_hid.cpp

@@ -168,6 +168,12 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 			}
 		}
 
+#if OPK_LOG
+		// Per-report Serial echo: gated to the diagnostic build only. handleSet runs on the TinyUSB device
+		// task, whose FreeRTOS stack is just 800 bytes; a Serial.printf there costs 200-500B and, stacked
+		// under TinyUSB's control-transfer frames + a preempting USB ISR, can blow the stack -> the exact
+		// SP-corruption-during-exception-entry that shows up as RR_LOCKUP. Production keeps this path
+		// printf-free; the same data is already in the OPK_LOG capture ring (hapLogAdd) for the WebUSB panel.
 		if (Serial.availableForWrite() > 80) {
 			Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
 				      n);
@@ -175,6 +181,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 				Serial.printf(" %02X", b[i]);
 			Serial.println();
 		}
+#endif
 		return;
 	}
 	if (type != HID_REPORT_TYPE_FEATURE || n < 1)
@@ -214,10 +221,12 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 			// multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
 			// 18B cap, and the chopped frames were why those settings never landed on the controller.
 			uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
+#if OPK_LOG
 			if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
 				Serial.printf(
 					"# RELAY TRUNC cmd=%02X len=%u>%u\n",
 					cmd, len, (unsigned)RELAY_MAXP);
+#endif
 			relayEnqueue(cmd, pl, rl, (uint8_t)slot);
 
 			// track from RELAYED frames only (see the OUTPUT path)
@@ -225,6 +234,9 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 				haptic82HostReport(pl, len);
 		}
 	}
+#if OPK_LOG
+	// Diagnostic-build only -- see the OUTPUT-path note: no Serial.printf on the 800B usbd-task stack in
+	// production (LOCKUP mitigation). The feature SET is already captured to the OPK_LOG ring above.
 	if (Serial.availableForWrite() > 80) {
 		Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
 			      cmd, len);
@@ -232,6 +244,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 			Serial.printf(" %02X", b[i]);
 		Serial.println();
 	}
+#endif
 	memset(S.resp, 0, sizeof S.resp);
 	S.resp_len = 0;
 	switch (cmd) {
@@ -270,7 +283,9 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 		break;
 	case 0xAD:
 		g_pairing = (pln > 0 && pl[0] != 0);
+#if OPK_LOG
 		Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
+#endif
 		S.resp[0] = 0xAD;
 		S.resp[1] = 0;
 		S.resp_len = 63;
@@ -285,8 +300,10 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 				S.used = true;
 			}
 			g_dirty = true;
+#if OPK_LOG
 			Serial.printf("# slot %d %s\n", slot,
 				      recEmpty(pl) ? "cleared" : "bonded");
+#endif
 		}
 		S.resp[0] = 0xA2;
 		S.resp[1] = 0;
@@ -337,6 +354,9 @@ static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
 			lastMs = millis();
 			// ring marker 0xFC = "host feature GET" (panel renders it as "GET rid=.."); payload = what we returned
 			hapLogAdd(0xFC, rid, S.resp, n);
+#if OPK_LOG
+			// Diagnostic build only -- no Serial.printf on the 800B usbd-task stack in production (LOCKUP
+			// mitigation). The GET is captured to the OPK_LOG ring above for the WebUSB panel.
 			if (Serial.availableForWrite() > 80)
 				Serial.printf(
 					"# GET if%d rid=%02X reqlen=%u -> %02X %02X %02X (batt=%u%%)\n",
@@ -345,6 +365,7 @@ static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
 					(slot >= 0 && slot < NSLOT) ?
 						g_battery[slot] :
 						0);
+#endif
 		}
 	}
 	return n;

+ 4 - 1
OpenPuck/rf_diag.cpp

@@ -297,7 +297,10 @@ void rfBeaconOnce()
 	// RAM: [LENGTH][ (S1 if S1INCL) ][payload]
 	rftx[0] = 0;
 	rftx[1] = g_plen;
-	memcpy(rftx + 2, pl, g_plen);
+	// g_plen comes unclamped from the console 'L' command (0..255); the source is pl[48] and the dest is
+	// rftx[100] written at +2, so copy at most 48 bytes -- otherwise we over-read the stack and overrun rftx.
+	uint8_t cpLen = g_plen > sizeof pl ? (uint8_t)sizeof pl : g_plen;
+	memcpy(rftx + 2, pl, cpLen);
 	NRF_RADIO->PACKETPTR = (uint32_t)rftx;
 	NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
 			    RADIO_SHORTS_END_DISABLE_Msk;

+ 11 - 2
OpenPuck/rf_link.cpp

@@ -6,6 +6,7 @@
 #include "haptics.h"
 #include "controllers.h"
 #include "status_led.h"
+#include "fault_diag.h"
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <string.h>
@@ -40,8 +41,8 @@ bool g_e7announce =
 bool g_e1keepalive = true;
 
 bool g_connVerbose = false;
-// poll RX-window (us); shorter=more polls/s but may miss DELAYED replies. Tunable 'r'.
-uint32_t g_rxWin = 1200;
+// poll RX-window (us); shorter=more polls/s but may miss DELAYED replies. Persisted + panel-adjustable; 'r'.
+uint32_t g_rxWin = 2000;
 unsigned long g_connCooldown = 0;
 
 uint8_t g_connSt = 0; // 0=announce awake, 1=poll loop
@@ -325,6 +326,13 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 				}
 			}
 			bool isF1 = (rtype == 0xF1);
+			// Every rfConnTx caller (rfConnStep) sets g_curSlot to a valid 0..NSLOT-1 before a poll, so it
+			// IS in range here today. But this block does ~50 unguarded g_curSlot array writes (g_in[],
+			// g_lastSeq[], and several static per-slot arrays) -- if any future/edge caller ever left
+			// g_curSlot at -1, those become out-of-bounds writes that corrupt RAM (the stack-smash / LOCKUP
+			// class we're chasing). Guard once at the top so the decode is robust by construction.
+			if (isF1 && (g_curSlot < 0 || g_curSlot >= NSLOT))
+				isF1 = false;
 			if (isF1) {
 				g_connF1++;
 				// walk ALL type6 TLVs (= HID report 0x45); taking only [0] halves the rate. idx is INT,
@@ -595,6 +603,7 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 						    !USBDevice.suspended()) {
 							saveMode(want);
 							delay(40);
+							faultDiagArmIntentionalReset();
 							NVIC_SystemReset();
 						}
 					} else {

+ 3 - 0
OpenPuck/serial_console.cpp

@@ -5,6 +5,7 @@
 #include "haptics.h"
 #include "bonds.h"
 #include "config.h"
+#include "fault_diag.h"
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <stdlib.h>
@@ -30,6 +31,7 @@ void serialConsolePoll()
 				Serial.println(
 					"# done -- rebooting into clean defaults; re-pair the controller");
 				delay(40);
+				faultDiagArmIntentionalReset();
 				NVIC_SystemReset();
 			} else if (line[0] == 'l')
 				rfListenStart();
@@ -104,6 +106,7 @@ void serialConsolePoll()
 						delay(20);
 						saveMode(m);
 						delay(40);
+						faultDiagArmIntentionalReset();
 						NVIC_SystemReset();
 					}
 				}

+ 45 - 4
OpenPuck/webusb_config.cpp

@@ -6,6 +6,7 @@
 #include "puck_hid.h"
 #include "triton.h" // g_in raw IMU (diagnostic readout)
 #include "build_info.h"
+#include "fault_diag.h"
 #include <Arduino.h>
 #include <string.h>
 
@@ -19,9 +20,10 @@ Adafruit_USBD_WebUSB usb_web;
 //                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
 //                [bondedCount][slot0_up][slot0_batt][slot0_rssi]...[slot3_up][slot3_batt][slot3_rssi]
 //                [v10: per-type cfg, 4x8B: ET_XBOX/SWITCH/DS4/DS5 each {back0..3, qam, abSwap, padHaptics, ledBright}]
-// p[6]/p[7]/p[8..11] mirror the ACTIVE type (legacy display). v10 extends to 105 bytes (107 total incl header);
+//                [v11: resetReason(RR_* code)][resetReas raw u32 LE][rxWin/10 us][hapticBlockOn][hapticBlock s]
+// p[6]/p[7]/p[8..11] mirror the ACTIVE type (legacy display). v11 extends to 113 bytes (115 total incl header);
 // browser reads with transferIn(128) to span the two USB-FS packets.
-#define WB_PAYLEN 105
+#define WB_PAYLEN 113
 static void webusbSendBlob()
 {
 	if (!usb_web.connected())
@@ -35,8 +37,8 @@ static void webusbSendBlob()
 	p[0] = 0xA5;
 	p[1] = WB_PAYLEN;
 
-	// protocol version (10 = +ledBright per type; 9 = +per-type cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
-	p[2] = 10;
+	// protocol version (11 = +reset cause; 10 = +ledBright per type; 9 = +per-type cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
+	p[2] = 11;
 	p[3] = g_usbMode;
 	p[4] = (uint8_t)g_mDiv;
 	p[5] = (uint8_t)g_mFric;
@@ -127,6 +129,19 @@ static void webusbSendBlob()
 		q[6] = g_type[et].padHaptics;
 		q[7] = g_type[et].ledBright;
 	}
+	// last-boot reset cause (protocol v11): why we (re)booted -- watchdog hang vs HardFault vs intentional
+	// reboot vs power-on (issue #72). p[107] = RR_* code; p[108..111] = raw RESETREAS for the curious.
+	p[107] = faultDiagReason();
+	uint32_t rr = faultDiagResetReas();
+	p[108] = (uint8_t)rr;
+	p[109] = (uint8_t)(rr >> 8);
+	p[110] = (uint8_t)(rr >> 16);
+	p[111] = (uint8_t)(rr >> 24);
+	// Connection tunables (protocol v11): poll RX window in 10us units, post-connect haptic block enable,
+	// and block duration in seconds.
+	p[112] = (uint8_t)(g_rxWin / 10);
+	p[113] = g_hapticBlockOn;
+	p[114] = (uint8_t)(g_hapticBlockMs / 1000);
 	usb_web.write(p, sizeof p);
 	usb_web.flush();
 }
@@ -235,6 +250,7 @@ void webusbPoll()
 					usb_web.flush();
 					factoryErase();
 					delay(40);
+					faultDiagArmIntentionalReset();
 					NVIC_SystemReset();
 				}
 
@@ -242,12 +258,14 @@ void webusbPoll()
 			} else if (op == 0x0B) {
 				usb_web.flush();
 				delay(40);
+				faultDiagArmIntentionalReset();
 				enterSerialDfu();
 
 				// reboot into UF2 bootloader (USB mass storage)
 			} else if (op == 0x0C) {
 				usb_web.flush();
 				delay(40);
+				faultDiagArmIntentionalReset();
 				enterUf2Dfu();
 
 			} else if (op == 0x02) {
@@ -358,6 +376,7 @@ void webusbPoll()
 					armDebugCdcNextBoot();
 					usb_web.flush();
 					delay(40);
+					faultDiagArmIntentionalReset();
 					NVIC_SystemReset();
 					break;
 
@@ -386,6 +405,27 @@ void webusbPoll()
 					swProSaveCfg();
 					persist = false;
 					break; // Switch Pro gyro scale x10
+
+				// connected-poll RX window, in 10us units: wider window catches a controller reply that
+				// lands late under haptic load; narrower allows a higher poll rate. Clamp 600..3000us.
+				case 25: {
+					uint32_t w = (uint32_t)v * 10;
+					g_rxWin = w < 600 ? 600 :
+						  w > 3000 ? 3000 :
+							     w;
+					break;
+				}
+
+				// post-connect haptic block enable: drop Steam haptics for g_hapticBlockMs after a connect
+				case 27:
+					g_hapticBlockOn = v ? 1 : 0;
+					break;
+
+				// post-connect haptic block duration, in seconds (clamp 0..60)
+				case 28:
+					g_hapticBlockMs =
+						(uint16_t)((v > 60 ? 60 : v) * 1000u);
+					break;
 				}
 				if (persist)
 					saveCfg();
@@ -397,6 +437,7 @@ void webusbPoll()
 					usb_web.flush();
 					saveMode(m);
 					delay(40);
+					faultDiagArmIntentionalReset();
 					NVIC_SystemReset();
 				}
 			}

+ 42 - 34
docs/index.html

@@ -113,12 +113,27 @@
         <div class="stat"><div class="k">Build (git)</div><div class="v" id="stBuild">—</div></div>
         <div class="stat"><div class="k">Last reset</div><div class="v" id="stReset">—</div></div>
       </div>
-      <p class="note" style="margin-top:6px"><strong>Reset history</strong> (newest first, persists across power-off): <span id="resetHist" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
-        <em>watchdog</em>/<em>LOCKUP</em> = a fault (loop hang / crash); <em>soft reboot</em> = an intentional reset (mode change); <em>power-on</em> = normal plug-in.</p>
+      <p class="note" style="margin-top:6px"><strong>Reset detail:</strong> <span id="resetHist" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
+        <em>watchdog (hang)</em>/<em>CPU lockup</em>/<em>HARDFAULT</em> = a fault worth reporting (issue #72); <em>reboot</em> = an intentional reset (mode change / config); <em>power-on</em> &amp; <em>pin/replug</em> = normal plug-in.</p>
       <p class="note" style="margin-top:8px"><strong>IMU (raw):</strong> <span id="stImu" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
         Raw SC2 accel (before scaling). This controller is a ±2 g sensor (~16384 = 1 g); the Switch report divides it by 4 to present the genuine ±8 g (4096 = 1 g) scale so the console's gravity-correction engages and the gyro stops drifting.</p>
       <p class="note logonly">Polls/s ≈ 250 expected; it's capped by Loop period (≈4000 µs needed). "Slowest stage" is the loop section eating the most time per iteration.</p>
-      <div class="row" style="margin:10px 0 0"><button id="hapClear">Clear stuck buzz</button>
+      <div class="row" style="margin:14px 0 0;border-top:1px solid #272d3d;padding-top:14px">
+        <strong style="flex-basis:100%">Connection tuning <span class="note">(persists across reboot)</span></strong>
+      </div>
+      <div class="row" style="margin:8px 0 0"><label style="min-width:150px">Poll RX window</label>
+        <input type="range" id="rxWin" min="60" max="300" step="10"><span class="val" id="rxWinV"></span>
+        <span class="note" style="flex:1">How long each poll waits for the controller's reply. Wider (↑) catches replies that arrive late under heavy haptics (fewer drops); narrower (↓) allows a higher poll rate. Default 2000 µs.</span>
+      </div>
+      <div class="row" style="margin:8px 0 0"><label style="min-width:150px">Block haptics on connect</label>
+        <button id="hapBlockOn">off</button>
+        <span class="note" style="flex:1">Drops all Steam haptics for a short window right after a controller (re)connects, so its haptic engine settles before the first real haptic.</span>
+      </div>
+      <div class="row" style="margin:8px 0 0"><label style="min-width:150px">Block duration</label>
+        <input type="range" id="hapBlockS" min="0" max="30" step="1"><span class="val" id="hapBlockSV"></span>
+        <span class="note" style="flex:1">How long to drop haptics after connect (seconds). Default 10 s.</span>
+      </div>
+      <div class="row" style="margin:14px 0 0;border-top:1px solid #272d3d;padding-top:14px"><button id="hapClear">Clear stuck buzz</button>
         <span class="note" style="flex:1">If the controller's haptics get stuck/buzzing, this re-inits them (same as pressing Steam).</span>
       </div>
       <div class="row" style="margin:10px 0 0"><button id="ctlrOff">Turn off controller</button>
@@ -441,35 +456,26 @@ function applyBlob(p){
   $("#stLoop").textContent = loopUs? loopUs+" µs" : "—";
   $("#stWorst").textContent = loopUs? (WORST[worstIdx]||worstIdx)+" "+worstUs+"µs" : "—";
   $("#stPollPer").textContent = up? (pollUs+" / "+pollIntended+" µs") : "—";
-  // reset-cause history (protocol v9): JS p[73]=count, p[74..83]=last 10 causes (newest first).
-  // Persisted in puck flash, so a reboot that happened on a Switch is still readable here after replugging.
-  function resetReasonText(rr){
-    if (rr === 0) return "power-on";
-    const r = [];
-    if (rr & 0x02) r.push("watchdog");      // DOG: loop wedge / hang
-    if (rr & 0x08) r.push("LOCKUP");        // CPU lockup = HardFault escalation (real crash)
-    if (rr & 0x04) r.push("soft reboot");   // SREQ: NVIC_SystemReset (mode change etc.)
-    if (rr & 0x01) r.push("reset pin");
-    return r.length ? r.join("+") : ("0x"+rr.toString(16));
-  }
-  if (p.length > 73) {
-    const n = Math.min(p[73], 10);
-    const hist = [];
-    for (let i=0; i<n; i++) hist.push(p[74+i]);
-    const fault = (rr)=>!!(rr & 0x0A); // watchdog or lockup = a real fault
-    if (n === 0) { $("#stReset").textContent = "—"; }
-    else {
-      const newest = hist[0];
-      const head = fault(newest) ? ('<span class="pill dn">'+resetReasonText(newest)+'</span>') : resetReasonText(newest);
-      $("#stReset").innerHTML = head;
-      // full history list under the tile (newest -> oldest)
-      const items = hist.map((rr,i)=>{
-        const t = resetReasonText(rr);
-        return (i===0?'<strong>':'')+ '#'+(n-i)+' '+(fault(rr)?('<span style="color:#ff6b6b">'+t+'</span>'):t) +(i===0?' (latest)</strong>':'');
-      });
-      const el = $("#resetHist"); if (el) el.innerHTML = items.join(' &nbsp;·&nbsp; ');
-    }
+  // last-boot reset cause (protocol v11): a single classified code (firmware classifies it from RESETREAS +
+  // a GPREGRET2 marker) plus the raw RESETREAS. Codes match RR_* in fault_diag.h. Earlier firmware (<v11)
+  // didn't send this -> guard on length and leave the tile blank.
+  const RR_NAMES = ["unknown","power-on","pin/replug","watchdog (hang)","CPU lockup",
+                    "HARDFAULT","reboot","soft reset","wake-from-off"];
+  const RR_FAULT = new Set([3,4,5]); // watchdog / lockup / hardfault = a real crash worth reporting (#72)
+  if (p.length > 109) {
+    const code = p[105];
+    const raw = ((p[106]|(p[107]<<8)|(p[108]<<16)|(p[109]<<24))>>>0);
+    const name = RR_NAMES[code] || ("code "+code);
+    $("#stReset").innerHTML = RR_FAULT.has(code) ? ('<span class="pill dn">'+name+'</span>') : name;
+    const el = $("#resetHist");
+    if (el) el.textContent = "RESETREAS=0x"+raw.toString(16).padStart(8,"0");
   }
+
+  // Connection tuning: poll RX window /10us (p[110]), haptic-block enable (p[111]), block seconds (p[112]).
+  if (p.length>110) setSlider("rxWin", p[110]);
+  const hapBlockOn = (p.length>111?p[111]:0);
+  $("#hapBlockOn").textContent = hapBlockOn?"on":"off"; $("#hapBlockOn").classList.toggle("active",!!hapBlockOn);
+  if (p.length>112) setSlider("hapBlockS", p[112]);
   const gitDirty=(p.length>38?p[38]:0); let gitHash="";
   for(let i=39;i<51 && i<p.length && p[i];i++) gitHash+=String.fromCharCode(p[i]);
   $("#stBuild").innerHTML = gitHash
@@ -504,7 +510,7 @@ function applyBlob(p){
   { const sel=$("#swGyro"); if(document.activeElement!==sel) sel.value=swGyro10; }
   for(const sel of document.querySelectorAll("select.chord")){ if(document.activeElement!==sel) sel.value=chord[+sel.dataset.i]; }
 }
-function fmtSlider(id,v){ return id==="rumble" ? v+"%" : ""+v; }
+function fmtSlider(id,v){ return id==="rumble" ? v+"%" : id==="rxWin" ? (v*10)+" µs" : id==="hapBlockS" ? v+" s" : ""+v; }
 function setSlider(id,v){ const el=$("#"+id); if(document.activeElement!==el){ el.value=v; } $("#"+id+"V").textContent=fmtSlider(id,el.value); }
 
 let inflight=false;
@@ -615,8 +621,10 @@ $("#factoryErase").onclick=async()=>{
   log("FACTORY ERASE sent — copycat is reformatting and rebooting to defaults. Re-pair the controller, then reconnect.");
 };
 $("#persistMode").onclick=()=>{ const on=$("#persistMode").classList.contains("active"); setField(16, on?0:1); };
-for(const id of ["mDiv","mFric","rumble"]){
-  const field={mDiv:1,mFric:2,rumble:22}[id];
+// Connection tuning: poll RX window /10us (field 25), post-connect haptic block on (field 27) + seconds (field 28)
+$("#hapBlockOn").onclick=()=>{ const on=$("#hapBlockOn").classList.contains("active"); setField(27, on?0:1); };
+for(const id of ["mDiv","mFric","rumble","rxWin","hapBlockS"]){
+  const field={mDiv:1,mFric:2,rumble:22,rxWin:25,hapBlockS:28}[id];
   $("#"+id).addEventListener("input", ()=>{ $("#"+id+"V").textContent=fmtSlider(id,$("#"+id).value); });
   $("#"+id).addEventListener("change", ()=>setField(field, +$("#"+id).value));
 }