webusb_config.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. #include "webusb_config.h"
  2. #include "config.h"
  3. #include "bonds.h"
  4. #include "rf_link.h"
  5. #include "haptics.h"
  6. #include "puck_hid.h"
  7. #include "triton.h" // g_in raw IMU (diagnostic readout)
  8. #include "build_info.h"
  9. #include "fault_diag.h"
  10. #include "usb_tx.h"
  11. #include <Arduino.h>
  12. #include <string.h>
  13. Adafruit_USBD_WebUSB usb_web;
  14. // Panel-blob send is deferred to the usbd task: webusbPoll() (loop) just sets this, and webusbSofDrain()
  15. // (registered with usb_tx, runs from tud_sof_cb on the usbd task) does the actual write+flush. usb_web.flush()
  16. // goes through the same blocking osd_queue_send path as HID sends, so doing it from loop() could stall the
  17. // loop and trip the watchdog under load -- with the panel open during heavy use that was a live risk.
  18. static volatile bool g_blobRequest = false;
  19. // Bond export ("clone" backup): a 0x09 panel command asks for a one-shot dump of all four bond slots so the
  20. // browser can save them to a file and restore them onto a second puck. Like the status blob, the actual send
  21. // is deferred to the usbd task (webusbSofDrain) so usb_web.write()/flush() never block loop().
  22. static volatile bool g_bondExportRequest = false;
  23. // blob payload = [ver=10][mode][mDiv][mFric][qamMap(active)][abSwap(active)][back0..3(active)][connSlot(0xFF=none)][linkUp]
  24. // [f1ps_lo][f1ps_hi][pollU100][newps_lo][newps_hi][e7b][relayOp][relaySub][fwdNewOnly]
  25. // [qos][persistMode][chordBtn B][chordBtn X][chordBtn Y][pollsps_lo][pollsps_hi]
  26. // [loopPeriod_lo][loopPeriod_hi][loopWorstIdx][loopWorstUs_lo][loopWorstUs_hi]
  27. // [pollPeriod_lo][pollPeriod_hi][logEnabled][battery%][rssi|dBm|]
  28. // [gitDirty][buildId 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
  29. // [bondedCount][slot0_up][slot0_batt][slot0_rssi]...[slot3_up][slot3_batt][slot3_rssi]
  30. // [v10: per-type cfg, 4x8B: ET_XBOX/SWITCH/DS4/DS5 each {back0..3, qam, abSwap, padHaptics, ledBright}]
  31. // [v11: resetReason(RR_* code)][resetReas raw u32 LE][rxWin/10 us][hapticBlockOn][hapticBlock s]
  32. // p[6]/p[7]/p[8..11] mirror the ACTIVE type (legacy display). v11 extends to 113 bytes (115 total incl header);
  33. // browser reads with transferIn(128) to span the two USB-FS packets.
  34. // [diag: crc/s][noRx/s][rfStallRecover count]
  35. // [v12: relayps(lo,hi)][clkLfSrc][clkHfSrc][usPerMs(lo,hi)][hangStage][curStage][stallMs/40][ringFault(lo,hi)][hangPC u32][hangLR u32][usbdStackFree(lo,hi)][loopStackFree(lo,hi)]
  36. // [v13: per-slot link stats, 4x9B from p[141]: {pollsps u16, f1ps u16, newps u16, crc/s u8,
  37. // noRx/s u8, relay/s u8} -- each controller's own rates (the v4 aggregates are their sums)]
  38. // [v14: p[177] landAll87 (verbatim-0x87-relay experiment toggle)]
  39. #define WB_PAYLEN 176
  40. // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob
  41. // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank
  42. // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without
  43. // that flag (or a future blob that outgrows the FIFO) fails loudly instead of shipping a dead panel.
  44. static_assert(CFG_TUD_VENDOR_TX_BUFSIZE >= 2 + WB_PAYLEN,
  45. "CFG_TUD_VENDOR_TX_BUFSIZE too small to hold a WebUSB blob in one "
  46. "write -- build via `make build` (sets 256), or raise the flag.");
  47. static void webusbSendBlob()
  48. {
  49. if (!usb_web.connected())
  50. return;
  51. // "connected" = the most recent poll cycle's slot had a fresh F-reply (matches what the firmware
  52. // presents to Steam on 0x79). The blob is sent on the panel's poll, so the slot + up flag change
  53. // every ~250ms in normal use.
  54. int cs = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  55. bool up = (g_curSlot >= 0 && (millis() - g_connReplyMs[cs]) < 300);
  56. // STATIC, not stack: this runs on the usbd task, whose 800B stack (hardcoded USBD_STACK_SZ in the
  57. // Adafruit core, not growable from the sketch) is the prime overflow suspect behind the issue-72
  58. // watchdog hangs (hang boots report usbd stack free = 0). A 178B frame on that stack was the single
  59. // biggest consumer. Single-writer (SOF drain only), so a static buffer is race-free.
  60. static uint8_t p[2 + WB_PAYLEN];
  61. p[0] = 0xA5;
  62. p[1] = WB_PAYLEN;
  63. // protocol version (14 = +landAll87 toggle; 13 = +per-slot link stats; 12 = +relay rate + clock fingerprint; 11 = +reset cause; 10 = +ledBright per type; 9 = +per-type cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
  64. p[2] = 14;
  65. p[3] = g_usbMode;
  66. p[4] = (uint8_t)g_mDiv;
  67. p[5] = (uint8_t)g_mFric;
  68. p[6] = g_qamMap;
  69. p[7] = g_abSwap;
  70. p[8] = g_back[0];
  71. p[9] = g_back[1];
  72. p[10] = g_back[2];
  73. p[11] = g_back[3];
  74. p[12] = (g_curSlot >= 0) ? (uint8_t)g_curSlot : 0xFF;
  75. p[13] = up ? 1 : 0;
  76. p[14] = (uint8_t)g_f1ps;
  77. p[15] = (uint8_t)(g_f1ps >> 8);
  78. p[16] = (uint8_t)(g_pollUs / 100);
  79. p[17] = (uint8_t)g_newps;
  80. p[18] = (uint8_t)(g_newps >> 8);
  81. p[19] = g_e7b;
  82. p[20] = g_relayOp;
  83. p[21] = g_relaySub;
  84. p[22] = g_fwdNewOnly;
  85. p[23] = g_qos;
  86. p[24] = g_persistMode ? 1 : 0;
  87. p[25] = g_chordBtn[0];
  88. p[26] = g_chordBtn[1];
  89. p[27] = g_chordBtn[2];
  90. p[28] = (uint8_t)g_pollsps;
  91. // poll TX rate (vs delivered/new -> starvation vs reply-loss)
  92. p[29] = (uint8_t)(g_pollsps >> 8);
  93. p[30] = (uint8_t)g_loopPeriodUs;
  94. p[31] = (uint8_t)(g_loopPeriodUs >> 8); // avg loop iteration time
  95. p[32] = g_loopWorst;
  96. p[33] = (uint8_t)g_loopWorstUs;
  97. p[34] = (uint8_t)(g_loopWorstUs >> 8);
  98. p[35] = (uint8_t)g_pollPeriodUs;
  99. p[36] = (uint8_t)(g_pollPeriodUs >>
  100. 8); // measured poll period (intended 4000)
  101. p[37] = OPK_LOG; // logging build? panel shows/hides its log UI
  102. p[38] = g_battery
  103. [g_curSlot >= 0 && g_curSlot < NSLOT ?
  104. g_curSlot :
  105. 0]; // controller battery % (report 0x43); 0=unknown
  106. p[39] = g_linkRssi[g_curSlot >= 0 && g_curSlot < NSLOT ?
  107. g_curSlot :
  108. 0]; // RAW signal strength |dBm| (0=no sample)
  109. // Release builds are tagged and easier to trace by semver; local/dev builds
  110. // still expose git hash when no version tag is available.
  111. p[40] = OPK_GIT_DIRTY ? 1 : 0;
  112. memset(&p[41], 0, 12); // 12B ASCII build ID, NUL-padded
  113. {
  114. const char *buildId = OPK_BUILD_VERSION[0] ? OPK_BUILD_VERSION :
  115. OPK_GIT_HASH;
  116. for (uint8_t i = 0; i < 12 && buildId[i]; i++)
  117. p[41 + i] = (uint8_t)buildId[i];
  118. }
  119. p[53] = g_rumbleScale; // rumble strength % (protocol v5)
  120. // Switch Pro report rate 0=66/1=120/2=full (protocol v6)
  121. p[54] = g_swProRate;
  122. // Switch Pro gyro sensitivity x10 (protocol v6)
  123. p[55] = g_swGyroScale10;
  124. {
  125. int16_t a[3] = { g_in[0].ax, g_in[0].ay, g_in[0].az };
  126. memcpy(&p[56], a, 6);
  127. } // raw accelerometer for scale diagnostics (protocol v7)
  128. // per-slot link status for all bond slots (protocol v8)
  129. p[62] = (uint8_t)bondedSlotCount();
  130. {
  131. unsigned long nowMs = millis();
  132. for (int s = 0; s < NSLOT; s++) {
  133. bool sup = g_slot[s].used && g_connReplyMs[s] != 0 &&
  134. (nowMs - g_connReplyMs[s]) < 300u;
  135. p[63 + s * 3] = sup ? 1 : 0;
  136. p[64 + s * 3] = g_battery[s];
  137. p[65 + s * 3] = g_linkRssi[s];
  138. }
  139. }
  140. // per-emulated-type button config (protocol v10): 4 types x 8 bytes from p[75]
  141. for (int et = 0; et < ET_COUNT; et++) {
  142. uint8_t *q = &p[75 + et * 8];
  143. q[0] = g_type[et].back[0];
  144. q[1] = g_type[et].back[1];
  145. q[2] = g_type[et].back[2];
  146. q[3] = g_type[et].back[3];
  147. q[4] = g_type[et].qamMap;
  148. q[5] = g_type[et].abSwap;
  149. q[6] = g_type[et].padHaptics;
  150. q[7] = g_type[et].ledBright;
  151. }
  152. // last-boot reset cause (protocol v11): why we (re)booted -- watchdog hang vs HardFault vs intentional
  153. // reboot vs power-on (issue #72). p[107] = RR_* code; p[108..111] = raw RESETREAS for the curious.
  154. p[107] = faultDiagReason();
  155. uint32_t rr = faultDiagResetReas();
  156. p[108] = (uint8_t)rr;
  157. p[109] = (uint8_t)(rr >> 8);
  158. p[110] = (uint8_t)(rr >> 16);
  159. p[111] = (uint8_t)(rr >> 24);
  160. // Connection tunables (protocol v11): poll RX window in 10us units, post-connect haptic block enable,
  161. // and block duration in seconds.
  162. p[112] = (uint8_t)(g_rxWin / 10);
  163. p[113] = g_hapticBlockOn;
  164. p[114] = (uint8_t)(g_hapticBlockMs / 1000);
  165. // RF wedge diagnostics (always populated, even when the link reads down): CRC-fail/s and no-reply/s,
  166. // capped at 255. With Polls/s this distinguishes poll-loop-stopped vs RX-dead vs corrupt-replies.
  167. p[115] = (uint8_t)(g_crcps > 255 ? 255 : g_crcps);
  168. p[116] = (uint8_t)(g_norxps > 255 ? 255 : g_norxps);
  169. p[117] = (uint8_t)(g_rfStallRecover > 255 ? 255 : g_rfStallRecover);
  170. // v12: relay-frame TX rate (separated from true poll cycles -- Polls/s now reads ~250, Relay/s exposes the
  171. // host output-report flood that steals reply windows), and the clock fingerprint for clone-board triage.
  172. p[118] = (uint8_t)g_relayps;
  173. p[119] = (uint8_t)(g_relayps >> 8);
  174. p[120] = clockLfSrc();
  175. p[121] = clockHfSrc();
  176. uint16_t upm = clockUsPerMs();
  177. p[122] = (uint8_t)upm;
  178. p[123] = (uint8_t)(upm >> 8);
  179. // last hang's loop stage (0xFF = last boot wasn't a watchdog/lockup hang, or GPREGRET2 didn't survive)
  180. p[124] = faultDiagHangStage();
  181. // LIVE hang localization: current loop stage + how long loop() has been stuck (40ms units, capped). When
  182. // loop() wedges these keep updating because the blob is sent from the SOF interrupt, not loop().
  183. p[125] = faultDiagCurStage();
  184. uint32_t sms = faultDiagStallMs();
  185. p[126] = (uint8_t)(sms / 40 > 255 ? 255 : sms / 40);
  186. // relay-ring fault count: non-zero = we caught+recovered a desynced/corrupt ring that would otherwise be an
  187. // invisible IRQ-off watchdog hang. A climbing count points at the haptic relay path / memory corruption.
  188. p[127] = (uint8_t)g_ringFault;
  189. p[128] = (uint8_t)(g_ringFault >> 8);
  190. // PC/LR of the stuck code captured by the WDT pre-reset ISR on the last watchdog hang (0 = none captured =
  191. // the hang hard-masked interrupts). Map with addr2line on the .elf to name the function.
  192. uint32_t hpc = faultDiagHangPC(), hlr = faultDiagHangLR();
  193. p[129] = (uint8_t)hpc;
  194. p[130] = (uint8_t)(hpc >> 8);
  195. p[131] = (uint8_t)(hpc >> 16);
  196. p[132] = (uint8_t)(hpc >> 24);
  197. p[133] = (uint8_t)hlr;
  198. p[134] = (uint8_t)(hlr >> 8);
  199. p[135] = (uint8_t)(hlr >> 16);
  200. p[136] = (uint8_t)(hlr >> 24);
  201. // least-ever free stack (words) on the usbd task (800B total) and loop task -- usbd trending to 0 confirms
  202. // the overflow. words, not bytes.
  203. uint16_t usbdFree = faultDiagUsbdStackFree(),
  204. loopFree = faultDiagLoopStackFree();
  205. p[137] = (uint8_t)usbdFree;
  206. p[138] = (uint8_t)(usbdFree >> 8);
  207. p[139] = (uint8_t)loopFree;
  208. p[140] = (uint8_t)(loopFree >> 8);
  209. // v13: per-slot link stats -- each controller's own poll/reply/error rates, so the panel's per-slot tabs
  210. // no longer conflate every controller into the aggregate numbers above.
  211. for (int s = 0; s < NSLOT; s++) {
  212. uint8_t *q = &p[141 + s * 9];
  213. q[0] = (uint8_t)g_slotPollsps[s];
  214. q[1] = (uint8_t)(g_slotPollsps[s] >> 8);
  215. q[2] = (uint8_t)g_slotF1ps[s];
  216. q[3] = (uint8_t)(g_slotF1ps[s] >> 8);
  217. q[4] = (uint8_t)g_slotNewps[s];
  218. q[5] = (uint8_t)(g_slotNewps[s] >> 8);
  219. q[6] = g_slotCrcps[s];
  220. q[7] = g_slotNoRxps[s];
  221. q[8] = g_slotRelayps[s];
  222. }
  223. // v14: verbatim-0x87-relay experiment toggle (panel reflects + toggles it)
  224. p[177] = g_landAll87;
  225. // CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the
  226. // panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a
  227. // backgrounded tab, or the host briefly not servicing transferIn under load -- the FIFO never empties and
  228. // that spin hangs loop() until the ~8s watchdog resets us (RESETREAS=watchdog/hang). This is the ONE
  229. // cross-task tud_ call the marshalling refactor left able to block loop() (HID goes through the drop-oldest
  230. // ring; this didn't). So DROP the blob whenever the FIFO can't take it whole -- a status panel missing an
  231. // occasional frame is invisible, and loop() can never stall here again. (Matches the closing-the-panel
  232. // "fix": that just flips _connected=false to break the same spin.)
  233. if (tud_vendor_write_available() >= sizeof p) {
  234. usb_web.write(p, sizeof p);
  235. usb_web.flush();
  236. }
  237. }
  238. // Bond export frame (panel "Export config to file"). One self-describing frame carrying all four bond slots so
  239. // the browser can clone a puck onto another without re-pairing:
  240. // [0xA7][len][ver=1][usedMask][slot0 rec 24]...[slot3 rec 24] (len = 2 + NSLOT*24 = 98)
  241. // usedMask bit s = slot s bonded. The session RF address is NOT exported: each puck re-derives it from the bond
  242. // UUID + its own FICR DEVICEID at boot, and the bonded controller relearns it from the E1 host beacon on every
  243. // reconnect (rf_link.cpp) -- so the 24-byte record is the whole portable identity. 98+2 < transferIn(128) and
  244. // < CFG_TUD_VENDOR_TX_BUFSIZE (256), so it sends whole in one drop-on-full write like the status blob.
  245. #define WB_BOND_PAYLEN (2 + NSLOT * 24)
  246. static void webusbSendBondExport()
  247. {
  248. if (!usb_web.connected())
  249. return;
  250. // static for the same reason as the status blob: keep the 100B frame off the 800B usbd-task stack.
  251. static uint8_t p[2 + WB_BOND_PAYLEN];
  252. p[0] = 0xA7;
  253. p[1] = WB_BOND_PAYLEN;
  254. p[2] = 1; // format version
  255. uint8_t mask = 0;
  256. for (int s = 0; s < NSLOT; s++) {
  257. if (g_slot[s].used)
  258. mask |= (uint8_t)(1u << s);
  259. memcpy(&p[4 + s * 24], g_slot[s].rec, 24);
  260. }
  261. p[3] = mask;
  262. // Same anti-hang rule as the status blob: drop the frame if the FIFO can't take it whole, never spin.
  263. if (tud_vendor_write_available() >= sizeof p) {
  264. usb_web.write(p, sizeof p);
  265. usb_web.flush();
  266. }
  267. }
  268. // Stream the saved (pre-reset) flight-recorder trail to the panel as 0xA8 frames. Always compiled -- the
  269. // recorder is not OPK_LOG-gated (field hangs happen on release builds too). Frame layout mirrors the 0xA6
  270. // capture drain so the same drop-on-full discipline applies (a write only issues when the FIFO can take the
  271. // whole frame -> usb_web.write() never spins -> loop() never stalls here):
  272. // header: [0xA8][29][T=2][count u16][total u16][loopPerSec u16][stallMs][curStage][usbdStk u16][loopStk u16]
  273. // [heapUsed u32][pollsps u16][relayps u16][crc u8][norx u8][heal u16][ringF u16]
  274. // entry: [0xA8][9][T=1][dtMs u32][evt][stage][arg u16] (dtMs = ms before the last recorded crumb)
  275. // end: [0xA8][1][T=0]
  276. // The panel sends 0x10 with buf[1]=1 to (re)start (emits the header + rewinds the cursor), then 0x10 buf[1]=0
  277. // repeatedly; each call streams a budgeted chunk and the final call appends the end frame.
  278. #define WB_FLIGHT_HDR 29
  279. // RAII boost: these drains run from loop() and push many usb_web.write()s per call -- each enters TinyUSB's
  280. // dcd DMA claim window (the issue-72 livelock; see usb_tx.cpp), so the whole burst runs boosted.
  281. struct TxBoostScope {
  282. TxBoostScope()
  283. {
  284. usbTxBoost();
  285. }
  286. ~TxBoostScope()
  287. {
  288. usbTxUnboost();
  289. }
  290. };
  291. static void webusbDrainFlight(bool restart)
  292. {
  293. if (!usb_web.connected())
  294. return;
  295. TxBoostScope boost;
  296. if (restart) {
  297. faultDiagFlightDrainReset();
  298. if (tud_vendor_write_available() >= 2 + WB_FLIGHT_HDR) {
  299. struct FaultFlightVitals v;
  300. bool have = faultDiagFlightVitals(&v);
  301. if (!have)
  302. memset(&v, 0, sizeof v);
  303. uint16_t cnt = faultDiagFlightCount();
  304. uint16_t tot = faultDiagFlightTotal();
  305. uint8_t f[2 + WB_FLIGHT_HDR];
  306. f[0] = 0xA8;
  307. f[1] = WB_FLIGHT_HDR;
  308. f[2] = 2; // header type
  309. f[3] = (uint8_t)cnt;
  310. f[4] = (uint8_t)(cnt >> 8);
  311. f[5] = (uint8_t)tot;
  312. f[6] = (uint8_t)(tot >> 8);
  313. f[7] = (uint8_t)v.loopPerSec;
  314. f[8] = (uint8_t)(v.loopPerSec >> 8);
  315. f[9] = v.stallMs;
  316. f[10] = v.curStage;
  317. f[11] = (uint8_t)v.usbdStkFree;
  318. f[12] = (uint8_t)(v.usbdStkFree >> 8);
  319. f[13] = (uint8_t)v.loopStkFree;
  320. f[14] = (uint8_t)(v.loopStkFree >> 8);
  321. f[15] = (uint8_t)v.heapUsed;
  322. f[16] = (uint8_t)(v.heapUsed >> 8);
  323. f[17] = (uint8_t)(v.heapUsed >> 16);
  324. f[18] = (uint8_t)(v.heapUsed >> 24);
  325. f[19] = (uint8_t)v.pollsps;
  326. f[20] = (uint8_t)(v.pollsps >> 8);
  327. f[21] = (uint8_t)v.relayps;
  328. f[22] = (uint8_t)(v.relayps >> 8);
  329. f[23] = (uint8_t)(v.crcps > 255 ? 255 : v.crcps);
  330. f[24] = (uint8_t)(v.norxps > 255 ? 255 : v.norxps);
  331. f[25] = (uint8_t)v.rfHeal;
  332. f[26] = (uint8_t)(v.rfHeal >> 8);
  333. f[27] = (uint8_t)v.ringFault;
  334. f[28] = (uint8_t)(v.ringFault >> 8);
  335. f[29] = (uint8_t)0; // reserved / pad to header len
  336. f[30] = (uint8_t)0;
  337. usb_web.write(f, sizeof f);
  338. usb_web.flush();
  339. }
  340. }
  341. uint32_t dt = 0;
  342. uint8_t evt = 0, stage = 0;
  343. uint16_t arg = 0;
  344. uint16_t budget = 64;
  345. bool done = false;
  346. while (budget--) {
  347. if (tud_vendor_write_available() < 2 + 9)
  348. break; // FIFO full -> resume on the panel's next 0x10 poll
  349. if (!faultDiagFlightPull(&dt, &evt, &stage, &arg)) {
  350. done = true;
  351. break;
  352. }
  353. uint8_t f[2 + 9];
  354. f[0] = 0xA8;
  355. f[1] = 9;
  356. f[2] = 1; // entry type
  357. f[3] = (uint8_t)dt;
  358. f[4] = (uint8_t)(dt >> 8);
  359. f[5] = (uint8_t)(dt >> 16);
  360. f[6] = (uint8_t)(dt >> 24);
  361. f[7] = evt;
  362. f[8] = stage;
  363. f[9] = (uint8_t)arg;
  364. f[10] = (uint8_t)(arg >> 8);
  365. usb_web.write(f, sizeof f);
  366. usb_web.flush();
  367. }
  368. if (done && tud_vendor_write_available() >= 3) {
  369. uint8_t e[3] = { 0xA8, 1, 0 };
  370. usb_web.write(e, 3);
  371. usb_web.flush();
  372. }
  373. }
  374. // Runs on the usbd task (registered via usbTxRegisterDrain -> tud_sof_cb). Sends the blob if loop() asked for
  375. // one. Keeps every usb_web write/flush off the loop task so it can't block on the device event queue.
  376. static void webusbSofDrain(void)
  377. {
  378. // If loop() has stopped beating, it's wedged -- keep pushing the blob (which carries the live stuck stage)
  379. // so the panel shows WHERE it hung during the ~8s before the watchdog fires. This runs on the SOF IRQ, so
  380. // it works even though loop() is dead. drop-on-full in webusbSendBlob keeps it from ever blocking.
  381. if (faultDiagStallMs() > 300)
  382. g_blobRequest = true;
  383. if (g_blobRequest) {
  384. g_blobRequest = false;
  385. webusbSendBlob();
  386. }
  387. if (g_bondExportRequest) {
  388. g_bondExportRequest = false;
  389. webusbSendBondExport();
  390. }
  391. }
  392. // ---- live wedge reporter (0xA9) --------------------------------------------------------------------------
  393. // THE one channel that survives a loop() wedge on boards that wipe .noinit/GPREGRET across the watchdog reset
  394. // (Teyleten): tud_sof_cb runs on the usbd task, which is HIGHER priority than the loop() task, so the USB
  395. // stack keeps dispatching it even while loop() is wedged (spinning or blocked) -- as long as interrupts aren't
  396. // hard-masked (PRIMASK). When loop() has been stuck a while, emit a tiny frame carrying the stuck loop stage so
  397. // the panel can log WHERE it hung, live, during the ~8s before the watchdog fires. Zero traffic when healthy
  398. // (returns immediately unless stalled); rate-limited; drop-on-full so it never spins the usbd task.
  399. // frame: [0xA9][3][stuck stage][stallMs lo][stallMs hi]
  400. extern "C" void tud_sof_cb(uint32_t frame_count)
  401. {
  402. (void)frame_count;
  403. uint32_t stall = faultDiagStallMs();
  404. if (stall <
  405. 1000) // loop() healthy -> nothing to report (the common case: near-zero overhead)
  406. return;
  407. static uint32_t lastMs = 0;
  408. uint32_t now = millis();
  409. if ((uint32_t)(now - lastMs) < 200) // ~5 Hz cap
  410. return;
  411. lastMs = now;
  412. if (!usb_web.connected())
  413. return;
  414. uint8_t f[2 + 3];
  415. f[0] = 0xA9;
  416. f[1] = 3;
  417. f[2] = faultDiagCurStage();
  418. uint16_t s = (stall > 0xFFFFu) ? 0xFFFFu : (uint16_t)stall;
  419. f[3] = (uint8_t)s;
  420. f[4] = (uint8_t)(s >> 8);
  421. if (tud_vendor_write_available() >= sizeof f) {
  422. usb_web.write(f, sizeof f);
  423. usb_web.flush();
  424. }
  425. }
  426. // Register the SOF drain. Call once from setup() (harmless even if WebUSB isn't enumerated -- webusbSendBlob
  427. // no-ops while disconnected, and g_blobRequest is only ever set by panel traffic, which needs a connection).
  428. void webusbInit(void)
  429. {
  430. usbTxRegisterDrain(webusbSofDrain);
  431. // Enable SOF-callback dispatch so tud_sof_cb() above fires -- our only live view into a loop() wedge on
  432. // boards whose bootloader wipes retained RAM. Cheap: the callback no-ops unless loop() is already stalled.
  433. tud_sof_cb_enable(true);
  434. }
  435. #if OPK_LOG
  436. // Stream the capture ring (haptics / relayed host commands) to the panel as 0xA6 frames. Frame formats:
  437. // entry: [0xA6][L][T=1][age u32 LE][slot][rid][n][bytes n] (L = 8 + n)
  438. // end: [0xA6][1][T=0]
  439. static void webusbCapFrame(uint32_t ms, uint8_t slot, uint8_t rid, uint8_t nb,
  440. const uint8_t *b)
  441. {
  442. if (nb > 16)
  443. nb = 16;
  444. uint8_t f[2 + 9 + 16];
  445. uint8_t L = (uint8_t)(8 + nb);
  446. f[0] = 0xA6;
  447. f[1] = L;
  448. f[2] = 1;
  449. f[3] = (uint8_t)ms;
  450. f[4] = (uint8_t)(ms >> 8);
  451. f[5] = (uint8_t)(ms >> 16);
  452. f[6] = (uint8_t)(ms >> 24); // absolute log time
  453. f[7] = slot;
  454. f[8] = rid;
  455. f[9] = nb;
  456. memcpy(f + 10, b, nb);
  457. usb_web.write(f, (uint16_t)(2 + L));
  458. usb_web.flush();
  459. }
  460. static void webusbCapEnd()
  461. {
  462. uint8_t e[3] = { 0xA6, 1, 0 };
  463. usb_web.write(e, 3);
  464. usb_web.flush();
  465. }
  466. // 0x06: drain entries since the last drain (or since the 0x05 start/rewind). The panel polls this on a
  467. // timer and accumulates, so a dump-from-boot of the whole ring streams over many polls without blocking the
  468. // loop. A per-call budget bounds how long one poll spends here.
  469. static void webusbDrainCapture()
  470. {
  471. if (!usb_web.connected())
  472. return;
  473. TxBoostScope
  474. boost; // capture streams at flood rate through the same dcd DMA window
  475. uint32_t ms = 0;
  476. uint8_t slot = 0, rid = 0, nb = 0, b[16];
  477. uint16_t budget = 128;
  478. // Same anti-hang rule as the blob: webusbCapFrame's usb_web.write() spins until the FIFO drains, so stop
  479. // pulling the moment the FIFO can't hold a max-size frame (2+9+16=27 B) -- the panel resumes the drain on
  480. // its next 0x06 poll. Never let a backed-up panel hang loop() here.
  481. while (budget-- && tud_vendor_write_available() >= 27 &&
  482. hapLogPull(&ms, &slot, &rid, &nb, b))
  483. webusbCapFrame(ms, slot, rid, nb, b);
  484. if (tud_vendor_write_available() >= 3)
  485. webusbCapEnd();
  486. }
  487. #endif // OPK_LOG
  488. void webusbPoll()
  489. {
  490. // 40 B holds the largest command: 0x0D writes one bond slot = [0x0D][slot][used][24 rec] = 27 B (still
  491. // inside one 64-B USB-FS OUT packet, so no command ever spans packets). Every other command is <= 4 B.
  492. static uint8_t buf[40];
  493. static uint8_t n = 0;
  494. while (usb_web.available()) {
  495. int c = usb_web.read();
  496. if (c < 0)
  497. break;
  498. if (n < sizeof buf)
  499. buf[n++] = (uint8_t)c;
  500. // process complete commands from the front of buf
  501. for (;;) {
  502. if (n == 0)
  503. break;
  504. uint8_t op = buf[0];
  505. if (op < 0x01 || op > 0x10) { // resync: drop one byte
  506. memmove(buf, buf + 1, --n);
  507. continue;
  508. }
  509. // Command length (fixed per opcode). 0x0D = write-one-bond-slot (27 B); 0x05/0x0E/0x0F/0x10 carry
  510. // one value byte; 0x02 a field+value; 0x0A a 3-byte magic.
  511. uint8_t need = (op == 0x0D) ? 27 :
  512. (op == 0x02) ? 3 :
  513. (op == 0x03 || op == 0x05 ||
  514. op == 0x0E || op == 0x0F ||
  515. op == 0x10) ?
  516. 2 :
  517. (op == 0x0A) ? 4 :
  518. 1;
  519. if (n < need)
  520. break; // wait for more bytes
  521. if (op == 0x01) {
  522. g_blobRequest =
  523. true; // sent from the usbd task (webusbSofDrain)
  524. }
  525. // 0x09: export all bond slots (clone backup). One 0xA7 frame, sent from the usbd task.
  526. else if (op == 0x09) {
  527. g_bondExportRequest = true;
  528. }
  529. // 0x0F: stability test on/off. Puck->controller haptics do NOT reset the controller's own
  530. // user-input idle auto-off (we already poll it every 4ms without keeping it awake), so instead
  531. // signal host-awake: enable the E7 announce (0xE7 00 00 = host-awake vs 00 01 = suspended, per
  532. // the RE) so the controller is told the host is active and (hopefully) doesn't power-save. The
  533. // 10s buzz stays -- it also exercises the haptic-relay path that correlates with the hang.
  534. else if (op == 0x0F) {
  535. g_stabTest = (buf[1] != 0);
  536. g_e7b = 0; // byte2=0 => host-awake
  537. g_e7announce = g_stabTest;
  538. }
  539. // 0x10: stream the saved flight-recorder trail (0xA8 frames). buf[1]=1 restarts (header + rewind),
  540. // buf[1]=0 continues the drain. Drained straight from loop context like the 0x06 capture drain --
  541. // webusbDrainFlight is drop-on-full so it never blocks. Always available (recorder isn't OPK_LOG).
  542. else if (op == 0x10) {
  543. webusbDrainFlight(buf[1] != 0);
  544. }
  545. #if OPK_LOG
  546. // rewind drain: buf[1]=1 from boot (whole ring), 0 from now
  547. else if (op == 0x05) {
  548. hapLogResetDrain(buf[1] != 0);
  549. } else if (op == 0x06) {
  550. webusbDrainCapture();
  551. } // drain entries since the rewind (panel polls this)
  552. #endif
  553. // clear a stuck/latched haptic buzz on the controller
  554. else if (op == 0x07) {
  555. hapticReinit();
  556. }
  557. // trigger controller power-off (same path Steam 0x9F / host-suspend use)
  558. else if (op == 0x08) {
  559. hapticSendShutdown();
  560. }
  561. // factory wipe: erase cfg.bin + bonds.bin, reboot to clean defaults.
  562. // Guarded by a 3-byte magic ("ERS") so a stray/corrupt byte can never trigger it.
  563. // Irreversible -- the controller must be re-paired afterwards.
  564. else if (op == 0x0A) {
  565. if (buf[1] == 0x45 && buf[2] == 0x52 &&
  566. buf[3] == 0x53) {
  567. usb_web.flush();
  568. factoryErase();
  569. delay(40);
  570. faultDiagArmIntentionalReset();
  571. NVIC_SystemReset();
  572. }
  573. // reboot into serial DFU (adafruit-nrfutil)
  574. } else if (op == 0x0B) {
  575. usb_web.flush();
  576. delay(40);
  577. faultDiagArmIntentionalReset();
  578. enterSerialDfu();
  579. // reboot into UF2 bootloader (USB mass storage)
  580. } else if (op == 0x0C) {
  581. usb_web.flush();
  582. delay(40);
  583. faultDiagArmIntentionalReset();
  584. enterUf2Dfu();
  585. // 0x0D: write ONE bond slot into RAM -- the "clone onto this puck" side of Export/Import.
  586. // [0x0D][slot][used][24 rec]. used=0 (or an empty record) clears the slot. The panel sends
  587. // one of these per slot, then a 0x0E to persist + reboot. Kept to a single small command
  588. // (<=27 B) so it never spans a USB packet, and acked with a status blob so the panel's
  589. // read-after-write keeps the OUT pipe flowing (same shape as the proven 0x02 setters).
  590. } else if (op == 0x0D) {
  591. uint8_t slot = buf[1], used = buf[2];
  592. if (slot < NSLOT) {
  593. if (used && !recEmpty(buf + 3)) {
  594. memcpy(g_slot[slot].rec,
  595. buf + 3, 24);
  596. g_slot[slot].used = true;
  597. } else {
  598. memset(g_slot[slot].rec, 0, 24);
  599. g_slot[slot].used = false;
  600. }
  601. }
  602. g_blobRequest = true; // ack
  603. // 0x0E: commit an import -- persist the bond slots written by 0x0D, optionally apply a USB
  604. // mode ([0x0E][mode]; 0xFF = leave unchanged), then reboot so the RF session addresses
  605. // regenerate from the new bond UUIDs.
  606. } else if (op == 0x0E) {
  607. uint8_t mode = buf[1];
  608. saveBonds();
  609. if (modeValid(mode))
  610. saveMode(mode);
  611. usb_web.flush();
  612. delay(40);
  613. faultDiagArmIntentionalReset();
  614. NVIC_SystemReset();
  615. } else if (op == 0x02) {
  616. uint8_t f = buf[1], v = buf[2];
  617. // every settable field persists (poll rate is no longer settable)
  618. bool persist = true;
  619. // per-type cfg writes (protocol v10): field = 40 + et*8 + k, k: 0..3 back, 4 qam, 5 abSwap,
  620. // 6 padHaptics, 7 ledBright. Edits g_type[et]; refresh the live mirrors if it's the active type.
  621. if (f >= 40 && f < 40 + ET_COUNT * 8) {
  622. uint8_t et = (uint8_t)((f - 40) / 8),
  623. k = (uint8_t)((f - 40) % 8);
  624. if (et < ET_COUNT) {
  625. if (k < 4)
  626. g_type[et].back[k] = v;
  627. else if (k == 4)
  628. g_type[et].qamMap = v;
  629. else if (k == 5)
  630. g_type[et].abSwap =
  631. v ? 1 : 0;
  632. else if (k == 6)
  633. g_type[et].padHaptics =
  634. v ? 1 : 0;
  635. else if (k == 7)
  636. g_type[et].ledBright =
  637. v > 100 ? 100 :
  638. v;
  639. if (et == g_etype)
  640. applyActiveType();
  641. }
  642. saveCfg();
  643. g_blobRequest = true;
  644. memmove(buf, buf + need, n - need);
  645. n -= need;
  646. continue;
  647. }
  648. switch (f) {
  649. case 1:
  650. g_mDiv = v < 4 ? 4 : v;
  651. break;
  652. case 2:
  653. g_mFric = v > 99 ? 99 : v;
  654. break;
  655. // case 3 (padSmooth) removed -- Steam-mode pad coords are forwarded raw; Steam does its own smoothing.
  656. // Legacy single-value fields (4 abSwap, 5-8 back, 21 qam) edit the ACTIVE emulated type.
  657. case 4:
  658. if (g_etype < ET_COUNT) {
  659. g_type[g_etype].abSwap = v ? 1 :
  660. 0;
  661. applyActiveType();
  662. }
  663. break;
  664. case 5:
  665. case 6:
  666. case 7:
  667. case 8:
  668. if (g_etype < ET_COUNT) {
  669. g_type[g_etype].back[f - 5] = v;
  670. applyActiveType();
  671. }
  672. break;
  673. // case 9 (pollU100) removed -- poll rate is fixed at POLL_US_DEFAULT and no longer configurable.
  674. // E7 protocol-version B-byte (experimental v1 fast)
  675. case 10:
  676. g_e7b = v ? 1 : 0;
  677. break;
  678. // haptic-relay opcode
  679. case 11:
  680. g_relayOp = v;
  681. break;
  682. // haptic-relay sub-type
  683. case 12:
  684. g_relaySub = v;
  685. break;
  686. case 13:
  687. g_testHaptic = v ? v : 40;
  688. break; // inject v test haptics (0->40)
  689. // Steam: forward only fresh reports (dedupe)
  690. case 14:
  691. g_fwdNewOnly = v ? 1 : 0;
  692. break;
  693. case 15:
  694. g_qos = v ? 1 : 0;
  695. g_hopIdx = 0;
  696. g_qosBad = 0;
  697. g_qosCheckMs = millis();
  698. break; // QoS adaptive channel hopping
  699. // persist last mode across reboots (else always boot Steam)
  700. case 16:
  701. g_persistMode = v ? true : false;
  702. break;
  703. // back4+B/X/Y mode assignments
  704. case 17:
  705. case 18:
  706. case 19:
  707. if (modeValid(v))
  708. g_chordBtn[f - 17] = v;
  709. break;
  710. // reboot once WITH the CDC serial console (puck mode), then auto-revert
  711. case 20:
  712. armDebugCdcNextBoot();
  713. usb_web.flush();
  714. delay(40);
  715. faultDiagArmIntentionalReset();
  716. NVIC_SystemReset();
  717. break;
  718. // QAM physical button remap code (0=default/unmapped) -- active emulated type
  719. case 21:
  720. if (g_etype < ET_COUNT) {
  721. g_type[g_etype].qamMap = v;
  722. applyActiveType();
  723. }
  724. break;
  725. // rumble strength % (0=off, 100=1x, 200=double)
  726. case 22:
  727. g_rumbleScale = v;
  728. break;
  729. // Switch Pro report rate (0=66Hz,1=120Hz,2=full)
  730. case 23:
  731. g_swProRate = (v <= 2) ? v : 2;
  732. swProSaveCfg();
  733. persist = false;
  734. break;
  735. case 24:
  736. g_swGyroScale10 =
  737. (v >= 5 && v <= 30) ? v : 10;
  738. swProSaveCfg();
  739. persist = false;
  740. break; // Switch Pro gyro scale x10
  741. // (field 25, poll RX window, removed -- g_rxWin is now FIXED/not configurable)
  742. // (fields 27/28, post-connect haptic block, removed -- permanently disabled)
  743. // EXPERIMENT: land ALL relayed 0x87 config verbatim (real-puck relay)
  744. // instead of the discard-whitelist. Persisted; blob p[177] reflects state.
  745. case 29:
  746. g_landAll87 = v ? 1 : 0;
  747. break;
  748. }
  749. if (persist)
  750. saveCfg();
  751. g_blobRequest = true;
  752. } else if (op == 0x03) {
  753. uint8_t m = buf[1];
  754. if (modeValid(m) && !USBDevice.suspended()) {
  755. // best-effort status to the panel before the reboot; the SOF drain
  756. // sends it during the delay(40) below (no blocking flush on loop()).
  757. g_blobRequest = true;
  758. saveMode(m);
  759. delay(40);
  760. faultDiagArmIntentionalReset();
  761. NVIC_SystemReset();
  762. }
  763. }
  764. memmove(buf, buf + need, n - need);
  765. n -= need;
  766. }
  767. }
  768. }