Selaa lähdekoodia

Revert "Change initial handshake to be closer to the recorded activity from official puck"

This reverts commit 399a8b9b11501c84d5c87931b0ffdae6f86db7d6.
Jariullah Safi 4 kuukautta sitten
vanhempi
commit
b27d390311
3 muutettua tiedostoa jossa 104 lisäystä ja 85 poistoa
  1. 85 73
      OpenPuck/haptics.cpp
  2. 13 7
      OpenPuck/haptics.h
  3. 6 5
      OpenPuck/puck_hid.cpp

+ 85 - 73
OpenPuck/haptics.cpp

@@ -41,7 +41,14 @@ static unsigned long g_rumble80Ms = 0;
 // Steam/Triton rumble is latched on until an explicit zero report
 static bool g_rumble80On = false;
 
-// haptic activity happened (tracked for the 0x82 quiet timeout; the old idle-clear that read it is gone)
+// when to fire the next post-reconnect haptic re-init (0 = none scheduled)
+static unsigned long g_reinitAt = 0;
+
+// how many re-init shots remain in this connect window
+static uint8_t g_reinitLeft = 0;
+
+// haptic activity happened -> arm a clear once it goes idle (catches a
+// latch that engaged during/after use, even seconds after connect)
 static bool g_hapClearArmed = false;
 
 // ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
@@ -49,7 +56,6 @@ static bool g_hapClearArmed = false;
 // touches the tail entry, which no producer writes while the ring isn't full -- so a flush can't be torn.
 struct RelayMsg {
 	uint8_t rid, len;
-	uint8_t land;	// 1 = force type-01 landing framing (our connect-init); 0 = whitelist decides
 	uint8_t data[RELAY_MAXP];
 };
 // deep enough to hold a full Steam settings/LED transaction burst without loss
@@ -67,8 +73,7 @@ bool relayPending()
 {
 	return g_rqHead != g_rqTail;
 }
