Quellcode durchsuchen

Some more changes trying to fix the xinput + mouse issue

Jariullah Safi vor 4 Monaten
Ursprung
Commit
53d855acca
2 geänderte Dateien mit 62 neuen und 34 gelöschten Zeilen
  1. 58 30
      OpenPuck.ino
  2. 4 4
      docs/PROTOCOL.md

+ 58 - 30
OpenPuck.ino

@@ -7,8 +7,8 @@
 //
 // USB presentations: Steam = the genuine 4-slot Valve puck (28DE:1304) with the feature-report command
 // channel (0x83/0xAE/0xB4/0xA3/0xA2/0xAD); desktop keyboard+mouse "lizard" is automatic when Steam is
-// closed. Xbox = a clean XInput controller (045E:028E) + right-pad mouse. Switch = a HORIPAD pad
-// (0F0D:0092). Xbox/Switch are non-composite (no CDC/WebUSB) so strict hosts bind them.
+// closed. Xbox = a clean XInput controller (045E:028E) + right-pad mouse. Switch = a HORIPAD pad (0F0D:0092).
+// Xbox/Switch omit CDC/WebUSB so strict hosts bind them without vendor/config side effects.
 //
 // Protocol reference: docs/PROTOCOL.md.  Bond record = [8 uuid][16 serial].
 
