haptics.h 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // haptics.h -- host -> controller relay queue (haptics + settings) + the watchdogs that stop a stuck buzz.
  2. //
  3. // Steam and translated host rumble send OUTPUT/feature reports the real dongle forwards to the controller as
  4. // a SET sub-TLV inside the E3 poll. We do the same: handleSet()/the console enqueue with relayEnqueue(), and
  5. // rfConnFlushRelay() emits ONE entry per poll cycle (rf_link.cpp), never at raw loop rate.
  6. //
  7. // One ring per bond slot: the consumer (rfConnFlushRelay) only drains the current slot's queue, so commands
  8. // addressed to other slots are never consumed out of turn. ISR producers write under PRIMASK; the consumer
  9. // runs in loop context and never races the same slot's head pointer.
  10. //
  11. // The controller's haptic LATCHES until told to stop. If the host's stop is lost over RF (or the link drops
  12. // mid-buzz) the actuator whines forever -- so we send 0x82-zero stop bursts on reconnect, but ONLY when a
  13. // haptic was actually active when the link dropped: every extra 0x82 frame is an audible click on the
  14. // controller (captured), so an unconditional burst on each link-up edge is itself a buzz source on a flapping
  15. // link. The g_hapLog ring captures recent OUTPUT reports for the 'H' dump.
  16. #pragma once
  17. #include <stdint.h>
  18. #include "config.h" // OPK_LOG
  19. #include "bonds.h" // NSLOT
  20. // after this much host silence, consider the current 0x82 haptic stream inactive
  21. #define HAPTIC_QUIET_MS 300u
  22. // Post-(re)connect haptic block default. While armed, ALL Steam haptic relays to that slot are dropped: a
  23. // just-powered-on controller's haptic engine isn't ready, and feeding it haptics in that window leaves it in a
  24. // degraded/latched state (slow/stuck/missing haptics until a re-init). Runtime-adjustable (g_hapticBlockMs) and
  25. // toggleable (g_hapticBlockOn) from the WebUSB panel; this is the boot default.
  26. #define HAPTIC_BLOCK_MS_DEFAULT 10000u
  27. // 0x82-zero relays per stop event (sent at poll cadence -- not loop rate)
  28. #define HAPTIC_STOP_BURST 4u
  29. // max relayed payload bytes per entry: RF frame = [E3][len][05][rid][payload] and MAXLEN=64 -> 60
  30. #define RELAY_MAXP 60u
  31. // Proactive post-(re)connect haptic re-init: this many shots, this far apart, starting ~200ms after the link
  32. // comes up -- covers ~0.2s..3s so the brief controller-side connect buzz gets reset before it can sustain.
  33. #define HAPTIC_REINIT_SHOTS 8u
  34. #define HAPTIC_REINIT_GAP_MS 350u
  35. // After haptic activity, if it's been idle this long, fire one clear-re-init -- kills a latch that engaged
  36. // during use (a buzz that starts seconds after connect and won't self-clear). Long enough not to fire between
  37. // rapid in-game haptics; short enough to clear a stuck buzz soon after the user pauses.
  38. #define HAPTIC_CLEAR_IDLE_MS 1200u
  39. // Controller power-off: hapticSendShutdown() relays Steam's confirmed "turn off controller" command (feature-0x01
  40. // cmd 0x9F, payload "off!" -- captured from the real puck). Sent as a small burst because the RF relay is NO-ACK.
  41. #define HAPTIC_SHUTDOWN_SHOTS 3u
  42. // ---- relay queue (written by puck_hid.cpp, mode_*.cpp, serial_console.cpp; drained by rf_link.cpp) ----
  43. // Enqueue one host->controller report. `slot` = bond slot (0..NSLOT-1) or 0xFF to broadcast to every
  44. // connected controller (used by hapticSendShutdown / hapticReinit / test haptics). ISR-safe (PRIMASK).
  45. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
  46. uint8_t slot = 0xFF);
  47. // Post-connect haptic block (persisted, panel-controlled): when g_hapticBlockOn, Steam haptics are dropped for
  48. // g_hapticBlockMs after a (re)connect so the controller's haptic engine settles before the first real haptic.
  49. extern uint8_t
  50. g_hapticBlockOn; // 1 = block enabled, 0 = relay haptics immediately on connect (default)
  51. extern uint16_t
  52. g_hapticBlockMs; // block duration in ms (default HAPTIC_BLOCK_MS_DEFAULT)
  53. // anything still queued (xinput uses it to pace rumble re-queues)
  54. bool relayPending();
  55. extern uint8_t g_relayOp; // relay frame opcode (E3 poll)
  56. extern uint8_t g_relaySub; // relay sub-TLV type byte = SET
  57. extern volatile uint8_t g_testHaptic; // 't<n>' injects n test haptics
  58. // pending haptic-STOP frames to relay (kill a latched whine)
  59. extern volatile uint8_t g_hapticStop;
  60. // Per-slot block: arm after a (re)connect, drop haptics aimed at the slot for g_hapticBlockMs (when g_hapticBlockOn).
  61. extern unsigned long g_hapticBlockUntil[NSLOT];
  62. // relay the controller power-off (0x9F "off!"), burst x3 (Steam 0x9F / host-suspend / test button)
  63. void hapticSendShutdown();
  64. // ---- diagnostic capture (compiled in only when OPK_LOG): a ring of recent host->controller commands +
  65. // link/TX markers, dumped over WebUSB. No-ops in a production build so call sites vanish. ----
  66. #if OPK_LOG
  67. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n);
  68. void hapticDumpLog(); // 'H' console dump of the recent OUTPUT-report history
  69. // WebUSB capture drain: resetDrain(true) rewinds to the OLDEST entry (dump the whole ring from boot);
  70. // resetDrain(false) starts at "now" (live only). pull yields each entry once, oldest->newest, skipping empties.
  71. void hapLogResetDrain(bool fromBoot);
  72. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  73. uint8_t bytes16[16]);
  74. #else
  75. static inline void hapLogAdd(uint8_t, uint8_t, const uint8_t *, uint16_t)
  76. {
  77. }
  78. static inline void hapticDumpLog()
  79. {
  80. }
  81. static inline void hapLogResetDrain(bool)
  82. {
  83. }
  84. static inline bool hapLogPull(uint32_t *, uint8_t *, uint8_t *, uint8_t *,
  85. uint8_t *)
  86. {
  87. return false;
  88. }
  89. #endif
  90. bool hapticLinkUp(int slot = -1);
  91. bool haptic82Blocked(int slot = -1);
  92. bool hapticRelaySlotOk(int slot);
  93. void haptic82HostReport(const uint8_t *p, uint16_t n);
  94. // queue a Steam/Triton 0x80 rumble frame. `slot` = bond slot of the originating controller (0..NSLOT-1);
  95. // defaults to 0 for the legacy single-controller callers. Per-slot so each connected controller can have its
  96. // own active rumble stream when the host presents multiple gamepads (e.g. 4 XInput devices).
  97. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot = 0);
  98. // queue + flush the pending host/test/stop relay inside the poll cadence (called from rf_link).
  99. // rfConnFlushRelay's s1 must carry a PID distinct from the GET poll that follows it (rf_link cycles the shared
  100. // PID counter), so the controller doesn't dedup the GET as a retransmit of the relay.
  101. void rfConnQueueHapticRelay();
  102. void rfConnFlushRelay(uint8_t ch, uint8_t s1);
  103. // boot reset: clear relay/active flags, arm the reconnect block
  104. void hapticInit();
  105. // per-loop upkeep: link-edge markers + steam 0x82 quiet timeout + fires the scheduled re-init
  106. void hapticTask();
  107. // replay Steam's haptic-subsystem re-init to the controller -> clears a latched/stuck buzz.
  108. // `slot` defaults to 0xFF (broadcast to all connected) -- the re-init is a settings-only reset and is
  109. // harmless on healthy controllers, so it's worth re-initializing every slot the firmware knows about.
  110. void hapticReinit(uint8_t slot = 0xFF);
  111. // Called from rf_link the instant a controller (re)connect is detected (an F-reply after a gap): blocks haptic
  112. // relays for g_hapticBlockMs and schedules a re-init just after, to keep the freshly-booted
  113. // controller out of the degraded/latched haptic state. Reliable -- independent of hapticTask's link heuristic.
  114. // Per-slot: only the slot that just reconnected is blocked, the others keep relaying.
  115. void hapticOnReconnect(int slot);
  116. // Write the controller's global trackpad-haptics enable setting (per active emulated type). slot 0xFF = broadcast.
  117. void hapticSetPadEnabled(uint8_t slot, bool on);