Переглянути джерело

more experiments but no fix

Jariullah Safi 4 місяців тому
батько
коміт
95754f2643

+ 16 - 0
OpenPuck/OpenPuck.ino

@@ -145,6 +145,22 @@ 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]);
+	// Reset-cause diagnostics: RESETREAS bits are STICKY (accumulate across resets) -- read then clear by writing
+	// 1s back. Tells us why the MCU last rebooted: DOG=watchdog (loop hang/8s timeout), SREQ=software
+	// (NVIC_SystemReset, e.g. the mode-switch chord), RESETPIN=button, OFF/LPCOMP=wake. A bare boot (power-on)
+	// shows 0. Decodes the reboot-on-buzz report without needing to catch it live.
+	{
+		uint32_t rr = NRF_POWER->RESETREAS;
+		g_bootResetReas = rr;	   // stash for WebUSB before clearing
+		NRF_POWER->RESETREAS = rr; // write-1-to-clear so the NEXT boot's cause is isolated
+		Serial.printf(
+			"# resetreas=0x%08lX%s%s%s%s%s\n", (unsigned long)rr,
+			(rr & POWER_RESETREAS_RESETPIN_Msk) ? " PIN" : "",
+			(rr & POWER_RESETREAS_DOG_Msk) ? " WATCHDOG" : "",
+			(rr & POWER_RESETREAS_SREQ_Msk) ? " SOFTRESET" : "",
+			(rr & POWER_RESETREAS_LOCKUP_Msk) ? " LOCKUP" : "",
+			(rr == 0) ? " (power-on)" : "");
+	}
 	if (puckMode)
 		Serial.printf(
 			"# puck USB: %s\n",

+ 1 - 0
OpenPuck/config.cpp

@@ -13,6 +13,7 @@ bool g_persistMode = false;
 uint8_t g_bootMode = 0xFF;
 
 bool g_debugCdcThisBoot = false;
+uint32_t g_bootResetReas = 0; // RESETREAS captured at boot (setup()); shown over CDC + WebUSB
 
 // persisted one-shot arm, stored in Cfg.rsvd0 (1 = keep CDC for the next boot)
 static uint8_t g_debugCdc = 0;

+ 4 - 0
OpenPuck/config.h

@@ -77,6 +77,10 @@ extern uint8_t g_bootMode;
 extern bool g_debugCdcThisBoot;
 void armDebugCdcNextBoot(); // persist the one-shot (caller reboots)
 
+// RESETREAS captured at boot (setup() reads + clears the sticky register). Surfaced over CDC and WebUSB so the
+// last MCU reset cause is visible: low bits = RESETPIN(0) DOG/watchdog(1) SREQ/software(2) LOCKUP(3). 0 = power-on.
+extern uint32_t g_bootResetReas;
+
 // persisted, runtime-tunable config:
 extern int g_mDiv, g_mFric; // xbox/lizard mouse sensitivity divisor / friction%
 extern uint8_t g_abSwap; // 1 = swap A/B and X/Y (Nintendo face-button layout)

+ 33 - 38
OpenPuck/haptics.cpp

@@ -51,8 +51,15 @@ struct RelayMsg {
 	uint8_t rid, len;
 	uint8_t data[RELAY_MAXP];
 };
-// deep enough to hold a full Steam settings/LED transaction burst without loss
-#define RELAY_QLEN 32
+// Deep enough to hold a FULL Steam reconnect handshake burst without eviction. MEASURED: on reconnect Steam
+// dumps ~72 config/haptic frames (identity, transport, 0x87 engine enable/config/disable, 0x81 resets) as a
+// tight burst; the relay drains 1-per-poll (~4ms, like the real dongle, whose handshake spans ~360ms). With the
+// old 32-deep ring the burst overflowed and the drop-oldest eviction discarded ~9 frames -- and the handshake
+// is an ORDERED sequence (enable->config->disable->reset), so dropping ANY frame leaves the controller's haptic
+// engine in a corrupt state: a lost 30=00/0x81 -> engine stays driven = stuck BUZZ; a lost 30=18 -> engine stays
+// disabled = trackpad haptics dead. 128 holds the whole burst so nothing is evicted and the engine ends in the
+// state Steam intended. (~8KB RAM.)
+#define RELAY_QLEN 128
 static RelayMsg g_rq[RELAY_QLEN];
 static volatile uint8_t
 	g_rqHead = 0,
@@ -93,21 +100,11 @@ bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen)
 	__set_PRIMASK(pm);
 	return true;
 }
