haptics.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. #include "haptics.h"
  2. #include "bonds.h"
  3. #include "config.h"
  4. #include "rf_link.h"
  5. // USBDevice.suspended() -> autonomous controller power-off on host sleep
  6. #include <Adafruit_TinyUSB.h>
  7. #include <Arduino.h>
  8. #include <string.h>
  9. uint8_t g_relayOp = 0xE3; // E3 poll
  10. // relay sub-TLV TYPE byte. Vestigial: rfConnFlushRelay derives the on-air type from the report id (0x05 for
  11. // actuators/haptics <0x87, 0x01 for config/settings/LED >=0x87). Still exposed over WebUSB diagnostics.
  12. uint8_t g_relaySub = 0x05;
  13. volatile uint8_t g_testHaptic = 0;
  14. volatile uint8_t g_hapticStop = 0;
  15. unsigned long g_hapticBlockUntil =
  16. 0; // drop Steam haptics briefly during reconnect settle
  17. // Controller power-off. CONFIRMED from a real Windows USB capture of the Valve puck: Steam's "turn off
  18. // controller" is the single feature-0x01 command 0x9F with payload ASCII "off!" (6F 66 66 21). The dongle
  19. // forwards host feature reports verbatim, so the controller acts on report 0x9F directly -- we relay it the
  20. // same way (E3 SET sub-TLV). The wire relay is NO-ACK, so send a small burst: a single lost frame must not
  21. // leave the controller on.
  22. void hapticSendShutdown()
  23. {
  24. static const uint8_t OFF[4] = { 0x6f, 0x66, 0x66, 0x21 }; // "off!"
  25. for (uint8_t i = 0; i < HAPTIC_SHUTDOWN_SHOTS; i++)
  26. relayEnqueue(0x9F, OFF, sizeof OFF);
  27. }
  28. // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
  29. static unsigned long g_haptic82Ms = 0;
  30. // a non-zero 0x82 haptic is currently active (awaiting host stop)
  31. static bool g_haptic82On = false;
  32. // millis of last translated host rumble (0x80)
  33. static unsigned long g_rumble80Ms = 0;
  34. // Steam/Triton rumble is latched on until an explicit zero report
  35. static bool g_rumble80On = false;
  36. // haptic activity happened (tracked for the 0x82 quiet timeout; the idle-clear that read it is gone)
  37. static bool g_hapClearArmed = false;
  38. // ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
  39. // Producers serialize through a brief PRIMASK critical section (copy is <=62 bytes); the consumer only ever
  40. // touches the tail entry, which no producer writes while the ring isn't full -- so a flush can't be torn.
  41. struct RelayMsg {
  42. uint8_t rid, len;
  43. uint8_t data[RELAY_MAXP];
  44. };
  45. // deep enough to hold a full Steam settings/LED transaction burst without loss
  46. #define RELAY_QLEN 32
  47. static RelayMsg g_rq[RELAY_QLEN];
  48. static volatile uint8_t
  49. g_rqHead = 0,
  50. g_rqTail = 0; // head=next write, tail=next read; empty when equal
  51. static inline uint8_t rqNext(uint8_t i)
  52. {
  53. return (uint8_t)((i + 1) % RELAY_QLEN);
  54. }
  55. bool relayPending()
  56. {
  57. return g_rqHead != g_rqTail;
  58. }
  59. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen)
  60. {
  61. // 60B is the RF frame ceiling; longer can't be relayed
  62. if (plen > RELAY_MAXP)
  63. plen = RELAY_MAXP;
  64. uint32_t pm = __get_PRIMASK();
  65. __disable_irq();
  66. uint8_t h = g_rqHead, nx = rqNext(h);
  67. // Full -> evict the OLDEST entry, never the newest. Steam sends commands as bursts where the meaningful
  68. // frame comes LAST (a settings transaction ends with its commit; a haptic stream ends with its stop), so
  69. // dropping the oldest guarantees the most recent command (LED commit, haptic stop) always lands.
  70. if (nx == g_rqTail)
  71. g_rqTail = rqNext(g_rqTail);
  72. g_rq[h].rid = rid;
  73. g_rq[h].len = plen;
  74. if (plen)
  75. memcpy(g_rq[h].data, payload, plen);
  76. g_rqHead = nx;
  77. // Any haptic relay (Steam OR Xbox rumble OR test) arms the idle-clear and refreshes its timer -- so the
  78. // during-use buzz gets cleared in EVERY USB mode. The re-init's own 0x81/0x87 deliberately don't match.
  79. if (rid == 0x82 || rid == 0x80) {
  80. g_haptic82Ms = millis();
  81. g_hapClearArmed = true;
  82. }
  83. __set_PRIMASK(pm);
  84. return true;
  85. }
  86. // Relay a haptic actuator report (0x80-0x86), BURSTING a stop. CONFIRMED in the real-puck sniff (during-play,
  87. // t=49075..50230): the glide is a stream of `82 [side] 01 [gain]`, and Steam DOES send explicit stops
  88. // `82 [side] 01 00` (gain 0). OpenPuck relays each frame exactly ONCE over a NO-ACK link, so a single dropped
  89. // stop leaves that side's actuator driven -> the persistent buzz that survives mode switches and only the Steam
  90. // button (controller-side reset) clears. The fix is the same one already used for power-off (hapticSendShutdown
  91. // bursts because "a single lost frame must not leave the controller on"): send the STOP several times. A gain-0
  92. // stop is SILENT, so repeating it adds no pad clicks -- unlike an ON pulse, which we still send exactly once.
  93. // * 0x82 stop = gain byte (payload[2]) == 0 (per side; Steam sends one stop frame per side)
  94. // * 0x80 stop = type byte (payload[0]) == 0 (the rumble off/zero report)
  95. // Non-haptic rids (LED/settings/power-off routed through here) are never stops -> single enqueue, unchanged.
  96. bool relayHaptic(uint8_t rid, const uint8_t *p, uint8_t n)
  97. {
  98. bool stop = (rid == 0x82 && n >= 3 && p[2] == 0) ||
  99. (rid == 0x80 && n >= 1 && p[0] == 0);
  100. uint8_t reps = stop ? HAPTIC_STOP_BURST : 1u;
  101. bool ok = false;
  102. for (uint8_t i = 0; i < reps; i++)
  103. ok = relayEnqueue(rid, p, n) || ok;
  104. return ok;
  105. }
  106. #if OPK_LOG
  107. // diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
  108. struct HapLog {
  109. uint32_t ms;
  110. uint8_t slot, rid, n, b[16];
  111. }; // 16 payload bytes: capture full 0x87 settings frames
  112. // Big always-on ring: log EVERYTHING from boot (Steam writes, our TX-to-controller, link edges) so a rare
  113. // reconnect-buzz can be caught after the fact -- the trigger happens moments after boot, while this RAM is
  114. // fresh, and we dump it once the panel reconnects. 4096 * 20B ~= 80KB.
  115. #define HAPLOG_N 4096
  116. static HapLog g_hapLog[HAPLOG_N];
  117. static uint16_t g_hapHead = 0;
  118. static uint16_t g_hapTail =
  119. 0; // live/dump drain cursor (loop-context reader; chases g_hapHead)
  120. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n)
  121. {
  122. // Written from the USB SET ISR (handleSet) AND loop context (relay flush / link edges) -> guard g_hapHead.
  123. // Special slot markers for the diagnostic capture: 0xFE = a frame WE transmitted to the controller (TX
  124. // relay); 0xFD = a link state edge (b[0]=1 up, 0 down). Real Steam writes use the interface index (0..3).
  125. uint32_t pm = __get_PRIMASK();
  126. __disable_irq();
  127. HapLog &e = g_hapLog[g_hapHead];
  128. e.ms = millis();
  129. e.slot = slot;
  130. e.rid = rid;
  131. e.n = (uint8_t)(n > 255 ? 255 : n);
  132. for (int i = 0; i < 16; i++)
  133. e.b[i] = (i < (int)n) ? b[i] : 0;
  134. g_hapHead = (uint8_t)((g_hapHead + 1) %
  135. (sizeof g_hapLog / sizeof g_hapLog[0]));
  136. __set_PRIMASK(pm);
  137. }
  138. void hapticDumpLog()
  139. {
  140. const uint16_t N = HAPLOG_N;
  141. uint32_t now = millis();
  142. Serial.printf("# --- capture history (now=%lu, connSlot=%d) ---\n",
  143. (unsigned long)now, g_connSlot);
  144. for (uint16_t i = 0; i < N; i++) {
  145. HapLog &e = g_hapLog[(uint16_t)((g_hapHead + i) % N)];
  146. if (!e.ms && !e.rid)
  147. continue;
  148. Serial.printf("# -%lums if%u rid=%02X n=%u:",
  149. (unsigned long)(now - e.ms), e.slot, e.rid, e.n);
  150. for (uint8_t j = 0; j < 16 && j < e.n; j++)
  151. Serial.printf(" %02X", e.b[j]);
  152. Serial.println();
  153. }
  154. Serial.println("# --- end ---");
  155. }
  156. // ---- drain cursor: stream entries to the WebUSB panel. resetDrain(false)=from "now" (live only);
  157. // resetDrain(true)=from the OLDEST entry (dump the whole ring from boot). pull skips empty slots. ----
  158. void hapLogResetDrain(bool fromBoot)
  159. {
  160. // oldest slot (the next-to-overwrite holds it)
  161. g_hapTail = fromBoot ? (uint16_t)((g_hapHead + 1) % HAPLOG_N) :
  162. // "now"
  163. g_hapHead;
  164. }
  165. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  166. uint8_t bytes16[16])
  167. {
  168. while (g_hapTail != g_hapHead) {
  169. HapLog &e = g_hapLog[g_hapTail];
  170. g_hapTail = (uint16_t)((g_hapTail + 1) % HAPLOG_N);
  171. if (!e.ms && !e.rid)
  172. continue; // skip empty slot (ring not yet full)
  173. // Return the ABSOLUTE log time (millis since boot), not "age now". The panel drains in batches 100ms
  174. // apart, so an age computed here would jump between batches; the panel computes age vs the newest entry.
  175. *logMs = e.ms;
  176. *slot = e.slot;
  177. *rid = e.rid;
  178. *n = (e.n > 16) ? 16 : e.n;
  179. memcpy(bytes16, e.b, 16);
  180. return true;
  181. }
  182. return false;
  183. }
  184. #endif // OPK_LOG
  185. bool hapticLinkUp()
  186. {
  187. return g_connSlot >= 0 && (millis() - g_connReplyMs) < 300;
  188. }
  189. bool haptic82Blocked()
  190. {
  191. return !hapticLinkUp() ||
  192. (g_hapticBlockUntil &&
  193. (int32_t)(millis() - g_hapticBlockUntil) < 0);
  194. }
  195. bool hapticRelaySlotOk(int slot)
  196. {
  197. return g_connSlot >= 0 && slot == g_connSlot;
  198. }
  199. static bool haptic82PayloadOn(const uint8_t *p, uint16_t n)
  200. {
  201. if (n < 3)
  202. return false;
  203. for (uint16_t i = 2; i < n; i++)
  204. if (p[i])
  205. // observed form is [01 01 gain], but treat any trailing non-zero as active
  206. return true;
  207. return false;
  208. }
  209. static void hapticCancelPendingOn()
  210. {
  211. // void queued ON entries (stale Steam haptics / translated rumble across a reconnect)
  212. uint32_t pm = __get_PRIMASK();
  213. __disable_irq();
  214. for (uint8_t i = g_rqTail; i != g_rqHead; i = rqNext(i)) {
  215. RelayMsg &m = g_rq[i];
  216. if (m.rid == 0x82) {
  217. bool on = false;
  218. for (uint8_t j = 2; j < m.len; j++)
  219. if (m.data[j]) {
  220. on = true;
  221. break;
  222. }
  223. if (on)
  224. m.rid = 0;
  225. }
  226. if (m.rid == 0x80) {
  227. bool on = false;
  228. for (uint8_t j = 0; j < m.len; j++)
  229. if (m.data[j]) {
  230. on = true;
  231. break;
  232. }
  233. if (on)
  234. m.rid = 0;
  235. }
  236. }
  237. __set_PRIMASK(pm);
  238. }
  239. void haptic82HostReport(const uint8_t *p, uint16_t n)
  240. {
  241. if (n < 3)
  242. return;
  243. g_haptic82Ms = millis();
  244. // any haptic activity arms a clear when it next goes idle (kills a latch from this use)
  245. g_hapClearArmed = true;
  246. // Track on/off only. Do NOT synthesize a stop burst: Steam's own stop is forwarded verbatim, and each extra
  247. // 0x82 is a discrete pad click -- the spurious end-of-movement "click"/buzz the real puck never produces.
  248. g_haptic82On = haptic82PayloadOn(p, n);
  249. }
  250. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq)
  251. {
  252. // user rumble-strength scale (percent; 200 = double). Clamp to 16-bit.
  253. if (g_rumbleScale != 100) {
  254. uint32_t l = (uint32_t)lowFreq * g_rumbleScale / 100,
  255. h = (uint32_t)highFreq * g_rumbleScale / 100;
  256. lowFreq = (l > 0xFFFF) ? 0xFFFF : (uint16_t)l;
  257. highFreq = (h > 0xFFFF) ? 0xFFFF : (uint16_t)h;
  258. }
  259. bool on = lowFreq || highFreq;
  260. if (on && haptic82Blocked())
  261. return false; // same settle gate as native Steam haptics
  262. if (!on && !hapticLinkUp())
  263. return false;
  264. // SDL's current Steam/Triton structs define output report 0x80 as:
  265. // type, uint16 intensity, {uint16 speed, int8 gain} left/right.
  266. // We map conventional gamepad low/high-frequency motors to left/right speeds and use max as intensity.
  267. uint16_t intensity = lowFreq > highFreq ? lowFreq : highFreq;
  268. uint8_t p[9];
  269. // haptic_type_t::HAPTIC_TYPE_RUMBLE; 0 is the off/zero report
  270. p[0] = on ? 0x04 : 0x00;
  271. p[1] = (uint8_t)(intensity & 0xFF);
  272. p[2] = (uint8_t)(intensity >> 8);
  273. p[3] = (uint8_t)(lowFreq & 0xFF);
  274. p[4] = (uint8_t)(lowFreq >> 8);
  275. p[5] = 0;
  276. p[6] = (uint8_t)(highFreq & 0xFF);
  277. p[7] = (uint8_t)(highFreq >> 8);
  278. p[8] = 0;
  279. if (!relayHaptic(0x80, p, sizeof p)) // bursts the zero/off report (p[0]==0)
  280. return false;
  281. g_rumble80Ms = millis();
  282. g_rumble80On = on;
  283. return true;
  284. }
  285. // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate).
  286. void rfConnQueueHapticRelay()
  287. {
  288. if (relayPending())
  289. return; // host relays first; injectables wait for an idle cycle
  290. static const uint8_t HAP_ON[3] = { 0x01, 0x01, 0xF7 };
  291. static const uint8_t HAP_OFF[3] = { 0x01, 0x01, 0x00 };
  292. if (g_testHaptic) {
  293. if (relayEnqueue(0x82, HAP_ON, 3))
  294. g_testHaptic--;
  295. } else if (g_hapticStop && !g_xbox) {
  296. if (relayEnqueue(0x82, HAP_OFF, 3))
  297. g_hapticStop--;
  298. }
  299. }
  300. void rfConnFlushRelay(uint8_t ch, uint8_t s1)
  301. {
  302. while (g_rqTail != g_rqHead) {
  303. RelayMsg &m = g_rq[g_rqTail];
  304. // rid 0 = entry voided by hapticCancelPendingOn -> skip, take the next
  305. if (m.rid) {
  306. uint8_t rl = m.len;
  307. if (rl > RELAY_MAXP)
  308. rl = RELAY_MAXP;
  309. // On-air sub-TLV framing. CONFIRMED from real puck<->controller sniffs (puck_sniffer): a command LANDS on
  310. // the controller only with the type-01 + inner-len form E3 [2+rl][01][rid][innerlen][data]; with the legacy
  311. // form E3 [1+rl][05][rid][data] (no inner-len) the controller DISCARDS any 0x87+ command (it reads data[0]
  312. // as the length).
  313. //
  314. // WHITELIST the landing form to EXACTLY the two commands that need it; everything else keeps legacy form:
  315. // * LED brightness -- report 0x87 whose first register byte is 0x2D
  316. // * controller power-off -- report 0x9F ("off!")
  317. // Steam ALSO sends other 0x87 passthrough writes during play -- the haptic-config block (reg 0x30 =
  318. // IMU/subsystem enable, 0x34/0x35 = haptic amplitude). Landing those regresses BOTH: a landed 0x30 FREEZES
  319. // the IMU (gyro stops) and landed 0x34/0x35 = the connect buzz. So keep this list tight.
  320. bool land01 =
  321. (m.rid == 0x9F) ||
  322. (m.rid == 0x87 && rl >= 1 && m.data[0] == 0x2D);
  323. uint8_t p[5 + RELAY_MAXP], plen;
  324. if (land01) {
  325. p[0] = g_relayOp;
  326. p[1] = (uint8_t)(2 + rl);
  327. p[2] = 0x01;
  328. p[3] = m.rid;
  329. p[4] = rl;
  330. memcpy(p + 5, m.data, rl);
  331. plen = (uint8_t)(5 + rl);
  332. } else {
  333. p[0] = g_relayOp;
  334. p[1] = (uint8_t)(1 + rl);
  335. p[2] = 0x05;
  336. p[3] = m.rid;
  337. memcpy(p + 4, m.data, rl);
  338. plen = (uint8_t)(4 + rl);
  339. }
  340. // log what we actually TX to the controller (slot 0xFE) for the buzz hunt
  341. hapLogAdd(0xFE, m.rid, m.data, rl);
  342. // copied out -> release the slot before the TX
  343. g_rqTail = rqNext(g_rqTail);
  344. // s1 carries a PID distinct from the GET poll that follows (caller cycles it), so the controller's ESB
  345. // dedup never mistakes the GET for a retransmit of this relay. 80us RX window: the relay is NO-ACK, so
  346. // don't burn the full ~1.2ms reply window (that halves the poll rate during haptics).
  347. rfConnTx(ch, s1, p, plen, 80);
  348. // ONE relay per poll cycle (matches the real puck's pacing)
  349. return;
  350. }
  351. g_rqTail = rqNext(g_rqTail);
  352. }
  353. }
  354. // Haptic-subsystem RE-INIT: the exact sequence Steam sends (captured on hardware) when it (re)takes control,
  355. // which clears a stuck haptic script on the controller -- a 0x81 reset action plus 0x87 writes to the haptic
  356. // registers (30/07/08/31/52, 18/2e/34/35). Replayed to recover from the latched-buzz the controller falls
  357. // into across a reconnect. Brightness (0x87 reg 2d) is deliberately OMITTED so we don't stomp the LED.
  358. //
  359. // The 0x87 frames below go out on LEGACY (type-05) framing (the whitelist lands 0x87 only for brightness
  360. // 0x2D), so the controller DISCARDS them -- kept verbatim only to match the captured sequence. The effective
  361. // re-init is the three 0x81 frames. (Landing the 0x30 here would freeze the gyro.)
  362. void hapticReinit()
  363. {
  364. static const uint8_t H30[] = { 0x30, 0x00, 0x00, 0x07, 0x07,
  365. 0x00, 0x08, 0x07, 0x00, 0x31,
  366. 0x02, 0x00, 0x52, 0x03, 0x00 };
  367. static const uint8_t H18[] = { 0x18, 0x00, 0x00, 0x2e, 0x00,
  368. 0x00, 0x34, 0xff, 0xff, 0x35,
  369. 0xff, 0xff, 0x34, 0xff, 0xff };
  370. static const uint8_t H35[] = { 0x35, 0xff, 0xff, 0x2e, 0x00, 0x00 };
  371. static const uint8_t T81A[] = {
  372. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  373. };
  374. static const uint8_t T81B[] = {
  375. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  376. };
  377. // reset action (FUN_0001f554) -- Steam sends this first
  378. relayEnqueue(0x81, nullptr, 0);
  379. relayEnqueue(0x87, H30, sizeof H30);
  380. // haptic config (enabled/amplifier/gain): the part that clears a latch
  381. relayEnqueue(0x87, H18, sizeof H18);
  382. relayEnqueue(0x87, H35, sizeof H35);
  383. relayEnqueue(0x81, T81A, sizeof T81A);
  384. relayEnqueue(0x81, T81B, sizeof T81B);
  385. }
  386. void hapticInit()
  387. {
  388. g_rqHead = g_rqTail = 0;
  389. g_haptic82On = false;
  390. g_rumble80On = false;
  391. // boot: block stale Steam 0x82 until link stable
  392. g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS;
  393. // NO fabricated stop burst. USB capture proves Steam only ever sends 0x82 [01 01 f7] pulses -- never a
  394. // zero-gain [01 01 00] "stop". An invented stop frame at boot/connect is a likelier cause of the connect-time
  395. // buzz than a cure. Be a pure pass-through of Steam's haptics, like the real puck.
  396. g_hapticStop = 0;
  397. }
  398. // Arm the post-(re)connect haptic block + schedule the clearing re-init. Called from rf_link the moment a
  399. // controller reply arrives after a gap (the reliable reconnect signal), and as a backup on hapticTask's
  400. // link-up edge. Idempotent -- safe to call repeatedly.
  401. void hapticOnReconnect()
  402. {
  403. // no haptics relayed for the next 3s
  404. g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS;
  405. g_haptic82On = false;
  406. g_rumble80On = false;
  407. // drop any haptic ON queued before the link came up
  408. hapticCancelPendingOn();
  409. // NO re-init injection. The buzz is fixed at its root by bursting the haptic STOP (relayHaptic) so a lost
  410. // NO-ACK stop can't latch the actuator -- so the old connect-time 0x81/0x87 re-init flood is redundant. It
  411. // also injected in EVERY mode and could freeze the controller IMU in the emulated modes, so it's removed
  412. // outright (all modes). We still hold haptics off for the 3s connect settle (block above). Be a pure relay.
  413. uint8_t mk = 2;
  414. // capture marker: RECONNECT detected (block armed, no reinit)
  415. hapLogAdd(0xFD, 0xEE, &mk, 1);
  416. }
  417. void hapticTask()
  418. {
  419. static bool wasHapticLinkUp = false;
  420. bool up = hapticLinkUp();
  421. // Link-edge markers are diagnostic only -- the block/re-init is armed reliably from rf_link
  422. // (hapticOnReconnect) on the first reply after a gap, which fires even when this 300ms edge doesn't.
  423. if (up && !wasHapticLinkUp) {
  424. uint8_t mk = 1;
  425. hapLogAdd(0xFD, 0xEE, &mk, 1);
  426. hapticOnReconnect();
  427. }
  428. if (!up && wasHapticLinkUp) {
  429. uint8_t mk = 0;
  430. hapLogAdd(0xFD, 0xEE, &mk, 1);
  431. }
  432. wasHapticLinkUp = up;
  433. // (Connect-time re-init flood removed -- the stop-burst is the real fix; see hapticOnReconnect.)
  434. // Controller power-off on host SLEEP: send the power-off command (0x9F "off!") the instant the USB bus
  435. // suspends, like the real puck. BUT only when USB power (VBUS) is still present -- i.e. a genuine host sleep,
  436. // NOT a cable unplug. Pulling the dongle ALSO trips the suspend edge (in the brief window it runs on residual
  437. // power), and we must NOT kill the controller then; it should only power off on a shutdown command or a real
  438. // host sleep. VBUSDETECT is 1 while the cable still delivers 5V, 0 once unplugged. wasSusp starts true so a
  439. // boot-into-suspended state never false-fires.
  440. static bool wasSusp = true;
  441. bool susp = USBDevice.suspended();
  442. bool vbus = (NRF_POWER->USBREGSTATUS &
  443. POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
  444. if (susp && !wasSusp && vbus)
  445. hapticSendShutdown();
  446. wasSusp = susp;
  447. // Steam-mode: host went quiet -> mark the 0x82 stream inactive. Do NOT synthesize a stop: trackpad haptics
  448. // are one-shot pulses, so firing a 0x82-zero ~HAPTIC_QUIET_MS after a swipe ends is the extra end-of-movement
  449. // click the real puck doesn't make. Steam forwards its own stop for any sustained haptic.
  450. if (!g_xbox && g_haptic82On &&
  451. millis() - g_haptic82Ms > HAPTIC_QUIET_MS)
  452. g_haptic82On = false;
  453. if (g_rumble80On && millis() - g_rumble80Ms > 2500u)
  454. hapticSteamRumble(0, 0);
  455. // Idle-clear re-init removed: with the stop-burst (relayHaptic) a lost stop can't latch the actuator, so
  456. // there's no during-use latch to clear -- and the old idle-clear injected 0x81/0x87 ~1.2s after every haptic
  457. // burst in ALL modes (a likelier latch CAUSE than cure, and an IMU-freeze risk in emulated modes).
  458. (void)g_hapClearArmed;
  459. }