Skip to content

Commit 7c0b783

Browse files
committed
feat(ds4): Dualshock 4 Bluetooth HIDRaw support
1 parent 6fd1f79 commit 7c0b783

File tree

4 files changed

+112
-23
lines changed

4 files changed

+112
-23
lines changed

scc/controller.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, Optional
44

55
from scc.constants import HapticPos
66

@@ -31,7 +31,7 @@ def __init__(self) -> None:
3131
self.time_elapsed = 0.0
3232

3333

34-
def get_type(self) -> None:
34+
def get_type(self) -> str:
3535
"""
3636
This method has to return type identifier - short string without spaces
3737
that describes type of controller which should be unique for each

scc/drivers/ds4drv.py

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
import ctypes
77
import logging
8+
import os
89
import sys
910
from typing import TYPE_CHECKING
1011

12+
from usb1 import USBDeviceHandle
13+
1114
from scc.constants import STICK_PAD_MAX, STICK_PAD_MIN, ControllerFlags, SCButtons
15+
from scc.controller import Controller
1216
from scc.drivers.evdevdrv import (
1317
HAVE_EVDEV,
1418
EvdevController,
@@ -28,9 +32,11 @@
2832
HIDController,
2933
HIDDecoder,
3034
_lib,
35+
button_to_bit,
3136
hiddrv_test,
3237
)
33-
from scc.drivers.usb import register_hotplug_device
38+
from scc.drivers.usb import USBDevice, register_hotplug_device
39+
from scc.lib.hidraw import HIDRaw
3440
from scc.tools import init_logging, set_logging_level
3541

3642
if TYPE_CHECKING:
@@ -45,7 +51,7 @@
4551
DS4_V1_PRODUCT_ID = 0x05C4
4652

4753

48-
class DS4Controller(HIDController):
54+
class DS4Controller(Controller):
4955
# Most of axes are the same
5056
BUTTON_MAP = (
5157
SCButtons.X,
@@ -73,6 +79,11 @@ class DS4Controller(HIDController):
7379
)
7480

7581

82+
def __init__(self, daemon: "SCCDaemon") -> None:
83+
self.daemon = daemon
84+
Controller.__init__(self)
85+
86+
7687
def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode):
7788
# Overrided and hardcoded
7889
self._decoder = HIDDecoder()
@@ -143,9 +154,8 @@ def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode):
143154
for x in range(BUTTON_COUNT):
144155
self._decoder.buttons.button_map[x] = 64
145156
for x, sc in enumerate(DS4Controller.BUTTON_MAP):
146-
self._decoder.buttons.button_map[x] = self.button_to_bit(sc)
157+
self._decoder.buttons.button_map[x] = button_to_bit(sc)
147158

148-
self._packet_size = 64
149159

150160
# TODO: Which commit made data switch from bytes to bytearray?
151161
def input(self, endpoint: int, data: bytearray) -> None:
@@ -192,6 +202,82 @@ def _generate_id(self) -> str:
192202
return id
193203

194204

205+
class DS4HIDController(DS4Controller, HIDController):
206+
def __init__(self, device: "USBDevice", daemon: "SCCDaemon", handle: "USBDeviceHandle", config_file, config, test_mode = False):
207+
DS4Controller.__init__(self, daemon)
208+
HIDController.__init__(self, device, daemon, handle, config_file, config, test_mode)
209+
210+
211+
class DS4HIDRawController(DS4Controller, Controller):
212+
def __init__(self, driver: "DS4HIDRawDriver", syspath, hidrawdev: "HIDRaw", vid, pid) -> None:
213+
self.driver = driver
214+
self.syspath = syspath
215+
216+
DS4Controller.__init__(self, driver.daemon)
217+
218+
self._device_name = hidrawdev.getName()
219+
self._hidrawdev = hidrawdev
220+
self._fileno = hidrawdev._device.fileno()
221+
self._id = self._generate_id() if driver else "-"
222+
223+
self._packet_size = 78
224+
self._load_hid_descriptor(driver.config, self._packet_size, vid, pid, None)
225+
226+
# self._set_operational()
227+
self.read_serial()
228+
self._poller = self.daemon.get_poller()
229+
if self._poller:
230+
self._poller.register(self._fileno, self._poller.POLLIN, self._input)
231+
# self.daemon.get_device_monitor().add_remove_callback(syspath, self.close)
232+
self.daemon.add_controller(self)
233+
234+
def read_serial(self):
235+
self._serial = (self._hidrawdev
236+
.getPhysicalAddress().replace(b":", b""))
237+
238+
def _input(self, *args):
239+
data = self._hidrawdev.read(self._packet_size)
240+
if data[0] != 0x11:
241+
return
242+
self.input(self._fileno, data[2:])
243+
244+
def close(self):
245+
if self._poller:
246+
self._poller.unregister(self._fileno)
247+
248+
self.daemon.remove_controller(self)
249+
self._hidrawdev._device.close()
250+
251+
252+
class DS4HIDRawDriver:
253+
def __init__(self, daemon: "SCCDaemon", config: dict):
254+
self.config = config
255+
self.daemon = daemon
256+
daemon.get_device_monitor().add_callback("bluetooth", VENDOR_ID, PRODUCT_ID, self.make_bt_hidraw_callback, None)
257+
daemon.get_device_monitor().add_callback("bluetooth", VENDOR_ID, DS4_V1_PRODUCT_ID, self.make_bt_hidraw_callback, None)
258+
259+
def retry(self, syspath: str):
260+
pass
261+
262+
def make_bt_hidraw_callback(self, syspath: str, vid, pid, *whatever):
263+
hidrawname = self.daemon.get_device_monitor().get_hidraw(syspath)
264+
if hidrawname is None:
265+
return None
266+
try:
267+
dev = HIDRaw(open(os.path.join("/dev/", hidrawname), "w+b"))
268+
return DS4HIDRawController(self, syspath, dev, vid, pid)
269+
except Exception as e:
270+
log.exception(e)
271+
return None
272+
273+
def get_device_name(self):
274+
return "Dualshock 4 over Bluetooth HIDRaw"
275+
276+
def get_type(self):
277+
return "ds4bt_hidraw"
278+
279+
280+
195281
class DS4EvdevController(EvdevController):
196282
TOUCH_FACTOR_X = STICK_PAD_MAX / 940.0
197283
TOUCH_FACTOR_Y = STICK_PAD_MAX / 470.0
@@ -381,8 +467,8 @@ def _generate_id(self) -> str:
381467
def init(daemon: "SCCDaemon", config: dict) -> bool:
382468
"""Register hotplug callback for DS4 device."""
383469

384-
def hid_callback(device, handle) -> DS4Controller:
385-
return DS4Controller(device, daemon, handle, None, None)
470+
def hid_callback(device, handle) -> DS4HIDController:
471+
return DS4HIDController(device, daemon, handle, None, None)
386472

387473
def make_evdev_device(sys_dev_path: str, *whatever):
388474
devices = get_evdev_devices_from_syspath(sys_dev_path)
@@ -435,7 +521,10 @@ def fail_cb(syspath: str, vid: int, pid: int) -> None:
435521
register_hotplug_device(hid_callback, VENDOR_ID, PRODUCT_ID, on_failure=fail_cb)
436522
# DS4 v.1
437523
register_hotplug_device(hid_callback, VENDOR_ID, DS4_V1_PRODUCT_ID, on_failure=fail_cb)
438-
if HAVE_EVDEV and config["drivers"].get("evdevdrv"):
524+
if config["drivers"].get("hiddrv"):
525+
# Only enable HIDRaw support for BT connections if hiddrv is enabled
526+
_drv = DS4HIDRawDriver(daemon, config)
527+
elif HAVE_EVDEV and config["drivers"].get("evdevdrv"):
439528
# DS4 v.2
440529
daemon.get_device_monitor().add_callback("bluetooth", VENDOR_ID, PRODUCT_ID, make_evdev_device, None)
441530
# DS4 v.1
@@ -449,4 +538,4 @@ def fail_cb(syspath: str, vid: int, pid: int) -> None:
449538
""" Called when executed as script """
450539
init_logging()
451540
set_logging_level(True, True)
452-
sys.exit(hiddrv_test(DS4Controller, [ "054c:09cc" ]))
541+
sys.exit(hiddrv_test(DS4HIDController, [ "054c:09cc" ]))

scc/drivers/ds5drv.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
HIDController,
4444
HIDDecoder,
4545
_lib,
46+
button_to_bit,
4647
hiddrv_test,
4748
)
4849
from scc.drivers.usb import register_hotplug_device
@@ -338,7 +339,7 @@ def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode):
338339
for x in range(BUTTON_COUNT):
339340
self._decoder.buttons.button_map[x] = 64
340341
for x, sc in enumerate(DS5Controller.BUTTON_MAP):
341-
self._decoder.buttons.button_map[x] = self.button_to_bit(sc)
342+
self._decoder.buttons.button_map[x] = button_to_bit(sc)
342343

343344
self._packet_size = 64
344345

scc/drivers/hiddrv.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959
]
6060

6161

62+
def button_to_bit(sc) -> int:
63+
sc, bit = int(sc), 0
64+
while sc and (sc & 1 == 0):
65+
sc >>= 1
66+
bit += 1
67+
if sc & 1 == 1:
68+
return bit
69+
return BUTTON_COUNT - 1
70+
71+
6272
class HIDDrvError(Exception):
6373
pass
6474
class NotHIDDevice(HIDDrvError):
@@ -286,24 +296,13 @@ def _build_button_map(self, config: dict):
286296
# Not used here
287297
pass
288298
else:
289-
buttons[keycode] = self.button_to_bit(getattr(SCButtons, value))
299+
buttons[keycode] = button_to_bit(getattr(SCButtons, value))
290300
else:
291301
buttons = list(range(BUTTON_COUNT))
292302

293303
return (ctypes.c_uint8 * BUTTON_COUNT)(*buttons)
294304

295305

296-
@staticmethod
297-
def button_to_bit(sc) -> int:
298-
sc, bit = int(sc), 0
299-
while sc and (sc & 1 == 0):
300-
sc >>= 1
301-
bit += 1
302-
if sc & 1 == 1:
303-
return bit
304-
return BUTTON_COUNT - 1
305-
306-
307306
def _build_axis_maping(self, axis, config: dict, mode = AxisMode.AXIS):
308307
"""Convert configuration mapping for _one_ axis to value situable for self._decoder.axes field."""
309308
axis_config = config.get("axes", {}).get(str(int(axis)))

0 commit comments

Comments
 (0)