-static bool relayEnqueueX(uint8_t rid, const uint8_t *payload, uint8_t plen,
-			  uint8_t land)
+bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen)
 {
 	// 60B is the RF frame ceiling; longer can't be relayed
 	if (plen > RELAY_MAXP)
@@ -83,12 +88,11 @@ static bool relayEnqueueX(uint8_t rid, const uint8_t *payload, uint8_t plen,
 		g_rqTail = rqNext(g_rqTail);
 	g_rq[h].rid = rid;
 	g_rq[h].len = plen;
-	g_rq[h].land = land;
 	if (plen)
 		memcpy(g_rq[h].data, payload, plen);
 	g_rqHead = nx;
-	// Any haptic relay (Steam OR Xbox rumble OR test) refreshes the activity timer (used by the 0x82 quiet
-	// timeout). Our connect-init (land=1, rid 0x87/0x81) deliberately doesn't match, so it isn't tracked as play.
+	// Any haptic relay (Steam OR Xbox rumble OR test) arms the idle-clear and refreshes its timer -- so the
+	// during-use buzz gets cleared in EVERY USB mode. The re-init's own 0x81/0x87 deliberately don't match.
 	if (rid == 0x82 || rid == 0x80) {
 		g_haptic82Ms = millis();
 		g_hapClearArmed = true;
@@ -96,15 +100,25 @@ static bool relayEnqueueX(uint8_t rid, const uint8_t *payload, uint8_t plen,
 	__set_PRIMASK(pm);
 	return true;
 }
-bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen)
-{
-	return relayEnqueueX(rid, payload, plen, 0);
-}
-// Enqueue a frame that MUST land on the controller (type-01 + inner-len), bypassing the discard whitelist.
-// Used ONLY by hapticConnectInit -- frames WE originate at connect, never Steam's runtime passthrough.
-bool relayEnqueueLand(uint8_t rid, const uint8_t *payload, uint8_t plen)
+// Relay a haptic actuator report (0x80-0x86), BURSTING a stop. CONFIRMED in the real-puck sniff (during-play,
+// t=49075..50230): the glide is a stream of `82 [side] 01 [gain]`, and Steam DOES send explicit stops
+// `82 [side] 01 00` (gain 0). OpenPuck relays each frame exactly ONCE over a NO-ACK link, so a single dropped
+// stop leaves that side's actuator driven -> the persistent buzz that survives mode switches and only the Steam
+// button (controller-side reset) clears. The fix is the same one already used for power-off (hapticSendShutdown
+// bursts because "a single lost frame must not leave the controller on"): send the STOP several times. A gain-0
+// stop is SILENT, so repeating it adds no pad clicks -- unlike an ON pulse, which we still send exactly once.
+//   * 0x82 stop = gain byte (payload[2]) == 0     (per side; Steam sends one stop frame per side)
+//   * 0x80 stop = type byte (payload[0]) == 0     (the rumble off/zero report)
+// Non-haptic rids (LED/settings/power-off routed through here) are never stops -> single enqueue, unchanged.
+bool relayHaptic(uint8_t rid, const uint8_t *p, uint8_t n)
 {
-	return relayEnqueueX(rid, payload, plen, 1);
+	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;
 }
 
 #if OPK_LOG
@@ -285,7 +299,7 @@ bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq)
 	p[6] = (uint8_t)(highFreq & 0xFF);
 	p[7] = (uint8_t)(highFreq >> 8);
 	p[8] = 0;
-	if (!relayEnqueue(0x80, p, sizeof p))
+	if (!relayHaptic(0x80, p, sizeof p)) // bursts the zero/off report (p[0]==0)
 		return false;
 	g_rumble80Ms = millis();
 	g_rumble80On = on;
@@ -327,10 +341,8 @@ void rfConnFlushRelay(uint8_t ch, uint8_t s1)
 			// Steam ALSO sends other 0x87 passthrough writes during play -- the haptic-config block (reg 0x30 =
 			// IMU/subsystem enable, 0x34/0x35 = haptic amplitude). Landing those regresses BOTH: a landed 0x30 FREEZES
 			// the IMU (gyro stops) and landed 0x34/0x35 = the connect buzz. So keep this list tight.
-			// m.land == our connect-init: force the landing form regardless of rid. Otherwise keep the tight
-			// whitelist (LED brightness + power-off); Steam's runtime 0x87 passthrough still goes legacy/discarded.
 			bool land01 =
-				m.land || (m.rid == 0x9F) ||
+				(m.rid == 0x9F) ||
 				(m.rid == 0x87 && rl >= 1 && m.data[0] == 0x2D);
 			uint8_t p[5 + RELAY_MAXP], plen;
 			if (land01) {
@@ -366,45 +378,38 @@ void rfConnFlushRelay(uint8_t ch, uint8_t s1)
 	}
 }
 
-// CONNECT-INIT: the haptic-engine initialization the real Valve puck performs immediately after the controller
-// connects, lifted BYTE-FOR-BYTE from a real puck<->controller sniff (steam-controller-sniff-data.json, the
-// t=41373..41736ms handshake right after the first F1 reply). All frames go out in the type-01 LANDING form
-// (relayEnqueueLand) -- this is the WHOLE point: the legacy form is discarded for 0x87, which is why the
-// engine never got initialized before and Steam's relayed 0x82/0x80 hit an un-init'd engine and latched.
+// Haptic-subsystem RE-INIT: the exact sequence Steam sends (captured on hardware) when it (re)takes control,
+// which clears a stuck haptic script on the controller -- a 0x81 reset action plus 0x87 writes to the haptic
+// registers (30/07/08/31/52, 18/2e/34/35). Replayed to recover from the latched-buzz the controller falls
+// into across a reconnect. Brightness (0x87 reg 2d) is deliberately OMITTED so we don't stomp the LED.
 //
