input_source.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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. # ── IMU axis remap: Deck IMU -> SC2 report-0x45 IMU ──────────────────────────
  100. # The Deck and the SC2 mount their IMUs in different orientations, so the raw Deck axes do NOT line up
  101. # with the gyro/accel convention Steam expects from an SC2. accel and gyro MUST use the SAME remap (shared
  102. # handedness) or the host's sensor fusion mixes axes intermittently (see memory: switch-imu-fusion).
  103. # Each entry = (source_axis, sign): source_axis 0=x 1=y 2=z indexes the raw Deck (x,y,z); sign flips it.
  104. # CALIBRATE empirically: in Steam's gyro test, rotate the Deck about one physical axis, see which on-screen
  105. # axis moves and which way, then swap the source / flip the sign here. Default = identity pass-through.
  106. IMU_GYRO_MAP = ((0, +1), (1, +1), (2, +1)) # -> (gx, gy, gz)
  107. IMU_ACCEL_MAP = ((0, +1), (1, +1), (2, +1)) # -> (ax, ay, az)
  108. # Deck->SC2 gyro sensitivity ratio. Both are int16 IMUs at similar full-scale, so 1.0 is the start point;
  109. # bump if the on-screen rotation is too slow, drop it if too fast/jittery.
  110. IMU_GYRO_SCALE = 1.0
  111. def _clamp16(v):
  112. return -32768 if v < -32768 else 32767 if v > 32767 else int(v)
  113. def _remap3(src, mapping, scale=1.0):
  114. """src = (x, y, z) raw; mapping = 3x (source_axis, sign). Returns the remapped, clamped triple."""
  115. return tuple(_clamp16(src[ax] * sign * scale) for ax, sign in mapping)
  116. def _s16(b, off):
  117. return struct.unpack_from("<h", b, off)[0]
  118. def _u16(b, off):
  119. return struct.unpack_from("<H", b, off)[0]
  120. def decode_deck(rpt, state):
  121. """Decode a 64-byte DeckState report (rpt) into the Triton `state` dict. Returns False if not a
  122. 0x09 state report."""
  123. if len(rpt) < 60 or rpt[2] != 0x09:
  124. return False
  125. btn = 0
  126. for off, mask, tmask in DECK_BTN_MAP:
  127. if tmask and (rpt[off] & mask):
  128. btn |= tmask
  129. state["buttons"] = btn
  130. # sticks (int16, center 0). Deck Y is up-positive; Triton expects up-positive too -> pass through.
  131. state["lx"] = _s16(rpt, _OFF_LSTICK_X)
  132. state["ly"] = _s16(rpt, _OFF_LSTICK_Y)
  133. state["rx"] = _s16(rpt, _OFF_RSTICK_X)
  134. state["ry"] = _s16(rpt, _OFF_RSTICK_Y)
  135. # analog triggers u16 -> 0..255
  136. state["lt"] = min(255, _u16(rpt, _OFF_TRIG_L) >> 7)
  137. state["rt"] = min(255, _u16(rpt, _OFF_TRIG_R) >> 7)
  138. # trackpads (int16) -> Triton pads (this is the FIX: pads no longer bleed into sticks/dpad)
  139. state["lpx"] = _s16(rpt, _OFF_LPAD_X)
  140. state["lpy"] = _s16(rpt, _OFF_LPAD_Y)
  141. state["rpx"] = _s16(rpt, _OFF_RPAD_X)
  142. state["rpy"] = _s16(rpt, _OFF_RPAD_Y)
  143. state["lpp"] = _u16(rpt, _OFF_LPAD_P) # trackpad pressure (u16)
  144. state["rpp"] = _u16(rpt, _OFF_RPAD_P)
  145. # IMU: raw Deck accel/gyro -> SC2 axes via the shared remap (calibrate IMU_*_MAP in Steam's gyro test).
  146. araw = (_s16(rpt, _OFF_ACCEL), _s16(rpt, _OFF_ACCEL + 2), _s16(rpt, _OFF_ACCEL + 4))
  147. graw = (_s16(rpt, _OFF_GYRO), _s16(rpt, _OFF_GYRO + 2), _s16(rpt, _OFF_GYRO + 4))
  148. state["ax"], state["ay"], state["az"] = _remap3(araw, IMU_ACCEL_MAP)
  149. state["gx"], state["gy"], state["gz"] = _remap3(graw, IMU_GYRO_MAP, IMU_GYRO_SCALE)
  150. return True
  151. # ─────────────────────────── USB source (default) ───────────────────────────
  152. class DeckSource:
  153. """Read + DETACH the Deck controller via libusb (pyusb). grab() detaches it from the kernel/Steam
  154. and starts reading EP 0x83; ungrab() hands it back."""
  155. IFACES = (0, 1, 2) # claim all HID interfaces so the pad fully leaves the kernel (mouse/kbd/steam)
  156. EP_IN = 0x83
  157. IFACE_READ = 2
  158. def __init__(self, path=None):
  159. import time as _time
  160. import usb.core # imported here so --help / --evdev work without pyusb
  161. self._time = _time
  162. self._usbutil = __import__("usb.util", fromlist=["util"])
  163. self.dev = usb.core.find(idVendor=DECK_VID, idProduct=DECK_PID)
  164. if self.dev is None:
  165. raise RuntimeError("Deck controller 28DE:1205 not found on USB")
  166. self.state = dict(NEUTRAL)
  167. self._claimed = False
  168. self._USBError = usb.core.USBError
  169. self._last_rumble = None # last (int,left,right,lg,rg) we sent -> dedupe
  170. self._last_rumble_t = 0.0 # throttle clock
  171. @property
  172. def name(self):
  173. return "Steam Deck controller (libusb 28DE:1205)"
  174. def grab(self):
  175. if self._claimed:
  176. return
  177. for i in self.IFACES:
  178. try:
  179. if self.dev.is_kernel_driver_active(i):
  180. self.dev.detach_kernel_driver(i)
  181. except Exception:
  182. pass
  183. try:
  184. self._usbutil.claim_interface(self.dev, i)
  185. except Exception:
  186. pass
  187. self._claimed = True
  188. def ungrab(self):
  189. if not self._claimed:
  190. return
  191. for i in self.IFACES:
  192. try:
  193. self._usbutil.release_interface(self.dev, i)
  194. except Exception:
  195. pass
  196. try:
  197. self.dev.attach_kernel_driver(i)
  198. except Exception:
  199. pass
  200. self._claimed = False
  201. def pump(self):
  202. # drain whatever reports are queued; keep the freshest decoded state
  203. for _ in range(8):
  204. try:
  205. rpt = self.dev.read(self.EP_IN, 64, timeout=2)
  206. except self._USBError:
  207. break # timeout / no data
  208. decode_deck(bytes(rpt), self.state)
  209. return self.state
  210. # don't issue rumble feature-reports faster than this -- protects the Deck pad's control endpoint
  211. # (hammering it at the poll rate is what wedged the pad). A repeated identical state is also dropped.
  212. _RUMBLE_MIN_DT = 0.015
  213. def rumble(self, intensity, left, right, lgain=0, rgain=0):
  214. """Play a rumble on the Deck's haptics via the 0xEB feature report (hid-steam SET_REPORT).
  215. intensity/left/right are the SC2 rumble u16s relayed back from Steam; 0 stops it. Rate-limited
  216. + deduped + short-timeout so a rumble burst can never flood or block the input loop."""
  217. if not self._claimed:
  218. return
  219. cur = (intensity & 0xFFFF, left & 0xFFFF, right & 0xFFFF, lgain & 0xFF, rgain & 0xFF)
  220. now = self._time.monotonic()
  221. # always let an OFF (all-zero) through promptly; otherwise dedupe + throttle
  222. if cur != (0, 0, 0, 0, 0):
  223. if cur == self._last_rumble:
  224. return
  225. if now - self._last_rumble_t < self._RUMBLE_MIN_DT:
  226. return
  227. self._last_rumble = cur
  228. self._last_rumble_t = now
  229. buf = bytearray(64)
  230. buf[0] = 0xEB # CMD_TRIGGER_HAPTIC_RUMBLE
  231. buf[1] = 0x09 # payload length
  232. buf[2] = 0x00 # reserved
  233. struct.pack_into("<HHH", buf, 3, cur[0], cur[1], cur[2])
  234. buf[9] = cur[3]
  235. buf[10] = cur[4]
  236. try:
  237. # SET_REPORT(Feature), report id 0, on the DeckState control interface (iface 2). Short
  238. # timeout: never block the input/UI loop if the endpoint is busy.
  239. self.dev.ctrl_transfer(0x21, 0x09, 0x0300, self.IFACE_READ, bytes(buf), timeout=20)
  240. except Exception:
  241. pass
  242. # ─────────────────────────── evdev fallback ───────────────────────────
  243. EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03
  244. BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST = 0x130, 0x131, 0x133, 0x134
  245. BTN_TL, BTN_TR = 0x136, 0x137
  246. BTN_SELECT, BTN_START, BTN_MODE = 0x13A, 0x13B, 0x13C
  247. BTN_THUMBL, BTN_THUMBR = 0x13D, 0x13E
  248. ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ = 0, 1, 2, 3, 4, 5
  249. ABS_HAT0X, ABS_HAT0Y = 0x10, 0x11
  250. KEYMAP = {
  251. BTN_SOUTH: TB_A, BTN_EAST: TB_B, BTN_WEST: TB_X, BTN_NORTH: TB_Y,
  252. BTN_TL: TB_LB, BTN_TR: TB_RB,
  253. BTN_SELECT: TB_VIEW, BTN_START: TB_MENU, BTN_MODE: TB_STEAM,
  254. BTN_THUMBL: TB_L3, BTN_THUMBR: TB_R3,
  255. }
  256. _EVENT = struct.Struct("qqHHi")
  257. EVENT_SZ = _EVENT.size
  258. def _IOC(d, t, nr, size):
  259. return (d << 30) | (size << 16) | (ord(t) << 8) | nr
  260. EVIOCGID = _IOC(2, 'E', 0x02, 8)
  261. def EVIOCGNAME(length):
  262. return _IOC(2, 'E', 0x06, length)
  263. def EVIOCGBIT(ev, length):
  264. return _IOC(2, 'E', 0x20 + ev, length)
  265. EVIOCGRAB = _IOC(1, 'E', 0x90, 4)
  266. def _test_bit(bm, bit):
  267. return bool(bm[bit // 8] & (1 << (bit % 8)))
  268. class _RawDev:
  269. def __init__(self, path):
  270. self.path = path
  271. self.fd = os.open(path, os.O_RDWR | os.O_NONBLOCK)
  272. def close(self):
  273. try:
  274. os.close(self.fd)
  275. except Exception:
  276. pass
  277. def ids(self):
  278. buf = bytearray(8)
  279. try:
  280. fcntl.ioctl(self.fd, EVIOCGID, buf, True)
  281. _b, vid, pid, _v = struct.unpack("<HHHH", buf)
  282. return vid, pid
  283. except Exception:
  284. return (0, 0)
  285. def has_key(self, code):
  286. buf = bytearray(96)
  287. try:
  288. fcntl.ioctl(self.fd, EVIOCGBIT(EV_KEY, len(buf)), buf, True)
  289. return _test_bit(buf, code)
  290. except Exception:
  291. return False
  292. def grab(self):
  293. fcntl.ioctl(self.fd, EVIOCGRAB, 1)
  294. def ungrab(self):
  295. try:
  296. fcntl.ioctl(self.fd, EVIOCGRAB, 0)
  297. except Exception:
  298. pass
  299. def read_events(self):
  300. try:
  301. data = os.read(self.fd, EVENT_SZ * 64)
  302. except (BlockingIOError, OSError):
  303. return
  304. for i in range(0, len(data) - EVENT_SZ + 1, EVENT_SZ):
  305. _s, _u, typ, code, val = _EVENT.unpack_from(data, i)
  306. yield typ, code, val
  307. def find_deck_evdev():
  308. for path in sorted(glob.glob("/dev/input/event*")):
  309. try:
  310. d = _RawDev(path)
  311. except Exception:
  312. continue
  313. try:
  314. if d.has_key(BTN_SOUTH):
  315. return path
  316. finally:
  317. d.close()
  318. return None
  319. class EvdevSource:
  320. def __init__(self, path=None):
  321. self.path = path or find_deck_evdev()
  322. if not self.path:
  323. raise RuntimeError("no gamepad evdev device found")
  324. self.dev = _RawDev(self.path)
  325. self.state = dict(NEUTRAL)
  326. self._grabbed = False
  327. @property
  328. def name(self):
  329. return "evdev " + self.path
  330. def grab(self):
  331. if not self._grabbed:
  332. self.dev.grab()
  333. self._grabbed = True
  334. def ungrab(self):
  335. if self._grabbed:
  336. self.dev.ungrab()
  337. self._grabbed = False
  338. def rumble(self, intensity, left, right, lgain=0, rgain=0):
  339. # evdev fallback has no haptic path (FF could be added, but the libusb source is the real one)
  340. pass
  341. def pump(self):
  342. s = self.state
  343. for typ, code, val in self.dev.read_events():
  344. if typ == EV_KEY:
  345. m = KEYMAP.get(code)
  346. if m is not None:
  347. s["buttons"] = (s["buttons"] | m) if val else (s["buttons"] & ~m)
  348. elif typ == EV_ABS:
  349. if code == ABS_X:
  350. s["lx"] = val
  351. elif code == ABS_Y:
  352. s["ly"] = -val
  353. elif code == ABS_RX:
  354. s["rx"] = val
  355. elif code == ABS_RY:
  356. s["ry"] = -val
  357. elif code == ABS_Z:
  358. s["lt"] = max(0, min(255, val))
  359. elif code == ABS_RZ:
  360. s["rt"] = max(0, min(255, val))
  361. return s
  362. def open_source(prefer_evdev=False, path=None):
  363. """Pick the best input source: libusb DeckSource by default, evdev if requested/unavailable."""
  364. if not prefer_evdev:
  365. try:
  366. return DeckSource(path)
  367. except Exception as ex:
  368. print("DeckSource (libusb) unavailable (%s); falling back to evdev" % ex)
  369. return EvdevSource(path)
  370. # ─────────────────────────── capture / calibration tool ───────────────────────────
  371. def _capture():
  372. """Live-dump the raw DeckState report; press one control at a time and watch which bytes/bits move,
  373. so you can correct DECK_BTN_MAP and the analog offsets for your unit. Ctrl-C to stop."""
  374. src = DeckSource()
  375. print("detaching controller (libusb) ... press ONE control at a time; Ctrl-C to stop")
  376. src.grab()
  377. base = None
  378. try:
  379. while True:
  380. try:
  381. rpt = bytes(src.dev.read(src.EP_IN, 64, timeout=200))
  382. except Exception:
  383. continue
  384. if len(rpt) < 60 or rpt[2] != 0x09:
  385. continue
  386. if base is None:
  387. base = rpt
  388. print("baseline:", base.hex())
  389. continue
  390. diff = [(i, base[i], rpt[i]) for i in range(8, 60) if base[i] != rpt[i]]
  391. if diff:
  392. print(" ".join("b%d:%02x->%02x" % d for d in diff))
  393. except KeyboardInterrupt:
  394. pass
  395. finally:
  396. src.ungrab()
  397. print("\nreattached controller to the kernel.")
  398. if __name__ == "__main__":
  399. _capture()