OpenPuck.ino 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. // OpenPuck.ino -- Steam Controller 2 ("Triton") puck reimplementation for an nRF52840.
  2. //
  3. // This firmware impersonates the Valve puck over USB, maintains puck-style bond slots, speaks the
  4. // reverse-engineered RF protocol to the controller, and re-enumerates into Steam, Xbox, Switch, PS5, or DS4
  5. // personalities. Build with -DCFG_TUD_HID=4 (the Adafruit nRF port defaults to 2).
  6. //
  7. // This file is just the entry point: setup() builds the USB presentation for the persisted mode and arms the
  8. // hardware watchdog; loop() pumps each subsystem. Everything substantive lives in the modules below -- see
  9. // ARCHITECTURE.md for the full map. The short version:
  10. // config / identity / bonds -- persisted settings, device serial, the four controller bond slots
  11. // radio / rf_link / rf_diag -- bare-metal nRF52 RADIO, the operational puck protocol, and RE tooling
  12. // triton / gamepad_util -- the decoded controller input (g_in) + shared host-report build helpers
  13. // controllers + mode_* -- one IController per USB personality (puck/lizard, xbox, switch, ps5, ds4)
  14. // haptics / webusb_config / serial_console -- haptic relay, the browser config channel, the CDC debug CLI
  15. //
  16. // Protocol reference: docs/PROTOCOL.md. Bond record = [8 uuid][16 serial].
  17. #include <Adafruit_TinyUSB.h>
  18. #include <Adafruit_LittleFS.h>
  19. #include <InternalFileSystem.h>
  20. using namespace Adafruit_LittleFS_Namespace;
  21. #include "config.h"
  22. #include "identity.h"
  23. #include "bonds.h"
  24. #include "radio.h"
  25. #include "controllers.h"
  26. #include "haptics.h"
  27. #include "rf_link.h"
  28. #include "rf_diag.h"
  29. #include "webusb_config.h"
  30. #include "serial_console.h"
  31. #include "wake_hid.h"
  32. #include "status_led.h"
  33. #if CFG_TUD_HID < 4
  34. #error "build with -DCFG_TUD_HID=4 (extra_flags): up to 4 HID interfaces per mode"
  35. #endif
  36. static uint8_t g_usbCfgDesc[512]; // puck composite (4 HID + WebUSB) exceeds the default 256 B config buffer
  37. void setup() {
  38. genSerial();
  39. ledInit();
  40. rfGenSessionAddr(); // per-device unique RF session address (advertised in the host frame; isolates pucks)
  41. InternalFS.begin();
  42. loadCfg(); g_xbox = !modeIsPuck(g_usbMode); // load persisted config + decide USB presentation BEFORE registering interfaces
  43. g_active = controllerFor(g_usbMode);
  44. // ---- USB descriptor rebuild ----
  45. // Puck mode NORMALLY drops the CDC serial console (clearConfiguration) to free a USB endpoint for the
  46. // wake-mouse interface that lets it wake a sleeping Windows host. The one-shot debug arm (g_debugCdcThisBoot,
  47. // set via the WebUSB panel / CDC 'D', auto-reverting after one boot) instead KEEPS the boot CDC composite
  48. // for that boot and skips the wake mouse (no endpoint room for both). Clean modes always rebuild bare.
  49. const bool puckMode = g_active->isPuck();
  50. const bool keepCdc = puckMode && g_debugCdcThisBoot;
  51. USBDevice.detach(); delay(30);
  52. if (keepCdc) {
  53. USBDevice.setConfigurationBuffer(g_usbCfgDesc, sizeof g_usbCfgDesc); // keep boot CDC composite (debug boot)
  54. } else {
  55. USBDevice.clearConfiguration();
  56. USBDevice.setConfigurationBuffer(g_usbCfgDesc, sizeof g_usbCfgDesc); // headroom over the default 256 B cap
  57. }
  58. // Distinct USB serial PER MODE (must be set AFTER clearConfiguration, which nulls it). Hosts cache USB
  59. // identity by VID:PID:serial; reusing one serial under a changing VID:PID can make a host refuse the new
  60. // identity. Steam keeps the exact unit serial (its pairing identity); the others get a 1-char suffix.
  61. static const char MODE_SUFFIX[] = {'X','N','L','P','S','G'}; // modes 1..6
  62. if (puckMode) { USBDevice.setSerialDescriptor(g_unit); }
  63. else { snprintf(g_usbSerial, sizeof g_usbSerial, "%s%c", g_unit, MODE_SUFFIX[g_usbMode-1]); USBDevice.setSerialDescriptor(g_usbSerial); }
  64. g_active->begin(); // register this mode's USB interface(s) + set VID/PID/strings
  65. // Boot-mouse wake interface so the host honors USBDevice.remoteWakeup() in this mode (see wake_hid.h).
  66. // Added for every clean mode, and for puck mode on a normal (CDC-dropped) boot. Skipped only on the one-shot
  67. // debug boot, where CDC takes the endpoint instead.
  68. if (!keepCdc) wakeHidBegin();
  69. // WebUSB config panel -- every mode. Puck: historical CDC+HID+vendor stack (Steam + Chrome can share it).
  70. // Clean modes: bare gamepad + WebUSB (begin() sets bcdUSB 0x0210 + BOS).
  71. usb_web.begin();
  72. // Enable USB Remote Wakeup (bit 5) so the host lets us signal wake-from-sleep. Bit 7 is always required.
  73. USBDevice.setConfigurationAttribute(0x80 | 0x20); // bmAttributes: required(0x80) | remote_wakeup(0x20)
  74. USBDevice.attach(); // re-connect with the final descriptor (host re-reads it fresh -> deterministic enumeration)
  75. Serial.begin(115200);
  76. for (int i=0; i<300 && !USBDevice.mounted(); i++) delay(10); // wait up to 3s for USB mount, but NEVER hang
  77. if (USBDevice.suspended()){ USBDevice.remoteWakeup(); ledWakePulse(); } // wake host if bus was sleeping when we (re-)attached
  78. loadBonds();
  79. hapticInit(); // clear relay/active flags + arm the reconnect block & initial stop burst
  80. static const char* MODE_NAME[]={"STEAM(puck)","XBOX(xinput+mouse)","SWITCH(horipad)","LIZARD(puck kb/mouse)","SWITCH(pro+gyro)","PS5(dualsense)","HIDGYRO(ds4+motion)"};
  81. Serial.printf("# copycat up: unit=%s board=%s, mode=%s\n", g_unit, g_board, MODE_NAME[g_usbMode<=MODE_MAX?g_usbMode:0]);
  82. if (puckMode) Serial.printf("# puck USB: %s\n", keepCdc ? "DEBUG boot (CDC console on, wake mouse off; reverts next boot)" : "normal (CDC off, wake mouse on)");
  83. Serial.printf("# session addr %02X%02X%02X%02X/%02X ch%u (discovery on ibex/ch2)\n",
  84. g_sessBase[0],g_sessBase[1],g_sessBase[2],g_sessBase[3],g_sessPrefix,g_sessCh);
  85. // Hardware watchdog: if loop() ever stops feeding it (a wedged radio busy-wait, a HardFault spin, a blocked
  86. // CDC write) the WDT resets the nRF52 after ~8s -- re-enumerating USB and re-initialising RF on its own, so a
  87. // hang no longer needs a physical replug. RUN keeps it counting in sleep; PAUSE freezes it under a debugger.
  88. NRF_WDT->CONFIG = (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos) | (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos);
  89. NRF_WDT->CRV = 8UL * 32768UL - 1; // timeout in 32.768 kHz ticks (~8 s)
  90. NRF_WDT->RREN = WDT_RREN_RR0_Msk; // arm reload register 0
  91. NRF_WDT->TASKS_START = 1;
  92. }
  93. // loop-timing diagnostics: the poll rate is capped by the loop ITERATION time (the pacing wants 4000us but a
  94. // slow loop fires the poll only as often as it comes around). Per-section us is accumulated and, each second,
  95. // the average loop period + the slowest section are published for the WebUSB panel to pinpoint the culprit.
  96. uint16_t g_loopPeriodUs = 0; // avg loop iteration time last second (1e6/iterations)
  97. uint8_t g_loopWorst = 0; // index of the slowest section: 0=webusb 1=ctrl.task 2=serial 3=rfdiag 4=rflink 5=haptic 6=led
  98. uint16_t g_loopWorstUs = 0; // that section's avg us per iteration
  99. void loop() {
  100. NRF_WDT->RR[0] = WDT_RR_RR_Reload; // feed the watchdog each loop; if we ever stop, the ~8s WDT auto-resets us
  101. if (g_dirty) { g_dirty = false; saveBonds(); }
  102. #if OPK_LOG
  103. // Diagnostic build: time each loop section so the panel can show the slowest one + the loop period.
  104. static uint32_t acc[7] = {0}; static uint32_t loops = 0; static unsigned long secMs = 0;
  105. uint32_t t;
  106. t=micros(); webusbPoll(); acc[0]+=(uint32_t)(micros()-t);
  107. t=micros(); if (g_active) g_active->task(); acc[1]+=(uint32_t)(micros()-t);
  108. t=micros(); serialConsolePoll(); acc[2]+=(uint32_t)(micros()-t);
  109. t=micros(); rfDiagTask(); acc[3]+=(uint32_t)(micros()-t);
  110. t=micros(); rfLinkTask(); acc[4]+=(uint32_t)(micros()-t);
  111. t=micros(); hapticTask(); acc[5]+=(uint32_t)(micros()-t);
  112. t=micros(); ledTask(); acc[6]+=(uint32_t)(micros()-t);
  113. loops++;
  114. if (millis()-secMs >= 1000) {
  115. g_loopPeriodUs = loops ? (uint16_t)(1000000UL/loops) : 0;
  116. uint8_t wi=0; uint32_t wv=0;
  117. for (int i=0;i<7;i++){ if(acc[i]>wv){ wv=acc[i]; wi=i; } acc[i]=0; }
  118. g_loopWorst = wi; g_loopWorstUs = loops ? (uint16_t)(wv/loops) : 0;
  119. loops=0; secMs=millis();
  120. }
  121. #else
  122. webusbPoll();
  123. if (g_active) g_active->task();
  124. serialConsolePoll();
  125. rfDiagTask();
  126. rfLinkTask();
  127. hapticTask();
  128. ledTask();
  129. #endif
  130. }