haptics.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  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" // puckLizardActive() -- gate the lizard-suppression keepalive
  7. #include "fault_diag.h" // faultDiagTrace() -- flight recorder
  8. // USBDevice.suspended() -> autonomous controller power-off on host sleep
  9. #include <Adafruit_TinyUSB.h>
  10. #include <Arduino.h>
  11. #include <string.h>
  12. uint8_t g_relayOp = 0xE3; // E3 poll
  13. // relay sub-TLV TYPE byte. Vestigial: rfConnFlushRelay derives the on-air type from the report id (0x05 for
  14. // actuators/haptics <0x87, 0x01 for config/settings/LED >=0x87). Still exposed over WebUSB diagnostics.
  15. uint8_t g_relaySub = 0x05;
  16. volatile uint8_t g_testHaptic = 0;
  17. volatile uint8_t g_hapticStop = 0;
  18. // Post-connect haptic block (see haptics.h): enabled + duration, both persisted and panel-adjustable.
  19. // Off by default -- opt in from the WebUSB panel if a controller's haptics come up degraded after connect.
  20. uint8_t g_hapticBlockOn = 0;
  21. uint16_t g_hapticBlockMs = HAPTIC_BLOCK_MS_DEFAULT;
  22. // id9 steering (see hapticTask): ON by default. Holds id9=0 while Steam is DRIVING (autonomous engine off,
  23. // Steam owns haptics, no reconnect buzz-latch) and lands id9=1 when lizard is PRESENTING (pure MODE_LIZARD
  24. // or Steam-closed fallback) so the autonomous pad layer -- the trackpad-tick source -- is on.
  25. // Persisted; console 'u' toggles for A/B.
  26. uint8_t g_lizKeep = 1;
  27. // EXPERIMENT (console "L87" toggles, persisted): land ALL relayed 0x87 SET_SETTINGS via type-01 framing so
  28. // Steam's haptic/amp/IMU config actually reaches the controller (verbatim relay = real-puck behavior),
  29. // instead of the whitelist discarding everything but LED(0x2D)/id9(0x09). Hypothesis: the buzz is the
  30. // controller's haptic engine left MISCONFIGURED because Steam's 0x87 config is discarded; landing it should
  31. // configure the engine correctly and stop the buzz. Default OFF (keeps the safe whitelist) because landing
  32. // reg 0x30 was once blamed for freezing the gyro and 0x34/0x35 for the buzz -- but those were OpenPuck's own
  33. // bad hapticReinit values, so Steam's real values may be fine. Flip it on hardware to test; flip back if the
  34. // gyro freezes.
  35. uint8_t g_landAll87 = 0;
  36. // Land Steam's amp/haptic-config 0x87 blocks (regs 0x18/0x2E/0x34/0x35, NOT gyro 0x30) so the controller's
  37. // amplifier is configured and haptics play as clean ticks instead of a default-amp buzz. On by default;
  38. // console "AMP" toggles for A/B. See the land01 whitelist in rfConnFlushRelay.
  39. uint8_t g_landAmp = 1;
  40. // Master enable for the puck->controller haptic RELAY (Steam OUTPUT reports 0x80-0x86, incl. the trackpad
  41. // texture-feedback stream Steam pushes WHILE you drag). Each relayed frame is an extra TX that precedes the
  42. // E3 poll and steals its reply window, and the controller must stop to process it -- both can depress the
  43. // input rate exactly during a drag. On by default; console "HR" toggles it so the drag-smoothness cost of
  44. // haptics can be isolated on hardware (drag with it OFF vs ON). OFF only affects Steam-driven rumble/pad
  45. // feedback; it does NOT touch settings/config (0x87) or power-off (0x9F) relays.
  46. bool g_hapticRelay = true;
  47. // Per-slot reconnect block. 0 = idle; non-zero = drop haptics aimed at this slot until millis() catches up.
  48. unsigned long g_hapticBlockUntil[NSLOT] = { 0 };
  49. // Controller power-off. CONFIRMED from a real Windows USB capture of the Valve puck: Steam's "turn off
  50. // controller" is the single feature-0x01 command 0x9F with payload ASCII "off!" (6F 66 66 21). The dongle
  51. // forwards host feature reports verbatim, so the controller acts on report 0x9F directly -- we relay it the
  52. // same way (E3 SET sub-TLV). The wire relay is NO-ACK, so send a small burst: a single lost frame must not
  53. // leave the controller on.
  54. void hapticSendShutdown(uint8_t slot)
  55. {
  56. static const uint8_t OFF[4] = { 0x6f, 0x66, 0x66, 0x21 }; // "off!"
  57. // slot-targeted: Steam's 0x9F arrives on ONE controller's interface and must power off only that
  58. // controller (broadcasting it killed every connected controller when the user turned off one). The
  59. // broadcast default (0xFF) remains for the triggers that logically mean "all off": host suspend and
  60. // the panel/test power-off button.
  61. faultDiagTrace(FR_OFF, slot);
  62. for (uint8_t i = 0; i < HAPTIC_SHUTDOWN_SHOTS; i++)
  63. relayEnqueue(0x9F, OFF, sizeof OFF, slot);
  64. }
  65. // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
  66. static unsigned long g_haptic82Ms = 0;
  67. // a non-zero 0x82 haptic is currently active (awaiting host stop)
  68. static bool g_haptic82On = false;
  69. // millis of last translated host rumble (0x80), per-slot (4 XInput interfaces each have their own stream)
  70. static unsigned long g_rumble80Ms[NSLOT] = { 0 };
  71. // Steam/Triton rumble is latched on until an explicit zero report; tracked per-slot so each controller's
  72. // stuck-rumble watchdog is independent
  73. static bool g_rumble80On[NSLOT] = { false, false, false, false };
  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. uint8_t data[RELAY_MAXP];
  79. };
  80. // deep enough to hold a full Steam settings/LED transaction burst without loss
  81. #define RELAY_QLEN 32
  82. static RelayMsg g_rq[NSLOT][RELAY_QLEN];
  83. static volatile uint8_t g_rqHead[NSLOT];
  84. static volatile uint8_t
  85. g_rqTail[NSLOT]; // head=next write, tail=next read; empty when equal
  86. static inline uint8_t rqNext(uint8_t i)
  87. {
  88. return (uint8_t)((i + 1) % RELAY_QLEN);
  89. }
  90. // Counts times a ring-drain loop hit its iteration cap = head/tail were desynced or corrupted (e.g. an
  91. // out-of-range g_rqHead from a stray write/stack overflow). These loops run with IRQs DISABLED, so an
  92. // unterminated one spins forever with interrupts off -> millis()/USB/SOF all freeze and only the hardware
  93. // watchdog recovers (an invisible "watchdog (hang)" -- the live stall monitor can't see it because the SOF
  94. // IRQ is dead). The cap turns that into a logged, recovered event instead of a hang. Surfaced on the panel.
  95. volatile uint16_t g_ringFault = 0;
  96. bool relayPending()
  97. {
  98. // check the current slot's queue; called from rfConnQueueHapticRelay which runs with g_curSlot set
  99. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  100. return g_rqHead[cur] != g_rqTail[cur];
  101. }
  102. bool relayEnqueue(uint8_t rid, const uint8_t *payload, uint8_t plen,
  103. uint8_t slot)
  104. {
  105. if (plen > RELAY_MAXP)
  106. plen = RELAY_MAXP;
  107. if (slot != 0xFF && slot >= NSLOT)
  108. return false;
  109. uint32_t pm = __get_PRIMASK();
  110. __disable_irq();
  111. // slot=0xFF: broadcast -- enqueue into every slot's ring.
  112. // Full queue: evict the oldest entry, never the newest. Steam bursts end with the commit/stop, so
  113. // dropping the oldest keeps the most-recent (meaningful) frame.
  114. uint8_t s0 = (slot == 0xFF) ? 0 : slot;
  115. uint8_t s1 = (slot == 0xFF) ? NSLOT : slot + 1;
  116. for (uint8_t s = s0; s < s1; s++) {
  117. uint8_t h = g_rqHead[s], nx = rqNext(h);
  118. if (nx == g_rqTail[s])
  119. g_rqTail[s] = rqNext(g_rqTail[s]);
  120. g_rq[s][h].rid = rid;
  121. g_rq[s][h].len = plen;
  122. if (plen)
  123. memcpy(g_rq[s][h].data, payload, plen);
  124. g_rqHead[s] = nx;
  125. }
  126. // Track last-haptic time for the Steam-mode quiet timeout (marks the 0x82 stream inactive after silence).
  127. if (rid == 0x82 || rid == 0x80)
  128. g_haptic82Ms = millis();
  129. __set_PRIMASK(pm);
  130. faultDiagTrace(FR_RELAY, (uint16_t)((slot << 8) | rid));
  131. return true;
  132. }
  133. #if OPK_LOG
  134. // diagnostic capture: a ring of the last OUTPUT reports Steam sends (rid/slot/bytes/ms), dumped with 'H'.
  135. struct HapLog {
  136. uint32_t ms;
  137. uint8_t slot, rid, n, b[16];
  138. }; // 16 payload bytes: capture full 0x87 settings frames
  139. // Big always-on ring: log EVERYTHING from boot (Steam writes, our TX-to-controller, link edges) so a rare
  140. // reconnect-buzz can be caught after the fact -- the trigger happens moments after boot, while this RAM is
  141. // fresh, and we dump it once the panel reconnects. 4096 * 20B ~= 80KB.
  142. #define HAPLOG_N 4096
  143. static HapLog g_hapLog[HAPLOG_N];
  144. static uint16_t g_hapHead = 0;
  145. static uint16_t g_hapTail =
  146. 0; // live/dump drain cursor (loop-context reader; chases g_hapHead)
  147. void hapLogAdd(uint8_t slot, uint8_t rid, const uint8_t *b, uint16_t n)
  148. {
  149. // Written from the USB SET ISR (handleSet) AND loop context (relay flush / link edges) -> guard g_hapHead.
  150. // Special slot markers for the diagnostic capture: 0xFE = a frame WE transmitted to the controller (TX
  151. // relay); 0xFD = a link state edge (b[0]=1 up, 0 down). Real Steam writes use the interface index (0..3).
  152. uint32_t pm = __get_PRIMASK();
  153. __disable_irq();
  154. HapLog &e = g_hapLog[g_hapHead];
  155. e.ms = millis();
  156. e.slot = slot;
  157. e.rid = rid;
  158. e.n = (uint8_t)(n > 255 ? 255 : n);
  159. for (int i = 0; i < 16; i++)
  160. e.b[i] = (i < (int)n) ? b[i] : 0;
  161. // uint16_t, NOT uint8_t: HAPLOG_N is 4096, so a uint8_t cast truncated the head to 0..255 and only the
  162. // first 256 ring slots were ever used (capture lost ~94% of its depth).
  163. g_hapHead = (uint16_t)((g_hapHead + 1) %
  164. (sizeof g_hapLog / sizeof g_hapLog[0]));
  165. __set_PRIMASK(pm);
  166. }
  167. void hapticDumpLog()
  168. {
  169. const uint16_t N = HAPLOG_N;
  170. uint32_t now = millis();
  171. Serial.printf("# --- capture history (now=%lu, curSlot=%d) ---\n",
  172. (unsigned long)now, g_curSlot);
  173. for (uint16_t i = 0; i < N; i++) {
  174. HapLog &e = g_hapLog[(uint16_t)((g_hapHead + i) % N)];
  175. if (!e.ms && !e.rid)
  176. continue;
  177. Serial.printf("# -%lums if%u rid=%02X n=%u:",
  178. (unsigned long)(now - e.ms), e.slot, e.rid, e.n);
  179. for (uint8_t j = 0; j < 16 && j < e.n; j++)
  180. Serial.printf(" %02X", e.b[j]);
  181. Serial.println();
  182. }
  183. Serial.println("# --- end ---");
  184. }
  185. // ---- drain cursor: stream entries to the WebUSB panel. resetDrain(false)=from "now" (live only);
  186. // resetDrain(true)=from the OLDEST entry (dump the whole ring from boot). pull skips empty slots. ----
  187. void hapLogResetDrain(bool fromBoot)
  188. {
  189. // oldest slot (the next-to-overwrite holds it)
  190. g_hapTail = fromBoot ? (uint16_t)((g_hapHead + 1) % HAPLOG_N) :
  191. // "now"
  192. g_hapHead;
  193. }
  194. bool hapLogPull(uint32_t *logMs, uint8_t *slot, uint8_t *rid, uint8_t *n,
  195. uint8_t bytes16[16])
  196. {
  197. while (g_hapTail != g_hapHead) {
  198. HapLog &e = g_hapLog[g_hapTail];
  199. g_hapTail = (uint16_t)((g_hapTail + 1) % HAPLOG_N);
  200. if (!e.ms && !e.rid)
  201. continue; // skip empty slot (ring not yet full)
  202. // Return the ABSOLUTE log time (millis since boot), not "age now". The panel drains in batches 100ms
  203. // apart, so an age computed here would jump between batches; the panel computes age vs the newest entry.
  204. *logMs = e.ms;
  205. *slot = e.slot;
  206. *rid = e.rid;
  207. *n = (e.n > 16) ? 16 : e.n;
  208. memcpy(bytes16, e.b, 16);
  209. return true;
  210. }
  211. return false;
  212. }
  213. #endif // OPK_LOG
  214. // Per-slot helpers. slot==-1 (default) checks the CURRENT poll slot (g_curSlot), used by flush-time code
  215. // paths that don't have a slot in hand. Callers with a real slot pass it in.
  216. bool hapticLinkUp(int slot)
  217. {
  218. int s = (slot >= 0) ? slot : g_curSlot;
  219. if (s < 0 || s >= NSLOT)
  220. return false;
  221. return g_slot[s].used && (millis() - g_connReplyMs[s]) < 300;
  222. }
  223. bool haptic82Blocked(int slot)
  224. {
  225. int s = (slot >= 0) ? slot : g_curSlot;
  226. if (s < 0 || s >= NSLOT)
  227. return true;
  228. return !hapticLinkUp(s) ||
  229. (g_hapticBlockUntil[s] &&
  230. (int32_t)(millis() - g_hapticBlockUntil[s]) < 0);
  231. }
  232. // "Is haptics from this USB interface's slot allowed through?" -- the slot must be currently connected.
  233. bool hapticRelaySlotOk(int slot)
  234. {
  235. return slot >= 0 && slot < NSLOT && hapticLinkUp(slot);
  236. }
  237. static bool haptic82PayloadOn(const uint8_t *p, uint16_t n)
  238. {
  239. if (n < 3)
  240. return false;
  241. for (uint16_t i = 2; i < n; i++)
  242. if (p[i])
  243. // observed form is [01 01 gain], but treat any trailing non-zero as active
  244. return true;
  245. return false;
  246. }
  247. static void hapticCancelPendingOn(int slot)
  248. {
  249. // void queued ON entries (stale haptics / rumble across a reconnect). Per-slot: only the reconnected
  250. // slot's queue is scrubbed -- another controller's pending haptics are its own business. slot=-1 keeps
  251. // the old scrub-everything behavior (boot).
  252. int c0 = (slot >= 0 && slot < NSLOT) ? slot : 0;
  253. int c1 = (slot >= 0 && slot < NSLOT) ? slot + 1 : NSLOT;
  254. uint32_t pm = __get_PRIMASK();
  255. __disable_irq();
  256. for (int s = c0; s < c1; s++) {
  257. uint8_t guard = RELAY_QLEN + 1;
  258. for (uint8_t i = g_rqTail[s]; i != g_rqHead[s]; i = rqNext(i)) {
  259. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off
  260. g_rqHead[s] = g_rqTail[s] = 0;
  261. g_ringFault++;
  262. faultDiagTrace(FR_RINGF, g_ringFault);
  263. break;
  264. }
  265. RelayMsg &m = g_rq[s][i];
  266. if (m.rid == 0x82) {
  267. bool on = false;
  268. for (uint8_t j = 2; j < m.len; j++)
  269. if (m.data[j]) {
  270. on = true;
  271. break;
  272. }
  273. if (on)
  274. m.rid = 0;
  275. }
  276. if (m.rid == 0x80) {
  277. bool on = false;
  278. for (uint8_t j = 0; j < m.len; j++)
  279. if (m.data[j]) {
  280. on = true;
  281. break;
  282. }
  283. if (on)
  284. m.rid = 0;
  285. }
  286. }
  287. }
  288. __set_PRIMASK(pm);
  289. }
  290. void haptic82HostReport(const uint8_t *p, uint16_t n)
  291. {
  292. if (n < 3)
  293. return;
  294. g_haptic82Ms = millis();
  295. // Track on/off only. Do NOT synthesize a stop burst: Steam's own stop is forwarded verbatim, and each extra
  296. // 0x82 is a discrete pad click -- the spurious end-of-movement "click"/buzz the real puck never produces.
  297. g_haptic82On = haptic82PayloadOn(p, n);
  298. }
  299. bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
  300. {
  301. if (slot >= NSLOT)
  302. return false;
  303. // user rumble-strength scale (percent; 200 = double). Clamp to 16-bit.
  304. if (g_rumbleScale != 100) {
  305. uint32_t l = (uint32_t)lowFreq * g_rumbleScale / 100,
  306. h = (uint32_t)highFreq * g_rumbleScale / 100;
  307. lowFreq = (l > 0xFFFF) ? 0xFFFF : (uint16_t)l;
  308. highFreq = (h > 0xFFFF) ? 0xFFFF : (uint16_t)h;
  309. }
  310. bool on = lowFreq || highFreq;
  311. // Per-slot settle gate (the per-slot reconnect block + link-up check). 0x82 haptics in Steam mode use the
  312. // same gate; for XInput, the host only sends a stream while a controller is connected, so this also doubles
  313. // as "no controller here, no relay".
  314. if (on && haptic82Blocked(slot))
  315. return false;
  316. if (!on && !hapticLinkUp(slot))
  317. return false;
  318. // SDL's current Steam/Triton structs define output report 0x80 as:
  319. // type, uint16 intensity, {uint16 speed, int8 gain} left/right.
  320. // We map conventional gamepad low/high-frequency motors to left/right speeds and use max as intensity.
  321. uint16_t intensity = lowFreq > highFreq ? lowFreq : highFreq;
  322. uint8_t p[9];
  323. // haptic_type_t::HAPTIC_TYPE_RUMBLE; 0 is the off/zero report
  324. p[0] = on ? 0x04 : 0x00;
  325. p[1] = (uint8_t)(intensity & 0xFF);
  326. p[2] = (uint8_t)(intensity >> 8);
  327. p[3] = (uint8_t)(lowFreq & 0xFF);
  328. p[4] = (uint8_t)(lowFreq >> 8);
  329. p[5] = 0;
  330. p[6] = (uint8_t)(highFreq & 0xFF);
  331. p[7] = (uint8_t)(highFreq >> 8);
  332. p[8] = 0;
  333. if (!relayEnqueue(0x80, p, sizeof p, slot))
  334. return false;
  335. g_rumble80Ms[slot] = millis();
  336. g_rumble80On[slot] = on;
  337. return true;
  338. }
  339. // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate). Test
  340. // haptics broadcast to all connected slots (slot 0xFF); the stop frame is broadcast too (a stuck latch can
  341. // affect any controller, and the haptic-engine clear-re-init is settings-only so it's harmless on healthy
  342. // ones).
  343. // Stability-test keepalive: while g_stabTest, buzz every connected controller for ~150ms once per 10s so an
  344. // unattended uptime measurement isn't ended by a controller idle-sleeping. Toggled by WebUSB cmd 0x0F; the
  345. // panel times uptime-until-reset. Reboots clear g_stabTest (the panel re-arms it on reconnect).
  346. bool g_stabTest = false;
  347. void hapticStabTask()
  348. {
  349. if (!g_stabTest)
  350. return;
  351. static const uint8_t on[3] = { 0x01, 0x01, 0xF7 };
  352. static const uint8_t off[3] = { 0x01, 0x01, 0x00 };
  353. static unsigned long lastOn = 0, offAt = 0;
  354. unsigned long now = millis();
  355. // Buzz only slots whose controller is actually answering polls (same 300ms liveness the panel/0x79 use).
  356. // The old 0xFF broadcast kept stuffing a powered-off controller's relay ring -- pure eviction churn plus
  357. // wasted TX at a radio that can't hear it, right inside the power-off hang window (issue #72 repro).
  358. auto enqLive = [&](const uint8_t *p) {
  359. for (uint8_t s = 0; s < NSLOT; s++)
  360. if (g_slot[s].used && g_connReplyMs[s] &&
  361. (unsigned long)(now - g_connReplyMs[s]) < 300u)
  362. relayEnqueue(0x82, p, 3, s);
  363. };
  364. if (lastOn == 0 || (uint32_t)(now - lastOn) >= 10000u) {
  365. lastOn = now;
  366. enqLive(on);
  367. offAt = now + 150;
  368. }
  369. if (offAt && (int32_t)(now - offAt) >= 0) {
  370. enqLive(off);
  371. offAt = 0;
  372. }
  373. }
  374. void rfConnQueueHapticRelay()
  375. {
  376. if (relayPending())
  377. return; // host relays first; injectables wait for an idle cycle
  378. static const uint8_t HAP_ON[3] = { 0x01, 0x01, 0xF7 };
  379. static const uint8_t HAP_OFF[3] = { 0x01, 0x01, 0x00 };
  380. if (g_testHaptic) {
  381. if (relayEnqueue(0x82, HAP_ON, 3, 0xFF))
  382. g_testHaptic--;
  383. } else if (g_hapticStop && !g_xbox) {
  384. if (relayEnqueue(0x82, HAP_OFF, 3, 0xFF))
  385. g_hapticStop--;
  386. }
  387. }
  388. // rfConnFlushRelay(ch, s1): drain one entry from the current slot's relay queue and TX it. Each slot's queue
  389. // is independent, so each controller only sees its own commands. With N connected slots the per-slot relay
  390. // rate is 1/N of the per-cycle rate; sustained buzz streams are still 1 packet/cycle/slot.
  391. bool rfConnFlushRelay(uint8_t ch, uint8_t s1)
  392. {
  393. int cur = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  394. // Snapshot one entry under a short critical section. relayEnqueue() (the producer) runs on the
  395. // high-priority usbd task and, when the ring is full, evicts the oldest by advancing g_rqTail itself --
  396. // the same variable this consumer reads/advances. Reading the entry while a producer could overwrite it
  397. // is a torn read (and dual-writing g_rqTail desyncs head/tail), so copy the entry out and consume the
  398. // slot atomically here, then do the (slow) RF TX on the copy with interrupts enabled.
  399. RelayMsg msg;
  400. bool have = false;
  401. uint32_t pm = __get_PRIMASK();
  402. __disable_irq();
  403. uint8_t guard = RELAY_QLEN + 1;
  404. while (g_rqTail[cur] != g_rqHead[cur]) {
  405. if (!guard--) { // desynced/corrupt ring -> recover, don't spin IRQs-off (watchdog-hang class)
  406. g_rqHead[cur] = g_rqTail[cur] = 0;
  407. g_ringFault++;
  408. faultDiagTrace(FR_RINGF, g_ringFault);
  409. break;
  410. }
  411. RelayMsg &m = g_rq[cur][g_rqTail[cur]];
  412. g_rqTail[cur] = rqNext(g_rqTail[cur]); // consume the slot
  413. // rid 0 = entry voided by hapticCancelPendingOn -> skip
  414. if (m.rid) {
  415. msg = m; // copy out before the producer can reuse the slot
  416. have = true;
  417. break;
  418. }
  419. }
  420. __set_PRIMASK(pm);
  421. if (have) {
  422. RelayMsg &m = msg;
  423. {
  424. uint8_t rl = m.len;
  425. if (rl > RELAY_MAXP)
  426. rl = RELAY_MAXP;
  427. // On-air sub-TLV framing. CONFIRMED from real puck<->controller sniffs: a command LANDS on
  428. // the controller only with the type-01 + inner-len form E3 [2+rl][01][rid][innerlen][data];
  429. // the legacy form E3 [1+rl][05][rid][data] makes the controller DISCARD any 0x87+ command.
  430. //
  431. // Whitelist type-01 to exactly three commands: LED brightness (0x87 reg 0x2D), power-off
  432. // (0x9F), and the STANDALONE lizard-suppression keepalive (0x87 [09 00 00] -- rl==3 so a
  433. // multi-register block that merely STARTS with 0x09 can't smuggle other registers into
  434. // landing). Setting index 9 = digital-mappings/lizard-active (ibex FUN_0001f554 decomp);
  435. // the real puck lands id9=0 every 3s -- without it the controller's revert timer fires,
  436. // re-enables autonomous mode and resets ALL settings to defaults (audible amp pop +
  437. // autonomous touchpad ticks = the spurious mid-session buzz). Other 0x87 writes (e.g. reg
  438. // 0x30 IMU enable, 0x34/0x35 haptic amplitude) must stay on legacy form -- landing 0x30
  439. // freezes the gyro; landing 0x34/0x35 causes the connect buzz.
  440. // Amp/haptic-config registers (0x18/0x2E/0x34/0x35): LAND these so the controller's amplifier is
  441. // configured the way Steam intends -> haptics (0x82 ticks, connect cue) play as clean ticks
  442. // instead of the default-amp BUZZ we get when the config is discarded. Steam sends these grouped
  443. // in 0x87 blocks that START with one of those regs and do NOT contain 0x30 (the gyro reg, which
  444. // must NOT land -- it freezes the gyro), so gating on data[0] lands the amp block without 0x30.
  445. // (g_landAmp, console "AMP"; on by default. The old "landing 0x34/0x35 buzzes" note was really the
  446. // 0x81 storm, now removed.)
  447. bool ampReg = (m.data[0] == 0x18 || m.data[0] == 0x2E ||
  448. m.data[0] == 0x34 || m.data[0] == 0x35);
  449. bool land01 =
  450. (m.rid == 0x9F) ||
  451. (m.rid == 0x87 && rl >= 1 &&
  452. m.data[0] == 0x2D) ||
  453. (m.rid == 0x87 && rl == 3 &&
  454. m.data[0] == 0x09) ||
  455. (g_landAmp && m.rid == 0x87 && rl >= 1 &&
  456. ampReg) ||
  457. // experiment: land EVERY 0x87 verbatim (real-puck relay) when enabled
  458. (g_landAll87 && m.rid == 0x87);
  459. uint8_t p[5 + RELAY_MAXP], plen;
  460. if (land01) {
  461. p[0] = g_relayOp;
  462. p[1] = (uint8_t)(2 + rl);
  463. p[2] = 0x01;
  464. p[3] = m.rid;
  465. p[4] = rl;
  466. memcpy(p + 5, m.data, rl);
  467. plen = (uint8_t)(5 + rl);
  468. } else {
  469. p[0] = g_relayOp;
  470. p[1] = (uint8_t)(1 + rl);
  471. p[2] = 0x05;
  472. p[3] = m.rid;
  473. memcpy(p + 4, m.data, rl);
  474. plen = (uint8_t)(4 + rl);
  475. }
  476. hapLogAdd(0xFE, m.rid, m.data, rl);
  477. // Lifecycle log (CDC debug boot): EXACTLY what we TX to the controller, so a capture shows
  478. // whether OpenPuck (or Steam in the background) pokes the controller right before a buzz
  479. // latches -- the piece the I45 stream (controller->us) can't show. Low rate outside a haptic
  480. // burst; guarded so it never blocks the loop. cur = target slot.
  481. if (Serial.availableForWrite() > 60) {
  482. // boosted: this print runs at relay-flood rate and CDC flush enters the same
  483. // TinyUSB DMA claim window as HID sends (the issue-72 livelock; see usb_tx.cpp)
  484. usbTxBoost();
  485. Serial.printf("# TX t=%lu slot%d %s rid=%02X:",
  486. (unsigned long)millis(), cur,
  487. land01 ? "L01" : "l05", m.rid);
  488. for (uint8_t i = 0; i < rl && i < 8; i++)
  489. Serial.printf(" %02X", m.data[i]);
  490. Serial.println();
  491. usbTxUnboost();
  492. }
  493. // slot was already consumed under the critical section above.
  494. // s1 carries a PID distinct from the GET poll (caller cycles it) so the controller's ESB
  495. // dedup never treats the GET as a retransmit of this relay.
  496. //
  497. // HARVEST the relay's reply as INPUT. Like any frame we send, the controller auto-ACKs a
  498. // relay with its current input in the ACK payload (~90us later, per the RE poll->ACK
  499. // capture). The old 80us window closed BEFORE that reply arrived, so every relay's input was
  500. // discarded -- and yet HW A/B showed the delivered input rate RISING when haptics relay
  501. // (an extra frame/cycle during a trackpad drag), because each frame makes the controller
  502. // refresh its ACK payload and the following E3 poll caught the fresher value. Reading the
  503. // reply directly turns each relay into a SECOND input sample per cycle: rfConnTx runs the
  504. // full F1 decode (seq-dedup guards double-forward), so a drag streaming haptics now collects
  505. // ~2x the samples, closing the gap to the real puck. A present reply returns early (~90us);
  506. // only a genuine no-reply pays the bounded 400us window, so airtime stays in budget.
  507. rfConnTx(
  508. ch, s1, p, plen,
  509. 400); // one relay per poll cycle -- reply harvested as input
  510. }
  511. }
  512. return have; // true = a relay frame went out this cycle (its reply is harvested as input, above)
  513. }
  514. // Haptic-subsystem re-init: the captured sequence Steam sends when it (re)takes control (0x81 reset + 0x87
  515. // register writes). Brightness (reg 0x2D) is omitted to avoid stomping the LED. The 0x87 frames go out on
  516. // legacy framing so the controller discards them; the three 0x81 frames are the effective reset. slot=0xFF
  517. // broadcasts to all connected controllers (the re-init is settings-only and harmless on healthy controllers).
  518. // One-shot SET_SETTINGS (0x87) write of the controller's GLOBAL trackpad-haptics enable. The controller plays
  519. // touchpad haptic sequences autonomously (haptics-sequencer-touchpad) gated by settings/haptics/enabled; in the
  520. // emulated (non-Steam) modes the host never sends SC haptic reports, so the only way to silence the pad buzz is
  521. // to write this setting. Wire format [id][val u16 LE] -- confirmed from the controller's SET_SETTINGS handler
  522. // (decomp FUN_0001f61c; id 0x30 == settings/sensors/imu/mode is the cross-check anchor in the same id space).
  523. // The exact id for settings/haptics/enabled needs a one-time `scmd labels` HW read (candidates from the captured
  524. // reinit: 0x18 / 0x2e / 0x34 / 0x35). Until it is set, SETTING_HAPTICS_ENABLED stays 0xFF and this is inert.
  525. #define SETTING_HAPTICS_ENABLED 0xFF
  526. void hapticSetPadEnabled(uint8_t slot, bool on)
  527. {
  528. #if SETTING_HAPTICS_ENABLED != 0xFF
  529. uint8_t pl[3] = { SETTING_HAPTICS_ENABLED, (uint8_t)(on ? 1 : 0), 0 };
  530. relayEnqueue(0x87, pl, 3, slot);
  531. #else
  532. (void)slot;
  533. (void)on;
  534. #endif
  535. }
  536. void hapticReinit(uint8_t slot)
  537. {
  538. static const uint8_t H30[] = { 0x30, 0x00, 0x00, 0x07, 0x07,
  539. 0x00, 0x08, 0x07, 0x00, 0x31,
  540. 0x02, 0x00, 0x52, 0x03, 0x00 };
  541. static const uint8_t H18[] = { 0x18, 0x00, 0x00, 0x2e, 0x00,
  542. 0x00, 0x34, 0xff, 0xff, 0x35,
  543. 0xff, 0xff, 0x34, 0xff, 0xff };
  544. static const uint8_t H35[] = { 0x35, 0xff, 0xff, 0x2e, 0x00, 0x00 };
  545. static const uint8_t T81A[] = {
  546. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  547. };
  548. static const uint8_t T81B[] = {
  549. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  550. };
  551. // reset action (FUN_0001f554) -- Steam sends this first
  552. relayEnqueue(0x81, nullptr, 0, slot);
  553. relayEnqueue(0x87, H30, sizeof H30, slot);
  554. // haptic config (enabled/amplifier/gain): the part that clears a latch
  555. relayEnqueue(0x87, H18, sizeof H18, slot);
  556. relayEnqueue(0x87, H35, sizeof H35, slot);
  557. relayEnqueue(0x81, T81A, sizeof T81A, slot);
  558. relayEnqueue(0x81, T81B, sizeof T81B, slot);
  559. // Re-apply the active emulated type's trackpad-haptics preference last (after the haptic-config writes
  560. // above, which would otherwise re-enable it). Default-on types send "enable"; Switch (padHaptics=0)
  561. // disables. Inert until the setting id is captured.
  562. hapticSetPadEnabled(slot, g_padHaptics != 0);
  563. // Apply the configured LED brightness for the active emulated type. Steam sets the
  564. // brightness each session; emulated modes never do, so the controller comes up at
  565. // full brightness. 0 = no override (preserve controller default).
  566. if (g_etype < ET_COUNT && g_ledBright > 0) {
  567. uint8_t pl[3] = { 0x2D, g_ledBright, 0x00 };
  568. relayEnqueue(0x87, pl, sizeof pl, slot);
  569. }
  570. }
  571. void hapticInit()
  572. {
  573. g_haptic82On = false;
  574. g_hapticStop = 0;
  575. for (int s = 0; s < NSLOT; s++) {
  576. g_rqHead[s] = g_rqTail[s] = 0;
  577. g_rumble80On[s] = false;
  578. g_rumble80Ms[s] = 0;
  579. // post-connect haptic block is permanently disabled (not armed, not configurable)
  580. g_hapticBlockUntil[s] = 0;
  581. }
  582. }
  583. // Schedule the clearing re-init for ONE slot. Called on the reliable first-reply signal from rf_link and
  584. // again from hapticTask's link-up edge detector. Strictly per-slot: only the reconnected controller gets
  585. // re-inits / its pending haptics scrubbed -- the other controllers hear nothing about it.
  586. void hapticOnReconnect(int slot)
  587. {
  588. if (slot < 0 || slot >= NSLOT)
  589. return;
  590. // post-connect haptic block is permanently disabled -- relay haptics immediately on (re)connect
  591. g_hapticBlockUntil[slot] = 0;
  592. g_haptic82On = false;
  593. g_rumble80On[slot] = false;
  594. g_rumble80Ms[slot] = 0;
  595. // Scrub haptics queued before the link came up (stale across the reconnect) -- this slot only.
  596. hapticCancelPendingOn(slot);
  597. // NO automatic haptic re-init. hapticReinit lands 0x81 (CLEAR DIGITAL MAPPINGS -- rid < 0x87 so it
  598. // EXECUTES even on legacy framing). 0x81 is NON-IDEMPOTENT (ibex FUN_0001f554): EVERY call re-runs the
  599. // lizard-disable event + func_0x0001bbf0 (a hardware peripheral re-arm unique to the 0x81 path) -- so
  600. // firing 8 across the connect window = 8 audible clicks = the "repeated non-periodic clicks at connect"
  601. // the real puck (which clears ONCE then holds) never produces. The clearing re-init stays available on
  602. // demand (WebUSB "Clear stuck buzz" -> hapticReinit), just not sprayed at connect.
  603. uint8_t mk = 2;
  604. hapLogAdd(0xFD, 0xEE, &mk, 1);
  605. }
  606. void hapticTask()
  607. {
  608. // id9 steering (SET_SETTINGS index 9 = digital-mappings / the controller's AUTONOMOUS mapping+haptic
  609. // engine, which is what generates the trackpad tick haptics). We decide per mode whether that autonomous
  610. // engine should be ON, then either land id9=1 ONCE per connect episode (engine on) or hold id9=0 every
  611. // LIZKEEP_MS (engine off). id9 gates the whole autonomous pad layer INCLUDING the trackpad ticks; 0x87 is
  612. // change-guarded in the controller so a repeated same-value write is silent (no 0x81-style click), and
  613. // re-landing id9=1 on (re)connect restores the pad layer immediately instead of waiting for the
  614. // controller's revert timer (which also resets all settings = audible pop).
  615. //
  616. // wantAuto = should the controller run its own pad layer (trackpad ticks) for the ACTIVE mode?
  617. // - puck modes (STEAM/LIZARD): ON while we present lizard (pure MODE_LIZARD, or MODE_STEAM with Steam
  618. // closed) so the pad keeps its ticks; OFF when Steam is driving (Steam owns haptics, and holding the
  619. // engine off stops it latching the deep-inside reconnect buzz). = puckLizardActive().
  620. // - emulated modes (Xbox/Switch/DS): follow the per-type trackpad-haptics config g_padHaptics (default
  621. // ON; Switch defaults OFF). This is the MIRROR of the lizard case -- here holding id9=0 is how we turn
  622. // the controller's autonomous trackpad haptics OFF for a type that doesn't want them.
  623. if (g_lizKeep) {
  624. static unsigned long lastKeep[NSLOT] = { 0 };
  625. static bool landedAuto[NSLOT] = { false };
  626. static const uint8_t K0[3] = { 0x09, 0x00, 0x00 };
  627. static const uint8_t K1[3] = { 0x09, 0x01, 0x00 };
  628. bool wantAuto = modeIsPuck(g_usbMode) ? puckLizardActive() :
  629. (g_padHaptics != 0);
  630. for (int s = 0; s < NSLOT; s++) {
  631. if (!g_slot[s].used || !hapticLinkUp(s)) {
  632. // re-land id9 on the next (re)connect: a fresh controller defaults to
  633. // autonomous, but one carrying our previous session's id9 does not
  634. landedAuto[s] = false;
  635. lastKeep[s] = 0;
  636. continue;
  637. }
  638. if (wantAuto) {
  639. if (!landedAuto[s]) {
  640. landedAuto[s] = true;
  641. relayEnqueue(0x87, K1, sizeof K1,
  642. (uint8_t)s);
  643. }
  644. } else {
  645. landedAuto[s] = false;
  646. if (lastKeep[s] &&
  647. (uint32_t)(millis() - lastKeep[s]) <
  648. LIZKEEP_MS)
  649. continue;
  650. lastKeep[s] = millis();
  651. relayEnqueue(0x87, K0, sizeof K0, (uint8_t)s);
  652. }
  653. }
  654. }
  655. // Per-slot link-edge detect (backup for hapticOnReconnect in rf_link).
  656. static bool wasHapticLinkUp[NSLOT] = { 0 };
  657. for (int s = 0; s < NSLOT; s++) {
  658. if (!g_slot[s].used)
  659. continue;
  660. bool up = hapticLinkUp(s);
  661. if (up && !wasHapticLinkUp[s]) {
  662. uint8_t mk = 1;
  663. hapLogAdd(0xFD, 0xEE, &mk, 1);
  664. hapticOnReconnect(s);
  665. }
  666. if (!up && wasHapticLinkUp[s]) {
  667. uint8_t mk = 0;
  668. hapLogAdd(0xFD, 0xEE, &mk, 1);
  669. // Lifecycle log (CDC debug boot): the 300ms link-up watchdog saw this slot go silent. Pairs
  670. // with the CONNECT/RECONNECT lines from rf_link so a session of cycles shows the full
  671. // down->up cadence -- how often the link actually drops (churn) vs stays up.
  672. if (Serial.availableForWrite() > 50)
  673. Serial.printf("# LC t=%lu slot%d link DOWN\n",
  674. (unsigned long)millis(), s);
  675. }
  676. wasHapticLinkUp[s] = up;
  677. }
  678. // (No automatic re-init firing -- see hapticOnReconnect: the connect-time 0x81 storm was the click train
  679. // the real puck never makes. hapticReinit is on-demand only, via the WebUSB "Clear stuck buzz" button.)
  680. // Power-off on host sleep: only when VBUS is present (genuine sleep, not a cable unplug which also
  681. // trips the suspend edge briefly) AND the suspend has PERSISTED >= SUSPEND_OFF_MS. A brief USB
  682. // selective-suspend (host idle power-management) resumes in <1s; firing the power-off on its edge
  683. // powered the controllers off ourselves -> random drop/reconnect churn. Arm only on a genuine
  684. // resume->suspend edge (wasSusp=true at boot suppresses a false fire on boot-into-suspended).
  685. static bool wasSusp = true;
  686. static unsigned long suspSinceMs = 0;
  687. static bool suspArmed = false;
  688. bool susp = USBDevice.suspended();
  689. bool vbus = (NRF_POWER->USBREGSTATUS &
  690. POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
  691. if (susp && !wasSusp) {
  692. suspSinceMs = millis();
  693. suspArmed = true;
  694. faultDiagTrace(FR_SUSP, 0);
  695. }
  696. if (!susp) {
  697. if (wasSusp)
  698. faultDiagTrace(FR_RESUME, 0);
  699. suspArmed = false;
  700. }
  701. if (suspArmed && vbus && (millis() - suspSinceMs) >= SUSPEND_OFF_MS) {
  702. hapticSendShutdown();
  703. suspArmed = false; // fire once per suspend
  704. }
  705. wasSusp = susp;
  706. // Steam-mode quiet timeout: mark 0x82 stream inactive. No synthesized stop -- Steam forwards its own.
  707. if (!g_xbox && g_haptic82On &&
  708. millis() - g_haptic82Ms > HAPTIC_QUIET_MS)
  709. g_haptic82On = false;
  710. // Per-slot stuck-rumble watchdog: force zero after 2.5s without a refresh.
  711. for (int s = 0; s < NSLOT; s++) {
  712. if (g_rumble80On[s] && millis() - g_rumble80Ms[s] > 2500u)
  713. hapticSteamRumble(0, 0, (uint8_t)s);
  714. }
  715. // (No automatic idle-clear re-init either: same 0x81-click reason. A genuinely stuck buzz is cleared
  716. // on demand from the panel. Verbatim relay -- like the real puck -- is the steady-state behavior.)
  717. }