-// We replay the puck's ENABLE half only and STOP at 0x30=0x18:
-//   87 22=0064, 87 23=0050            -- settings the puck writes just before the haptic block
-//   87 [30=18 07=07 08=07 31=02 52=03] -- 0x30=0x18 ENABLES the gyro+haptic subsystem, plus engine config
-//   87 [18=00 2e=00 34=ffff 35=ffff 2e=00] / 87 [34=ffff 35=ffff] -- amplifier/gain (max)
-//   81                                 -- reset/commit trigger (clears any latched haptic script)
-// The real puck then writes 0x30=0x00 (DISABLE) to end the sequence -- verified in the capture, after which the
-// gyro stream FREEZES at a constant. On the real system that's fine because Steam re-enables 0x30=0x18 on demand
-// in puck mode. OpenPuck relies on the controller's default-on gyro stream in EVERY mode (Steam raw-forwards
-// 0x45, the emulated modes read g_in) and never sends a play-time 0x30=0x18 -- so we deliberately OMIT the
-// disable tail and leave the subsystem ENABLED. (This is exactly why the earlier "replay the full sequence"
-// attempt froze the gyro: it wasn't a framing bug, the puck genuinely disables gyro at the end.)
-void hapticConnectInit()
-{
-	static const uint8_t S22[] = { 0x22, 0x64, 0x00 };
-	static const uint8_t S23[] = { 0x23, 0x50, 0x00 };
-	static const uint8_t EN[] = { 0x30, 0x18, 0x00, 0x07, 0x07,
-				      0x00, 0x08, 0x07, 0x00, 0x31,
-				      0x02, 0x00, 0x52, 0x03, 0x00 };
-	static const uint8_t CFG[] = { 0x18, 0x00, 0x00, 0x2e, 0x00,
-				       0x00, 0x34, 0xff, 0xff, 0x35,
-				       0xff, 0xff, 0x2e, 0x00, 0x00 };
-	static const uint8_t AMP[] = { 0x34, 0xff, 0xff, 0x35, 0xff, 0xff };
-	relayEnqueueLand(0x87, S22, sizeof S22);
-	relayEnqueueLand(0x87, S23, sizeof S23);
-	relayEnqueueLand(0x87, EN, sizeof EN);	 // 0x30=0x18 enable + engine config
-	relayEnqueueLand(0x87, CFG, sizeof CFG); // amplifier/gain
-	relayEnqueueLand(0x87, AMP, sizeof AMP);
-	relayEnqueueLand(0x81, nullptr, 0);	 // reset/commit -- clears any latched script
-}
-// Manual "clear stuck buzz" (WebUSB op) now routes through the faithful landing init.
+// The 0x87 frames below go out on LEGACY (type-05) framing (the whitelist lands 0x87 only for brightness
+// 0x2D), so the controller DISCARDS them -- kept verbatim only to match the captured sequence. The effective
+// re-init is the three 0x81 frames. (Landing the 0x30 here would freeze the gyro.)
 void hapticReinit()
 {
-	hapticConnectInit();
+	static const uint8_t H30[] = { 0x30, 0x00, 0x00, 0x07, 0x07,
+				       0x00, 0x08, 0x07, 0x00, 0x31,
+				       0x02, 0x00, 0x52, 0x03, 0x00 };
+	static const uint8_t H18[] = { 0x18, 0x00, 0x00, 0x2e, 0x00,
+				       0x00, 0x34, 0xff, 0xff, 0x35,
+				       0xff, 0xff, 0x34, 0xff, 0xff };
+	static const uint8_t H35[] = { 0x35, 0xff, 0xff, 0x2e, 0x00, 0x00 };
+	static const uint8_t T81A[] = {
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+	};
+	static const uint8_t T81B[] = {
+		0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+	};
+	// reset action (FUN_0001f554) -- Steam sends this first
+	relayEnqueue(0x81, nullptr, 0);
+	relayEnqueue(0x87, H30, sizeof H30);
+
+	// haptic config (enabled/amplifier/gain): the part that clears a latch
+	relayEnqueue(0x87, H18, sizeof H18);
+	relayEnqueue(0x87, H35, sizeof H35);
+	relayEnqueue(0x81, T81A, sizeof T81A);
+	relayEnqueue(0x81, T81B, sizeof T81B);
 }
 void hapticInit()
 {
@@ -430,19 +435,16 @@ void hapticOnReconnect()
 
 	// drop any haptic ON queued before the link came up
 	hapticCancelPendingOn();
-	// Replay the real puck's connect-init ONCE, exactly as it does right after the controller answers: land the
-	// haptic-engine enable+config so Steam's subsequent haptics hit an initialized engine (instead of latching).
-	// Dedup: this fires both from rf_link (gap>1500ms) and hapticTask's 300ms link-up edge, which land within a
-	// few ms of each other -- guard so a single reconnect injects the init once, while a genuine new reconnect
-	// (>1s later) re-inits. The frames drain over ~6 poll cycles, inside the 3s block, before any Steam haptic.
-	static unsigned long lastInit = 0;
-	if (millis() - lastInit > 1000u) {
-		lastInit = millis();
-		hapticConnectInit();
-	}
+	// Re-init the haptic engine repeatedly across the settle window rather than waiting for the block to end: the
+	// brief connect buzz engages early (during the block, controller-internal), so a single late shot misses it.
+	// The re-init is settings only (no haptic play) and Steam haptics are blocked throughout, so it can't buzz.
+	g_reinitAt = millis() + 200u; // first reset ~200ms after (re)connect
+
+	// then every HAPTIC_REINIT_GAP_MS across the window
+	g_reinitLeft = HAPTIC_REINIT_SHOTS;
 	uint8_t mk = 2;
 
-	// capture marker: RECONNECT detected (block armed + connect-init queued)
+	// capture marker: RECONNECT detected (block+reinit armed)
 	hapLogAdd(0xFD, 0xEE, &mk, 1);
 }
 void hapticTask()
