6 Commits

Author SHA1 Message Date
876962bfa8 Updated. /JL 2025-03-21 19:31:21 +01: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
12 changed files with 1039 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
pydualsense/interface.py
pydualsense/interface.ui
pydualsense/interface.ui

View File

@@ -6,6 +6,10 @@
# PS5 DualSense controller over USB hidraw
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
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"

View File

@@ -15,4 +15,6 @@ dualsense.light.setPlayerID(PlayerID.PLAYER_1)
# this is not needed in normal usage
time.sleep(2)
# terminate the thread for message and close the device
dualsense.light.setColorI(0, 0, 255)
dualsense.close()

9
examples/sounds.py Normal file
View File

@@ -0,0 +1,9 @@
from pydualsense import *
# get dualsense instance
dualsense = pydualsense()
dualsense.init()
print('Trigger Sound demo started')
dualsense.close()

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

View File

@@ -4,6 +4,7 @@ from enum import IntFlag
class ConnectionType(IntFlag):
BT = 0x0
USB = 0x1
ERROR = 0xFF
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
"""
@@ -7,9 +11,9 @@ class Event(object):
"""
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
@@ -19,7 +23,7 @@ class Event(object):
self._event_handler.append(fn)
return self
def unsubscribe(self, fn):
def unsubscribe(self, fn: Callable) -> Any:
"""
delete event subscription fn
@@ -29,7 +33,7 @@ class Event(object):
self._event_handler.remove(fn)
return self
def __iadd__(self, fn):
def __iadd__(self, fn: Callable) -> Any:
"""
add event subscription fn
@@ -39,7 +43,7 @@ class Event(object):
self._event_handler.append(fn)
return self
def __isub__(self, fn):
def __isub__(self, fn: Callable) -> Any:
"""
delete event subscription fn
@@ -49,9 +53,9 @@ class Event(object):
self._event_handler.remove(fn)
return self
def __call__(self, *args, **keywargs):
def __call__(self, *args, **kwargs) -> None: # type: ignore[arg-type]
"""
calls all event subscription functions
"""
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__)
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 (
BatteryState,
Brightness,
ConnectionType,
LedOptions,
PlayerID,
PulseOptions,
TriggerModes,
Brightness,
ConnectionType,
BatteryState,
) # type: ignore
import threading
)
from .event_system import Event
from .checksum import compute
from copy import deepcopy
logger = logging.getLogger()
FORMAT = "%(asctime)s %(message)s"
@@ -29,7 +31,7 @@ logging.basicConfig(format=FORMAT)
logger.setLevel(logging.INFO)
class pydualsense:
class pydualsense: # noqa: N801
OUTPUT_REPORT_USB = 0x02
OUTPUT_REPORT_BT = 0x31
@@ -49,7 +51,7 @@ class pydualsense:
self.leftMotor = 0
self.rightMotor = 0
self.last_states = None
self.last_states: DSState = None # type: ignore[assignment]
self.register_available_events()
@@ -113,7 +115,10 @@ class pydualsense:
self.state = DSState() # controller states
self.battery = DSBattery()
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.connected = True
self.report_thread = threading.Thread(target=self.sendReport)
self.report_thread.start()
self.states = None
@@ -144,6 +149,8 @@ class pydualsense:
self.output_report_length = 78
return ConnectionType.BT
return ConnectionType.ERROR
def close(self) -> None:
"""
Stops the report thread and closes the HID device
@@ -177,7 +184,7 @@ class pydualsense:
detected_device: hidapi.Device = None
devices = hidapi.enumerate(vendor_id=0x054C)
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
if detected_device is None:
@@ -227,42 +234,48 @@ class pydualsense:
def sendReport(self) -> None:
"""background thread handling the reading of the device and updating its states"""
while self.ds_thread:
# read data from the input report of the controller
inReport = self.device.read(self.input_report_length)
if self.verbose:
logger.debug(inReport)
# decrypt the packet and bind the inputs
self.readInput(inReport)
try:
# read data from the input report of the controller
inReport = self.device.read(self.input_report_length)
if self.verbose:
logger.debug(inReport)
# decrypt the packet and bind the inputs
self.readInput(inReport)
# prepare new report for device
outReport = self.prepareReport()
# prepare new report for device
outReport = self.prepareReport()
# write the report to the device
self.writeReport(outReport)
# write the report to the device
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
Args:
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
self.state.LX = states[1] - 128
self.state.LY = states[2] - 128
self.state.RX = states[3] - 128
self.state.RY = states[4] - 128
self.state.L2 = states[5]
self.state.R2 = states[6]
self.state.L2 = bool(states[5])
self.state.R2 = bool(states[6])
# 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
if self.last_states is None:
self.last_states = deepcopy(self.state)
self.last_states: DSState = deepcopy(self.state) # type: ignore[assignment]
return
# send all events if neede
@@ -433,7 +446,7 @@ class pydualsense:
# 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
@@ -442,18 +455,18 @@ class pydualsense:
"""
self.device.write(bytes(outReport))
def prepareReport(self) -> None:
def prepareReport(self) -> List[int]:
"""
prepare the output to be send to the controller
Returns:
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:
outReport = (
[0] * self.output_report_length
) # create empty list with range of output report
# packet type
outReport[0] = self.OUTPUT_REPORT_USB
@@ -516,9 +529,6 @@ class pydualsense:
outReport[47] = self.light.TouchpadColor[2]
elif self.conType == ConnectionType.BT:
outReport = (
[0] * self.output_report_length
) # create empty list with range of output report
# packet type
outReport[0] = self.OUTPUT_REPORT_BT # bt type
@@ -649,7 +659,7 @@ class DSState:
self.gyro = DSGyro()
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
@@ -684,7 +694,7 @@ class DSState:
elif dpad_state == 5:
self.DpadUp = False
self.DpadDown = True
self.DpadLeft = False
self.DpadLeft = True
self.DpadRight = False
elif dpad_state == 6:
self.DpadUp = False
@@ -715,7 +725,7 @@ class DSLight:
self.pulseOptions: PulseOptions = PulseOptions.Off
self.TouchpadColor = (0, 0, 255)
def setLEDOption(self, option: LedOptions):
def setLEDOption(self, option: LedOptions) -> None:
"""
Sets the LED Option
@@ -729,7 +739,7 @@ class DSLight:
raise TypeError("Need LEDOption type")
self.ledOption = option
def setPulseOption(self, option: PulseOptions):
def setPulseOption(self, option: PulseOptions) -> None:
"""
Sets the Pulse Option of the LEDs
@@ -743,7 +753,7 @@ class DSLight:
raise TypeError("Need PulseOption type")
self.pulseOptions = option
def setBrightness(self, brightness: Brightness):
def setBrightness(self, brightness: Brightness) -> None:
"""
Defines the brightness of the Player LEDs
@@ -757,7 +767,7 @@ class DSLight:
raise TypeError("Need Brightness type")
self.brightness = brightness
def setPlayerID(self, player: PlayerID):
def setPlayerID(self, player: PlayerID) -> None:
"""
Sets the PlayerID of the controller with the choosen LEDs.
The controller has 4 Player states
@@ -792,7 +802,7 @@ class DSLight:
raise Exception("colors have values from 0 to 255 only")
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
@@ -821,7 +831,7 @@ class DSAudio:
self.microphone_mute = 0
self.microphone_led = 0
def setMicrophoneLED(self, value):
def setMicrophoneLED(self, value: bool) -> None:
"""
Activates or disables the microphone led.
This doesnt change the mute/unmutes the microphone itself.
@@ -836,7 +846,7 @@ class DSAudio:
raise TypeError("MicrophoneLED can only be a bool")
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
@@ -868,7 +878,7 @@ class DSTrigger:
# force parameters for the triggers
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
@@ -888,7 +898,7 @@ class DSTrigger:
self.forces[forceID] = force
def setMode(self, mode: TriggerModes):
def setMode(self, mode: TriggerModes) -> None:
"""
Set the Mode for the Trigger

View File

@@ -1,10 +1,27 @@
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[project]
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]
name = "pydualsense"
version = "0.7.1"
version = "0.7.3"
description = "use your DualSense (PS5) controller with python"
license = "MIT"
repository = "https://github.com/flok/pydualsense"
@@ -12,13 +29,23 @@ authors = ["Florian (flok) K"]
readme = "README.md"
packages = [{include = "pydualsense"}]
include = ["pydualsense/hidapi.dll"]
keywords = ['ps5', 'controller', 'dualsense', 'pydualsense']
[tool.poetry.dependencies]
python = "^3.7"
hidapi-usb = "^0.3.1"
python = ">=3.8,<4.0"
hidapi-usb = "^0.3.2"
[tool.poetry.group.dev.dependencies]
python = ">=3.9,<4.0"
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]
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."pyproject.toml"]
[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"]
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']
)