puck_hid.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. #include "puck_hid.h"
  2. #include "bonds.h"
  3. #include "config.h"
  4. #include "identity.h"
  5. #include "haptics.h"
  6. #include "rf_link.h"
  7. #include "triton.h"
  8. #include "mode_lizard.h"
  9. #include <Adafruit_TinyUSB.h>
  10. #include <Arduino.h>
  11. #include <string.h>
  12. uint8_t g_fwdNewOnly = 1;
  13. SteamPuckController g_steamPuck;
  14. // Cloned puck HID report descriptor: mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte FEATURE
  15. // command reports on report id 1/2. Each of the 4 interfaces uses this.
  16. static const uint8_t PUCK_HID_DESC[] = {
  17. 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
  18. 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  19. 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01,
  20. 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02,
  21. 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38,
  22. 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1,
  23. 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25,
  24. 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29,
  25. 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0,
  26. 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26,
  27. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44,
  28. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81,
  29. 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01,
  30. 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  31. 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26,
  32. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45,
  33. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81,
  34. 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09,
  35. 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  36. 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26,
  37. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83,
  38. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91,
  39. 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08,
  40. 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  41. 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26,
  42. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87,
  43. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91,
  44. 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F,
  45. 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  46. 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09,
  47. 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0
  48. };
  49. // Pure MODE_LIZARD only: same puck/lizard descriptor plus a tiny Consumer Control report for media keys.
  50. // MODE_STEAM must keep PUCK_HID_DESC verbatim so Steam's controller/haptics path sees the normal puck shape.
  51. static const uint8_t PUCK_LIZARD_HID_DESC[] = {
  52. 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
  53. 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  54. 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01,
  55. 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02,
  56. 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38,
  57. 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1,
  58. 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25,
  59. 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29,
  60. 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0,
  61. 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26,
  62. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44,
  63. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81,
  64. 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01,
  65. 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  66. 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26,
  67. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45,
  68. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81,
  69. 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09,
  70. 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  71. 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26,
  72. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83,
  73. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91,
  74. 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08,
  75. 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  76. 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26,
  77. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87,
  78. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91,
  79. 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F,
  80. 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  81. 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09,
  82. 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0,
  83. 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x03, 0x15, 0x00, 0x25, 0x01,
  84. 0x09, 0xE9, 0x09, 0xEA, 0x75, 0x01, 0x95, 0x02, 0x81, 0x02, 0x75, 0x06,
  85. 0x95, 0x01, 0x81, 0x01, 0xC0
  86. };
  87. static Adafruit_USBD_HID hid[NSLOT];
  88. // ===================== seamless LIZARD decision =====================
  89. // Steam, while running, re-sends settings report 0x87 (lizard-off) every ~3s as a heartbeat (captured on HW),
  90. // and ANY OUTPUT report likewise stamps g_steamAliveMs. When the heartbeat stops we fall back to lizard, so the
  91. // controller drives desktop keyboard+mouse whenever Steam isn't running. MODE_LIZARD forces lizard always.
  92. // millis of last Steam OUTPUT/settings write; 0 at boot => lizard until Steam appears
  93. static unsigned long g_steamAliveMs = 0;
  94. // Fall back to lizard this long after Steam's ~3s settings heartbeat stops. Keep >2x the cadence: the haptic
  95. // relay is gated by !lizardActive(), so a shorter window lets a jittered/delayed heartbeat flip lizard
  96. // mid-session while Steam is still running -> a haptic arriving in that window gets gated out (dropped). 7s
  97. // tolerates one missed beat. Do not shorten without a lizard trigger independent of the heartbeat timeout.
  98. #define LIZARD_WD_MS 7000u
  99. static bool g_autoLizard =
  100. true; // master switch; false => Steam mode always forwards 0x45
  101. // Single source of truth, shared by the USB input path AND the haptic relay gate: if we ever relay a 0x82 to
  102. // the controller while presenting lizard (Steam isn't reading 0x45 back), Steam loops the same haptic -> buzz.
  103. static inline bool steamDrivingGamepad()
  104. {
  105. return g_steamAliveMs && (millis() - g_steamAliveMs < LIZARD_WD_MS);
  106. }
  107. static inline bool lizardActive()
  108. {
  109. return modeIsPuck(g_usbMode) &&
  110. (g_usbMode == MODE_LIZARD ||
  111. (g_autoLizard && !steamDrivingGamepad()));
  112. }
  113. static inline void hostStampAlive()
  114. {
  115. g_steamAliveMs = millis();
  116. }
  117. // Right after the host resumes from suspend, MUTE input forwarding briefly. Otherwise the controller's input
  118. // in that instant (a trackpad click/trigger, or residual button state from the wake gesture) gets forwarded
  119. // as a real click/keypress into the just-woken desktop. Set when task() sees the suspended->active transition.
  120. static unsigned long g_resumeMs = 0;
  121. #define POST_RESUME_MUTE_MS 1500u
  122. // ===================== puck feature command channel =====================
  123. // `slot` is the interface index (interface N == bond slot N).
  124. static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
  125. uint8_t const *b, uint16_t n)
  126. {
  127. // Steam OUTPUT reports 0x80-0x89. The haptic/actuator reports (0x80-0x86) are relayed to the controller,
  128. // and ONLY when they arrive on the CONNECTED slot's interface: we have one controller but expose 4 puck
  129. // slots, and a report aimed at a DIFFERENT slot made the controller buzz at random (the slot gate below
  130. // fixes that). Must NOT clamp to 0x82-only: ping/grip/test haptics ride other report IDs (0x85/0x86). The
  131. // 63-byte settings/config reports 0x87/0x88/0x89 are NOT haptics and reach the controller via the
  132. // feature-0x01 passthrough path instead.
  133. if (type == HID_REPORT_TYPE_OUTPUT) {
  134. if (rid >= 0x80 && rid <= 0x89) {
  135. // capture ALL OUTPUT reports (even un-relayed) for the 'H' dump
  136. hapLogAdd((uint8_t)slot, rid, b, n);
  137. // ANY Steam OUTPUT report (not just the 0x87 heartbeat) means Steam is present and driving ->
  138. // leave lizard for gamepad NOW, so a haptic arriving before the first 0x87 isn't relayed while
  139. // we're still presenting lizard (-> buzz loop).
  140. hostStampAlive();
  141. }
  142. // Post-resume mute also gates haptics: while onReport45 is muted Steam reads NO 0x45 back, which is the
  143. // exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
  144. bool muted = g_resumeMs &&
  145. millis() - g_resumeMs < POST_RESUME_MUTE_MS;
  146. if (rid >= 0x80 && rid <= 0x86 && n >= 1 &&
  147. hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
  148. if (!haptic82Blocked(slot)) {
  149. relayEnqueue(rid, b,
  150. (uint8_t)(n > RELAY_MAXP ?
  151. RELAY_MAXP :
  152. n),
  153. (uint8_t)slot);
  154. // Track on/off from what was actually RELAYED (= the controller's believed state): a
  155. // BLOCKED stop must leave "on" set (controller may be latched -> reconnect stop-burst),
  156. // a blocked ON must not set it (nothing reached the controller -> no spurious clicks).
  157. if (rid == 0x82)
  158. haptic82HostReport(b, n);
  159. }
  160. }
  161. if (Serial.availableForWrite() > 80) {
  162. Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
  163. n);
  164. for (uint16_t i = 0; i < n && i < 14; i++)
  165. Serial.printf(" %02X", b[i]);
  166. Serial.println();
  167. }
  168. return;
  169. }
  170. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  171. return;
  172. Slot &S = g_slot[slot];
  173. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  174. const uint8_t *pl = b + 2;
  175. uint16_t pln = (n >= 2) ? n - 2 : 0;
  176. // settings/haptic/LED report (incl. 0x87 lizard-off heartbeat, SDL Triton lizard-disable)
  177. if (cmd >= 0x80 && cmd <= 0x89)
  178. hostStampAlive();
  179. // Controller power-off: Steam's "turn off controller" is feature-0x01 frame 9F 04 6F 66 66 21 ("off!"),
  180. // confirmed from a real puck capture. The feature-0x01 relay below forwards it once; hapticSendShutdown()
  181. // bursts it for NO-ACK reliability (the single hook the test button + host-suspend also drive).
  182. if (rid == 1 && cmd == 0x9F)
  183. hapticSendShutdown();
  184. // report 0x01 = raw passthrough -> queue for RF relay to the controller
  185. if (rid == 1 && n >= 2) {
  186. // capture EVERY relayed feature-1 command for the WebUSB capture view (haptics, LED SET_LED_COLOR,
  187. // 0x87 settings, 0x9F power-off). Log shows cmd as "rid"; bytes start [cmd][len]...
  188. hapLogAdd((uint8_t)slot, cmd, b, n);
  189. bool haptic82 = (cmd == 0x82 && len <= pln);
  190. bool muted = g_resumeMs &&
  191. millis() - g_resumeMs < POST_RESUME_MUTE_MS;
  192. // never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
  193. bool relayOk = hapticRelaySlotOk(slot) &&
  194. !(haptic82 && (lizardActive() || muted));
  195. if (relayOk && (!haptic82 || !haptic82Blocked(slot))) {
  196. // Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
  197. // multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
  198. // 18B cap, and the chopped frames were why those settings never landed on the controller.
  199. uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
  200. if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
  201. Serial.printf(
  202. "# RELAY TRUNC cmd=%02X len=%u>%u\n",
  203. cmd, len, (unsigned)RELAY_MAXP);
  204. relayEnqueue(cmd, pl, rl, (uint8_t)slot);
  205. // track from RELAYED frames only (see the OUTPUT path)
  206. if (haptic82)
  207. haptic82HostReport(pl, len);
  208. }
  209. }
  210. if (Serial.availableForWrite() > 80) {
  211. Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
  212. cmd, len);
  213. for (uint16_t i = 0; i < n && i < 14; i++)
  214. Serial.printf(" %02X", b[i]);
  215. Serial.println();
  216. }
  217. memset(S.resp, 0, sizeof S.resp);
  218. S.resp_len = 0;
  219. switch (cmd) {
  220. case 0x83:
  221. S.resp[0] = 0x83;
  222. S.resp[1] = sizeof ATTR83;
  223. memcpy(S.resp + 2, ATTR83, sizeof ATTR83);
  224. S.resp_len = 63;
  225. break;
  226. case 0xAE: {
  227. uint8_t idx = pln > 0 ? pl[0] : 1;
  228. const char *s = (idx == 0) ? g_board :
  229. (idx == 1) ? g_unit :
  230. "NA";
  231. S.resp[0] = 0xAE;
  232. S.resp[1] = 0x14;
  233. S.resp[2] = idx;
  234. memset(S.resp + 3, 0, 60);
  235. memcpy(S.resp + 3, s, strlen(s));
  236. S.resp_len = 63;
  237. break;
  238. }
  239. // connection/version state per slot: value 0x02 = controller connected, 0x01 = not
  240. case 0xB4:
  241. // SDL Triton polls this on init; treat like Steam contact so we forward 0x45
  242. hostStampAlive();
  243. S.resp[0] = 0xB4;
  244. S.resp[1] = 0x01;
  245. S.resp[2] = (slot >= 0 && slot < NSLOT && !g_xbox &&
  246. g_slot[slot].used &&
  247. (millis() - g_connReplyMs[slot] < 500)) ?
  248. 0x02 :
  249. 0x01;
  250. S.resp_len = 63;
  251. break;
  252. case 0xAD:
  253. g_pairing = (pln > 0 && pl[0] != 0);
  254. Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
  255. S.resp[0] = 0xAD;
  256. S.resp[1] = 0;
  257. S.resp_len = 63;
  258. break;
  259. case 0xA2: // write/clear THIS interface's slot
  260. if (len >= 24 && pln >= 24) {
  261. if (recEmpty(pl)) {
  262. S.used = false;
  263. memset(S.rec, 0, 24);
  264. } else {
  265. memcpy(S.rec, pl, 24);
  266. S.used = true;
  267. }
  268. g_dirty = true;
  269. Serial.printf("# slot %d %s\n", slot,
  270. recEmpty(pl) ? "cleared" : "bonded");
  271. }
  272. S.resp[0] = 0xA2;
  273. S.resp[1] = 0;
  274. S.resp_len = 63;
  275. break;
  276. case 0xA3: // read THIS interface's slot
  277. S.resp[0] = 0xA3;
  278. S.resp[1] = 0x18;
  279. memset(S.resp + 2, 0, 24);
  280. if (S.used)
  281. memcpy(S.resp + 2, S.rec, 24);
  282. S.resp_len = 63;
  283. break;
  284. default:
  285. S.resp[0] = cmd;
  286. S.resp[1] = len;
  287. if (pln)
  288. memcpy(S.resp + 2, pl, pln > 60 ? 60 : pln);
  289. S.resp_len = 63;
  290. break;
  291. }
  292. }
  293. static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
  294. uint8_t *buf, uint16_t reqlen)
  295. {
  296. (void)rid;
  297. if (type != HID_REPORT_TYPE_FEATURE)
  298. return 0;
  299. // host reading feature data => gamepad consumer active (Steam/SDL)
  300. if (g_usbMode == MODE_STEAM)
  301. hostStampAlive();
  302. Slot &S = g_slot[slot];
  303. uint16_t n = S.resp_len ? S.resp_len : 63;
  304. if (n > reqlen)
  305. n = reqlen;
  306. memcpy(buf, S.resp, n);
  307. return n;
  308. }
  309. // one callback pair per interface (the Adafruit core routes by interface to the matching object)
  310. #define SLOTCB(N) \
  311. static void setcb##N(uint8_t r, hid_report_type_t t, uint8_t const *b, \
  312. uint16_t n) \
  313. { \
  314. handleSet(N, r, t, b, n); \
  315. } \
  316. static uint16_t getcb##N(uint8_t r, hid_report_type_t t, uint8_t *bf, \
  317. uint16_t rl) \
  318. { \
  319. return handleGet(N, r, t, bf, rl); \
  320. }
  321. // clang-format off
  322. SLOTCB(0)
  323. SLOTCB(1)
  324. SLOTCB(2)
  325. SLOTCB(3)
  326. // clang-format on
  327. typedef uint16_t (*getcb_t)(uint8_t, hid_report_type_t, uint8_t *, uint16_t);
  328. typedef void (*setcb_t)(uint8_t, hid_report_type_t, uint8_t const *, uint16_t);
  329. static getcb_t GETCB[NSLOT] = { getcb0, getcb1, getcb2, getcb3 };
  330. static setcb_t SETCB[NSLOT] = { setcb0, setcb1, setcb2, setcb3 };
  331. // ===================== IController =====================
  332. void SteamPuckController::begin()
  333. {
  334. USBDevice.setID(0x28DE, 0x1304);
  335. // Distinct bcdDevice so Windows keys a FRESH usbflags entry (cache is VID:PID:bcdDevice) and actually runs
  336. // MS OS 2.0 / WinUSB binding for the WebUSB vendor interface, instead of reusing a stale "no WinUSB" entry
  337. // tied to the real Steam Controller (28DE:1304, no WebUSB interface). The normal (wake-mouse) and one-shot
  338. // debug (CDC) boots present DIFFERENT interface sets, so they need DIFFERENT bcdDevice values or Windows
  339. // serves one's cached descriptor for the other across a reboot.
  340. USBDevice.setDeviceVersion(
  341. g_debugCdcThisBoot ?
  342. 0x0212 :
  343. (g_usbMode == MODE_LIZARD ? 0x0213 : 0x0211));
  344. USBDevice.setManufacturerDescriptor("Valve Software");
  345. USBDevice.setProductDescriptor("Steam Controller Puck");
  346. const uint8_t *desc = (g_usbMode == MODE_LIZARD) ?
  347. PUCK_LIZARD_HID_DESC :
  348. PUCK_HID_DESC;
  349. const uint16_t descLen = (g_usbMode == MODE_LIZARD) ?
  350. sizeof PUCK_LIZARD_HID_DESC :
  351. sizeof PUCK_HID_DESC;
  352. for (int i = 0; i < NSLOT; i++) {
  353. hid[i].setReportDescriptor(desc, descLen);
  354. hid[i].setReportCallback(GETCB[i], SETCB[i]);
  355. // 1ms USB poll (was default 10ms = 100/s cap -> choppy)
  356. hid[i].setPollInterval(1);
  357. hid[i].begin();
  358. }
  359. }
  360. // Forward the controller's report 0x45 to Steam, or drive lizard kb/mouse when Steam is closed. PURELY a
  361. // USB-side decision -- changes nothing about the RF poll or the host->controller relay. Per-slot: each
  362. // controller's 0x45 goes to its OWN hid[slot], so Steam sees four independent inputs. Lizard remains
  363. // single-controller (slot 0); the other slots stay quiet in lizard mode -- one mouse + one keyboard is
  364. // the only thing a desktop can consume, and the comment in mode_lizard.h documents this.
  365. void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
  366. uint8_t bodyTlen)
  367. {
  368. (void)fresh;
  369. (void)bodyTlen;
  370. // Host asleep -> forward NOTHING. While suspended, every sendReport attempt can translate into a host wake,
  371. // making the PC wake on any controller movement. Waking is an explicit gesture only (Steam-button short
  372. // press / controller connect), handled in rf_link.cpp via the device-level USB resume signal.
  373. if (USBDevice.suspended())
  374. return;
  375. // Just woke? Hold off forwarding so the wake gesture's residual controller input doesn't click/type into
  376. // the freshly-woken desktop.
  377. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  378. return;
  379. if (slot < 0 || slot >= NSLOT)
  380. return;
  381. if (!g_slot[slot].used)
  382. return;
  383. if (lizardActive()) {
  384. // Lizard single-controller: only slot 0's interface drives the mouse/keyboard. Other slots
  385. // stay quiet in lizard mode.
  386. if (slot == 0)
  387. rfLizard(rep, &hid[0], &hid[0], 0x40, 0x41);
  388. } else {
  389. uint8_t blen = bodyTlen - 1;
  390. if (blen > 45)
  391. blen = 45; // body after the 0x45 id byte
  392. // forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
  393. // only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
  394. // stair-step. g_fwdNewOnly toggles for A/B.
  395. if ((fresh || !g_fwdNewOnly) && hid[slot].ready())
  396. hid[slot].sendReport(
  397. 0x45, rep + 1,
  398. blen); // Steam/SDL Triton: input report 0x45
  399. }
  400. }
  401. // Forward the controller's NON-input status reports (0x43 power/battery, 0x44) to Steam verbatim -- the real
  402. // puck does this and it's how Steam reads battery. Same host-asleep / post-resume gating as 0x45; no lizard
  403. // path (status reports aren't input, so they forward regardless of the lizard decision). Per-slot: each
  404. // controller's status goes to its own hid[slot].
  405. void SteamPuckController::onAuxReport(int slot, uint8_t rid,
  406. const uint8_t *data, uint8_t n)
  407. {
  408. if (USBDevice.suspended())
  409. return;
  410. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  411. return;
  412. if (slot < 0 || slot >= NSLOT)
  413. return;
  414. // Forward the controller's status report VERBATIM (the real puck does this; it's how the host reads
  415. // battery). Padding the report to the descriptor-declared length broke battery in both lizard and
  416. // Steam, so it's reverted -- send exactly what the controller sent.
  417. if (g_slot[slot].used && hid[slot].ready())
  418. hid[slot].sendReport(rid, data, n);
  419. }
  420. // wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they only
  421. // wake when actual mouse/keyboard input follows. So on a deliberate wake gesture we play a HARMLESS mouse
  422. // JIGGLE (move a few px right, then back -- NET ZERO cursor, NO button): real mouse activity wakes the host
  423. // but clicks/activates nothing (an open Start menu stays open). Queued by wakeEvent() (rf_link, on a Steam
  424. // short press / controller connect while suspended); delivered once the suspended bus has resumed. Per-slot
  425. // so every connected interface gets the jiggle (Windows can credit any of them with the wake).
  426. static uint8_t g_nudgeStep[NSLOT] = { 0 }; // 0=idle; 1=jiggle+, 2=jiggle-
  427. static unsigned long g_nudgeMs[NSLOT] = { 0 };
  428. #define NUDGE_JIGGLE_PX 10
  429. void SteamPuckController::wakeEvent()
  430. {
  431. // arm the jiggle on every connected slot -- any one of them is enough to wake Windows
  432. for (int s = 0; s < NSLOT; s++) {
  433. if (g_slot[s].used) {
  434. g_nudgeStep[s] = 1;
  435. g_nudgeMs[s] = millis();
  436. }
  437. }
  438. }
  439. static void wakeNudgeTask()
  440. {
  441. if (USBDevice.suspended())
  442. return; // wait for resume; reports can't cross a suspended bus
  443. static unsigned long stepMs[NSLOT] = { 0 };
  444. for (int s = 0; s < NSLOT; s++) {
  445. if (!g_nudgeStep[s])
  446. continue;
  447. if (millis() - g_nudgeMs[s] > 5000) {
  448. g_nudgeStep[s] = 0;
  449. continue;
  450. } // bus never resumed -> drop the nudge
  451. if (!hid[s].ready())
  452. continue;
  453. if (millis() - stepMs[s] < 15)
  454. continue; // pace the edges
  455. stepMs[s] = millis();
  456. hid_mouse_report_t m;
  457. m.buttons = 0;
  458. m.x = (g_nudgeStep[s] == 1) ? NUDGE_JIGGLE_PX :
  459. -NUDGE_JIGGLE_PX;
  460. m.y = 0;
  461. m.wheel = 0;
  462. m.pan = 0;
  463. hid[s].sendReport(0x40, &m,
  464. sizeof m); // jiggle right, then back
  465. g_nudgeStep[s] = (g_nudgeStep[s] >= 2) ?
  466. 0 :
  467. (uint8_t)(g_nudgeStep[s] + 1);
  468. }
  469. }
  470. // USB connection presentation (like the real dongle): report 0x79 = connection state (01=disc, 02=conn),
  471. // edge-triggered, + periodic 0x7B status. Live-captured: this is what Steam reads to mark the controller
  472. // connected. Without it Steam shows disconnected even though 0x45 input is streaming.
  473. void SteamPuckController::task()
  474. {
  475. wakeNudgeTask();
  476. {
  477. static bool wasSusp = false;
  478. // stamp the suspended->active edge for the post-resume mute
  479. bool susp = USBDevice.suspended();
  480. if (wasSusp && !susp)
  481. g_resumeMs = millis();
  482. wasSusp = susp;
  483. }
  484. // no periodic 0x79/0x7B while the host sleeps -- those sends can wake it too
  485. if (USBDevice.suspended())
  486. return;
  487. // Per-slot 0x79/0x7B: each connected slot reports its OWN edge and its OWN status. State arrays are
  488. // per-slot so each controller's "connected" edge fires once and is re-sent only until Steam acks THAT
  489. // slot. The real puck's per-slot edge-triggered 0x79 prevents re-triggering Steam's connect-chime loop.
  490. static bool usbConn[NSLOT] = { 0 };
  491. static unsigned long last79[NSLOT] = { 0 }, last7B[NSLOT] = { 0 },
  492. connEdgeMs[NSLOT] = { 0 }, last43[NSLOT] = { 0 };
  493. for (int s = 0; s < NSLOT; s++) {
  494. if (!g_slot[s].used || !hid[s].ready())
  495. continue;
  496. // Debounced link state, not the raw 300 ms reply window: a brief RF gap must NOT toggle the
  497. // connection edge, or each blip re-fires the 0x79=02 connect edge -> Steam re-runs connect handling
  498. // (chime + connect-time haptic) -> buzz loop. g_linkUp drops only after a real outage (rf_link).
  499. bool conn = g_linkUp[s];
  500. // 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
  501. // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
  502. // unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before
  503. // Steam consumes 0x45 -> a loop of connect-time haptic buzzes. Resending until acked still covers
  504. // "Steam missed the edge".
  505. bool steamAcked = g_steamAliveMs &&
  506. (int32_t)(g_steamAliveMs - connEdgeMs[s]) >=
  507. 0;
  508. if (conn != usbConn[s] ||
  509. (conn && !steamAcked && millis() - last79[s] >= 750)) {
  510. if (conn && !usbConn[s])
  511. connEdgeMs[s] = millis();
  512. uint8_t st = conn ? 0x02 : 0x01;
  513. hid[s].sendReport(0x79, &st, 1);
  514. usbConn[s] = conn;
  515. last79[s] = millis();
  516. } else if (conn && millis() - last7B[s] >= 2000) {
  517. // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed
  518. // dBm (capture showed 0xDD = -35) -- patch in the smoothed RSSI the radio samples on each
  519. // controller reply (rf_link). 0 = no sample yet -> keep the capture value rather than garbage.
  520. //
  521. // CALIBRATION: our raw RSSISAMPLE reads ~RSSI_DBM_OFFSET dB lower than the real Valve puck at the
  522. // same distance (Pro Micro PCB-trace antenna vs Valve's tuned front-end). The 2Mbit ESB link has
  523. // ~55dB margin, so a -75dBm reading still works across a house, but Steam's bar maps raw dBm to
  524. // "weak" long before that. The offset lines our close-range value up with the puck's captured -35
  525. // so the bar tracks usable range, not antenna gain. Clamp keeps it in a sane window.
  526. uint8_t s7b[12] = { 0xF7, 0x01, 0x89, 0x00, 0x00, 0x00,
  527. 0x03, 0x00, 0xDD, 0x00, 0x3A, 0x02 };
  528. if (g_linkRssi[s]) {
  529. int mag = (int)g_linkRssi[s] - RSSI_DBM_OFFSET;
  530. if (mag < 25)
  531. mag = 25;
  532. else if (mag > 95)
  533. mag = 95;
  534. s7b[8] = (uint8_t)(0u - (uint8_t)mag);
  535. }
  536. hid[s].sendReport(0x7B, s7b, 12);
  537. last7B[s] = millis();
  538. }
  539. // Synthesized 0x43 = ID_TRITON_BATTERY_STATUS for SDL/Steam's gamepad driver. The verbatim forward of
  540. // the controller's own 0x43 (onAuxReport) is what the LIZARD/kernel path reads, but SDL's Triton driver
  541. // requires the FULL TritonBatteryStatus_t length (r >= 1 + 14) and lapses to the "wired" glyph without a
  542. // fresh one -- so we push a clean 14-byte report from g_battery every 2s. Body: [ucChargeState][ucBattery
  543. // Level][voltages/current/temp = 0]; SDL only reads the first two. Map unknown/reset state -> discharging
  544. // so it shows ON_BATTERY + % rather than UNKNOWN. Skipped until the controller has reported a level.
  545. if (conn && g_battery[s] && millis() - last43[s] >= 2000) {
  546. uint8_t st = g_batteryState[s];
  547. // EChargeState discharging -> SDL_POWERSTATE_ON_BATTERY
  548. if (st != 1 && st != 2 && st != 4)
  549. st = 1;
  550. uint8_t b43[14] = { 0 };
  551. b43[0] = st; // ucChargeState
  552. b43[1] = g_battery[s]; // ucBatteryLevel (percent)
  553. hid[s].sendReport(0x43, b43, sizeof b43);
  554. last43[s] = millis();
  555. }
  556. }
  557. // Reset edge state for slots that are no longer used/ready (so a re-bond sees a fresh edge).
  558. for (int s = 0; s < NSLOT; s++)
  559. if (!g_slot[s].used || !hid[s].ready())
  560. usbConn[s] = false;
  561. }