webusb_config.cpp 48 KB

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