Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
876962bfa8 | |||
|
42df47ae26 | ||
|
66deffb545 | ||
|
8a35b1696a | ||
|
17182ff311 | ||
|
645fad053d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
@@ -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"
|
||||
|
@@ -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
9
examples/sounds.py
Normal 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
970
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
@@ -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
|
||||
|
@@ -4,6 +4,7 @@ from enum import IntFlag
|
||||
class ConnectionType(IntFlag):
|
||||
BT = 0x0
|
||||
USB = 0x1
|
||||
ERROR = 0xFF
|
||||
|
||||
|
||||
class LedOptions(IntFlag):
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
18
setup.py
18
setup.py
@@ -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']
|
||||
)
|
Reference in New Issue
Block a user