puck_hid.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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()) {
  149. relayEnqueue(rid, b,
  150. (uint8_t)(n > RELAY_MAXP ?
  151. RELAY_MAXP :
  152. n));
  153. // Track on/off from what was actually RELAYED (= the controller's believed state): a
  154. // BLOCKED stop must leave "on" set (controller may be latched -> reconnect stop-burst),
  155. // a blocked ON must not set it (nothing reached the controller -> no spurious clicks).
  156. if (rid == 0x82)
  157. haptic82HostReport(b, n);
  158. }
  159. }
  160. if (Serial.availableForWrite() > 80) {
  161. Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
  162. n);
  163. for (uint16_t i = 0; i < n && i < 14; i++)
  164. Serial.printf(" %02X", b[i]);
  165. Serial.println();
  166. }
  167. return;
  168. }
  169. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  170. return;
  171. Slot &S = g_slot[slot];
  172. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  173. const uint8_t *pl = b + 2;
  174. uint16_t pln = (n >= 2) ? n - 2 : 0;
  175. // settings/haptic/LED report (incl. 0x87 lizard-off heartbeat, SDL Triton lizard-disable)
  176. if (cmd >= 0x80 && cmd <= 0x89)
  177. hostStampAlive();
  178. // Controller power-off: Steam's "turn off controller" is feature-0x01 frame 9F 04 6F 66 66 21 ("off!"),
  179. // confirmed from a real puck capture. The feature-0x01 relay below forwards it once; hapticSendShutdown()
  180. // bursts it for NO-ACK reliability (the single hook the test button + host-suspend also drive).
  181. if (rid == 1 && cmd == 0x9F)
  182. hapticSendShutdown();
  183. // report 0x01 = raw passthrough -> queue for RF relay to the controller
  184. if (rid == 1 && n >= 2) {
  185. // capture EVERY relayed feature-1 command for the WebUSB capture view (haptics, LED SET_LED_COLOR,
  186. // 0x87 settings, 0x9F power-off). Log shows cmd as "rid"; bytes start [cmd][len]...
  187. hapLogAdd((uint8_t)slot, cmd, b, n);
  188. bool haptic82 = (cmd == 0x82 && len <= pln);
  189. bool muted = g_resumeMs &&
  190. millis() - g_resumeMs < POST_RESUME_MUTE_MS;
  191. // never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
  192. bool relayOk = hapticRelaySlotOk(slot) &&
  193. !(haptic82 && (lizardActive() || muted));
  194. if (relayOk && (!haptic82 || !haptic82Blocked())) {
  195. // Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
  196. // multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
  197. // 18B cap, and the chopped frames were why those settings never landed on the controller.
  198. uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
  199. if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
  200. Serial.printf(
  201. "# RELAY TRUNC cmd=%02X len=%u>%u\n",
  202. cmd, len, (unsigned)RELAY_MAXP);
  203. relayEnqueue(cmd, pl, rl);
  204. // track from RELAYED frames only (see the OUTPUT path)
  205. if (haptic82)
  206. haptic82HostReport(pl, len);
  207. }
  208. }
  209. if (Serial.availableForWrite() > 80) {
  210. Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
  211. cmd, len);
  212. for (uint16_t i = 0; i < n && i < 14; i++)
  213. Serial.printf(" %02X", b[i]);
  214. Serial.println();
  215. }
  216. memset(S.resp, 0, sizeof S.resp);
  217. S.resp_len = 0;
  218. switch (cmd) {
  219. case 0x83:
  220. S.resp[0] = 0x83;
  221. S.resp[1] = sizeof ATTR83;
  222. memcpy(S.resp + 2, ATTR83, sizeof ATTR83);
  223. S.resp_len = 63;
  224. break;
  225. case 0xAE: {
  226. uint8_t idx = pln > 0 ? pl[0] : 1;
  227. const char *s = (idx == 0) ? g_board :
  228. (idx == 1) ? g_unit :
  229. "NA";
  230. S.resp[0] = 0xAE;
  231. S.resp[1] = 0x14;
  232. S.resp[2] = idx;
  233. memset(S.resp + 3, 0, 60);
  234. memcpy(S.resp + 3, s, strlen(s));
  235. S.resp_len = 63;
  236. break;
  237. }
  238. // connection/version state per slot: value 0x02 = controller connected, 0x01 = not
  239. case 0xB4:
  240. // SDL Triton polls this on init; treat like Steam contact so we forward 0x45
  241. hostStampAlive();
  242. S.resp[0] = 0xB4;
  243. S.resp[1] = 0x01;
  244. S.resp[2] = (slot == g_connSlot && !g_xbox &&
  245. (millis() - g_connReplyMs < 500)) ?
  246. 0x02 :
  247. 0x01;
  248. S.resp_len = 63;
  249. break;
  250. case 0xAD:
  251. g_pairing = (pln > 0 && pl[0] != 0);
  252. Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
  253. S.resp[0] = 0xAD;
  254. S.resp[1] = 0;
  255. S.resp_len = 63;
  256. break;
  257. case 0xA2: // write/clear THIS interface's slot
  258. if (len >= 24 && pln >= 24) {
  259. if (recEmpty(pl)) {
  260. S.used = false;
  261. memset(S.rec, 0, 24);
  262. } else {
  263. memcpy(S.rec, pl, 24);
  264. S.used = true;
  265. }
  266. g_dirty = true;
  267. Serial.printf("# slot %d %s\n", slot,
  268. recEmpty(pl) ? "cleared" : "bonded");
  269. }
  270. S.resp[0] = 0xA2;
  271. S.resp[1] = 0;
  272. S.resp_len = 63;
  273. break;
  274. case 0xA3: // read THIS interface's slot
  275. S.resp[0] = 0xA3;
  276. S.resp[1] = 0x18;
  277. memset(S.resp + 2, 0, 24);
  278. if (S.used)
  279. memcpy(S.resp + 2, S.rec, 24);
  280. S.resp_len = 63;
  281. break;
  282. default:
  283. S.resp[0] = cmd;
  284. S.resp[1] = len;
  285. if (pln)
  286. memcpy(S.resp + 2, pl, pln > 60 ? 60 : pln);
  287. S.resp_len = 63;
  288. break;
  289. }
  290. }
  291. static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
  292. uint8_t *buf, uint16_t reqlen)
  293. {
  294. (void)rid;
  295. if (type != HID_REPORT_TYPE_FEATURE)
  296. return 0;
  297. // host reading feature data => gamepad consumer active (Steam/SDL)
  298. if (g_usbMode == MODE_STEAM)
  299. hostStampAlive();
  300. Slot &S = g_slot[slot];
  301. uint16_t n = S.resp_len ? S.resp_len : 63;
  302. if (n > reqlen)
  303. n = reqlen;
  304. memcpy(buf, S.resp, n);
  305. return n;
  306. }
  307. // one callback pair per interface (the Adafruit core routes by interface to the matching object)
  308. #define SLOTCB(N) \
  309. static void setcb##N(uint8_t r, hid_report_type_t t, uint8_t const *b, \
  310. uint16_t n) \
  311. { \
  312. handleSet(N, r, t, b, n); \
  313. } \
  314. static uint16_t getcb##N(uint8_t r, hid_report_type_t t, uint8_t *bf, \
  315. uint16_t rl) \
  316. { \
  317. return handleGet(N, r, t, bf, rl); \
  318. }
  319. // clang-format off
  320. SLOTCB(0)
  321. SLOTCB(1)
  322. SLOTCB(2)
  323. SLOTCB(3)
  324. // clang-format on
  325. typedef uint16_t (*getcb_t)(uint8_t, hid_report_type_t, uint8_t *, uint16_t);
  326. typedef void (*setcb_t)(uint8_t, hid_report_type_t, uint8_t const *, uint16_t);
  327. static getcb_t GETCB[NSLOT] = { getcb0, getcb1, getcb2, getcb3 };
  328. static setcb_t SETCB[NSLOT] = { setcb0, setcb1, setcb2, setcb3 };
  329. // ===================== IController =====================
  330. void SteamPuckController::begin()
  331. {
  332. USBDevice.setID(0x28DE, 0x1304);
  333. // Distinct bcdDevice so Windows keys a FRESH usbflags entry (cache is VID:PID:bcdDevice) and actually runs
  334. // MS OS 2.0 / WinUSB binding for the WebUSB vendor interface, instead of reusing a stale "no WinUSB" entry
  335. // tied to the real Steam Controller (28DE:1304, no WebUSB interface). The normal (wake-mouse) and one-shot
  336. // debug (CDC) boots present DIFFERENT interface sets, so they need DIFFERENT bcdDevice values or Windows
  337. // serves one's cached descriptor for the other across a reboot.
  338. USBDevice.setDeviceVersion(
  339. g_debugCdcThisBoot ?
  340. 0x0212 :
  341. (g_usbMode == MODE_LIZARD ? 0x0213 : 0x0211));
  342. USBDevice.setManufacturerDescriptor("Valve Software");
  343. USBDevice.setProductDescriptor("Steam Controller Puck");
  344. const uint8_t *desc = (g_usbMode == MODE_LIZARD) ?
  345. PUCK_LIZARD_HID_DESC :
  346. PUCK_HID_DESC;
  347. const uint16_t descLen = (g_usbMode == MODE_LIZARD) ?
  348. sizeof PUCK_LIZARD_HID_DESC :
  349. sizeof PUCK_HID_DESC;
  350. for (int i = 0; i < NSLOT; i++) {
  351. hid[i].setReportDescriptor(desc, descLen);
  352. hid[i].setReportCallback(GETCB[i], SETCB[i]);
  353. // 1ms USB poll (was default 10ms = 100/s cap -> choppy)
  354. hid[i].setPollInterval(1);
  355. hid[i].begin();
  356. }
  357. }
  358. // Forward the controller's report 0x45 to Steam, or drive lizard kb/mouse when Steam is closed. PURELY a
  359. // USB-side decision -- changes nothing about the RF poll or the host->controller relay.
  360. void SteamPuckController::onReport45(const uint8_t *rep, bool fresh,
  361. uint8_t bodyTlen)
  362. {
  363. // Host asleep -> forward NOTHING. While suspended, every sendReport attempt can translate into a host wake,
  364. // making the PC wake on any controller movement. Waking is an explicit gesture only (Steam-button short
  365. // press / controller connect), handled in rf_link.cpp via the device-level USB resume signal.
  366. if (USBDevice.suspended())
  367. return;
  368. // Just woke? Hold off forwarding so the wake gesture's residual controller input doesn't click/type into
  369. // the freshly-woken desktop.
  370. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  371. return;
  372. if (g_connSlot < 0 || g_connSlot >= NSLOT)
  373. return;
  374. if (lizardActive()) {
  375. rfLizard(rep, &hid[g_connSlot], &hid[g_connSlot], 0x40, 0x41);
  376. } else {
  377. uint8_t blen = bodyTlen - 1;
  378. if (blen > 45)
  379. blen = 45; // body after the 0x45 id byte
  380. // forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
  381. // only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
  382. // stair-step. g_fwdNewOnly toggles for A/B.
  383. if ((fresh || !g_fwdNewOnly) && g_slot[g_connSlot].used &&
  384. hid[g_connSlot].ready())
  385. hid[g_connSlot].sendReport(
  386. 0x45, rep + 1,
  387. blen); // Steam/SDL Triton: input report 0x45
  388. }
  389. }
  390. // Forward the controller's NON-input status reports (0x43 power/battery, 0x44) to Steam verbatim -- the real
  391. // puck does this and it's how Steam reads battery. Same host-asleep / post-resume gating as 0x45; no lizard
  392. // path (status reports aren't input, so they forward regardless of the lizard decision).
  393. void SteamPuckController::onAuxReport(uint8_t rid, const uint8_t *data,
  394. uint8_t n)
  395. {
  396. if (USBDevice.suspended())
  397. return;
  398. if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
  399. return;
  400. if (g_connSlot < 0 || g_connSlot >= NSLOT)
  401. return;
  402. if (g_slot[g_connSlot].used && hid[g_connSlot].ready())
  403. hid[g_connSlot].sendReport(rid, data, n);
  404. }
  405. // wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they only
  406. // wake when actual mouse/keyboard input follows. So on a deliberate wake gesture we play a HARMLESS mouse
  407. // JIGGLE (move a few px right, then back -- NET ZERO cursor, NO button): real mouse activity wakes the host
  408. // but clicks/activates nothing (an open Start menu stays open). Queued by wakeEvent() (rf_link, on a Steam
  409. // short press / controller connect while suspended); delivered once the suspended bus has resumed.
  410. static uint8_t g_nudgeStep = 0; // 0=idle; 1=jiggle+, 2=jiggle-
  411. static unsigned long g_nudgeMs = 0;
  412. #define NUDGE_JIGGLE_PX 10
  413. void SteamPuckController::wakeEvent()
  414. {
  415. g_nudgeStep = 1;
  416. g_nudgeMs = millis();
  417. }
  418. static void wakeNudgeTask()
  419. {
  420. if (!g_nudgeStep)
  421. return;
  422. if (millis() - g_nudgeMs > 5000) {
  423. g_nudgeStep = 0;
  424. return;
  425. } // bus never resumed -> drop the nudge
  426. if (USBDevice.suspended())
  427. return; // wait for resume; reports can't cross a suspended bus
  428. if (g_connSlot < 0 || g_connSlot >= NSLOT || !hid[g_connSlot].ready())
  429. return;
  430. static unsigned long stepMs = 0;
  431. if (millis() - stepMs < 15)
  432. return; // pace the edges
  433. stepMs = millis();
  434. hid_mouse_report_t m;
  435. m.buttons = 0;
  436. m.x = (g_nudgeStep == 1) ? NUDGE_JIGGLE_PX : -NUDGE_JIGGLE_PX;
  437. m.y = 0;
  438. m.wheel = 0;
  439. m.pan = 0;
  440. hid[g_connSlot].sendReport(0x40, &m,
  441. sizeof m); // jiggle right, then back
  442. g_nudgeStep = (g_nudgeStep >= 2) ? 0 : (uint8_t)(g_nudgeStep + 1);
  443. }
  444. // USB connection presentation (like the real dongle): report 0x79 = connection state (01=disc, 02=conn),
  445. // edge-triggered, + periodic 0x7B status. Live-captured: this is what Steam reads to mark the controller
  446. // connected. Without it Steam shows disconnected even though 0x45 input is streaming.
  447. void SteamPuckController::task()
  448. {
  449. wakeNudgeTask();
  450. {
  451. static bool wasSusp = false;
  452. // stamp the suspended->active edge for the post-resume mute
  453. bool susp = USBDevice.suspended();
  454. if (wasSusp && !susp)
  455. g_resumeMs = millis();
  456. wasSusp = susp;
  457. }
  458. // no periodic 0x79/0x7B while the host sleeps -- those sends can wake it too
  459. if (USBDevice.suspended())
  460. return;
  461. static bool usbConn = false;
  462. static unsigned long last79 = 0, last7B = 0, connEdgeMs = 0;
  463. bool conn = (g_connSlot >= 0 && millis() - g_connReplyMs < 300);
  464. if (g_connSlot >= 0 && g_connSlot < NSLOT && hid[g_connSlot].ready()) {
  465. // 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
  466. // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
  467. // unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before
  468. // Steam consumes 0x45 -> a loop of connect-time haptic buzzes. Resending until acked still covers
  469. // "Steam missed the edge".
  470. bool steamAcked = g_steamAliveMs &&
  471. (int32_t)(g_steamAliveMs - connEdgeMs) >= 0;
  472. if (conn != usbConn ||
  473. (conn && !steamAcked && millis() - last79 >= 750)) {
  474. if (conn && !usbConn)
  475. connEdgeMs = millis();
  476. uint8_t st = conn ? 0x02 : 0x01;
  477. hid[g_connSlot].sendReport(0x79, &st, 1);
  478. usbConn = conn;
  479. last79 = millis();
  480. } else if (conn && millis() - last7B >= 2000) {
  481. // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed
  482. // dBm (capture showed 0xDD = -35) -- patch in the smoothed RSSI the radio samples on each
  483. // controller reply (rf_link). 0 = no sample yet -> keep the capture value rather than garbage.
  484. //
  485. // CALIBRATION: our raw RSSISAMPLE reads ~RSSI_DBM_OFFSET dB lower than the real Valve puck at the
  486. // same distance (Pro Micro PCB-trace antenna vs Valve's tuned front-end). The 2Mbit ESB link has
  487. // ~55dB margin, so a -75dBm reading still works across a house, but Steam's bar maps raw dBm to
  488. // "weak" long before that. The offset lines our close-range value up with the puck's captured -35
  489. // so the bar tracks usable range, not antenna gain. Clamp keeps it in a sane window.
  490. uint8_t s7b[12] = { 0xF7, 0x01, 0x89, 0x00, 0x00, 0x00,
  491. 0x03, 0x00, 0xDD, 0x00, 0x3A, 0x02 };
  492. if (g_linkRssi) {
  493. int mag = (int)g_linkRssi - RSSI_DBM_OFFSET;
  494. if (mag < 25)
  495. mag = 25;
  496. else if (mag > 95)
  497. mag = 95;
  498. s7b[8] = (uint8_t)(0u - (uint8_t)mag);
  499. }
  500. hid[g_connSlot].sendReport(0x7B, s7b, 12);
  501. last7B = millis();
  502. }
  503. } else if (!conn)
  504. usbConn = false;
  505. }