-// Relay a haptic actuator report (0x80-0x86). A STOP (0x82 gain-0 / 0x80 zero) is enqueued HAPTIC_STOP_BURST
-// times ACROSS poll cycles (distinct NO-ACK frames), not retransmitted in-cycle: an in-cycle same-PID retransmit
-// RE-TRIGGERS the actuator (the controller does NOT dedup our raw-radio retransmits -> extra clicks / a buzz on
-// connect -- proven on hardware), so reliable-delivery-by-retransmit is NOT viable here. Spreading distinct stop
-// frames across cycles raises the odds one lands without re-firing. ON pulses go once (bursting ON = stacked
-// clicks). A dropped stop over the NO-ACK relay is what latches the actuator = the persistent buzz.
+// Relay a haptic actuator report (0x80-0x86) ONCE, verbatim -- like the real dongle. (No stop-burst: the
+// dongle relays each frame single-shot; bursting/retransmitting deviates and re-triggers the actuator.)
 bool relayHaptic(uint8_t rid, const uint8_t *p, uint8_t n)
 {
-	bool stop = (rid == 0x82 && n >= 3 && p[2] == 0) ||
-		    (rid == 0x80 && n >= 1 && p[0] == 0);
-	uint8_t reps = stop ? HAPTIC_STOP_BURST : 1u;
-	bool ok = false;
-	for (uint8_t i = 0; i < reps; i++)
-		ok = relayEnqueue(rid, p, n) || ok;
-	return ok;
+	return relayEnqueue(rid, p, n);
 }
 
 #if OPK_LOG
@@ -322,23 +319,11 @@ void rfConnFlushRelay(uint8_t ch, uint8_t s1)
 			// On-air sub-TLV framing. CONFIRMED from real puck<->controller sniffs (puck_sniffer): config/settings
 			// reports (rid >= 0x87) are only ACTED ON in the type-01 + inner-len form  E3 [2+rl][01][rid][innerlen][data];
 			// in the legacy form  E3 [1+rl][05][rid][data]  the controller mis-parses (reads data[0] as a length) and
-			// ignores them. Actuator reports (rid < 0x87, e.g. 0x82 haptic / 0x81 trigger) ACT in the legacy form.
+			// ignores them. Actuator reports (rid < 0x87, e.g. 0x82 haptic / 0x81 reset) ACT in the legacy form.
 			//
-			// Land ALL config/settings (rid >= 0x87) like the real dongle, but FORCE THE GYRO-ENABLE BIT ON in any
-			// 0x30 subsystem-enable write. 0x30 register = [reg][lo][hi]; lo bit 0x10 = gyro/IMU, bit 0x08 = haptic
-			// engine. We require gyro ALWAYS streaming (default-on; SDL-without-Steam and the emulated modes read it
-			// raw), but we MUST let Steam's haptic enable/DISABLE through: PROVEN from a buzz capture -- Steam's
-			// connect handshake relays an 0x81 haptic TRIGGER then a 0x30=0x00 that DISABLES (stops) the engine; if
-			// we discard 0x30 the stop is lost and the trigger's haptic runs forever = the connect buzz. Forcing
-			// ONLY bit 0x10 on (|= 0x10) passes the haptic bit verbatim while pinning gyro:
-			//   Steam 30=18 -> 30=18 (gyro on, haptic on)   Steam 30=00 -> 30=10 (gyro on, haptic OFF = stop)
-			// (Earlier tries: discard 0x30 -> lost stop -> buzz; land verbatim -> 30=00 freezes gyro; force 0x18
-			// -> haptic bit never clears so the engine never stops -> buzz/no-haptics. |=0x10 is the only correct one.)
-			if (m.rid == 0x87)
-				for (uint8_t i = 0; (uint16_t)i + 3 <= rl;
-				     i += 3)
-					if (m.data[i] == 0x30)
-						m.data[i + 1] |= 0x10;
+			// PURE VERBATIM: land config/settings (rid >= 0x87) in type-01, send actuators (< 0x87) legacy. No
+			// rewrite of any register (incl. 0x30) -- Steam owns the haptic engine + gyro exactly as on a real
+			// puck; we just translate framing.
 			bool land01 = (m.rid >= 0x87);
 			uint8_t p[5 + RELAY_MAXP], plen;
 			if (land01) {
@@ -458,18 +443,28 @@ void hapticTask()
 	}
 	wasHapticLinkUp = up;
 	// (Connect-time re-init flood removed -- the stop-burst is the real fix; see hapticOnReconnect.)
