puck_hid.cpp 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. #include "puck_hid.h"
  2. #include "bonds.h"
  3. #include "config.h"
  4. #include "identity.h"
  5. #include "haptics.h"
  6. #include "fault_diag.h"
  7. #include "rf_link.h"
  8. #include "triton.h"
  9. #include "mode_lizard.h"
  10. #include "steam_commands.h"
  11. #include "wake_hid.h"
  12. #include "build_info.h"
  13. #include "usb_tx.h"
  14. #include <Adafruit_TinyUSB.h>
  15. #include <Arduino.h>
  16. #include <string.h>
  17. uint8_t g_fwdNewOnly = 1;
  18. // Content dedup for the Steam input forward: only forward a 0x45/0x42 report when its body EXCLUDING the
  19. // counter byte differs from the last one forwarded on that slot. The controller's counter (rep[1]) FREE-RUNS
  20. // -- it advances on every reply even with static input -- so the seq-based g_fwdNewOnly can't suppress true
  21. // repeats, and a poll rate above the controller's ~250 Hz sample rate forwards identical inputs with only the
  22. // counter bumped (host tester reads >250/s for a <=250 Hz controller). Content dedup is what the real puck
  23. // effectively does. On by default; console "CD" toggles for A/B.
  24. uint8_t g_fwdContentDedup = 1;
  25. SteamPuckController g_steamPuck;
  26. // millis() a power-off (0x9F) was last relayed to each slot; 0 = never. See puckNotePowerOff() / task().
  27. static volatile unsigned long g_powerOffMs[NSLOT] = { 0 };
  28. // Present a powered-off slot as DISCONNECTED to Steam for this long, to ride out the controller's post-off
  29. // F1 tail (measured ~<=1s of dying replies after the "off!" command) without bouncing the connection state.
  30. #define POWEROFF_HOLD_MS 2000u
  31. void puckNotePowerOff(uint8_t slot)
  32. {
  33. if (slot >=
  34. NSLOT) { // 0xFF broadcast ("all off": host suspend, panel/test button)
  35. for (int s = 0; s < NSLOT; s++)
  36. if (g_slot[s].used)
  37. g_powerOffMs[s] = millis();
  38. } else {
  39. g_powerOffMs[slot] = millis();
  40. }
  41. }
  42. // True while `slot` is inside its post-power-off hold window (present it disconnected, ignore dying replies).
  43. static inline bool slotPoweringOff(int slot)
  44. {
  45. return slot >= 0 && slot < NSLOT && g_powerOffMs[slot] &&
  46. (millis() - g_powerOffMs[slot] < POWEROFF_HOLD_MS);
  47. }
  48. // Cloned puck HID report descriptor: mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte FEATURE
  49. // command reports on report id 1/2. Each of the 4 interfaces uses this.
  50. static const uint8_t PUCK_HID_DESC[] = {
  51. 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
  52. 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  53. 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01,
  54. 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02,
  55. 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38,
  56. 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1,
  57. 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25,
  58. 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29,
  59. 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0,
  60. 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26,
  61. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44,
  62. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81,
  63. 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01,
  64. 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  65. 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26,
  66. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45,
  67. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81,
  68. 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09,
  69. 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  70. 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26,
  71. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83,
  72. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91,
  73. 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08,
  74. 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  75. 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26,
  76. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87,
  77. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91,
  78. 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F,
  79. 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  80. 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09,
  81. 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0
  82. };
  83. // Pure MODE_LIZARD only: same puck/lizard descriptor plus a tiny Consumer Control report for media keys.
  84. // MODE_STEAM must keep PUCK_HID_DESC verbatim so Steam's controller/haptics path sees the normal puck shape.
  85. static const uint8_t PUCK_LIZARD_HID_DESC[] = {
  86. 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
  87. 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  88. 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01,
  89. 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02,
  90. 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38,
  91. 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1,
  92. 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25,
  93. 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29,
  94. 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0,
  95. 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26,
  96. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44,
  97. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81,
  98. 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01,
  99. 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  100. 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26,
  101. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45,
  102. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81,
  103. 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09,
  104. 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  105. 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26,
  106. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83,
  107. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91,
  108. 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08,
  109. 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  110. 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26,
  111. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87,
  112. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91,
  113. 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F,
  114. 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  115. 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09,
  116. 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0,
  117. 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x03, 0x15, 0x00, 0x25, 0x01,
  118. 0x09, 0xE9, 0x09, 0xEA, 0x75, 0x01, 0x95, 0x02, 0x81, 0x02, 0x75, 0x06,
  119. 0x95, 0x01, 0x81, 0x01, 0xC0
  120. };
  121. static Adafruit_USBD_HID hid[NSLOT];
  122. // ---- feature-command capture (diagnostic) ---------------------------------------------------------------
  123. // Log the USB feature command channel (Steam's SET/GET) to serial to see the connect handshake -- WITHOUT a
  124. // Serial.printf on the fragile 800B usbd task (that path can blow the stack -> LOCKUP; it's why production
  125. // avoids printf here). handleSet/handleGet (usbd task) push a compact record under PRIMASK; puckCmdLogDrain()
  126. // prints them from loop() context. When g_cmdCapture is on, the high-rate I45 input stream is also suppressed
  127. // (rf_link) so the command sequence is readable. Toggle from the console with "FC".
  128. bool g_cmdCapture = true;
  129. struct FCmdRec {
  130. uint8_t dir; // 0 = SET (host->puck write), 1 = GET (host read)
  131. uint8_t iface; // HID interface index = bond slot the command hit
  132. uint8_t rid, cmd, len, b[10];
  133. };
  134. static FCmdRec g_fc[32];
  135. static volatile uint8_t g_fcHead, g_fcTail;
  136. static void fcPush(uint8_t dir, int iface, uint8_t rid, uint8_t cmd,
  137. uint8_t len, const uint8_t *b, uint8_t n)
  138. {
  139. if (!g_cmdCapture)
  140. return;
  141. uint32_t pm = __get_PRIMASK();
  142. __disable_irq();
  143. uint8_t h = g_fcHead, nx = (uint8_t)((h + 1) & 31);
  144. if (nx !=
  145. g_fcTail) { // drop-newest when full (loop drains fast); never block the usbd task
  146. g_fc[h].dir = dir;
  147. g_fc[h].iface = (uint8_t)iface;
  148. g_fc[h].rid = rid;
  149. g_fc[h].cmd = cmd;
  150. g_fc[h].len = len;
  151. for (uint8_t i = 0; i < 10; i++)
  152. g_fc[h].b[i] = (i < n) ? b[i] : 0;
  153. g_fcHead = nx;
  154. }
  155. __set_PRIMASK(pm);
  156. }
  157. void puckCmdLogDrain(void)
  158. {
  159. // boosted: runs at Steam's feature-storm rate and CDC flush enters the same TinyUSB DMA claim window
  160. // as HID sends (the issue-72 livelock; see usb_tx.cpp)
  161. usbTxBoost();
  162. while (g_fcHead != g_fcTail) {
  163. if (Serial.availableForWrite() < 70)
  164. break; // don't stall loop on CDC backpressure; resume next iteration
  165. FCmdRec r = g_fc[g_fcTail];
  166. g_fcTail = (uint8_t)((g_fcTail + 1) & 31);
  167. Serial.printf("# FC %s if=%u rid=%u cmd=%02X len=%u:",
  168. r.dir ? "GET" : "SET", r.iface, r.rid, r.cmd,
  169. r.len);
  170. for (uint8_t i = 0; i < 10; i++)
  171. Serial.printf(" %02X", r.b[i]);
  172. Serial.println();
  173. }
  174. usbTxUnboost();
  175. }
  176. // ===================== seamless LIZARD decision =====================
  177. // Steam, while running, re-sends settings report 0x87 (lizard-off) every ~3s as a heartbeat (captured on HW),
  178. // and ANY OUTPUT report likewise stamps g_steamAliveMs. When the heartbeat stops we fall back to lizard, so the
  179. // controller drives desktop keyboard+mouse whenever Steam isn't running. MODE_LIZARD forces lizard always.
  180. // millis of last Steam OUTPUT/settings write; 0 at boot => lizard until Steam appears
  181. static unsigned long g_steamAliveMs = 0;
  182. // Fall back to lizard this long after Steam's ~3s settings heartbeat stops. Keep >2x the cadence: the haptic
  183. // relay is gated by !lizardActive(), so a shorter window lets a jittered/delayed heartbeat flip lizard
  184. // mid-session while Steam is still running -> a haptic arriving in that window gets gated out (dropped). 7s
  185. // tolerates one missed beat. Do not shorten without a lizard trigger independent of the heartbeat timeout.
  186. #define LIZARD_WD_MS 7000u
  187. static bool g_autoLizard =
  188. true; // master switch; false => Steam mode always forwards 0x45
  189. // Single source of truth, shared by the USB input path AND the haptic relay gate: if we ever relay a 0x82 to
  190. // the controller while presenting lizard (Steam isn't reading 0x45 back), Steam loops the same haptic -> buzz.
  191. static inline bool steamDrivingGamepad()
  192. {
  193. return g_steamAliveMs && (millis() - g_steamAliveMs < LIZARD_WD_MS);
  194. }
  195. static inline bool lizardActive()
  196. {
  197. return modeIsPuck(g_usbMode) &&
  198. (g_usbMode == MODE_LIZARD ||
  199. (g_autoLizard && !steamDrivingGamepad()));
  200. }
  201. // Public accessor for the haptic layer (haptics.cpp gates the lizard-suppression keepalive on this).
  202. bool puckLizardActive()
  203. {
  204. return lizardActive();
  205. }
  206. static inline void hostStampAlive()
  207. {
  208. g_steamAliveMs = millis();
  209. }
  210. // Right after the host resumes from suspend, MUTE input forwarding briefly. Otherwise the controller's input
  211. // in that instant (a trackpad click/trigger, or residual button state from the wake gesture) gets forwarded
  212. // as a real click/keypress into the just-woken desktop. Set when task() sees the suspended->active transition.
  213. static unsigned long g_resumeMs = 0;
  214. #define POST_RESUME_MUTE_MS 1500u
  215. // How long to keep re-asserting the 0x79 DISCONNECT edge to Steam after a controller drops, so a single
  216. // lost report can't strand Steam in "connected" (see task()). Bounded to avoid forever-spamming disc.
  217. #define DISC_RESEND_MS 6000u
  218. // ===================== puck feature command channel =====================
  219. // `slot` is the interface index (interface N == bond slot N).
  220. static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
  221. uint8_t const *b, uint16_t n)
  222. {
  223. // Steam OUTPUT reports 0x80-0x89. The haptic/actuator reports (0x80-0x86) are relayed to the controller,
  224. // and ONLY when they arrive on the CONNECTED slot's interface: we have one controller but expose 4 puck
  225. // slots, and a report aimed at a DIFFERENT slot made the controller buzz at random (the slot gate below
  226. // fixes that). Must NOT clamp to 0x82-only -- Steam drives the actuators through the WHOLE Triton OUTPUT
  227. // report space (ValveTritonOutReportMessageIDs, SDL steam/controller_structs.h; the ids match this
  228. // interface's descriptor payload sizes exactly):
  229. // 0x80 HAPTIC_RUMBLE(9) 0x81 HAPTIC_PULSE(7) 0x82 HAPTIC_COMMAND(3)
  230. // 0x83 HAPTIC_LFO_TONE(9) 0x84 HAPTIC_LOG_SWEEP(8) 0x85 HAPTIC_SCRIPT(3) 0x86 (3, unnamed)
  231. // These ids are NOT the feature-0x01 command ids (controller_constants.h) -- same numbers, different
  232. // meanings -- so a rule written for one channel must never be applied to the other.
  233. // The 63-byte settings/config reports 0x87/0x88/0x89 are NOT haptics and reach the controller via the
  234. // feature-0x01 passthrough path instead.
  235. if (type == HID_REPORT_TYPE_OUTPUT) {
  236. if (rid >= 0x80 && rid <= 0x89) {
  237. // capture ALL OUTPUT reports (even un-relayed) for the 'H' dump
  238. hapLogAdd((uint8_t)slot, rid, b, n);
  239. // ANY Steam OUTPUT report (not just the 0x87 heartbeat) means Steam is present and driving ->
  240. // leave lizard for gamepad NOW, so a haptic arriving before the first 0x87 isn't relayed while
  241. // we're still presenting lizard (-> buzz loop).
  242. hostStampAlive();
  243. }
  244. // Post-resume mute also gates haptics: while onReport45 is muted Steam reads NO 0x45 back, which is the
  245. // exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
  246. bool muted = g_resumeMs &&
  247. millis() - g_resumeMs < POST_RESUME_MUTE_MS;
  248. // NOTE: report id 0x81 is NOT dropped here, unlike the feature-0x01 cmd 0x81. The two 0x81s live in
  249. // DIFFERENT id spaces (SDL steam/controller_structs.h vs controller_constants.h):
  250. // OUTPUT rid 0x81 = ID_OUT_REPORT_HAPTIC_PULSE, MsgHapticPulse {u8 side; u16 on_us; u16 off_us;
  251. // u16 repeat_count} = the 7-byte "00.."/"01.." (side=left/right) reports Steam
  252. // fires for trackpad CLICK feedback ("Regular Press"), the trigger FULL-PULL
  253. // click and GripSense cues.
  254. // FEATURE cmd 0x81 = ID_CLEAR_DIGITAL_MAPPINGS (the mapping-engine reset, dropped below).
  255. // Dropping the OUTPUT form as if it were the same command is what made those haptics missing
  256. // (issues #163 / #166): "Soft Press" survived because it rides 0x82 ID_OUT_REPORT_HAPTIC_COMMAND,
  257. // while every pulse-based click was thrown away. Pulses are self-terminating (repeat_count in the
  258. // payload), so there is no latch to strand and no stop frame to lose.
  259. if (g_hapticRelay && rid >= 0x80 && rid <= 0x86 && n >= 1 &&
  260. hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
  261. if (!haptic82Blocked(slot)) {
  262. relayEnqueue(rid, b,
  263. (uint8_t)(n > RELAY_MAXP ?
  264. RELAY_MAXP :
  265. n),
  266. true, (uint8_t)slot);
  267. }
  268. }
  269. #if OPK_LOG
  270. // Per-report Serial echo: gated to the diagnostic build only. handleSet runs on the TinyUSB device
  271. // task, whose FreeRTOS stack is just 800 bytes; a Serial.printf there costs 200-500B and, stacked
  272. // under TinyUSB's control-transfer frames + a preempting USB ISR, can blow the stack -> the exact
  273. // SP-corruption-during-exception-entry that shows up as RR_LOCKUP. Production keeps this path
  274. // printf-free; the same data is already in the OPK_LOG capture ring (hapLogAdd) for the WebUSB panel.
  275. if (Serial.availableForWrite() > 80) {
  276. Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
  277. n);
  278. for (uint16_t i = 0; i < n && i < 14; i++)
  279. Serial.printf(" %02X", b[i]);
  280. Serial.println();
  281. }
  282. #endif
  283. return;
  284. }
  285. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  286. return;
  287. Slot &S = g_slot[slot];
  288. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  289. const uint8_t *pl = b + 2;
  290. uint16_t pln = (n >= 2) ? n - 2 : 0;
  291. // Capture EVERY feature SET (the whole cmd channel: 0x83 attr, 0xAE strings, 0xB4 conn, 0xA2/A3 bond, the
  292. // feature-1 relay, AND any host battery query) to the WebUSB ring so the panel's capture view shows it.
  293. hapLogAdd((uint8_t)slot, cmd, b, n);
  294. // ...and to the serial feature-command capture (diagnostic; drained in loop, see puckCmdLogDrain).
  295. fcPush(0, slot, rid, cmd, len, pl, pln > 10 ? 10 : (uint8_t)pln);
  296. // ...and to the flight recorder (survives a watchdog reset). This runs ON the fragile usbd task, so the
  297. // command flood that (theory) overflows it lands in the post-mortem trail rather than being lost with USB.
  298. faultDiagTrace(FR_SET, (uint16_t)((rid << 8) | cmd));
  299. // Disable lizard if requested by host.
  300. if (cmd == IBEX_CMD_SET_SETTINGS_VALUES) {
  301. // Host sent a settings write. Check if this is a write with the lizard suppression tag.
  302. for (int off = 0; off <= len; off += 3) {
  303. uint8_t settings_key = pl[off];
  304. uint16_t settings_val = pl[off + 1] + 256 * pl[off + 2];
  305. if (settings_key == SETTING_LIZARD_MODE &&
  306. settings_val == 0) {
  307. hostStampAlive();
  308. break;
  309. }
  310. }
  311. }
  312. // Controller power-off: Steam's "turn off controller" is feature-0x01 frame 9F 04 6F 66 66 21 ("off!"),
  313. // confirmed from a real puck capture. The feature-0x01 relay below forwards it once; hapticSendShutdown
  314. // bursts it for NO-ACK reliability. Slot-targeted: the command arrived on THIS controller's interface,
  315. // so only this controller powers off (broadcasting killed all connected controllers at once).
  316. // TODO: Why do we spam this? Doesn't the controller respond with a TAG4 upon command receival?
  317. if (rid == 1 && cmd == IBEX_CMD_TURN_OFF_CONTROLLER)
  318. hapticSendShutdown((uint8_t)slot);
  319. // report 0x01 = raw passthrough -> queue for RF relay to the controller
  320. if (rid == 1 && n >= 2) {
  321. // (feature-1 commands -- haptics, LED, 0x87 settings, 0x9F power-off -- are captured by the
  322. // general feature-SET hapLogAdd above.)
  323. bool muted = g_resumeMs &&
  324. millis() - g_resumeMs < POST_RESUME_MUTE_MS;
  325. // Reports in relayQuery will be forwarded to the controller if rid==1 and may be answered locally
  326. // if it makes sense and rid==2.
  327. // Reports in localAnswer will always be answered by the puck and not be forwarded.
  328. // Forwarding uses the type-01 framing PLUS a trailing `01 03 00` (rfConnFlushRelay's `queryTrailer`,
  329. // haptics.cpp -- without it the controller only ACKs and never answers), the controller replies --
  330. // on a LATER poll -- with a tag-4 TLV carrying `[echoed cmd][len][payload]`, decoded in rf_link.cpp's
  331. // F1 walk and written into this slot's `resp` (pendingQueryCmd, bonds.h).
  332. // If rid==1:
  333. // ID in relayQuery: Sent to controller, expect a response.
  334. // ID in localAnswer: Not sent to controller, response generated by Puck.
  335. // ID in neither: Sent to controller, no response expected.
  336. // clang-format off
  337. bool relayQuery =
  338. (cmd == IBEX_CMD_GET_DIGITAL_MAPPINGS || // 0x82
  339. cmd == IBEX_CMD_GET_ATTRIBUTES_VALUES || // 0x83
  340. cmd == IBEX_CMD_GET_SETTINGS_VALUES || // 0x89
  341. cmd == IBEX_CMD_GET_SETTINGS_MAXS || // 0x8B
  342. cmd == IBEX_CMD_GET_SETTINGS_DEFAULTS || // 0x8C
  343. cmd == IBEX_CMD_GET_DEVICE_INFO || // 0xA1
  344. cmd == IBEX_CMD_GET_STRING_ATTRIBUTE || // 0xAE
  345. cmd == IBEX_CMD_GET_CHIPID || // 0xBA
  346. cmd == IBEX_CMD_TRITON_READ_SETTING || // 0xED
  347. cmd == IBEX_CMD_GET_SYSTEM_INFO); // 0xF2
  348. bool localAnswer =
  349. (cmd == IBEX_CMD_TRITON_A2_OBSERVED_PAIRING_RECORD || // 0xA2
  350. cmd == IBEX_CMD_TRITON_A3_BOND_EVENT_OR_STATUS || // 0xA3
  351. cmd == IBEX_CMD_ENABLE_PAIRING || // 0xAD
  352. cmd == IBEX_CMD_DONGLE_GET_WIRELESS_STATE || // 0xB4
  353. cmd == 0xA4); // Not sure what that's for?
  354. // clang-format on
  355. bool queryArmed = false;
  356. bool relayOk = hapticRelaySlotOk(slot) && !localAnswer;
  357. if (relayOk) {
  358. // Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
  359. // multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
  360. // 18B cap, and the chopped frames were why those settings never landed on the controller.
  361. uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
  362. #if OPK_LOG
  363. if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
  364. Serial.printf(
  365. "# RELAY TRUNC cmd=%02X len=%u>%u\n",
  366. cmd, len, (unsigned)RELAY_MAXP);
  367. #endif
  368. relayEnqueue(cmd, pl, rl, false, (uint8_t)slot,
  369. relayQuery);
  370. if (relayQuery && slot >= 0 && slot < NSLOT) {
  371. g_slot[slot].pendingQueryCmd = cmd;
  372. queryArmed = true;
  373. }
  374. }
  375. // No real controller answer is EVER coming for this SET: it wasn't a tracked query
  376. // (haptics/0x87/0x9F/localAnswer/...), or it was one but couldn't relay right now (link down,
  377. // blocked). `resp` must still be made to reflect CMD, not left holding whatever unrelated
  378. // command's data happened to be cached there from an earlier request -- otherwise a later
  379. // GET(rid=1), seeing pendingQueryCmd==0, would silently hand Steam stale/wrong data instead of
  380. // a defined answer. Same echo shape the old always-run `default:` switch case used.
  381. if (!queryArmed) {
  382. S.resp[0] = cmd;
  383. S.resp[1] = len;
  384. if (pln)
  385. memcpy(S.resp + 2, pl, pln > 60 ? 60 : pln);
  386. S.resp_len = 63;
  387. }
  388. }
  389. #if OPK_LOG
  390. // Diagnostic-build only -- see the OUTPUT-path note: no Serial.printf on the 800B usbd-task stack in
  391. // production (LOCKUP mitigation). The feature SET is already captured to the OPK_LOG ring above.
  392. if (Serial.availableForWrite() > 80) {
  393. Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
  394. cmd, len);
  395. for (uint16_t i = 0; i < n && i < 14; i++)
  396. Serial.printf(" %02X", b[i]);
  397. Serial.println();
  398. }
  399. #endif
  400. if (rid == 2) {
  401. memset(S.resp, 0, sizeof S.resp);
  402. S.resp_len = 0;
  403. } else {
  404. // If we forwarded the packet to the controller, do NOT clear the buffer.
  405. // The controller will respond and put its data into the buffer.
  406. return;
  407. // rid == 1 will be handled by the controller, no need to respond here.
  408. }
  409. switch (cmd) {
  410. case IBEX_CMD_GET_ATTRIBUTES_VALUES: // 0x83
  411. S.resp[0] = IBEX_CMD_GET_ATTRIBUTES_VALUES;
  412. S.resp[1] = sizeof ATTR83;
  413. memcpy(S.resp + 2, ATTR83, sizeof ATTR83);
  414. S.resp_len = 63;
  415. break;
  416. case IBEX_CMD_GET_STRING_ATTRIBUTE: { // 0xAE
  417. uint8_t idx = pln > 0 ? pl[0] : 1;
  418. // Report-id 1 = string attributes of the bonded CONTROLLER, not the puck. Not handled here, this request
  419. // will have been forwarded to the controller by earlier code.
  420. S.resp[0] = IBEX_CMD_GET_STRING_ATTRIBUTE;
  421. S.resp[1] = 0x14; // todo: is this correct?
  422. S.resp[2] = idx;
  423. memset(S.resp + 3, 0, 60);
  424. // Any other idx -> "NA".
  425. const char *s = (idx == 0 || idx == 4) ? g_board :
  426. (idx == 1) ? g_unit :
  427. (idx == 3) ? "OpenPuck " :
  428. "NA";
  429. memcpy(S.resp + 3, s, strlen(s));
  430. if (idx == 3) {
  431. // The official puck has a 12-character GIT commit hash of the firmware in here.
  432. // Since I doubt any official process is ever going to parse / use this,
  433. // looks like the perfect place to put the OpenPuck version number.
  434. char *off = (char *)(S.resp + 3 + strlen(s));
  435. memcpy(off, OPK_BUILD_VERSION,
  436. strlen(OPK_BUILD_VERSION));
  437. off += strlen(OPK_BUILD_VERSION);
  438. memcpy(off, " ", 1);
  439. memcpy(off + 1, OPK_GIT_HASH, strlen(OPK_GIT_HASH));
  440. if (OPK_GIT_DIRTY != 0) {
  441. off += 1 + strlen(OPK_GIT_HASH);
  442. memcpy(off, "-dirty", 6);
  443. }
  444. }
  445. S.resp_len = 63;
  446. break;
  447. }
  448. // connection/version state per slot: value 0x02 = controller connected, 0x01 = not
  449. case IBEX_CMD_DONGLE_GET_WIRELESS_STATE: // 0xB4
  450. // SDL Triton polls this on init; treat like Steam contact so we forward 0x45
  451. hostStampAlive();
  452. S.resp[0] = IBEX_CMD_DONGLE_GET_WIRELESS_STATE;
  453. S.resp[1] = 0x01;
  454. // Report disconnected during the post-power-off hold so B4 agrees with the 0x79 disconnect (they used
  455. // different windows -- 500ms here vs the 300ms conn/DOWN edge -- so right after a power-off Steam's B4
  456. // probe answered "still connected" and contradicted the disconnect we just pushed).
  457. S.resp[2] = (slot >= 0 && slot < NSLOT && !g_xbox &&
  458. g_slot[slot].used && !slotPoweringOff(slot) &&
  459. (millis() - g_connReplyMs[slot] < 500)) ?
  460. 0x02 :
  461. 0x01;
  462. S.resp_len = 63;
  463. break;
  464. case IBEX_CMD_ENABLE_PAIRING: // 0xAD
  465. // TODO: Check if this should be relayed?
  466. g_pairing = (pln > 0 && pl[0] != 0);
  467. #if OPK_LOG
  468. Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
  469. #endif
  470. S.resp[0] = IBEX_CMD_ENABLE_PAIRING;
  471. S.resp[1] = 0;
  472. S.resp_len = 63;
  473. break;
  474. case IBEX_CMD_TRITON_A2_OBSERVED_PAIRING_RECORD: // 0xA2: write/clear THIS interface's slot
  475. if (len >= 24 && pln >= 24) {
  476. if (recEmpty(pl)) {
  477. S.used = false;
  478. memset(S.rec, 0, 24);
  479. } else {
  480. bool wasUsed = S.used;
  481. memcpy(S.rec, pl, 24);
  482. S.used = true;
  483. // Fresh bond into this slot: drop anything queued while it was empty (an
  484. // unbonded slot's ring is never flushed, so a stale frame -- e.g. the "off!"
  485. // from a host-suspend broadcast -- would land on the new controller).
  486. if (!wasUsed)
  487. relayClearSlot((uint8_t)slot);
  488. }
  489. g_dirty = true;
  490. #if OPK_LOG
  491. Serial.printf("# slot %d %s\n", slot,
  492. recEmpty(pl) ? "cleared" : "bonded");
  493. #endif
  494. }
  495. S.resp[0] = IBEX_CMD_TRITON_A2_OBSERVED_PAIRING_RECORD;
  496. S.resp[1] = 0;
  497. S.resp_len = 63;
  498. break;
  499. case IBEX_CMD_TRITON_A3_BOND_EVENT_OR_STATUS: // 0xA3: read THIS interface's slot
  500. S.resp[0] = IBEX_CMD_TRITON_A3_BOND_EVENT_OR_STATUS;
  501. S.resp[1] = 0x18;
  502. memset(S.resp + 2, 0, 24);
  503. if (S.used)
  504. memcpy(S.resp + 2, S.rec, 24);
  505. S.resp_len = 63;
  506. break;
  507. default:
  508. S.resp[0] = cmd;
  509. S.resp[1] = len;
  510. if (pln)
  511. memcpy(S.resp + 2, pl, pln > 60 ? 60 : pln);
  512. S.resp_len = 63;
  513. break;
  514. }
  515. }
  516. static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
  517. uint8_t *buf, uint16_t reqlen)
  518. {
  519. if (type != HID_REPORT_TYPE_FEATURE)
  520. return 0;
  521. // Do NOT treat a feature GET as "Steam is driving." A bare read is weak evidence: on Linux the kernel
  522. // hid-steam driver (and any hidapi enumerator) issues GET_FEATURE probes even with Steam's window closed,
  523. // which pinned steamDrivingGamepad() true forever and blocked the Steam-closed lizard fallback (keyboard/
  524. // mouse dead, and the lizard-off writes those readers pair with the probe also killed the autonomous
  525. // touchpad haptics). Steam actually taking over is detected by its OUTPUT/SET WRITES (the 0x87 lizard-off
  526. // heartbeat ~every 3s, and 0x82 haptics), which stamp g_steamAliveMs on the handleSet paths. So drop the
  527. // GET-based stamp entirely; a read alone no longer suppresses lizard.
  528. Slot &S = g_slot[slot];
  529. // A feature-01 query (0x83/0xAE/0xED, relayed for real when rid==1 -- see handleSet's `relayQuery`)
  530. // is still waiting on the controller's actual reply: `resp` only holds the local placeholder for it
  531. // right now (pendingQueryCmd, bonds.h; cleared by rf_link.cpp when the real answer lands).
  532. // STALL this GET so the host retries shortly after (-EPIPE), like the real Puck does.
  533. //
  534. // Returning plain 0 does NOT stall here: this device's reports are all NUMBERED (report id 1/2), and
  535. // TinyUSB's callback (adafruit/Adafruit_TinyUSB_Arduino, src/class/hid/hid_device.c)
  536. // auto-prepends that report id byte into the reply BEFORE calling this callback --
  537. // uint16_t xferlen = 0;
  538. // if (report_id != HID_REPORT_TYPE_INVALID && req_len > 1) { *report_buf++ = report_id; xferlen++; }
  539. // xferlen += tud_hid_get_report_cb(...); // <- our return value lands here
  540. // TU_ASSERT(xferlen > 0); // stalls the control transfer iff this is false
  541. // -- so xferlen is already 1 before we're even asked, and adding our 0 leaves it at 1: the assert
  542. // never fires and TinyUSB happily ships that lone prepended report-id byte as a "successful" 1-byte
  543. // reply.
  544. // This bug has been reported to upstream at https://github.com/hathach/tinyusb/issues/3814
  545. //
  546. // `xferlen` is uint16_t, so returning (uint16_t)-1 (0xFFFF) makes `1 + 0xFFFF` wrap to exactly 0
  547. // -- landing on the SAME TU_ASSERT(xferlen > 0) failure a genuinely-empty callback would have hit,
  548. // The additional check for rid is necessary so if the controller never replies, a new query for a
  549. // puck feature can get the puck un-stuck.
  550. // TODO: This is a very, very, very ugly solution and may break with library updates. Can we find a cleaner one?
  551. if (rid == 1 && S.pendingQueryCmd != 0 && reqlen > 1) {
  552. return 0xFFFF;
  553. }
  554. // rf_link.cpp will set pendingQueryCmd to 0 once the controller answered.
  555. uint16_t n = S.resp_len ? S.resp_len : 63;
  556. if (n > reqlen)
  557. n = reqlen;
  558. memcpy(buf, S.resp, n);
  559. // Flight recorder (every GET, un-deduped): a read STORM -- e.g. the "AE x39" identity hammering seen before
  560. // the identity fix -- is itself a wedge signal, so we want it filling the post-mortem trail if it happens.
  561. faultDiagTrace(FR_GET, (uint16_t)((rid << 8) | S.resp[0]));
  562. // Battery diagnostic: in gamepad (Steam) mode battery is read host-side via the feature channel, NOT the
  563. // forwarded 0x43 (that path is verbatim-identical to lizard mode, where battery works). Capture what Steam
  564. // GETs so a WebUSB-panel (or CDC) capture in Steam mode shows the report id it polls for battery (then we
  565. // answer it with g_battery in handleSet). De-duped by report id so the high-rate polling doesn't flood the
  566. // ring/console -- a freshly-requested id is logged once, then again only after 1s.
  567. {
  568. // Dedup by (rid, interface, response-cmd) so each interface's GET is captured (not collapsed to one
  569. // line across all 4 slots) -- the per-interface bond/identity responses are exactly what we need to
  570. // see -- while still not flooding on high-rate same-report polling.
  571. static uint8_t lastRid = 0xFF, lastSlot = 0xFF, lastCmd = 0xFF;
  572. static unsigned long lastMs = 0;
  573. if (rid != lastRid || (uint8_t)slot != lastSlot ||
  574. S.resp[0] != lastCmd || millis() - lastMs > 1000) {
  575. lastRid = rid;
  576. lastSlot = (uint8_t)slot;
  577. lastCmd = S.resp[0];
  578. lastMs = millis();
  579. // ring marker 0xFC = "host feature GET" (panel renders it as "GET rid=.."); payload = what we returned
  580. hapLogAdd(0xFC, rid, S.resp, n);
  581. // serial feature-command capture: what Steam READ on this interface + our answer
  582. fcPush(1, slot, rid, S.resp[0], (uint8_t)n, S.resp + 2,
  583. 10);
  584. #if OPK_LOG
  585. // Diagnostic build only -- no Serial.printf on the 800B usbd-task stack in production (LOCKUP
  586. // mitigation). The GET is captured to the OPK_LOG ring above for the WebUSB panel.
  587. if (Serial.availableForWrite() > 80)
  588. Serial.printf(
  589. "# GET if%d rid=%02X reqlen=%u -> %02X %02X %02X (batt=%u%%)\n",
  590. slot, rid, reqlen, S.resp[0], S.resp[1],
  591. S.resp[2],
  592. (slot >= 0 && slot < NSLOT) ?
  593. g_battery[slot] :
  594. 0);
  595. #endif
  596. }
  597. }
  598. return n;
  599. }
  600. // one callback pair per interface (the Adafruit core routes by interface to the matching object)
  601. #define SLOTCB(N) \
  602. static void setcb##N(uint8_t r, hid_report_type_t t, uint8_t const *b, \
  603. uint16_t n) \
  604. { \
  605. handleSet(N, r, t, b, n); \
  606. } \
  607. static uint16_t getcb##N(uint8_t r, hid_report_type_t t, uint8_t *bf, \
  608. uint16_t rl) \
  609. { \
  610. return handleGet(N, r, t, bf, rl); \
  611. }
  612. // clang-format off
  613. SLOTCB(0)
  614. SLOTCB(1)
  615. SLOTCB(2)
  616. SLOTCB(3)
  617. // clang-format on
  618. typedef uint16_t (*getcb_t)(uint8_t, hid_report_type_t, uint8_t *, uint16_t);
  619. typedef void (*setcb_t)(uint8_t, hid_report_type_t, uint8_t const *, uint16_t);
  620. static getcb_t GETCB[NSLOT] = { getcb0, getcb1, getcb2, getcb3 };
  621. static setcb_t SETCB[NSLOT] = { setcb0, setcb1, setcb2, setcb3 };
  622. // ===================== IController =====================
  623. void SteamPuckController::begin()
  624. {
  625. USBDevice.setID(0x28DE, 0x1304);
  626. USBDevice.setVersion(0x0201); // bcdUSB 2.01
  627. USBDevice.setDeviceVersion(2);
  628. // Distinct serial number so Windows keys a FRESH usbflags entry.
  629. if (g_debugCdcThisBoot) {
  630. snprintf(g_usbSerial, sizeof g_usbSerial, "%sC", g_unit);
  631. USBDevice.setSerialDescriptor(g_usbSerial);
  632. } else if (g_usbMode == MODE_LIZARD) {
  633. snprintf(g_usbSerial, sizeof g_usbSerial, "%sL", g_unit);
  634. USBDevice.setSerialDescriptor(g_usbSerial);
  635. } else {
  636. USBDevice.setSerialDescriptor(g_unit);
  637. }
  638. USBDevice.setManufacturerDescriptor("Valve Software");
  639. USBDevice.setProductDescriptor("Steam Controller Puck");
  640. const uint8_t *desc = (g_usbMode == MODE_LIZARD) ?
  641. PUCK_LIZARD_HID_DESC :
  642. PUCK_HID_DESC;
  643. const uint16_t descLen = (g_usbMode == MODE_LIZARD) ?
  644. sizeof PUCK_LIZARD_HID_DESC :
  645. sizeof PUCK_HID_DESC;
  646. for (int i = 0; i < NSLOT; i++) {
  647. hid[i].setReportDescriptor(desc, descLen);
  648. hid[i].setReportCallback(GETCB[i], SETCB[i]);
  649. // 1ms USB poll (was default 10ms = 100/s cap -> choppy)
  650. hid[i].setPollInterval(1);
  651. hid[i].begin();
  652. }
  653. }
  654. // Forward the controller's report 0x45 to Steam, or drive lizard kb/mouse when Steam is closed. PURELY a
  655. // USB-side decision -- changes nothing about the RF poll or the host->controller relay. Per-slot: each
  656. // controller's 0x45 goes to its OWN hid[slot], so Steam sees four independent inputs. Lizard, by contrast,
  657. // presents ONE mouse + keyboard (a desktop can only consume one): rfLizard merges every bonded controller's
  658. // input (g_in[] across all used slots, populated by the per-slot decode) onto hid[0], so all connected
  659. // controllers drive the same cursor/keys together -- and it works regardless of which bond slot is live.
  660. void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
  661. uint8_t bodyTlen)
  662. {
  663. (void)fresh;
  664. (void)bodyTlen;
  665. // Host asleep -> forward NOTHING. While suspended, every sendReport attempt can translate into a host wake,
  666. // making the PC wake on any controller movement. Waking is an explicit gesture only (Steam-button short
  667. // press / controller connect), handled in rf_link.cpp via the device-level USB resume signal.
  668. if (USBDevice.suspended())
  669. return;
  670. // Just woke? Hold off forwarding so the wake gesture's residual controller input doesn't click/type into
  671. // the freshly-woken desktop.
  672. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  673. return;
  674. if (slot < 0 || slot >= NSLOT)
  675. return;
  676. if (!g_slot[slot].used)
  677. return;
  678. // Report id: 0x45 = legacy main input; 0x42 = new-firmware (SC2 beta ~2026-07) main input (53B). Forward
  679. // whichever the controller actually sent, verbatim, under its own id -- both are declared in PUCK_HID_DESC
  680. // and this is exactly what the real puck does. rep[0] is the id byte (rep points at it in the F1 TLV).
  681. const uint8_t rid = rep[0];
  682. if (lizardActive()) {
  683. // All bonded controllers share ONE desktop mouse/keyboard (mounted on hid[0] regardless of
  684. // which RF slots are in use). rfLizard merges g_in[] across every used slot internally, so we
  685. // fire it ONCE per cycle -- gated on the lowest CONNECTED slot's report so it runs at a steady
  686. // cadence (not once per slot, which would multiply the output rate). g_in[] is populated by the
  687. // per-slot decode for BOTH report 0x45 and the beta 0x42 (byte-identical front section), so
  688. // lizard works on new-firmware controllers too without reading rep[] here.
  689. // Gate on the lowest RECENTLY-REPLYING slot, not merely the lowest bonded one: a bonded-but-
  690. // offline low slot (e.g. a stale/phantom bond from a cloned backup) must not starve lizard when
  691. // a higher slot is the live controller (main's #98 fix -- do not regress it). onReport45 only
  692. // runs for a slot that just decoded a report, so the reporting slot is itself connected.
  693. unsigned long now = millis();
  694. int lizSlot = -1;
  695. for (int s = 0; s < NSLOT; s++)
  696. if (g_slot[s].used && g_connReplyMs[s] &&
  697. (now - g_connReplyMs[s]) < 1200u) {
  698. lizSlot = s;
  699. break;
  700. }
  701. if (slot == lizSlot)
  702. rfLizard(&hid[0], &hid[0], 0x40, 0x41);
  703. } else {
  704. // body length after the id byte, clamped to the descriptor's declared size for this report id
  705. // (0x42 = 53B vendor input, 0x45 = 45B input).
  706. uint8_t maxb = (rid == 0x42) ? 53 : 45;
  707. uint8_t blen = bodyTlen - 1;
  708. if (blen > maxb)
  709. blen = maxb;
  710. // forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
  711. // only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
  712. // stair-step. g_fwdNewOnly toggles for A/B.
  713. //
  714. // Do NOT gate on hid[slot].ready() here: usbTxHid enqueues into a ring buffer (drop-oldest)
  715. // that exists precisely to hold a report while the endpoint is briefly busy -- usbTxDrain
  716. // sends it the instant the host next polls. Gating on ready() at enqueue time DROPPED every
  717. // fresh report that landed while the endpoint was busy (~1 ms after each send), which at
  718. // 300+ captured reports/s silently discarded ~a third of them (measured: RF new/s ~313 but
  719. // host delivered ~180). Always enqueue; the ring paces delivery to the host without loss.
  720. bool send = (fresh || !g_fwdNewOnly);
  721. // Content dedup: the counter byte (rep[1]) free-runs, so drop a report whose body AFTER the
  722. // counter is byte-identical to the last one forwarded on this slot -- a true repeat (same
  723. // physical input, bumped counter), not a new sample. Caps delivery at the controller's real
  724. // distinct-report rate instead of the (higher) poll rate. See g_fwdContentDedup above.
  725. if (send && g_fwdContentDedup && blen >= 2) {
  726. static uint8_t lastBody[NSLOT][53];
  727. static uint8_t lastBodyLen[NSLOT] = { 0 };
  728. uint8_t clen =
  729. (uint8_t)(blen - 1); // bytes after the counter
  730. if (clen > sizeof lastBody[0])
  731. clen = sizeof lastBody[0];
  732. if (lastBodyLen[slot] == clen &&
  733. memcmp(lastBody[slot], rep + 2, clen) == 0)
  734. send = false; // identical input, only the counter moved
  735. else {
  736. lastBodyLen[slot] = clen;
  737. memcpy(lastBody[slot], rep + 2, clen);
  738. }
  739. }
  740. if (send)
  741. usbTxHid(
  742. &hid[slot], rid, rep + 1,
  743. blen); // Steam/SDL Triton: input report 0x45 (old) / 0x42 (new fw)
  744. }
  745. }
  746. // Forward the controller's NON-input status reports (0x43 power/battery, 0x44) to Steam verbatim -- the real
  747. // puck does this and it's how Steam reads battery. Same host-asleep / post-resume gating as 0x45; no lizard
  748. // path (status reports aren't input, so they forward regardless of the lizard decision). Per-slot: each
  749. // controller's status goes to its own hid[slot].
  750. void SteamPuckController::onAuxReport(int slot, uint8_t rid,
  751. const uint8_t *data, uint8_t n)
  752. {
  753. if (USBDevice.suspended())
  754. return;
  755. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  756. return;
  757. if (slot < 0 || slot >= NSLOT)
  758. return;
  759. // Forward the controller's status report VERBATIM (the real puck does this; it's how the host reads
  760. // battery). Padding the report to the descriptor-declared length broke battery in both lizard and
  761. // Steam, so it's reverted -- send exactly what the controller sent.
  762. if (g_slot[slot].used && hid[slot].ready()) {
  763. // capture the pushed status report (0x43 battery / 0x44) device->host for the WebUSB panel: this is
  764. // the channel Steam actually reads battery from; marker 0xFB = "->host push".
  765. hapLogAdd(0xFB, rid, data, n);
  766. usbTxHid(&hid[slot], rid, data, n);
  767. }
  768. }
  769. // wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they only
  770. // wake when actual mouse/keyboard input follows. So on a deliberate wake gesture we play a HARMLESS mouse
  771. // JIGGLE (move a few px right, then back -- NET ZERO cursor, NO button): real mouse activity wakes the host
  772. // but clicks/activates nothing (an open Start menu stays open). Queued by wakeEvent() (rf_link, on a Steam
  773. // short press / controller connect while suspended); delivered once the suspended bus has resumed. Per-slot
  774. // so every connected interface gets the jiggle (Windows can credit any of them with the wake).
  775. static uint8_t g_nudgeStep[NSLOT] = { 0 }; // 0=idle; 1=jiggle+, 2=jiggle-
  776. static unsigned long g_nudgeMs[NSLOT] = { 0 };
  777. #define NUDGE_JIGGLE_PX 10
  778. void SteamPuckController::wakeEvent()
  779. {
  780. // arm the jiggle on every connected slot -- any one of them is enough to wake Windows
  781. for (int s = 0; s < NSLOT; s++) {
  782. if (g_slot[s].used) {
  783. g_nudgeStep[s] = 1;
  784. g_nudgeMs[s] = millis();
  785. }
  786. }
  787. }
  788. static void wakeNudgeTask()
  789. {
  790. if (USBDevice.suspended())
  791. return; // wait for resume; reports can't cross a suspended bus
  792. // Expire stale arms (bus never resumed) and see if any slot still wants a wake nudge.
  793. bool armed = false;
  794. for (int s = 0; s < NSLOT; s++) {
  795. if (!g_nudgeStep[s])
  796. continue;
  797. if (millis() - g_nudgeMs[s] > 5000) {
  798. g_nudgeStep[s] = 0;
  799. continue;
  800. }
  801. armed = true;
  802. }
  803. if (!armed)
  804. return;
  805. // Ride the BOOT MOUSE -- the interface Windows armed as the wake source; a gamepad-slot report does not
  806. // wake Modern Standby. A single jiggle on it wakes the host regardless of how many slots are connected.
  807. if (wakeHidPresent()) {
  808. if (!wakeHidReady())
  809. return;
  810. static unsigned long stepMs = 0;
  811. static uint8_t step = 1;
  812. if (millis() - stepMs < 15)
  813. return; // pace the edges
  814. stepMs = millis();
  815. wakeHidMove((step == 1) ? NUDGE_JIGGLE_PX : -NUDGE_JIGGLE_PX,
  816. 0);
  817. if (step >=
  818. 2) { // jiggle (right, then back) delivered -> disarm every slot
  819. step = 1;
  820. for (int s = 0; s < NSLOT; s++)
  821. g_nudgeStep[s] = 0;
  822. } else
  823. step++;
  824. return;
  825. }
  826. // Fallback (debug-CDC boot: no wake mouse): per-slot jiggle on the gamepad slot HIDs.
  827. static unsigned long stepMs[NSLOT] = { 0 };
  828. for (int s = 0; s < NSLOT; s++) {
  829. if (!g_nudgeStep[s])
  830. continue;
  831. if (!hid[s].ready())
  832. continue;
  833. if (millis() - stepMs[s] < 15)
  834. continue; // pace the edges
  835. stepMs[s] = millis();
  836. hid_mouse_report_t m;
  837. m.buttons = 0;
  838. m.x = (g_nudgeStep[s] == 1) ? NUDGE_JIGGLE_PX :
  839. -NUDGE_JIGGLE_PX;
  840. m.y = 0;
  841. m.wheel = 0;
  842. m.pan = 0;
  843. usbTxHid(&hid[s], 0x40, &m,
  844. sizeof m); // jiggle right, then back
  845. g_nudgeStep[s] = (g_nudgeStep[s] >= 2) ?
  846. 0 :
  847. (uint8_t)(g_nudgeStep[s] + 1);
  848. }
  849. }
  850. // USB connection presentation (like the real dongle): report 0x79 = connection state (01=disc, 02=conn),
  851. // edge-triggered, + periodic 0x7B status. Live-captured: this is what Steam reads to mark the controller
  852. // connected. Without it Steam shows disconnected even though 0x45 input is streaming.
  853. void SteamPuckController::task()
  854. {
  855. wakeNudgeTask();
  856. {
  857. static bool wasSusp = false;
  858. // stamp the suspended->active edge for the post-resume mute
  859. bool susp = USBDevice.suspended();
  860. if (wasSusp && !susp)
  861. g_resumeMs = millis();
  862. wasSusp = susp;
  863. }
  864. // no periodic 0x79/0x7B while the host sleeps -- those sends can wake it too
  865. if (USBDevice.suspended())
  866. return;
  867. // Lizard<->Steam handoff: release whatever the OUTGOING path was holding. lizardActive() is a pure
  868. // runtime decision -- Steam opening/closing (its ~3s settings heartbeat starting/stopping) flips it with
  869. // NO USB re-enumeration -- so a key/mouse-button held when Steam TAKES OVER, or a gamepad button held
  870. // when Steam CLOSES, is otherwise never released and sticks on the host until a reconnect or power-cycle
  871. // (the reported "stuck input after switching modes"). The link-drop neutral in rf_link only covers an RF
  872. // outage, not this handoff. Fire a neutral through the path we're leaving on each edge. Placed after the
  873. // suspend early-return: while suspended nothing can be sent, and a flip across a sleep is covered on
  874. // resume (this edge fires once, plus the post-resume input mute). MODE_LIZARD never leaves lizard, so
  875. // this is inert there.
  876. {
  877. static bool wasLizard = lizardActive();
  878. bool nowLizard = lizardActive();
  879. if (nowLizard != wasLizard) {
  880. if (wasLizard) {
  881. // leaving lizard -> release held desktop keyboard/mouse/consumer
  882. rfLizardRelease(&hid[0], &hid[0], 0x40, 0x41);
  883. } else {
  884. // leaving gamepad-forward -> release held 0x45 buttons/sticks/triggers on every slot
  885. static const uint8_t neutral45[45] = { 0 };
  886. for (int s = 0; s < NSLOT; s++)
  887. if (g_slot[s].used && hid[s].ready())
  888. usbTxHid(&hid[s], 0x45,
  889. neutral45,
  890. sizeof neutral45);
  891. }
  892. wasLizard = nowLizard;
  893. }
  894. }
  895. // Per-slot 0x79/0x7B: each connected slot reports its OWN edge and its OWN status. State arrays are
  896. // per-slot so each controller's "connected" edge fires once and is re-sent only until Steam acks THAT
  897. // slot. The real puck's per-slot edge-triggered 0x79 prevents re-triggering Steam's connect-chime loop.
  898. static bool usbConn[NSLOT] = { 0 };
  899. static unsigned long last79[NSLOT] = { 0 }, last7B[NSLOT] = { 0 },
  900. connEdgeMs[NSLOT] = { 0 },
  901. discEdgeMs[NSLOT] = { 0 }, last43[NSLOT] = { 0 },
  902. poHandled[NSLOT] = { 0 };
  903. for (int s = 0; s < NSLOT; s++) {
  904. if (!g_slot[s].used || !hid[s].ready())
  905. continue;
  906. // Power-off just relayed to this slot -> force ONE clean disconnect to Steam now, regardless of the
  907. // prior edge state. The controller's noisy shutdown (it keeps streaming F1 for ~1s after "off!") can
  908. // leave usbConn desynced from what Steam shows, so the ordinary conn!=usbConn edge sometimes never
  909. // fired -> Steam kept the controller in its list (the "doesn't get removed" case). Anchor discEdgeMs
  910. // too so the bounded resend covers a dropped packet.
  911. if (g_powerOffMs[s] && g_powerOffMs[s] != poHandled[s]) {
  912. poHandled[s] = g_powerOffMs[s];
  913. uint8_t st = 0x01;
  914. hapLogAdd(0xFB, 0x79, &st, 1); // ->host push (capture)
  915. usbTxHid(&hid[s], 0x79, &st, 1);
  916. usbConn[s] = false;
  917. discEdgeMs[s] = millis();
  918. last79[s] = millis();
  919. }
  920. // Hold the slot DISCONNECTED through the controller's post-power-off F1 tail (see slotPoweringOff);
  921. // otherwise a stray dying reply bounces conn true -> a phantom 0x79=02 that Steam reads as a reconnect
  922. // and answers by re-running its connect config (the "reappears for a split second").
  923. bool conn = !slotPoweringOff(s) &&
  924. (millis() - g_connReplyMs[s] < 300);
  925. // 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
  926. // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
  927. // unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before
  928. // Steam consumes 0x45 -> a loop of connect-time haptic buzzes. Resending until acked still covers
  929. // "Steam missed the edge".
  930. bool steamAcked = g_steamAliveMs &&
  931. (int32_t)(g_steamAliveMs - connEdgeMs[s]) >=
  932. 0;
  933. // The DISCONNECT edge (0x79=01) also needs resending: it used to be sent exactly ONCE, so a single
  934. // dropped report left Steam believing the controller was still connected indefinitely -- observed as
  935. // "WebUSB panel shows the slot DOWN (g_connReplyMs stale) while Steam still lists it connected, and the
  936. // controller light is solid (it re-adopted the beacon) but no input flows." Resend disc every 750ms but
  937. // BOUNDED to DISC_RESEND_MS so a lost packet converges without the forever-spam the "real puck sends
  938. // 0x79 once" note warns against.
  939. bool discResend = !conn &&
  940. (millis() - discEdgeMs[s] < DISC_RESEND_MS);
  941. if (conn != usbConn[s] ||
  942. (conn && !steamAcked && millis() - last79[s] >= 750) ||
  943. (discResend && millis() - last79[s] >= 750)) {
  944. if (conn && !usbConn[s])
  945. connEdgeMs[s] = millis();
  946. if (!conn &&
  947. usbConn[s]) // connect->disc edge: anchor the resend window
  948. discEdgeMs[s] = millis();
  949. uint8_t st = conn ? 0x02 : 0x01;
  950. hapLogAdd(0xFB, 0x79, &st, 1); // ->host push (capture)
  951. usbTxHid(&hid[s], 0x79, &st, 1);
  952. usbConn[s] = conn;
  953. last79[s] = millis();
  954. } else if (conn && millis() - last7B[s] >= 2000) {
  955. // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed
  956. // dBm (capture showed 0xDD = -35) -- patch in the smoothed RSSI the radio samples on each
  957. // controller reply (rf_link). 0 = no sample yet -> keep the capture value rather than garbage.
  958. //
  959. // CALIBRATION: our raw RSSISAMPLE reads ~RSSI_DBM_OFFSET dB lower than the real Valve puck at the
  960. // same distance (Pro Micro PCB-trace antenna vs Valve's tuned front-end). The 2Mbit ESB link has
  961. // ~55dB margin, so a -75dBm reading still works across a house, but Steam's bar maps raw dBm to
  962. // "weak" long before that. The offset lines our close-range value up with the puck's captured -35
  963. // so the bar tracks usable range, not antenna gain. Clamp keeps it in a sane window.
  964. uint8_t s7b[12] = { 0xF7, 0x01, 0x89, 0x00, 0x00, 0x00,
  965. 0x03, 0x00, 0xDD, 0x00, 0x3A, 0x02 };
  966. if (g_linkRssi[s]) {
  967. int mag = (int)g_linkRssi[s] - RSSI_DBM_OFFSET;
  968. if (mag < 25)
  969. mag = 25;
  970. else if (mag > 95)
  971. mag = 95;
  972. s7b[8] = (uint8_t)(0u - (uint8_t)mag);
  973. }
  974. hapLogAdd(0xFB, 0x7B, s7b, 12); // ->host push (capture)
  975. usbTxHid(&hid[s], 0x7B, s7b, 12);
  976. last7B[s] = millis();
  977. }
  978. // Synthesized 0x43 = ID_TRITON_BATTERY_STATUS for SDL/Steam's gamepad driver. The verbatim forward of
  979. // the controller's own 0x43 (onAuxReport) is what the LIZARD/kernel path reads, but SDL's Triton driver
  980. // requires the FULL TritonBatteryStatus_t length (r >= 1 + 14) and lapses to the "wired" glyph without a
  981. // fresh one -- so we push a clean 14-byte report from g_battery every 2s. Body: [ucChargeState][ucBattery
  982. // Level][voltages/current/temp = 0]; SDL only reads the first two. Map unknown/reset state -> discharging
  983. // so it shows ON_BATTERY + % rather than UNKNOWN. Skipped until the controller has reported a level.
  984. if (conn && g_battery[s] && millis() - last43[s] >= 2000) {
  985. uint8_t st = g_batteryState[s];
  986. // EChargeState discharging -> SDL_POWERSTATE_ON_BATTERY
  987. if (st != 1 && st != 2 && st != 4)
  988. st = 1;
  989. uint8_t b43[14] = { 0 };
  990. b43[0] = st; // ucChargeState
  991. b43[1] = g_battery[s]; // ucBatteryLevel (percent)
  992. hapLogAdd(0xFB, 0x43, b43, 14); // ->host push (capture)
  993. usbTxHid(&hid[s], 0x43, b43, sizeof b43);
  994. last43[s] = millis();
  995. }
  996. }
  997. // Reset edge state for slots that are no longer used/ready (so a re-bond sees a fresh edge).
  998. for (int s = 0; s < NSLOT; s++)
  999. if (!g_slot[s].used || !hid[s].ready())
  1000. usbConn[s] = false;
  1001. }