haptics.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. #include "haptics.h"
  2. #include "bonds.h"
  3. #include "config.h"
  4. #include "rf_link.h"
  5. #include "usb_tx.h" // usbTxBoost/Unboost -- flood-rate CDC prints share the dcd DMA claim window
  6. #include "puck_hid.h" // puckNotePowerOff() -- hold a powered-off slot disconnected to Steam
  7. #include "steam_commands.h"
  8. #include "fault_diag.h" // faultDiagTrace() -- flight recorder
  9. // USBDevice.suspended() -> autonomous controller power-off on host sleep
  10. #include <Adafruit_TinyUSB.h>
  11. #include <Arduino.h>
  12. #include <string.h>
  13. uint8_t g_relayOp = 0xE3; // E3 poll
  14. // relay sub-TLV TYPE byte. Vestigial: rfConnFlushRelay derives the on-air type from the report id (0x05 for
  15. // actuators/haptics <0x87, 0x01 for config/settings/LED >=0x87). Still exposed over WebUSB diagnostics.
  16. uint8_t g_relaySub = 0x05;
  17. volatile uint8_t g_testHaptic = 0;
  18. volatile uint8_t g_hapticStop = 0;
  19. // Post-connect haptic block (see haptics.h): enabled + duration, both persisted and panel-adjustable.
  20. // Off by default -- opt in from the WebUSB panel if a controller's haptics come up degraded after connect.
  21. uint8_t g_hapticBlockOn = 0;
  22. uint16_t g_hapticBlockMs = HAPTIC_BLOCK_MS_DEFAULT;
  23. // id9 steering (see hapticTask): ON by default. EMULATED modes only -- it lands id9=1 (autonomous pad layer,
  24. // the trackpad-tick source, on) or holds id9=0 every LIZKEEP_MS per the per-type g_padHaptics config. Puck
  25. // modes (STEAM/LIZARD) are left alone: Steam owns the controller's haptics there.
  26. // Persisted; console 'u' toggles for A/B.
  27. uint8_t g_lizKeep = 1;
  28. // Power the controllers off when host sleep persists (see haptics.h for the wake trade-off). Persisted;
  29. // console "SO" toggles.
  30. uint8_t g_suspendOff = 1;
  31. // Host-rumble shaping (persisted in cfg.bin; console "RS<pct>" / "RY<n>").
  32. uint16_t g_rumbleScale = RUMBLE_SCALE_PCT;
  33. uint8_t g_rumbleStyle = RUMBLE_STYLE_NORMAL;
  34. // Master enable for the puck->controller haptic RELAY (Steam OUTPUT reports 0x80-0x86, incl. the trackpad
  35. // texture-feedback stream Steam pushes WHILE you drag). Each relayed frame is an extra TX that precedes the
  36. // E3 poll and steals its reply window, and the controller must stop to process it -- both can depress the
  37. // input rate exactly during a drag. On by default; console "HR" toggles it so the drag-smoothness cost of
  38. // haptics can be isolated on hardware (drag with it OFF vs ON). OFF only affects Steam-driven rumble/pad
  39. // feedback; it does NOT touch settings/config (0x87) or power-off (0x9F) relays.
  40. bool g_hapticRelay = true;
  41. // Per-slot reconnect block. 0 = idle; non-zero = drop haptics aimed at this slot until millis() catches up.
  42. unsigned long g_hapticBlockUntil[NSLOT] = { 0 };
  43. // Controller power-off. CONFIRMED from a real Windows USB capture of the Valve puck: Steam's "turn off
  44. // controller" is the single feature-0x01 command 0x9F with payload ASCII "off!" (6F 66 66 21). The dongle
  45. // forwards host feature reports verbatim, so the controller acts on report 0x9F directly -- we relay it the
  46. // same way (E3 SET sub-TLV). The wire relay is NO-ACK, so send a small burst: a single lost frame must not
  47. // leave the controller on.
  48. void hapticSendShutdown(uint8_t slot)
  49. {
  50. static const uint8_t OFF[4] = { 0x6f, 0x66, 0x66, 0x21 }; // "off!"
  51. // slot-targeted: Steam's 0x9F arrives on ONE controller's interface and must power off only that
  52. // controller (broadcasting it killed every connected controller when the user turned off one). The
  53. // broadcast default (0xFF) remains for the triggers that logically mean "all off": host suspend and
  54. // the panel/test power-off button.
  55. faultDiagTrace(FR_OFF, slot);
  56. for (uint8_t i = 0; i < HAPTIC_SHUTDOWN_SHOTS; i++)
  57. relayEnqueue(IBEX_CMD_TURN_OFF_CONTROLLER, OFF, sizeof OFF,
  58. false, slot);
  59. // Tell the puck presentation layer to show this slot (or all, for 0xFF broadcast) cleanly DISCONNECTED to
  60. // Steam and hold it there through the controller's post-off F1 tail -- otherwise the dying replies bounce
  61. // the connection state (phantom reconnect / never-removed). Covers every power-off path (Steam 0x9F, the
  62. // Steam+Y chord, the panel button, host suspend) since they all funnel through here.
  63. puckNotePowerOff(slot);
  64. }
  65. // millis of last translated host rumble (0x80), per-slot (4 XInput interfaces each have their own stream)
  66. static unsigned long g_rumble80Ms[NSLOT] = { 0 };
  67. // Steam/Triton rumble is latched on until an explicit zero report; tracked per-slot so each controller's
  68. // stuck-rumble watchdog is independent
  69. static bool g_rumble80On[NSLOT] = { false, false, false, false };
  70. // A rumble STOP is relayed as this many copies over successive poll cycles (see hapticSteamRumble): the relay
  71. // is NO-ACK, and a lost final stop leaves the controller latched rumbling. 3 gives temporal diversity a
  72. // single-frame RF loss can't wipe out without meaningfully changing steady-state traffic.
  73. #define RUMBLE_STOP_REPS 3
  74. // ---- relay rings: one per bond slot. Multi-producer (USB ISR + loop-context console/xinput), one consumer
  75. // per slot (rfConnFlushRelay on that slot's poll turn). Producers serialize under PRIMASK.
  76. struct RelayMsg {
  77. uint8_t rid, len;
  78. bool isHaptic;
  79. uint8_t data[RELAY_MAXP];
  80. // set by the caller at relayEnqueue() time (see haptics.h) -- true iff a real controller reply is
  81. // wanted for this specific message, i.e. whether rfConnFlushRelay appends the query-reply trailer.
  82. bool expectReply;
  83. };
  84. // deep enough to hold a full Steam settings/LED transaction burst without loss
  85. #define RELAY_QLEN 32
  86. static RelayMsg g_rq[NSLOT][RELAY_QLEN];
  87. static volatile uint8_t g_rqHead[NSLOT];
  88. static volatile uint8_t
  89. g_rqTail[NSLOT]; // head=next write, tail=next read; empty when equal
  90. static inline uint8_t rqNext(uint8_t i)
  91. {
  92. return (uint8_t)((i + 1) % RELAY_QLEN);
  93. }
  94. // Counts times a ring-drain loop hit its iteration cap = head/tail were desynced or corrupted (e.g. an
  95. // out-of-range g_rqHead from a stray write/stack overflow). These loops run with IRQs DISABLED, so an
  96. // unterminated one spins forever with interrupts off -> millis()/USB/SOF all freeze and only the hardware
  97. // watchdog recovers (an invisible "watchdog (hang)" -- the live stall monitor can't see it because the SOF
  98. // IRQ is dead). The cap turns that into a logged, recovered event instead of a hang. Surfaced on the panel.
  99. volatile uint16_t g_ringFault = 0;
  100. bool relayPending()
  101. {
  102. // check the current slot's queue; called from rfConnQueueHapticRelay which runs with g_curSlot set
  103. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  104. return g_rqHead[cur] != g_rqTail[cur];
  105. }
  106. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
  107. bool isHaptic, uint8_t slot, bool expectReply)
  108. {
  109. if (plen > RELAY_MAXP)
  110. plen = RELAY_MAXP;
  111. if (slot != 0xFF && slot >= NSLOT)
  112. return false;
  113. uint32_t pm = __get_PRIMASK();
  114. __disable_irq();
  115. // slot=0xFF: broadcast -- enqueue into every BONDED slot's ring. Unbonded slots are skipped: only used
  116. // slots are ever flushed (rfConnStep/doPoll), so anything queued for an empty slot sits in its ring
  117. // indefinitely and is then delivered to whatever controller later pairs INTO that slot -- e.g. a host
  118. // suspend with one controller bonded left "off!" (0x9F) frames in the empty rings, and the next
  119. // controller paired there powered itself off on its first flush. puckNotePowerOff() already gates its
  120. // broadcast on used; this is the same rule for the wire side.
  121. // Full queue: evict the oldest entry, never the newest. Steam bursts end with the commit/stop, so
  122. // dropping the oldest keeps the most-recent (meaningful) frame.
  123. uint8_t s0 = (slot == 0xFF) ? 0 : slot;
  124. uint8_t s1 = (slot == 0xFF) ? NSLOT : slot + 1;
  125. for (uint8_t s = s0; s < s1; s++) {
  126. if (slot == 0xFF && !g_slot[s].used)
  127. continue;
  128. uint8_t h = g_rqHead[s], nx = rqNext(h);
  129. if (nx == g_rqTail[s])
  130. g_rqTail[s] = rqNext(g_rqTail[s]);
  131. g_rq[s][h].rid = rid;
  132. g_rq[s][h].len = plen;
  133. g_rq[s][h].expectReply = expectReply;
  134. g_rq[s][h].isHaptic = isHaptic;
  135. if (plen)
  136. memcpy(g_rq[s][h].data, payload, plen);
  137. g_rqHead[s] = nx;
  138. }
  139. __set_PRIMASK(pm);
  140. faultDiagTrace(FR_RELAY, (uint16_t)((slot << 8) | rid));
  141. return true;
  142. }
  143. void relayClearSlot(uint8_t slot)
  144. {
  145. if (slot >= NSLOT)
  146. return;
  147. uint32_t pm = __get_PRIMASK();
  148. __disable_irq();
  149. g_rqHead[slot] = g_rqTail[slot] = 0;
  150. __set_PRIMASK(pm);
  151. }
  152. #if OPK_LOG
  153. // diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
  154. struct HapLog {
  155. uint32_t ms;
  156. uint8_t slot, rid, n, b[16];
  157. }; // 16 payload bytes: capture full 0x87 settings frames
  158. // Big always-on ring: log EVERYTHING from boot (Steam writes, our TX-to-controller, link edges) so a rare
  159. // reconnect-buzz can be caught after the fact -- the trigger happens moments after boot, while this RAM is
  160. // fresh, and we dump it once the panel reconnects. 4096 * 20B ~= 80KB.
  161. #define HAPLOG_N 4096
  162. static HapLog g_hapLog[HAPLOG_N];
  163. static uint16_t g_hapHead = 0;
  164. static uint16_t g_hapTail =
  165. 0; // live/dump drain cursor (loop-context reader; chases g_hapHead)
  166. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n)
  167. {
  168. // Written from the USB SET ISR (handleSet) AND loop context (relay flush / link edges) -> guard g_hapHead.
  169. // Special slot markers for the diagnostic capture: 0xFE = a frame WE transmitted to the controller (TX
  170. // relay); 0xFD = a link state edge (b[0]=1 up, 0 down). Real Steam writes use the interface index (0..3).
  171. uint32_t pm = __get_PRIMASK();
  172. __disable_irq();
  173. HapLog &e = g_hapLog[g_hapHead];
  174. e.ms = millis();
  175. e.slot = slot;
  176. e.rid = rid;
  177. e.n = (uint8_t)(n > 255 ? 255 : n);
  178. for (int i = 0; i < 16; i++)
  179. e.b[i] = (i < (int)n) ? b[i] : 0;
  180. // uint16_t, NOT uint8_t: HAPLOG_N is 4096, so a uint8_t cast truncated the head to 0..255 and only the
  181. // first 256 ring slots were ever used (capture lost ~94% of its depth).
  182. g_hapHead = (uint16_t)((g_hapHead + 1) %
  183. (sizeof g_hapLog / sizeof g_hapLog[0]));
  184. __set_PRIMASK(pm);
  185. }
  186. void hapticDumpLog()
  187. {
  188. const uint16_t N = HAPLOG_N;
  189. uint32_t now = millis();
  190. Serial.printf("# --- capture history (now=%lu, curSlot=%d) ---\n",
  191. (unsigned long)now, g_curSlot);
  192. for (uint16_t i = 0; i < N; i++) {
  193. HapLog &e = g_hapLog[(uint16_t)((g_hapHead + i) % N)];
  194. if (!e.ms && !e.rid)
  195. continue;
  196. Serial.printf("# -%lums if%u rid=%02X n=%u:",
  197. (unsigned long)(now - e.ms), e.slot, e.rid, e.n);
  198. for (uint8_t j = 0; j < 16 && j < e.n; j++)
  199. Serial.printf(" %02X", e.b[j]);
  200. Serial.println();
  201. }
  202. Serial.println("# --- end ---");
  203. }
  204. // ---- drain cursor: stream entries to the WebUSB panel. resetDrain(false)=from "now" (live only);
  205. // resetDrain(true)=from the OLDEST entry (dump the whole ring from boot). pull skips empty slots. ----
  206. void hapLogResetDrain(bool fromBoot)
  207. {
  208. // oldest slot (the next-to-overwrite holds it)
  209. g_hapTail = fromBoot ? (uint16_t)((g_hapHead + 1) % HAPLOG_N) :
  210. // "now"
  211. g_hapHead;
  212. }
  213. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  214. uint8_t bytes16[16])
  215. {
  216. while (g_hapTail != g_hapHead) {
  217. HapLog &e = g_hapLog[g_hapTail];
  218. g_hapTail = (uint16_t)((g_hapTail + 1) % HAPLOG_N);
  219. if (!e.ms && !e.rid)
  220. continue; // skip empty slot (ring not yet full)
  221. // Return the ABSOLUTE log time (millis since boot), not "age now". The panel drains in batches 100ms
  222. // apart, so an age computed here would jump between batches; the panel computes age vs the newest entry.
  223. *logMs = e.ms;
  224. *slot = e.slot;
  225. *rid = e.rid;
  226. *n = (e.n > 16) ? 16 : e.n;
  227. memcpy(bytes16, e.b, 16);
  228. return true;
  229. }
  230. return false;
  231. }
  232. #endif // OPK_LOG
  233. // Per-slot helpers. slot==-1 (default) checks the CURRENT poll slot (g_curSlot), used by flush-time code
  234. // paths that don't have a slot in hand. Callers with a real slot pass it in.
  235. bool hapticLinkUp(int slot)
  236. {
  237. int s = (slot >= 0) ? slot : g_curSlot;
  238. if (s < 0 || s >= NSLOT)
  239. return false;
  240. return g_slot[s].used && (millis() - g_connReplyMs[s]) < 300;
  241. }
  242. bool haptic82Blocked(int slot)
  243. {
  244. int s = (slot >= 0) ? slot : g_curSlot;
  245. if (s < 0 || s >= NSLOT)
  246. return true;
  247. return !hapticLinkUp(s) ||
  248. (g_hapticBlockUntil[s] &&
  249. (int32_t)(millis() - g_hapticBlockUntil[s]) < 0);
  250. }
  251. // "Is haptics from this USB interface's slot allowed through?" -- the slot must be currently connected.
  252. bool hapticRelaySlotOk(int slot)
  253. {
  254. return slot >= 0 && slot < NSLOT && hapticLinkUp(slot);
  255. }
  256. static void hapticCancelPendingOn(int slot)
  257. {
  258. // void queued ON entries (stale haptics / rumble across a reconnect). Per-slot: only the reconnected
  259. // slot's queue is scrubbed -- another controller's pending haptics are its own business. slot=-1 keeps
  260. // the old scrub-everything behavior (boot).
  261. int c0 = (slot >= 0 && slot < NSLOT) ? slot : 0;
  262. int c1 = (slot >= 0 && slot < NSLOT) ? slot + 1 : NSLOT;
  263. uint32_t pm = __get_PRIMASK();
  264. __disable_irq();
  265. for (int s = c0; s < c1; s++) {
  266. uint8_t guard = RELAY_QLEN + 1;
  267. for (uint8_t i = g_rqTail[s]; i != g_rqHead[s]; i = rqNext(i)) {
  268. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off
  269. g_rqHead[s] = g_rqTail[s] = 0;
  270. g_ringFault++;
  271. faultDiagTrace(FR_RINGF, g_ringFault);
  272. break;
  273. }
  274. RelayMsg &m = g_rq[s][i];
  275. if (m.rid == 0x82) {
  276. bool on = false;
  277. for (uint8_t j = 2; j < m.len; j++)
  278. if (m.data[j]) {
  279. on = true;
  280. break;
  281. }
  282. if (on)
  283. m.rid = 0;
  284. }
  285. if (m.rid == 0x80) {
  286. bool on = false;
  287. for (uint8_t j = 0; j < m.len; j++)
  288. if (m.data[j]) {
  289. on = true;
  290. break;
  291. }
  292. if (on)
  293. m.rid = 0;
  294. }
  295. }
  296. }
  297. __set_PRIMASK(pm);
  298. }
  299. // Integer square root (binary restoring). Used by RUMBLE_STYLE_SOFT; avoids pulling float math into the USB
  300. // OUT callback that calls hapticSteamRumble().
  301. static uint32_t isqrt32(uint32_t v)
  302. {
  303. uint32_t r = 0, b = 1UL << 30;
  304. while (b > v)
  305. b >>= 2;
  306. while (b) {
  307. if (v >= r + b) {
  308. v -= r + b;
  309. r = (r >> 1) + b;
  310. } else {
  311. r >>= 1;
  312. }
  313. b >>= 2;
  314. }
  315. return r;
  316. }
  317. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
  318. {
  319. if (slot >= NSLOT)
  320. return false;
  321. // Shape the decoded host amplitudes: style first (which motor plays, and the response curve), then the
  322. // strength scale. Integer-only -- this runs in the USB OUT callback (ISR context), so no float math.
  323. {
  324. uint32_t l = lowFreq, h = highFreq, t;
  325. switch (g_rumbleStyle) {
  326. case RUMBLE_STYLE_MONO:
  327. l = h = (l > h) ? l : h;
  328. break;
  329. case RUMBLE_STYLE_HEAVY:
  330. h = 0;
  331. break;
  332. case RUMBLE_STYLE_LIGHT:
  333. l = 0;
  334. break;
  335. case RUMBLE_STYLE_SWAP:
  336. t = l;
  337. l = h;
  338. h = t;
  339. break;
  340. case RUMBLE_STYLE_PUNCHY:
  341. // x^2/FS: 0xFFFF*0xFFFF fits uint32 exactly, so no intermediate overflow
  342. l = l * l / 0xFFFF;
  343. h = h * h / 0xFFFF;
  344. break;
  345. case RUMBLE_STYLE_SOFT:
  346. l = isqrt32(l * 0xFFFF);
  347. h = isqrt32(h * 0xFFFF);
  348. break;
  349. default: // RUMBLE_STYLE_NORMAL
  350. break;
  351. }
  352. l = l * g_rumbleScale / 100;
  353. h = h * g_rumbleScale / 100;
  354. lowFreq = (l > 0xFFFF) ? 0xFFFF : (uint16_t)l;
  355. highFreq = (h > 0xFFFF) ? 0xFFFF : (uint16_t)h;
  356. }
  357. bool on = lowFreq || highFreq;
  358. // per-type rumble disable: drop ON commands; zero/stop still pass to clear any queued relay
  359. if (on && !g_rumble)
  360. return false;
  361. // Per-slot settle gate (the per-slot reconnect block + link-up check). 0x82 haptics in Steam mode use the
  362. // same gate; for XInput, the host only sends a stream while a controller is connected, so this also doubles
  363. // as "no controller here, no relay".
  364. if (on && haptic82Blocked(slot))
  365. return false;
  366. if (!on && !hapticLinkUp(slot))
  367. return false;
  368. // SDL's current Steam/Triton structs define output report 0x80 as:
  369. // type, uint16 intensity, {uint16 speed, int8 gain} left/right.
  370. // We map conventional gamepad low/high-frequency motors to left/right speeds and use max as intensity.
  371. uint16_t intensity = lowFreq > highFreq ? lowFreq : highFreq;
  372. uint8_t p[9];
  373. // haptic_type_t::HAPTIC_TYPE_RUMBLE; 0 is the off/zero report
  374. p[0] = on ? 0x04 : 0x00;
  375. p[1] = (uint8_t)(intensity & 0xFF);
  376. p[2] = (uint8_t)(intensity >> 8);
  377. p[3] = (uint8_t)(lowFreq & 0xFF);
  378. p[4] = (uint8_t)(lowFreq >> 8);
  379. p[5] = 0;
  380. p[6] = (uint8_t)(highFreq & 0xFF);
  381. p[7] = (uint8_t)(highFreq >> 8);
  382. p[8] = 0;
  383. // The RF relay is NO-ACK -- any single frame can be lost. For an ON that's self-healing: a continuous
  384. // stream of rumble commands follows during play, so a dropped frame is corrected microseconds later. A
  385. // STOP is the dangerous one: if the host's FINAL zero (game/stream quit) -- or the watchdog's stop below
  386. // -- is the last frame on the wire and it's lost, the controller stays latched rumbling with nothing left
  387. // to correct it (the reported "constant rumble that didn't stop after closing GFN", cleared only by a
  388. // replug). And once we optimistically mark g_rumble80On=false, the watchdog can't rescue it either. So
  389. // relay a STOP as a short BURST: rfConnFlushRelay drains one ring entry per poll cycle, so N copies go out
  390. // on successive cycles (~4ms apart) -- temporal diversity that a single-frame RF loss can't wipe out.
  391. // Bursting only the on->off transition leaves steady-state (repeated-ON / repeated-OFF) traffic unchanged.
  392. bool stopping = !on && g_rumble80On[slot];
  393. uint8_t reps = stopping ? RUMBLE_STOP_REPS : 1;
  394. bool queued = false;
  395. for (uint8_t i = 0; i < reps; i++)
  396. if (relayEnqueue(0x80, p, sizeof p, true, slot))
  397. queued = true;
  398. if (!queued)
  399. return false;
  400. g_rumble80Ms[slot] = millis();
  401. g_rumble80On[slot] = on;
  402. return true;
  403. }
  404. // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate). Test
  405. // haptics broadcast to all connected slots (slot 0xFF); the stop frame is broadcast too (a stuck latch can
  406. // affect any controller, and the haptic-engine clear-re-init is settings-only so it's harmless on healthy
  407. // ones).
  408. // Stability-test keepalive: while g_stabTest, buzz every connected controller for ~150ms once per 10s so an
  409. // unattended uptime measurement isn't ended by a controller idle-sleeping. Toggled by WebUSB cmd 0x0F; the
  410. // panel times uptime-until-reset. Reboots clear g_stabTest (the panel re-arms it on reconnect).
  411. bool g_stabTest = false;
  412. void hapticStabTask()
  413. {
  414. if (!g_stabTest)
  415. return;
  416. static const uint8_t on[3] = { 0x01, 0x01, 0xF7 };
  417. static const uint8_t off[3] = { 0x01, 0x01, 0x00 };
  418. static unsigned long lastOn = 0, offAt = 0;
  419. unsigned long now = millis();
  420. // Buzz only slots whose controller is actually answering polls (same 300ms liveness the panel/0x79 use).
  421. // The old 0xFF broadcast kept stuffing a powered-off controller's relay ring -- pure eviction churn plus
  422. // wasted TX at a radio that can't hear it, right inside the power-off hang window (issue #72 repro).
  423. auto enqLive = [&](const uint8_t *p) {
  424. for (uint8_t s = 0; s < NSLOT; s++)
  425. if (g_slot[s].used && g_connReplyMs[s] &&
  426. (unsigned long)(now - g_connReplyMs[s]) < 300u)
  427. relayEnqueue(0x82, p, 3, true, s);
  428. };
  429. if (lastOn == 0 || (uint32_t)(now - lastOn) >= 10000u) {
  430. lastOn = now;
  431. enqLive(on);
  432. offAt = now + 150;
  433. }
  434. if (offAt && (int32_t)(now - offAt) >= 0) {
  435. enqLive(off);
  436. offAt = 0;
  437. }
  438. }
  439. void rfConnQueueHapticRelay()
  440. {
  441. if (relayPending())
  442. return; // host relays first; injectables wait for an idle cycle
  443. static const uint8_t HAP_ON[3] = { 0x01, 0x01, 0xF7 };
  444. static const uint8_t HAP_OFF[3] = { 0x01, 0x01, 0x00 };
  445. if (g_testHaptic) {
  446. if (relayEnqueue(0x82, HAP_ON, 3, true, 0xFF))
  447. g_testHaptic--;
  448. } else if (g_hapticStop && !g_xbox) {
  449. if (relayEnqueue(0x82, HAP_OFF, 3, true, 0xFF))
  450. g_hapticStop--;
  451. }
  452. }
  453. // rfConnFlushRelay(ch, s1): drain one entry from the current slot's relay queue and TX it. Each slot's queue
  454. // is independent, so each controller only sees its own commands. With N connected slots the per-slot relay
  455. // rate is 1/N of the per-cycle rate; sustained buzz streams are still 1 packet/cycle/slot.
  456. bool rfConnFlushRelay(uint8_t ch, uint8_t s1)
  457. {
  458. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  459. // Snapshot one entry under a short critical section. relayEnqueue() (the producer) runs on the
  460. // high-priority usbd task and, when the ring is full, evicts the oldest by advancing g_rqTail itself --
  461. // the same variable this consumer reads/advances. Reading the entry while a producer could overwrite it
  462. // is a torn read (and dual-writing g_rqTail desyncs head/tail), so copy the entry out and consume the
  463. // slot atomically here, then do the (slow) RF TX on the copy with interrupts enabled.
  464. RelayMsg msg;
  465. bool have = false;
  466. uint32_t pm = __get_PRIMASK();
  467. __disable_irq();
  468. uint8_t guard = RELAY_QLEN + 1;
  469. while (g_rqTail[cur] != g_rqHead[cur]) {
  470. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off (watchdog-hang class)
  471. g_rqHead[cur] = g_rqTail[cur] = 0;
  472. g_ringFault++;
  473. faultDiagTrace(FR_RINGF, g_ringFault);
  474. break;
  475. }
  476. RelayMsg &m = g_rq[cur][g_rqTail[cur]];
  477. g_rqTail[cur] = rqNext(g_rqTail[cur]); // consume the slot
  478. // rid 0 = entry voided by hapticCancelPendingOn -> skip
  479. if (m.rid) {
  480. msg = m; // copy out before the producer can reuse the slot
  481. have = true;
  482. break;
  483. }
  484. }
  485. __set_PRIMASK(pm);
  486. if (have) {
  487. RelayMsg &m = msg;
  488. {
  489. uint8_t rl = m.len;
  490. if (rl > RELAY_MAXP)
  491. rl = RELAY_MAXP;
  492. // On-air sub-TLV framing. CONFIRMED from real puck<->controller sniffs: a command LANDS on
  493. // the controller only with the type-01 + inner-len form E3 [2+rl][01][rid][innerlen][data];
  494. // the legacy form E3 [1+rl][05][rid][data] makes the controller DISCARD any 0x87+ command.
  495. // Same shape as the `[len][tag][value]` TLV grammar the F1 REPLY side
  496. // already uses (tags 0x02/0x04/0x06, docs/PROTOCOL.md sec 7.3): read as len=1, tag=3,
  497. // value=0. If the caller decides they want an answer, we add the queryTrailer.
  498. // Should check later if there's a way to somehow determine which types need the trailer and which ones don't.
  499. bool queryTrailer = m.expectReply;
  500. uint8_t p[5 + RELAY_MAXP + 3], plen;
  501. if (!m.isHaptic) {
  502. // Non-haptics use type 01
  503. p[0] = g_relayOp;
  504. p[1] = (uint8_t)(2 + rl);
  505. p[2] = 0x01;
  506. p[3] = m.rid;
  507. p[4] = rl;
  508. memcpy(p + 5, m.data, rl);
  509. plen = (uint8_t)(5 + rl);
  510. if (queryTrailer && plen + 3 <= sizeof p) {
  511. p[plen + 0] = 0x01;
  512. p[plen + 1] = 0x03;
  513. p[plen + 2] = 0x00;
  514. plen = (uint8_t)(plen + 3);
  515. }
  516. } else {
  517. // Haptics use type 05
  518. p[0] = g_relayOp;
  519. p[1] = (uint8_t)(1 + rl);
  520. p[2] = 0x05;
  521. p[3] = m.rid;
  522. memcpy(p + 4, m.data, rl);
  523. plen = (uint8_t)(4 + rl);
  524. }
  525. hapLogAdd(0xFE, m.rid, m.data, rl);
  526. // Lifecycle log (CDC debug boot): EXACTLY what we TX to the controller, so a capture shows
  527. // whether OpenPuck (or Steam in the background) pokes the controller right before a buzz
  528. // latches -- the piece the I45 stream (controller->us) can't show. Low rate outside a haptic
  529. // burst; guarded so it never blocks the loop. cur = target slot.
  530. if (Serial.availableForWrite() > 60) {
  531. // boosted: this print runs at relay-flood rate and CDC flush enters the same
  532. // TinyUSB DMA claim window as HID sends (the issue-72 livelock; see usb_tx.cpp)
  533. usbTxBoost();
  534. Serial.printf("# TX t=%lu slot%d %s rid=%02X:",
  535. (unsigned long)millis(), cur,
  536. m.isHaptic ? "L05" : "L01",
  537. m.rid);
  538. for (uint8_t i = 0; i < rl && i < 8; i++)
  539. Serial.printf(" %02X", m.data[i]);
  540. Serial.println();
  541. usbTxUnboost();
  542. }
  543. // slot was already consumed under the critical section above.
  544. // s1 carries a PID distinct from the GET poll (caller cycles it) so the controller's ESB
  545. // dedup never treats the GET as a retransmit of this relay.
  546. //
  547. // HARVEST the relay's reply as INPUT. Like any frame we send, the controller auto-ACKs a
  548. // relay with its current input in the ACK payload (~90us later, per the RE poll->ACK
  549. // capture). The old 80us window closed BEFORE that reply arrived, so every relay's input was
  550. // discarded -- and yet HW A/B showed the delivered input rate RISING when haptics relay
  551. // (an extra frame/cycle during a trackpad drag), because each frame makes the controller
  552. // refresh its ACK payload and the following E3 poll caught the fresher value. Reading the
  553. // reply directly turns each relay into a SECOND input sample per cycle: rfConnTx runs the
  554. // full F1 decode (seq-dedup guards double-forward), so a drag streaming haptics now collects
  555. // ~2x the samples, closing the gap to the real puck. A present reply returns early (~90us);
  556. // only a genuine no-reply pays the bounded 400us window, so airtime stays in budget.
  557. rfConnTx(
  558. ch, s1, p, plen,
  559. 400); // one relay per poll cycle -- reply harvested as input
  560. }
  561. }
  562. return have; // true = a relay frame went out this cycle (its reply is harvested as input, above)
  563. }
  564. void hapticReinit(uint8_t slot)
  565. {
  566. // TODO: This function is only ever called from WebUSB in debug mode
  567. // when the user clicks the "Clear stuck buzz" button.
  568. // I believe with MR 230 the haptics issues should be gone,
  569. // so this can probably be removed?
  570. // clang-format off
  571. static const uint8_t haptic_reset_data_1[] = {
  572. SETTING_LEFT_TRACKPAD_MODE, 0x07, 0x00,
  573. SETTING_RIGHT_TRACKPAD_MODE, 0x07, 0x00,
  574. SETTING_WIRELESS_PACKET_VERSION, 0x02, 0x00,
  575. SETTING_COUNT, 0x03, 0x00 };
  576. static const uint8_t haptic_reset_data_2[] = {
  577. SETTING_SMOOTH_ABSOLUTE_MOUSE, 0x00, 0x00,
  578. SETTING_ENABLE_RAW_JOYSTICK, 0x00, 0x00,
  579. SETTING_LEFT_TRACKPAD_CLICK_PRESSURE, 0xff, 0xff,
  580. SETTING_RIGHT_TRACKPAD_CLICK_PRESSURE, 0xff, 0xff,
  581. SETTING_LEFT_TRACKPAD_CLICK_PRESSURE, 0xff, 0xff };
  582. static const uint8_t haptic_reset_data_3[] = {
  583. SETTING_RIGHT_TRACKPAD_CLICK_PRESSURE, 0xff, 0xff,
  584. SETTING_ENABLE_RAW_JOYSTICK, 0x00, 0x00 };
  585. // clang-format on
  586. static const uint8_t T81A[] = {
  587. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  588. };
  589. static const uint8_t T81B[] = {
  590. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  591. };
  592. // reset action (FUN_0001f554) -- Steam sends this first
  593. relayEnqueue(0x81, nullptr, 0, true, slot);
  594. // This is probably split into three commands because you can only update
  595. // five parameters in one batch.
  596. relayEnqueue(IBEX_CMD_SET_SETTINGS_VALUES, haptic_reset_data_1,
  597. sizeof haptic_reset_data_1, false, slot);
  598. relayEnqueue(IBEX_CMD_SET_SETTINGS_VALUES, haptic_reset_data_2,
  599. sizeof haptic_reset_data_2, false, slot);
  600. relayEnqueue(IBEX_CMD_SET_SETTINGS_VALUES, haptic_reset_data_3,
  601. sizeof haptic_reset_data_3, false, slot);
  602. // explicitly trigger an empty HAPTIC_PULSE: the part that clears a latch
  603. relayEnqueue(0x81, T81A, sizeof T81A, true, slot);
  604. relayEnqueue(0x81, T81B, sizeof T81B, true, slot);
  605. // Apply the configured LED brightness for the active emulated type. Steam sets the
  606. // brightness each session; emulated modes never do, so the controller comes up at
  607. // full brightness. 0 = no override (preserve controller default).
  608. // TODO: This has nothing to do with haptics, why is this here?
  609. if (g_etype < ET_COUNT && g_ledBright > 0) {
  610. uint8_t pl[3] = { SETTING_LED_USER_BRIGHTNESS, g_ledBright,
  611. 0x00 };
  612. relayEnqueue(IBEX_CMD_SET_SETTINGS_VALUES, pl, sizeof pl, false,
  613. slot);
  614. }
  615. }
  616. void hapticInit()
  617. {
  618. g_hapticStop = 0;
  619. for (int s = 0; s < NSLOT; s++) {
  620. g_rqHead[s] = g_rqTail[s] = 0;
  621. g_rumble80On[s] = false;
  622. g_rumble80Ms[s] = 0;
  623. // post-connect haptic block is permanently disabled (not armed, not configurable)
  624. g_hapticBlockUntil[s] = 0;
  625. }
  626. }
  627. // Emulated modes that report motion from the controller's RF IMU samples.
  628. static const uint32_t IMU_KEEP_MS = 5000;
  629. static bool modeWantsImu(uint8_t m)
  630. {
  631. return m == MODE_SW_PRO || m == MODE_PS5 || m == MODE_PS5_GAME ||
  632. m == MODE_HIDGYRO || m == MODE_DS4_GAME || m == MODE_PS3 ||
  633. m == MODE_DINPUT || m == MODE_SINPUT;
  634. }
  635. // Steam can leave the controller's persisted IMU mode off, and it survives across pucks. Every emulated
  636. // gyro mode needs the raw RF samples that setting suppresses, so write raw accel + gyro for this slot.
  637. static void imuForceRaw(int slot)
  638. {
  639. static const uint8_t RAW_IMU[] = {
  640. SETTING_IMU_MODE,
  641. SETTING_GYRO_MODE_SEND_RAW_ACCEL |
  642. SETTING_GYRO_MODE_SEND_RAW_GYRO,
  643. 0x00
  644. };
  645. if (!modeWantsImu(g_usbMode))
  646. return;
  647. relayEnqueue(IBEX_CMD_SET_SETTINGS_VALUES, RAW_IMU, sizeof RAW_IMU,
  648. false, (uint8_t)slot);
  649. }
  650. // Schedule the clearing re-init for ONE slot. Called on the reliable first-reply signal from rf_link and
  651. // again from hapticTask's link-up edge detector. Strictly per-slot: only the reconnected controller gets
  652. // re-inits / its pending haptics scrubbed -- the other controllers hear nothing about it.
  653. void hapticOnReconnect(int slot)
  654. {
  655. if (slot < 0 || slot >= NSLOT)
  656. return;
  657. // post-connect haptic block is permanently disabled -- relay haptics immediately on (re)connect
  658. g_hapticBlockUntil[slot] = 0;
  659. g_rumble80On[slot] = false;
  660. g_rumble80Ms[slot] = 0;
  661. // Scrub haptics queued before the link came up (stale across the reconnect) -- this slot only.
  662. hapticCancelPendingOn(slot);
  663. imuForceRaw(slot);
  664. // NO automatic haptic re-init. hapticReinit lands 0x81 (CLEAR DIGITAL MAPPINGS -- rid < 0x87 so it
  665. // EXECUTES even on legacy framing). 0x81 is NON-IDEMPOTENT (ibex FUN_0001f554): EVERY call re-runs the
  666. // lizard-disable event + func_0x0001bbf0 (a hardware peripheral re-arm unique to the 0x81 path) -- so
  667. // firing 8 across the connect window = 8 audible clicks = the "repeated non-periodic clicks at connect"
  668. // the real puck (which clears ONCE then holds) never produces. The clearing re-init stays available on
  669. // demand (WebUSB "Clear stuck buzz" -> hapticReinit), just not sprayed at connect.
  670. uint8_t mk = 2;
  671. hapLogAdd(0xFD, 0xEE, &mk, 1);
  672. }
  673. // Panel/console rumble test: buzz every connected controller at RUMBLE_TEST_AMP through the normal
  674. // hapticSteamRumble() path (so g_rumbleStyle / g_rumbleScale / the per-type rumble toggle all apply), and
  675. // schedule the stop. Deadline 0 = idle; millis()+MS can be 0 only once every 49 days, so bias it off zero.
  676. static unsigned long g_rumbleTestStop = 0;
  677. static unsigned long g_rfJournalBuilderTickStop = 0;
  678. static uint8_t g_rfJournalBuilderTickMask = 0;
  679. static const uint8_t RF_JOURNAL_BUILDER_TICK_ON[3] = { 0x01, 0x01, 0xF7 };
  680. static const uint8_t RF_JOURNAL_BUILDER_TICK_OFF[3] = { 0x01, 0x01, 0x00 };
  681. void hapticRfJournalBuilderTick(uint8_t participantMask)
  682. {
  683. g_rfJournalBuilderTickMask = 0;
  684. for (uint8_t s = 0; s < NSLOT; s++) {
  685. const uint8_t bit = (uint8_t)(1u << s);
  686. if ((participantMask & bit) && hapticLinkUp((int)s) &&
  687. relayEnqueue(0x82, RF_JOURNAL_BUILDER_TICK_ON,
  688. sizeof RF_JOURNAL_BUILDER_TICK_ON, true, s))
  689. g_rfJournalBuilderTickMask |= bit;
  690. }
  691. if (!g_rfJournalBuilderTickMask) {
  692. g_rfJournalBuilderTickStop = 0;
  693. return;
  694. }
  695. g_rfJournalBuilderTickStop = millis() + RF_JOURNAL_BUILDER_TICK_MS;
  696. if (!g_rfJournalBuilderTickStop)
  697. g_rfJournalBuilderTickStop = 1;
  698. }
  699. void hapticTestRumble()
  700. {
  701. for (uint8_t s = 0; s < NSLOT; s++)
  702. if (hapticLinkUp((int)s))
  703. hapticSteamRumble(RUMBLE_TEST_AMP, RUMBLE_TEST_AMP, s);
  704. g_rumbleTestStop = millis() + RUMBLE_TEST_MS;
  705. if (!g_rumbleTestStop)
  706. g_rumbleTestStop = 1;
  707. }
  708. void hapticTask()
  709. {
  710. // stop the test buzz -- signed compare so the millis() rollover cannot strand a latched actuator
  711. if (g_rumbleTestStop && (long)(millis() - g_rumbleTestStop) >= 0) {
  712. g_rumbleTestStop = 0;
  713. for (uint8_t s = 0; s < NSLOT; s++)
  714. hapticSteamRumble(0, 0, s);
  715. }
  716. // The journal-builder tick is outside the measured window. Queue explicit
  717. // stops before the builder's inter-channel quiet guard expires.
  718. if (g_rfJournalBuilderTickStop &&
  719. (long)(millis() - g_rfJournalBuilderTickStop) >= 0) {
  720. g_rfJournalBuilderTickStop = 0;
  721. const uint8_t mask = g_rfJournalBuilderTickMask;
  722. g_rfJournalBuilderTickMask = 0;
  723. for (uint8_t s = 0; s < NSLOT; s++)
  724. if (mask & (uint8_t)(1u << s))
  725. relayEnqueue(0x82, RF_JOURNAL_BUILDER_TICK_OFF,
  726. sizeof RF_JOURNAL_BUILDER_TICK_OFF,
  727. true, s);
  728. }
  729. // id9 steering (SET_SETTINGS index 9 = digital-mappings / the controller's AUTONOMOUS mapping+haptic
  730. // engine, which is what generates the trackpad tick haptics). We decide per mode whether that autonomous
  731. // engine should be ON, then either land id9=1 ONCE per connect episode (engine on) or hold id9=0 every
  732. // LIZKEEP_MS (engine off). id9 gates the whole autonomous pad layer INCLUDING the trackpad ticks; 0x87 is
  733. // change-guarded in the controller so a repeated same-value write is silent (no 0x81-style click), and
  734. // re-landing id9=1 on (re)connect restores the pad layer immediately instead of waiting for the
  735. // controller's revert timer (which also resets all settings = audible pop).
  736. //
  737. // wantAuto = should the controller run its own pad layer (trackpad ticks) for the ACTIVE mode?
  738. // - puck modes (STEAM/LIZARD): NOT STEERED AT ALL. Steam owns the controller's haptics in puck mode
  739. // (it writes id9 itself), so the whole block below is skipped -- we used to drive id9 from
  740. // puckLizardActive() here, which fought Steam's own writes.
  741. // - emulated modes (Xbox/Switch/DS): follow the per-type trackpad-haptics config g_padHaptics (default
  742. // ON; Switch defaults OFF). Holding id9=0 is how we turn the controller's autonomous trackpad
  743. // haptics OFF for a type that doesn't want them.
  744. if (g_lizKeep) {
  745. static unsigned long lastKeep[NSLOT] = { 0 };
  746. static bool landedAuto[NSLOT] = { false };
  747. static const uint8_t DATA_LIZARD_OFF[3] = { SETTING_LIZARD_MODE,
  748. 0x00, 0x00 };
  749. static const uint8_t DATA_LIZARD_ON[3] = { SETTING_LIZARD_MODE,
  750. 0x01, 0x00 };
  751. // In puck mode Steam owns haptics; skip id9 steering.
  752. if (!modeIsPuck(g_usbMode)) {
  753. bool wantAuto = (g_padHaptics != 0);
  754. for (int s = 0; s < NSLOT; s++) {
  755. if (!g_slot[s].used || !hapticLinkUp(s)) {
  756. // re-land id9 on the next (re)connect: a fresh controller defaults to
  757. // autonomous, but one carrying our previous session's id9 does not
  758. landedAuto[s] = false;
  759. lastKeep[s] = 0;
  760. continue;
  761. }
  762. if (wantAuto) {
  763. if (!landedAuto[s]) {
  764. landedAuto[s] = true;
  765. relayEnqueue(
  766. IBEX_CMD_SET_SETTINGS_VALUES,
  767. DATA_LIZARD_ON,
  768. sizeof DATA_LIZARD_ON,
  769. false, (uint8_t)s);
  770. }
  771. } else {
  772. landedAuto[s] = false;
  773. if (lastKeep[s] &&
  774. (uint32_t)(millis() - lastKeep[s]) <
  775. LIZKEEP_MS)
  776. continue;
  777. lastKeep[s] = millis();
  778. relayEnqueue(
  779. IBEX_CMD_SET_SETTINGS_VALUES,
  780. DATA_LIZARD_OFF,
  781. sizeof DATA_LIZARD_OFF, false,
  782. (uint8_t)s);
  783. }
  784. }
  785. } // !modeIsPuck
  786. }
  787. // Re-assert the raw IMU mode on a mode switch (the link stays up, so hapticOnReconnect never fires)
  788. // and periodically, in case Steam or the controller turned it back off while we were connected.
  789. {
  790. static uint8_t lastMode = 0xFF;
  791. static unsigned long lastImu[NSLOT] = { 0 };
  792. bool modeChanged = (g_usbMode != lastMode);
  793. lastMode = g_usbMode;
  794. for (int s = 0; s < NSLOT; s++) {
  795. if (!g_slot[s].used || !hapticLinkUp(s))
  796. continue;
  797. if (!modeChanged &&
  798. (uint32_t)(millis() - lastImu[s]) < IMU_KEEP_MS)
  799. continue;
  800. lastImu[s] = millis();
  801. imuForceRaw(s);
  802. }
  803. }
  804. // Per-slot link-edge detect (backup for hapticOnReconnect in rf_link).
  805. static bool wasHapticLinkUp[NSLOT] = { 0 };
  806. for (int s = 0; s < NSLOT; s++) {
  807. if (!g_slot[s].used)
  808. continue;
  809. bool up = hapticLinkUp(s);
  810. if (up && !wasHapticLinkUp[s]) {
  811. uint8_t mk = 1;
  812. hapLogAdd(0xFD, 0xEE, &mk, 1);
  813. hapticOnReconnect(s);
  814. }
  815. if (!up && wasHapticLinkUp[s]) {
  816. uint8_t mk = 0;
  817. hapLogAdd(0xFD, 0xEE, &mk, 1);
  818. // Lifecycle log (CDC debug boot): the 300ms link-up watchdog saw this slot go silent. Pairs
  819. // with the CONNECT/RECONNECT lines from rf_link so a session of cycles shows the full
  820. // down->up cadence -- how often the link actually drops (churn) vs stays up.
  821. if (Serial.availableForWrite() > 50)
  822. Serial.printf("# LC t=%lu slot%d link DOWN\n",
  823. (unsigned long)millis(), s);
  824. }
  825. wasHapticLinkUp[s] = up;
  826. }
  827. // (No automatic re-init firing -- see hapticOnReconnect: the connect-time 0x81 storm was the click train
  828. // the real puck never makes. hapticReinit is on-demand only, via the WebUSB "Clear stuck buzz" button.)
  829. // Power-off on host sleep: only when VBUS is present (genuine sleep, not a cable unplug which also
  830. // trips the suspend edge briefly) AND the suspend has PERSISTED >= SUSPEND_OFF_MS. A brief USB
  831. // selective-suspend (host idle power-management) resumes in <1s; firing the power-off on its edge
  832. // powered the controllers off ourselves -> random drop/reconnect churn. Arm only on a genuine
  833. // resume->suspend edge (wasSusp=true at boot suppresses a false fire on boot-into-suspended).
  834. static bool wasSusp = true;
  835. static unsigned long suspSinceMs = 0;
  836. static bool suspArmed = false;
  837. bool susp = USBDevice.suspended();
  838. bool vbus = (NRF_POWER->USBREGSTATUS &
  839. POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
  840. if (susp && !wasSusp) {
  841. suspSinceMs = millis();
  842. suspArmed = true;
  843. faultDiagTrace(FR_SUSP, 0);
  844. }
  845. if (!susp) {
  846. if (wasSusp)
  847. faultDiagTrace(FR_RESUME, 0);
  848. suspArmed = false;
  849. }
  850. // g_suspendOff: opt out of the power-off entirely -- keeps the controller awake through host sleep so its
  851. // short-Steam-press remote wakeup stays available (see haptics.h). Disarm either way so re-enabling it
  852. // mid-suspend can't fire late.
  853. if (suspArmed && vbus && (millis() - suspSinceMs) >= SUSPEND_OFF_MS) {
  854. if (g_suspendOff)
  855. hapticSendShutdown();
  856. suspArmed = false; // fire once per suspend
  857. }
  858. wasSusp = susp;
  859. // Per-slot stuck-rumble watchdog: force zero after 2.5s without a refresh.
  860. for (int s = 0; s < NSLOT; s++) {
  861. if (g_rumble80On[s] && millis() - g_rumble80Ms[s] > 2500u)
  862. hapticSteamRumble(0, 0, (uint8_t)s);
  863. }
  864. // (No automatic idle-clear re-init either: same 0x81-click reason. A genuinely stuck buzz is cleared
  865. // on demand from the panel. Verbatim relay -- like the real puck -- is the steady-state behavior.)
  866. }