|
|
@@ -13,7 +13,9 @@ uint8_t g_relayOp = 0xE3; // E3 poll
|
|
|
uint8_t g_relaySub = 0x01;
|
|
|
volatile uint8_t g_testHaptic = 0;
|
|
|
volatile uint8_t g_hapticStop = 0;
|
|
|
+unsigned long g_buzzFloodUntil = 0; // 10Hz re-init flood active while millis() < this; armed boot/connect/panel
|
|
|
|
|
|
+void hapticArmBuzzFlood(){ g_buzzFloodUntil = millis() + HAPTIC_FLOOD_MS; }
|
|
|
// Controller power-off. CONFIRMED from a real Windows USB capture of the Valve puck (shutoff.pcapng): Steam's
|
|
|
// "turn off controller" is the single feature-0x01 command 0x9F with payload ASCII "off!" (6F 66 66 21). The
|
|
|
// PROTEUS dongle forwards host feature reports to the controller verbatim as SET_FR (esb.json host_usb_bridge),
|
|
|
@@ -27,7 +29,9 @@ unsigned long g_hapticBlockUntil = 0; // drop Steam haptics briefly during
|
|
|
|
|
|
static unsigned long g_haptic82Ms = 0; // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
|
|
|
static bool g_haptic82On = false; // a non-zero 0x82 haptic is currently active (awaiting host stop)
|
|
|
-static unsigned long g_reinitAt = 0; // when to fire the ONE post-reconnect haptic init (0 = none scheduled)
|
|
|
+static unsigned long g_reinitAt = 0; // when to fire the ONE post-(re)connect haptic CONNECT-INIT (0 = none)
|
|
|
+static bool g_hapClearArmed=false;// haptic activity happened -> arm a clear once it goes idle (catches a
|
|
|
+ // latch that engaged during/after use, even seconds after connect)
|
|
|
|
|
|
// ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
|
|
|
// Producers serialize through a brief PRIMASK critical section (copy is <=62 bytes); the consumer only ever
|
|
|
@@ -52,7 +56,9 @@ bool relayEnqueue(uint8_t rid, const uint8_t* payload, uint8_t plen){
|
|
|
g_rq[h].rid = rid; g_rq[h].len = plen;
|
|
|
if (plen) memcpy(g_rq[h].data, payload, plen);
|
|
|
g_rqHead = nx;
|
|
|
- if (rid==0x82) g_haptic82Ms = millis(); // track last haptic activity (for the Steam-mode quiet timeout)
|
|
|
+ // 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, not just Steam. The re-init's own 0x81/0x87 don't match.
|
|
|
+ if (rid==0x82){ g_haptic82Ms = millis(); g_hapClearArmed = true; }
|
|
|
__set_PRIMASK(pm); return true;
|
|
|
}
|
|
|
|
|
|
@@ -131,6 +137,7 @@ static void hapticCancelPendingOn(){ // void queued 0x82-ON + 0x9F-shutdown en
|
|
|
void haptic82HostReport(const uint8_t* p, uint16_t n){
|
|
|
if(n<3) return;
|
|
|
g_haptic82Ms = millis();
|
|
|
+ g_hapClearArmed = true; // any haptic activity arms a clear when it next goes idle (kills a latch from this use)
|
|
|
// Track on/off only. Do NOT synthesize a stop burst when Steam's own stop arrives: that stop is already
|
|
|
// forwarded verbatim, so adding 0x82-zero frames on top just makes the controller see the stop several times
|
|
|
// over. Each 0x82 is a discrete pad click, so the extra frames are exactly the spurious end-of-movement
|
|
|
@@ -187,30 +194,52 @@ void rfConnFlushRelay(uint8_t ch, uint8_t s1){
|
|
|
// controller falls into across a reconnect. Brightness (0x87 reg 2d) is deliberately OMITTED so we don't
|
|
|
// stomp the LED. Enqueued onto the normal relay (drains in the poll cadence).
|
|
|
void hapticReinit(){
|
|
|
- // The haptic-engine init the real puck sends ONCE in the ~300ms window right after the controller connects
|
|
|
- // (verified against a puck<->controller sniff): an 0x81 reset bracketing 0x87 writes to the haptic registers
|
|
|
- // 07/08/31/52 and 18/2e/34/35, then a final 0x81 [01 00 00 00 00 00 00]. We must call this EXACTLY ONCE per
|
|
|
- // connect, like the real puck -- NOT on a timer/flood. Repeatedly hammering these (the old 10Hz/30s buzz-flood
|
|
|
- // + 8 reconnect shots + per-idle re-init) is what latched the controller into the self-sustaining buzz.
|
|
|
+ // The haptic-subsystem re-init Steam/the real puck sends to (re)take control of the haptic engine: a 0x81 reset
|
|
|
+ // action plus 0x87 writes to the haptic registers (07/08/31/52, 18/2e/34/35), then two trailing 0x81 frames.
|
|
|
+ // Replaying it clears a latched/stuck haptic on the controller. Driven from the flood + reconnect re-init in
|
|
|
+ // hapticTask -- NOT one-shot: the connect buzz engages at a random point in the first ~minute, so we keep
|
|
|
+ // replaying this (10Hz for 30s on connect) until the latch clears.
|
|
|
//
|
|
|
- // Two deliberate deviations from the raw capture: (1) the leading [0x30 ...] triplet (register 0x30 =
|
|
|
- // SETTING_GYRO_MODE; LED brightness is 0x2D, gyro mode 0x30) is OMITTED -- replaying Steam's 0x30=0 there
|
|
|
- // disabled the gyro, and the controller streams gyro on by default, so we leave 0x30 alone. (2) brightness
|
|
|
- // (0x87 reg 2d) is OMITTED so we don't stomp the user's LED. The real puck's 0x81 payloads are [01 00..] and
|
|
|
- // never the all-zero form, so we send only the [01 00..] one.
|
|
|
+ // Deliberate deviation: the real puck's sequence leads with [0x30 ...] (register 0x30 = SETTING_GYRO_MODE; LED
|
|
|
+ // brightness is 0x2D, gyro mode 0x30). Replaying 0x30=0 there DISABLED the gyro, and since the controller
|
|
|
+ // streams gyro on by default we OMIT the 0x30 triplet entirely. Brightness (0x87 reg 2d) is likewise omitted
|
|
|
+ // so the flood doesn't flicker the user's LED.
|
|
|
static const uint8_t H30[]={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};
|
|
|
- relayEnqueue(0x81, nullptr, 0); // reset action (FUN_0001f554) -- the puck brackets the config with this
|
|
|
+ relayEnqueue(0x81, nullptr, 0); // reset action (FUN_0001f554) -- Steam sends this first
|
|
|
relayEnqueue(0x87, H30, sizeof H30);
|
|
|
- relayEnqueue(0x87, H18, sizeof H18); // haptic config (enabled/amplifier/gain)
|
|
|
+ relayEnqueue(0x87, H18, sizeof H18); // haptic config (enabled/amplifier/gain): the part that clears a latch
|
|
|
relayEnqueue(0x87, H35, sizeof H35);
|
|
|
- relayEnqueue(0x81, nullptr, 0); // closing 0x81 bracket
|
|
|
- relayEnqueue(0x81, T81B, sizeof T81B); // final [01 00 00 00 00 00 00] (matches the puck's last connect frame)
|
|
|
+ relayEnqueue(0x81, T81A, sizeof T81A);
|
|
|
+ relayEnqueue(0x81, T81B, sizeof T81B);
|
|
|
+}
|
|
|
+// ONE-TIME connect config, sent ~200ms after every (re)connect -- this is the part the previous build never sent.
|
|
|
+// The real puck, right after the controller connects, sends a config burst BEYOND the haptic-register block:
|
|
|
+// 0xdc/0xe2 setup commands and 0x87 register writes 0x22=0x64 / 0x23=0x50 (verified in steam-controller-sniff-
|
|
|
+// data.json). OpenPuck only ever sent the haptic-register subset, so the controller came up in a state prone to
|
|
|
+// the connect-buzz latch. We replay that extra config here, then the haptic-register block, ONCE per connect --
|
|
|
+// the intended root-cause fix so the buzz never engages (the 10Hz flood becomes an opt-in fallback, g_buzzFlood).
|
|
|
+// Deliberately omitted vs the raw capture: the LED writes (0x87 reg 0x2d brightness, 0xc1 color -- OpenPuck owns
|
|
|
+// the LED), the 0xed "user/wireless_transport" string (alters transport, Steam-session-specific), and the 0x30
|
|
|
+// gyro-mode writes (0x30=0 disables the gyro -- see hapticReinit). The on-air framing of these matches the puck
|
|
|
+// exactly: rid>=0x87 -> E3 [2+len][01][rid][len][data] (relayEnqueue), e.g. 0xdc -> e3 04 01 dc 02 01 02.
|
|
|
+void hapticConnectInit(){
|
|
|
+ static const uint8_t DC[] = {0x01,0x02}; // 0xdc setup -> e3 04 01 dc 02 01 02
|
|
|
+ static const uint8_t E2[] = {0x01,0x20}; // 0xe2 setup -> e3 04 01 e2 02 01 20
|
|
|
+ static const uint8_t R22[] = {0x22,0x64,0x00}; // 0x87 reg 0x22 = 0x0064
|
|
|
+ static const uint8_t R23[] = {0x23,0x50,0x00}; // 0x87 reg 0x23 = 0x0050
|
|
|
+ relayEnqueue(0xdc, DC, sizeof DC);
|
|
|
+ relayEnqueue(0xe2, E2, sizeof E2);
|
|
|
+ relayEnqueue(0x87, R22, sizeof R22);
|
|
|
+ relayEnqueue(0x87, R23, sizeof R23);
|
|
|
+ hapticReinit(); // then the haptic-engine register block + 0x81 resets (same as the puck's connect haptic init)
|
|
|
}
|
|
|
void hapticInit(){
|
|
|
g_rqHead = g_rqTail = 0; g_haptic82On=false;
|
|
|
+ if(g_buzzFlood) hapticArmBuzzFlood(); // opt-in only: flood the buzz-clear for the first HAPTIC_FLOOD_MS
|
|
|
g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS; // boot: block stale Steam 0x82 until link stable
|
|
|
// NO fabricated stop burst. USB capture proves Steam only ever sends 0x82 [01 01 f7] pulses -- never a
|
|
|
// zero-gain [01 01 00] "stop". Injecting our invented stop frame (which the real puck never sends) at
|
|
|
@@ -219,16 +248,19 @@ void hapticInit(){
|
|
|
// pure pass-through of Steam's haptics, like the real puck.
|
|
|
g_hapticStop = 0;
|
|
|
}
|
|
|
-// Arm the post-(re)connect haptic block + schedule the ONE haptic init. Called from rf_link the moment a
|
|
|
+// Arm the post-(re)connect haptic block + schedule the clearing re-init. Called from rf_link the moment a
|
|
|
// controller reply arrives after a gap (the reliable reconnect signal), and as a backup on hapticTask's
|
|
|
-// link-up edge. Idempotent -- safe to call repeatedly (it just re-arms the single pending init).
|
|
|
+// link-up edge. Idempotent -- safe to call repeatedly.
|
|
|
void hapticOnReconnect(){
|
|
|
g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS; // no haptics relayed for the next 3s
|
|
|
g_haptic82On = false;
|
|
|
hapticCancelPendingOn(); // drop any haptic ON queued before the link came up
|
|
|
- // Schedule the haptic-engine init ONCE, shortly after the link is up -- exactly what the real puck does. (The
|
|
|
- // old build flooded this at 10Hz for 30s + 8 shots, which is what latched the controller into a stuck buzz.)
|
|
|
- g_reinitAt = millis() + HAPTIC_REINIT_DELAY_MS;
|
|
|
+ // DEFAULT: send the full connect config (hapticConnectInit) ONCE, ~200ms after the link is up -- like the real
|
|
|
+ // puck, so the haptic engine never latches in the first place.
|
|
|
+ g_reinitAt = millis() + 200u;
|
|
|
+ // OPT-IN fallback (g_buzzFlood): also hammer the buzz-clear re-init at 10Hz for 30s, in case the config alone
|
|
|
+ // doesn't fully prevent the buzz on a given board. Off by default.
|
|
|
+ if(g_buzzFlood) hapticArmBuzzFlood();
|
|
|
uint8_t mk=2; hapLogAdd(0xFD, 0xEE, &mk, 1); // capture marker: RECONNECT detected (block+init armed)
|
|
|
}
|
|
|
void hapticTask(){
|
|
|
@@ -239,10 +271,16 @@ void hapticTask(){
|
|
|
if(up && !wasHapticLinkUp){ uint8_t mk=1; hapLogAdd(0xFD,0xEE,&mk,1); hapticOnReconnect(); }
|
|
|
if(!up && wasHapticLinkUp){ uint8_t mk=0; hapLogAdd(0xFD,0xEE,&mk,1); }
|
|
|
wasHapticLinkUp=up;
|
|
|
- if(g_reinitAt && up && (int32_t)(millis()-g_reinitAt) >= 0){ // the ONE post-(re)connect haptic init, like the real puck
|
|
|
- hapticReinit();
|
|
|
+ if(g_reinitAt && up && (int32_t)(millis()-g_reinitAt) >= 0){ // the ONE connect config, ~200ms after (re)connect
|
|
|
+ hapticConnectInit();
|
|
|
g_reinitAt = 0;
|
|
|
}
|
|
|
+ // OPT-IN buzz-clear FLOOD (g_buzzFlood): while the armed window is open (-> g_buzzFloodUntil) and the link is up,
|
|
|
+ // replay the haptic re-init every HAPTIC_FLOOD_GAP_MS (10Hz) to hammer a stubborn latch. Auto-stops after 30s;
|
|
|
+ // re-armed on connect (only when enabled) and from the WebUSB panel. Off by default -- the connect config above
|
|
|
+ // is meant to prevent the buzz; this is the fallback for boards where it doesn't.
|
|
|
+ static unsigned long floodMs=0;
|
|
|
+ if (g_buzzFlood && (int32_t)(g_buzzFloodUntil - millis()) > 0 && up && millis()-floodMs >= HAPTIC_FLOOD_GAP_MS){ floodMs=millis(); hapticReinit(); }
|
|
|
// 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
|
|
|
@@ -257,6 +295,8 @@ void hapticTask(){
|
|
|
// are one-shot pulses, so firing a 0x82-zero ~HAPTIC_QUIET_MS after a swipe ends is the extra end-of-movement
|
|
|
// click the real puck doesn't make. Steam forwards its own stop for any sustained haptic.
|
|
|
if (!g_xbox && g_haptic82On && millis()-g_haptic82Ms > HAPTIC_QUIET_MS) g_haptic82On=false;
|
|
|
- // NO periodic / idle-triggered re-init. The real puck inits the haptic engine ONCE at connect and never again;
|
|
|
- // the old per-idle + 10Hz-flood re-init is exactly what drove the controller into the stuck buzz this removes.
|
|
|
+ // OPT-IN (g_buzzFlood): after haptic activity goes idle, fire one re-init to clear any latch it left behind.
|
|
|
+ // Off by default along with the flood; the connect config is the primary defense. Always disarm so it doesn't
|
|
|
+ // accumulate while disabled.
|
|
|
+ if (g_hapClearArmed && (millis()-g_haptic82Ms) > HAPTIC_CLEAR_IDLE_MS){ g_hapClearArmed=false; if(g_buzzFlood) hapticReinit(); }
|
|
|
}
|