haptics.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. #include "haptics.h"
  2. #include "bonds.h"
  3. #include "config.h"
  4. #include "rf_link.h"
  5. #include <Arduino.h>
  6. #include <string.h>
  7. uint8_t g_relayOp = 0xE3; // E3 poll
  8. // relay sub-TLV type byte = SET (confirmed on hardware: E3 + [len][05][rid][payload] makes the controller act
  9. // on it -- haptics buzz + lizard-off lands). GET is 01.
  10. uint8_t g_relaySub = 0x05;
  11. volatile uint8_t g_testHaptic = 0;
  12. volatile uint8_t g_hapticStop = 0;
  13. unsigned long g_hapticBlockUntil = 0; // drop Steam haptics briefly during reconnect settle
  14. static unsigned long g_haptic82Ms = 0; // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
  15. static bool g_haptic82On = false; // a non-zero 0x82 haptic is currently active (awaiting host stop)
  16. static unsigned long g_reinitAt = 0; // when to fire the next post-reconnect haptic re-init (0 = none scheduled)
  17. static uint8_t g_reinitLeft = 0; // how many re-init shots remain in this connect window
  18. // ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
  19. // Producers serialize through a brief PRIMASK critical section (copy is <=62 bytes); the consumer only ever
  20. // touches the tail entry, which no producer writes while the ring isn't full -- so a flush can't be torn.
  21. struct RelayMsg { uint8_t rid, len; uint8_t data[RELAY_MAXP]; };
  22. #define RELAY_QLEN 32 // deep enough to hold a full Steam settings/LED transaction burst without loss
  23. static RelayMsg g_rq[RELAY_QLEN];
  24. static volatile uint8_t g_rqHead = 0, g_rqTail = 0; // head=next write, tail=next read; empty when equal
  25. static inline uint8_t rqNext(uint8_t i){ return (uint8_t)((i+1)%RELAY_QLEN); }
  26. bool relayPending(){ return g_rqHead != g_rqTail; }
  27. bool relayEnqueue(uint8_t rid, const uint8_t* payload, uint8_t plen){
  28. if (plen > RELAY_MAXP) plen = RELAY_MAXP; // 60B is the RF frame ceiling; longer can't be relayed
  29. uint32_t pm = __get_PRIMASK(); __disable_irq();
  30. uint8_t h = g_rqHead, nx = rqNext(h);
  31. // Full -> evict the OLDEST entry, never the newest. Steam sends commands as bursts where the meaningful
  32. // frame comes LAST (a settings transaction ends with its commit; a haptic stream ends with its stop). The
  33. // old drop-newest policy threw away exactly those, so LED commits and haptic stops were the casualties --
  34. // brightness never applied and a glide's final stop never reached the controller (stuck buzz). Dropping the
  35. // oldest instead guarantees the most recent command always lands.
  36. if (nx == g_rqTail) g_rqTail = rqNext(g_rqTail);
  37. g_rq[h].rid = rid; g_rq[h].len = plen;
  38. if (plen) memcpy(g_rq[h].data, payload, plen);
  39. g_rqHead = nx;
  40. __set_PRIMASK(pm); return true;
  41. }
  42. #if OPK_LOG
  43. // diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
  44. // Reproduce the whine, press Steam to stop it, then 'H' to see the ON stream + the exact OFF frame Steam sends.
  45. struct HapLog { uint32_t ms; uint8_t slot, rid, n, b[16]; }; // 16 payload bytes: capture full 0x87 settings frames
  46. // Big always-on ring: log EVERYTHING from boot (Steam writes, our TX-to-controller, link edges) so a rare
  47. // reconnect-buzz can be caught after the fact -- the trigger happens moments after the puck boots, while this
  48. // RAM is fresh, and we dump it once the panel reconnects. 4096 * 20B ~= 80KB (we have plenty of free RAM).
  49. #define HAPLOG_N 4096
  50. static HapLog g_hapLog[HAPLOG_N];
  51. static uint16_t g_hapHead = 0;
  52. static uint16_t g_hapTail = 0; // live/dump drain cursor (loop-context reader; chases g_hapHead)
  53. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t* b, uint16_t n){
  54. // Written from the USB SET ISR (handleSet) AND loop context (relay flush / link edges) -> guard g_hapHead.
  55. // Special slot markers for the diagnostic capture: 0xFE = a frame WE transmitted to the controller (TX
  56. // relay); 0xFD = a link state edge (b[0]=1 up, 0 down). Real Steam writes use the interface index (0..3).
  57. uint32_t pm = __get_PRIMASK(); __disable_irq();
  58. HapLog &e = g_hapLog[g_hapHead]; e.ms = millis(); e.slot = slot; e.rid = rid; e.n = (uint8_t)(n>255?255:n);
  59. for(int i=0;i<16;i++) e.b[i] = (i<(int)n) ? b[i] : 0;
  60. g_hapHead = (uint8_t)((g_hapHead+1) % (sizeof g_hapLog / sizeof g_hapLog[0]));
  61. __set_PRIMASK(pm);
  62. }
  63. void hapticDumpLog(){ // dump the captured history (oldest->newest) over CDC for the haptic-whine hunt
  64. const uint16_t N=HAPLOG_N; uint32_t now=millis();
  65. Serial.printf("# --- capture history (now=%lu, connSlot=%d) ---\n",(unsigned long)now,g_connSlot);
  66. for(uint16_t i=0;i<N;i++){ HapLog &e=g_hapLog[(uint16_t)((g_hapHead+i)%N)]; if(!e.ms && !e.rid) continue;
  67. Serial.printf("# -%lums if%u rid=%02X n=%u:",(unsigned long)(now-e.ms),e.slot,e.rid,e.n);
  68. for(uint8_t j=0;j<16 && j<e.n;j++) Serial.printf(" %02X",e.b[j]); Serial.println(); }
  69. Serial.println("# --- end ---");
  70. }
  71. // ---- drain cursor: stream entries to the WebUSB panel. resetDrain(false)=from "now" (live only);
  72. // resetDrain(true)=from the OLDEST entry (dump the whole ring from boot). pull skips empty slots. ----
  73. void hapLogResetDrain(bool fromBoot){
  74. g_hapTail = fromBoot ? (uint16_t)((g_hapHead+1)%HAPLOG_N) // oldest slot (the next-to-overwrite holds it)
  75. : g_hapHead; // "now"
  76. }
  77. bool hapLogPull(uint32_t* logMs, uint8_t* slot, uint8_t* rid, uint8_t* n, uint8_t bytes16[16]){
  78. while(g_hapTail != g_hapHead){
  79. HapLog &e=g_hapLog[g_hapTail];
  80. g_hapTail=(uint16_t)((g_hapTail+1)%HAPLOG_N);
  81. if(!e.ms && !e.rid) continue; // skip empty slot (ring not yet full)
  82. // Return the ABSOLUTE log time (millis since boot), not "age now". The panel drains in batches 100ms
  83. // apart, so an age computed here would jump between batches; the panel computes age vs the newest entry.
  84. *logMs=e.ms; *slot=e.slot; *rid=e.rid; *n=(e.n>16)?16:e.n; memcpy(bytes16,e.b,16);
  85. return true;
  86. }
  87. return false;
  88. }
  89. #endif // OPK_LOG
  90. bool hapticLinkUp(){
  91. return g_connSlot>=0 && (millis()-g_connReplyMs) < 300;
  92. }
  93. bool haptic82Blocked(){
  94. return !hapticLinkUp() || (g_hapticBlockUntil && (int32_t)(millis()-g_hapticBlockUntil) < 0);
  95. }
  96. bool hapticRelaySlotOk(int slot){
  97. return g_connSlot>=0 && slot==g_connSlot;
  98. }
  99. static bool haptic82PayloadOn(const uint8_t* p, uint16_t n){
  100. if(n<3) return false;
  101. for(uint16_t i=2;i<n;i++) if(p[i]) return true; // observed form is [01 01 gain], but treat any trailing non-zero as active
  102. return false;
  103. }
  104. static void hapticCancelPendingOn(){ // void queued 0x82-ON entries (stale Steam haptics across a reconnect)
  105. uint32_t pm = __get_PRIMASK(); __disable_irq();
  106. for(uint8_t i=g_rqTail; i!=g_rqHead; i=rqNext(i)){
  107. RelayMsg &m = g_rq[i];
  108. if(m.rid==0x82){ bool on=false; for(uint8_t j=2;j<m.len;j++) if(m.data[j]){on=true;break;} if(on) m.rid=0; }
  109. }
  110. __set_PRIMASK(pm);
  111. }
  112. void haptic82HostReport(const uint8_t* p, uint16_t n){
  113. if(n<3) return;
  114. g_haptic82Ms = millis();
  115. // Track on/off only. Do NOT synthesize a stop burst when Steam's own stop arrives: that stop is already
  116. // forwarded verbatim, so adding 0x82-zero frames on top just makes the controller see the stop several times
  117. // over. Each 0x82 is a discrete pad click, so the extra frames are exactly the spurious end-of-movement
  118. // "click"/buzz that the real puck never produces. (Connect-time clearing still runs in hapticTask().)
  119. g_haptic82On = haptic82PayloadOn(p,n);
  120. }
  121. // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate).
  122. void rfConnQueueHapticRelay(){
  123. if(relayPending()) return; // host relays first; injectables wait for an idle cycle
  124. static const uint8_t HAP_ON[3] = {0x01,0x01,0xF7};
  125. static const uint8_t HAP_OFF[3] = {0x01,0x01,0x00};
  126. if(g_testHaptic){
  127. if(relayEnqueue(0x82, HAP_ON, 3)) g_testHaptic--;
  128. } else if(g_hapticStop && !g_xbox){
  129. if(relayEnqueue(0x82, HAP_OFF, 3)) g_hapticStop--;
  130. }
  131. }
  132. void rfConnFlushRelay(uint8_t ch, uint8_t s1){
  133. while(g_rqTail != g_rqHead){
  134. RelayMsg &m = g_rq[g_rqTail];
  135. if(m.rid){ // rid 0 = entry voided by hapticCancelPendingOn -> skip, take the next
  136. uint8_t rl = m.len; if(rl > RELAY_MAXP) rl = RELAY_MAXP;
  137. uint8_t p[4+RELAY_MAXP]; p[0]=g_relayOp; p[1]=(uint8_t)(1+rl); p[2]=g_relaySub; p[3]=m.rid;
  138. memcpy(p+4, m.data, rl);
  139. hapLogAdd(0xFE, m.rid, m.data, rl); // log what we actually TX to the controller (slot 0xFE) for the buzz hunt
  140. g_rqTail = rqNext(g_rqTail); // copied out -> release the slot before the TX
  141. // s1 carries a PID distinct from the GET poll that follows (caller cycles it), so the controller's ESB
  142. // dedup never mistakes the GET for a retransmit of this relay. 80us RX window: the relay is NO-ACK, the
  143. // controller sends nothing back, so don't burn the full ~1.2ms reply window (that was halving the poll
  144. // rate during haptics).
  145. rfConnTx(ch,s1,p,(uint8_t)(4+rl),80);
  146. return; // ONE relay per poll cycle (matches the real puck's pacing)
  147. }
  148. g_rqTail = rqNext(g_rqTail);
  149. }
  150. }
  151. // Haptic-subsystem RE-INIT: the exact sequence Steam sends (captured on hardware) when it (re)takes control,
  152. // and which clears a stuck haptic script on the controller -- a 0x81 reset action plus 0x87 writes to the
  153. // haptic registers (30/07/08/31/52, 18/2e/34/35). We replay it to recover from the latched-buzz the
  154. // controller falls into across a reconnect. Brightness (0x87 reg 2d) is deliberately OMITTED so we don't
  155. // stomp the LED. Enqueued onto the normal relay (drains in the poll cadence).
  156. void hapticReinit(){
  157. // The FULL sequence Steam sends when it (re)takes control / on a Steam-button press -- captured on hardware
  158. // and proven to clear a stuck haptic. Replayed verbatim and in order (no trimming this time: the brightness
  159. // write 0x87[2d] and the two 7-byte 0x81 frames are part of what Steam sends, and dropping them may be why
  160. // the trimmed version didn't always clear the connect buzz).
  161. static const uint8_t H30[]={0x30,0x00,0x00,0x07,0x07,0x00,0x08,0x07,0x00,0x31,0x02,0x00,0x52,0x03,0x00};
  162. static const uint8_t H18[]={0x18,0x00,0x00,0x2e,0x00,0x00,0x34,0xff,0xff,0x35,0xff,0xff,0x34,0xff,0xff};
  163. static const uint8_t H35[]={0x35,0xff,0xff,0x2e,0x00,0x00};
  164. static const uint8_t H2D[]={0x2d,0x64,0x00}; // brightness (Steam sets it here too)
  165. static const uint8_t T81A[]={0x00,0x00,0x00,0x00,0x00,0x00,0x00};
  166. static const uint8_t T81B[]={0x01,0x00,0x00,0x00,0x00,0x00,0x00};
  167. relayEnqueue(0x81, nullptr, 0); // reset action (FUN_0001f554) -- Steam sends this first
  168. relayEnqueue(0x87, H30, sizeof H30);
  169. relayEnqueue(0x87, H18, sizeof H18);
  170. relayEnqueue(0x87, H35, sizeof H35);
  171. relayEnqueue(0x87, H2D, sizeof H2D);
  172. relayEnqueue(0x81, T81A, sizeof T81A);
  173. relayEnqueue(0x81, T81B, sizeof T81B);
  174. }
  175. void hapticInit(){
  176. g_rqHead = g_rqTail = 0; g_haptic82On=false;
  177. g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS; // boot: block stale Steam 0x82 until link stable
  178. // NO fabricated stop burst. USB capture proves Steam only ever sends 0x82 [01 01 f7] pulses -- never a
  179. // zero-gain [01 01 00] "stop". Injecting our invented stop frame (which the real puck never sends) at
  180. // boot/connect is unfaithful and, given the controller's "haptic script already active -> ignore" behavior,
  181. // a likelier cause of the connect-time buzz than a cure (the buzz happened WITH this burst in place). Be a
  182. // pure pass-through of Steam's haptics, like the real puck.
  183. g_hapticStop = 0;
  184. }
  185. // Arm the post-(re)connect haptic block + schedule the clearing re-init. Called from rf_link the moment a
  186. // controller reply arrives after a gap (the reliable reconnect signal), and as a backup on hapticTask's
  187. // link-up edge. Idempotent -- safe to call repeatedly.
  188. void hapticOnReconnect(){
  189. g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS; // no haptics relayed for the next 3s
  190. g_haptic82On = false;
  191. hapticCancelPendingOn(); // drop any haptic ON queued before the link came up
  192. // Be PROACTIVE: start re-initing the haptic engine right after the link is up and repeat across the whole
  193. // settle window, instead of waiting for the block to end. The brief connect buzz engages early (during the
  194. // block, controller-internal), so a single late shot misses it -- frequent early resets cut it short or
  195. // preempt it. The re-init is settings only (no haptic play), and Steam haptics are blocked the whole time,
  196. // so repeating it can't itself buzz.
  197. g_reinitAt = millis() + 200u; // first reset ~200ms after (re)connect
  198. g_reinitLeft = HAPTIC_REINIT_SHOTS; // then every HAPTIC_REINIT_GAP_MS across the window
  199. uint8_t mk=2; hapLogAdd(0xFD, 0xEE, &mk, 1); // capture marker: RECONNECT detected (block+reinit armed)
  200. }
  201. void hapticTask(){
  202. static bool wasHapticLinkUp=false;
  203. bool up=hapticLinkUp();
  204. // Link-edge markers are diagnostic only now -- the block/re-init is armed reliably from rf_link
  205. // (hapticOnReconnect) on the first reply after a gap, which fires even when this 300ms edge doesn't.
  206. if(up && !wasHapticLinkUp){ uint8_t mk=1; hapLogAdd(0xFD,0xEE,&mk,1); hapticOnReconnect(); }
  207. if(!up && wasHapticLinkUp){ uint8_t mk=0; hapLogAdd(0xFD,0xEE,&mk,1); }
  208. wasHapticLinkUp=up;
  209. if(g_reinitAt && up && (int32_t)(millis()-g_reinitAt) >= 0){ // proactive haptic re-init across the connect window
  210. hapticReinit();
  211. g_reinitAt = (g_reinitLeft && --g_reinitLeft) ? (millis()+HAPTIC_REINIT_GAP_MS) : 0;
  212. }
  213. // Steam-mode: host went quiet -> mark the 0x82 stream inactive. Do NOT synthesize a stop: trackpad haptics
  214. // are one-shot pulses, so firing a 0x82-zero ~HAPTIC_QUIET_MS after a swipe ends is the extra end-of-movement
  215. // click the real puck doesn't make. Steam forwards its own stop for any sustained haptic.
  216. if (!g_xbox && g_haptic82On && millis()-g_haptic82Ms > HAPTIC_QUIET_MS) g_haptic82On=false;
  217. }