@@ -461,7 +463,14 @@ void hapticTask()
 		hapLogAdd(0xFD, 0xEE, &mk, 1);
 	}
 	wasHapticLinkUp = up;
-	// (Connect re-init is now a single faithful hapticConnectInit() fired from hapticOnReconnect -- no flood.)
+	if (g_reinitAt && up &&
+	    (int32_t)(millis() - g_reinitAt) >=
+		    0) { // proactive haptic re-init across the connect window
+		hapticReinit();
+		g_reinitAt = (g_reinitLeft && --g_reinitLeft) ?
+				     (millis() + HAPTIC_REINIT_GAP_MS) :
+				     0;
+	}
 	// 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
@@ -483,10 +492,13 @@ void hapticTask()
 		g_haptic82On = false;
 	if (g_rumble80On && millis() - g_rumble80Ms > 2500u)
 		hapticSteamRumble(0, 0);
-	// Idle-clear re-init REMOVED. The engine is now properly initialized once at connect (hapticConnectInit), so
-	// Steam's haptics shouldn't latch in the first place -- and the old idle-clear re-injected 0x81/0x87 ~1.2s
-	// after every haptic burst, which was a likelier latch CAUSE than cure. If a host-driven latch ever resurfaces
-	// (a continuous 0x80 whose zero-stop was lost over the NO-ACK relay), the fix is to deliver that stop reliably
-	// (burst it), not to re-inject. g_hapClearArmed is left set for potential future use.
-	(void)g_hapClearArmed;
+	// Haptic activity has gone idle for a while -> fire one re-init to clear any latch it left behind (the buzz
+	// that engages during/after use and won't self-clear, incl. after a mode switch). Fires only after a quiet
+	// gap, so it never interrupts active haptics; the brightness-less re-init is silent (settings, no play, no
+	// LED). Runs in ALL modes (the controller-side latch is mode-independent).
+	if (g_hapClearArmed &&
+	    (millis() - g_haptic82Ms) > HAPTIC_CLEAR_IDLE_MS) {
+		g_hapClearArmed = false;
+		hapticReinit();
+	}
 }

