ctrl_usb.cpp 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. // Select bond slot from a Zephyr settings key string: "esb/bond" -> 0, "esb/bond_2" -> 1.
  49. static int slotForKey(const char *key)
  50. {
  51. if (strcmp(key, "esb/bond") == 0)
  52. return 0;
  53. if (strcmp(key, "esb/bond_2") == 0)
  54. return 1;
  55. return -1;
  56. }
  57. static void writeBond(int slot, const uint8_t *rec24)
  58. {
  59. if (slot < 0 || slot >= NBOND)
  60. return;
  61. if (ctrlRecEmpty(rec24)) {
  62. g_bond[slot].used = false;
  63. memset(g_bond[slot].rec, 0, 24);
  64. } else {
  65. memcpy(g_bond[slot].rec, rec24, 24);
  66. g_bond[slot].used = true;
  67. }
  68. g_bondDirty = true;
  69. }
  70. // 0xAE string attribute by index, matching a real controller: 0=board serial, 1=unit serial (both our
  71. // unique generated values), 3=the Valve constant "7054257d2da7" Steam checks, everything else "NA".
  72. static const char *aeString(uint8_t idx)
  73. {
  74. return (idx == 0) ? g_board :
  75. (idx == 1) ? g_unit :
  76. (idx == 3) ? "7054257d2da7" :
  77. "NA";
  78. }
  79. // Build the controller's reply to a feature/command GET as [cmd][len][payload] -- the SAME shape a real
  80. // controller returns over USB (`83 19 …`, `AE 14 idx …`), confirmed by scmd. The inner length byte IS
  81. // required (these are command-channel replies, NOT the 0x45 input report). The puck relays this value
  82. // straight back to Steam as the feature response. ctrl_link wraps it in the F1 type-6 TLV. Returns the
  83. // byte count written to out (out must hold >= 63).
  84. uint8_t ctrlFeatureResp(uint8_t cmd, const uint8_t *param, uint8_t plen,
  85. uint8_t *out)
  86. {
  87. switch (cmd) {
  88. case 0x83: { // product/attribute blob: [0x83][len][ATTR83...]
  89. uint8_t alen = ATTR83_LEN > 58 ? 58 : (uint8_t)ATTR83_LEN;
  90. out[0] = 0x83;
  91. out[1] = alen;
  92. memcpy(out + 2, ATTR83, alen);
  93. return (uint8_t)(2 + alen);
  94. }
  95. case 0xAE: { // string attr: [0xAE][0x14][idx][serial 19]
  96. uint8_t idx = plen > 0 ? param[0] : 1;
  97. const char *s = aeString(idx);
  98. out[0] = 0xAE;
  99. out[1] = 0x14;
  100. out[2] = idx;
  101. memset(out + 3, 0, 0x13);
  102. memcpy(out + 3, s, strlen(s) < 0x13 ? strlen(s) : 0x13);
  103. return (uint8_t)(2 + 0x14);
  104. }
  105. case 0xB4: // wireless transport state: [0xB4][len=1][state]
  106. out[0] = 0xB4;
  107. out[1] = 0x01;
  108. out[2] = ctrlLinkUp() ? 0x02 : 0x01;
  109. return 3;
  110. default: // unknown query -> [cmd][len=0]
  111. out[0] = cmd;
  112. out[1] = 0;
  113. return 2;
  114. }
  115. }
  116. // Feature SET: framing [cmd][len][payload...]. rid (1 or 2) is not significant for our commands.
  117. static void handleSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
  118. uint16_t n)
  119. {
  120. (void)rid;
  121. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  122. return;
  123. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  124. const uint8_t *pl = b + 2;
  125. uint16_t pln = (n >= 2) ? n - 2 : 0;
  126. (void)len;
  127. // Plain-text diagnostics for a serial monitor during pairing. The Deck app resyncs on the frame
  128. // sync word, so these unframed lines are ignored by it -- only a human monitor sees them.
  129. if (Serial.availableForWrite() > 40) {
  130. Serial.printf("# SET rid=%u cmd=%02X len=%u pln=%u:", rid, cmd,
  131. len, (unsigned)pln);
  132. for (uint16_t i = 0; i < n && i < 12; i++)
  133. Serial.printf(" %02X", b[i]);
  134. Serial.println();
  135. }
  136. memset(g_resp, 0, sizeof g_resp);
  137. g_resp_len = 0;
  138. switch (cmd) {
  139. case 0x83: { // product attributes (0x1302)
  140. uint16_t alen = ATTR83_LEN > 60 ? 60 : ATTR83_LEN;
  141. g_resp[0] = 0x83;
  142. g_resp[1] = (uint8_t)alen;
  143. memcpy(g_resp + 2, ATTR83, alen);
  144. g_resp_len = 63;
  145. break;
  146. }
  147. // string attributes: 0=board, 1=unit serial, 3="7054257d2da7"
  148. case 0xAE: {
  149. uint8_t idx = pln > 0 ? pl[0] : 1;
  150. const char *s = aeString(idx);
  151. g_resp[0] = 0xAE;
  152. g_resp[1] = 0x14;
  153. g_resp[2] = idx;
  154. memcpy(g_resp + 3, s, strlen(s));
  155. g_resp_len = 63;
  156. break;
  157. }
  158. case 0xB4: // wireless transport state (1 = ok)
  159. g_resp[0] = 0xB4;
  160. g_resp[1] = 0x01;
  161. g_resp[2] = ctrlLinkUp() ? 0x02 : 0x01;
  162. g_resp_len = 63;
  163. break;
  164. case 0xEE: { // keyed bond write: ["key"\0][r1 4][r2 4][puck_serial 16]
  165. char key[24];
  166. uint16_t k = 0;
  167. while (k < pln && k < sizeof(key) - 1 && pl[k] != 0)
  168. key[k] = (char)pl[k], k++;
  169. key[k] = 0;
  170. const uint8_t *rec = pl + k + 1; // skip the NUL
  171. uint16_t reclen =
  172. (pln > (uint16_t)(k + 1)) ? (uint16_t)(pln - k - 1) : 0;
  173. // Find-or-allocate by the record's uuid key rather than the Zephyr settings key, so we can
  174. // bond an arbitrary number of pucks (not just the two legacy esb/bond[_2] keys).
  175. int sk = (reclen >= 24) ? ctrlBondFindOrAlloc(rec) :
  176. slotForKey(key);
  177. if (reclen >= 24)
  178. writeBond(sk, rec);
  179. if (Serial.availableForWrite() > 40)
  180. Serial.printf(
  181. "# EE key='%s' slot=%d reclen=%u stored=%d\n",
  182. key, sk, (unsigned)reclen,
  183. (sk >= 0 && reclen >= 24) ? 1 : 0);
  184. g_resp[0] = 0xEE;
  185. g_resp_len = 63;
  186. break;
  187. }
  188. case 0xEF: // commit keyed value (we write-through on 0xEE, so just ack)
  189. g_resp[0] = 0xEF;
  190. g_resp_len = 63;
  191. break;
  192. case 0xA2: // direct 24-byte bond write to slot 0 (fallback shape)
  193. if (pln >= 24)
  194. writeBond(0, pl);
  195. g_resp[0] = 0xA2;
  196. g_resp_len = 63;
  197. break;
  198. case 0xA3: // read slot 0 bond
  199. g_resp[0] = 0xA3;
  200. g_resp[1] = 0x18;
  201. if (g_bond[0].used)
  202. memcpy(g_resp + 2, g_bond[0].rec, 24);
  203. g_resp_len = 63;
  204. break;
  205. // reboot into wireless mode (magic 0xA427AF52); the reset happens after the flash flush
  206. case 0x95:
  207. g_rebootPending = true;
  208. if (Serial.availableForWrite() > 40)
  209. Serial.println("# 0x95 reboot-to-wireless requested");
  210. g_resp[0] = 0x95;
  211. g_resp_len = 63;
  212. break;
  213. default:
  214. g_resp[0] = cmd;
  215. g_resp[1] = len;
  216. if (pln)
  217. memcpy(g_resp + 2, pl, pln > 60 ? 60 : pln);
  218. g_resp_len = 63;
  219. break;
  220. }
  221. }
  222. static uint16_t handleGet(uint8_t rid, hid_report_type_t type, uint8_t *buf,
  223. uint16_t reqlen)
  224. {
  225. (void)rid;
  226. if (type != HID_REPORT_TYPE_FEATURE)
  227. return 0;
  228. uint16_t nn = g_resp_len ? g_resp_len : 63;
  229. if (nn > reqlen)
  230. nn = reqlen;
  231. memcpy(buf, g_resp, nn);
  232. return nn;
  233. }
  234. void ctrlUsbBegin()
  235. {
  236. USBDevice.setID(0x28DE, 0x1302);
  237. USBDevice.setDeviceVersion(0x0101);
  238. USBDevice.setManufacturerDescriptor("Valve Software");
  239. USBDevice.setProductDescriptor("Steam Controller");
  240. g_hid.setReportDescriptor(CTRL_HID_DESC, sizeof CTRL_HID_DESC);
  241. g_hid.setReportCallback(handleGet, handleSet);
  242. g_hid.setPollInterval(1);
  243. g_hid.begin();
  244. }
  245. void ctrlUsbPoll()
  246. {
  247. if (g_bondDirty) {
  248. g_bondDirty = false;
  249. saveCtrlBonds();
  250. }
  251. // reboot only AFTER the freshly-written bond is on flash (else it reloads a stale bond on boot).
  252. if (g_rebootPending && !g_bondDirty) {
  253. delay(40);
  254. NVIC_SystemReset();
  255. }
  256. }