Просмотр исходного кода

fix(rf): debounce link state so a transient RF gap can't loop the connect haptic

The connect-time buzz is an app-layer feedback loop. A brief reply gap was
treated as a disconnect+reconnect, which (a) re-armed the haptic re-init burst
via hapticOnReconnect and (b) re-fired the USB 0x79=02 connect edge, making
Steam re-run its connect handling (chime + connect haptic). On a flapping link
this repeats and the controller buzzes nonstop.

Introduce a debounced per-slot g_linkUp: set on the first F-type reply, held
through gaps shorter than LINK_DOWN_MS (2s), dropped only on a sustained outage
(watchdog in rfLinkTask) or a 0xF2 disconnect. Drive the haptic re-init, the
0x79 connect edge, and the haptic-task edge off g_linkUp instead of the raw
300ms/1500ms reply windows, so only a genuine reconnect re-inits/re-edges.

Also slow the session-channel E1 keepalive from 25ms to 1s while connected: the
controller adopts and HOLDS the advertised base/prefix/channel for the session
(RE shows the bond stores only UUIDs+serial), so frequent session E1 only
steals poll reply windows, causing the reply-rate sag that produced the gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jariullah Safi 4 месяцев назад
Родитель
Сommit
dfa06af720
4 измененных файлов с 53 добавлено и 23 удалено
  1. 8 11
      OpenPuck/haptics.cpp
  2. 4 1
      OpenPuck/puck_hid.cpp
  3. 34 11
      OpenPuck/rf_link.cpp
  4. 7 0
      OpenPuck/rf_link.h

+ 8 - 11
OpenPuck/haptics.cpp

