ctrl_usb.cpp 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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. // Feature SET: framing [cmd][len][payload...]. rid (1 or 2) is not significant for our commands.
  71. static void handleSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
  72. uint16_t n)
  73. {
  74. (void)rid;
  75. if (type != HID_REPORT_TYPE_FEATURE || n < 1)
  76. return;
  77. uint8_t cmd = b[0], len = (n > 1) ? b[1] : 0;
  78. const uint8_t *pl = b + 2;
  79. uint16_t pln = (n >= 2) ? n - 2 : 0;
  80. (void)len;
  81. // Plain-text diagnostics for a serial monitor during pairing. The Deck app resyncs on the frame
  82. // sync word, so these unframed lines are ignored by it -- only a human monitor sees them.
  83. if (Serial.availableForWrite() > 40) {
  84. Serial.printf("# SET rid=%u cmd=%02X len=%u pln=%u:", rid, cmd,
  85. len, (unsigned)pln);
  86. for (uint16_t i = 0; i < n && i < 12; i++)
  87. Serial.printf(" %02X", b[i]);
  88. Serial.println();
  89. }
  90. memset(g_resp, 0, sizeof g_resp);
  91. g_resp_len = 0;
  92. switch (cmd) {
  93. case 0x83: { // product attributes (0x1302)
  94. uint16_t alen = ATTR83_LEN > 60 ? 60 : ATTR83_LEN;
  95. g_resp[0] = 0x83;
  96. g_resp[1] = (uint8_t)alen;
  97. memcpy(g_resp + 2, ATTR83, alen);
  98. g_resp_len = 63;
  99. break;
  100. }
  101. case 0xAE: { // string attributes: 0=board, 1=unit serial
  102. uint8_t idx = pln > 0 ? pl[0] : 1;
  103. const char *s = (idx == 0) ? g_board :
  104. (idx == 1) ? g_unit :
  105. "NA";
  106. g_resp[0] = 0xAE;
  107. g_resp[1] = 0x14;
  108. g_resp[2] = idx;
  109. memcpy(g_resp + 3, s, strlen(s));
  110. g_resp_len = 63;
  111. break;
  112. }
  113. case 0xB4: // wireless transport state (1 = ok)
  114. g_resp[0] = 0xB4;
  115. g_resp[1] = 0x01;
  116. g_resp[2] = ctrlLinkUp() ? 0x02 : 0x01;
  117. g_resp_len = 63;
  118. break;
  119. case 0xEE: { // keyed bond write: ["key"\0][r1 4][r2 4][puck_serial 16]
  120. char key[24];
  121. uint16_t k = 0;
  122. while (k < pln && k < sizeof(key) - 1 && pl[k] != 0)
  123. key[k] = (char)pl[k], k++;
  124. key[k] = 0;
  125. const uint8_t *rec = pl + k + 1; // skip the NUL
  126. uint16_t reclen =
  127. (pln > (uint16_t)(k + 1)) ? (uint16_t)(pln - k - 1) : 0;
  128. int sk = slotForKey(key);
  129. if (reclen >= 24)
  130. writeBond(sk, rec);
  131. if (Serial.availableForWrite() > 40)
  132. Serial.printf(
  133. "# EE key='%s' slot=%d reclen=%u stored=%d\n",
  134. key, sk, (unsigned)reclen,
  135. (sk >= 0 && reclen >= 24) ? 1 : 0);
  136. g_resp[0] = 0xEE;
  137. g_resp_len = 63;
  138. break;
  139. }
  140. case 0xEF: // commit keyed value (we write-through on 0xEE, so just ack)
  141. g_resp[0] = 0xEF;
  142. g_resp_len = 63;
  143. break;
  144. case 0xA2: // direct 24-byte bond write to slot 0 (fallback shape)
  145. if (pln >= 24)
  146. writeBond(0, pl);
  147. g_resp[0] = 0xA2;
  148. g_resp_len = 63;
  149. break;
  150. case 0xA3: // read slot 0 bond
  151. g_resp[0] = 0xA3;
  152. g_resp[1] = 0x18;
  153. if (g_bond[0].used)
  154. memcpy(g_resp + 2, g_bond[0].rec, 24);
  155. g_resp_len = 63;
  156. break;
  157. // reboot into wireless mode (magic 0xA427AF52); the reset happens after the flash flush
  158. case 0x95:
  159. g_rebootPending = true;
  160. if (Serial.availableForWrite() > 40)
  161. Serial.println("# 0x95 reboot-to-wireless requested");
  162. g_resp[0] = 0x95;
  163. g_resp_len = 63;
  164. break;
  165. default:
  166. g_resp[0] = cmd;
  167. g_resp[1] = len;
  168. if (pln)
  169. memcpy(g_resp + 2, pl, pln > 60 ? 60 : pln);
  170. g_resp_len = 63;
  171. break;
  172. }
  173. }
  174. static uint16_t handleGet(uint8_t rid, hid_report_type_t type, uint8_t *buf,
  175. uint16_t reqlen)
  176. {
  177. (void)rid;
  178. if (type != HID_REPORT_TYPE_FEATURE)
  179. return 0;
  180. uint16_t nn = g_resp_len ? g_resp_len : 63;
  181. if (nn > reqlen)
  182. nn = reqlen;
  183. memcpy(buf, g_resp, nn);
  184. return nn;
  185. }
  186. void ctrlUsbBegin()
  187. {
  188. USBDevice.setID(0x28DE, 0x1302);
  189. USBDevice.setDeviceVersion(0x0101);
  190. USBDevice.setManufacturerDescriptor("Valve Software");
  191. USBDevice.setProductDescriptor("Steam Controller");
  192. g_hid.setReportDescriptor(CTRL_HID_DESC, sizeof CTRL_HID_DESC);
  193. g_hid.setReportCallback(handleGet, handleSet);
  194. g_hid.setPollInterval(1);
  195. g_hid.begin();
  196. }
  197. void ctrlUsbPoll()
  198. {
  199. if (g_bondDirty) {
  200. g_bondDirty = false;
  201. saveCtrlBonds();
  202. }
  203. // reboot only AFTER the freshly-written bond is on flash (else it reloads a stale bond on boot).
  204. if (g_rebootPending && !g_bondDirty) {
  205. delay(40);
  206. NVIC_SystemReset();
  207. }
  208. }