input_source.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. """input_source.py -- read the Steam Deck's built-in controls and normalize to Triton fields.
  2. TWO sources:
  3. * **DeckSource (default, libusb/pyusb)** -- the CORRECT way, modeled on ~/Work/joverlay. The Deck
  4. controller is USB 28DE:1205; Steam reads its 64-byte "DeckState" vendor report on interface 2,
  5. EP 0x83. We **detach the kernel driver from interfaces 0/1/2 and claim them** -> the controller
  6. vanishes from the kernel/Steam (true USB-level detach, so Steam stops eating Steam/QAM), and we read
  7. EP 0x83 ourselves and decode the raw report (trackpads, sticks, triggers, gyro, ALL buttons incl.
  8. Steam/QAM). Release + reattach hands it back to Steam. Needs access to the usbfs node (see setup.sh:
  9. `chmod` the /dev/bus/usb node, or run as root).
  10. * **EvdevSource (fallback, --evdev)** -- pure-stdlib evdev. Works without libusb but the Deck exposes
  11. trackpads as sticks/dpad and can't see Steam/QAM, hence the original mapping problems.
  12. DeckState layout: buttons in bytes 8-19 (the bits below are from joverlay's live captures + hid-steam.c /
  13. SDL_hidapi_steamdeck.c); analog (pads/sticks/triggers/IMU) at byte 20+. Run `python3 input_source.py`
  14. to live-dump the raw report and verify/correct the offsets on your unit.
  15. """
  16. from __future__ import annotations
  17. import fcntl
  18. import glob
  19. import os
  20. import struct
  21. # ---- Triton button masks (mirror triton.h) ----
  22. TB_A = 0x1
  23. TB_B = 0x2
  24. TB_X = 0x4
  25. TB_Y = 0x8
  26. TB_QAM = 0x10
  27. TB_R3 = 0x20
  28. TB_VIEW = 0x40
  29. TB_R4 = 0x80
  30. TB_R5 = 0x100
  31. TB_RB = 0x200
  32. TB_DDN = 0x400
  33. TB_DRT = 0x800
  34. TB_DLF = 0x1000
  35. TB_DUP = 0x2000
  36. TB_MENU = 0x4000
  37. TB_L3 = 0x8000
  38. TB_STEAM = 0x10000
  39. TB_L4 = 0x20000
  40. TB_L5 = 0x40000
  41. TB_LB = 0x80000
  42. NEUTRAL = {
  43. "buttons": 0, "lx": 0, "ly": 0, "rx": 0, "ry": 0, "lt": 0, "rt": 0,
  44. "lpx": 0, "lpy": 0, "rpx": 0, "rpy": 0, "lpp": 0, "rpp": 0,
  45. "ax": 0, "ay": 0, "az": 0, "gx": 0, "gy": 0, "gz": 0,
  46. }
  47. DECK_VID = 0x28DE
  48. DECK_PID = 0x1205
  49. # ─────────────────────────── DeckState decode ───────────────────────────
  50. # Each entry: (byte_offset, bit_mask, Triton mask). EXACT byte/bit from the Linux kernel
  51. # drivers/hid/hid-steam.c steam_do_deck_input_event() -- authoritative, not guessed.
  52. DECK_BTN_MAP = [
  53. # byte 8 -- digital triggers (full-pull) + bumpers + face buttons
  54. (8, 0x01, 0x00800000), # R2 full-pull -> RTrig click (triton bit 23)
  55. (8, 0x02, 0x08000000), # L2 full-pull -> LTrig click (triton bit 27)
  56. (8, 0x04, TB_RB), # R1
  57. (8, 0x08, TB_LB), # L1
  58. (8, 0x10, TB_Y),
  59. (8, 0x20, TB_B),
  60. (8, 0x40, TB_X),
  61. (8, 0x80, TB_A),
  62. # byte 9 -- dpad + system buttons + lower-left grip
  63. (9, 0x01, TB_DUP),
  64. (9, 0x02, TB_DRT),
  65. (9, 0x04, TB_DLF),
  66. (9, 0x08, TB_DDN),
  67. # SELECT (byte9 bit4) and START (byte9 bit6) are SWAPPED vs the Triton View/Menu so they read
  68. # right on the host (kernel calls bit4 "menu left"/SELECT, bit6 "menu right"/START).
  69. (9, 0x10, TB_MENU), # ⊟ (BTN_SELECT) -> Triton Menu
  70. (9, 0x20, TB_STEAM), # Steam (BTN_MODE)
  71. (9, 0x40, TB_VIEW), # ☰ (BTN_START) -> Triton View
  72. (9, 0x80, TB_L5), # GRIPL2 -- lower-left back paddle
  73. # byte 10 -- lower-right grip + pad click/TOUCH + left stick click
  74. (10, 0x01, TB_R5), # GRIPR2 -- lower-right back paddle
  75. (10, 0x02, 0x04000000), # BTN_THUMB -> left pad CLICK (TB_LPADC)
  76. (10, 0x04, 0x00400000), # BTN_THUMB2 -> right pad CLICK (TB_RPADC)
  77. (10, 0x08, 0x02000000), # left pad TOUCH (TB_LPADT bit25) -- needed for pad->mouse
  78. (10, 0x10, 0x00200000), # right pad TOUCH (TB_RPADT bit21)
  79. (10, 0x40, TB_L3), # BTN_THUMBL -> left stick click
  80. # byte 11 -- right stick click
  81. (11, 0x04, TB_R3), # BTN_THUMBR
  82. # byte 13 -- upper back paddles + thumbstick capacitive touch
  83. (13, 0x02, TB_L4), # BTN_GRIPL (upper-left)
  84. (13, 0x04, TB_R4), # BTN_GRIPR (upper-right)
  85. (13, 0x40, 0x01000000), # left thumbstick TOUCH (LJoy touch, bit24)
  86. (13, 0x80, 0x00100000), # right thumbstick TOUCH (RJoy touch, bit20)
  87. # byte 14 -- QAM (•••)
  88. (14, 0x04, TB_QAM), # BTN_BASE
  89. ]
  90. # analog block: little-endian 16-bit, EXACT offsets from hid-steam.c steam_do_deck_input_event().
  91. _OFF_LPAD_X, _OFF_LPAD_Y = 16, 18 # s16 left trackpad
  92. _OFF_RPAD_X, _OFF_RPAD_Y = 20, 22 # s16 right trackpad
  93. _OFF_ACCEL = 24 # s16 ax,ay,az (24/26/28)
  94. _OFF_GYRO = 30 # s16 gx,gy,gz (30/32/34)
  95. _OFF_TRIG_L, _OFF_TRIG_R = 44, 46 # u16 analog triggers
  96. _OFF_LSTICK_X, _OFF_LSTICK_Y = 48, 50
  97. _OFF_RSTICK_X, _OFF_RSTICK_Y = 52, 54
  98. _OFF_LPAD_P, _OFF_RPAD_P = 56, 58 # u16 trackpad pressure
  99. def _s16(b, off):
  100. return struct.unpack_from("<h", b, off)[0]
  101. def _u16(b, off):
  102. return struct.unpack_from("<H", b, off)[0]
  103. def decode_deck(rpt, state):
  104. """Decode a 64-byte DeckState report (rpt) into the Triton `state` dict. Returns False if not a
  105. 0x09 state report."""
  106. if len(rpt) < 60 or rpt[2] != 0x09:
  107. return False
  108. btn = 0
  109. for off, mask, tmask in DECK_BTN_MAP:
  110. if tmask and (rpt[off] & mask):
  111. btn |= tmask
  112. state["buttons"] = btn
  113. # sticks (int16, center 0). Deck Y is up-positive; Triton expects up-positive too -> pass through.
  114. state["lx"] = _s16(rpt, _OFF_LSTICK_X)
  115. state["ly"] = _s16(rpt, _OFF_LSTICK_Y)
  116. state["rx"] = _s16(rpt, _OFF_RSTICK_X)
  117. state["ry"] = _s16(rpt, _OFF_RSTICK_Y)
  118. # analog triggers u16 -> 0..255
  119. state["lt"] = min(255, _u16(rpt, _OFF_TRIG_L) >> 7)
  120. state["rt"] = min(255, _u16(rpt, _OFF_TRIG_R) >> 7)
  121. # trackpads (int16) -> Triton pads (this is the FIX: pads no longer bleed into sticks/dpad)
  122. state["lpx"] = _s16(rpt, _OFF_LPAD_X)
  123. state["lpy"] = _s16(rpt, _OFF_LPAD_Y)
  124. state["rpx"] = _s16(rpt, _OFF_RPAD_X)
  125. state["rpy"] = _s16(rpt, _OFF_RPAD_Y)
  126. state["lpp"] = _u16(rpt, _OFF_LPAD_P) # trackpad pressure (u16)
  127. state["rpp"] = _u16(rpt, _OFF_RPAD_P)
  128. # IMU
  129. state["ax"] = _s16(rpt, _OFF_ACCEL)
  130. state["ay"] = _s16(rpt, _OFF_ACCEL + 2)
  131. state["az"] = _s16(rpt, _OFF_ACCEL + 4)
  132. state["gx"] = _s16(rpt, _OFF_GYRO)
  133. state["gy"] = _s16(rpt, _OFF_GYRO + 2)
  134. state["gz"] = _s16(rpt, _OFF_GYRO + 4)
  135. return True
  136. # ─────────────────────────── USB source (default) ───────────────────────────
  137. class DeckSource:
  138. """Read + DETACH the Deck controller via libusb (pyusb). grab() detaches it from the kernel/Steam
  139. and starts reading EP 0x83; ungrab() hands it back."""
  140. IFACES = (0, 1, 2) # claim all HID interfaces so the pad fully leaves the kernel (mouse/kbd/steam)
  141. EP_IN = 0x83
  142. IFACE_READ = 2
  143. def __init__(self, path=None):
  144. import time as _time
  145. import usb.core # imported here so --help / --evdev work without pyusb
  146. self._time = _time
  147. self._usbutil = __import__("usb.util", fromlist=["util"])
  148. self.dev = usb.core.find(idVendor=DECK_VID, idProduct=DECK_PID)
  149. if self.dev is None:
  150. raise RuntimeError("Deck controller 28DE:1205 not found on USB")
  151. self.state = dict(NEUTRAL)
  152. self._claimed = False
  153. self._USBError = usb.core.USBError
  154. self._last_rumble = None # last (int,left,right,lg,rg) we sent -> dedupe
  155. self._last_rumble_t = 0.0 # throttle clock
  156. @property
  157. def name(self):
  158. return "Steam Deck controller (libusb 28DE:1205)"
  159. def grab(self):
  160. if self._claimed:
  161. return
  162. for i in self.IFACES:
  163. try:
  164. if self.dev.is_kernel_driver_active(i):
  165. self.dev.detach_kernel_driver(i)
  166. except Exception:
  167. pass
  168. try:
  169. self._usbutil.claim_interface(self.dev, i)
  170. except Exception:
  171. pass
  172. self._claimed = True
  173. def ungrab(self):
  174. if not self._claimed:
  175. return
  176. for i in self.IFACES:
  177. try:
  178. self._usbutil.release_interface(self.dev, i)
  179. except Exception:
  180. pass
  181. try:
  182. self.dev.attach_kernel_driver(i)
  183. except Exception:
  184. pass
  185. self._claimed = False
  186. def pump(self):
  187. # drain whatever reports are queued; keep the freshest decoded state
  188. for _ in range(8):
  189. try:
  190. rpt = self.dev.read(self.EP_IN, 64, timeout=2)
  191. except self._USBError:
  192. break # timeout / no data
  193. decode_deck(bytes(rpt), self.state)
  194. return self.state
  195. # don't issue rumble feature-reports faster than this -- protects the Deck pad's control endpoint
  196. # (hammering it at the poll rate is what wedged the pad). A repeated identical state is also dropped.
  197. _RUMBLE_MIN_DT = 0.015
  198. def rumble(self, intensity, left, right, lgain=0, rgain=0):
  199. """Play a rumble on the Deck's haptics via the 0xEB feature report (hid-steam SET_REPORT).
  200. intensity/left/right are the SC2 rumble u16s relayed back from Steam; 0 stops it. Rate-limited
  201. + deduped + short-timeout so a rumble burst can never flood or block the input loop."""
  202. if not self._claimed:
  203. return
  204. cur = (intensity & 0xFFFF, left & 0xFFFF, right & 0xFFFF, lgain & 0xFF, rgain & 0xFF)
  205. now = self._time.monotonic()
  206. # always let an OFF (all-zero) through promptly; otherwise dedupe + throttle
  207. if cur != (0, 0, 0, 0, 0):
  208. if cur == self._last_rumble:
  209. return
  210. if now - self._last_rumble_t < self._RUMBLE_MIN_DT:
  211. return
  212. self._last_rumble = cur
  213. self._last_rumble_t = now
  214. buf = bytearray(64)
  215. buf[0] = 0xEB # CMD_TRIGGER_HAPTIC_RUMBLE
  216. buf[1] = 0x09 # payload length
  217. buf[2] = 0x00 # reserved
  218. struct.pack_into("<HHH", buf, 3, cur[0], cur[1], cur[2])
  219. buf[9] = cur[3]
  220. buf[10] = cur[4]
  221. try:
  222. # SET_REPORT(Feature), report id 0, on the DeckState control interface (iface 2). Short
  223. # timeout: never block the input/UI loop if the endpoint is busy.
  224. self.dev.ctrl_transfer(0x21, 0x09, 0x0300, self.IFACE_READ, bytes(buf), timeout=20)
  225. except Exception:
  226. pass
  227. # ─────────────────────────── evdev fallback ───────────────────────────
  228. EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03
  229. BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST = 0x130, 0x131, 0x133, 0x134
  230. BTN_TL, BTN_TR = 0x136, 0x137
  231. BTN_SELECT, BTN_START, BTN_MODE = 0x13A, 0x13B, 0x13C
  232. BTN_THUMBL, BTN_THUMBR = 0x13D, 0x13E
  233. ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ = 0, 1, 2, 3, 4, 5
  234. ABS_HAT0X, ABS_HAT0Y = 0x10, 0x11
  235. KEYMAP = {
  236. BTN_SOUTH: TB_A, BTN_EAST: TB_B, BTN_WEST: TB_X, BTN_NORTH: TB_Y,
  237. BTN_TL: TB_LB, BTN_TR: TB_RB,
  238. BTN_SELECT: TB_VIEW, BTN_START: TB_MENU, BTN_MODE: TB_STEAM,
  239. BTN_THUMBL: TB_L3, BTN_THUMBR: TB_R3,
  240. }
  241. _EVENT = struct.Struct("qqHHi")
  242. EVENT_SZ = _EVENT.size
  243. def _IOC(d, t, nr, size):
  244. return (d << 30) | (size << 16) | (ord(t) << 8) | nr
  245. EVIOCGID = _IOC(2, 'E', 0x02, 8)
  246. def EVIOCGNAME(length):
  247. return _IOC(2, 'E', 0x06, length)
  248. def EVIOCGBIT(ev, length):
  249. return _IOC(2, 'E', 0x20 + ev, length)
  250. EVIOCGRAB = _IOC(1, 'E', 0x90, 4)
  251. def _test_bit(bm, bit):
  252. return bool(bm[bit // 8] & (1 << (bit % 8)))
  253. class _RawDev:
  254. def __init__(self, path):
  255. self.path = path
  256. self.fd = os.open(path, os.O_RDWR | os.O_NONBLOCK)
  257. def close(self):
  258. try:
  259. os.close(self.fd)
  260. except Exception:
  261. pass
  262. def ids(self):
  263. buf = bytearray(8)
  264. try:
  265. fcntl.ioctl(self.fd, EVIOCGID, buf, True)
  266. _b, vid, pid, _v = struct.unpack("<HHHH", buf)
  267. return vid, pid
  268. except Exception:
  269. return (0, 0)
  270. def has_key(self, code):
  271. buf = bytearray(96)
  272. try:
  273. fcntl.ioctl(self.fd, EVIOCGBIT(EV_KEY, len(buf)), buf, True)
  274. return _test_bit(buf, code)
  275. except Exception:
  276. return False
  277. def grab(self):
  278. fcntl.ioctl(self.fd, EVIOCGRAB, 1)
  279. def ungrab(self):
  280. try:
  281. fcntl.ioctl(self.fd, EVIOCGRAB, 0)
  282. except Exception:
  283. pass
  284. def read_events(self):
  285. try:
  286. data = os.read(self.fd, EVENT_SZ * 64)
  287. except (BlockingIOError, OSError):
  288. return
  289. for i in range(0, len(data) - EVENT_SZ + 1, EVENT_SZ):
  290. _s, _u, typ, code, val = _EVENT.unpack_from(data, i)
  291. yield typ, code, val
  292. def find_deck_evdev():
  293. for path in sorted(glob.glob("/dev/input/event*")):
  294. try:
  295. d = _RawDev(path)
  296. except Exception:
  297. continue
  298. try:
  299. if d.has_key(BTN_SOUTH):
  300. return path
  301. finally:
  302. d.close()
  303. return None
  304. class EvdevSource:
  305. def __init__(self, path=None):
  306. self.path = path or find_deck_evdev()
  307. if not self.path:
  308. raise RuntimeError("no gamepad evdev device found")
  309. self.dev = _RawDev(self.path)
  310. self.state = dict(NEUTRAL)
  311. self._grabbed = False
  312. @property
  313. def name(self):
  314. return "evdev " + self.path
  315. def grab(self):
  316. if not self._grabbed:
  317. self.dev.grab()
  318. self._grabbed = True
  319. def ungrab(self):
  320. if self._grabbed:
  321. self.dev.ungrab()
  322. self._grabbed = False
  323. def rumble(self, intensity, left, right, lgain=0, rgain=0):
  324. # evdev fallback has no haptic path (FF could be added, but the libusb source is the real one)
  325. pass
  326. def pump(self):
  327. s = self.state
  328. for typ, code, val in self.dev.read_events():
  329. if typ == EV_KEY:
  330. m = KEYMAP.get(code)
  331. if m is not None:
  332. s["buttons"] = (s["buttons"] | m) if val else (s["buttons"] & ~m)
  333. elif typ == EV_ABS:
  334. if code == ABS_X:
  335. s["lx"] = val
  336. elif code == ABS_Y:
  337. s["ly"] = -val
  338. elif code == ABS_RX:
  339. s["rx"] = val
  340. elif code == ABS_RY:
  341. s["ry"] = -val
  342. elif code == ABS_Z:
  343. s["lt"] = max(0, min(255, val))
  344. elif code == ABS_RZ:
  345. s["rt"] = max(0, min(255, val))
  346. return s
  347. def open_source(prefer_evdev=False, path=None):
  348. """Pick the best input source: libusb DeckSource by default, evdev if requested/unavailable."""
  349. if not prefer_evdev:
  350. try:
  351. return DeckSource(path)
  352. except Exception as ex:
  353. print("DeckSource (libusb) unavailable (%s); falling back to evdev" % ex)
  354. return EvdevSource(path)
  355. # ─────────────────────────── capture / calibration tool ───────────────────────────
  356. def _capture():
  357. """Live-dump the raw DeckState report; press one control at a time and watch which bytes/bits move,
  358. so you can correct DECK_BTN_MAP and the analog offsets for your unit. Ctrl-C to stop."""
  359. src = DeckSource()
  360. print("detaching controller (libusb) ... press ONE control at a time; Ctrl-C to stop")
  361. src.grab()
  362. base = None
  363. try:
  364. while True:
  365. try:
  366. rpt = bytes(src.dev.read(src.EP_IN, 64, timeout=200))
  367. except Exception:
  368. continue
  369. if len(rpt) < 60 or rpt[2] != 0x09:
  370. continue
  371. if base is None:
  372. base = rpt
  373. print("baseline:", base.hex())
  374. continue
  375. diff = [(i, base[i], rpt[i]) for i in range(8, 60) if base[i] != rpt[i]]
  376. if diff:
  377. print(" ".join("b%d:%02x->%02x" % d for d in diff))
  378. except KeyboardInterrupt:
  379. pass
  380. finally:
  381. src.ungrab()
  382. print("\nreattached controller to the kernel.")
  383. if __name__ == "__main__":
  384. _capture()