|
|
@@ -41,14 +41,7 @@ static unsigned long g_rumble80Ms = 0;
|
|
|
// Steam/Triton rumble is latched on until an explicit zero report
|
|
|
static bool g_rumble80On = false;
|
|
|
|
|
|
-// 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)
|
|
|
+// haptic activity happened (tracked for the 0x82 quiet timeout; the old idle-clear that read it is gone)
|
|
|
static bool g_hapClearArmed = false;
|
|
|
|
|
|
// ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
|
|
|
@@ -56,6 +49,7 @@ 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
|
|
|
@@ -73,7 +67,8 @@ bool relayPending()
|
|
|
{
|
|
|
return g_rqHead != g_rqTail;
|
|
|
}
|
|
|
-bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen)
|
|
|
+static bool relayEnqueueX(uint8_t rid, const uint8_t *payload, uint8_t plen,
|
|
|
+ uint8_t land)
|
|
|
{
|
|
|
// 60B is the RF frame ceiling; longer can't be relayed
|
|
|
if (plen > RELAY_MAXP)
|
|
|
@@ -88,11 +83,12 @@ bool relayEnqueue(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) 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.
|
|
|
+ // 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.
|
|
|
if (rid == 0x82 || rid == 0x80) {
|
|
|
g_haptic82Ms = millis();
|
|
|
g_hapClearArmed = true;
|
|
|
@@ -100,6 +96,16 @@ bool relayEnqueue(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)
|
|
|
+{
|
|
|
+ return relayEnqueueX(rid, payload, plen, 1);
|
|
|
+}
|
|
|
|
|
|
#if OPK_LOG
|
|
|
// diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
|
|
|
@@ -321,8 +327,10 @@ 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.rid == 0x9F) ||
|
|
|
+ m.land || (m.rid == 0x9F) ||
|
|
|
(m.rid == 0x87 && rl >= 1 && m.data[0] == 0x2D);
|
|
|
uint8_t p[5 + RELAY_MAXP], plen;
|
|
|
if (land01) {
|
|
|
@@ -358,38 +366,45 @@ void rfConnFlushRelay(uint8_t ch, uint8_t s1)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 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.
|
|
|
+// 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.
|
|
|
//
|
|
|
-// 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()
|
|
|
+// 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 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,
|
|
|
+ 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, 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);
|
|
|
+ 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.
|
|
|
+void hapticReinit()
|
|
|
+{
|
|
|
+ hapticConnectInit();
|
|
|
}
|
|
|
void hapticInit()
|
|
|
{
|
|
|
@@ -415,16 +430,19 @@ void hapticOnReconnect()
|
|
|
|
|
|
// drop any haptic ON queued before the link came up
|
|
|
hapticCancelPendingOn();
|
|
|
- // 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;
|
|
|
+ // 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();
|
|
|
+ }
|
|
|
uint8_t mk = 2;
|
|
|
|
|
|
- // capture marker: RECONNECT detected (block+reinit armed)
|
|
|
+ // capture marker: RECONNECT detected (block armed + connect-init queued)
|
|
|
hapLogAdd(0xFD, 0xEE, &mk, 1);
|
|
|
}
|
|
|
void hapticTask()
|
|
|
@@ -443,14 +461,7 @@ void hapticTask()
|
|
|
hapLogAdd(0xFD, 0xEE, &mk, 1);
|
|
|
}
|
|
|
wasHapticLinkUp = up;
|
|
|
- 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;
|
|
|
- }
|
|
|
+ // (Connect re-init is now a single faithful hapticConnectInit() fired from hapticOnReconnect -- no flood.)
|
|
|
// 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
|
|
|
@@ -472,13 +483,10 @@ void hapticTask()
|
|
|
g_haptic82On = false;
|
|
|
if (g_rumble80On && millis() - g_rumble80Ms > 2500u)
|
|
|
hapticSteamRumble(0, 0);
|
|
|
- // 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();
|
|
|
- }
|
|
|
+ // 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;
|
|
|
}
|