haptics.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  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. // Post-connect haptic block (see haptics.h): enabled + duration, both persisted and panel-adjustable.
  16. // Off by default -- opt in from the WebUSB panel if a controller's haptics come up degraded after connect.
  17. uint8_t g_hapticBlockOn = 0;
  18. uint16_t g_hapticBlockMs = HAPTIC_BLOCK_MS_DEFAULT;
  19. // Per-slot reconnect block. 0 = idle; non-zero = drop haptics aimed at this slot until millis() catches up.
  20. unsigned long g_hapticBlockUntil[NSLOT] = { 0 };
  21. // Controller power-off. CONFIRMED from a real Windows USB capture of the Valve puck: Steam's "turn off
  22. // controller" is the single feature-0x01 command 0x9F with payload ASCII "off!" (6F 66 66 21). The dongle
  23. // forwards host feature reports verbatim, so the controller acts on report 0x9F directly -- we relay it the
  24. // same way (E3 SET sub-TLV). The wire relay is NO-ACK, so send a small burst: a single lost frame must not
  25. // leave the controller on.
  26. void hapticSendShutdown()
  27. {
  28. static const uint8_t OFF[4] = { 0x6f, 0x66, 0x66, 0x21 }; // "off!"
  29. // broadcast: every connected controller should power off, not just the slot the shutdown was triggered
  30. // from (the trigger can come from a Steam interface, the test button, or host-suspend -- any of which
  31. // "logically" means "all controllers off").
  32. for (uint8_t i = 0; i < HAPTIC_SHUTDOWN_SHOTS; i++)
  33. relayEnqueue(0x9F, OFF, sizeof OFF, 0xFF);
  34. }
  35. // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
  36. static unsigned long g_haptic82Ms = 0;
  37. // a non-zero 0x82 haptic is currently active (awaiting host stop)
  38. static bool g_haptic82On = false;
  39. // millis of last translated host rumble (0x80), per-slot (4 XInput interfaces each have their own stream)
  40. static unsigned long g_rumble80Ms[NSLOT] = { 0 };
  41. // Steam/Triton rumble is latched on until an explicit zero report; tracked per-slot so each controller's
  42. // stuck-rumble watchdog is independent
  43. static bool g_rumble80On[NSLOT] = { false, false, false, false };
  44. // when to fire the next post-reconnect haptic re-init (0 = none scheduled)
  45. static unsigned long g_reinitAt = 0;
  46. // how many re-init shots remain in this connect window
  47. static uint8_t g_reinitLeft = 0;
  48. // haptic activity happened -> arm a clear once it goes idle (catches a
  49. // latch that engaged during/after use, even seconds after connect)
  50. static bool g_hapClearArmed = false;
  51. // ---- relay rings: one per bond slot. Multi-producer (USB ISR + loop-context console/xinput), one consumer
  52. // per slot (rfConnFlushRelay on that slot's poll turn). Producers serialize under PRIMASK.
  53. struct RelayMsg {
  54. uint8_t rid, len;
  55. uint8_t data[RELAY_MAXP];
  56. };
  57. // deep enough to hold a full Steam settings/LED transaction burst without loss
  58. #define RELAY_QLEN 32
  59. static RelayMsg g_rq[NSLOT][RELAY_QLEN];
  60. static volatile uint8_t g_rqHead[NSLOT];
  61. static volatile uint8_t
  62. g_rqTail[NSLOT]; // head=next write, tail=next read; empty when equal
  63. static inline uint8_t rqNext(uint8_t i)
  64. {
  65. return (uint8_t)((i + 1) % RELAY_QLEN);
  66. }
  67. // Counts times a ring-drain loop hit its iteration cap = head/tail were desynced or corrupted (e.g. an
  68. // out-of-range g_rqHead from a stray write/stack overflow). These loops run with IRQs DISABLED, so an
  69. // unterminated one spins forever with interrupts off -> millis()/USB/SOF all freeze and only the hardware
  70. // watchdog recovers (an invisible "watchdog (hang)" -- the live stall monitor can't see it because the SOF
  71. // IRQ is dead). The cap turns that into a logged, recovered event instead of a hang. Surfaced on the panel.
  72. volatile uint16_t g_ringFault = 0;
  73. bool relayPending()
  74. {
  75. // check the current slot's queue; called from rfConnQueueHapticRelay which runs with g_curSlot set
  76. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  77. return g_rqHead[cur] != g_rqTail[cur];
  78. }
  79. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
  80. uint8_t slot)
  81. {
  82. if (plen > RELAY_MAXP)
  83. plen = RELAY_MAXP;
  84. if (slot != 0xFF && slot >= NSLOT)
  85. return false;
  86. uint32_t pm = __get_PRIMASK();
  87. __disable_irq();
  88. // slot=0xFF: broadcast -- enqueue into every slot's ring.
  89. // Full queue: evict the oldest entry, never the newest. Steam bursts end with the commit/stop, so
  90. // dropping the oldest keeps the most-recent (meaningful) frame.
  91. uint8_t s0 = (slot == 0xFF) ? 0 : slot;
  92. uint8_t s1 = (slot == 0xFF) ? NSLOT : slot + 1;
  93. for (uint8_t s = s0; s < s1; s++) {
  94. uint8_t h = g_rqHead[s], nx = rqNext(h);
  95. if (nx == g_rqTail[s])
  96. g_rqTail[s] = rqNext(g_rqTail[s]);
  97. g_rq[s][h].rid = rid;
  98. g_rq[s][h].len = plen;
  99. if (plen)
  100. memcpy(g_rq[s][h].data, payload, plen);
  101. g_rqHead[s] = nx;
  102. }
  103. // Any haptic relay arms the idle-clear (so the during-use latch gets cleared in every mode).
  104. if (rid == 0x82 || rid == 0x80) {
  105. g_haptic82Ms = millis();
  106. g_hapClearArmed = true;
  107. }
  108. __set_PRIMASK(pm);
  109. return true;
  110. }
  111. #if OPK_LOG
  112. // diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
  113. struct HapLog {
  114. uint32_t ms;
  115. uint8_t slot, rid, n, b[16];
  116. }; // 16 payload bytes: capture full 0x87 settings frames
  117. // Big always-on ring: log EVERYTHING from boot (Steam writes, our TX-to-controller, link edges) so a rare
  118. // reconnect-buzz can be caught after the fact -- the trigger happens moments after boot, while this RAM is
  119. // fresh, and we dump it once the panel reconnects. 4096 * 20B ~= 80KB.
  120. #define HAPLOG_N 4096
  121. static HapLog g_hapLog[HAPLOG_N];
  122. static uint16_t g_hapHead = 0;
  123. static uint16_t g_hapTail =
  124. 0; // live/dump drain cursor (loop-context reader; chases g_hapHead)
  125. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n)
  126. {
  127. // Written from the USB SET ISR (handleSet) AND loop context (relay flush / link edges) -> guard g_hapHead.
  128. // Special slot markers for the diagnostic capture: 0xFE = a frame WE transmitted to the controller (TX
  129. // relay); 0xFD = a link state edge (b[0]=1 up, 0 down). Real Steam writes use the interface index (0..3).
  130. uint32_t pm = __get_PRIMASK();
  131. __disable_irq();
  132. HapLog &e = g_hapLog[g_hapHead];
  133. e.ms = millis();
  134. e.slot = slot;
  135. e.rid = rid;
  136. e.n = (uint8_t)(n > 255 ? 255 : n);
  137. for (int i = 0; i < 16; i++)
  138. e.b[i] = (i < (int)n) ? b[i] : 0;
  139. // uint16_t, NOT uint8_t: HAPLOG_N is 4096, so a uint8_t cast truncated the head to 0..255 and only the
  140. // first 256 ring slots were ever used (capture lost ~94% of its depth).
  141. g_hapHead = (uint16_t)((g_hapHead + 1) %
  142. (sizeof g_hapLog / sizeof g_hapLog[0]));
  143. __set_PRIMASK(pm);
  144. }
  145. void hapticDumpLog()
  146. {
  147. const uint16_t N = HAPLOG_N;
  148. uint32_t now = millis();
  149. Serial.printf("# --- capture history (now=%lu, curSlot=%d) ---\n",
  150. (unsigned long)now, g_curSlot);
  151. for (uint16_t i = 0; i < N; i++) {
  152. HapLog &e = g_hapLog[(uint16_t)((g_hapHead + i) % N)];
  153. if (!e.ms && !e.rid)
  154. continue;
  155. Serial.printf("# -%lums if%u rid=%02X n=%u:",
  156. (unsigned long)(now - e.ms), e.slot, e.rid, e.n);
  157. for (uint8_t j = 0; j < 16 && j < e.n; j++)
  158. Serial.printf(" %02X", e.b[j]);
  159. Serial.println();
  160. }
  161. Serial.println("# --- end ---");
  162. }
  163. // ---- drain cursor: stream entries to the WebUSB panel. resetDrain(false)=from "now" (live only);
  164. // resetDrain(true)=from the OLDEST entry (dump the whole ring from boot). pull skips empty slots. ----
  165. void hapLogResetDrain(bool fromBoot)
  166. {
  167. // oldest slot (the next-to-overwrite holds it)
  168. g_hapTail = fromBoot ? (uint16_t)((g_hapHead + 1) % HAPLOG_N) :
  169. // "now"
  170. g_hapHead;
  171. }
  172. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  173. uint8_t bytes16[16])
  174. {
  175. while (g_hapTail != g_hapHead) {
  176. HapLog &e = g_hapLog[g_hapTail];
  177. g_hapTail = (uint16_t)((g_hapTail + 1) % HAPLOG_N);
  178. if (!e.ms && !e.rid)
  179. continue; // skip empty slot (ring not yet full)
  180. // Return the ABSOLUTE log time (millis since boot), not "age now". The panel drains in batches 100ms
  181. // apart, so an age computed here would jump between batches; the panel computes age vs the newest entry.
  182. *logMs = e.ms;
  183. *slot = e.slot;
  184. *rid = e.rid;
  185. *n = (e.n > 16) ? 16 : e.n;
  186. memcpy(bytes16, e.b, 16);
  187. return true;
  188. }
  189. return false;
  190. }
  191. #endif // OPK_LOG
  192. // Per-slot helpers. slot==-1 (default) checks the CURRENT poll slot (g_curSlot), used by flush-time code
  193. // paths that don't have a slot in hand. Callers with a real slot pass it in.
  194. bool hapticLinkUp(int slot)
  195. {
  196. int s = (slot >= 0) ? slot : g_curSlot;
  197. if (s < 0 || s >= NSLOT)
  198. return false;
  199. return g_slot[s].used && (millis() - g_connReplyMs[s]) < 300;
  200. }
  201. bool haptic82Blocked(int slot)
  202. {
  203. int s = (slot >= 0) ? slot : g_curSlot;
  204. if (s < 0 || s >= NSLOT)
  205. return true;
  206. return !hapticLinkUp(s) ||
  207. (g_hapticBlockUntil[s] &&
  208. (int32_t)(millis() - g_hapticBlockUntil[s]) < 0);
  209. }
  210. // "Is haptics from this USB interface's slot allowed through?" -- the slot must be currently connected.
  211. bool hapticRelaySlotOk(int slot)
  212. {
  213. return slot >= 0 && slot < NSLOT && hapticLinkUp(slot);
  214. }
  215. static bool haptic82PayloadOn(const uint8_t *p, uint16_t n)
  216. {
  217. if (n < 3)
  218. return false;
  219. for (uint16_t i = 2; i < n; i++)
  220. if (p[i])
  221. // observed form is [01 01 gain], but treat any trailing non-zero as active
  222. return true;
  223. return false;
  224. }
  225. static void hapticCancelPendingOn()
  226. {
  227. // void queued ON entries across all slot queues (stale haptics / rumble across a reconnect)
  228. uint32_t pm = __get_PRIMASK();
  229. __disable_irq();
  230. for (int s = 0; s < NSLOT; s++) {
  231. uint8_t guard = RELAY_QLEN + 1;
  232. for (uint8_t i = g_rqTail[s]; i != g_rqHead[s]; i = rqNext(i)) {
  233. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off
  234. g_rqHead[s] = g_rqTail[s] = 0;
  235. g_ringFault++;
  236. break;
  237. }
  238. RelayMsg &m = g_rq[s][i];
  239. if (m.rid == 0x82) {
  240. bool on = false;
  241. for (uint8_t j = 2; j < m.len; j++)
  242. if (m.data[j]) {
  243. on = true;
  244. break;
  245. }
  246. if (on)
  247. m.rid = 0;
  248. }
  249. if (m.rid == 0x80) {
  250. bool on = false;
  251. for (uint8_t j = 0; j < m.len; j++)
  252. if (m.data[j]) {
  253. on = true;
  254. break;
  255. }
  256. if (on)
  257. m.rid = 0;
  258. }
  259. }
  260. }
  261. __set_PRIMASK(pm);
  262. }
  263. void haptic82HostReport(const uint8_t *p, uint16_t n)
  264. {
  265. if (n < 3)
  266. return;
  267. g_haptic82Ms = millis();
  268. // any haptic activity arms a clear when it next goes idle (kills a latch from this use)
  269. g_hapClearArmed = true;
  270. // Track on/off only. Do NOT synthesize a stop burst: Steam's own stop is forwarded verbatim, and each extra
  271. // 0x82 is a discrete pad click -- the spurious end-of-movement "click"/buzz the real puck never produces.
  272. g_haptic82On = haptic82PayloadOn(p, n);
  273. }
  274. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
  275. {
  276. if (slot >= NSLOT)
  277. return false;
  278. // user rumble-strength scale (percent; 200 = double). Clamp to 16-bit.
  279. if (g_rumbleScale != 100) {
  280. uint32_t l = (uint32_t)lowFreq * g_rumbleScale / 100,
  281. h = (uint32_t)highFreq * g_rumbleScale / 100;
  282. lowFreq = (l > 0xFFFF) ? 0xFFFF : (uint16_t)l;
  283. highFreq = (h > 0xFFFF) ? 0xFFFF : (uint16_t)h;
  284. }
  285. bool on = lowFreq || highFreq;
  286. // Per-slot settle gate (the per-slot reconnect block + link-up check). 0x82 haptics in Steam mode use the
  287. // same gate; for XInput, the host only sends a stream while a controller is connected, so this also doubles
  288. // as "no controller here, no relay".
  289. if (on && haptic82Blocked(slot))
  290. return false;
  291. if (!on && !hapticLinkUp(slot))
  292. return false;
  293. // SDL's current Steam/Triton structs define output report 0x80 as:
  294. // type, uint16 intensity, {uint16 speed, int8 gain} left/right.
  295. // We map conventional gamepad low/high-frequency motors to left/right speeds and use max as intensity.
  296. uint16_t intensity = lowFreq > highFreq ? lowFreq : highFreq;
  297. uint8_t p[9];
  298. // haptic_type_t::HAPTIC_TYPE_RUMBLE; 0 is the off/zero report
  299. p[0] = on ? 0x04 : 0x00;
  300. p[1] = (uint8_t)(intensity & 0xFF);
  301. p[2] = (uint8_t)(intensity >> 8);
  302. p[3] = (uint8_t)(lowFreq & 0xFF);
  303. p[4] = (uint8_t)(lowFreq >> 8);
  304. p[5] = 0;
  305. p[6] = (uint8_t)(highFreq & 0xFF);
  306. p[7] = (uint8_t)(highFreq >> 8);
  307. p[8] = 0;
  308. if (!relayEnqueue(0x80, p, sizeof p, slot))
  309. return false;
  310. g_rumble80Ms[slot] = millis();
  311. g_rumble80On[slot] = on;
  312. return true;
  313. }
  314. // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate). Test
  315. // haptics broadcast to all connected slots (slot 0xFF); the stop frame is broadcast too (a stuck latch can
  316. // affect any controller, and the haptic-engine clear-re-init is settings-only so it's harmless on healthy
  317. // ones).
  318. // Stability-test keepalive: while g_stabTest, buzz every connected controller for ~150ms once per 10s so an
  319. // unattended uptime measurement isn't ended by a controller idle-sleeping. Toggled by WebUSB cmd 0x0F; the
  320. // panel times uptime-until-reset. Reboots clear g_stabTest (the panel re-arms it on reconnect).
  321. bool g_stabTest = false;
  322. void hapticStabTask()
  323. {
  324. if (!g_stabTest)
  325. return;
  326. static const uint8_t on[3] = { 0x01, 0x01, 0xF7 };
  327. static const uint8_t off[3] = { 0x01, 0x01, 0x00 };
  328. static unsigned long lastOn = 0, offAt = 0;
  329. unsigned long now = millis();
  330. if (lastOn == 0 || (uint32_t)(now - lastOn) >= 10000u) {
  331. lastOn = now;
  332. relayEnqueue(0x82, on, 3, 0xFF); // broadcast buzz-on to all slots
  333. offAt = now + 150;
  334. }
  335. if (offAt && (int32_t)(now - offAt) >= 0) {
  336. relayEnqueue(0x82, off, 3, 0xFF);
  337. offAt = 0;
  338. }
  339. }
  340. void rfConnQueueHapticRelay()
  341. {
  342. if (relayPending())
  343. return; // host relays first; injectables wait for an idle cycle
  344. static const uint8_t HAP_ON[3] = { 0x01, 0x01, 0xF7 };
  345. static const uint8_t HAP_OFF[3] = { 0x01, 0x01, 0x00 };
  346. if (g_testHaptic) {
  347. if (relayEnqueue(0x82, HAP_ON, 3, 0xFF))
  348. g_testHaptic--;
  349. } else if (g_hapticStop && !g_xbox) {
  350. if (relayEnqueue(0x82, HAP_OFF, 3, 0xFF))
  351. g_hapticStop--;
  352. }
  353. }
  354. // rfConnFlushRelay(ch, s1): drain one entry from the current slot's relay queue and TX it. Each slot's queue
  355. // is independent, so each controller only sees its own commands. With N connected slots the per-slot relay
  356. // rate is 1/N of the per-cycle rate; sustained buzz streams are still 1 packet/cycle/slot.
  357. bool rfConnFlushRelay(uint8_t ch, uint8_t s1)
  358. {
  359. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  360. // Snapshot one entry under a short critical section. relayEnqueue() (the producer) runs on the
  361. // high-priority usbd task and, when the ring is full, evicts the oldest by advancing g_rqTail itself --
  362. // the same variable this consumer reads/advances. Reading the entry while a producer could overwrite it
  363. // is a torn read (and dual-writing g_rqTail desyncs head/tail), so copy the entry out and consume the
  364. // slot atomically here, then do the (slow) RF TX on the copy with interrupts enabled.
  365. RelayMsg msg;
  366. bool have = false;
  367. uint32_t pm = __get_PRIMASK();
  368. __disable_irq();
  369. uint8_t guard = RELAY_QLEN + 1;
  370. while (g_rqTail[cur] != g_rqHead[cur]) {
  371. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off (watchdog-hang class)
  372. g_rqHead[cur] = g_rqTail[cur] = 0;
  373. g_ringFault++;
  374. break;
  375. }
  376. RelayMsg &m = g_rq[cur][g_rqTail[cur]];
  377. g_rqTail[cur] = rqNext(g_rqTail[cur]); // consume the slot
  378. // rid 0 = entry voided by hapticCancelPendingOn -> skip
  379. if (m.rid) {
  380. msg = m; // copy out before the producer can reuse the slot
  381. have = true;
  382. break;
  383. }
  384. }
  385. __set_PRIMASK(pm);
  386. if (have) {
  387. RelayMsg &m = msg;
  388. {
  389. uint8_t rl = m.len;
  390. if (rl > RELAY_MAXP)
  391. rl = RELAY_MAXP;
  392. // On-air sub-TLV framing. CONFIRMED from real puck<->controller sniffs: a command LANDS on
  393. // the controller only with the type-01 + inner-len form E3 [2+rl][01][rid][innerlen][data];
  394. // the legacy form E3 [1+rl][05][rid][data] makes the controller DISCARD any 0x87+ command.
  395. //
  396. // Whitelist type-01 to only two commands: LED brightness (0x87 reg 0x2D) and power-off
  397. // (0x9F). Other 0x87 writes (e.g. reg 0x30 IMU enable, 0x34/0x35 haptic amplitude) must
  398. // stay on legacy form -- landing 0x30 freezes the gyro; landing 0x34/0x35 causes the buzz.
  399. bool land01 =
  400. (m.rid == 0x9F) ||
  401. (m.rid == 0x87 && rl >= 1 && m.data[0] == 0x2D);
  402. uint8_t p[5 + RELAY_MAXP], plen;
  403. if (land01) {
  404. p[0] = g_relayOp;
  405. p[1] = (uint8_t)(2 + rl);
  406. p[2] = 0x01;
  407. p[3] = m.rid;
  408. p[4] = rl;
  409. memcpy(p + 5, m.data, rl);
  410. plen = (uint8_t)(5 + rl);
  411. } else {
  412. p[0] = g_relayOp;
  413. p[1] = (uint8_t)(1 + rl);
  414. p[2] = 0x05;
  415. p[3] = m.rid;
  416. memcpy(p + 4, m.data, rl);
  417. plen = (uint8_t)(4 + rl);
  418. }
  419. hapLogAdd(0xFE, m.rid, m.data, rl);
  420. // slot was already consumed under the critical section above.
  421. // s1 carries a PID distinct from the GET poll (caller cycles it) so the controller's ESB
  422. // dedup never treats the GET as a retransmit of this relay. 80us RX: relay is NO-ACK.
  423. rfConnTx(ch, s1, p, plen,
  424. 80); // one relay per poll cycle
  425. }
  426. }
  427. return have; // true = a relay frame went out this cycle (steals a reply window from the E3 poll)
  428. }
  429. // Haptic-subsystem re-init: the captured sequence Steam sends when it (re)takes control (0x81 reset + 0x87
  430. // register writes). Brightness (reg 0x2D) is omitted to avoid stomping the LED. The 0x87 frames go out on
  431. // legacy framing so the controller discards them; the three 0x81 frames are the effective reset. slot=0xFF
  432. // broadcasts to all connected controllers (the re-init is settings-only and harmless on healthy controllers).
  433. // One-shot SET_SETTINGS (0x87) write of the controller's GLOBAL trackpad-haptics enable. The controller plays
  434. // touchpad haptic sequences autonomously (haptics-sequencer-touchpad) gated by settings/haptics/enabled; in the
  435. // emulated (non-Steam) modes the host never sends SC haptic reports, so the only way to silence the pad buzz is
  436. // to write this setting. Wire format [id][val u16 LE] -- confirmed from the controller's SET_SETTINGS handler
  437. // (decomp FUN_0001f61c; id 0x30 == settings/sensors/imu/mode is the cross-check anchor in the same id space).
  438. // The exact id for settings/haptics/enabled needs a one-time `scmd labels` HW read (candidates from the captured
  439. // reinit: 0x18 / 0x2e / 0x34 / 0x35). Until it is set, SETTING_HAPTICS_ENABLED stays 0xFF and this is inert.
  440. #define SETTING_HAPTICS_ENABLED 0xFF
  441. void hapticSetPadEnabled(uint8_t slot, bool on)
  442. {
  443. #if SETTING_HAPTICS_ENABLED != 0xFF
  444. uint8_t pl[3] = { SETTING_HAPTICS_ENABLED, (uint8_t)(on ? 1 : 0), 0 };
  445. relayEnqueue(0x87, pl, 3, slot);
  446. #else
  447. (void)slot;
  448. (void)on;
  449. #endif
  450. }
  451. void hapticReinit(uint8_t slot)
  452. {
  453. static const uint8_t H30[] = { 0x30, 0x00, 0x00, 0x07, 0x07,
  454. 0x00, 0x08, 0x07, 0x00, 0x31,
  455. 0x02, 0x00, 0x52, 0x03, 0x00 };
  456. static const uint8_t H18[] = { 0x18, 0x00, 0x00, 0x2e, 0x00,
  457. 0x00, 0x34, 0xff, 0xff, 0x35,
  458. 0xff, 0xff, 0x34, 0xff, 0xff };
  459. static const uint8_t H35[] = { 0x35, 0xff, 0xff, 0x2e, 0x00, 0x00 };
  460. static const uint8_t T81A[] = {
  461. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  462. };
  463. static const uint8_t T81B[] = {
  464. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  465. };
  466. // reset action (FUN_0001f554) -- Steam sends this first
  467. relayEnqueue(0x81, nullptr, 0, slot);
  468. relayEnqueue(0x87, H30, sizeof H30, slot);
  469. // haptic config (enabled/amplifier/gain): the part that clears a latch
  470. relayEnqueue(0x87, H18, sizeof H18, slot);
  471. relayEnqueue(0x87, H35, sizeof H35, slot);
  472. relayEnqueue(0x81, T81A, sizeof T81A, slot);
  473. relayEnqueue(0x81, T81B, sizeof T81B, slot);
  474. // Re-apply the active emulated type's trackpad-haptics preference last (after the haptic-config writes
  475. // above, which would otherwise re-enable it). Default-on types send "enable"; Switch (padHaptics=0)
  476. // disables. Inert until the setting id is captured.
  477. hapticSetPadEnabled(slot, g_padHaptics != 0);
  478. // Apply the configured LED brightness for the active emulated type. Steam sets the
  479. // brightness each session; emulated modes never do, so the controller comes up at
  480. // full brightness. 0 = no override (preserve controller default).
  481. if (g_etype < ET_COUNT && g_ledBright > 0) {
  482. uint8_t pl[3] = { 0x2D, g_ledBright, 0x00 };
  483. relayEnqueue(0x87, pl, sizeof pl, slot);
  484. }
  485. }
  486. void hapticInit()
  487. {
  488. g_haptic82On = false;
  489. g_hapticStop = 0;
  490. for (int s = 0; s < NSLOT; s++) {
  491. g_rqHead[s] = g_rqTail[s] = 0;
  492. g_rumble80On[s] = false;
  493. g_rumble80Ms[s] = 0;
  494. // post-connect haptic block is permanently disabled (not armed, not configurable)
  495. g_hapticBlockUntil[s] = 0;
  496. }
  497. }
  498. // Arm the post-(re)connect haptic block + schedule the clearing re-init. Called on the reliable first-reply
  499. // signal from rf_link and again from hapticTask's link-up edge detector. Per-slot: only the reconnected
  500. // slot is blocked; the broadcast re-init covers all slots.
  501. void hapticOnReconnect(int slot)
  502. {
  503. if (slot < 0 || slot >= NSLOT)
  504. return;
  505. // post-connect haptic block is permanently disabled -- relay haptics immediately on (re)connect
  506. g_hapticBlockUntil[slot] = 0;
  507. g_haptic82On = false;
  508. g_rumble80On[slot] = false;
  509. g_rumble80Ms[slot] = 0;
  510. hapticCancelPendingOn();
  511. // Re-init repeatedly across the settle window: the connect buzz engages early (during the block,
  512. // controller-internal), so a single late shot misses it.
  513. g_reinitAt = millis() + 200u;
  514. g_reinitLeft = HAPTIC_REINIT_SHOTS;
  515. uint8_t mk = 2;
  516. hapLogAdd(0xFD, 0xEE, &mk, 1);
  517. }
  518. void hapticTask()
  519. {
  520. // Per-slot link-edge detect (backup for hapticOnReconnect in rf_link).
  521. static bool wasHapticLinkUp[NSLOT] = { 0 };
  522. for (int s = 0; s < NSLOT; s++) {
  523. if (!g_slot[s].used)
  524. continue;
  525. bool up = hapticLinkUp(s);
  526. if (up && !wasHapticLinkUp[s]) {
  527. uint8_t mk = 1;
  528. hapLogAdd(0xFD, 0xEE, &mk, 1);
  529. hapticOnReconnect(s);
  530. }
  531. if (!up && wasHapticLinkUp[s]) {
  532. uint8_t mk = 0;
  533. hapLogAdd(0xFD, 0xEE, &mk, 1);
  534. }
  535. wasHapticLinkUp[s] = up;
  536. }
  537. if (g_reinitAt && anySlotLinkUp() &&
  538. (int32_t)(millis() - g_reinitAt) >= 0) {
  539. hapticReinit();
  540. g_reinitAt = (g_reinitLeft && --g_reinitLeft) ?
  541. (millis() + HAPTIC_REINIT_GAP_MS) :
  542. 0;
  543. }
  544. // Power-off on host sleep: only when VBUS is present (genuine sleep, not a cable unplug which also
  545. // trips the suspend edge briefly) AND the suspend has PERSISTED >= SUSPEND_OFF_MS. A brief USB
  546. // selective-suspend (host idle power-management) resumes in <1s; firing the power-off on its edge
  547. // powered the controllers off ourselves -> random drop/reconnect churn. Arm only on a genuine
  548. // resume->suspend edge (wasSusp=true at boot suppresses a false fire on boot-into-suspended).
  549. static bool wasSusp = true;
  550. static unsigned long suspSinceMs = 0;
  551. static bool suspArmed = false;
  552. bool susp = USBDevice.suspended();
  553. bool vbus = (NRF_POWER->USBREGSTATUS &
  554. POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
  555. if (susp && !wasSusp) {
  556. suspSinceMs = millis();
  557. suspArmed = true;
  558. }
  559. if (!susp)
  560. suspArmed = false;
  561. if (suspArmed && vbus && (millis() - suspSinceMs) >= SUSPEND_OFF_MS) {
  562. hapticSendShutdown();
  563. suspArmed = false; // fire once per suspend
  564. }
  565. wasSusp = susp;
  566. // Steam-mode quiet timeout: mark 0x82 stream inactive. No synthesized stop -- Steam forwards its own.
  567. if (!g_xbox && g_haptic82On &&
  568. millis() - g_haptic82Ms > HAPTIC_QUIET_MS)
  569. g_haptic82On = false;
  570. // Per-slot stuck-rumble watchdog: force zero after 2.5s without a refresh.
  571. for (int s = 0; s < NSLOT; s++) {
  572. if (g_rumble80On[s] && millis() - g_rumble80Ms[s] > 2500u)
  573. hapticSteamRumble(0, 0, (uint8_t)s);
  574. }
  575. // Idle-clear: after haptic activity goes quiet, re-init once to clear any latch left behind.
  576. if (g_hapClearArmed &&
  577. (millis() - g_haptic82Ms) > HAPTIC_CLEAR_IDLE_MS) {
  578. g_hapClearArmed = false;
  579. hapticReinit();
  580. }
  581. }