6 Commits

Author SHA1 Message Date
dependabot[bot]
4858fc2450 Bump jinja2 from 3.1.5 to 3.1.6
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 04:58:38 +00:00
Flo
42df47ae26 Add apple silicon support for cffi dependency (#65) 2025-01-28 22:59:36 +01:00
Flo
66deffb545 V7.2.0 (#59)
* Add mypy, ruff stuff

* Add mypy, ruff stuff

* Update pyproject.toml with fixes

* Fix double version string, keywords

* resolve hidapi unhandled exception issue

-added try except around thread code to handle the IOError raised by hidapi.
-added 'connected' property to indicate when controller is disconnected.

(cherry picked from commit a5ed4192fb52ec6f410f785cb3289b8015f5810f)

* resolve exception on system startup

(cherry picked from commit 6aeeee1a564b509ea87c1e8ca0d90d3e4790592f)

* resolve dpad down left issue

(cherry picked from commit f58c61b7317731a4532a4acd724895a6bfa41cd1)

---------

Co-authored-by: dalethomas81 <dalethomas81@gmail.com>
2024-10-05 23:23:09 +02:00
Flo
8a35b1696a Merge pull request #57 from scj643/dualsense-edge
Add DualSense Edge support
2024-08-10 11:09:48 +02:00
Chloe Surett
17182ff311 Add Dualsense Edge 2024-07-30 11:29:37 -04:00
Chloe Surett
645fad053d Add Dualsense Edge 2024-07-30 11:28:42 -04:00
10 changed files with 1027 additions and 200 deletions

2
.gitignore vendored
View File

@@ -155,4 +155,4 @@ dmypy.json
# End of https://www.toptal.com/developers/gitignore/api/python,vscode # End of https://www.toptal.com/developers/gitignore/api/python,vscode
pydualsense/interface.py pydualsense/interface.py
pydualsense/interface.ui pydualsense/interface.ui

View File

@@ -6,6 +6,10 @@
# PS5 DualSense controller over USB hidraw # PS5 DualSense controller over USB hidraw
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0660", TAG+="uaccess"
# PS5 DualSense Edge controller over USB hidraw
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0df2", MODE="0660", TAG+="uaccess"
# PS5 DualSense controller over bluetooth hidraw # PS5 DualSense controller over bluetooth hidraw
KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", MODE="0660", TAG+="uaccess"
# PS5 DualSense Edge controller over bluetooth hidraw
KERNEL=="hidraw*", KERNELS=="*054C:0DF2*", MODE="0660", TAG+="uaccess"

969
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,4 +6,4 @@ from .enums import LedOptions, Brightness, PlayerID, PulseOptions, TriggerModes
from .event_system import Event # noqa : F401 from .event_system import Event # noqa : F401
from .pydualsense import pydualsense, DSLight, DSState, DSTouchpad, DSTrigger, DSAudio # noqa : F401 from .pydualsense import pydualsense, DSLight, DSState, DSTouchpad, DSTrigger, DSAudio # noqa : F401
__version__ = "0.7.1" __version__ = "0.7.3"

View File

@@ -1,4 +1,5 @@
import array import array
from typing import List
# from South-River # from South-River
# fmt: off # fmt: off
@@ -39,10 +40,10 @@ hashTable = array.array('I', [
# fmt:on # fmt:on
def compute(buffer): def compute(buffer: List[int]) -> int:
result = 0xEADA2D49 result: int = 0xEADA2D49
for i in range(0, 74): for i in range(74):
result = hashTable[(result & 0xFF) ^ (buffer[i] & 0xFF)] ^ (result >> 8) result = hashTable[(result & 0xFF) ^ (buffer[i] & 0xFF)] ^ (result >> 8)
return result return result

View File

@@ -4,6 +4,7 @@ from enum import IntFlag
class ConnectionType(IntFlag): class ConnectionType(IntFlag):
BT = 0x0 BT = 0x0
USB = 0x1 USB = 0x1
ERROR = 0xFF
class LedOptions(IntFlag): class LedOptions(IntFlag):

View File

@@ -1,4 +1,8 @@
class Event(object): from typing import Any, Callable, List
# mypy: disable_error_code="type-arg"
class Event:
""" """
Base class for the event driven system Base class for the event driven system
""" """
@@ -7,9 +11,9 @@ class Event(object):
""" """
initialise event system initialise event system
""" """
self._event_handler = [] self._event_handler: List[Callable] = []
def subscribe(self, fn): def subscribe(self, fn: Callable) -> Any:
""" """
add a event subscription add a event subscription
@@ -19,7 +23,7 @@ class Event(object):
self._event_handler.append(fn) self._event_handler.append(fn)
return self return self
def unsubscribe(self, fn): def unsubscribe(self, fn: Callable) -> Any:
""" """
delete event subscription fn delete event subscription fn
@@ -29,7 +33,7 @@ class Event(object):
self._event_handler.remove(fn) self._event_handler.remove(fn)
return self return self
def __iadd__(self, fn): def __iadd__(self, fn: Callable) -> Any:
""" """
add event subscription fn add event subscription fn
@@ -39,7 +43,7 @@ class Event(object):
self._event_handler.append(fn) self._event_handler.append(fn)
return self return self
def __isub__(self, fn): def __isub__(self, fn: Callable) -> Any:
""" """
delete event subscription fn delete event subscription fn
@@ -49,9 +53,9 @@ class Event(object):
self._event_handler.remove(fn) self._event_handler.remove(fn)
return self return self
def __call__(self, *args, **keywargs): def __call__(self, *args, **kwargs) -> None: # type: ignore[arg-type]
""" """
calls all event subscription functions calls all event subscription functions
""" """
for eventhandler in self._event_handler: for eventhandler in self._event_handler:
eventhandler(*args, **keywargs) eventhandler(*args, **kwargs)

View File

@@ -7,21 +7,23 @@ if platform.startswith("win32") and sys.version_info >= (3, 8):
os.environ["PATH"] += os.pathsep + os.path.dirname(__file__) os.environ["PATH"] += os.pathsep + os.path.dirname(__file__)
import hidapi import threading
from copy import deepcopy
from typing import List, Tuple
import hidapi # type: ignore[import]
from .checksum import compute
from .enums import ( from .enums import (
BatteryState,
Brightness,
ConnectionType,
LedOptions, LedOptions,
PlayerID, PlayerID,
PulseOptions, PulseOptions,
TriggerModes, TriggerModes,
Brightness, )
ConnectionType,
BatteryState,
) # type: ignore
import threading
from .event_system import Event from .event_system import Event
from .checksum import compute
from copy import deepcopy
logger = logging.getLogger() logger = logging.getLogger()
FORMAT = "%(asctime)s %(message)s" FORMAT = "%(asctime)s %(message)s"
@@ -29,7 +31,7 @@ logging.basicConfig(format=FORMAT)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
class pydualsense: class pydualsense: # noqa: N801
OUTPUT_REPORT_USB = 0x02 OUTPUT_REPORT_USB = 0x02
OUTPUT_REPORT_BT = 0x31 OUTPUT_REPORT_BT = 0x31
@@ -49,7 +51,7 @@ class pydualsense:
self.leftMotor = 0 self.leftMotor = 0
self.rightMotor = 0 self.rightMotor = 0
self.last_states = None self.last_states: DSState = None # type: ignore[assignment]
self.register_available_events() self.register_available_events()
@@ -113,7 +115,10 @@ class pydualsense:
self.state = DSState() # controller states self.state = DSState() # controller states
self.battery = DSBattery() self.battery = DSBattery()
self.conType = self.determineConnectionType() # determine USB or BT connection self.conType = self.determineConnectionType() # determine USB or BT connection
if self.conType is ConnectionType.ERROR:
raise Exception("Couldn't determine connection type")
self.ds_thread = True self.ds_thread = True
self.connected = True
self.report_thread = threading.Thread(target=self.sendReport) self.report_thread = threading.Thread(target=self.sendReport)
self.report_thread.start() self.report_thread.start()
self.states = None self.states = None
@@ -144,6 +149,8 @@ class pydualsense:
self.output_report_length = 78 self.output_report_length = 78
return ConnectionType.BT return ConnectionType.BT
return ConnectionType.ERROR
def close(self) -> None: def close(self) -> None:
""" """
Stops the report thread and closes the HID device Stops the report thread and closes the HID device
@@ -177,7 +184,7 @@ class pydualsense:
detected_device: hidapi.Device = None detected_device: hidapi.Device = None
devices = hidapi.enumerate(vendor_id=0x054C) devices = hidapi.enumerate(vendor_id=0x054C)
for device in devices: for device in devices:
if device.vendor_id == 0x054C and device.product_id == 0x0CE6: if device.vendor_id == 0x054C and device.product_id in (0x0CE6, 0x0DF2):
detected_device = device detected_device = device
if detected_device is None: if detected_device is None:
@@ -227,42 +234,48 @@ class pydualsense:
def sendReport(self) -> None: def sendReport(self) -> None:
"""background thread handling the reading of the device and updating its states""" """background thread handling the reading of the device and updating its states"""
while self.ds_thread: while self.ds_thread:
# read data from the input report of the controller try:
inReport = self.device.read(self.input_report_length) # read data from the input report of the controller
if self.verbose: inReport = self.device.read(self.input_report_length)
logger.debug(inReport) if self.verbose:
# decrypt the packet and bind the inputs logger.debug(inReport)
self.readInput(inReport) # decrypt the packet and bind the inputs
self.readInput(inReport)
# prepare new report for device # prepare new report for device
outReport = self.prepareReport() outReport = self.prepareReport()
# write the report to the device # write the report to the device
self.writeReport(outReport) self.writeReport(outReport)
except IOError:
self.connected = False
break
except AttributeError:
self.connected = False
break
def readInput(self, inReport) -> None: def readInput(self, inReport : List[int]) -> None:
""" """
read the input from the controller and assign the states read the input from the controller and assign the states
Args: Args:
inReport (bytearray): read bytearray containing the state of the whole controller inReport (bytearray): read bytearray containing the state of the whole controller
""" """
if self.conType == ConnectionType.BT:
# the reports for BT and USB are structured the same,
# but there is one more byte at the start of the bluetooth report.
# We drop that byte, so that the format matches up again.
states = list(inReport)[1:] # convert bytes to list
else: # USB
states = list(inReport) # convert bytes to list
self.states = states # the reports for BT and USB are structured the same,
# but there is one more byte at the start of the bluetooth report.
# We drop that byte, so that the format matches up again.
states: List[int] = list(inReport)[1:] if self.conType == ConnectionType.BT else list(inReport)
self.states: List[int] = states # type: ignore[assigment]
# states 0 is always 1 # states 0 is always 1
self.state.LX = states[1] - 128 self.state.LX = states[1] - 128
self.state.LY = states[2] - 128 self.state.LY = states[2] - 128
self.state.RX = states[3] - 128 self.state.RX = states[3] - 128
self.state.RY = states[4] - 128 self.state.RY = states[4] - 128
self.state.L2 = states[5] self.state.L2 = bool(states[5])
self.state.R2 = states[6] self.state.R2 = bool(states[6])
# state 7 always increments -> not used anywhere # state 7 always increments -> not used anywhere
@@ -336,7 +349,7 @@ class pydualsense:
# first call we dont have a "last state" so we create if with the first occurence # first call we dont have a "last state" so we create if with the first occurence
if self.last_states is None: if self.last_states is None:
self.last_states = deepcopy(self.state) self.last_states: DSState = deepcopy(self.state) # type: ignore[assignment]
return return
# send all events if neede # send all events if neede
@@ -433,7 +446,7 @@ class pydualsense:
# TODO: control mouse with touchpad for fun as DS4Windows # TODO: control mouse with touchpad for fun as DS4Windows
def writeReport(self, outReport) -> None: def writeReport(self, outReport : List[int]) -> None: # noqa: N803
""" """
write the report to the device write the report to the device
@@ -442,18 +455,18 @@ class pydualsense:
""" """
self.device.write(bytes(outReport)) self.device.write(bytes(outReport))
def prepareReport(self) -> None: def prepareReport(self) -> List[int]:
""" """
prepare the output to be send to the controller prepare the output to be send to the controller
Returns: Returns:
list: report to send to controller list: report to send to controller
""" """
outReport = (
[0] * self.output_report_length
) # create empty list with range of output report
if self.conType == ConnectionType.USB: if self.conType == ConnectionType.USB:
outReport = (
[0] * self.output_report_length
) # create empty list with range of output report
# packet type # packet type
outReport[0] = self.OUTPUT_REPORT_USB outReport[0] = self.OUTPUT_REPORT_USB
@@ -516,9 +529,6 @@ class pydualsense:
outReport[47] = self.light.TouchpadColor[2] outReport[47] = self.light.TouchpadColor[2]
elif self.conType == ConnectionType.BT: elif self.conType == ConnectionType.BT:
outReport = (
[0] * self.output_report_length
) # create empty list with range of output report
# packet type # packet type
outReport[0] = self.OUTPUT_REPORT_BT # bt type outReport[0] = self.OUTPUT_REPORT_BT # bt type
@@ -649,7 +659,7 @@ class DSState:
self.gyro = DSGyro() self.gyro = DSGyro()
self.accelerometer = DSAccelerometer() self.accelerometer = DSAccelerometer()
def setDPadState(self, dpad_state: int): def setDPadState(self, dpad_state: int) -> None:
""" """
Sets the dpad state variables according to the integers that was read from the controller Sets the dpad state variables according to the integers that was read from the controller
@@ -684,7 +694,7 @@ class DSState:
elif dpad_state == 5: elif dpad_state == 5:
self.DpadUp = False self.DpadUp = False
self.DpadDown = True self.DpadDown = True
self.DpadLeft = False self.DpadLeft = True
self.DpadRight = False self.DpadRight = False
elif dpad_state == 6: elif dpad_state == 6:
self.DpadUp = False self.DpadUp = False
@@ -715,7 +725,7 @@ class DSLight:
self.pulseOptions: PulseOptions = PulseOptions.Off self.pulseOptions: PulseOptions = PulseOptions.Off
self.TouchpadColor = (0, 0, 255) self.TouchpadColor = (0, 0, 255)
def setLEDOption(self, option: LedOptions): def setLEDOption(self, option: LedOptions) -> None:
""" """
Sets the LED Option Sets the LED Option
@@ -729,7 +739,7 @@ class DSLight:
raise TypeError("Need LEDOption type") raise TypeError("Need LEDOption type")
self.ledOption = option self.ledOption = option
def setPulseOption(self, option: PulseOptions): def setPulseOption(self, option: PulseOptions) -> None:
""" """
Sets the Pulse Option of the LEDs Sets the Pulse Option of the LEDs
@@ -743,7 +753,7 @@ class DSLight:
raise TypeError("Need PulseOption type") raise TypeError("Need PulseOption type")
self.pulseOptions = option self.pulseOptions = option
def setBrightness(self, brightness: Brightness): def setBrightness(self, brightness: Brightness) -> None:
""" """
Defines the brightness of the Player LEDs Defines the brightness of the Player LEDs
@@ -757,7 +767,7 @@ class DSLight:
raise TypeError("Need Brightness type") raise TypeError("Need Brightness type")
self.brightness = brightness self.brightness = brightness
def setPlayerID(self, player: PlayerID): def setPlayerID(self, player: PlayerID) -> None:
""" """
Sets the PlayerID of the controller with the choosen LEDs. Sets the PlayerID of the controller with the choosen LEDs.
The controller has 4 Player states The controller has 4 Player states
@@ -792,7 +802,7 @@ class DSLight:
raise Exception("colors have values from 0 to 255 only") raise Exception("colors have values from 0 to 255 only")
self.TouchpadColor = (r, g, b) self.TouchpadColor = (r, g, b)
def setColorT(self, color: tuple) -> None: def setColorT(self, color: Tuple[int, int, int]) -> None:
""" """
Sets the Color around the Touchpad as a tuple Sets the Color around the Touchpad as a tuple
@@ -821,7 +831,7 @@ class DSAudio:
self.microphone_mute = 0 self.microphone_mute = 0
self.microphone_led = 0 self.microphone_led = 0
def setMicrophoneLED(self, value): def setMicrophoneLED(self, value: bool) -> None:
""" """
Activates or disables the microphone led. Activates or disables the microphone led.
This doesnt change the mute/unmutes the microphone itself. This doesnt change the mute/unmutes the microphone itself.
@@ -836,7 +846,7 @@ class DSAudio:
raise TypeError("MicrophoneLED can only be a bool") raise TypeError("MicrophoneLED can only be a bool")
self.microphone_led = value self.microphone_led = value
def setMicrophoneState(self, state: bool): def setMicrophoneState(self, state: bool) -> None:
""" """
Set the microphone state and also sets the microphone led accordingle Set the microphone state and also sets the microphone led accordingle
@@ -868,7 +878,7 @@ class DSTrigger:
# force parameters for the triggers # force parameters for the triggers
self.forces = [0 for i in range(7)] self.forces = [0 for i in range(7)]
def setForce(self, forceID: int = 0, force: int = 0): def setForce(self, forceID: int = 0, force: int = 0) -> None:
""" """
Sets the forces of the choosen force parameter Sets the forces of the choosen force parameter
@@ -888,7 +898,7 @@ class DSTrigger:
self.forces[forceID] = force self.forces[forceID] = force
def setMode(self, mode: TriggerModes): def setMode(self, mode: TriggerModes) -> None:
""" """
Set the Mode for the Trigger Set the Mode for the Trigger

View File

@@ -1,10 +1,27 @@
[build-system]
requires = ["poetry-core"] [project]
build-backend = "poetry.core.masonry.api" name = "pydualsense"
version = "0.7.3"
description = "use your DualSense (PS5) controller with python"
readme = "README.md"
requires-python = ">=3.8,<4.0"
license = { text = "MIT License" }
authors = [{ name = "Florian (flok) K", email = "37000563+flok@users.noreply.github.com" }]
keywords = ['ps5', 'controller', 'dualsense', 'pydualsense']
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
[tool.poetry] [tool.poetry]
name = "pydualsense" name = "pydualsense"
version = "0.7.1" version = "0.7.3"
description = "use your DualSense (PS5) controller with python" description = "use your DualSense (PS5) controller with python"
license = "MIT" license = "MIT"
repository = "https://github.com/flok/pydualsense" repository = "https://github.com/flok/pydualsense"
@@ -12,13 +29,23 @@ authors = ["Florian (flok) K"]
readme = "README.md" readme = "README.md"
packages = [{include = "pydualsense"}] packages = [{include = "pydualsense"}]
include = ["pydualsense/hidapi.dll"] include = ["pydualsense/hidapi.dll"]
keywords = ['ps5', 'controller', 'dualsense', 'pydualsense']
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = ">=3.8,<4.0"
hidapi-usb = "^0.3.1" hidapi-usb = "^0.3.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
python = ">=3.9,<4.0"
taskipy = "^1.12.2" taskipy = "^1.12.2"
hidapi-usb = "^0.3.2"
sphinx = { version= "^7.3.7", python=">=3.9" }
furo = "^2024.5.6"
[tool.poetry.group.typing.dependencies]
mypy = "^1.3.0"
types-python-dateutil = "^2.8.19"
types-pytz = ">=2022.7.1.2"
[tool.taskipy.tasks] [tool.taskipy.tasks]
clear = "find pydualsense/ -type f \\( -iname \\*.c -o -iname \\*.cpp -o -iname \\*.pyd -o -iname \\*.so \\) -delete" clear = "find pydualsense/ -type f \\( -iname \\*.c -o -iname \\*.cpp -o -iname \\*.pyd -o -iname \\*.so \\) -delete"
@@ -29,8 +56,53 @@ post_test = "task clear"
[tool.poetry_bumpversion.file."pydualsense/__init__.py"] [tool.poetry_bumpversion.file."pydualsense/__init__.py"]
[tool.poetry_bumpversion.file."pyproject.toml"]
[tool.ruff] [tool.ruff]
fix = true
unfixable = [
"ERA", # do not autoremove commented out code
]
extend-select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"ERA", # flake8-eradicate/eradicate
"I", # isort
"N", # pep8-naming
"PIE", # flake8-pie
"PGH", # pygrep
"RUF", # ruff checks
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"TID", # flake8-tidy-imports
"UP", # pyupgrade
]
ignore = [
"B904", # use 'raise ... from err'
"B905", # use explicit 'strict=' parameter with 'zip()'
"N818", # Exception name should be named with an Error suffix
"RUF001",
"N816",
"ERA001",
"N802",
"N806"
]
target-version = "py38"
exclude = [".venv"] exclude = [".venv"]
line-length = 120 line-length = 120
[tool.mypy]
strict = true
files = "pydualsense"
show_error_codes = true
pretty = true
warn_unused_ignores = true
enable_incomplete_feature = ["Unpack"]
exclude = [
"^docs\\.py$",
"^build\\.py$",
]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,18 +0,0 @@
from setuptools import setup
import setuptools
with open("README.md", "r") as fh:
long_description = fh.read()
setup(
name='pydualsense',
version='0.7.0',
description='use your DualSense (PS5) controller with python',
long_description=long_description,
long_description_content_type="text/markdown",
url='https://github.com/flok/pydualsense',
author='Florian (flok) K',
license='MIT License',
packages=setuptools.find_packages(),
install_requires=['hidapi-usb>=0.3', 'cffi']
)