Преглед на файлове

Merge upstream main into Switch 2 Pro branch

Edgar преди 1 месец
родител
ревизия
cafaba6eae
променени са 2 файла, в които са добавени 88 реда и са изтрити 36 реда
  1. 1 0
      README.md
  2. 87 36
      ReversePuck/scpair.py

+ 1 - 0
README.md

@@ -108,6 +108,7 @@ You can copy configurations between OpenPucks using the export/import card in th
 - [BOT-Yanni](https://www.reddit.com/user/BOT-Yanni/) designed a slim case with OpenPuck written on it and a cute little glyph of the Steam Controller. You can find that [here](https://www.thingiverse.com/thing:7379316).
 - [StonnedModder](https://www.printables.com/model/1760684-openpuck-promicro-nrf52840-case) built a case meant to accomodate a USB C to USB A adapter which you can find [here](https://www.printables.com/model/1760684-openpuck-promicro-nrf52840-case).
 - Another plain case for these pro micros can be found [here](https://www.printables.com/model/1285346-pro-micro-nicenano-nrf52840-dongle-case/collections).
+- Possibly [the cutest case](https://www.printables.com/model/1830063-openpuck-case-pro-micro-nrf52840) by TheMeanCanEHdian
 
 # ReversePuck
 This is a tool to emulate a Steam Controller 2 with almost all of its inputs (except grip) using a Steam Deck and allow it to connect over a low latency 2.4ghz connection to OpenPuck (this does not work with the official puck yet). Flash the firmware onto an NRF52840 Pro Micro and copy over the ReversePuck folder onto the Steam Deck (you might need to install UV). Then add the ReversePuck script as a non steam game. Attach both the dongle and OpenPuck to the same machine and do pairing through Steam (which will say pairing has failed but it's actually fine). Then connect the OpenPuck to whatever machine you want to use your controllers on and the dongle to the Steam Deck. Launch the ReversePuck app in gamemode and you'll see the serial number of the OpenPuck show up in green. Press it and it'll turn blue at which point the Deck will show up as a controller on the host device. See video below for a demonstration of this in action (yes, other Steam Controllers can be connected to the same OpenPuck at the same time).

+ 87 - 36
ReversePuck/scpair.py

@@ -151,19 +151,52 @@ class HidDev:
     def get_feature(self, rid, max_len=64):
         buf = bytearray(max_len)
         buf[0] = rid
-        try: 
-            fcntl.ioctl(self.fd, _IOC_GF(max_len), buf, True)
-        except: 
-            # wireless connections sometimes take two attempts ...
-            time.sleep(0.05)
-            fcntl.ioctl(self.fd, _IOC_GF(max_len), buf, True)
-        return bytes(buf)
+        attempts = 2
+        while attempts > 0:
+            try: 
+                fcntl.ioctl(self.fd, _IOC_GF(max_len), buf, True)
+                return bytes(buf)
+            except: 
+                # wireless connections sometimes take two attempts ...
+                time.sleep(0.02)
+                attempts = attempts - 1
+        return False
+
+    def set_get_feature(self, rid, cmd, payload=b"", max_len=64):
+        attempts = 5
+        while attempts > 0:
+            self.set_feature(rid, cmd, payload)
+            r = self.get_feature(rid, max_len)
+
+            if isinstance(r, bool):
+                # set_get failed, wait and try again
+                time.sleep(0.03)
+                attempts = attempts - 1
+                continue
+
+            if r[0] != rid or r[1] != cmd: 
+                # Controller responded to a different request. 
+                # This can happen if it disconnects and reconnects mid-request, 
+                # or if other tools like Steam interact with the controller
+                # at the same time. 
+                time.sleep(0.03)
+                attempts = attempts - 1
+                continue
+
+            return r
+
+        return None
+
 
     def read_attribute_values(self, rid, cmd):
-        self.set_feature(rid, cmd)
-        data = self.get_feature(rid, 256)   # Feature report with attributes can be longer than 64.
+        # Feature report with attributes can be longer than 64.
+        data = self.set_get_feature(rid, cmd, max_len=256)
+
+        if data is None: 
+            return None
 
         data = data[3:]
+
         attribute_count = len(data) // 5
         padding_len = len(data) - (attribute_count * 5)
 
@@ -193,7 +226,10 @@ class HidDev:
                 # Valve device uses a BCD other than 2. Maybe a prototype?
                 attrs = self.read_attribute_values(1, 0xa6)
 
-        return attrs[4]
+        try: 
+            return attrs[4]
+        except:
+            return None
 
     def read_serial(self, force_esb = False, index = 1):
         # When sent to a Puck, force_esb=False will return the puck's serial, 
@@ -209,19 +245,16 @@ class HidDev:
         r = None
 
         if self.pid in CTRL_PIDS or force_esb:
-            self.set_feature(1, 0xae, bytes([index]))
-            r = self.get_feature(1)
+            r = self.set_get_feature(1, 0xae, bytes([index]))
               
         elif self.pid in PUCK_PIDS:
             if self.get_bcd_version() == 2:
-                # 2 = Valve Puck or Machine, 211-213 OpenPuck? Unclear why.
-                self.set_feature(2, 0xae, bytes([index]))
-                r = self.get_feature(2)
+                # 2 = Valve Puck or Machine
+                r = self.set_get_feature(2, 0xae, bytes([index]))
             else: 
                 # I wonder which official Valve device uses 
                 # a BCD other than 2. Maybe a prototype?
-                self.set_feature(1, 0xa4, bytes([index]))
-                r = self.get_feature(1)
+                r = self.set_get_feature(1, 0xa4, bytes([index]))
 
         if r is None: 
             return None
@@ -273,16 +306,22 @@ def enumerate_devices():
         # If we're here, this is a Puck. Let's detect and display its type.
         # OpenPuck can be detected by the OpenPuck mouse wake interface. 
 
+        # Check device type:
         devnode = iface_dir.parent
+        isOpenpuck = False
         device_vendor = "Valve Puck"
 
+        for subnode in glob.glob(str(devnode) + "/" + devnode.name + ":*/interface"):
+            txt = Path(subnode).read_text().strip()
+            if "OpenPuck" in txt:
+                isOpenpuck = True
+
         if pid == 0x1305:
-            device_vendor = "Valve Steam Machine"
-        else:
-            for subnode in glob.glob(str(devnode) + "/" + devnode.name + ":*/interface"):
-                txt = Path(subnode).read_text().strip()
-                if "OpenPuck" in txt:
-                    device_vendor = "OpenPuck"
+            device_vendor = "Steam Machine"
+            if isOpenpuck:
+                device_vendor = "OpenPuck internal"
+        elif isOpenpuck:
+            device_vendor = "OpenPuck"
 
         try:
             dev = HidDev(node, vid, pid, device_vendor)
@@ -355,16 +394,15 @@ def read_slots(puck_list):
         for esb in esb_devices:
             try:
 
-                esb.set_feature(2, 0xA3)
-                r = esb.get_feature(2)  # [02 A3 18 <8 uuid><16 serial>]
+                r = esb.set_get_feature(2, 0xA3) # [02 A3 18 <8 uuid><16 serial>]
+
                 rec = r[3:3 + 24]
                 used = any(rec[0:8])
                 serial = rec[8:24].split(b"\x00")[0].decode("latin1", "replace") if used else ""
                 key = rec[:8].hex()
     
                 # Check if a controller is currently connected to this slot:
-                esb.set_feature(2, 0xB4)
-                r = esb.get_feature(2)
+                r = esb.set_get_feature(2, 0xB4)
                 active = (r[0] == 0x02 and r[1] == 0xb4 and r[3] == 2)
     
                 slots.append({"idx": idx, "dev": esb, "used": used, "active": active, "serial": serial, "rec": rec})
@@ -597,21 +635,32 @@ def write_controller_slot(controller_serial=None, slot=0, puck_serial=None, pkey
 
 def get_single_controller_pairings(controller_dev):
 
+    # Check which of the two controller slots is currently selected:
+    bondslot = controller_dev.set_get_feature(1, 0xED, b"user/wireless_transport\x00")
+    active_controller_slot = None
+    if bondslot[0] == 0x01 and bondslot[1] == 0xed and bondslot[2] == 1:
+        if bondslot[3] == 2:
+            # Connected to ESB slot 0
+            active_controller_slot = 0
+        elif bondslot[3] == 3:
+            active_controller_slot = 1
+
     slots = []
-    i = 0
+    idx = 0
     for bond_str in [b"esb/bond\x00", b"esb/bond_2\x00"]:
-        controller_dev.set_feature(1, 0xED, bond_str)
-        bond = controller_dev.get_feature(1)
+        bond = controller_dev.set_get_feature(1, 0xED, bond_str)
+        if bond is None: 
+            return None
 
         if bond[0] == 0x01 and bond[1] == 0xed and bond[2] > 0:
             bond_len = bond[2]
             bond_key = bond[3:3+8]
-            bond_name = bond[11:11+(bond_len-8)]
-            slots.append({"idx": i, "used": True, "key": bond_key.hex(), "serial": bond_name.decode("latin1", "replace").split('\x00')[0]})
+            bond_name = bond[11:11+(bond_len-8)].split(b'\x00')[0].decode("latin1", "replace")
+            slots.append({"idx": idx, "used": True, "active": (active_controller_slot == idx), "serial": bond_name, "key": bond_key.hex()})
         else: 
-            slots.append({"idx": i, "used": False})
+            slots.append({"idx": idx, "used": False, "active": (active_controller_slot == idx)})
 
-        i = i + 1
+        idx = idx + 1
 
     return {"serial": controller_dev.read_serial(True), 
             "firmware_ts": controller_dev.read_firmware_version(True), 
@@ -730,6 +779,8 @@ def cmd_list():
                 continue
 
             cdata = get_single_controller_pairings(c)
+            if cdata is None: 
+                continue
 
             firmware_str = hex(cdata['firmware_ts']).replace('0x', '').upper() + " (" + cdata['firmware_commit']  + ")"
         
@@ -737,9 +788,9 @@ def cmd_list():
 
             for s in cdata['slots']:
                 if s['used']:
-                    print("  slot %d: %s (key %s)" % (s["idx"], s["serial"], s["key"]))
+                    print("  slot %d: %s (key %s)%s" % (s["idx"], s["serial"], s["key"], (', selected' if s['active'] else '')))
                 else:
-                    print("  slot %d: Empty" % (s["idx"]))
+                    print("  slot %d: Empty%s" % (s["idx"], (', selected' if s['active'] else '')))
 
             print()