webusb_ctrl.cpp 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #include "webusb_ctrl.h"
  2. #include "ctrl_bonds.h"
  3. #include "ctrl_link.h" // g_linkSlot, ctrlLinkUp()
  4. #include "deck_input.h" // deckForwarding()
  5. #include "fw_update.h"
  6. #include <Arduino.h>
  7. #include <string.h>
  8. Adafruit_USBD_WebUSB usb_web;
  9. // The bond-list (0xAC) frame can carry all NBOND=8 pucks: 5-byte header + 8*26 = 213 B. It must fit the
  10. // vendor TX FIFO whole (we send drop-on-full, never spin), so the build sets CFG_TUD_VENDOR_TX_BUFSIZE=256.
  11. // Guard it so a build without that flag fails loudly instead of shipping a dongle whose panel never lists a
  12. // puck. (The core default is 64 -- see arduino/ports/nrf/tusb_config_nrf.h.)
  13. static_assert(
  14. CFG_TUD_VENDOR_TX_BUFSIZE >= 5 + NBOND * 26,
  15. "CFG_TUD_VENDOR_TX_BUFSIZE too small for the 0xAC bond list -- build "
  16. "the dongle with -DCFG_TUD_VENDOR_TX_BUFSIZE=256 (see Makefile).");
  17. // ---- deferred replies (both sent from webusbCtrlPoll, gated on FIFO room so usb_web.write() never spins) ----
  18. // usb_web.write() SPINS until the IN FIFO drains or the panel disconnects; a panel that holds the interface
  19. // open but stops reading would hang loop() -> watchdog reset. So EVERY send here is gated on
  20. // tud_vendor_write_available(): the bond list is drop-on-full (the panel re-requests), the fw-update ack is
  21. // kept pending until there is room (the panel's strict ping-pong drains the IN pipe, so that is immediate).
  22. static volatile bool g_bondListPend = false;
  23. static volatile bool g_fwupAckPend = false;
  24. static volatile uint8_t g_fwupAckStatus = 0;
  25. static volatile uint32_t g_fwupAckOff = 0;
  26. static void fwupAckPost(uint8_t status)
  27. {
  28. g_fwupAckStatus = status;
  29. g_fwupAckOff = fwupNextOff();
  30. g_fwupAckPend = true;
  31. }
  32. // Build + send the paired-pucks list. Mirrors deckStatusTask()'s per-bond record so the two management
  33. // surfaces (the Deck app over CDC, the browser panel over WebUSB) show the same thing.
  34. static void sendBondList()
  35. {
  36. if (!usb_web.connected())
  37. return;
  38. static uint8_t p[5 + NBOND * 26];
  39. bool linkUp = ctrlLinkUp();
  40. p[0] = 0xAC;
  41. p[2] = 1; // format version
  42. p[3] = (uint8_t)((deckForwarding() ? 1 : 0) | (linkUp ? 2 : 0));
  43. uint8_t count = 0;
  44. uint8_t *q = p + 5;
  45. for (int i = 0; i < NBOND; i++) {
  46. if (!g_bond[i].used)
  47. continue;
  48. q[0] = (uint8_t)
  49. i; // firmware slot index (target for 0x30 remove)
  50. q[1] = (i == g_linkSlot && linkUp) ? 1 : 0; // alive
  51. memcpy(q + 2, g_bond[i].rec + 0, 4); // proteus_uuid
  52. memcpy(q + 6, g_bond[i].rec + 4, 4); // ibex_uuid
  53. memcpy(q + 10, g_bond[i].rec + 8, 16); // puck serial
  54. q += 26;
  55. count++;
  56. }
  57. p[4] = count;
  58. uint16_t frame = (uint16_t)(5 + count * 26);
  59. p[1] = (uint8_t)(3 + count * 26); // payload length after [marker][len]
  60. if (tud_vendor_write_available() >= frame) {
  61. usb_web.write(p, frame);
  62. usb_web.flush();
  63. g_bondListPend = false;
  64. } else {
  65. g_bondListPend = true; // no room now -> retry next poll
  66. }
  67. }
  68. static void flushDeferred()
  69. {
  70. if (g_fwupAckPend && tud_vendor_write_available() >= 7) {
  71. uint8_t f[7] = { 0xAB,
  72. 5,
  73. g_fwupAckStatus,
  74. (uint8_t)(g_fwupAckOff),
  75. (uint8_t)(g_fwupAckOff >> 8),
  76. (uint8_t)(g_fwupAckOff >> 16),
  77. (uint8_t)(g_fwupAckOff >> 24) };
  78. usb_web.write(f, sizeof f);
  79. usb_web.flush();
  80. g_fwupAckPend = false;
  81. }
  82. if (g_bondListPend)
  83. sendBondList();
  84. }
  85. void webusbCtrlBegin()
  86. {
  87. usb_web.begin(); // adds the vendor interface + bumps bcdUSB to 2.1; no landing page (no Chrome popup)
  88. }
  89. void webusbCtrlPoll()
  90. {
  91. flushDeferred();
  92. if (!usb_web.connected())
  93. return;
  94. // 134 B holds the largest command: a firmware-update data chunk 0x21 = [op][off u32][len][<=128 data].
  95. // Byte-wise accumulation reassembles a command that spans USB-FS packets; a bad byte costs a 1-byte resync.
  96. static uint8_t buf[160];
  97. static uint8_t n = 0;
  98. while (usb_web.available()) {
  99. int c = usb_web.read();
  100. if (c < 0)
  101. break;
  102. if (n < sizeof buf)
  103. buf[n++] = (uint8_t)c;
  104. for (;;) {
  105. if (n == 0)
  106. break;
  107. uint8_t op = buf[0];
  108. // valid opcodes: 0x01, 0x0B, 0x0C, 0x20..0x25, 0x30. Anything else -> drop one byte + resync.
  109. bool known = (op == 0x01 || op == 0x0B || op == 0x0C ||
  110. op == 0x30 || (op >= 0x20 && op <= 0x25));
  111. if (!known) {
  112. memmove(buf, buf + 1, --n);
  113. continue;
  114. }
  115. // 0x21 carries its own data length at [5]; sanity-gate it before trusting it as a length so a
  116. // corrupt byte can only cost a one-byte resync, never wedge the parser.
  117. if (op == 0x21 && n >= 6) {
  118. uint8_t dl = buf[5];
  119. if (dl == 0 || dl > 128 || (dl & 3)) {
  120. memmove(buf, buf + 1, --n);
  121. continue;
  122. }
  123. }
  124. uint8_t need =
  125. (op == 0x20) ?
  126. 9 :
  127. (op == 0x21) ?
  128. (uint8_t)(6 + (n >= 6 ? buf[5] : 0)) :
  129. (op == 0x25) ? 5 :
  130. (op == 0x30) ? 2 :
  131. 1;
  132. if (n < need)
  133. break; // wait for more bytes
  134. if (op == 0x01) {
  135. sendBondList();
  136. } else if (op == 0x30) {
  137. // un-bond one paired puck by firmware slot, persist, then re-report the list.
  138. ctrlBondClear(buf[1]);
  139. if (g_bondDirty) {
  140. g_bondDirty = false;
  141. saveCtrlBonds();
  142. }
  143. sendBondList();
  144. } else if (op == 0x0B) {
  145. usb_web.flush();
  146. delay(40);
  147. enterSerialDfu();
  148. } else if (op == 0x0C) {
  149. usb_web.flush();
  150. delay(40);
  151. enterUf2Dfu();
  152. } else if (op == 0x20) {
  153. uint32_t sz = (uint32_t)buf[1] |
  154. ((uint32_t)buf[2] << 8) |
  155. ((uint32_t)buf[3] << 16) |
  156. ((uint32_t)buf[4] << 24);
  157. uint32_t crc = (uint32_t)buf[5] |
  158. ((uint32_t)buf[6] << 8) |
  159. ((uint32_t)buf[7] << 16) |
  160. ((uint32_t)buf[8] << 24);
  161. fwupAckPost(fwupBegin(sz, crc));
  162. } else if (op == 0x21) {
  163. uint32_t off = (uint32_t)buf[1] |
  164. ((uint32_t)buf[2] << 8) |
  165. ((uint32_t)buf[3] << 16) |
  166. ((uint32_t)buf[4] << 24);
  167. fwupAckPost(fwupChunk(off, buf + 6, buf[5]));
  168. } else if (op == 0x22) {
  169. fwupAckPost(fwupEnd());
  170. } else if (op == 0x23) {
  171. // clean reboot. After a committed 0x22 the boot path applies the staged image;
  172. // unarmed it is just a normal restart.
  173. usb_web.flush();
  174. delay(40);
  175. NVIC_SystemReset();
  176. } else if (op == 0x24) {
  177. fwupAbort();
  178. fwupAckPost(FWUP_OK);
  179. } else if (op == 0x25) {
  180. // FULL BOARD WIPE (debug). Guarded by the 4-byte magic "WIPE" so no stray byte can
  181. // trigger it. Arms the wipe marker + reboots; fwupWipeIfArmed() erases at next boot.
  182. if (buf[1] == 0x57 && buf[2] == 0x49 &&
  183. buf[3] == 0x50 && buf[4] == 0x45) {
  184. usb_web.flush();
  185. fwupArmFullWipe();
  186. delay(40);
  187. NVIC_SystemReset();
  188. }
  189. }
  190. // consume the processed command from the front of buf
  191. if (n > need)
  192. memmove(buf, buf + need, n - need);
  193. n -= need;
  194. }
  195. }
  196. }