+ 13 - 7
OpenPuck/haptics.h

@@ -29,6 +29,14 @@
 #define HAPTIC_STOP_BURST 4u
 // max relayed payload bytes per entry: RF frame = [E3][len][05][rid][payload] and MAXLEN=64 -> 60
 #define RELAY_MAXP 60u
+// Proactive post-(re)connect haptic re-init: this many shots, this far apart, starting ~200ms after the link
+// comes up -- covers ~0.2s..3s so the brief controller-side connect buzz gets reset before it can sustain.
+#define HAPTIC_REINIT_SHOTS 8u
+#define HAPTIC_REINIT_GAP_MS 350u
+// After haptic activity, if it's been idle this long, fire one clear-re-init -- kills a latch that engaged
+// during use (a buzz that starts seconds after connect and won't self-clear). Long enough not to fire between
+// rapid in-game haptics; short enough to clear a stuck buzz soon after the user pauses.
+#define HAPTIC_CLEAR_IDLE_MS 1200u
 // 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
@@ -37,8 +45,10 @@
 // Enqueue one host->controller report: rid = report/command id, payload = the bytes AFTER [cmd][len] (what
 // goes on the air). ISR-safe (brief PRIMASK critical section). Returns false (dropped) when the ring is full.
 bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen);
-// Same, but forces the type-01 LANDING framing (bypasses the discard whitelist). Connect-init use only.
-bool relayEnqueueLand(uint8_t rid, const uint8_t *payload, uint8_t plen);
+// Relay a haptic actuator report (0x80-0x86), BURSTING a stop (0x82 gain==0 / 0x80 type==0) HAPTIC_STOP_BURST
+// times so a single NO-ACK loss can't leave the actuator latched (the buzz). ON pulses enqueue once. Use this
+// for the host->controller haptic relay instead of relayEnqueue.
+bool relayHaptic(uint8_t rid, const uint8_t *payload, uint8_t plen);
 
 // anything still queued (xinput uses it to pace rumble re-queues)
 bool relayPending();
@@ -99,11 +109,7 @@ void hapticInit();
 // per-loop upkeep: link-edge markers + steam 0x82 quiet timeout + fires the scheduled re-init
 void hapticTask();
 
-// Replay the real puck's connect-time haptic-engine init (landing-framed 0x87 enable+config + 0x81), lifted
-// byte-for-byte from the hardware sniff. Initializes the engine so Steam's haptics don't latch. Leaves the
-// subsystem ENABLED (omits the puck's 0x30=0x00 disable tail, which would freeze OpenPuck's always-on gyro).
-void hapticConnectInit();
-// Manual "clear stuck buzz" (WebUSB op) -- alias for hapticConnectInit (the faithful landing init).
+// replay Steam's haptic-subsystem re-init to the controller -> clears a latched/stuck buzz
 void hapticReinit();
 // 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

+ 6 - 5
OpenPuck/puck_hid.cpp

@@ -154,10 +154,10 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 		if (rid >= 0x80 && rid <= 0x86 && n >= 1 &&
 		    hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
 			if (!haptic82Blocked()) {
-				relayEnqueue(rid, b,
-					     (uint8_t)(n > RELAY_MAXP ?
-							       RELAY_MAXP :
-							       n));
+				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).
@@ -213,7 +213,8 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
 				Serial.printf(
 					"# RELAY TRUNC cmd=%02X len=%u>%u\n",
 					cmd, len, (unsigned)RELAY_MAXP);
-			relayEnqueue(cmd, pl, rl);
+			// 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)