-	// Controller power-off on host SLEEP: send the power-off command (0x9F "off!") the instant the USB bus
-	// suspends, like the real puck. BUT only when USB power (VBUS) is still present -- i.e. a genuine host sleep,
-	// NOT a cable unplug. Pulling the dongle ALSO trips the suspend edge (in the brief window it runs on residual
-	// power), and we must NOT kill the controller then; it should only power off on a shutdown command or a real
-	// host sleep. VBUSDETECT is 1 while the cable still delivers 5V, 0 once unplugged. wasSusp starts true so a
-	// boot-into-suspended state never false-fires.
+	// Controller power-off on host SLEEP (0x9F "off!"), like the real puck -- but only after the bus stays
+	// SUSPENDED for SUSPEND_OFF_MS, not on the bare edge. PROVEN from a capture: firing on the edge killed the
+	// controller mid-glide on a brief USB selective-suspend (host idle power-mgmt, indistinguishable from sleep at
+	// the edge) -> disconnect -> reconnect-handshake churn that read as a stuck buzz / dropped haptics. A real
+	// sleep persists; a selective-suspend resumes in << 1s, so the timer never elapses and we don't power off.
+	// Guards: VBUS present (a cable unplug also suspends but must NOT power-off the controller), once per suspend.
+	// wasSusp starts true so a boot-into-suspended state never false-fires.
 	static bool wasSusp = true;
+	static unsigned long suspMs = 0;
+	static bool offSent = false;
 	bool susp = USBDevice.suspended();
 	bool vbus = (NRF_POWER->USBREGSTATUS &
 		     POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
-	if (susp && !wasSusp && vbus)
+	if (susp && !wasSusp)
+		suspMs = millis(); // suspend edge -> start the persistence timer
+	if (susp && !offSent && vbus &&
+	    (unsigned long)(millis() - suspMs) >= SUSPEND_OFF_MS) {
 		hapticSendShutdown();
+		offSent = true; // once per suspend
+	}
+	if (!susp)
+		offSent = false; // resumed -> re-arm for the next genuine sleep
 	wasSusp = susp;
 	// Steam-mode: host went quiet -> mark the 0x82 stream inactive. Do NOT synthesize a stop: trackpad haptics
 	// are one-shot pulses, so firing a 0x82-zero ~HAPTIC_QUIET_MS after a swipe ends is the extra end-of-movement

+ 5 - 0
OpenPuck/haptics.h

@@ -32,6 +32,11 @@
 // Controller power-off: hapticSendShutdown() relays Steam's confirmed "turn off controller" command (feature-0x01
 // cmd 0x9F, payload "off!" -- captured from the real puck). Sent as a small burst because the RF relay is NO-ACK.
 #define HAPTIC_SHUTDOWN_SHOTS 3u
+// Host-sleep power-off DEBOUNCE: the bus must stay SUSPENDED this long before we power the controller off. Firing
+// on the bare suspend edge killed the controller on a brief USB SELECTIVE-SUSPEND (idle power-mgmt, looks like
+// sleep at the edge) mid-use -> disconnect -> reconnect handshake churn read as a stuck buzz. A real system sleep
+// persists; a selective-suspend resumes in << 1s. So wait this long.
+#define SUSPEND_OFF_MS 4000u
 
 // ---- relay queue (written by puck_hid.cpp, mode_*.cpp, serial_console.cpp; drained by rf_link.cpp) ----
 // Enqueue one host->controller report: rid = report/command id, payload = the bytes AFTER [cmd][len] (what

+ 24 - 37
OpenPuck/puck_hid.cpp

@@ -147,23 +147,18 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 			// we're still presenting lizard (-> buzz loop).
 			hostStampAlive();
 		}
