11import logging
22import time
33from collections import defaultdict
4- from typing import Sequence
4+ from typing import Sequence , cast
55
66from hhd .controller import Consumer , Event , Producer
77from hhd .controller .lib .uhid import UhidDevice , BUS_USB
8+ from hhd .controller .lib .common import encode_axis , set_button
9+ from hhd .controller .lib .ccache import ControllerCache
810
911from .const import (
1012 SDCONT_VENDOR ,
11- SDCONT_PRODUCT ,
1213 SDCONT_VERSION ,
1314 SDCONT_COUNTRY ,
14- SDCONT_NAME ,
1515 SDCONT_DESCRIPTOR ,
16+ SD_AXIS_MAP ,
17+ SD_BTN_MAP ,
1618)
1719
20+ MAX_IMU_SYNC_DELAY = 2
21+
1822logger = logging .getLogger (__name__ )
1923
24+ _cache = ControllerCache ()
25+
2026
2127def trim (rep : bytes ):
2228 if not rep :
@@ -31,42 +37,84 @@ def pad(rep):
3137 return bytes (rep ) + bytes ([0 for _ in range (64 - len (rep ))])
3238
3339
34- class SteamdeckOLEDController (Producer , Consumer ):
40+ class SteamdeckController (Producer , Consumer ):
41+ @staticmethod
42+ def close_cached ():
43+ _cache .close ()
44+
3545 def __init__ (
3646 self ,
47+ pid ,
48+ name ,
49+ touchpad : bool = False ,
3750 ) -> None :
3851 self .available = False
39- self .report = None
4052 self .dev = None
4153 self .start = 0
54+ self .pid = pid
55+ self .name = name
56+ self .sync_gyro = False
57+ self .enable_touchpad = touchpad
58+ self .report = bytearray (64 )
59+ self .i = 0
4260 self .last_rep = None
4361
4462 def open (self ) -> Sequence [int ]:
4563 self .available = False
46- self .report = bytearray ([ 64 ] + [ 0 for _ in range ( 64 )])
47- self .dev = UhidDevice (
48- vid = SDCONT_VENDOR ,
49- pid = SDCONT_PRODUCT ,
50- bus = BUS_USB ,
51- version = SDCONT_VERSION ,
52- country = SDCONT_COUNTRY ,
53- name = SDCONT_NAME ,
54- report_descriptor = SDCONT_DESCRIPTOR ,
64+ self .report [ 0 ] = 0x01
65+ self .report [ 2 ] = 0x09
66+ self . report [ 3 ] = 0x40
67+ self . i = 0
68+
69+ # Use cached controller to avoid disconnects
70+ cached = cast (
71+ SteamdeckController | None ,
72+ _cache . get () ,
5573 )
74+ self .dev = None
75+ if cached :
76+ if self .pid == cached .pid and self .name == cached .name :
77+ logger .warning (
78+ f"Using cached controller node for Steamdeck Controller."
79+ )
80+ self .dev = cached .dev
81+ if self .dev and self .dev .fd :
82+ self .fd = self .dev .fd
83+ else :
84+ logger .warning (f"Throwing away cached Steamdeck Controller." )
85+ cached .close (True , in_cache = True )
86+ if not self .dev :
87+ self .dev = UhidDevice (
88+ vid = SDCONT_VENDOR ,
89+ pid = self .pid ,
90+ bus = BUS_USB ,
91+ version = SDCONT_VERSION ,
92+ country = SDCONT_COUNTRY ,
93+ name = bytes (self .name , "utf-8" ),
94+ report_descriptor = SDCONT_DESCRIPTOR ,
95+ unique_name = b"" ,
96+ physical_name = b"" ,
97+ )
98+ self .fd = self .dev .open ()
5699
57100 self .state : dict = defaultdict (lambda : 0 )
58101 self .rumble = False
59102 self .touchpad_touch = False
60- self .start = time .perf_counter_ns ()
61- self .fd = self .dev .open ()
62- return [self .fd ]
103+ curr = time .perf_counter ()
104+ self .start = curr
105+ self .touchpad_down = curr
106+ self .last_imu = curr
107+ self .imu_failed = False
63108
64- def close (self , exit : bool ) -> bool :
65- if not exit :
66- """This is a consumer, so we would deadlock if it was disabled."""
67- return False
109+ logger .info (f"Starting '{ self .name } '." )
110+ assert self .fd
111+ return [self .fd ]
68112
69- if self .dev :
113+ def close (self , exit : bool , in_cache : bool = False ) -> bool :
114+ if not in_cache and time .perf_counter () - self .start :
115+ logger .warning (f"Caching Steam Controller to avoid reconnection." )
116+ _cache .add (self )
117+ elif self .dev :
70118 self .dev .send_destroy ()
71119 self .dev .close ()
72120 self .dev = None
@@ -83,12 +131,67 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]:
83131 assert self .dev
84132 while ev := self .dev .read_event ():
85133 match ev ["type" ]:
86- case "open" :
87- logger .info (f"OPENED" )
88- case "close" :
89- logger .info (f"CLOSED" )
134+ # case "open":
135+ # logger.info(f"SD OPENED")
136+ # case "close":
137+ # logger.info(f"SD CLOSED")
90138 case "get_report" :
91139 match self .last_rep :
140+ case 0x83 :
141+ rep = bytes (
142+ [
143+ 0x83 ,
144+ 0x2D , # 45/5=9 attrs
145+ # https://github.com/libsdl-org/SDL/blob/eed94cb0345cbf6dc9088c7bfc3d10828cb19f9d/src/joystick/hidapi/steam/controller_constants.h#L363
146+ 0x01 , # ATTRIB_PRODUCT_ID
147+ 0x05 ,
148+ 0x12 ,
149+ 0x00 ,
150+ 0x00 ,
151+ 0x02 , # ATTRIB_PRODUCT_REVISON
152+ 0x00 ,
153+ 0x00 ,
154+ 0x00 ,
155+ 0x00 ,
156+ 0x0A , # ATTRIB_BOOTLOADER_BUILD_TIME
157+ 0x2B ,
158+ 0x12 ,
159+ 0xA9 ,
160+ 0x62 ,
161+ 0x04 , # ATTRIB_FIRMWARE_BUILD_TIME
162+ 0xB7 ,
163+ 0x61 ,
164+ 0x7C ,
165+ 0x67 ,
166+ 0x09 , # ATTRIB_BOARD_REVISION
167+ 0x2E ,
168+ 0x00 ,
169+ 0x00 ,
170+ 0x00 ,
171+ 0x0B , # ATTRIB_CONNECTION_INTERVAL_IN_US
172+ 0xA0 ,
173+ 0x0F ,
174+ 0x00 ,
175+ 0x00 ,
176+ 0x0D , # attr
177+ 0x00 ,
178+ 0x00 ,
179+ 0x00 ,
180+ 0x00 ,
181+ 0x0C , # attr
182+ 0x00 ,
183+ 0x00 ,
184+ 0x00 ,
185+ 0x00 ,
186+ 0x0E , # attr
187+ 0x00 ,
188+ 0x00 ,
189+ 0x00 ,
190+ 0x00 ,
191+ ]
192+ )
193+ if not ev ["rnum" ]:
194+ rep = bytes ([0 ]) + rep
92195 case 0xAE :
93196 rep = bytes (
94197 [
@@ -107,16 +210,129 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]:
107210 )
108211 case "set_report" :
109212 self .dev .send_set_report_reply (ev ["id" ], 0 )
110- logger .info (
111- f"SET_REPORT({ ev ['rnum' ]:02x} :{ ev ['rtype' ]:02x} ): { trim (ev ['data' ]).hex ()} "
112- )
113213 self .last_rep = ev ["data" ][3 ]
214+
215+ match self .last_rep :
216+ case 0xEB :
217+ left = int .from_bytes (
218+ ev ["data" ][8 :10 ], byteorder = "little" , signed = True
219+ )
220+ right = int .from_bytes (
221+ ev ["data" ][10 :12 ], byteorder = "little" , signed = True
222+ )
223+ out .append (
224+ {
225+ "type" : "rumble" ,
226+ "code" : "main" ,
227+ # For some reason goes to 127
228+ "strong_magnitude" : left / (2 ** 16 - 1 ),
229+ "weak_magnitude" : right / (2 ** 16 - 1 ),
230+ }
231+ )
232+ case 0x8f :
233+ pass
234+ case _:
235+ logger .info (
236+ f"SD SET_REPORT({ ev ['rnum' ]:02x} :{ ev ['rtype' ]:02x} ): { trim (ev ['data' ]).hex ()} "
237+ )
238+
239+ # 410000eb 0901401f 0000 0000 fbfb
240+ # 410000eb 0901401f ff7f ff7f fbfb
114241 case "output" :
115- logger .info (f"OUTPUT" )
242+ logger .info (f"SD OUTPUT" )
116243 case _:
117- logger .warning (f"UKN_EVENT: { ev } " )
244+ logger .warning (f"SD UKN_EVENT: { ev } " )
118245
119246 return out
120247
121248 def consume (self , events : Sequence [Event ]):
122- pass
249+ if not self .dev :
250+ return
251+
252+ assert self .report
253+
254+ # To fix gyro to mouse in latest steam
255+ # only send updates when gyro sends a timestamp
256+ send = not self .sync_gyro
257+ curr = time .perf_counter ()
258+
259+ new_rep = bytearray (self .report )
260+ for ev in events :
261+ code = ev ["code" ]
262+ match ev ["type" ]:
263+ case "axis" :
264+ if not self .enable_touchpad and code .startswith ("touchpad" ):
265+ continue
266+ if code in SD_AXIS_MAP :
267+ try :
268+ encode_axis (new_rep , SD_AXIS_MAP [code ], ev ["value" ])
269+ except Exception :
270+ logger .warning (
271+ f"Encoding '{ ev ['code' ]} ' with { ev ['value' ]} overflowed."
272+ )
273+ # DPAD is weird
274+ match code :
275+ case "hat_x" :
276+ self .state ["hat_x" ] = ev ["value" ]
277+ # patch_dpad_val(
278+ # new_rep,
279+ # self.ofs,
280+ # self.state["hat_x"],
281+ # self.state["hat_y"],
282+ # )
283+ case "hat_y" :
284+ self .state ["hat_y" ] = ev ["value" ]
285+ # patch_dpad_val(
286+ # new_rep,
287+ # self.ofs,
288+ # self.state["hat_x"],
289+ # self.state["hat_y"],
290+ # )
291+ case "gyro_ts" | "accel_ts" | "imu_ts" :
292+ send = True
293+ self .last_imu = time .perf_counter ()
294+ self .last_imu_ts = ev ["value" ]
295+ # new_rep[self.ofs + 27 : self.ofs + 31] = int(
296+ # ev["value"] / DS5_EDGE_DELTA_TIME_NS
297+ # ).to_bytes(8, byteorder="little", signed=False)[:4]
298+ case "button" :
299+ if code in SD_BTN_MAP :
300+ set_button (new_rep , SD_BTN_MAP [code ], ev ["value" ])
301+
302+ # # Fix touchpad click requiring touch
303+ # if code == "touchpad_touch":
304+ # self.touchpad_touch = ev["value"]
305+ # if code == "touchpad_left":
306+ # set_button(
307+ # new_rep,
308+ # SD_BTN_MAP["touchpad_touch"],
309+ # ev["value"] or self.touchpad_touch,
310+ # )
311+ # # Also add right click
312+ # if code == "touchpad_right":
313+ # set_button(
314+ # new_rep,
315+ # SD_BTN_MAP["touchpad_touch"],
316+ # ev["value"] or self.touchpad_touch,
317+ # )
318+ # set_button(
319+ # new_rep,
320+ # SD_BTN_MAP["touchpad_touch2"],
321+ # ev["value"],
322+ # )
323+
324+ # If the IMU breaks, smoothly re-enable the controller
325+ failover = self .last_imu + MAX_IMU_SYNC_DELAY < curr
326+ if self .sync_gyro and failover and not self .imu_failed :
327+ self .imu_failed = True
328+ logger .error (
329+ f"IMU Did not send information for { MAX_IMU_SYNC_DELAY } s. Disabling Gyro Sync."
330+ )
331+
332+ self .report = new_rep
333+ # sign_crc32_inplace(self.report, DS5_INPUT_CRC32_SEED)
334+ if send or failover :
335+ new_rep [4 :8 ] = self .i .to_bytes (4 , byteorder = "little" , signed = False )
336+ self .i = self .i + 1 if self .i < 0xFFFF else 0
337+ self .dev .send_input_report (self .report )
338+ # logger.info(self.report.hex())
0 commit comments