haptics.h 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. // Post-(re)connect haptic block default. While armed, ALL Steam haptic relays to that slot are dropped: a
  21. // just-powered-on controller's haptic engine isn't ready, and feeding it haptics in that window leaves it in a
  22. // degraded/latched state (slow/stuck/missing haptics until a re-init). Runtime-adjustable (g_hapticBlockMs) and
  23. // toggleable (g_hapticBlockOn) from the WebUSB panel; this is the boot default.
  24. #define HAPTIC_BLOCK_MS_DEFAULT 10000u
  25. // max relayed payload bytes per entry: RF frame = [E3][len][05][rid][payload] and MAXLEN=64 -> 60
  26. #define RELAY_MAXP 60u
  27. // Controller power-off: hapticSendShutdown() relays Steam's confirmed "turn off controller" command (feature-0x01
  28. // cmd 0x9F, payload "off!" -- captured from the real puck). Sent as a small burst because the RF relay is NO-ACK.
  29. #define HAPTIC_SHUTDOWN_SHOTS 3u
  30. // Rumble strength: percent of the decoded host amplitude applied in every translated (non-puck) mode. 200
  31. // (double) is the default the panel's removed rumble-strength slider shipped with, so an untouched puck feels
  32. // exactly as before. Runtime-adjustable and persisted -- console "RS<pct>".
  33. #define RUMBLE_SCALE_PCT 200u
  34. #define RUMBLE_SCALE_MIN 10u
  35. #define RUMBLE_SCALE_MAX 500u
  36. // Rumble style: how the decoded low/high motor amplitudes are shaped before the 0x80 frame is built. Applied
  37. // BEFORE the strength scale, on the raw host amplitudes. Persisted -- console "RY<n>".
  38. #define RUMBLE_STYLE_NORMAL \
  39. 0 // as captured: low -> left speed, high -> right speed
  40. #define RUMBLE_STYLE_MONO 1 // both motors at max(low,high) -- heavier, fuller
  41. #define RUMBLE_STYLE_HEAVY 2 // low-frequency motor only (mute the buzzy one)
  42. #define RUMBLE_STYLE_LIGHT \
  43. 3 // high-frequency motor only (crisp, no deep rumble)
  44. #define RUMBLE_STYLE_SWAP 4 // swap the two motors
  45. #define RUMBLE_STYLE_PUNCHY \
  46. 5 // squared curve: weak effects softer, strong ones untouched
  47. #define RUMBLE_STYLE_SOFT \
  48. 6 // sqrt curve: lifts weak effects so subtle rumble is felt
  49. #define RUMBLE_STYLE_MAX 6
  50. extern uint16_t g_rumbleScale; // percent, RUMBLE_SCALE_MIN..RUMBLE_SCALE_MAX
  51. extern uint8_t g_rumbleStyle; // RUMBLE_STYLE_*
  52. // Test buzz for the panel/console: a fixed mid-scale amplitude pushed through the SAME shaping path host
  53. // rumble takes, so what you feel is what a game at that amplitude would feel like. Auto-stops in hapticTask()
  54. // -- the controller's haptic LATCHES, so the stop is not optional.
  55. #define RUMBLE_TEST_AMP 0x8000u
  56. #define RUMBLE_TEST_MS 500u
  57. void hapticTestRumble();
  58. // Journal-builder progress tick. It is deliberately short and is emitted only
  59. // between measured channels, never inside a quality window. Hardware testing
  60. // showed that this 0x82 haptic does not reset the controller inactivity timer.
  61. #define RF_JOURNAL_BUILDER_TICK_MS 150u
  62. void hapticRfJournalBuilderTick(uint8_t participantMask);
  63. // ---- relay queue (written by puck_hid.cpp, mode_*.cpp, serial_console.cpp; drained by rf_link.cpp) ----
  64. // Enqueue one host->controller report. `slot` = bond slot (0..NSLOT-1) or 0xFF to broadcast to every
  65. // connected controller (used by hapticSendShutdown / hapticReinit / test haptics). ISR-safe (PRIMASK).
  66. // `expectReply`: true iff the CALLER is relaying this specifically because it wants the controller's real
  67. // answer back (a query like GET_ATTRIBUTES/GET_STRING_ATTRIBUTE/READ_SETTING -- see puck_hid.cpp
  68. // handleSet's `relayQuery`), as opposed to a fire-and-forget action (haptics, settings writes, power-off)
  69. // that nothing will ever wait on a reply for. rfConnFlushRelay uses this.
  70. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
  71. bool isHaptic, uint8_t slot = 0xFF, bool expectReply = false);
  72. // Drop everything queued for one bond slot. Called when a slot becomes BONDED (Steam's 0xA2 pairing write,
  73. // the panel's bond import): whatever was queued while the slot was empty was aimed at a controller that no
  74. // longer -- or never did -- live there, and an unbonded slot's ring is never flushed, so it would otherwise
  75. // be handed straight to the freshly paired controller (a stale "off!" powered it back off). ISR-safe.
  76. void relayClearSlot(uint8_t slot);
  77. // id9 steering (EMULATED modes only): land the controller's SET_SETTINGS index 9 (digital-mappings /
  78. // lizard-active) at 0 once per LIZKEEP_MS per connected slot to hold its autonomous mapping/haptic engine
  79. // OFF, or at 1 once per connect episode to turn it on -- per the active type's g_padHaptics config. id9
  80. // gates the whole autonomous pad layer, including the trackpad ticks, and holding it off also stops the
  81. // engine latching the deep-inside buzz seen after repeated reconnects (capture-for-haptics.txt: that buzz is
  82. // controller-internal; OpenPuck relays no haptics in that state). PUCK modes (STEAM/LIZARD) are not steered:
  83. // Steam writes id9 itself there, and driving it from the puck side fought those writes.
  84. #define LIZKEEP_MS 2000u
  85. extern uint8_t
  86. g_lizKeep; // 1 = hold on (default, persisted); console 'u' toggles for A/B
  87. // Autonomous controller power-off on host sleep (see hapticTask). 1 = power the controllers off once the
  88. // USB suspend has persisted SUSPEND_OFF_MS (default -- what the real puck does, so the controllers don't sit
  89. // awake draining while the host sleeps). 0 = leave them on.
  90. //
  91. // TRADE-OFF, deliberate: with this ON the controller is off while the host sleeps, so the short-Steam-press
  92. // remote-wakeup gesture in rf_link (guarded on USBDevice.suspended()) can no longer be sent from it. Waking
  93. // the host from the controller then goes through the OTHER path: pressing Steam on a powered-off controller
  94. // turns it back on, the link comes up, and the reconnect-wake in rfConnStep issues the remote wakeup. Same
  95. // physical gesture; the power-off fires once per suspend so the returning controller is not shut off again.
  96. // Turn this OFF (console "SO") if a controller must stay awake through host sleep. Persisted.
  97. extern uint8_t g_suspendOff;
  98. // Power a controller off on charge-state 2 (charging) or 4 (charge complete).
  99. extern uint8_t g_chargeOff;
  100. void hapticOnChargeState(int slot, uint8_t state);
  101. // Master enable for the puck->controller haptic relay (Steam 0x80-0x86 rumble/pad-feedback). Console "HR"
  102. // toggles it to isolate the drag-smoothness cost of relaying Steam's trackpad haptics. See haptics.cpp.
  103. extern bool g_hapticRelay;
  104. // Post-connect haptic block (persisted, panel-controlled): when g_hapticBlockOn, Steam haptics are dropped for
  105. // g_hapticBlockMs after a (re)connect so the controller's haptic engine settles before the first real haptic.
  106. extern uint8_t
  107. g_hapticBlockOn; // 1 = block enabled, 0 = relay haptics immediately on connect (default)
  108. extern uint16_t
  109. g_hapticBlockMs; // block duration in ms (default HAPTIC_BLOCK_MS_DEFAULT)
  110. // anything still queued (xinput uses it to pace rumble re-queues)
  111. bool relayPending();
  112. extern uint8_t g_relayOp; // relay frame opcode (E3 poll)
  113. extern uint8_t g_relaySub; // relay sub-TLV type byte = SET
  114. extern volatile uint8_t g_testHaptic; // 't<n>' injects n test haptics
  115. // pending haptic-STOP frames to relay (kill a latched whine)
  116. extern volatile uint8_t g_hapticStop;
  117. // Per-slot block: arm after a (re)connect, drop haptics aimed at the slot for g_hapticBlockMs (when g_hapticBlockOn).
  118. extern unsigned long g_hapticBlockUntil[NSLOT];
  119. // relay the controller power-off (0x9F "off!"), burst x3. Steam's per-interface 0x9F passes that slot so
  120. // only that controller powers off; host-suspend / the panel test button keep the broadcast default (all off).
  121. void hapticSendShutdown(uint8_t slot = 0xFF);
  122. // ---- diagnostic capture (compiled in only when OPK_LOG): a ring of recent host->controller commands +
  123. // link/TX markers, dumped over WebUSB. No-ops in a production build so call sites vanish. ----
  124. #if OPK_LOG
  125. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n);
  126. void hapticDumpLog(); // 'H' console dump of the recent OUTPUT-report history
  127. // WebUSB capture drain: resetDrain(true) rewinds to the OLDEST entry (dump the whole ring from boot);
  128. // resetDrain(false) starts at "now" (live only). pull yields each entry once, oldest->newest, skipping empties.
  129. void hapLogResetDrain(bool fromBoot);
  130. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  131. uint8_t bytes16[16]);
  132. #else
  133. static inline void hapLogAdd(uint8_t, uint8_t, const uint8_t *, uint16_t)
  134. {
  135. }
  136. static inline void hapticDumpLog()
  137. {
  138. }
  139. static inline void hapLogResetDrain(bool)
  140. {
  141. }
  142. static inline bool hapLogPull(uint32_t *, uint8_t *, uint8_t *, uint8_t *,
  143. uint8_t *)
  144. {
  145. return false;
  146. }
  147. #endif
  148. bool hapticLinkUp(int slot = -1);
  149. bool haptic82Blocked(int slot = -1);
  150. bool hapticRelaySlotOk(int slot);
  151. // queue a Steam/Triton 0x80 rumble frame. `slot` = bond slot of the originating controller (0..NSLOT-1);
  152. // defaults to 0 for the legacy single-controller callers. Per-slot so each connected controller can have its
  153. // own active rumble stream when the host presents multiple gamepads (e.g. 4 XInput devices).
  154. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot = 0);
  155. // queue + flush the pending host/test/stop relay inside the poll cadence (called from rf_link).
  156. // rfConnFlushRelay's s1 must carry a PID distinct from the GET poll that follows it. g_relayPid
  157. // is initialised 2 ahead of g_pollPid and both increment once per cycle, so the 2-bit PIDs stay
  158. // 2 apart (mod 4) forever and never collide — keeping the controller from deduplicating the GET.
  159. // Stability test: when g_stabTest (WebUSB cmd 0x0F), buzz all controllers every 10s to keep them awake for an
  160. // unattended uptime-until-hang measurement. hapticStabTask() is called from loop().
  161. extern bool g_stabTest;
  162. void hapticStabTask();
  163. void rfConnQueueHapticRelay();
  164. // returns true if a relay frame was actually transmitted this call (queue had an entry), so the poll loop can
  165. // count relay TXs separately from poll cycles.
  166. bool rfConnFlushRelay(uint8_t ch, uint8_t s1);
  167. // times a relay-ring drain hit its iteration cap (head/tail desync or corruption) -- non-zero means we caught
  168. // and recovered from what would otherwise be an IRQ-off watchdog hang. Surfaced on the WebUSB panel.
  169. extern volatile uint16_t g_ringFault;
  170. // boot reset: clear relay/active flags, arm the reconnect block
  171. void hapticInit();
  172. // per-loop upkeep: link-edge markers + steam 0x82 quiet timeout + fires the scheduled re-init
  173. void hapticTask();
  174. // replay Steam's haptic-subsystem re-init to the controller -> clears a latched/stuck buzz.
  175. // `slot` defaults to 0xFF (broadcast to all connected) -- the re-init is a settings-only reset and is
  176. // harmless on healthy controllers, so it's worth re-initializing every slot the firmware knows about.
  177. void hapticReinit(uint8_t slot = 0xFF);
  178. // Called from rf_link the instant a controller (re)connect is detected (an F-reply after a gap): blocks haptic
  179. // relays for g_hapticBlockMs and schedules a re-init just after, to keep the freshly-booted
  180. // controller out of the degraded/latched haptic state. Reliable -- independent of hapticTask's link heuristic.
  181. // Per-slot: only the slot that just reconnected is blocked, the others keep relaying.
  182. void hapticOnReconnect(int slot);