@@ -61,7 +61,7 @@ static const uint8_t PUCK_HID_DESC[] = {
 #define NSLOT 4
 Adafruit_USBD_HID hid[NSLOT];
 // XBOX MODE = a MUTUALLY-EXCLUSIVE USB presentation: the device enumerates as EITHER the 4 puck slot
-// interfaces (Steam mode) OR a standard gamepad + a mouse (Xbox mode) — never both. The mode is persisted to
+// interfaces (Steam mode) OR an XInput controller + a mouse (Xbox mode) — never both. The mode is persisted to
 // flash and a button combo (back4+X -> xbox, back4+A -> steam) saves it and reboots, so each boot enumerates
 // the right interface set. RF (beacon/poll/bonds) is identical in both modes; only the USB side differs.
 static const uint8_t MOUSE_HID_DESC[] = { TUD_HID_REPORT_DESC_MOUSE() };
@@ -74,7 +74,7 @@ static bool    g_xbox = false;   // == (g_usbMode != 0); kept for the existing p
 // last selected mode across reboots.
 static bool    g_persistMode = false;   // false (default) = always boot Steam; true = boot into last mode
 static uint8_t g_bootMode = 0xFF;       // one-shot: boot into this mode once then clear (used when !persistMode + explicit switch)
-volatile uint8_t g_rumble = 0;   // Xbox-mode rumble strength from the host's XInput OUT packet (0=off); drives the haptic relay
+volatile uint8_t g_rumble = 0;   // legacy XInput rumble strength from the host's OUT packet (0=off); drives the haptic relay when the XInput descriptor is used
 volatile unsigned long g_rumbleMs = 0;   // millis of last rumble OUT packet (declared here — used by xi_xfer above the watchdog block); stuck-rumble watchdog
 // persisted, runtime-tunable config (Cfg struct + load/saveCfg below; CDC M/F/D/W/B + WebUSB set these):
 static int     g_mDiv = 64, g_mFric = 94, g_padSmooth = 35;  // xbox mouse sens/friction%, steam pad smoothing%
@@ -201,8 +201,10 @@ static volatile uint8_t g_testHaptic = 0;   // 't<n>' injects n test haptics (ou
 // can likewise latch on the controller if Steam's stop relay is lost. We auto-release on host silence.
 static unsigned long g_haptic82Ms = 0;            // millis of last 0x82 haptic OUTPUT relayed (Steam mode)
 static bool          g_haptic82On = false;        // a non-zero 0x82 haptic is currently active (awaiting host stop)
+static unsigned long g_hapticBlockUntil = 0;        // drop Steam haptics briefly during reconnect settle
 #define RUMBLE_STUCK_MS   2500u   // Xbox: release a held rumble if no OUT packet refreshes it for this long (covers a lost stop without cutting normal short rumbles)
-#define HAPTIC_QUIET_MS    300u   // Steam: if 0x82 haptics go silent this long after being active, send an explicit off (glide stream is ~25ms-spaced, so 300ms silence = really stopped)
+#define HAPTIC_QUIET_MS    300u   // Steam: after this much host silence, consider the current 0x82 haptic stream inactive
+#define HAPTIC_RECONNECT_BLOCK_MS 1500u
 // Seamless lizard (Steam mode): exactly like the real puck — when Steam is running it forwards the gamepad
 // report 0x45; when Steam is closed it drives mouse(0x40)+keyboard(0x41) on the SAME puck interface. This is
 // a PURELY USB-SIDE decision — it changes nothing about the RF poll or the host->controller relay, so the
@@ -269,14 +271,34 @@ static void loadBonds() {
   f.close();
 }
 
+static bool hapticLinkUp(){
+  return g_connSlot>=0 && (millis()-g_connReplyMs) < 300;
+}
+static bool haptic82Blocked(){
+  return !hapticLinkUp() || (g_hapticBlockUntil && (int32_t)(millis()-g_hapticBlockUntil) < 0);
+}
+static bool haptic82PayloadOn(const uint8_t* p, uint16_t n){
+  if(n<3) return false;
+  for(uint16_t i=2;i<n;i++) if(p[i]) return true;   // observed form is [01 01 gain], but treat any trailing non-zero as active
+  return false;
+}
+static void haptic82HostReport(const uint8_t* p, uint16_t n){
+  if(n<3) return;
+  g_haptic82Ms = millis();
+  g_haptic82On = haptic82PayloadOn(p,n);
+}
+
 // ---- command channel; `slot` is the interface index (interface N == bond slot N) ----
 static void handleSet(int slot, uint8_t rid, hid_report_type_t type, uint8_t const *b, uint16_t n) {
   if (type == HID_REPORT_TYPE_OUTPUT) {   // Steam haptic/LED OUTPUT reports 0x80-0x89 -> relay to controller
     if (rid >= 0x80 && rid <= 0x89 && n >= 1) {                 // wrap as a SET sub-TLV like the report-01 path
-      uint8_t m = n > (uint16_t)(sizeof g_relayBuf - 2) ? (sizeof g_relayBuf - 2) : n;
-      g_relayBuf[0] = rid; g_relayBuf[1] = m; memcpy(g_relayBuf + 2, b, m);
-      g_relayN = m + 2; g_relayPend = true;
-      if (rid == 0x82) { g_haptic82Ms = millis(); g_haptic82On = (n >= 3 && b[2] != 0); }  // track on/off for the stuck-haptic watchdog
+      bool haptic82 = (rid == 0x82);
+      if (!haptic82 || !haptic82Blocked()) {
+        uint8_t m = n > (uint16_t)(sizeof g_relayBuf - 2) ? (sizeof g_relayBuf - 2) : n;
+        g_relayBuf[0] = rid; g_relayBuf[1] = m; memcpy(g_relayBuf + 2, b, m);
+        g_relayN = m + 2; g_relayPend = true;
+      }
+      if (haptic82) haptic82HostReport(b, n);  // track on/off for the stuck-haptic watchdog
     }
     if (Serial.availableForWrite() > 80) {                      // log so we can see what Steam actually sends (e.g. glide haptics)
       Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid, n);
@@ -291,8 +313,12 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type, uint8_t con
   const uint8_t *pl = b + 2; uint16_t pln = (n >= 2) ? n - 2 : 0;
   if (cmd == 0x87) g_steamAliveMs = millis();   // Steam's settings/lizard-off heartbeat (~every 3s) -> keep forwarding gamepad, suppress auto-lizard
   if (rid == 1 && n >= 2) {   // report 0x01 = raw passthrough -> queue for RF relay to the controller
-    uint16_t m = n > sizeof g_relayBuf ? sizeof g_relayBuf : n;
-    memcpy(g_relayBuf, b, m); g_relayN = m; g_relayPend = true;
+    bool haptic82 = (cmd == 0x82 && len <= pln);
+    if (!haptic82 || !haptic82Blocked()) {
+      uint16_t m = n > sizeof g_relayBuf ? sizeof g_relayBuf : n;
+      memcpy(g_relayBuf, b, m); g_relayN = m; g_relayPend = true;
+    }
+    if (haptic82) haptic82HostReport(pl, len);  // Steam can send haptics through feature passthrough too
   }
   if (Serial.availableForWrite() > 80) {   // log host feature writes (non-blocking)
     Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid, cmd, len);
@@ -724,7 +750,7 @@ static void smoothPad(uint8_t* rep){     // rep = report 0x45; smooth LPad(@18/2
     int16_t ox=(int16_t)rsx,oy=(int16_t)rsy; rep[24]=ox;rep[25]=ox>>8;rep[26]=oy;rep[27]=oy>>8; }
   rpt=rt;
 }
-// button code (g_back[], g_abSwap targets) -> XInput bit. 0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=Start 11=Guide
+// button code (g_back[], g_abSwap targets) -> legacy XInput bit. 0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=Start 11=Guide
 static uint16_t codeToXB(uint8_t c){
   switch(c){ case 1:return XB_A; case 2:return XB_B; case 3:return XB_X; case 4:return XB_Y;
     case 5:return XB_LB; case 6:return XB_RB; case 7:return XB_L3; case 8:return XB_R3;
@@ -751,8 +777,7 @@ static void rfXboxGamepad(const uint8_t* r){
 // as Lizard's right pad (velocity injected from pad deltas, friction decay, sub-pixel carry). This is purely
 // a USB-side translation of the received 0x45 report — it touches NOTHING about the RF poll or relay, so the
 // controller behaves exactly as it does for the bare gamepad. RPad click = left button, LPad click = right.
-// (The earlier "mouse vs controller race" was actually the CDC composite, now fixed: Xbox drops CDC/WebUSB,
-//  so the only interfaces are XInput (MI_00) + mouse (MI_01).)
+// Xbox drops CDC/WebUSB, so the only active USB interfaces are XInput (MI_00) + boot mouse (MI_01).
 static void rfXboxMouse(const uint8_t* r){
   uint32_t b=btnsOf(r);
   static int prx=0,pry=0; static bool prt=false; static float vx=0,vy=0,rmx=0,rmy=0; static uint8_t pmb=0;
@@ -1278,21 +1303,20 @@ void setup() {
   InternalFS.begin();
   loadCfg(); g_xbox = (g_usbMode != 0);   // load persisted config + decide USB presentation BEFORE registering interfaces
 
-  // ---- CLEAN, NON-COMPOSITE presentation for Xbox + Switch ----
+  // ---- CLEAN presentation for Xbox + Switch ----
   // The Adafruit core auto-adds a CDC (Serial) interface at startup, making every device a CDC composite
-  // (bDeviceClass=0xEF/IAD). That breaks the two real-controller personalities:
+  // (bDeviceClass=0xEF/IAD). That breaks the clean controller personalities:
   //   * XBOX: Windows' xusb (Xbox 360) driver matches 045E:028E at the DEVICE level. In a composite the
-  //     gamepad is interface MI_02 and xusb's INF never matches it -> the controller never appears. (Dropping
-  //     only the mouse earlier left the device composite, so it stayed broken on every host.)
+  //     gamepad must stay MI_00 and the extra mouse must be a plain boot mouse interface.
   //   * SWITCH: a real console rejects composite devices outright; it accepts only a bare HID gamepad.
-  // Fix: tear the auto CDC composite down and rebuild a single-interface device. clearConfiguration() also
+  // Fix: tear the auto CDC composite down and rebuild only the intended controller interfaces. clearConfiguration() also
   // resets bDeviceClass to 0x00 (single function). We deliberately KEEP bcdUSB at 0x0200 (no WebUSB / USB 2.1)
   // so the host never requests the WebUSB BOS — whose MS-OS-2.0 blob would otherwise tell Windows to bind
   // WinUSB to interface 0 (our gamepad). detach()/attach() forces the host to re-read the rebuilt descriptor.
   // Force a CLEAN re-enumeration on every boot / mode-switch: detach FIRST so the host drops the previous
   // identity, rebuild the descriptor, then re-attach (at the end of setup). This is done in ALL modes, not
   // just the clean ones: switching Xbox->Steam while Steam holds the device open could otherwise leave the
-  // host caching the old 045E:028E device, so the puck sometimes failed to enumerate. The detach gives the OS
+  // host caching the old controller identity, so the puck sometimes failed to enumerate. The detach gives the OS
   // an unambiguous disconnect to release its handles before the new descriptor comes up.
   const bool cleanDevice = (g_usbMode == 1 || g_usbMode == 2);
   USBDevice.detach(); delay(30);
@@ -1303,13 +1327,15 @@ void setup() {
   // identity. Steam keeps the exact unit serial (its pairing identity); the others get a 1-char suffix.
   if (g_usbMode == 0) { USBDevice.setSerialDescriptor(g_unit); }
   else { snprintf(g_usbSerial, sizeof g_usbSerial, "%s%c", g_unit, g_usbMode==1?'X':'N'); USBDevice.setSerialDescriptor(g_usbSerial); }
-  if (g_usbMode == 1) {                // XBOX presentation: a LONE XInput vendor interface == a real wired Xbox 360 pad
+  if (g_usbMode == 1) {                // XBOX presentation: XInput vendor interface + HID boot mouse
     USBDevice.setID(0x045E, 0x028E);   // device-level 045E:028E match -> Windows xusb / SDL / Linux xpad all bind it
     USBDevice.setVersion(0x0200); USBDevice.setDeviceVersion(0x0114);   // bcdUSB 0x0200 = no BOS request (clean, no WinUSB fight)
     USBDevice.setManufacturerDescriptor("Microsoft");
     USBDevice.setProductDescriptor("Controller");
     g_xinput.setStringDescriptor("Controller"); g_xinput.begin();   // XInput vendor interface (FF/5D/01) -> MI_00
-    g_mouse.setReportDescriptor(MOUSE_HID_DESC, sizeof MOUSE_HID_DESC); g_mouse.setPollInterval(1); g_mouse.begin();   // right-pad mouse -> MI_01 (no CDC/WebUSB, so XInput stays MI_00)
+    g_mouse.setStringDescriptor("OpenPuck Mouse");
+    g_mouse.setBootProtocol(HID_ITF_PROTOCOL_MOUSE);
+    g_mouse.setReportDescriptor(MOUSE_HID_DESC, sizeof MOUSE_HID_DESC); g_mouse.setPollInterval(1); g_mouse.begin();
   } else if (g_usbMode == 2) {         // SWITCH presentation: a LONE HID HORIPAD/Pokkén pad (console-compatible)
     USBDevice.setID(0x0F0D, 0x0092);   // HORI Pokkén Tournament Pro Pad (whitelisted by a real Switch console)
     USBDevice.setVersion(0x0200); USBDevice.setDeviceVersion(0x0200);
@@ -1371,8 +1397,13 @@ void loop() {
     if (millis()-g_lastDisc >= (connNow ? 200u : 0u)) { g_lastDisc=millis(); g_rfCh=2; for (int s=0;s<NSLOT;s++) rfHostFrameOnce(s); }
   }
   if (g_connOn && millis()-g_connCooldown > 2500) { rfConnStep(); }            // connected-mode: poll controller, read input
-  // Xbox-mode rumble -> relay the haptic (0x82 [01 01 gain]) to the controller while the host commands it,
-  // re-queued ~40/s like Steam's glide haptic so it sustains. gain scaled by the XInput motor strength.
+  { static bool wasHapticLinkUp=false;
+    bool up=hapticLinkUp();
+    if(up && !wasHapticLinkUp) g_hapticBlockUntil = millis() + HAPTIC_RECONNECT_BLOCK_MS;   // reconnect hygiene: don't replay Steam's stale startup haptics
+    wasHapticLinkUp=up;
+  }
+  // Legacy XInput rumble -> relay the haptic (0x82 [01 01 gain]) to the controller while the host commands it,
+  // re-queued ~40/s like Steam's glide haptic so it sustains. Not active in the current HID Xbox presentation.
   if (g_usbMode==1 && g_rumble && millis()-g_rumbleMs > RUMBLE_STUCK_MS) g_rumble=0;   // watchdog: lost stop packet -> release the held rumble instead of buzzing forever
   if (g_usbMode==1 && g_rumble && !g_relayPend && g_connSlot>=0) {
     static unsigned long lastRumble=0;
@@ -1381,12 +1412,9 @@ void loop() {
       g_relayBuf[0]=0x82; g_relayBuf[1]=3; g_relayBuf[2]=0x01; g_relayBuf[3]=0x01; g_relayBuf[4]=gain; g_relayPend=true;
     }
   }
-  // Steam-mode haptic watchdog: glide haptics are one-shot relays, but if Steam's stop is lost the controller
-  // can latch buzzing. If a 0x82 haptic was active and the host's stream has gone quiet, send one explicit off.
-  if (!g_xbox && g_haptic82On && millis()-g_haptic82Ms > HAPTIC_QUIET_MS && !g_relayPend && g_connSlot>=0) {
-    g_haptic82On=false;
-    g_relayBuf[0]=0x82; g_relayBuf[1]=3; g_relayBuf[2]=0x01; g_relayBuf[3]=0x01; g_relayBuf[4]=0x00; g_relayN=5; g_relayPend=true;   // 0x82 [01 01 00] = haptic off
-  }
+  // Steam-mode haptic guard: do not synthesize haptic packets. The reconnect guard above only drops stale
+  // host 0x82 traffic until the link is stable; active-session haptics are relayed verbatim.
+  if (!g_xbox && g_haptic82On && millis()-g_haptic82Ms > HAPTIC_QUIET_MS) g_haptic82On=false;
   // QoS: if the current channel is degrading (crcfail+noRx), hop to the next clean candidate (conservative).
   if (g_qos && g_connSlot>=0 && millis()-g_qosCheckMs>=600) {
     uint16_t bad=g_qosBad; g_qosBad=0; g_qosCheckMs=millis();

+ 4 - 4
docs/PROTOCOL.md

@@ -275,10 +275,10 @@ offset  size  meaning
 The RF side stays the same across modes. Only USB enumeration changes.
 
 There are three switchable modes (`0=Steam 1=Xbox 2=Switch`). Steam mode is a CDC + WebUSB composite;
-Xbox and Switch are **clean, non-composite, single-function** devices — the auto-added CDC/WebUSB
+Xbox and Switch are **clean controller-only** devices — the auto-added CDC/WebUSB
 interfaces are torn down (`clearConfiguration`) and `bcdUSB` stays `0x0200` (no USB-2.1 BOS). This is
-required because Windows' `xusb` driver matches `045E:028E` at the device level (a composite hides the
-gamepad behind an `MI_xx`) and a real Switch console rejects composite devices. Every boot/mode-switch
+required because Windows' `xusb` driver expects the Xbox 360 controller on the primary device/interface,
+and a real Switch console rejects composite devices. Every boot/mode-switch
 does a `detach -> rebuild -> attach` so the host re-reads the descriptor cleanly.
 
 ### 9.1 Steam mode
@@ -295,7 +295,7 @@ does a `detach -> rebuild -> attach` so the host re-reads the descriptor cleanly
 ### 9.2 Xbox mode
 
 - VID:PID `045E:028E`, clean device (no CDC/WebUSB)
-- Custom XInput-compatible vendor interface on `MI_00` + a HID mouse on `MI_01` (right-pad emulation)
+- Custom XInput-compatible vendor interface on `MI_00` + HID boot mouse on `MI_01` (right-pad emulation)
 - `0x45` is converted into a 20-byte XInput report
 
 ### 9.3 Switch mode