OpenPuck.ino 5.8 KB

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