webusb_config.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  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][gitHash 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. #define WB_PAYLEN 139
  37. // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob
  38. // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank
  39. // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without
  40. // that flag (or a future blob that outgrows the FIFO) fails loudly instead of shipping a dead panel.
  41. static_assert(CFG_TUD_VENDOR_TX_BUFSIZE >= 2 + WB_PAYLEN,
  42. "CFG_TUD_VENDOR_TX_BUFSIZE too small to hold a WebUSB blob in one "
  43. "write -- build via `make build` (sets 256), or raise the flag.");
  44. static void webusbSendBlob()
  45. {
  46. if (!usb_web.connected())
  47. return;
  48. // "connected" = the most recent poll cycle's slot had a fresh F-reply (matches what the firmware
  49. // presents to Steam on 0x79). The blob is sent on the panel's poll, so the slot + up flag change
  50. // every ~250ms in normal use.
  51. int cs = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  52. bool up = (g_curSlot >= 0 && (millis() - g_connReplyMs[cs]) < 300);
  53. uint8_t p[2 + WB_PAYLEN];
  54. p[0] = 0xA5;
  55. p[1] = WB_PAYLEN;
  56. // protocol version (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)
  57. p[2] = 12;
  58. p[3] = g_usbMode;
  59. p[4] = (uint8_t)g_mDiv;
  60. p[5] = (uint8_t)g_mFric;
  61. p[6] = g_qamMap;
  62. p[7] = g_abSwap;
  63. p[8] = g_back[0];
  64. p[9] = g_back[1];
  65. p[10] = g_back[2];
  66. p[11] = g_back[3];
  67. p[12] = (g_curSlot >= 0) ? (uint8_t)g_curSlot : 0xFF;
  68. p[13] = up ? 1 : 0;
  69. p[14] = (uint8_t)g_f1ps;
  70. p[15] = (uint8_t)(g_f1ps >> 8);
  71. p[16] = (uint8_t)(g_pollUs / 100);
  72. p[17] = (uint8_t)g_newps;
  73. p[18] = (uint8_t)(g_newps >> 8);
  74. p[19] = g_e7b;
  75. p[20] = g_relayOp;
  76. p[21] = g_relaySub;
  77. p[22] = g_fwdNewOnly;
  78. p[23] = g_qos;
  79. p[24] = g_persistMode ? 1 : 0;
  80. p[25] = g_chordBtn[0];
  81. p[26] = g_chordBtn[1];
  82. p[27] = g_chordBtn[2];
  83. p[28] = (uint8_t)g_pollsps;
  84. // poll TX rate (vs delivered/new -> starvation vs reply-loss)
  85. p[29] = (uint8_t)(g_pollsps >> 8);
  86. p[30] = (uint8_t)g_loopPeriodUs;
  87. p[31] = (uint8_t)(g_loopPeriodUs >> 8); // avg loop iteration time
  88. p[32] = g_loopWorst;
  89. p[33] = (uint8_t)g_loopWorstUs;
  90. p[34] = (uint8_t)(g_loopWorstUs >> 8);
  91. p[35] = (uint8_t)g_pollPeriodUs;
  92. p[36] = (uint8_t)(g_pollPeriodUs >>
  93. 8); // measured poll period (intended 4000)
  94. p[37] = OPK_LOG; // logging build? panel shows/hides its log UI
  95. p[38] = g_battery
  96. [g_curSlot >= 0 && g_curSlot < NSLOT ?
  97. g_curSlot :
  98. 0]; // controller battery % (report 0x43); 0=unknown
  99. p[39] = g_linkRssi[g_curSlot >= 0 && g_curSlot < NSLOT ?
  100. g_curSlot :
  101. 0]; // RAW signal strength |dBm| (0=no sample)
  102. // git commit this firmware was built from + dirty flag; injected at build time
  103. // (build_info.h / gen_version.sh); "unknown" if the version header wasn't generated.
  104. p[40] = OPK_GIT_DIRTY ? 1 : 0;
  105. memset(&p[41], 0, 12); // 12B ASCII git hash, NUL-padded
  106. {
  107. const char *h = OPK_GIT_HASH;
  108. for (uint8_t i = 0; i < 12 && h[i]; i++)
  109. p[41 + i] = (uint8_t)h[i];
  110. }
  111. p[53] = g_rumbleScale; // rumble strength % (protocol v5)
  112. // Switch Pro report rate 0=66/1=120/2=full (protocol v6)
  113. p[54] = g_swProRate;
  114. // Switch Pro gyro sensitivity x10 (protocol v6)
  115. p[55] = g_swGyroScale10;
  116. {
  117. int16_t a[3] = { g_in[0].ax, g_in[0].ay, g_in[0].az };
  118. memcpy(&p[56], a, 6);
  119. } // raw accelerometer for scale diagnostics (protocol v7)
  120. // per-slot link status for all bond slots (protocol v8)
  121. p[62] = (uint8_t)bondedSlotCount();
  122. {
  123. unsigned long nowMs = millis();
  124. for (int s = 0; s < NSLOT; s++) {
  125. bool sup = g_slot[s].used && g_connReplyMs[s] != 0 &&
  126. (nowMs - g_connReplyMs[s]) < 300u;
  127. p[63 + s * 3] = sup ? 1 : 0;
  128. p[64 + s * 3] = g_battery[s];
  129. p[65 + s * 3] = g_linkRssi[s];
  130. }
  131. }
  132. // per-emulated-type button config (protocol v10): 4 types x 8 bytes from p[75]
  133. for (int et = 0; et < ET_COUNT; et++) {
  134. uint8_t *q = &p[75 + et * 8];
  135. q[0] = g_type[et].back[0];
  136. q[1] = g_type[et].back[1];
  137. q[2] = g_type[et].back[2];
  138. q[3] = g_type[et].back[3];
  139. q[4] = g_type[et].qamMap;
  140. q[5] = g_type[et].abSwap;
  141. q[6] = g_type[et].padHaptics;
  142. q[7] = g_type[et].ledBright;
  143. }
  144. // last-boot reset cause (protocol v11): why we (re)booted -- watchdog hang vs HardFault vs intentional
  145. // reboot vs power-on (issue #72). p[107] = RR_* code; p[108..111] = raw RESETREAS for the curious.
  146. p[107] = faultDiagReason();
  147. uint32_t rr = faultDiagResetReas();
  148. p[108] = (uint8_t)rr;
  149. p[109] = (uint8_t)(rr >> 8);
  150. p[110] = (uint8_t)(rr >> 16);
  151. p[111] = (uint8_t)(rr >> 24);
  152. // Connection tunables (protocol v11): poll RX window in 10us units, post-connect haptic block enable,
  153. // and block duration in seconds.
  154. p[112] = (uint8_t)(g_rxWin / 10);
  155. p[113] = g_hapticBlockOn;
  156. p[114] = (uint8_t)(g_hapticBlockMs / 1000);
  157. // RF wedge diagnostics (always populated, even when the link reads down): CRC-fail/s and no-reply/s,
  158. // capped at 255. With Polls/s this distinguishes poll-loop-stopped vs RX-dead vs corrupt-replies.
  159. p[115] = (uint8_t)(g_crcps > 255 ? 255 : g_crcps);
  160. p[116] = (uint8_t)(g_norxps > 255 ? 255 : g_norxps);
  161. p[117] = (uint8_t)(g_rfStallRecover > 255 ? 255 : g_rfStallRecover);
  162. // v12: relay-frame TX rate (separated from true poll cycles -- Polls/s now reads ~250, Relay/s exposes the
  163. // host output-report flood that steals reply windows), and the clock fingerprint for clone-board triage.
  164. p[118] = (uint8_t)g_relayps;
  165. p[119] = (uint8_t)(g_relayps >> 8);
  166. p[120] = clockLfSrc();
  167. p[121] = clockHfSrc();
  168. uint16_t upm = clockUsPerMs();
  169. p[122] = (uint8_t)upm;
  170. p[123] = (uint8_t)(upm >> 8);
  171. // last hang's loop stage (0xFF = last boot wasn't a watchdog/lockup hang, or GPREGRET2 didn't survive)
  172. p[124] = faultDiagHangStage();
  173. // LIVE hang localization: current loop stage + how long loop() has been stuck (40ms units, capped). When
  174. // loop() wedges these keep updating because the blob is sent from the SOF interrupt, not loop().
  175. p[125] = faultDiagCurStage();
  176. uint32_t sms = faultDiagStallMs();
  177. p[126] = (uint8_t)(sms / 40 > 255 ? 255 : sms / 40);
  178. // relay-ring fault count: non-zero = we caught+recovered a desynced/corrupt ring that would otherwise be an
  179. // invisible IRQ-off watchdog hang. A climbing count points at the haptic relay path / memory corruption.
  180. p[127] = (uint8_t)g_ringFault;
  181. p[128] = (uint8_t)(g_ringFault >> 8);
  182. // PC/LR of the stuck code captured by the WDT pre-reset ISR on the last watchdog hang (0 = none captured =
  183. // the hang hard-masked interrupts). Map with addr2line on the .elf to name the function.
  184. uint32_t hpc = faultDiagHangPC(), hlr = faultDiagHangLR();
  185. p[129] = (uint8_t)hpc;
  186. p[130] = (uint8_t)(hpc >> 8);
  187. p[131] = (uint8_t)(hpc >> 16);
  188. p[132] = (uint8_t)(hpc >> 24);
  189. p[133] = (uint8_t)hlr;
  190. p[134] = (uint8_t)(hlr >> 8);
  191. p[135] = (uint8_t)(hlr >> 16);
  192. p[136] = (uint8_t)(hlr >> 24);
  193. // least-ever free stack (words) on the usbd task (800B total) and loop task -- usbd trending to 0 confirms
  194. // the overflow. words, not bytes.
  195. uint16_t usbdFree = faultDiagUsbdStackFree(), loopFree = faultDiagLoopStackFree();
  196. p[137] = (uint8_t)usbdFree;
  197. p[138] = (uint8_t)(usbdFree >> 8);
  198. p[139] = (uint8_t)loopFree;
  199. p[140] = (uint8_t)(loopFree >> 8);
  200. // CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the
  201. // panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a
  202. // backgrounded tab, or the host briefly not servicing transferIn under load -- the FIFO never empties and
  203. // that spin hangs loop() until the ~8s watchdog resets us (RESETREAS=watchdog/hang). This is the ONE
  204. // cross-task tud_ call the marshalling refactor left able to block loop() (HID goes through the drop-oldest
  205. // ring; this didn't). So DROP the blob whenever the FIFO can't take it whole -- a status panel missing an
  206. // occasional frame is invisible, and loop() can never stall here again. (Matches the closing-the-panel
  207. // "fix": that just flips _connected=false to break the same spin.)
  208. if (tud_vendor_write_available() >= sizeof p) {
  209. usb_web.write(p, sizeof p);
  210. usb_web.flush();
  211. }
  212. }
  213. // Bond export frame (panel "Export config to file"). One self-describing frame carrying all four bond slots so
  214. // the browser can clone a puck onto another without re-pairing:
  215. // [0xA7][len][ver=1][usedMask][slot0 rec 24]...[slot3 rec 24] (len = 2 + NSLOT*24 = 98)
  216. // usedMask bit s = slot s bonded. The session RF address is NOT exported: each puck re-derives it from the bond
  217. // UUID + its own FICR DEVICEID at boot, and the bonded controller relearns it from the E1 host beacon on every
  218. // reconnect (rf_link.cpp) -- so the 24-byte record is the whole portable identity. 98+2 < transferIn(128) and
  219. // < CFG_TUD_VENDOR_TX_BUFSIZE (256), so it sends whole in one drop-on-full write like the status blob.
  220. #define WB_BOND_PAYLEN (2 + NSLOT * 24)
  221. static void webusbSendBondExport()
  222. {
  223. if (!usb_web.connected())
  224. return;
  225. uint8_t p[2 + WB_BOND_PAYLEN];
  226. p[0] = 0xA7;
  227. p[1] = WB_BOND_PAYLEN;
  228. p[2] = 1; // format version
  229. uint8_t mask = 0;
  230. for (int s = 0; s < NSLOT; s++) {
  231. if (g_slot[s].used)
  232. mask |= (uint8_t)(1u << s);
  233. memcpy(&p[4 + s * 24], g_slot[s].rec, 24);
  234. }
  235. p[3] = mask;
  236. // Same anti-hang rule as the status blob: drop the frame if the FIFO can't take it whole, never spin.
  237. if (tud_vendor_write_available() >= sizeof p) {
  238. usb_web.write(p, sizeof p);
  239. usb_web.flush();
  240. }
  241. }
  242. // Runs on the usbd task (registered via usbTxRegisterDrain -> tud_sof_cb). Sends the blob if loop() asked for
  243. // one. Keeps every usb_web write/flush off the loop task so it can't block on the device event queue.
  244. static void webusbSofDrain(void)
  245. {
  246. // If loop() has stopped beating, it's wedged -- keep pushing the blob (which carries the live stuck stage)
  247. // so the panel shows WHERE it hung during the ~8s before the watchdog fires. This runs on the SOF IRQ, so
  248. // it works even though loop() is dead. drop-on-full in webusbSendBlob keeps it from ever blocking.
  249. if (faultDiagStallMs() > 300)
  250. g_blobRequest = true;
  251. if (g_blobRequest) {
  252. g_blobRequest = false;
  253. webusbSendBlob();
  254. }
  255. if (g_bondExportRequest) {
  256. g_bondExportRequest = false;
  257. webusbSendBondExport();
  258. }
  259. }
  260. // Register the SOF drain. Call once from setup() (harmless even if WebUSB isn't enumerated -- webusbSendBlob
  261. // no-ops while disconnected, and g_blobRequest is only ever set by panel traffic, which needs a connection).
  262. void webusbInit(void)
  263. {
  264. usbTxRegisterDrain(webusbSofDrain);
  265. }
  266. #if OPK_LOG
  267. // Stream the capture ring (haptics / relayed host commands) to the panel as 0xA6 frames. Frame formats:
  268. // entry: [0xA6][L][T=1][age u32 LE][slot][rid][n][bytes n] (L = 8 + n)
  269. // end: [0xA6][1][T=0]
  270. static void webusbCapFrame(uint32_t ms, uint8_t slot, uint8_t rid, uint8_t nb,
  271. const uint8_t *b)
  272. {
  273. if (nb > 16)
  274. nb = 16;
  275. uint8_t f[2 + 9 + 16];
  276. uint8_t L = (uint8_t)(8 + nb);
  277. f[0] = 0xA6;
  278. f[1] = L;
  279. f[2] = 1;
  280. f[3] = (uint8_t)ms;
  281. f[4] = (uint8_t)(ms >> 8);
  282. f[5] = (uint8_t)(ms >> 16);
  283. f[6] = (uint8_t)(ms >> 24); // absolute log time
  284. f[7] = slot;
  285. f[8] = rid;
  286. f[9] = nb;
  287. memcpy(f + 10, b, nb);
  288. usb_web.write(f, (uint16_t)(2 + L));
  289. usb_web.flush();
  290. }
  291. static void webusbCapEnd()
  292. {
  293. uint8_t e[3] = { 0xA6, 1, 0 };
  294. usb_web.write(e, 3);
  295. usb_web.flush();
  296. }
  297. // 0x06: drain entries since the last drain (or since the 0x05 start/rewind). The panel polls this on a
  298. // timer and accumulates, so a dump-from-boot of the whole ring streams over many polls without blocking the
  299. // loop. A per-call budget bounds how long one poll spends here.
  300. static void webusbDrainCapture()
  301. {
  302. if (!usb_web.connected())
  303. return;
  304. uint32_t ms = 0;
  305. uint8_t slot = 0, rid = 0, nb = 0, b[16];
  306. uint16_t budget = 128;
  307. // Same anti-hang rule as the blob: webusbCapFrame's usb_web.write() spins until the FIFO drains, so stop
  308. // pulling the moment the FIFO can't hold a max-size frame (2+9+16=27 B) -- the panel resumes the drain on
  309. // its next 0x06 poll. Never let a backed-up panel hang loop() here.
  310. while (budget-- && tud_vendor_write_available() >= 27 &&
  311. hapLogPull(&ms, &slot, &rid, &nb, b))
  312. webusbCapFrame(ms, slot, rid, nb, b);
  313. if (tud_vendor_write_available() >= 3)
  314. webusbCapEnd();
  315. }
  316. #endif // OPK_LOG
  317. void webusbPoll()
  318. {
  319. // 40 B holds the largest command: 0x0D writes one bond slot = [0x0D][slot][used][24 rec] = 27 B (still
  320. // inside one 64-B USB-FS OUT packet, so no command ever spans packets). Every other command is <= 4 B.
  321. static uint8_t buf[40];
  322. static uint8_t n = 0;
  323. while (usb_web.available()) {
  324. int c = usb_web.read();
  325. if (c < 0)
  326. break;
  327. if (n < sizeof buf)
  328. buf[n++] = (uint8_t)c;
  329. // process complete commands from the front of buf
  330. for (;;) {
  331. if (n == 0)
  332. break;
  333. uint8_t op = buf[0];
  334. if (op < 0x01 || op > 0x0F) { // resync: drop one byte
  335. memmove(buf, buf + 1, --n);
  336. continue;
  337. }
  338. // Command length (fixed per opcode). 0x0D = write-one-bond-slot (27 B); 0x05/0x0E/0x0F carry
  339. // one value byte; 0x02 a field+value; 0x0A a 3-byte magic.
  340. uint8_t need = (op == 0x0D) ? 27 :
  341. (op == 0x02) ? 3 :
  342. (op == 0x03 || op == 0x05 ||
  343. op == 0x0E || op == 0x0F) ? 2 :
  344. (op == 0x0A) ? 4 :
  345. 1;
  346. if (n < need)
  347. break; // wait for more bytes
  348. if (op == 0x01) {
  349. g_blobRequest =
  350. true; // sent from the usbd task (webusbSofDrain)
  351. }
  352. // 0x09: export all bond slots (clone backup). One 0xA7 frame, sent from the usbd task.
  353. else if (op == 0x09) {
  354. g_bondExportRequest = true;
  355. }
  356. // 0x0F: stability test on/off. Puck->controller haptics do NOT reset the controller's own
  357. // user-input idle auto-off (we already poll it every 4ms without keeping it awake), so instead
  358. // signal host-awake: enable the E7 announce (0xE7 00 00 = host-awake vs 00 01 = suspended, per
  359. // the RE) so the controller is told the host is active and (hopefully) doesn't power-save. The
  360. // 10s buzz stays -- it also exercises the haptic-relay path that correlates with the hang.
  361. else if (op == 0x0F) {
  362. g_stabTest = (buf[1] != 0);
  363. g_e7b = 0; // byte2=0 => host-awake
  364. g_e7announce = g_stabTest;
  365. }
  366. #if OPK_LOG
  367. // rewind drain: buf[1]=1 from boot (whole ring), 0 from now
  368. else if (op == 0x05) {
  369. hapLogResetDrain(buf[1] != 0);
  370. } else if (op == 0x06) {
  371. webusbDrainCapture();
  372. } // drain entries since the rewind (panel polls this)
  373. #endif
  374. // clear a stuck/latched haptic buzz on the controller
  375. else if (op == 0x07) {
  376. hapticReinit();
  377. }
  378. // trigger controller power-off (same path Steam 0x9F / host-suspend use)
  379. else if (op == 0x08) {
  380. hapticSendShutdown();
  381. }
  382. // factory wipe: erase cfg.bin + bonds.bin, reboot to clean defaults.
  383. // Guarded by a 3-byte magic ("ERS") so a stray/corrupt byte can never trigger it.
  384. // Irreversible -- the controller must be re-paired afterwards.
  385. else if (op == 0x0A) {
  386. if (buf[1] == 0x45 && buf[2] == 0x52 &&
  387. buf[3] == 0x53) {
  388. usb_web.flush();
  389. factoryErase();
  390. delay(40);
  391. faultDiagArmIntentionalReset();
  392. NVIC_SystemReset();
  393. }
  394. // reboot into serial DFU (adafruit-nrfutil)
  395. } else if (op == 0x0B) {
  396. usb_web.flush();
  397. delay(40);
  398. faultDiagArmIntentionalReset();
  399. enterSerialDfu();
  400. // reboot into UF2 bootloader (USB mass storage)
  401. } else if (op == 0x0C) {
  402. usb_web.flush();
  403. delay(40);
  404. faultDiagArmIntentionalReset();
  405. enterUf2Dfu();
  406. // 0x0D: write ONE bond slot into RAM -- the "clone onto this puck" side of Export/Import.
  407. // [0x0D][slot][used][24 rec]. used=0 (or an empty record) clears the slot. The panel sends
  408. // one of these per slot, then a 0x0E to persist + reboot. Kept to a single small command
  409. // (<=27 B) so it never spans a USB packet, and acked with a status blob so the panel's
  410. // read-after-write keeps the OUT pipe flowing (same shape as the proven 0x02 setters).
  411. } else if (op == 0x0D) {
  412. uint8_t slot = buf[1], used = buf[2];
  413. if (slot < NSLOT) {
  414. if (used && !recEmpty(buf + 3)) {
  415. memcpy(g_slot[slot].rec, buf + 3,
  416. 24);
  417. g_slot[slot].used = true;
  418. } else {
  419. memset(g_slot[slot].rec, 0, 24);
  420. g_slot[slot].used = false;
  421. }
  422. }
  423. g_blobRequest = true; // ack
  424. // 0x0E: commit an import -- persist the bond slots written by 0x0D, optionally apply a USB
  425. // mode ([0x0E][mode]; 0xFF = leave unchanged), then reboot so the RF session addresses
  426. // regenerate from the new bond UUIDs.
  427. } else if (op == 0x0E) {
  428. uint8_t mode = buf[1];
  429. saveBonds();
  430. if (modeValid(mode))
  431. saveMode(mode);
  432. usb_web.flush();
  433. delay(40);
  434. faultDiagArmIntentionalReset();
  435. NVIC_SystemReset();
  436. } else if (op == 0x02) {
  437. uint8_t f = buf[1], v = buf[2];
  438. // every settable field persists (poll rate is no longer settable)
  439. bool persist = true;
  440. // per-type cfg writes (protocol v10): field = 40 + et*8 + k, k: 0..3 back, 4 qam, 5 abSwap,
  441. // 6 padHaptics, 7 ledBright. Edits g_type[et]; refresh the live mirrors if it's the active type.
  442. if (f >= 40 && f < 40 + ET_COUNT * 8) {
  443. uint8_t et = (uint8_t)((f - 40) / 8),
  444. k = (uint8_t)((f - 40) % 8);
  445. if (et < ET_COUNT) {
  446. if (k < 4)
  447. g_type[et].back[k] = v;
  448. else if (k == 4)
  449. g_type[et].qamMap = v;
  450. else if (k == 5)
  451. g_type[et].abSwap =
  452. v ? 1 : 0;
  453. else if (k == 6)
  454. g_type[et].padHaptics =
  455. v ? 1 : 0;
  456. else if (k == 7)
  457. g_type[et].ledBright =
  458. v > 100 ? 100 :
  459. v;
  460. if (et == g_etype)
  461. applyActiveType();
  462. }
  463. saveCfg();
  464. g_blobRequest = true;
  465. memmove(buf, buf + need, n - need);
  466. n -= need;
  467. continue;
  468. }
  469. switch (f) {
  470. case 1:
  471. g_mDiv = v < 4 ? 4 : v;
  472. break;
  473. case 2:
  474. g_mFric = v > 99 ? 99 : v;
  475. break;
  476. // case 3 (padSmooth) removed -- Steam-mode pad coords are forwarded raw; Steam does its own smoothing.
  477. // Legacy single-value fields (4 abSwap, 5-8 back, 21 qam) edit the ACTIVE emulated type.
  478. case 4:
  479. if (g_etype < ET_COUNT) {
  480. g_type[g_etype].abSwap = v ? 1 :
  481. 0;
  482. applyActiveType();
  483. }
  484. break;
  485. case 5:
  486. case 6:
  487. case 7:
  488. case 8:
  489. if (g_etype < ET_COUNT) {
  490. g_type[g_etype].back[f - 5] = v;
  491. applyActiveType();
  492. }
  493. break;
  494. // case 9 (pollU100) removed -- poll rate is fixed at POLL_US_DEFAULT and no longer configurable.
  495. // E7 protocol-version B-byte (experimental v1 fast)
  496. case 10:
  497. g_e7b = v ? 1 : 0;
  498. break;
  499. // haptic-relay opcode
  500. case 11:
  501. g_relayOp = v;
  502. break;
  503. // haptic-relay sub-type
  504. case 12:
  505. g_relaySub = v;
  506. break;
  507. case 13:
  508. g_testHaptic = v ? v : 40;
  509. break; // inject v test haptics (0->40)
  510. // Steam: forward only fresh reports (dedupe)
  511. case 14:
  512. g_fwdNewOnly = v ? 1 : 0;
  513. break;
  514. case 15:
  515. g_qos = v ? 1 : 0;
  516. g_hopIdx = 0;
  517. g_qosBad = 0;
  518. g_qosCheckMs = millis();
  519. break; // QoS adaptive channel hopping
  520. // persist last mode across reboots (else always boot Steam)
  521. case 16:
  522. g_persistMode = v ? true : false;
  523. break;
  524. // back4+B/X/Y mode assignments
  525. case 17:
  526. case 18:
  527. case 19:
  528. if (modeValid(v))
  529. g_chordBtn[f - 17] = v;
  530. break;
  531. // reboot once WITH the CDC serial console (puck mode), then auto-revert
  532. case 20:
  533. armDebugCdcNextBoot();
  534. usb_web.flush();
  535. delay(40);
  536. faultDiagArmIntentionalReset();
  537. NVIC_SystemReset();
  538. break;
  539. // QAM physical button remap code (0=default/unmapped) -- active emulated type
  540. case 21:
  541. if (g_etype < ET_COUNT) {
  542. g_type[g_etype].qamMap = v;
  543. applyActiveType();
  544. }
  545. break;
  546. // rumble strength % (0=off, 100=1x, 200=double)
  547. case 22:
  548. g_rumbleScale = v;
  549. break;
  550. // Switch Pro report rate (0=66Hz,1=120Hz,2=full)
  551. case 23:
  552. g_swProRate = (v <= 2) ? v : 2;
  553. swProSaveCfg();
  554. persist = false;
  555. break;
  556. case 24:
  557. g_swGyroScale10 =
  558. (v >= 5 && v <= 30) ? v : 10;
  559. swProSaveCfg();
  560. persist = false;
  561. break; // Switch Pro gyro scale x10
  562. // (field 25, poll RX window, removed -- g_rxWin is now FIXED/not configurable)
  563. // (fields 27/28, post-connect haptic block, removed -- permanently disabled)
  564. }
  565. if (persist)
  566. saveCfg();
  567. g_blobRequest = true;
  568. } else if (op == 0x03) {
  569. uint8_t m = buf[1];
  570. if (modeValid(m) && !USBDevice.suspended()) {
  571. // best-effort status to the panel before the reboot; the SOF drain
  572. // sends it during the delay(40) below (no blocking flush on loop()).
  573. g_blobRequest = true;
  574. saveMode(m);
  575. delay(40);
  576. faultDiagArmIntentionalReset();
  577. NVIC_SystemReset();
  578. }
  579. }
  580. memmove(buf, buf + need, n - need);
  581. n -= need;
  582. }
  583. }
  584. }