Ver Fonte

rumble seems to work across all modes

Jariullah Safi há 4 meses atrás
pai
commit
106af576c6

+ 8 - 3
OpenPuck/OpenPuck.ino

@@ -74,6 +74,11 @@ void setup() {
   if (puckMode) { USBDevice.setSerialDescriptor(g_unit); }
   else { snprintf(g_usbSerial, sizeof g_usbSerial, "%s%c", g_unit, MODE_SUFFIX[g_usbMode-1]); USBDevice.setSerialDescriptor(g_usbSerial); }
 
+  // SDL's Steam dongle backend only opens Valve HID interfaces numbered 1..4. Register WebUSB before the
+  // puck slots so the four slot HIDs land on interfaces 1..4 like the real dongle; clean modes keep their
+  // controller interface first so Windows/SDL bind them normally.
+  if (puckMode) usb_web.begin();
+
   g_active->begin();   // register this mode's USB interface(s) + set VID/PID/strings
 
   // Boot-mouse wake interface so the host honors USBDevice.remoteWakeup() in this mode (see wake_hid.h).
@@ -81,9 +86,9 @@ void setup() {
   // debug boot, where CDC takes the endpoint instead.
   if (!keepCdc) wakeHidBegin();
 
-  // WebUSB config panel -- every mode. Puck: historical CDC+HID+vendor stack (Steam + Chrome can share it).
-  // Clean modes: bare gamepad + WebUSB (begin() sets bcdUSB 0x0210 + BOS).
-  usb_web.begin();
+  // WebUSB config panel -- every mode. In puck mode this was registered before the slot HIDs to keep SDL's
+  // expected interface numbers; clean modes register it after the controller/wake interfaces.
+  if (!puckMode) usb_web.begin();
   // Enable USB Remote Wakeup (bit 5) so the host lets us signal wake-from-sleep. Bit 7 is always required.
   USBDevice.setConfigurationAttribute(0x80 | 0x20);  // bmAttributes: required(0x80) | remote_wakeup(0x20)
   USBDevice.attach();   // re-connect with the final descriptor (host re-reads it fresh -> deterministic enumeration)

+ 2 - 2
OpenPuck/identity.cpp

@@ -7,8 +7,8 @@ char g_unit[16];
 char g_board[16];
 char g_usbSerial[18];
 
-const uint8_t ATTR83[25] = {     // 0x83 attributes (product 0x1304 = puck)
-  0x01,0x04,0x13,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x0A,0xF2,0xF9,0xD2,0x68,
+const uint8_t ATTR83[25] = {     // 0x83 attributes (product 0x1142 = classic Steam Controller dongle)
+  0x01,0x42,0x11,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x0A,0xF2,0xF9,0xD2,0x68,
   0x04,0x53,0xD0,0x18,0x6A,0x09,0x47,0x00,0x00,0x00 };
 
 void genSerial() {

+ 1 - 1
OpenPuck/identity.h

@@ -9,7 +9,7 @@ extern char g_unit[16];        // "FXB99602xxxxx"
 extern char g_board[16];       // "MXB99602xxxxx"
 extern char g_usbSerial[18];   // per-mode USB serial (Steam uses g_unit; others get a suffix)
 
-// 0x83 attributes (product 0x1304 = puck), returned by the puck feature command channel.
+// 0x83 attributes, returned by the puck feature command channel.
 extern const uint8_t ATTR83[25];
 
 void genSerial();

+ 31 - 1
OpenPuck/mode_ps5.cpp

@@ -33,10 +33,40 @@ static const uint8_t PS5_HID_DESC[]={
 static unsigned long g_ps5LastMs=0;
 static Adafruit_USBD_HID g_ps5;
 
+static uint16_t ps5Get(uint8_t rid, hid_report_type_t type, uint8_t* buf, uint16_t reqlen){
+  if(type!=HID_REPORT_TYPE_FEATURE || !buf || reqlen==0) return 0;
+  memset(buf,0,reqlen);
+  switch(rid){
+    case 0x03: {                         // capabilities: identify as DualSense-capable (SDL checks byte 2 == 0x28)
+      if(reqlen<47) return 0;
+      buf[0]=0x00; buf[1]=0x28; buf[2]=0x01; buf[3]=0x00;
+      buf[4]=0x0E;                       // sensors + lightbar + vibration capability bits
+      return 47;
+    }
+    case 0x05: {                         // calibration: neutral-ish non-zero block so SDL's read succeeds
+      uint16_t n = reqlen<40 ? reqlen : 40;
+      for(uint16_t i=0;i<n;i++) buf[i]=0;
+      return n;
+    }
+    case 0x09: {                         // serial number feature
+      uint16_t n = reqlen<20 ? reqlen : 20;
+      for(uint16_t i=0;i<n && i<12;i++) buf[i]=(uint8_t)("010203040506"[i]);
+      return n;
+    }
+    case 0x20: {                         // firmware info
+      uint16_t n = reqlen<63 ? reqlen : 63;
+      buf[0]=0x01; buf[1]=0x00;          // minimal non-zero firmware version
+      return n;
+    }
+    default:
+      return 0;
+  }
+}
 static void ps5Set(uint8_t rid, hid_report_type_t type, uint8_t const* b, uint16_t n){
   if(type!=HID_REPORT_TYPE_OUTPUT || n<1) return;
   uint8_t id; const uint8_t* p; uint16_t pn;
   if(rid==0){ id=b[0]; p=b+1; pn=(uint16_t)(n-1); }
+  else if(rid==0x02 && b[0]==0x02 && n>=5){ id=rid; p=b+1; pn=(uint16_t)(n-1); } // some paths leave report id in b
   else      { id=rid;  p=b;   pn=n;               }
   if(id!=0x02 || pn<4) return;
   hapticSteamRumble((uint16_t)p[3]*257u, (uint16_t)p[2]*257u);   // DualSense: left=low, right=high
@@ -71,7 +101,7 @@ void Ps5Controller::begin(){
   USBDevice.setManufacturerDescriptor("Sony Interactive Entertainment");
   USBDevice.setProductDescriptor("DualSense Wireless Controller");
   g_ps5.enableOutEndpoint(true);
-  g_ps5.setReportCallback(NULL, ps5Set);
+  g_ps5.setReportCallback(ps5Get, ps5Set);
   g_ps5.setReportDescriptor(PS5_HID_DESC, sizeof PS5_HID_DESC);
   g_ps5.setPollInterval(4);
   g_ps5.begin();

+ 64 - 7
OpenPuck/puck_hid.cpp

@@ -13,8 +13,9 @@
 uint8_t g_fwdNewOnly = 1;
 SteamPuckController g_steamPuck;
 
-// ---- cloned puck HID report descriptor (verbatim): mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte
-//      FEATURE command reports on report id 1/2. Each of the 4 interfaces uses this. ----
+// ---- cloned puck HID report descriptor: mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte FEATURE
+//      command reports on report id 1/2. Steam mode also declares input report 0x01 for SDL's Steam HIDAPI
+//      compatibility stream (ValveInReport packets). Each of the 4 interfaces uses this. ----
 static const uint8_t PUCK_HID_DESC[] = {
   0x05,0x01,0x09,0x02,0xA1,0x01,0x85,0x40,0x09,0x01,0xA1,0x00,0x05,0x09,0x19,0x01,
   0x29,0x02,0x15,0x00,0x25,0x01,0x75,0x01,0x95,0x02,0x81,0x02,0x75,0x06,0x95,0x01,
@@ -38,7 +39,7 @@ static const uint8_t PUCK_HID_DESC[] = {
   0x95,0x03,0x09,0x86,0x91,0x02,0x85,0x87,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,
   0x3F,0x09,0x87,0x91,0x02,0x85,0x89,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,0x3F,
   0x09,0x89,0x91,0x02,0x85,0x88,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,0x3F,0x09,
-  0x88,0x91,0x02,0x85,0x01,0x95,0x3F,0x09,0x01,0xB1,0x02,0x85,0x02,0x95,0x3F,0x09,
+  0x88,0x91,0x02,0x85,0x01,0x95,0x2F,0x09,0x01,0x81,0x02,0x85,0x01,0x95,0x3F,0x09,0x01,0xB1,0x02,0x85,0x02,0x95,0x3F,0x09,
   0x01,0xB1,0x02,0xC0
 };
 
@@ -67,7 +68,7 @@ static const uint8_t PUCK_LIZARD_HID_DESC[] = {
   0x95,0x03,0x09,0x86,0x91,0x02,0x85,0x87,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,
   0x3F,0x09,0x87,0x91,0x02,0x85,0x89,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,0x3F,
   0x09,0x89,0x91,0x02,0x85,0x88,0x15,0x00,0x26,0xFF,0x00,0x75,0x08,0x95,0x3F,0x09,
-  0x88,0x91,0x02,0x85,0x01,0x95,0x3F,0x09,0x01,0xB1,0x02,0x85,0x02,0x95,0x3F,0x09,
+  0x88,0x91,0x02,0x85,0x01,0x95,0x2F,0x09,0x01,0x81,0x02,0x85,0x01,0x95,0x3F,0x09,0x01,0xB1,0x02,0x85,0x02,0x95,0x3F,0x09,
   0x01,0xB1,0x02,0xC0,
   0x05,0x0C,0x09,0x01,0xA1,0x01,0x85,0x03,0x15,0x00,0x25,0x01,0x09,0xE9,0x09,0xEA,
   0x75,0x01,0x95,0x02,0x81,0x02,0x75,0x06,0x95,0x01,0x81,0x01,0xC0
@@ -75,6 +76,8 @@ static const uint8_t PUCK_LIZARD_HID_DESC[] = {
 
 static Adafruit_USBD_HID hid[NSLOT];
 
+static uint32_t g_sdlPacketNum = 0;
+
 // ===================== seamless LIZARD decision =====================
 // Steam, while running, re-sends settings report 0x87 (lizard-off) every ~3s as a heartbeat (captured on HW),
 // and ANY OUTPUT report likewise stamps g_steamAliveMs. When the heartbeat stops we fall back to lizard, so the
@@ -91,6 +94,47 @@ static bool g_autoLizard = true;            // master switch; false => Steam mod
 // the controller while presenting lizard (Steam isn't reading 0x45 back), Steam loops the same haptic -> buzz.
 static inline bool steamDrivingGamepad(){ return g_steamAliveMs && (millis()-g_steamAliveMs < LIZARD_WD_MS); }
 static inline bool lizardActive(){ return modeIsPuck(g_usbMode) && (g_usbMode==MODE_LIZARD || (g_autoLizard && !steamDrivingGamepad())); }
+static void put16(uint8_t* p, int16_t v){ p[0]=(uint8_t)v; p[1]=(uint8_t)((uint16_t)v>>8); }
+static void putu16(uint8_t* p, uint16_t v){ p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); }
+static void put32(uint8_t* p, uint32_t v){ p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); p[2]=(uint8_t)(v>>16); p[3]=(uint8_t)(v>>24); }
+static void put64(uint8_t* p, uint64_t v){ for(uint8_t i=0;i<8;i++) p[i]=(uint8_t)(v>>(8*i)); }
+static uint16_t sdlTrigger(uint8_t v){ return (uint16_t)(((uint32_t)v * 26000u + 127u) / 255u); }
+static uint64_t sdlButtons(uint32_t b){
+  uint64_t s=0;
+  if(b&TB_RB)s|=0x00000004ull; if(b&TB_LB)s|=0x00000008ull;
+  if(b&TB_Y)s|=0x00000010ull;  if(b&TB_B)s|=0x00000020ull;
+  if(b&TB_X)s|=0x00000040ull;  if(b&TB_A)s|=0x00000080ull;
+  if(b&TB_DUP)s|=0x00000100ull; if(b&TB_DRT)s|=0x00000200ull;
+  if(b&TB_DLF)s|=0x00000400ull; if(b&TB_DDN)s|=0x00000800ull;
+  if(b&TB_VIEW)s|=0x00001000ull; if(b&TB_STEAM)s|=0x00002000ull;
+  if(b&TB_MENU)s|=0x00004000ull; if(b&TB_L4)s|=0x00008000ull; if(b&TB_R4)s|=0x00010000ull;
+  if(b&TB_LPADC)s|=0x00020000ull; if(b&TB_RPADC)s|=0x00040000ull;
+  if(b&TB_LPADT)s|=0x00080000ull; if(b&TB_RPADT)s|=0x00100000ull;
+  if(b&TB_L3)s|=0x00400000ull;
+  return s;
+}
+static void sendSdlWirelessEvent(uint8_t slot, bool connected){
+  uint8_t p[4]={0x00,0x03,0x01,(uint8_t)(connected?0x02:0x01)}; // report id 1 supplies version low byte
+  hid[slot].sendReport(0x01, p, sizeof p);
+}
+static void sendSdlState(uint8_t slot){
+  uint8_t p[47]; memset(p,0,sizeof p);        // report id 1 + payload => ValveInReportHeader starts 01 00 01 2C
+  p[0]=0x00; p[1]=0x01; p[2]=44;              // version high, type=controller state, payload length
+  put32(p+3, ++g_sdlPacketNum);
+  put64(p+7, sdlButtons(g_in.buttons));
+  put16(p+15, g_in.lpx); put16(p+17, g_in.lpy);
+  put16(p+19, g_in.rpx); put16(p+21, g_in.rpy);
+  putu16(p+23, sdlTrigger(g_in.lt)); putu16(p+25, sdlTrigger(g_in.rt));
+  put16(p+27, g_in.ax); put16(p+29, g_in.ay); put16(p+31, g_in.az);
+  put16(p+33, g_in.gx); put16(p+35, g_in.gy); put16(p+37, g_in.gz);
+  hid[slot].sendReport(0x01, p, sizeof p);
+}
+static void sendSdlCompatReports(bool state){
+  if(g_usbMode != MODE_STEAM) return;
+  if(g_connSlot < 0 || g_connSlot >= NSLOT || !hid[g_connSlot].ready()) return;
+  if(state) sendSdlState((uint8_t)g_connSlot);
+  else sendSdlWirelessEvent((uint8_t)g_connSlot, true);
+}
 // Right after the host resumes from suspend, MUTE input forwarding briefly. Otherwise the controller's input
 // in that instant (a trackpad click/trigger, or residual button state from the wake gesture) gets forwarded
 // as a real click/keypress into the just-woken desktop -- which was activating the highlighted Start tile
@@ -177,6 +221,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type, uint8_t con
     case 0xB4:    // connection/version state per slot: value 0x02 = controller connected, 0x01 = not
       S.resp[0] = 0xB4; S.resp[1] = 0x01;
       S.resp[2] = (slot == g_connSlot && !g_xbox && (millis() - g_connReplyMs < 500)) ? 0x02 : 0x01;
+      if (g_usbMode == MODE_STEAM && S.resp[2] == 0x02 && hid[slot].ready()) sendSdlWirelessEvent((uint8_t)slot, true);
       S.resp_len = 63; break;
     case 0xAD:
       g_pairing = (pln > 0 && pl[0] != 0); Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
@@ -215,15 +260,19 @@ static setcb_t SETCB[NSLOT] = { setcb0, setcb1, setcb2, setcb3 };
 
 // ===================== IController =====================
 void SteamPuckController::begin(){
-  USBDevice.setID(0x28DE, 0x1304);
+  // SDL's Steam HIDAPI driver recognizes Valve's original wireless dongle PID (0x1142) as the 4-slot Steam
+  // Controller receiver. The newer puck PID (0x1304) works for Steam, but many SDL builds don't route it
+  // through the Steam Controller driver, so apps never see a joystick. Keep the puck protocol/descriptor, but
+  // wear the classic dongle USB identity for broad SDL compatibility.
+  USBDevice.setID(0x28DE, 0x1142);
   // Distinct bcdDevice so Windows keys a FRESH usbflags entry (cache is VID:PID:bcdDevice) and actually runs
   // MS OS 2.0 / WinUSB binding for the WebUSB vendor interface -- instead of reusing a stale "no WinUSB" entry
-  // tied to the real Steam Controller (28DE:1304), which has no WebUSB interface. The normal (wake-mouse) and
+  // tied to the real Valve dongle, which has no WebUSB interface. The normal (wake-mouse) and
   // one-shot debug (CDC) boots present DIFFERENT interface sets, so they use DIFFERENT bcdDevice values --
   // otherwise Windows would serve one's cached descriptor for the other when you reboot between them.
   USBDevice.setDeviceVersion(g_debugCdcThisBoot ? 0x0212 : (g_usbMode == MODE_LIZARD ? 0x0213 : 0x0211));
   USBDevice.setManufacturerDescriptor("Valve Software");
-  USBDevice.setProductDescriptor("Steam Controller Puck");
+  USBDevice.setProductDescriptor("Steam Controller Dongle");
   const uint8_t* desc = (g_usbMode == MODE_LIZARD) ? PUCK_LIZARD_HID_DESC : PUCK_HID_DESC;
   const uint16_t descLen = (g_usbMode == MODE_LIZARD) ? sizeof PUCK_LIZARD_HID_DESC : sizeof PUCK_HID_DESC;
   for (int i = 0; i < NSLOT; i++) {
@@ -255,6 +304,7 @@ void SteamPuckController::onReport45(const uint8_t* rep, bool fresh, uint8_t bod
     // Steam's velocity/smoothing stair-step). g_fwdNewOnly toggles for A/B.
     if((fresh || !g_fwdNewOnly) && g_slot[g_connSlot].used && hid[g_connSlot].ready())
       hid[g_connSlot].sendReport(0x45, rep+1, blen);   // Steam/SDL: input report -> "connected"
+    if(fresh) sendSdlCompatReports(true);
   }
 }
 
@@ -306,8 +356,13 @@ void SteamPuckController::task(){
     wasSusp=susp; }
   if (USBDevice.suspended()) return;   // no periodic 0x79/0x7B while the host sleeps -- those sends can wake it too
   static bool usbConn=false; static unsigned long last79=0, last7B=0, connEdgeMs=0;
+  static unsigned long lastSdlCompat=0;
   bool conn = (g_connSlot>=0 && millis()-g_connReplyMs < 300);
   if (g_connSlot>=0 && g_connSlot<NSLOT && hid[g_connSlot].ready()){
+    if(conn && millis()-lastSdlCompat>=250){
+      sendSdlCompatReports(false);
+      lastSdlCompat=millis();
+    }
     // 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
     // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; the old
     // unconditional forever-resend could re-trigger Steam's connect handling (connect chime) every 750ms
@@ -317,6 +372,7 @@ void SteamPuckController::task(){
     if (conn!=usbConn || (conn && !steamAcked && millis()-last79>=750)){
       if (conn && !usbConn) connEdgeMs = millis();
       uint8_t st=conn?0x02:0x01; hid[g_connSlot].sendReport(0x79,&st,1); usbConn=conn; last79=millis();
+      if(conn) sendSdlCompatReports(false);
     } else if (conn && millis()-last7B>=2000){
       // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed dBm
       // (capture showed 0xDD = -35; replaying it verbatim is why Steam pinned -35dBm forever) -- patch in the
@@ -335,6 +391,7 @@ void SteamPuckController::task(){
         s7b[8]=(uint8_t)(0u-(uint8_t)mag);
       }
       hid[g_connSlot].sendReport(0x7B,s7b,12); last7B=millis();
+      sendSdlCompatReports(false);
     }
   } else if(!conn) usbConn=false;
 }

+ 1 - 1
OpenPuck/puck_hid.h

@@ -1,6 +1,6 @@
 // puck_hid.h -- the Steam Controller puck personality (MODE_STEAM + MODE_LIZARD).
 //
-// This is the controller that wears Valve's identity (28DE:1304) and exposes the four puck HID slot interfaces
+// This is the controller that wears Valve's Steam Controller dongle identity and exposes the four puck HID slot interfaces
 // (interface N owns bond slot N). It implements:
 //   - the puck feature COMMAND CHANNEL (handleSet/handleGet): reports 0x83/0xAE/0xB4/0xAD/0xA2/0xA3 the host
 //     uses to read attributes/serials and read/write/clear bond slots, plus the host->controller haptic relay

+ 2 - 2
docs/PROTOCOL.md

@@ -30,7 +30,7 @@ An empty slot is all zeroes.
 
 ### 3.1 Steam-mode USB layout
 
-Steam mode enumerates as Valve `28DE:1304` and exposes four HID interfaces with the same report descriptor as the real puck. Each interface owns one bond slot.
+Steam mode enumerates as Valve `28DE:1142` and exposes four HID interfaces with the same report descriptor as the real puck. Each interface owns one bond slot. The firmware uses the classic Steam Controller dongle PID because SDL's Steam HIDAPI driver reliably treats `0x1142` as the four-slot wireless receiver; newer puck PIDs may work in Steam but are not consistently surfaced as Steam Controllers to SDL apps. SDL also expects the Steam slot HIDs on USB interface numbers 1..4, so WebUSB is registered before the slot HIDs in puck mode to reserve interface 0.
 
 Relevant report IDs:
 
@@ -320,7 +320,7 @@ does a `detach -> rebuild -> attach` so the host re-reads the descriptor cleanly
 
 ### 9.1 Steam mode
 
-- VID:PID `28DE:1304`
+- VID:PID `28DE:1142`
 - Four puck HID interfaces (CDC + WebUSB also present)
 - `0x45` reports are forwarded to the connected slot's HID interface
 - **Seamless lizard**: when Steam is driving the device, `0x45` is forwarded; when it isn't