OpenPuck.ino 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  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. #if CFG_TUD_HID < 4
  33. #error "build with -DCFG_TUD_HID=4 (extra_flags): up to 4 HID interfaces per mode"
  34. #endif
  35. static uint8_t g_usbCfgDesc[512]; // puck composite (4 HID + WebUSB) exceeds the default 256 B config buffer
  36. void setup() {
  37. genSerial();
  38. rfGenSessionAddr(); // per-device unique RF session address (advertised in the host frame; isolates pucks)
  39. InternalFS.begin();
  40. loadCfg(); g_xbox = !modeIsPuck(g_usbMode); // load persisted config + decide USB presentation BEFORE registering interfaces
  41. g_active = controllerFor(g_usbMode);
  42. // ---- USB descriptor rebuild ----
  43. // Puck (Steam/Lizard): KEEP the boot CDC composite (do NOT clearConfiguration) and append 4 HID + WebUSB --
  44. // this is the layout that worked with Steam running + Chrome WebUSB at the same time. Clearing puck to "save
  45. // space" broke that coexistence. Clean controller modes still clear the CDC composite and rebuild bare.
  46. const bool puckMode = g_active->isPuck();
  47. USBDevice.detach(); delay(30);
  48. if (puckMode) {
  49. USBDevice.setConfigurationBuffer(g_usbCfgDesc, sizeof g_usbCfgDesc); // headroom over the default 256 B cap
  50. } else {
  51. USBDevice.clearConfiguration();
  52. USBDevice.setConfigurationBuffer(g_usbCfgDesc, sizeof g_usbCfgDesc);
  53. }
  54. // Distinct USB serial PER MODE (must be set AFTER clearConfiguration, which nulls it). Hosts cache USB
  55. // identity by VID:PID:serial; reusing one serial under a changing VID:PID can make a host refuse the new
  56. // identity. Steam keeps the exact unit serial (its pairing identity); the others get a 1-char suffix.
  57. static const char MODE_SUFFIX[] = {'X','N','L','P','S','G'}; // modes 1..6
  58. if (puckMode) { USBDevice.setSerialDescriptor(g_unit); }
  59. else { snprintf(g_usbSerial, sizeof g_usbSerial, "%s%c", g_unit, MODE_SUFFIX[g_usbMode-1]); USBDevice.setSerialDescriptor(g_usbSerial); }
  60. g_active->begin(); // register this mode's USB interface(s) + set VID/PID/strings
  61. // Boot-mouse wake interface so the host honors USBDevice.remoteWakeup() in this mode (see wake_hid.h).
  62. // Skipped in puck mode: its CDC + 4 HID + WebUSB composite already uses all 7 data IN endpoints.
  63. if (!puckMode) wakeHidBegin();
  64. // WebUSB config panel -- every mode. Puck: historical CDC+HID+vendor stack (Steam + Chrome can share it).
  65. // Clean modes: bare gamepad + WebUSB (begin() sets bcdUSB 0x0210 + BOS).
  66. usb_web.begin();
  67. // Enable USB Remote Wakeup (bit 5) so the host lets us signal wake-from-sleep. Bit 7 is always required.
  68. USBDevice.setConfigurationAttribute(0x80 | 0x20); // bmAttributes: required(0x80) | remote_wakeup(0x20)
  69. USBDevice.attach(); // re-connect with the final descriptor (host re-reads it fresh -> deterministic enumeration)
  70. Serial.begin(115200);
  71. for (int i=0; i<300 && !USBDevice.mounted(); i++) delay(10); // wait up to 3s for USB mount, but NEVER hang
  72. if (USBDevice.suspended()) USBDevice.remoteWakeup(); // wake host if bus was sleeping when we (re-)attached
  73. loadBonds();
  74. hapticInit(); // clear relay/active flags + arm the reconnect block & initial stop burst
  75. static const char* MODE_NAME[]={"STEAM(puck)","XBOX(xinput+mouse)","SWITCH(horipad)","LIZARD(puck kb/mouse)","SWITCH(pro+gyro)","PS5(dualsense)","HIDGYRO(ds4+motion)"};
  76. Serial.printf("# copycat up: unit=%s board=%s, mode=%s\n", g_unit, g_board, MODE_NAME[g_usbMode<=MODE_MAX?g_usbMode:0]);
  77. Serial.printf("# session addr %02X%02X%02X%02X/%02X ch%u (discovery on ibex/ch2)\n",
  78. g_sessBase[0],g_sessBase[1],g_sessBase[2],g_sessBase[3],g_sessPrefix,g_sessCh);
  79. // Hardware watchdog: if loop() ever stops feeding it (a wedged radio busy-wait, a HardFault spin, a blocked
  80. // CDC write) the WDT resets the nRF52 after ~8s -- re-enumerating USB and re-initialising RF on its own, so a
  81. // hang no longer needs a physical replug. RUN keeps it counting in sleep; PAUSE freezes it under a debugger.
  82. NRF_WDT->CONFIG = (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos) | (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos);
  83. NRF_WDT->CRV = 8UL * 32768UL - 1; // timeout in 32.768 kHz ticks (~8 s)
  84. NRF_WDT->RREN = WDT_RREN_RR0_Msk; // arm reload register 0
  85. NRF_WDT->TASKS_START = 1;
  86. }
  87. void loop() {
  88. NRF_WDT->RR[0] = WDT_RR_RR_Reload; // feed the watchdog each loop; if we ever stop, the ~8s WDT auto-resets us
  89. if (g_dirty) { g_dirty = false; saveBonds(); }
  90. webusbPoll();
  91. if (g_active) g_active->task(); // streamed-mode emit / handshake drains / mode upkeep (puck: USB conn presentation; xbox: rumble relay)
  92. serialConsolePoll();
  93. rfDiagTask(); // service whichever RF RE/calibration mode is active (no-op in normal use)
  94. rfLinkTask(); // host-frame beacons + connected-mode poll (decodes input -> g_in -> active controller) + QoS + stats
  95. hapticTask(); // reconnect-block edge handling + steam 0x82 quiet timeout
  96. }