webusb_config.cpp 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. #include "webusb_config.h"
  2. #include "board_config.h"
  3. #include "config.h"
  4. #include "bonds.h"
  5. #include "rf_link.h"
  6. #include "haptics.h"
  7. #include "puck_hid.h"
  8. #include "triton.h" // g_in raw IMU (diagnostic readout)
  9. #include "lizard_map.h"
  10. #include "build_info.h"
  11. #include "fault_diag.h"
  12. #include "fw_update.h"
  13. #include "usb_tx.h"
  14. #include "usb_mount.h" // modeSwitchReboot()
  15. #include <Arduino.h>
  16. #include <string.h>
  17. Adafruit_USBD_WebUSB usb_web;
  18. // Vendor-type control requests and the vendor interface's class requests both land on one global TinyUSB
  19. // callback, which the Adafruit core defines for WebUSB. Linker wrapping (-Wl,--wrap, set in the Makefile)
  20. // gives MODE_XBOX_OG first refusal on the XID requests without copying that router; whatever it declines, and
  21. // every other mode, must still reach __real_ -- that answers WebUSB's 0x22, whose connected flag gates
  22. // the panel sends.
  23. bool xboxOgVendorControlXfer(uint8_t rhport, uint8_t stage,
  24. const tusb_control_request_t *request);
  25. extern "C" bool
  26. __real_tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage,
  27. const tusb_control_request_t *request);
  28. extern "C" bool
  29. __wrap_tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage,
  30. const tusb_control_request_t *request)
  31. {
  32. if (g_usbMode == MODE_XBOX_OG &&
  33. xboxOgVendorControlXfer(rhport, stage, request))
  34. return true;
  35. return __real_tud_vendor_control_xfer_cb(rhport, stage, request);
  36. }
  37. // Panel-blob send is deferred to the usbd task: webusbPoll() (loop) just sets this, and webusbSofDrain()
  38. // (registered with usb_tx, runs from tud_sof_cb on the usbd task) does the actual write+flush. usb_web.flush()
  39. // goes through the same blocking osd_queue_send path as HID sends, so doing it from loop() could stall the
  40. // loop and trip the watchdog under load -- with the panel open during heavy use that was a live risk.
  41. static volatile bool g_blobRequest = false;
  42. // Bond export ("clone" backup): a 0x09 panel command asks for a one-shot dump of all four bond slots so the
  43. // browser can save them to a file and restore them onto a second puck. Like the status blob, the actual send
  44. // is deferred to the usbd task (webusbSofDrain) so usb_web.write()/flush() never block loop().
  45. static volatile bool g_bondExportRequest = false;
  46. // Firmware-update ack ([0xAB][5][status][nextOff u32 LE]). Like the blob it is written from the usbd task
  47. // (webusbSofDrain), but unlike the blob it is NEVER dropped -- the panel's transfer flow-control is strict
  48. // ping-pong on these acks, so an unsent ack just stays pending until the FIFO has room (the panel is
  49. // actively draining the IN pipe during an update, so that is at most a few SOFs).
  50. static volatile bool g_fwupAckPend = false;
  51. static volatile uint8_t g_fwupAckStatus = 0;
  52. static volatile uint32_t g_fwupAckOff = 0;
  53. static void fwupAckPost(uint8_t status)
  54. {
  55. g_fwupAckStatus = status;
  56. g_fwupAckOff = fwupNextOff();
  57. g_fwupAckPend = true;
  58. }
  59. static bool boardCommand(uint8_t op)
  60. {
  61. #if !OPK_HAS_ADAFRUIT_DFU
  62. // The factory Open DFU bootloader uses a different settings page.
  63. // 0x25 (full board wipe) is rejected for the same reason AND because its recovery story doesn't exist
  64. // here: it erases the app expecting the resident UF2 bootloader to come up as a drag-and-drop drive,
  65. // and its bootloader-settings page (0xFF000) lands inside this board's factory bootloader region.
  66. if (op == 0x0B || op == 0x0C || op == 0x25) {
  67. g_blobRequest = true;
  68. return true;
  69. }
  70. if (op >= 0x20 && op <= 0x24) {
  71. fwupAckPost(FWUP_ERR_STATE);
  72. return true;
  73. }
  74. #endif
  75. return false;
  76. }
  77. // blob payload = [ver=10][mode][mDiv][mFric][qamMap(active)][abSwap(active)][back0..3(active)][connSlot(0xFF=none)][linkUp]
  78. // [f1ps_lo][f1ps_hi][pollU100][newps_lo][newps_hi][e7b][relayOp][relaySub][fwdNewOnly]
  79. // [qos][persistMode][chordBtn B][chordBtn X][chordBtn Y][pollsps_lo][pollsps_hi]
  80. // [loopPeriod_lo][loopPeriod_hi][loopWorstIdx][loopWorstUs_lo][loopWorstUs_hi]
  81. // [pollPeriod_lo][pollPeriod_hi][logEnabled][battery%][rssi|dBm|]
  82. // [gitDirty][buildId 12B ASCII, NUL-padded][rsvd x3: ex rumbleScale/swProRate/swGyroScale10][raw accel ax ay az 3x s16 LE]
  83. // [bondedCount][slot0_up][slot0_batt][slot0_rssi]...[slot3_up][slot3_batt][slot3_rssi]
  84. // [v10/v17: per-type cfg, 4x9B: ET_XBOX/SWITCH/DS4/DS5 each {back0..3, qam, abSwap, padHaptics, ledBright, rumble}]
  85. // [v11: resetReason(RR_* code)][resetReas raw u32 LE][rxWin/10 us][hapticBlockOn][hapticBlock s]
  86. // p[6]/p[7]/p[8..11] mirror the ACTIVE type (legacy display). v11 extends to 113 bytes (115 total incl header);
  87. // browser reads with transferIn(128) to span the two USB-FS packets.
  88. // [diag: crc/s][noRx/s][rfStallRecover count]
  89. // [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)]
  90. // [v13: per-slot link stats, 4x9B from p[145]: {pollsps u16, f1ps u16, newps u16, crc/s u8,
  91. // noRx/s u8, relay/s u8} -- each controller's own rates (the v4 aggregates are their sums)]
  92. // [v14/v17: p[181] landAll87 (verbatim-0x87-relay experiment toggle)]
  93. // [v18: p[182..185] chordDpad left/up/right/down (back4+D-pad mode assignments)]
  94. // [v19: p[186] swGyroLegacy (Switch Pro gyro mapping: 0 = corrected, 1 = legacy/pre-#189)]
  95. // [v20: p[187..194] per-type trackpad->stick map, 4x2B {left pad, right pad} (PS_OFF/LEFT/RIGHT)]
  96. #define WB_PAYLEN 193
  97. // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob
  98. // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank
  99. // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without
  100. // that flag (or a future blob that outgrows the FIFO) fails loudly instead of shipping a dead panel.
  101. static_assert(CFG_TUD_VENDOR_TX_BUFSIZE >= 2 + WB_PAYLEN,
  102. "CFG_TUD_VENDOR_TX_BUFSIZE too small to hold a WebUSB blob in one "
  103. "write -- build via `make build` (sets 256), or raise the flag.");
  104. static void webusbSendBlob()
  105. {
  106. if (!usb_web.connected())
  107. return;
  108. // "connected" = the most recent poll cycle's slot had a fresh F-reply (matches what the firmware
  109. // presents to Steam on 0x79). The blob is sent on the panel's poll, so the slot + up flag change
  110. // every ~250ms in normal use.
  111. int cs = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
  112. bool up = (g_curSlot >= 0 && (millis() - g_connReplyMs[cs]) < 300);
  113. // STATIC, not stack: this runs on the usbd task, whose 800B stack (hardcoded USBD_STACK_SZ in the
  114. // Adafruit core, not growable from the sketch) is the prime overflow suspect behind the issue-72
  115. // watchdog hangs (hang boots report usbd stack free = 0). A 178B frame on that stack was the single
  116. // biggest consumer. Single-writer (SOF drain only), so a static buffer is race-free.
  117. static uint8_t p[2 + WB_PAYLEN];
  118. p[0] = 0xA5;
  119. p[1] = WB_PAYLEN;
  120. // protocol version (20 = +per-type trackpad->stick mapping (fields 80..87, blob p[187..194]);
  121. // 19 = +Switch Pro legacy-gyro select (field 38, blob p[186]); the rumble-strength,
  122. // Switch report-rate and Switch gyro-scale settings (fields 22/23/24, blob p[53..55]) are GONE -- those
  123. // bytes now read 0; 18 = +configurable back4+D-pad chords (fields 34..37, blob p[182..185]);
  124. // 17 = per-type rumble field (TypeCfg k=8), per-type stride 8->9; 16 = +configurable
  125. // lizard-map ops 0x11..0x15 / 0xAA frame, payload unchanged -- the panel MUST see >=16 before it dares
  126. // send 0x11, or a blocking readLizard() would hang forever against a firmware that silently drops the
  127. // unknown op; 15 = +staged firmware-update ops 0x20..0x24; 14 = +landAll87 toggle; 13 = +per-slot link
  128. // stats; 12 = +relay rate + clock fingerprint; 11 = +reset cause; 10 = +ledBright per type; 9 = +per-type
  129. // cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
  130. p[2] = 20;
  131. p[3] = g_usbMode;
  132. p[4] = (uint8_t)g_mDiv;
  133. p[5] = (uint8_t)g_mFric;
  134. p[6] = g_qamMap;
  135. p[7] = g_abSwap;
  136. p[8] = g_back[0];
  137. p[9] = g_back[1];
  138. p[10] = g_back[2];
  139. p[11] = g_back[3];
  140. p[12] = (g_curSlot >= 0) ? (uint8_t)g_curSlot : 0xFF;
  141. p[13] = up ? 1 : 0;
  142. p[14] = (uint8_t)g_f1ps;
  143. p[15] = (uint8_t)(g_f1ps >> 8);
  144. p[16] = (uint8_t)(g_pollUs / 100);
  145. p[17] = (uint8_t)g_newps;
  146. p[18] = (uint8_t)(g_newps >> 8);
  147. p[19] = g_e7b;
  148. p[20] = g_relayOp;
  149. p[21] = g_relaySub;
  150. p[22] = g_fwdNewOnly;
  151. p[23] = g_qos;
  152. p[24] = g_persistMode ? 1 : 0;
  153. p[25] = g_chordBtn[0];
  154. p[26] = g_chordBtn[1];
  155. p[27] = g_chordBtn[2];
  156. p[28] = (uint8_t)g_pollsps;
  157. // poll TX rate (vs delivered/new -> starvation vs reply-loss)
  158. p[29] = (uint8_t)(g_pollsps >> 8);
  159. p[30] = (uint8_t)g_loopPeriodUs;
  160. p[31] = (uint8_t)(g_loopPeriodUs >> 8); // avg loop iteration time
  161. p[32] = g_loopWorst;
  162. p[33] = (uint8_t)g_loopWorstUs;
  163. p[34] = (uint8_t)(g_loopWorstUs >> 8);
  164. p[35] = (uint8_t)g_pollPeriodUs;
  165. p[36] = (uint8_t)(g_pollPeriodUs >>
  166. 8); // measured poll period (intended 4000)
  167. p[37] = OPK_LOG; // logging build? panel shows/hides its log UI
  168. p[38] = g_battery
  169. [g_curSlot >= 0 && g_curSlot < NSLOT ?
  170. g_curSlot :
  171. 0]; // controller battery % (report 0x43); 0=unknown
  172. p[39] = g_linkRssi[g_curSlot >= 0 && g_curSlot < NSLOT ?
  173. g_curSlot :
  174. 0]; // RAW signal strength |dBm| (0=no sample)
  175. // Release builds are tagged and easier to trace by semver; local/dev builds
  176. // still expose git hash when no version tag is available.
  177. p[40] = OPK_GIT_DIRTY ? 1 : 0;
  178. memset(&p[41], 0, 12); // 12B ASCII build ID, NUL-padded
  179. {
  180. const char *buildId = OPK_BUILD_VERSION;
  181. if (buildId[0] == '\0')
  182. buildId = OPK_GIT_HASH;
  183. for (uint8_t i = 0; i < 12 && buildId[i]; i++)
  184. p[41 + i] = (uint8_t)buildId[i];
  185. }
  186. // v19: reserved. Were rumble strength % (v5) and the Switch Pro report rate + gyro scale (v6); all three
  187. // settings were removed, so these read 0 and the panel ignores them.
  188. p[53] = 0;
  189. p[54] = 0;
  190. p[55] = 0;
  191. {
  192. // Read the ACTIVE slot's accel, not a hardcoded g_in[0]: a controller bonded to a non-zero
  193. // slot leaves g_in[0] zeroed, which made this readout (and lizard) look dead.
  194. int16_t a[3] = { g_in[cs].ax, g_in[cs].ay, g_in[cs].az };
  195. memcpy(&p[56], a, 6);
  196. } // raw accelerometer for scale diagnostics (protocol v7)
  197. // per-slot link status for all bond slots (protocol v8)
  198. p[62] = (uint8_t)bondedSlotCount();
  199. {
  200. unsigned long nowMs = millis();
  201. for (int s = 0; s < NSLOT; s++) {
  202. bool sup = g_slot[s].used && g_connReplyMs[s] != 0 &&
  203. (nowMs - g_connReplyMs[s]) < 300u;
  204. p[63 + s * 3] = sup ? 1 : 0;
  205. p[64 + s * 3] = g_battery[s];
  206. p[65 + s * 3] = g_linkRssi[s];
  207. }
  208. }
  209. // per-emulated-type button config (v10/v17): 4 types x 9 bytes from p[75]
  210. for (int et = 0; et < ET_COUNT; et++) {
  211. uint8_t *q = &p[75 + et * 9];
  212. q[0] = g_type[et].back[0];
  213. q[1] = g_type[et].back[1];
  214. q[2] = g_type[et].back[2];
  215. q[3] = g_type[et].back[3];
  216. q[4] = g_type[et].qamMap;
  217. q[5] = g_type[et].abSwap;
  218. q[6] = g_type[et].padHaptics;
  219. q[7] = g_type[et].ledBright;
  220. q[8] = g_type[et].rumble;
  221. }
  222. // last-boot reset cause (protocol v11): why we (re)booted -- watchdog hang vs HardFault vs intentional
  223. // reboot vs power-on (issue #72). p[111] = RR_* code; p[112..115] = raw RESETREAS for the curious.
  224. p[111] = faultDiagReason();
  225. uint32_t rr = faultDiagResetReas();
  226. p[112] = (uint8_t)rr;
  227. p[113] = (uint8_t)(rr >> 8);
  228. p[114] = (uint8_t)(rr >> 16);
  229. p[115] = (uint8_t)(rr >> 24);
  230. // Connection tunables (protocol v11): poll RX window in 10us units, post-connect haptic block enable,
  231. // and block duration in seconds.
  232. p[116] = (uint8_t)(g_rxWin / 10);
  233. p[117] = g_hapticBlockOn;
  234. p[118] = (uint8_t)(g_hapticBlockMs / 1000);
  235. // RF wedge diagnostics (always populated, even when the link reads down): CRC-fail/s and no-reply/s,
  236. // capped at 255. With Polls/s this distinguishes poll-loop-stopped vs RX-dead vs corrupt-replies.
  237. p[119] = (uint8_t)(g_crcps > 255 ? 255 : g_crcps);
  238. p[120] = (uint8_t)(g_norxps > 255 ? 255 : g_norxps);
  239. p[121] = (uint8_t)(g_rfStallRecover > 255 ? 255 : g_rfStallRecover);
  240. // v12: relay-frame TX rate (separated from true poll cycles -- Polls/s now reads ~250, Relay/s exposes the
  241. // host output-report flood that steals reply windows), and the clock fingerprint for clone-board triage.
  242. p[122] = (uint8_t)g_relayps;
  243. p[123] = (uint8_t)(g_relayps >> 8);
  244. p[124] = clockLfSrc();
  245. p[125] = clockHfSrc();
  246. uint16_t upm = clockUsPerMs();
  247. p[126] = (uint8_t)upm;
  248. p[127] = (uint8_t)(upm >> 8);
  249. // last hang's loop stage (0xFF = last boot wasn't a watchdog/lockup hang, or GPREGRET2 didn't survive)
  250. p[128] = faultDiagHangStage();
  251. // LIVE hang localization: current loop stage + how long loop() has been stuck (40ms units, capped). When
  252. // loop() wedges these keep updating because the blob is sent from the SOF interrupt, not loop().
  253. p[129] = faultDiagCurStage();
  254. uint32_t sms = faultDiagStallMs();
  255. p[130] = (uint8_t)(sms / 40 > 255 ? 255 : sms / 40);
  256. // relay-ring fault count: non-zero = we caught+recovered a desynced/corrupt ring that would otherwise be an
  257. // invisible IRQ-off watchdog hang. A climbing count points at the haptic relay path / memory corruption.
  258. p[131] = (uint8_t)g_ringFault;
  259. p[132] = (uint8_t)(g_ringFault >> 8);
  260. // PC/LR of the stuck code captured by the WDT pre-reset ISR on the last watchdog hang (0 = none captured =
  261. // the hang hard-masked interrupts). Map with addr2line on the .elf to name the function.
  262. uint32_t hpc = faultDiagHangPC(), hlr = faultDiagHangLR();
  263. p[133] = (uint8_t)hpc;
  264. p[134] = (uint8_t)(hpc >> 8);
  265. p[135] = (uint8_t)(hpc >> 16);
  266. p[136] = (uint8_t)(hpc >> 24);
  267. p[137] = (uint8_t)hlr;
  268. p[138] = (uint8_t)(hlr >> 8);
  269. p[139] = (uint8_t)(hlr >> 16);
  270. p[140] = (uint8_t)(hlr >> 24);
  271. // least-ever free stack (words) on the usbd task (800B total) and loop task -- usbd trending to 0 confirms
  272. // the overflow. words, not bytes.
  273. uint16_t usbdFree = faultDiagUsbdStackFree(),
  274. loopFree = faultDiagLoopStackFree();
  275. p[141] = (uint8_t)usbdFree;
  276. p[142] = (uint8_t)(usbdFree >> 8);
  277. p[143] = (uint8_t)loopFree;
  278. p[144] = (uint8_t)(loopFree >> 8);
  279. // v13: per-slot link stats -- each controller's own poll/reply/error rates, so the panel's per-slot tabs
  280. // no longer conflate every controller into the aggregate numbers above.
  281. for (int s = 0; s < NSLOT; s++) {
  282. uint8_t *q = &p[145 + s * 9];
  283. q[0] = (uint8_t)g_slotPollsps[s];
  284. q[1] = (uint8_t)(g_slotPollsps[s] >> 8);
  285. q[2] = (uint8_t)g_slotF1ps[s];
  286. q[3] = (uint8_t)(g_slotF1ps[s] >> 8);
  287. q[4] = (uint8_t)g_slotNewps[s];
  288. q[5] = (uint8_t)(g_slotNewps[s] >> 8);
  289. q[6] = g_slotCrcps[s];
  290. q[7] = g_slotNoRxps[s];
  291. q[8] = g_slotRelayps[s];
  292. }
  293. // v14/v17: verbatim-0x87-relay experiment toggle (panel reflects + toggles it)
  294. p[181] = 0; // used to be g_landAll87
  295. // v18: back4+D-pad mode assignments (panel renders these as selects next to the B/X/Y ones)
  296. p[182] = g_chordDpad[CHD_LEFT];
  297. p[183] = g_chordDpad[CHD_UP];
  298. p[184] = g_chordDpad[CHD_RIGHT];
  299. p[185] = g_chordDpad[CHD_DOWN];
  300. // v19: Switch Pro gyro mapping (0 = corrected/default, 1 = legacy pre-#189 raw axes)
  301. p[186] = g_swGyroLegacy;
  302. // v20: per-type trackpad->stick mapping, {left pad, right pad} per emulated type
  303. for (int et = 0; et < ET_COUNT; et++) {
  304. p[187 + et * 2] = g_padStickCfg[et][0];
  305. p[188 + et * 2] = g_padStickCfg[et][1];
  306. }
  307. // CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the
  308. // panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a
  309. // backgrounded tab, or the host briefly not servicing transferIn under load -- the FIFO never empties and
  310. // that spin hangs loop() until the ~8s watchdog resets us (RESETREAS=watchdog/hang). This is the ONE
  311. // cross-task tud_ call the marshalling refactor left able to block loop() (HID goes through the drop-oldest
  312. // ring; this didn't). So DROP the blob whenever the FIFO can't take it whole -- a status panel missing an
  313. // occasional frame is invisible, and loop() can never stall here again. (Matches the closing-the-panel
  314. // "fix": that just flips _connected=false to break the same spin.)
  315. if (tud_vendor_write_available() >= sizeof p) {
  316. usb_web.write(p, sizeof p);
  317. usb_web.flush();
  318. }
  319. }
  320. // Bond export frame (panel "Export config to file"). One self-describing frame carrying all four bond slots so
  321. // the browser can clone a puck onto another without re-pairing:
  322. // [0xA7][len][ver=1][usedMask][slot0 rec 24]...[slot3 rec 24] (len = 2 + NSLOT*24 = 98)
  323. // usedMask bit s = slot s bonded. The session RF address is NOT exported: each puck re-derives it from the bond
  324. // UUID + its own FICR DEVICEID at boot, and the bonded controller relearns it from the E1 host beacon on every
  325. // reconnect (rf_link.cpp) -- so the 24-byte record is the whole portable identity. 98+2 < transferIn(128) and
  326. // < CFG_TUD_VENDOR_TX_BUFSIZE (256), so it sends whole in one drop-on-full write like the status blob.
  327. #define WB_BOND_PAYLEN (2 + NSLOT * 24)
  328. static void webusbSendBondExport()
  329. {
  330. if (!usb_web.connected())
  331. return;
  332. // static for the same reason as the status blob: keep the 100B frame off the 800B usbd-task stack.
  333. static uint8_t p[2 + WB_BOND_PAYLEN];
  334. p[0] = 0xA7;
  335. p[1] = WB_BOND_PAYLEN;
  336. p[2] = 1; // format version
  337. uint8_t mask = 0;
  338. for (int s = 0; s < NSLOT; s++) {
  339. if (g_slot[s].used)
  340. mask |= (uint8_t)(1u << s);
  341. memcpy(&p[4 + s * 24], g_slot[s].rec, 24);
  342. }
  343. p[3] = mask;
  344. // Same anti-hang rule as the status blob: drop the frame if the FIFO can't take it whole, never spin.
  345. if (tud_vendor_write_available() >= sizeof p) {
  346. usb_web.write(p, sizeof p);
  347. usb_web.flush();
  348. }
  349. }
  350. // Stream the saved (pre-reset) flight-recorder trail to the panel as 0xA8 frames. Always compiled -- the
  351. // recorder is not OPK_LOG-gated (field hangs happen on release builds too). Frame layout mirrors the 0xA6
  352. // capture drain so the same drop-on-full discipline applies (a write only issues when the FIFO can take the
  353. // whole frame -> usb_web.write() never spins -> loop() never stalls here):
  354. // header: [0xA8][29][T=2][count u16][total u16][loopPerSec u16][stallMs][curStage][usbdStk u16][loopStk u16]
  355. // [heapUsed u32][pollsps u16][relayps u16][crc u8][norx u8][heal u16][ringF u16]
  356. // entry: [0xA8][9][T=1][dtMs u32][evt][stage][arg u16] (dtMs = ms before the last recorded crumb)
  357. // end: [0xA8][1][T=0]
  358. // The panel sends 0x10 with buf[1]=1 to (re)start (emits the header + rewinds the cursor), then 0x10 buf[1]=0
  359. // repeatedly; each call streams a budgeted chunk and the final call appends the end frame.
  360. #define WB_FLIGHT_HDR 29
  361. // RAII boost: these drains run from loop() and push many usb_web.write()s per call -- each enters TinyUSB's
  362. // dcd DMA claim window (the issue-72 livelock; see usb_tx.cpp), so the whole burst runs boosted.
  363. struct TxBoostScope {
  364. TxBoostScope()
  365. {
  366. usbTxBoost();
  367. }
  368. ~TxBoostScope()
  369. {
  370. usbTxUnboost();
  371. }
  372. };
  373. static void webusbDrainFlight(bool restart)
  374. {
  375. if (!usb_web.connected())
  376. return;
  377. TxBoostScope boost;
  378. if (restart) {
  379. faultDiagFlightDrainReset();
  380. if (tud_vendor_write_available() >= 2 + WB_FLIGHT_HDR) {
  381. struct FaultFlightVitals v;
  382. bool have = faultDiagFlightVitals(&v);
  383. if (!have)
  384. memset(&v, 0, sizeof v);
  385. uint16_t cnt = faultDiagFlightCount();
  386. uint16_t tot = faultDiagFlightTotal();
  387. uint8_t f[2 + WB_FLIGHT_HDR];
  388. f[0] = 0xA8;
  389. f[1] = WB_FLIGHT_HDR;
  390. f[2] = 2; // header type
  391. f[3] = (uint8_t)cnt;
  392. f[4] = (uint8_t)(cnt >> 8);
  393. f[5] = (uint8_t)tot;
  394. f[6] = (uint8_t)(tot >> 8);
  395. f[7] = (uint8_t)v.loopPerSec;
  396. f[8] = (uint8_t)(v.loopPerSec >> 8);
  397. f[9] = v.stallMs;
  398. f[10] = v.curStage;
  399. f[11] = (uint8_t)v.usbdStkFree;
  400. f[12] = (uint8_t)(v.usbdStkFree >> 8);
  401. f[13] = (uint8_t)v.loopStkFree;
  402. f[14] = (uint8_t)(v.loopStkFree >> 8);
  403. f[15] = (uint8_t)v.heapUsed;
  404. f[16] = (uint8_t)(v.heapUsed >> 8);
  405. f[17] = (uint8_t)(v.heapUsed >> 16);
  406. f[18] = (uint8_t)(v.heapUsed >> 24);
  407. f[19] = (uint8_t)v.pollsps;
  408. f[20] = (uint8_t)(v.pollsps >> 8);
  409. f[21] = (uint8_t)v.relayps;
  410. f[22] = (uint8_t)(v.relayps >> 8);
  411. f[23] = (uint8_t)(v.crcps > 255 ? 255 : v.crcps);
  412. f[24] = (uint8_t)(v.norxps > 255 ? 255 : v.norxps);
  413. f[25] = (uint8_t)v.rfHeal;
  414. f[26] = (uint8_t)(v.rfHeal >> 8);
  415. f[27] = (uint8_t)v.ringFault;
  416. f[28] = (uint8_t)(v.ringFault >> 8);
  417. f[29] = (uint8_t)0; // reserved / pad to header len
  418. f[30] = (uint8_t)0;
  419. usb_web.write(f, sizeof f);
  420. usb_web.flush();
  421. }
  422. }
  423. uint32_t dt = 0;
  424. uint8_t evt = 0, stage = 0;
  425. uint16_t arg = 0;
  426. uint16_t budget = 64;
  427. bool done = false;
  428. while (budget--) {
  429. if (tud_vendor_write_available() < 2 + 9)
  430. break; // FIFO full -> resume on the panel's next 0x10 poll
  431. if (!faultDiagFlightPull(&dt, &evt, &stage, &arg)) {
  432. done = true;
  433. break;
  434. }
  435. uint8_t f[2 + 9];
  436. f[0] = 0xA8;
  437. f[1] = 9;
  438. f[2] = 1; // entry type
  439. f[3] = (uint8_t)dt;
  440. f[4] = (uint8_t)(dt >> 8);
  441. f[5] = (uint8_t)(dt >> 16);
  442. f[6] = (uint8_t)(dt >> 24);
  443. f[7] = evt;
  444. f[8] = stage;
  445. f[9] = (uint8_t)arg;
  446. f[10] = (uint8_t)(arg >> 8);
  447. usb_web.write(f, sizeof f);
  448. usb_web.flush();
  449. }
  450. if (done && tud_vendor_write_available() >= 3) {
  451. uint8_t e[3] = { 0xA8, 1, 0 };
  452. usb_web.write(e, 3);
  453. usb_web.flush();
  454. }
  455. }
  456. // Runs on the usbd task (registered via usbTxRegisterDrain -> tud_sof_cb). Sends the blob if loop() asked for
  457. // one. Keeps every usb_web write/flush off the loop task so it can't block on the device event queue.
  458. static void webusbSofDrain(void)
  459. {
  460. // If loop() has stopped beating, it's wedged -- keep pushing the blob (which carries the live stuck stage)
  461. // so the panel shows WHERE it hung during the ~8s before the watchdog fires. This runs on the SOF IRQ, so
  462. // it works even though loop() is dead. drop-on-full in webusbSendBlob keeps it from ever blocking.
  463. if (faultDiagStallMs() > 300)
  464. g_blobRequest = true;
  465. // firmware-update ack first: the panel is blocked on it (ping-pong), and it must never be dropped --
  466. // retry every SOF until the FIFO has room.
  467. if (g_fwupAckPend && tud_vendor_write_available() >= 7) {
  468. uint8_t f[7] = { 0xAB,
  469. 5,
  470. g_fwupAckStatus,
  471. (uint8_t)(g_fwupAckOff),
  472. (uint8_t)(g_fwupAckOff >> 8),
  473. (uint8_t)(g_fwupAckOff >> 16),
  474. (uint8_t)(g_fwupAckOff >> 24) };
  475. usb_web.write(f, sizeof f);
  476. usb_web.flush();
  477. g_fwupAckPend = false;
  478. }
  479. if (g_blobRequest) {
  480. g_blobRequest = false;
  481. webusbSendBlob();
  482. }
  483. if (g_bondExportRequest) {
  484. g_bondExportRequest = false;
  485. webusbSendBondExport();
  486. }
  487. }
  488. // ---- live wedge reporter (0xA9) --------------------------------------------------------------------------
  489. // THE one channel that survives a loop() wedge on boards that wipe .noinit/GPREGRET across the watchdog reset
  490. // (Teyleten): tud_sof_cb runs on the usbd task, which is HIGHER priority than the loop() task, so the USB
  491. // stack keeps dispatching it even while loop() is wedged (spinning or blocked) -- as long as interrupts aren't
  492. // hard-masked (PRIMASK). When loop() has been stuck a while, emit a tiny frame carrying the stuck loop stage so
  493. // the panel can log WHERE it hung, live, during the ~8s before the watchdog fires. Zero traffic when healthy
  494. // (returns immediately unless stalled); rate-limited; drop-on-full so it never spins the usbd task.
  495. // frame: [0xA9][3][stuck stage][stallMs lo][stallMs hi]
  496. extern "C" void tud_sof_cb(uint32_t frame_count)
  497. {
  498. (void)frame_count;
  499. uint32_t stall = faultDiagStallMs();
  500. if (stall <
  501. 1000) // loop() healthy -> nothing to report (the common case: near-zero overhead)
  502. return;
  503. static uint32_t lastMs = 0;
  504. uint32_t now = millis();
  505. if ((uint32_t)(now - lastMs) < 200) // ~5 Hz cap
  506. return;
  507. lastMs = now;
  508. if (!usb_web.connected())
  509. return;
  510. uint8_t f[2 + 3];
  511. f[0] = 0xA9;
  512. f[1] = 3;
  513. f[2] = faultDiagCurStage();
  514. uint16_t s = (stall > 0xFFFFu) ? 0xFFFFu : (uint16_t)stall;
  515. f[3] = (uint8_t)s;
  516. f[4] = (uint8_t)(s >> 8);
  517. if (tud_vendor_write_available() >= sizeof f) {
  518. usb_web.write(f, sizeof f);
  519. usb_web.flush();
  520. }
  521. }
  522. // Register the SOF drain. Call once from setup() (harmless even if WebUSB isn't enumerated -- webusbSendBlob
  523. // no-ops while disconnected, and g_blobRequest is only ever set by panel traffic, which needs a connection).
  524. void webusbInit(void)
  525. {
  526. usbTxRegisterDrain(webusbSofDrain);
  527. // Enable SOF-callback dispatch so tud_sof_cb() above fires -- our only live view into a loop() wedge on
  528. // boards whose bootloader wipes retained RAM. Cheap: the callback no-ops unless loop() is already stalled.
  529. tud_sof_cb_enable(true);
  530. }
  531. // Send the whole lizard binding table as one 0xAA frame so the panel can edit it:
  532. // [0xAA][count][ per binding (16B): outType, outData[0..6], trigMask LE(4), holdMask LE(4) ]
  533. // Total = 2 + count*16 (max 2 + 32*16 = 514). The panel reads byte[1]=count and accumulates
  534. // transferIn packets until it has the full frame.
  535. static void webusbSendLizard()
  536. {
  537. if (!usb_web.connected())
  538. return;
  539. uint8_t count = g_lizardMap.count;
  540. if (count > LZ_MAX_BINDINGS)
  541. count = LZ_MAX_BINDINGS;
  542. // static (not stack): 514 B is large for the USB task stack, and webusbPoll is single-threaded.
  543. static uint8_t f[2 + LZ_MAX_BINDINGS * 16];
  544. // 0xAA, NOT 0xA7/0xA8: main claimed 0xA7 (bond export) and 0xA8 (flight recorder), plus 0xA9 (wedge
  545. // reporter) and 0xAB (fwup ack) -- 0xAA is the free device->host marker for the lizard map.
  546. f[0] = 0xAA;
  547. f[1] = count;
  548. for (uint8_t i = 0; i < count; i++) {
  549. const LizardBinding &b = g_lizardMap.bindings[i];
  550. uint8_t *q = &f[2 + i * 16];
  551. q[0] = b.outType;
  552. for (int k = 0; k < 7; k++)
  553. q[1 + k] = b.outData[k];
  554. q[8] = (uint8_t)b.trigMask;
  555. q[9] = (uint8_t)(b.trigMask >> 8);
  556. q[10] = (uint8_t)(b.trigMask >> 16);
  557. q[11] = (uint8_t)(b.trigMask >> 24);
  558. q[12] = (uint8_t)b.holdMask;
  559. q[13] = (uint8_t)(b.holdMask >> 8);
  560. q[14] = (uint8_t)(b.holdMask >> 16);
  561. q[15] = (uint8_t)(b.holdMask >> 24);
  562. }
  563. usb_web.write(f, (uint16_t)(2 + count * 16));
  564. usb_web.flush();
  565. }
  566. #if OPK_LOG
  567. // Stream the capture ring (haptics / relayed host commands) to the panel as 0xA6 frames. Frame formats:
  568. // entry: [0xA6][L][T=1][age u32 LE][slot][rid][n][bytes n] (L = 8 + n)
  569. // end: [0xA6][1][T=0]
  570. static void webusbCapFrame(uint32_t ms, uint8_t slot, uint8_t rid, uint8_t nb,
  571. const uint8_t *b)
  572. {
  573. if (nb > 16)
  574. nb = 16;
  575. uint8_t f[2 + 9 + 16];
  576. uint8_t L = (uint8_t)(8 + nb);
  577. f[0] = 0xA6;
  578. f[1] = L;
  579. f[2] = 1;
  580. f[3] = (uint8_t)ms;
  581. f[4] = (uint8_t)(ms >> 8);
  582. f[5] = (uint8_t)(ms >> 16);
  583. f[6] = (uint8_t)(ms >> 24); // absolute log time
  584. f[7] = slot;
  585. f[8] = rid;
  586. f[9] = nb;
  587. memcpy(f + 10, b, nb);
  588. usb_web.write(f, (uint16_t)(2 + L));
  589. usb_web.flush();
  590. }
  591. static void webusbCapEnd()
  592. {
  593. uint8_t e[3] = { 0xA6, 1, 0 };
  594. usb_web.write(e, 3);
  595. usb_web.flush();
  596. }
  597. // 0x06: drain entries since the last drain (or since the 0x05 start/rewind). The panel polls this on a
  598. // timer and accumulates, so a dump-from-boot of the whole ring streams over many polls without blocking the
  599. // loop. A per-call budget bounds how long one poll spends here.
  600. static void webusbDrainCapture()
  601. {
  602. if (!usb_web.connected())
  603. return;
  604. TxBoostScope
  605. boost; // capture streams at flood rate through the same dcd DMA window
  606. uint32_t ms = 0;
  607. uint8_t slot = 0, rid = 0, nb = 0, b[16];
  608. uint16_t budget = 128;
  609. // Same anti-hang rule as the blob: webusbCapFrame's usb_web.write() spins until the FIFO drains, so stop
  610. // pulling the moment the FIFO can't hold a max-size frame (2+9+16=27 B) -- the panel resumes the drain on
  611. // its next 0x06 poll. Never let a backed-up panel hang loop() here.
  612. while (budget-- && tud_vendor_write_available() >= 27 &&
  613. hapLogPull(&ms, &slot, &rid, &nb, b))
  614. webusbCapFrame(ms, slot, rid, nb, b);
  615. if (tud_vendor_write_available() >= 3)
  616. webusbCapEnd();
  617. }
  618. #endif // OPK_LOG
  619. void webusbPoll()
  620. {
  621. // 160 B holds the largest command: a firmware-update data chunk 0x21 = [op][off u32][len][<=128 data]
  622. // = 134 B (spans USB-FS packets; the byte-wise accumulation below reassembles it). Next-largest are the
  623. // 0x0D bond-slot write (27 B) and the 0x12 lizard set-binding (18 B); everything else is <= 9 B.
  624. static uint8_t buf[160];
  625. static uint8_t n = 0;
  626. while (usb_web.available()) {
  627. int c = usb_web.read();
  628. if (c < 0)
  629. break;
  630. if (n < sizeof buf)
  631. buf[n++] = (uint8_t)c;
  632. // process complete commands from the front of buf
  633. for (;;) {
  634. if (n == 0)
  635. break;
  636. uint8_t op = buf[0];
  637. if ((op < 0x01 || op > 0x15) &&
  638. (op < 0x20 || op > 0x25)) { // resync: drop one byte
  639. memmove(buf, buf + 1, --n);
  640. continue;
  641. }
  642. // 0x21 (fwup data) carries its own length at [5]; sanity-gate it BEFORE trusting it as a
  643. // command length so a corrupt byte can only cost a one-byte resync, never a stuck parser.
  644. if (op == 0x21 && n >= 6) {
  645. uint8_t dl = buf[5];
  646. if (dl == 0 || dl > 128 || (dl & 3)) {
  647. memmove(buf, buf + 1, --n);
  648. continue;
  649. }
  650. }
  651. // Command length (fixed per opcode). 0x0D = write-one-bond-slot (27 B); 0x12 = set-one-lizard-
  652. // binding (18 B); 0x05/0x0E/0x0F/0x10/0x13 carry one value byte; 0x02 a field+value; 0x0A a
  653. // 3-byte magic. Firmware update: 0x20 begin [size u32][crc32 u32], 0x21 data (6 B header +
  654. // payload), 0x22/0x23/0x24 bare. (0x11/0x14/0x15 are bare lizard opcodes -> default 1.)
  655. uint8_t need =
  656. (op == 0x0D) ? 27 :
  657. (op == 0x12) ? 18 :
  658. (op == 0x02) ? 3 :
  659. (op == 0x03 || op == 0x05 || op == 0x0E ||
  660. op == 0x0F || op == 0x10 || op == 0x13) ?
  661. 2 :
  662. (op == 0x0A) ? 4 :
  663. (op == 0x25) ? 5 :
  664. (op == 0x20) ? 9 :
  665. (op == 0x21) ? (uint8_t)(6 + (n >= 6 ? buf[5] :
  666. 0)) :
  667. 1;
  668. if (n < need)
  669. break; // wait for more bytes
  670. if (boardCommand(op)) {
  671. memmove(buf, buf + need, n - need);
  672. n -= need;
  673. continue;
  674. }
  675. if (op == 0x01) {
  676. g_blobRequest =
  677. true; // sent from the usbd task (webusbSofDrain)
  678. }
  679. // 0x09: export all bond slots (clone backup). One 0xA7 frame, sent from the usbd task.
  680. else if (op == 0x09) {
  681. g_bondExportRequest = true;
  682. }
  683. // 0x0F: stability test on/off. Puck->controller haptics do NOT reset the controller's own
  684. // user-input idle auto-off (we already poll it every 4ms without keeping it awake), so instead
  685. // signal host-awake: enable the E7 announce (0xE7 00 00 = host-awake vs 00 01 = suspended, per
  686. // the RE) so the controller is told the host is active and (hopefully) doesn't power-save. The
  687. // 10s buzz stays -- it also exercises the haptic-relay path that correlates with the hang.
  688. else if (op == 0x0F) {
  689. g_stabTest = (buf[1] != 0);
  690. g_e7b = 0; // byte2=0 => host-awake
  691. g_e7announce = g_stabTest;
  692. }
  693. // 0x10: stream the saved flight-recorder trail (0xA8 frames). buf[1]=1 restarts (header + rewind),
  694. // buf[1]=0 continues the drain. Drained straight from loop context like the 0x06 capture drain --
  695. // webusbDrainFlight is drop-on-full so it never blocks. Always available (recorder isn't OPK_LOG).
  696. else if (op == 0x10) {
  697. webusbDrainFlight(buf[1] != 0);
  698. }
  699. #if OPK_LOG
  700. // rewind drain: buf[1]=1 from boot (whole ring), 0 from now
  701. else if (op == 0x05) {
  702. hapLogResetDrain(buf[1] != 0);
  703. } else if (op == 0x06) {
  704. webusbDrainCapture();
  705. } // drain entries since the rewind (panel polls this)
  706. #endif
  707. // clear a stuck/latched haptic buzz on the controller
  708. else if (op == 0x07) {
  709. hapticReinit();
  710. }
  711. // trigger controller power-off (same path Steam 0x9F / host-suspend use)
  712. else if (op == 0x08) {
  713. hapticSendShutdown();
  714. }
  715. // factory wipe: erase cfg.bin + bonds.bin, reboot to clean defaults.
  716. // Guarded by a 3-byte magic ("ERS") so a stray/corrupt byte can never trigger it.
  717. // Irreversible -- the controller must be re-paired afterwards.
  718. else if (op == 0x0A) {
  719. if (buf[1] == 0x45 && buf[2] == 0x52 &&
  720. buf[3] == 0x53) {
  721. usb_web.flush();
  722. factoryErase();
  723. delay(40);
  724. faultDiagArmIntentionalReset();
  725. NVIC_SystemReset();
  726. }
  727. // reboot into serial DFU (adafruit-nrfutil)
  728. } else if (op == 0x0B) {
  729. usb_web.flush();
  730. delay(40);
  731. faultDiagArmIntentionalReset();
  732. enterSerialDfu();
  733. // reboot into UF2 bootloader (USB mass storage)
  734. } else if (op == 0x0C) {
  735. usb_web.flush();
  736. delay(40);
  737. faultDiagArmIntentionalReset();
  738. enterUf2Dfu();
  739. // 0x25: FULL BOARD WIPE (debug panel only). Erase the app + config/bond + bootloader-settings
  740. // flash and reboot app-less, so the board mounts as the UF2 drive on EVERY boot until it is
  741. // flashed again -- no trace of the firmware left. Distinct from 0x0A (factory reset, which
  742. // reformats settings but keeps the firmware). Guarded by the 4-byte magic "WIPE" so no stray
  743. // byte can ever trigger it. Irreversible without re-flashing.
  744. } else if (op == 0x25) {
  745. if (buf[1] == 0x57 && buf[2] == 0x49 &&
  746. buf[3] == 0x50 && buf[4] == 0x45) {
  747. usb_web.flush();
  748. fwupArmFullWipe();
  749. delay(40);
  750. faultDiagArmIntentionalReset();
  751. NVIC_SystemReset();
  752. }
  753. // 0x0D: write ONE bond slot into RAM -- the "clone onto this puck" side of Export/Import.
  754. // [0x0D][slot][used][24 rec]. used=0 (or an empty record) clears the slot. The panel sends
  755. // one of these per slot, then a 0x0E to persist + reboot. Kept to a single small command
  756. // (<=27 B) so it never spans a USB packet, and acked with a status blob so the panel's
  757. // read-after-write keeps the OUT pipe flowing (same shape as the proven 0x02 setters).
  758. } else if (op == 0x0D) {
  759. uint8_t slot = buf[1], used = buf[2];
  760. if (slot < NSLOT) {
  761. if (used && !recEmpty(buf + 3)) {
  762. memcpy(g_slot[slot].rec,
  763. buf + 3, 24);
  764. g_slot[slot].used = true;
  765. } else {
  766. memset(g_slot[slot].rec, 0, 24);
  767. g_slot[slot].used = false;
  768. }
  769. }
  770. g_blobRequest = true; // ack
  771. // 0x0E: commit an import -- persist the bond slots written by 0x0D, optionally apply a USB
  772. // mode ([0x0E][mode]; 0xFF = leave unchanged), then reboot so the RF session addresses
  773. // regenerate from the new bond UUIDs.
  774. } else if (op == 0x0E) {
  775. uint8_t mode = buf[1];
  776. saveBonds();
  777. usb_web.flush();
  778. // clean detach + reboot into `mode` (0xFF = keep current); regenerates RF
  779. // session addrs from the imported bonds and releases any held input.
  780. modeSwitchReboot(mode);
  781. // ---- lizard (desktop) binding map editor ----
  782. // Renumbered to 0x11..0x15 on the merge with main: main already owns 0x0D (bond-slot
  783. // write), 0x0E (import commit), 0x0F (stability test) and 0x10 (flight-recorder drain),
  784. // which the pre-merge branch had collided with. The panel JS uses the same 0x11..0x15.
  785. // 0x11: dump the current map as a 0xAA frame.
  786. } else if (op == 0x11) {
  787. webusbSendLizard();
  788. // 0x13 [count]: BEGIN a map edit -- set the binding count (clamped). The
  789. // panel then sends one 0x12 per binding (0..count-1) and a 0x14 to persist.
  790. } else if (op == 0x13) {
  791. g_lizardMap.count =
  792. (buf[1] <= LZ_MAX_BINDINGS) ?
  793. buf[1] :
  794. LZ_MAX_BINDINGS;
  795. // 0x12 [idx][outType][od0..6][trig LE4][hold LE4]: set one binding.
  796. } else if (op == 0x12) {
  797. uint8_t idx = buf[1];
  798. if (idx < LZ_MAX_BINDINGS) {
  799. LizardBinding &b =
  800. g_lizardMap.bindings[idx];
  801. b.outType = buf[2];
  802. for (int k = 0; k < 7; k++)
  803. b.outData[k] = buf[3 + k];
  804. b.trigMask = (uint32_t)buf[10] |
  805. ((uint32_t)buf[11] << 8) |
  806. ((uint32_t)buf[12] << 16) |
  807. ((uint32_t)buf[13] << 24);
  808. b.holdMask = (uint32_t)buf[14] |
  809. ((uint32_t)buf[15] << 8) |
  810. ((uint32_t)buf[16] << 16) |
  811. ((uint32_t)buf[17] << 24);
  812. }
  813. // 0x14: COMMIT the edited map to flash and echo it back.
  814. } else if (op == 0x14) {
  815. saveLizardMap();
  816. webusbSendLizard();
  817. // 0x15: reset the map to the built-in defaults, persist, echo back.
  818. } else if (op == 0x15) {
  819. defaultLizardMap();
  820. saveLizardMap();
  821. webusbSendLizard();
  822. // 0x20..0x24: staged firmware update (see fw_update.h). Each op is acked with an 0xAB
  823. // frame from the usbd task; the panel ping-pongs on those acks, so at most one command
  824. // is ever in flight (also what bounds the flash-stall per loop() pass to one page erase).
  825. } else if (op == 0x20) {
  826. uint32_t sz = (uint32_t)buf[1] |
  827. ((uint32_t)buf[2] << 8) |
  828. ((uint32_t)buf[3] << 16) |
  829. ((uint32_t)buf[4] << 24);
  830. uint32_t crc = (uint32_t)buf[5] |
  831. ((uint32_t)buf[6] << 8) |
  832. ((uint32_t)buf[7] << 16) |
  833. ((uint32_t)buf[8] << 24);
  834. fwupAckPost(fwupBegin(sz, crc));
  835. } else if (op == 0x21) {
  836. uint32_t off = (uint32_t)buf[1] |
  837. ((uint32_t)buf[2] << 8) |
  838. ((uint32_t)buf[3] << 16) |
  839. ((uint32_t)buf[4] << 24);
  840. fwupAckPost(fwupChunk(off, buf + 6, buf[5]));
  841. } else if (op == 0x22) {
  842. fwupAckPost(fwupEnd());
  843. // 0x23: plain clean reboot. After a committed 0x22 the boot path applies the staged
  844. // image; unarmed it is just a normal restart.
  845. } else if (op == 0x23) {
  846. usb_web.flush();
  847. delay(40);
  848. faultDiagArmIntentionalReset();
  849. NVIC_SystemReset();
  850. } else if (op == 0x24) {
  851. fwupAbort();
  852. fwupAckPost(FWUP_OK);
  853. } else if (op == 0x02) {
  854. uint8_t f = buf[1], v = buf[2];
  855. // every settable field persists (poll rate is no longer settable)
  856. bool persist = true;
  857. // per-type cfg writes (protocol v10/v17): field = 40 + et*9 + k, k: 0..3 back, 4 qam, 5 abSwap,
  858. // 6 padHaptics, 7 ledBright, 8 rumble. Edits g_type[et]; refresh the live mirrors if it's the active type.
  859. if (f >= 40 && f < 40 + ET_COUNT * 9) {
  860. uint8_t et = (uint8_t)((f - 40) / 9),
  861. k = (uint8_t)((f - 40) % 9);
  862. if (et < ET_COUNT) {
  863. if (k < 4)
  864. g_type[et].back[k] = v;
  865. else if (k == 4)
  866. g_type[et].qamMap = v;
  867. else if (k == 5)
  868. g_type[et].abSwap =
  869. v ? 1 : 0;
  870. else if (k == 6)
  871. g_type[et].padHaptics =
  872. v ? 1 : 0;
  873. else if (k == 7)
  874. g_type[et].ledBright =
  875. v > 100 ? 100 :
  876. v;
  877. else if (k == 8)
  878. g_type[et].rumble =
  879. v ? 1 : 0;
  880. if (et == g_etype)
  881. applyActiveType();
  882. }
  883. saveCfg();
  884. g_blobRequest = true;
  885. memmove(buf, buf + need, n - need);
  886. n -= need;
  887. continue;
  888. }
  889. switch (f) {
  890. case 1:
  891. g_mDiv = v < 4 ? 4 : v;
  892. break;
  893. case 2:
  894. g_mFric = v > 99 ? 99 : v;
  895. break;
  896. // case 3 (padSmooth) removed -- Steam-mode pad coords are forwarded raw; Steam does its own smoothing.
  897. // Legacy single-value fields (4 abSwap, 5-8 back, 21 qam) edit the ACTIVE emulated type.
  898. case 4:
  899. if (g_etype < ET_COUNT) {
  900. g_type[g_etype].abSwap = v ? 1 :
  901. 0;
  902. applyActiveType();
  903. }
  904. break;
  905. case 5:
  906. case 6:
  907. case 7:
  908. case 8:
  909. if (g_etype < ET_COUNT) {
  910. g_type[g_etype].back[f - 5] = v;
  911. applyActiveType();
  912. }
  913. break;
  914. // case 9 (pollU100) removed -- poll rate is fixed at POLL_US_DEFAULT and no longer configurable.
  915. // E7 protocol-version B-byte (experimental v1 fast)
  916. case 10:
  917. g_e7b = v ? 1 : 0;
  918. break;
  919. // haptic-relay opcode
  920. case 11:
  921. g_relayOp = v;
  922. break;
  923. // haptic-relay sub-type
  924. case 12:
  925. g_relaySub = v;
  926. break;
  927. case 13:
  928. g_testHaptic = v ? v : 40;
  929. break; // inject v test haptics (0->40)
  930. // Steam: forward only fresh reports (dedupe)
  931. case 14:
  932. g_fwdNewOnly = v ? 1 : 0;
  933. break;
  934. case 15:
  935. g_qos = v ? 1 : 0;
  936. g_hopIdx = 0;
  937. g_qosBad = 0;
  938. g_qosCheckMs = millis();
  939. break; // QoS adaptive channel hopping
  940. // persist last mode across reboots (else always boot Steam)
  941. case 16:
  942. g_persistMode = v ? true : false;
  943. break;
  944. // back4+B/X/Y mode assignments
  945. case 17:
  946. case 18:
  947. case 19:
  948. if (modeValid(v))
  949. g_chordBtn[f - 17] = v;
  950. break;
  951. // back4+D-pad mode assignments (left/up/right/down), protocol v18
  952. case 34:
  953. case 35:
  954. case 36:
  955. case 37:
  956. if (modeValid(v))
  957. g_chordDpad[f - 34] = v;
  958. break;
  959. // reboot once WITH the CDC serial console (puck mode), then auto-revert
  960. case 20:
  961. armDebugCdcNextBoot();
  962. usb_web.flush();
  963. delay(40);
  964. faultDiagArmIntentionalReset();
  965. NVIC_SystemReset();
  966. break;
  967. // QAM physical button remap code (0=default/unmapped) -- active emulated type
  968. case 21:
  969. if (g_etype < ET_COUNT) {
  970. g_type[g_etype].qamMap = v;
  971. applyActiveType();
  972. }
  973. break;
  974. // 80..87: per-type trackpad->stick mapping, field = 80 + et*2 + pad
  975. // (pad 0 = left, 1 = right; value PS_OFF/PS_LEFT/PS_RIGHT).
  976. // Must stay clear of the per-type cfg range 40..40+ET_COUNT*9-1
  977. // (40..75), which is claimed before this switch runs.
  978. case 80:
  979. case 81:
  980. case 82:
  981. case 83:
  982. case 84:
  983. case 85:
  984. case 86:
  985. case 87: {
  986. uint8_t et = (uint8_t)((f - 80) / 2),
  987. pad = (uint8_t)((f - 80) % 2);
  988. if (et < ET_COUNT && v <= PS_MAX) {
  989. g_padStickCfg[et][pad] = v;
  990. if (et == g_etype)
  991. applyActiveType();
  992. }
  993. break;
  994. }
  995. // Switch Pro gyro mapping: 0 = corrected (default), 1 = legacy
  996. // (pre-#189 raw axes, no sensitivity trim). Protocol v19.
  997. case 38:
  998. g_swGyroLegacy = v ? 1 : 0;
  999. swProSaveCfg();
  1000. persist = false;
  1001. break;
  1002. // (field 22, rumble strength, removed -- fixed at RUMBLE_SCALE_PCT)
  1003. // (fields 23/24, Switch Pro report rate + gyro scale, removed -- rate is
  1004. // fixed at full and the gyro mapping is now field 38)
  1005. // (field 25, poll RX window, removed -- g_rxWin is now FIXED/not configurable)
  1006. // (fields 27/28, post-connect haptic block, removed -- permanently disabled)
  1007. // Used to be experimental g_landAll87. Persisted; blob p[181] reflects state. Now ignored.
  1008. case 29:
  1009. // g_landAll87 = v ? 1 : 0;
  1010. break;
  1011. }
  1012. if (persist)
  1013. saveCfg();
  1014. g_blobRequest = true;
  1015. } else if (op == 0x03) {
  1016. uint8_t m = buf[1];
  1017. if (modeValid(m) && !USBDevice.suspended()) {
  1018. // best-effort status to the panel before the reboot; the SOF drain
  1019. // sends it during the flush below (no blocking flush on loop()).
  1020. g_blobRequest = true;
  1021. usb_web.flush();
  1022. // clean detach + reboot (releases held input on the outgoing device)
  1023. modeSwitchReboot(m);
  1024. }
  1025. }
  1026. memmove(buf, buf + need, n - need);
  1027. n -= need;
  1028. }
  1029. }
  1030. }