@@ -466,22 +466,19 @@ void hapticOnReconnect(int slot)
 }
 }
 void hapticTask()
 void hapticTask()
 {
 {
-	// Per-slot link-edge detect (backup for hapticOnReconnect in rf_link).
-	static bool wasHapticLinkUp[NSLOT] = { 0 };
+	// Link-edge LOGGING only. The (re)connect haptic re-init is owned by rf_link's debounced g_linkUp
+	// down->up edge (fired off the reply stream). Re-arming here on the raw 300 ms hapticLinkUp() flag
+	// turned every transient reply gap into a re-init burst -> connect-time buzz on a flapping link.
+	static bool wasUp[NSLOT] = { 0 };
 	for (int s = 0; s < NSLOT; s++) {
 	for (int s = 0; s < NSLOT; s++) {
 		if (!g_slot[s].used)
 		if (!g_slot[s].used)
 			continue;
 			continue;
-		bool up = hapticLinkUp(s);
-		if (up && !wasHapticLinkUp[s]) {
-			uint8_t mk = 1;
+		bool up = g_linkUp[s];
+		if (up != wasUp[s]) {
+			uint8_t mk = up ? 1 : 0;
 			hapLogAdd(0xFD, 0xEE, &mk, 1);
 			hapLogAdd(0xFD, 0xEE, &mk, 1);
-			hapticOnReconnect(s);
 		}
 		}
-		if (!up && wasHapticLinkUp[s]) {
-			uint8_t mk = 0;
-			hapLogAdd(0xFD, 0xEE, &mk, 1);
-		}
-		wasHapticLinkUp[s] = up;
+		wasUp[s] = up;
 	}
 	}
 	if (g_reinitAt && anySlotLinkUp() &&
 	if (g_reinitAt && anySlotLinkUp() &&
 	    (int32_t)(millis() - g_reinitAt) >= 0) {
 	    (int32_t)(millis() - g_reinitAt) >= 0) {

+ 4 - 1
OpenPuck/puck_hid.cpp

@@ -518,7 +518,10 @@ void SteamPuckController::task()
 	for (int s = 0; s < NSLOT; s++) {
 	for (int s = 0; s < NSLOT; s++) {
 		if (!g_slot[s].used || !hid[s].ready())
 		if (!g_slot[s].used || !hid[s].ready())
 			continue;
 			continue;
-		bool conn = (millis() - g_connReplyMs[s] < 300);
+		// Debounced link state, not the raw 300 ms reply window: a brief RF gap must NOT toggle the
+		// connection edge, or each blip re-fires the 0x79=02 connect edge -> Steam re-runs connect handling
+		// (chime + connect-time haptic) -> buzz loop. g_linkUp drops only after a real outage (rf_link).
+		bool conn = g_linkUp[s];
 		// 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
 		// 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
 		// settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
 		// settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
 		// unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before
 		// unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before

+ 34 - 11
OpenPuck/rf_link.cpp

@@ -63,6 +63,9 @@ uint16_t g_pollsps = 0;
 uint16_t g_pollPeriodUs = 0;
 uint16_t g_pollPeriodUs = 0;
 static uint32_t g_pollDtSum = 0;
 static uint32_t g_pollDtSum = 0;
 static uint16_t g_pollDtCnt = 0;
 static uint16_t g_pollDtCnt = 0;
+// Debounced per-slot link state (see rf_link.h). Drives the haptic re-init, the 0x79 connect edge and the
+// haptic-task edge so a transient reply gap is not mistaken for a reconnect.
+volatile bool g_linkUp[NSLOT] = { false };
 // smoothed |dBm| of the controller's replies, per slot (0 = none yet)
 // smoothed |dBm| of the controller's replies, per slot (0 = none yet)
 volatile uint8_t g_linkRssi[NSLOT] = { 0 };
 volatile uint8_t g_linkRssi[NSLOT] = { 0 };
 // battery % from the controller's report 0x43 (body[1]); 0 = none yet. Per-slot -- the active controller's
 // battery % from the controller's report 0x43 (body[1]); 0 = none yet. Per-slot -- the active controller's
@@ -268,13 +271,15 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 				int s = (g_curSlot >= 0 && g_curSlot < NSLOT) ?
 				int s = (g_curSlot >= 0 && g_curSlot < NSLOT) ?
 						g_curSlot :
 						g_curSlot :
 						0;
 						0;
-				// A reply after a long gap (or the first ever) = a (re)connect. Arm the haptic block + re-init HERE,
-				// directly off the reply stream -- reliable even when hapticTask's 300ms link-up edge doesn't fire
-				// (e.g. a power-cycled controller that reconnects without us cleanly seeing the link drop).
-				if (g_connReplyMs[s] == 0 ||
-				    (uint32_t)(millis() - g_connReplyMs[s]) >
-					    1500u)
+				// Genuine (re)connect = a debounced down->up edge, not merely a long reply gap. A
+				// transient gap (beacon contention, a handful of lost polls) keeps g_linkUp set, so it
+				// no longer re-arms the haptic block + re-init burst -- that re-arm on every >1.5s gap was
+				// the connect-time buzz on a flapping link. g_linkUp is cleared by the LINK_DOWN_MS
+				// watchdog in rfLinkTask or by a 0xF2, so a real reconnect still fires exactly once.
+				if (!g_linkUp[s]) {
+					g_linkUp[s] = true;
 					hapticOnReconnect(s);
 					hapticOnReconnect(s);
+				}
 				g_connRx++;
 				g_connRx++;
 				// link alive -> loop() suppresses the redundant E1 beacon
 				// link alive -> loop() suppresses the redundant E1 beacon
 				g_connReplyMs[s] = millis();
 				g_connReplyMs[s] = millis();
@@ -299,6 +304,10 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 			// controller for 2.5s (a real multi-controller disconnect). The powering-off slot goes silent
 			// controller for 2.5s (a real multi-controller disconnect). The powering-off slot goes silent
 			// and ages out via SLOT_COLD on its own.
 			// and ages out via SLOT_COLD on its own.
 			if (rtype == 0xF2) {
 			if (rtype == 0xF2) {
+				// explicit disconnect -> drop the link latch so the controller's eventual return is a
+				// clean reconnect (single haptic re-init), not a continuation of the old session.
+				if (g_curSlot >= 0 && g_curSlot < NSLOT)
+					g_linkUp[g_curSlot] = false;
 				int others = 0;
 				int others = 0;
 				for (int i = 0; i < NSLOT; i++)
 				for (int i = 0; i < NSLOT; i++)
 					if (i != g_curSlot && g_slot[i].used &&
 					if (i != g_curSlot && g_slot[i].used &&
@@ -647,6 +656,11 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 #define SLOT_COLD_RETRY_MS 2000u
 #define SLOT_COLD_RETRY_MS 2000u
 static unsigned long g_slotLastAttemptMs[NSLOT] = {};
 static unsigned long g_slotLastAttemptMs[NSLOT] = {};
 
 
+// A slot stays "linked" the instant it answers and through brief RF gaps; only a sustained outage this long
+// (or a 0xF2) clears g_linkUp and counts as a disconnect for the haptic re-init / 0x79 connect edge. Well
+// above the worst transient gap (beacon contention, a few lost polls) and below SLOT_COLD_MS.
+#define LINK_DOWN_MS 2000u
+
 // Drive the connected-mode sequence. The cycle gate fires once per g_pollUs (250 Hz); each fire
 // Drive the connected-mode sequence. The cycle gate fires once per g_pollUs (250 Hz); each fire
 // polls EVERY warm slot back-to-back so all bonded controllers run at full rate regardless of count.
 // polls EVERY warm slot back-to-back so all bonded controllers run at full rate regardless of count.
 // "Cold" means the slot WAS connected but has been silent for > SLOT_COLD_MS; slots that have never
 // "Cold" means the slot WAS connected but has been silent for > SLOT_COLD_MS; slots that have never
@@ -762,18 +776,27 @@ static void rfConnStep()
 
 
 void rfLinkTask()
 void rfLinkTask()
 {
 {
+	// Clear the debounced link latch after a sustained outage so the controller's next reply is treated as
+	// a fresh (re)connect (one haptic re-init), while brief gaps below LINK_DOWN_MS keep it up.
+	for (int s = 0; s < NSLOT; s++)
+		if (g_linkUp[s] &&
+		    (uint32_t)(millis() - g_connReplyMs[s]) > LINK_DOWN_MS)
+			g_linkUp[s] = false;
 	// Host-frame beacon: sent continuously, INCLUDING while connected. The controller uses the periodic E1 (the
 	// Host-frame beacon: sent continuously, INCLUDING while connected. The controller uses the periodic E1 (the
 	// real puck's per-hop-cycle announce) to stay synced and keep answering polls at full rate; suppressing it
 	// real puck's per-hop-cycle announce) to stay synced and keep answering polls at full rate; suppressing it
 	// drops the reply rate from ~210/s to ~38/s. Paused only during the post-disconnect cooldown so a controller
 	// drops the reply rate from ~210/s to ~38/s. Paused only during the post-disconnect cooldown so a controller
 	// that's powering off isn't immediately re-woken/reconnected.
 	// that's powering off isn't immediately re-woken/reconnected.
 	if (g_rfHost && millis() - g_connCooldown > 2500) {
 	if (g_rfHost && millis() - g_connCooldown > 2500) {
 		bool connNow = anySlotLinkUp();
 		bool connNow = anySlotLinkUp();
-		// session keepalive on the clean channel: every loop while connecting (fast), every 25ms once connected
-		// (every-loop beaconing also hammers the session ch and steals reply slots from the poll). The real puck
-		// sends NO E1 on its session channel; gated by g_e1keepalive ('m') so this can be A/B'd on hardware --
-		// but it stays ON by default because OpenPuck's shared-address model relies on E1 to advertise the session.
+		// session keepalive on the clean channel: every loop while connecting (fast), then SLOW once
+		// connected. RE of the controller shows it ADOPTS the advertised base/prefix/channel and HOLDS
+		// them for the session (the bond stores only UUIDs+serial), so it does not need the address
+		// re-advertised at 25ms -- the real puck sends no session E1 at all. A fast session beacon only
+		// steals reply windows from the poll -> reply-rate sag -> >LINK_DOWN_MS gaps -> spurious reconnect
+		// -> connect-time buzz. 1s is enough to re-sync a controller that briefly lost the session without
+		// contending with the 250Hz poll. Gated by g_e1keepalive ('m').
 		if (g_e1keepalive &&
 		if (g_e1keepalive &&
-		    millis() - g_lastSessBeacon >= (connNow ? 25u : 0u)) {
+		    millis() - g_lastSessBeacon >= (connNow ? 1000u : 0u)) {
 			g_lastSessBeacon = millis();
 			g_lastSessBeacon = millis();
 			g_rfCh = g_sessCh;
 			g_rfCh = g_sessCh;
 			for (int s = 0; s < NSLOT; s++)
 			for (int s = 0; s < NSLOT; s++)

+ 7 - 0
OpenPuck/rf_link.h

@@ -61,6 +61,13 @@ extern int g_curSlot;
 // re-init gating (we only re-init on a real link) and the beacon pacing.
 // re-init gating (we only re-init on a real link) and the beacon pacing.
 bool anySlotLinkUp();
 bool anySlotLinkUp();
 
 
+// Debounced per-slot link state: set true on the first F-type reply, held through brief reply gaps, and
+// dropped only on a real outage (no reply for LINK_DOWN_MS) or an explicit 0xF2 disconnect. The haptic
+// re-init, the USB 0x79 connect edge, and the haptic-task link edge all key off THIS rather than the raw
+// 300 ms reply window, so a transient RF gap (beacon contention, a few lost polls) can't masquerade as a
+// reconnect and loop the connect-time haptic.
+extern volatile bool g_linkUp[NSLOT];
+
 // QoS adaptive channel hopping
 // QoS adaptive channel hopping
 extern uint8_t g_qos; // 0=off (static g_sessCh), 1=auto-hop on degradation
 extern uint8_t g_qos; // 0=off (static g_sessCh), 1=auto-hop on degradation
 extern uint8_t g_hopIdx;
 extern uint8_t g_hopIdx;