-		// Post-resume mute also gates haptics: while onReport45 is muted Steam reads NO 0x45 back, which is the
-		// exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
+		// PURE VERBATIM RELAY (like the real dongle): forward every actuator report 0x80-0x86 to the connected
+		// controller as-is. Steam owns the haptic engine; we inject/gate/rewrite nothing. The only gates kept are
+		// OpenPuck-environment ones the always-connected real puck never faces: relay only to the CONNECTED slot
+		// (we expose 4), only while the RF link is up (else it goes nowhere), NOT while presenting lizard (Steam
+		// isn't reading 0x45 back -> it would buzz-loop), and not during the brief post-resume input mute.
 		bool muted = g_resumeMs &&
 			     millis() - g_resumeMs < POST_RESUME_MUTE_MS;
 		if (rid >= 0x80 && rid <= 0x86 && n >= 1 &&
-		    hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
-			if (!haptic82Blocked()) {
-				relayHaptic(rid, b,
-					    (uint8_t)(n > RELAY_MAXP ?
-							      RELAY_MAXP :
-							      n));
-				// Track on/off from what was actually RELAYED (= the controller's believed state): a
-				// BLOCKED stop must leave "on" set (controller may be latched -> reconnect stop-burst),
-				// a blocked ON must not set it (nothing reached the controller -> no spurious clicks).
-				if (rid == 0x82)
-					haptic82HostReport(b, n);
-			}
+		    hapticRelaySlotOk(slot) && hapticLinkUp() &&
+		    !lizardActive() && !muted) {
+			relayEnqueue(rid, b,
+				     (uint8_t)(n > RELAY_MAXP ? RELAY_MAXP : n));
 		}
 
 		if (Serial.availableForWrite() > 80) {
@@ -196,37 +191,29 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 		// capture EVERY relayed feature-1 command for the WebUSB capture view (haptics, LED SET_LED_COLOR,
 		// 0x87 settings, 0x9F power-off). Log shows cmd as "rid"; bytes start [cmd][len]...
 		hapLogAdd((uint8_t)slot, cmd, b, n);
-		bool haptic82 = (cmd == 0x82 && len <= pln);
-		// ALL actuator reports 0x80-0x86 (not just 0x82) are gated like a haptic: blocked during the connect
-		// settle / lizard / post-resume mute. This matters at CONNECT -- Steam's handshake fires 0x81 haptic
-		// TRIGGERS, and since our framing discards Steam's bracketing 0x87 engine-config (whitelist) but the
-		// 0x81 still lands, those triggers played on an un-configured engine = the spurious connect buzzes the
-		// real puck never makes. Holding 0x80-0x86 off during the 3s settle suppresses them. 0x87+ settings and
-		// 0x9F power-off are NOT actuators and still relay. (The OUTPUT path already gates 0x80-0x86 this way;
-		// this aligns the feature path, which previously leaked 0x81/0x83-0x86 straight through.)
+		// PURE VERBATIM RELAY of the feature-0x01 passthrough, like the real dongle: forward every command to the
+		// connected controller as-is -- config/settings (0x87+), LED, 0x9F power-off, AND the haptic actuators
+		// (0x80-0x86). No 0x30 rewrite, no 0x81 special-case, no connect-settle block, no stop-burst: Steam owns
+		// the engine and the gyro exactly as on a real puck. Gates kept are only the OpenPuck-environment ones:
+		// connected slot; and for the haptic ACTUATORS (0x80-0x86) also link-up / not-lizard / not-post-resume
+		// (relaying a play while presenting lizard, i.e. Steam not reading 0x45, loops the haptic). Config/LED/
+		// power-off relay regardless (they're not plays and the controller needs them to track Steam's settings).
 		bool isActuator = (cmd >= 0x80 && cmd <= 0x86);
-
 		bool muted = g_resumeMs &&
 			     millis() - g_resumeMs < POST_RESUME_MUTE_MS;
-
-		// never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
-		bool relayOk = hapticRelaySlotOk(slot) &&
-			       !(isActuator && (lizardActive() || muted));
-		if (relayOk && (!isActuator || !haptic82Blocked())) {
-			// Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
-			// 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.
+		bool relayOk =
+			hapticRelaySlotOk(slot) &&
+			(!isActuator ||
+			 (hapticLinkUp() && !lizardActive() && !muted));
+		if (relayOk) {
+			// Relay the DECLARED length (up to the 60B RF frame ceiling); Steam's multi-register 0x87 settings
+			// blocks (LED brightness) and calibration writes exceed the old 18B cap.
 			uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
 			if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
 				Serial.printf(
 					"# RELAY TRUNC cmd=%02X len=%u>%u\n",
 					cmd, len, (unsigned)RELAY_MAXP);
-			// relayHaptic bursts a 0x82/0x80 STOP (gain/type 0); non-haptic cmds enqueue once, unchanged.
-			relayHaptic(cmd, pl, rl);
-
-			// track from RELAYED frames only (see the OUTPUT path)
-			if (haptic82)
-				haptic82HostReport(pl, len);
+			relayEnqueue(cmd, pl, rl);
 		}
 	}
 	if (Serial.availableForWrite() > 80) {

+ 7 - 4
OpenPuck/webusb_config.cpp

@@ -17,8 +17,8 @@ Adafruit_USBD_WebUSB usb_web;
 //                [loopPeriod_lo][loopPeriod_hi][loopWorstIdx][loopWorstUs_lo][loopWorstUs_hi]
 //                [pollPeriod_lo][pollPeriod_hi][logEnabled][battery%][rssi|dBm|]
 //                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
-// keep the blob under 64 bytes total (readBlob reads one 64-byte packet)
-#define WB_PAYLEN 60
+// keep the blob under 64 bytes total (readBlob reads one 64-byte packet); 2-byte header + payload
+#define WB_PAYLEN 61
 static void webusbSendBlob()
 {
 	if (!usb_web.connected())
@@ -28,8 +28,8 @@ static void webusbSendBlob()
 	p[0] = 0xA5;
 	p[1] = WB_PAYLEN;
 
-	// protocol version (7 = +raw accel; 6 = +swPro120/gyroScale; 5 = +rumbleScale)
-	p[2] = 7;
+	// protocol version (8 = +bootResetReas; 7 = +raw accel; 6 = +swPro120/gyroScale; 5 = +rumbleScale)
+	p[2] = 8;
 	p[3] = g_usbMode;
 	p[4] = (uint8_t)g_mDiv;
 	p[5] = (uint8_t)g_mFric;
@@ -90,6 +90,9 @@ static void webusbSendBlob()
 		int16_t a[3] = { g_in.ax, g_in.ay, g_in.az };
 		memcpy(&p[56], a, 6);
 	} // raw accelerometer for scale diagnostics (protocol v7)
+	// last MCU reset cause (protocol v8): low byte of the captured RESETREAS (bit0 PIN, 1 DOG/watchdog, 2 SREQ/
+	// software, 3 LOCKUP; 0 = power-on). Surfaces the reboot-on-buzz cause in the panel without the CDC console.
+	p[62] = (uint8_t)g_bootResetReas;
 	usb_web.write(p, sizeof p);
 	usb_web.flush();
 }

+ 8 - 0
docs/index.html

@@ -92,6 +92,7 @@
         <div class="stat logonly"><div class="k">Slowest stage</div><div class="v" id="stWorst">—</div></div>
         <div class="stat logonly"><div class="k">Poll period act/want</div><div class="v" id="stPollPer">—</div></div>
         <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: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>
@@ -296,6 +297,13 @@ function applyBlob(p){
     const amag=Math.round(Math.sqrt(ax*ax+ay*ay+az*az));
     $("#stImu").textContent = `a=(${ax}, ${ay}, ${az})  |a|=${amag}`;
   }
+  // last MCU reset cause (protocol v8): low byte of RESETREAS at p[60]. Bits: 0 PIN, 1 watchdog, 2 software, 3 lockup.
+  if(p.length>60){ const rr=p[60]; const r=[];
+    if(rr&1)r.push("pin"); if(rr&2)r.push("WATCHDOG"); if(rr&4)r.push("software"); if(rr&8)r.push("LOCKUP");
+    const bad=(rr&2)||(rr&8);   // watchdog/lockup = a hang or fault
+    $("#stReset").innerHTML = rr===0 ? '<span class="pill up">power-on</span>'
+      : '<span class="pill '+(bad?'dn':'up')+'">'+r.join("+")+'</span> (0x'+rr.toString(16)+')';
+  }
   { const sel=$("#swRate"); if(document.activeElement!==sel) sel.value=swProRate; }
   { 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]; }