scpair.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. #!/usr/bin/env python3
  2. """scpair.py -- Linux hidraw steamless pairing for the Steam Controller 2 puck + (emulated) controller.
  3. Original code by safijari
  4. Steam Machine, slot cloning and wireless controller support, firmware version query by Leseratte10.
  5. The Linux equivalent of pairtui/scmd.c (which is macOS/IOKit). Talks the Valve feature-report command
  6. channel on the usagePage-0x01 control interface over /dev/hidraw* (HIDIOCSFEATURE / HIDIOCGFEATURE).
  7. Used to pair on the Steam Deck itself when BOTH the puck (PID 1304) and the ReversePuck controller
  8. (PID 1302) are plugged into the Deck -- or run it on any Linux box.
  9. This code is not limited to the ReversePuck, it can also be used to manage, reset, pair and clone
  10. normal pairings between a Puck (or OpenPuck) and a Steam Controller.
  11. Pairing flow (protocol/USB_COMMANDS.md, == pairtui.pair_full):
  12. 1. fresh 8-byte key (r1,r2)
  13. 2. puck slot N (rid 2): 0xA2 [r1][r2][controller_serial 16]
  14. 3. ctrl (rid 1): 0xEE ["esb/bond"\\0][r1][r2][puck_serial 16] then 0xEF ["esb/bond"\\0]
  15. 4. ctrl (rid 1): 0x95 52 AF 27 A4 (reboot into wireless)
  16. sudo python3 scpair.py list
  17. sudo python3 scpair.py pair [--slot N]
  18. sudo python3 scpair.py unpair --slot N
  19. Needs read/write on /dev/hidraw* (run with sudo or install the udev rule in README.md).
  20. """
  21. import argparse
  22. import array
  23. import fcntl
  24. import glob
  25. import os
  26. import struct
  27. import sys
  28. import time
  29. from pathlib import Path
  30. # ---- Linux ioctl numbers for hidraw (asm-generic _IOC) ----
  31. _IOC_NONE, _IOC_WRITE, _IOC_READ = 0, 1, 2
  32. def _IOC(d, t, nr, size):
  33. return (d << 30) | (size << 16) | (ord(t) << 8) | nr
  34. def _IOR(t, nr, size):
  35. return _IOC(_IOC_READ, t, nr, size)
  36. def _IOC_SF(length):
  37. return _IOC(_IOC_READ | _IOC_WRITE, 'H', 0x06, length) # HIDIOCSFEATURE
  38. def _IOC_GF(length):
  39. return _IOC(_IOC_READ | _IOC_WRITE, 'H', 0x07, length) # HIDIOCGFEATURE
  40. HIDIOCGRDESCSIZE = _IOR('H', 0x01, 4)
  41. # HIDIOCGRDESC: struct hidraw_report_descriptor { __u32 size; __u8 value[4096]; }
  42. HIDIOCGRDESC = _IOR('H', 0x02, 4 + 4096)
  43. VALVE_VID = 0x28DE
  44. PUCK_PIDS = (0x1304, 0x1305)
  45. CTRL_PIDS = (0x1302, 0x1301, 0x1303, 0x1205)
  46. def _sysfs_ids(node):
  47. """Return (vid, pid) for a /dev/hidrawN node, or (None, None)."""
  48. name = os.path.basename(node)
  49. try:
  50. # /sys/class/hidraw/hidrawN/device/uevent has HID_ID=0003:000028DE:00001304
  51. ue = open("/sys/class/hidraw/%s/device/uevent" % name).read()
  52. except Exception:
  53. return (None, None)
  54. vid = None
  55. pid = None
  56. for line in ue.splitlines():
  57. if line.startswith("HID_ID="):
  58. parts = line.split(":")
  59. try:
  60. vid = int(parts[1], 16) & 0xFFFF
  61. pid = int(parts[2], 16) & 0xFFFF
  62. return (vid, pid)
  63. except:
  64. pass
  65. return (None, None)
  66. def _first_usage_page(fd):
  67. """Parse the report descriptor's first Usage Page item. 0x01=generic desktop (control), 0xFF00=vendor."""
  68. try:
  69. size = array.array('i', [0])
  70. fcntl.ioctl(fd, HIDIOCGRDESCSIZE, size, True)
  71. buf = bytearray(4 + 4096)
  72. struct.pack_into("<I", buf, 0, size[0])
  73. fcntl.ioctl(fd, HIDIOCGRDESC, buf, True)
  74. n = struct.unpack_from("<I", buf, 0)[0]
  75. d = buf[4:4 + n]
  76. i = 0
  77. while i < len(d):
  78. b = d[i]
  79. if b == 0x05: # Usage Page, 1-byte
  80. return d[i + 1]
  81. if b == 0x06: # Usage Page, 2-byte
  82. return d[i + 1] | (d[i + 2] << 8)
  83. # skip item by its size (low 2 bits)
  84. i += 1 + (b & 0x03)
  85. except Exception:
  86. pass
  87. return None
  88. class HidDev:
  89. def __init__(self, node, vid, pid, info_str = "Valve"):
  90. self.node = node
  91. self.vid = vid
  92. self.pid = pid
  93. self.info_str = info_str
  94. self.fd = os.open(node, os.O_RDWR)
  95. self.usage_page = _first_usage_page(self.fd)
  96. def close(self):
  97. try:
  98. os.close(self.fd)
  99. except Exception:
  100. pass
  101. def get_bcd_version(self):
  102. """
  103. Relevant for Pucks. Both the standalone Puck and the receiver in the Steam Machine
  104. use bcdVersion 2, but Steam also supports Pucks with other versions that sometimes
  105. behave differently.
  106. """
  107. dxi = self.node.replace('/dev/', '')
  108. base = Path(f"/sys/class/hidraw/{dxi}/device")
  109. dev_dir = base.resolve().parent.parent
  110. bcdDevice = int((dev_dir / "bcdDevice").read_text().strip(), 16)
  111. return bcdDevice
  112. def set_feature(self, rid, cmd, payload=b""):
  113. buf = bytearray(64)
  114. buf[0] = rid
  115. buf[1] = cmd
  116. buf[2] = len(payload)
  117. buf[3:3 + len(payload)] = bytes(payload)
  118. fcntl.ioctl(self.fd, _IOC_SF(64), buf, True)
  119. def get_feature(self, rid, max_len=64):
  120. buf = bytearray(max_len)
  121. buf[0] = rid
  122. try:
  123. fcntl.ioctl(self.fd, _IOC_GF(max_len), buf, True)
  124. except:
  125. # wireless connections sometimes take two attempts ...
  126. time.sleep(0.05)
  127. fcntl.ioctl(self.fd, _IOC_GF(max_len), buf, True)
  128. return bytes(buf)
  129. def read_attribute_values(self, rid, cmd):
  130. self.set_feature(rid, cmd)
  131. data = self.get_feature(rid, 256) # Feature report with attributes can be longer than 64.
  132. data = data[3:]
  133. attribute_count = len(data) // 5
  134. padding_len = len(data) - (attribute_count * 5)
  135. fstr = '<' + 'BL' * attribute_count + 'B' * padding_len
  136. unpacked = struct.unpack(fstr, data)
  137. attribute_list = {}
  138. for i in range(attribute_count):
  139. tag = unpacked[2*i]
  140. val = unpacked[2*i + 1]
  141. attribute_list[tag] = val
  142. return attribute_list
  143. def read_firmware_version(self, force_esb = False):
  144. # When sent to a Puck, force_esb=False will return the puck's firmware,
  145. # while force_esb=True will return the connected controller's firmware.
  146. if self.pid in CTRL_PIDS or force_esb:
  147. attrs = self.read_attribute_values(1, 0x83)
  148. elif self.pid in PUCK_PIDS:
  149. if self.get_bcd_version() in [2, 0x212, 0x213, 0x211]:
  150. # 2 = Valve Puck or Machine, 211-213 OpenPuck? Unclear why.
  151. attrs = self.read_attribute_values(2, 0x83)
  152. else:
  153. # I wonder which official
  154. # Valve device uses a BCD other than 2. Maybe a prototype?
  155. attrs = self.read_attribute_values(1, 0xa6)
  156. return attrs[4]
  157. def read_serial(self, force_esb = False, index = 1):
  158. # When sent to a Puck, force_esb=False will return the puck's serial,
  159. # while force_esb=True will return the connected controller's serial.
  160. # Index:
  161. # 0: MXA123123123A ??
  162. # 1: FXB9912345678 (serial number on sticker)
  163. # 2: NA
  164. # 3: fec01234c8af git firmware commit hash
  165. # 4: MXA123123123A
  166. r = None
  167. if self.pid in CTRL_PIDS or force_esb:
  168. self.set_feature(1, 0xae, bytes([index]))
  169. r = self.get_feature(1)
  170. elif self.pid in PUCK_PIDS:
  171. if self.get_bcd_version() in [2, 0x212, 0x213, 0x211]:
  172. # 2 = Valve Puck or Machine, 211-213 OpenPuck? Unclear why.
  173. self.set_feature(2, 0xae, bytes([index]))
  174. r = self.get_feature(2)
  175. else:
  176. # I wonder which official Valve device uses
  177. # a BCD other than 2. Maybe a prototype?
  178. self.set_feature(1, 0xa4, bytes([index]))
  179. r = self.get_feature(1)
  180. if r is None:
  181. return None
  182. return r[4:].split(b"\x00")[0].decode("latin1", "replace")
  183. def nodesort(nodeobj):
  184. """Sorts the node path properly (numerically)."""
  185. # TODO: This doesn't seem to be 100% reliable, if you plug in two Pucks at the exact same time
  186. # the interface order might get messed up.
  187. if isinstance(nodeobj, HidDev):
  188. return nodesort(nodeobj.node)
  189. if isinstance(nodeobj, str):
  190. return int(nodeobj.replace("/dev/hidraw", ""))
  191. def enumerate_devices():
  192. """Return dict role -> list[HidDev]: {'puck':[...], 'ctrl':[...]}."""
  193. out = {"puck": [], "ctrl": []}
  194. last_serial = None
  195. current_ctrl_iface = None
  196. current_esb_ifaces = []
  197. for node in sorted(glob.glob("/dev/hidraw*"), key=nodesort):
  198. vid, pid = _sysfs_ids(node)
  199. if vid != VALVE_VID:
  200. continue
  201. # Check if this hidraw object is actually a puck slot, or if it's a OpenPuck Wake mouse or similar
  202. dxi = node.replace('/dev/', '')
  203. base = Path(f"/sys/class/hidraw/{dxi}/device")
  204. iface_dir = base.resolve().parent
  205. bInterfaceClass = int((iface_dir / "bInterfaceClass").read_text().strip(), 16)
  206. bInterfaceSubClass = int((iface_dir / "bInterfaceSubClass").read_text().strip(), 16)
  207. bInterfaceProtocol = int((iface_dir / "bInterfaceProtocol").read_text().strip(), 16)
  208. # A real puck slot has these values:
  209. if (bInterfaceClass != 3 or bInterfaceSubClass != 0 or bInterfaceProtocol != 0):
  210. # If these don't match, this is not a Puck slot, but some other random HID interface.
  211. continue
  212. # If we're here, this is a Puck. Let's detect and display its type.
  213. # OpenPuck can be detected by the OpenPuck mouse wake interface.
  214. devnode = iface_dir.parent
  215. device_vendor = "Valve Puck"
  216. if pid == 0x1305:
  217. device_vendor = "Valve Steam Machine"
  218. else:
  219. for subnode in glob.glob(str(devnode) + "/" + devnode.name + ":*/interface"):
  220. txt = Path(subnode).read_text().strip()
  221. if "OpenPuck" in txt:
  222. device_vendor = "OpenPuck"
  223. try:
  224. dev = HidDev(node, vid, pid, device_vendor)
  225. except PermissionError:
  226. print("permission denied on %s (run with sudo or install the udev rule)" % node, file=sys.stderr)
  227. continue
  228. except Exception:
  229. continue
  230. if pid in CTRL_PIDS:
  231. out["ctrl"].append(dev)
  232. continue
  233. serial = dev.read_serial()
  234. if pid in PUCK_PIDS and last_serial is not None and last_serial != serial:
  235. # OpenCode has no control interface.
  236. current_puck = {
  237. "control": current_ctrl_iface,
  238. "esb": current_esb_ifaces
  239. }
  240. out["puck"].append(current_puck)
  241. current_esb_ifaces = []
  242. current_ctrl_iface = None
  243. last_serial = serial
  244. if pid in PUCK_PIDS and dev.usage_page in (0x01, None):
  245. current_esb_ifaces.append(dev)
  246. elif pid in PUCK_PIDS:
  247. current_ctrl_iface = dev
  248. elif pid in CTRL_PIDS:
  249. out["ctrl"].append(dev)
  250. else:
  251. dev.close()
  252. if len(current_esb_ifaces) > 0:
  253. # Add puck interfaces from last loop
  254. # OpenCode has no control interface.
  255. current_puck = {
  256. "control": current_ctrl_iface,
  257. "esb": current_esb_ifaces
  258. }
  259. out["puck"].append(current_puck)
  260. return out
  261. def _ser16(serial):
  262. b = serial.encode("latin1", "replace")[:16]
  263. return b + b"\x00" * (16 - len(b))
  264. def read_slots(puck_list):
  265. """Read all bond slots from all attached pucks (each puck control interface == one slot)."""
  266. pucks = []
  267. for puck in puck_list:
  268. esb_devices = puck['esb']
  269. slots = []
  270. idx = 0
  271. for esb in esb_devices:
  272. try:
  273. esb.set_feature(2, 0xA3)
  274. r = esb.get_feature(2) # [02 A3 18 <8 uuid><16 serial>]
  275. rec = r[3:3 + 24]
  276. used = any(rec[0:8])
  277. serial = rec[8:24].split(b"\x00")[0].decode("latin1", "replace") if used else ""
  278. key = rec[:8].hex()
  279. # Check if a controller is currently connected to this slot:
  280. esb.set_feature(2, 0xB4)
  281. r = esb.get_feature(2)
  282. active = (r[0] == 0x02 and r[1] == 0xb4 and r[3] == 2)
  283. slots.append({"idx": idx, "dev": esb, "used": used, "active": active, "serial": serial, "rec": rec})
  284. except Exception as ex:
  285. slots.append({"idx": idx, "dev": esb, "used": False, "active": False, "serial": "", "rec": b"", "err": str(ex)})
  286. idx = idx + 1
  287. if esb is not None:
  288. # This is a new puck, append the old puck and all its slots to the list.
  289. pucks.append({"serial": esb.read_serial(),
  290. "firmware_ts": esb.read_firmware_version(),
  291. "firmware_commit": esb.read_serial(False, 3),
  292. "slots": slots,
  293. "info_str": esb.info_str})
  294. return pucks
  295. def get_puck_slot_for_writing(puck_serial=None, slot=None):
  296. devs = enumerate_devices()
  297. pucks = devs["puck"]
  298. if not pucks:
  299. raise SystemExit("no puck (28DE:1304/1305) control interface found")
  300. puck_objects = read_slots(pucks)
  301. if puck_serial is None and len(puck_objects) > 1:
  302. raise SystemExit("Multiple Pucks connected, you need to specify its serial number")
  303. puck_object = None
  304. if puck_serial is not None:
  305. for puck in puck_objects:
  306. if puck['serial'] == puck_serial:
  307. puck_object = puck
  308. break
  309. if puck_object is None:
  310. raise SystemExit("Can't find a puck with that serial number!")
  311. else:
  312. puck_object = puck_objects[0]
  313. slots = puck_object['slots']
  314. if slot is None:
  315. slot_object = next((s for s in slots if not s["used"]), None)
  316. else:
  317. slot_object = next((s for s in slots if s["idx"] == slot), None)
  318. if slot_object is None:
  319. raise SystemExit("This Puck has all slots occupied!")
  320. return (puck_object['serial'], slot_object)
  321. def pair(puck_serial=None, puck_slot=None, controller_serial=None, controller_slot=0, puck_name=None, controller_name=None, pkey=None):
  322. devs = enumerate_devices()
  323. puck_devs = devs["puck"]
  324. pucks = []
  325. for puck in puck_devs:
  326. pucks.append(puck["esb"])
  327. ctrls = sorted(devs["ctrl"], key=nodesort)
  328. if not pucks:
  329. raise SystemExit("no puck (28DE:1304/1305) control interface found")
  330. ctrl = None
  331. if controller_serial is not None:
  332. # Check controllers connected through USB
  333. for c in ctrls:
  334. if c.read_serial() == controller_serial:
  335. print(f"Found controller {controller_serial} connected over USB")
  336. ctrl = c
  337. if ctrl is None:
  338. # Check controllers connected wirelessly
  339. for puck in pucks:
  340. for slot in puck:
  341. try:
  342. if slot.read_serial(True) == controller_serial:
  343. print(f"Found controller {controller_serial} connected wirelessly")
  344. ctrl = puck
  345. except BrokenPipeError:
  346. pass
  347. else:
  348. # Controller serial is none. Assume the user wants to use the 1st wired controller.
  349. if not ctrls:
  350. raise SystemExit("no controller (28DE:1302) control interface found - please plug in either a Steam Controller or the ReversePuck controller")
  351. ctrl = ctrls[0]
  352. if ctrl is None:
  353. raise SystemExit(f"Controller with serial {controller_serial} not found.")
  354. if controller_name is None:
  355. ctrl_serial_str = ctrl.read_serial(True)
  356. else:
  357. ctrl_serial_str = controller_name[:15]
  358. puck_serial, slot_obj = get_puck_slot_for_writing(puck_serial, puck_slot)
  359. if puck_name is None:
  360. puck_serial_str = puck_serial
  361. else:
  362. puck_serial_str = puck_name[:15]
  363. if pkey is None:
  364. r = os.urandom(8)
  365. else:
  366. r = pkey
  367. key = b"esb/bond\x00" if controller_slot == 0 else b"esb/bond_2\x00"
  368. print("Pairing key: %s" % (r.hex()))
  369. # Puck name gets written to controller (defaults to puck serial).
  370. # Controller name gets written to Puck
  371. print("Puck: %s slot %d controller %s" % (puck_serial, slot_obj['idx'], ctrl_serial_str))
  372. print("Controller: %s slot %d puck %s" % (ctrl.read_serial(True), controller_slot, puck_serial_str))
  373. # puck slot: 0xA2 [r1 r2][ctrl serial]
  374. slot_obj['dev'].set_feature(2, 0xA2, r + _ser16(ctrl_serial_str))
  375. # controller esb/bond: 0xEE [key\0][r1 r2][puck serial] + 0xEF [key\0]
  376. ctrl.set_feature(1, 0xEE, key + r + _ser16(puck_serial_str))
  377. ctrl.set_feature(1, 0xEF, key)
  378. time.sleep(0.1)
  379. # reboot controller into wireless mode (magic 0xA427AF52)
  380. ctrl.set_feature(1, 0x95, bytes([0x52, 0xAF, 0x27, 0xA4]))
  381. print("paired. controller rebooting into wireless — move the puck to your host.")
  382. def write_puck_slot(puck_serial=None, puck_slot=None, ctrl_name=None, pkey=None):
  383. puck_serial, slot_obj = get_puck_slot_for_writing(puck_serial, puck_slot)
  384. if ctrl_name is None and pkey is None:
  385. print("wiping puck %s slot %d" % (puck_serial, slot_obj['idx']))
  386. slot_obj['dev'].set_feature(2, 0xA2, b"\x00" * 24)
  387. else:
  388. if ctrl_name is None or len(ctrl_name) > 15:
  389. raise SystemExit("Invalid controller name.")
  390. if pkey is None:
  391. raise SystemExit("No key provided!")
  392. print("writing to puck %s slot %d: controller %s key=%s" %
  393. (puck_serial, slot_obj['idx'], ctrl_name, pkey.hex()))
  394. # puck slot: 0xA2 [r1 r2][ctrl serial]
  395. print(slot_obj['dev'].node)
  396. slot_obj['dev'].set_feature(2, 0xA2, pkey + _ser16(ctrl_name))
  397. print("done")
  398. def write_controller_slot(controller_serial=None, slot=0, puck_serial=None, pkey=None):
  399. devs = enumerate_devices()
  400. puck_devs = devs["puck"]
  401. pucks = []
  402. for puck in puck_devs:
  403. pucks.append(puck["esb"])
  404. ctrls = sorted(devs["ctrl"], key=nodesort)
  405. ctrl = None
  406. if controller_serial is not None:
  407. # Check controllers connected through USB
  408. for c in ctrls:
  409. if c.read_serial() == controller_serial:
  410. print(f"Found controller {controller_serial} connected over USB")
  411. ctrl = c
  412. if ctrl is None:
  413. # Check controllers connected wirelessly
  414. for puck in pucks:
  415. for slot in puck:
  416. print(slot)
  417. try:
  418. if slot.read_serial(True) == controller_serial:
  419. print(f"Found controller {controller_serial} connected wirelessly")
  420. ctrl = puck
  421. except BrokenPipeError:
  422. pass
  423. else:
  424. # Controller serial is none. Assume the user wants to use the 1st wired controller.
  425. if not ctrls:
  426. raise SystemExit("no controller (28DE:1302) control interface found - please plug in a Steam Controller")
  427. ctrl = ctrls[0]
  428. if ctrl is None:
  429. raise SystemExit(f"Controller with serial {controller_serial} not found.")
  430. if pkey is None:
  431. r = os.urandom(8)
  432. else:
  433. r = pkey
  434. key = b"esb/bond\x00" if slot == 0 else b"esb/bond_2\x00"
  435. if puck_serial is None and pkey is None:
  436. print("Wiping controller slot %d" % (slot))
  437. try:
  438. ctrl.set_feature(1, 0xEE, key + b"\x00" * 24)
  439. ctrl.set_feature(1, 0xEF, key)
  440. print("Slot wiped.")
  441. except Exception:
  442. print("Slot wipe failed.")
  443. else:
  444. print("Writing controller slot %d: puck serial %s key=%s" %
  445. (slot, puck_serial, r.hex()))
  446. # controller esb/bond: 0xEE [key\0][r1 r2][puck serial] + 0xEF [key\0]
  447. ctrl.set_feature(1, 0xEE, key + r + _ser16(puck_serial))
  448. ctrl.set_feature(1, 0xEF, key)
  449. time.sleep(0.1)
  450. # reboot controller into wireless mode (magic 0xA427AF52)
  451. ctrl.set_feature(1, 0x95, bytes([0x52, 0xAF, 0x27, 0xA4]))
  452. print("paired. controller rebooting into wireless — move the puck to your host.")
  453. def get_single_controller_pairings(controller_dev):
  454. slots = []
  455. i = 0
  456. for bond_str in [b"esb/bond\x00", b"esb/bond_2\x00"]:
  457. controller_dev.set_feature(1, 0xED, bond_str)
  458. bond = controller_dev.get_feature(1)
  459. if bond[0] == 0x01 and bond[1] == 0xed and bond[2] > 0:
  460. bond_len = bond[2]
  461. bond_key = bond[3:3+8]
  462. bond_name = bond[11:11+(bond_len-8)]
  463. slots.append({"idx": i, "used": True, "key": bond_key.hex(), "serial": bond_name.decode("latin1", "replace").split('\x00')[0]})
  464. else:
  465. slots.append({"idx": i, "used": False})
  466. i = i + 1
  467. return {"serial": controller_dev.read_serial(True),
  468. "firmware_ts": controller_dev.read_firmware_version(True),
  469. "firmware_commit": controller_dev.read_serial(True, 3),
  470. "slots": slots}
  471. def cmd_reboot(device_serial: str, reboot_type: int):
  472. """
  473. type:
  474. These types don't correspond to anything in the protocol, these are just flags used for this function.
  475. 1: reboot (controller only)
  476. 2: reboot to bootloader (puck or controller)
  477. 3: reboot to wireless connection (controller only)
  478. 4: power off (controller only)
  479. """
  480. devs = enumerate_devices()
  481. puck_devs = sorted(devs["puck"], key=nodesort)
  482. ctrls = sorted(devs["ctrl"], key=nodesort)
  483. # Check if we want to reboot a Puck
  484. for p in puck_devs:
  485. pc = p['control']
  486. if pc.read_serial() == device_serial:
  487. if reboot_type == 2:
  488. print(f"Found puck with serial {device_serial}, rebooting to bootloader...")
  489. if pc.get_bcd_version() == 2:
  490. pc.set_feature(2, 0x90)
  491. else:
  492. pc.set_feature(1, 0x90)
  493. return True
  494. else:
  495. print(f"Found puck with serial {device_serial}, but pucks can only reboot to bootloader. Skipping.")
  496. print(f"If that's what you want, run again with --bootloader.")
  497. # Check if we want to reboot a controller that's wireless.
  498. found_puck_dev = None
  499. ps = read_slots(puck_devs)
  500. for puck in ps:
  501. slots = puck['slots']
  502. for s in slots:
  503. if s["active"]:
  504. if s['dev'].read_serial(True) == device_serial:
  505. found_puck_dev = s['dev']
  506. break
  507. # Check if we want to reboot a controller that's wired.
  508. if found_puck_dev is None:
  509. for c in ctrls:
  510. if c.read_serial() == device_serial:
  511. found_puck_dev = c
  512. break
  513. if found_puck_dev is not None:
  514. if reboot_type == 1:
  515. print(f"Found controller with serial {device_serial}, rebooting...")
  516. found_puck_dev.set_feature(1, 0x95)
  517. elif reboot_type == 2:
  518. print(f"Found controller with serial {device_serial}, rebooting to bootloader...")
  519. found_puck_dev.set_feature(1, 0x90)
  520. elif reboot_type == 3:
  521. print(f"Found controller with serial {device_serial}, rebooting to wireless...")
  522. found_puck_dev.set_feature(1, 0x95, bytes([0x52, 0xAF, 0x27, 0xA4]))
  523. elif reboot_type == 4:
  524. print(f"Found controller with serial {device_serial}, powering off...")
  525. found_puck_dev.set_feature(1, 0x9F, b'off!')
  526. return True
  527. def cmd_list():
  528. devs = enumerate_devices()
  529. pucks = devs["puck"]
  530. ctrls = devs["ctrl"]
  531. ctrls_esb = []
  532. if pucks:
  533. ps = read_slots(pucks)
  534. for puck in ps:
  535. slots = puck['slots']
  536. firmware_str = hex(puck['firmware_ts']).replace('0x', '').upper() + " (" + puck['firmware_commit'] + ")"
  537. print(f"Puck: {puck['serial']} ({puck['info_str']}), firmware {firmware_str}")
  538. for s in slots:
  539. suffix = ""
  540. if s["active"]:
  541. conn_serial = s['dev'].read_serial(True)
  542. suffix = f", connected to controller " + conn_serial
  543. ctrls_esb.append(s['dev'])
  544. if s["used"]:
  545. print(" slot %d: %s (key %s)%s" % (s["idx"], s["serial"], s["rec"][:8].hex(), suffix))
  546. else:
  547. print(" slot %d: Empty" % (s["idx"]))
  548. print()
  549. else:
  550. print("No Puck connected.")
  551. if ctrls or ctrls_esb:
  552. ctype = "wired"
  553. for c in [*ctrls, None, *ctrls_esb]:
  554. if c is None:
  555. ctype = "wireless"
  556. continue
  557. cdata = get_single_controller_pairings(c)
  558. firmware_str = hex(cdata['firmware_ts']).replace('0x', '').upper() + " (" + cdata['firmware_commit'] + ")"
  559. print(f"Controller ({ctype}): {cdata['serial']}, firmware {firmware_str}")
  560. for s in cdata['slots']:
  561. if s['used']:
  562. print(" slot %d: %s (key %s)" % (s["idx"], s["serial"], s["key"]))
  563. else:
  564. print(" slot %d: Empty" % (s["idx"]))
  565. print()
  566. else:
  567. print("No controller connected.")
  568. def steam_key_check(key):
  569. try:
  570. key = bytes.fromhex(key)
  571. if len(key) == 8:
  572. return key
  573. except:
  574. raise argparse.ArgumentTypeError("Invalid pairing key!")
  575. def main():
  576. ap = argparse.ArgumentParser(description="Linux hidraw steamless pairing")
  577. sub = ap.add_subparsers(dest="cmd", required=True)
  578. sub.add_parser("list")
  579. p = sub.add_parser("pair")
  580. #def pair(puck_serial=None, puck_slot=None, controller_serial=None, controller_slot=0,
  581. # puck_name=None, controller_name=None, pkey=None)
  582. p.add_argument("--puck-serial", type=str, default=None)
  583. p.add_argument("--puck-slot", type=int, default=None)
  584. p.add_argument("--puck-name", type=str, default=None)
  585. p.add_argument("--controller-serial", type=str, default=None)
  586. p.add_argument("--controller-slot", type=int, default=0)
  587. p.add_argument("--controller-name", type=str, default=None)
  588. p.add_argument("--key", type=steam_key_check, default=None)
  589. s = sub.add_parser("write-puck")
  590. #def write_puck_slot(puck_serial=None, puck_slot=None, ctrl_name=None, pkey=None):
  591. s.add_argument("--puck-serial", type=str, default=None)
  592. s.add_argument("--puck-slot", type=int, default=None)
  593. s.add_argument("--controller-name", type=str, default=None)
  594. s.add_argument("--key", type=steam_key_check, default=None)
  595. c = sub.add_parser("write-controller")
  596. #def write_controller_slot(controller_serial=None, slot=0, puck_serial=None, pkey=None):
  597. c.add_argument("--controller-serial", type=str, default=None)
  598. c.add_argument("--controller-slot", type=int, default=None)
  599. c.add_argument("--puck-name", type=str, default=None)
  600. c.add_argument("--key", type=steam_key_check, default=None)
  601. rb = sub.add_parser("reboot")
  602. #def cmd_reboot(device_serial: str, reboot_type: int):
  603. rb.add_argument("--serial", type=str, required=True)
  604. reboot_group = rb.add_mutually_exclusive_group() # no parameters: normal reboot, type 1
  605. reboot_group.add_argument("--bootloader", action='store_true') # type 2
  606. reboot_group.add_argument("--wireless", action='store_true') # type 3
  607. reboot_group.add_argument("--poweroff", action='store_true') # type 4
  608. args = ap.parse_args()
  609. if args.cmd == "list":
  610. cmd_list()
  611. elif args.cmd == "pair":
  612. pair(args.puck_serial, args.puck_slot, args.controller_serial,
  613. args.controller_slot, args.puck_name, args.controller_name, args.key)
  614. elif args.cmd == "write-puck":
  615. write_puck_slot(args.puck_serial, args.puck_slot, args.controller_name, args.key)
  616. elif args.cmd == "write-controller":
  617. write_controller_slot(args.controller_slot, args.puck_name, args.key)
  618. elif args.cmd == "reboot":
  619. reboot_type = 1
  620. if args.bootloader:
  621. reboot_type = 2
  622. if args.wireless:
  623. reboot_type = 3
  624. if args.poweroff:
  625. reboot_type = 4
  626. cmd_reboot(args.serial, reboot_type)
  627. if __name__ == "__main__":
  628. main()