ctrl_usb.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #include "ctrl_usb.h"
  2. #include "ctrl_bonds.h"
  3. #include "ctrl_link.h"
  4. #include "identity.h"
  5. #include <Adafruit_TinyUSB.h>
  6. #include <Arduino.h>
  7. #include <string.h>
  8. // Same Valve HID descriptor shape the puck uses (mouse 0x40 + keyboard 0x41 + vendor input reports +
  9. // the 63-byte FEATURE command reports on id 1/2). The first collection is Generic Desktop (usagePage
  10. // 0x01), so the host classifies this as the usagePage-0x01 control interface Steam/PairTUI write to.
  11. static const uint8_t CTRL_HID_DESC[] = {
  12. 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
  13. 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  14. 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01,
  15. 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02,
  16. 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38,
  17. 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1,
  18. 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25,
  19. 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29,
  20. 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0,
  21. 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26,
  22. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44,
  23. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81,
  24. 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01,
  25. 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  26. 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26,
  27. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45,
  28. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81,
  29. 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09,
  30. 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  31. 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26,
  32. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83,
  33. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91,
  34. 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08,
  35. 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  36. 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26,
  37. 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87,
  38. 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91,
  39. 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F,
  40. 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75,
  41. 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09,
  42. 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0
  43. };
  44. static Adafruit_USBD_HID g_hid;
  45. static uint8_t g_resp[63];
  46. static uint16_t g_resp_len = 0;
  47. static volatile bool g_rebootPending = false;
  48. // 0x95 reboots into wireless mode, which drops USB. If we reset before the host has read back the
  49. // command's ACK, Steam sees the disconnect mid-transaction and reports "pairing failed" even though the
  50. // bond is already on flash. So we wait for the host to GET the response (g_respRead), or REBOOT_GRACE_MS
  51. // for a host that fires 0x95 without reading back (scpair.py does this), before resetting.
  52. static volatile bool g_respRead = false;
  53. static uint32_t g_rebootAt = 0;
  54. static const uint32_t REBOOT_GRACE_MS = 400;
  55. // Select bond slot from a Zephyr settings key string: "esb/bond" -> 0, "esb/bond_2" -> 1.
  56. static int slotForKey(const char *key)
  57. {
  58. if (strcmp(key, "esb/bond") == 0)
  59. return 0;
  60. if (strcmp(key, "esb/bond_2") == 0)
  61. return 1;
  62. return -1;
  63. }
  64. static void writeBond(int slot, const uint8_t *rec24)
  65. {
  66. if (slot < 0 || slot >= NBOND)
  67. return;
  68. if (ctrlRecEmpty(rec24)) {
  69. g_bond[slot].used = false;
  70. memset(g_bond[slot].rec, 0, 24);
  71. } else {
  72. memcpy(g_bond[slot].rec, rec24, 24);
  73. g_bond[slot].used = true;
  74. }
  75. g_bondDirty = true;
  76. }
  77. // 0xAE string attribute by index, matching a real controller: 0=board serial, 1=unit serial (both our
  78. // unique generated values), 3=the Valve constant "7054257d2da7" Steam checks, everything else "NA".
  79. static const char *aeString(uint8_t idx)
  80. {
  81. return (idx == 0) ? g_board :
  82. (idx == 1) ? g_unit :
  83. (idx == 3) ? "7054257d2da7" :
  84. "NA";
  85. }
  86. // Build the controller's reply to a feature/command GET as [cmd][len][payload] -- the SAME shape a real
  87. // controller returns over USB (`83 19 …`, `AE 14 idx …`), confirmed by scmd. The inner length byte IS
  88. // required (these are command-channel replies, NOT the 0x45 input report). The puck relays this value
  89. // straight back to Steam as the feature response. ctrl_link wraps it in the F1 type-6 TLV. Returns the
  90. // byte count written to out (out must hold >= 63).
  91. uint8_t ctrlFeatureResp(uint8_t cmd, const uint8_t *param, uint8_t plen,
  92. uint8_t *out)
  93. {
  94. switch (cmd) {
  95. case 0x83: { // product/attribute blob: [0x83][len][ATTR83...]
  96. uint8_t alen = ATTR83_LEN > 58 ? 58 : (uint8_t)ATTR83_LEN;
  97. out[0] = 0x83;
  98. out[1] = alen;
  99. memcpy(out + 2, ATTR83, alen);
  100. return (uint8_t)(2 + alen);
  101. }
  102. case 0xAE: { // string attr: [0xAE][0x14][idx][serial 19]
  103. uint8_t idx = plen > 0 ? param[0] : 1;
  104. const char *s = aeString(idx);
  105. out[0] = 0xAE;
  106. out[1] = 0x14;
  107. out[2] = idx;
  108. memset(out + 3, 0, 0x13);
  109. memcpy(out + 3, s, strlen(s) < 0x13 ? strlen(s) : 0x13);
  110. return (uint8_t)(2 + 0x14);
  111. }
  112. case 0xB4: // wireless transport state: [0xB4][len=1][state]
  113. out[0] = 0xB4;
  114. out[1] = 0x01;
  115. out[2] = ctrlLinkUp() ? 0x02 : 0x01;
  116. return 3;
  117. default: // unknown query -> [cmd][len=0]
  118. out[0] = cmd;
  119. out[1] = 0;
  120. return 2;
  121. }
  122. }
  123. // Feature SET: framing [cmd][len][payload...]. rid (1 or 2) is not significant for our commands.
  124. static void handleSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
  125. uint16_t n)
  126. {
  127. (void)rid;
  128. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  129. return;
  130. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  131. const uint8_t *pl = b + 2;
  132. uint16_t pln = (n >= 2) ? n - 2 : 0;
  133. (void)len;
  134. // Plain-text diagnostics for a serial monitor during pairing. The Deck app resyncs on the frame
  135. // sync word, so these unframed lines are ignored by it -- only a human monitor sees them.
  136. if (Serial.availableForWrite() > 40) {
  137. Serial.printf("# SET rid=%u cmd=%02X len=%u pln=%u:", rid, cmd,
  138. len, (unsigned)pln);
  139. for (uint16_t i = 0; i < n && i < 12; i++)
  140. Serial.printf(" %02X", b[i]);
  141. Serial.println();
  142. }
  143. memset(g_resp, 0, sizeof g_resp);
  144. g_resp_len = 0;
  145. switch (cmd) {
  146. case 0x83: { // product attributes (0x1302)
  147. uint16_t alen = ATTR83_LEN > 60 ? 60 : ATTR83_LEN;
  148. g_resp[0] = 0x83;
  149. g_resp[1] = (uint8_t)alen;
  150. memcpy(g_resp + 2, ATTR83, alen);
  151. g_resp_len = 63;
  152. break;
  153. }
  154. // string attributes: 0=board, 1=unit serial, 3="7054257d2da7"
  155. case 0xAE: {
  156. uint8_t idx = pln > 0 ? pl[0] : 1;
  157. const char *s = aeString(idx);
  158. g_resp[0] = 0xAE;
  159. g_resp[1] = 0x14;
  160. g_resp[2] = idx;
  161. memcpy(g_resp + 3, s, strlen(s));
  162. g_resp_len = 63;
  163. break;
  164. }
  165. case 0xB4: // wireless transport state (1 = ok)
  166. g_resp[0] = 0xB4;
  167. g_resp[1] = 0x01;
  168. g_resp[2] = ctrlLinkUp() ? 0x02 : 0x01;
  169. g_resp_len = 63;
  170. break;
  171. case 0xEE: { // keyed bond write: ["key"\0][r1 4][r2 4][puck_serial 16]
  172. char key[24];
  173. uint16_t k = 0;
  174. while (k < pln && k < sizeof(key) - 1 && pl[k] != 0)
  175. key[k] = (char)pl[k], k++;
  176. key[k] = 0;
  177. const uint8_t *rec = pl + k + 1; // skip the NUL
  178. uint16_t reclen =
  179. (pln > (uint16_t)(k + 1)) ? (uint16_t)(pln - k - 1) : 0;
  180. // Find-or-allocate by the record's uuid key rather than the Zephyr settings key, so we can
  181. // bond an arbitrary number of pucks (not just the two legacy esb/bond[_2] keys).
  182. int sk = (reclen >= 24) ? ctrlBondFindOrAlloc(rec) :
  183. slotForKey(key);
  184. if (reclen >= 24)
  185. writeBond(sk, rec);
  186. if (Serial.availableForWrite() > 40)
  187. Serial.printf(
  188. "# EE key='%s' slot=%d reclen=%u stored=%d\n",
  189. key, sk, (unsigned)reclen,
  190. (sk >= 0 && reclen >= 24) ? 1 : 0);
  191. g_resp[0] = 0xEE;
  192. g_resp_len = 63;
  193. break;
  194. }
  195. case 0xEF: // commit keyed value (we write-through on 0xEE, so just ack)
  196. g_resp[0] = 0xEF;
  197. g_resp_len = 63;
  198. break;
  199. case 0xA2: // direct 24-byte bond write to slot 0 (fallback shape)
  200. if (pln >= 24)
  201. writeBond(0, pl);
  202. g_resp[0] = 0xA2;
  203. g_resp_len = 63;
  204. break;
  205. case 0xA3: // read slot 0 bond
  206. g_resp[0] = 0xA3;
  207. g_resp[1] = 0x18;
  208. if (g_bond[0].used)
  209. memcpy(g_resp + 2, g_bond[0].rec, 24);
  210. g_resp_len = 63;
  211. break;
  212. // reboot into wireless mode (magic 0xA427AF52); the reset happens after the flash flush AND once the
  213. // host has read the ACK back (see g_respRead / REBOOT_GRACE_MS above)
  214. case 0x95:
  215. g_rebootPending = true;
  216. g_respRead = false;
  217. g_rebootAt = millis();
  218. if (Serial.availableForWrite() > 40)
  219. Serial.println("# 0x95 reboot-to-wireless requested");
  220. g_resp[0] = 0x95;
  221. g_resp_len = 63;
  222. break;
  223. default:
  224. g_resp[0] = cmd;
  225. g_resp[1] = len;
  226. if (pln)
  227. memcpy(g_resp + 2, pl, pln > 60 ? 60 : pln);
  228. g_resp_len = 63;
  229. break;
  230. }
  231. }
  232. static uint16_t handleGet(uint8_t rid, hid_report_type_t type, uint8_t *buf,
  233. uint16_t reqlen)
  234. {
  235. (void)rid;
  236. if (type != HID_REPORT_TYPE_FEATURE)
  237. return 0;
  238. // The host read the staged response -> the SET/GET transaction completed. Used to know a 0x95 ACK
  239. // landed before we drop USB, and as a serial-monitor trace of Steam's pairing read sequence (the SET
  240. // side is logged in handleSet; a bare GET with no preceding SET shows up here as a stale g_resp[0]).
  241. g_respRead = true;
  242. if (Serial.availableForWrite() > 40)
  243. Serial.printf("# GET rid=%u -> %02X %02X req=%u\n", rid,
  244. g_resp[0], g_resp[1], (unsigned)reqlen);
  245. uint16_t nn = g_resp_len ? g_resp_len : 63;
  246. if (nn > reqlen)
  247. nn = reqlen;
  248. memcpy(buf, g_resp, nn);
  249. return nn;
  250. }
  251. void ctrlUsbBegin()
  252. {
  253. USBDevice.setID(0x28DE, 0x1302);
  254. USBDevice.setDeviceVersion(0x0101);
  255. USBDevice.setManufacturerDescriptor("Valve Software");
  256. USBDevice.setProductDescriptor("Steam Controller");
  257. g_hid.setReportDescriptor(CTRL_HID_DESC, sizeof CTRL_HID_DESC);
  258. g_hid.setReportCallback(handleGet, handleSet);
  259. g_hid.setPollInterval(1);
  260. g_hid.begin();
  261. }
  262. void ctrlUsbPoll()
  263. {
  264. if (g_bondDirty) {
  265. g_bondDirty = false;
  266. saveCtrlBonds();
  267. }
  268. // reboot only AFTER the freshly-written bond is on flash (else it reloads a stale bond on boot) AND
  269. // the host has read the 0x95 ACK back (or the grace window lapsed for a fire-and-forget host) so the
  270. // USB drop never lands mid-transaction and reads to Steam as a pairing failure.
  271. if (g_rebootPending && !g_bondDirty &&
  272. (g_respRead ||
  273. (uint32_t)(millis() - g_rebootAt) > REBOOT_GRACE_MS)) {
  274. delay(20);
  275. NVIC_SystemReset();
  276. }
  277. }