25 Commits

Author SHA1 Message Date
Flo
7c79dc5fbf Update setup.py 2023-01-04 17:57:12 +01:00
Flo
98271e866f Merge pull request #39 from dsb298/dsb298-contribution
fixed left/right motors
2023-01-04 17:56:24 +01:00
devlin
caa2062cea fixed left/right motors 2023-01-02 00:14:57 -05:00
Flo
9359e6eac4 Merge pull request #37 from flok/dev
Update doc
2022-10-23 09:56:49 +02:00
Florian Kaiser
c54f58bcee Update doc 2022-10-23 09:55:56 +02:00
Flo
d19bfc6507 release of 0.6.2
Update to 0.6.2 for typo and new release
2022-08-17 15:57:28 +02:00
Flo
2a448890ff Fix small typo on l2 event
Add small typo on the l2 event #35 - thanks to @thiagonc2 for reporting
2022-08-17 15:56:24 +02:00
Flo
5404852ec4 Merge pull request #34 from flok/dev
Update readme with docs link
2022-08-15 13:54:44 +02:00
Florian Kaiser
12b5743895 Update readme with docs link 2022-08-15 13:53:13 +02:00
Florian Kaiser
ab3f786013 added more documentation 2022-08-14 22:51:18 +02:00
Florian Kaiser
e8cb5de594 Update doc-strings and examples 2022-08-14 22:51:02 +02:00
Florian Kaiser
79bf833c9a Update requirements and author name in setup.py 2022-08-14 22:06:34 +02:00
Florian Kaiser
e1907e7a6f Add github action to publish docs 2022-08-14 20:29:41 +02:00
Florian Kaiser
60aa11b496 Add Docs 2022-08-14 20:27:55 +02:00
Florian Kaiser
330d117340 Fix wrong import v0.6.1 2022-08-14 16:20:15 +02:00
Flo
9d8ab950de Merge pull request #32 from flok/event_system
Event System and Gyro / Accelerometer support
2022-08-14 15:55:03 +02:00
Florian Kaiser
40f74472d7 Update README.md and version to 0.6 2022-08-14 15:51:39 +02:00
Florian Kaiser
b091660130 Add event system for states, added gyro and accelerometer 2022-08-14 15:48:58 +02:00
Florian Kaiser
82d407bfe2 Linting with pep8 2022-08-14 14:08:57 +02:00
Flo
0f279f1ee8 Update hidapi-usb version for linux support 2021-08-05 23:02:39 +02:00
Flo
624d17c919 Update README.md with Linux instructions 2021-08-05 21:55:24 +02:00
Florian Kaiser
24c628a182 Update package version 2021-06-28 23:04:55 +02:00
Florian Kaiser
36e8886754 Update version 2021-06-28 23:03:23 +02:00
Florian Kaiser
28605e0023 Update with linux support over usb 2021-06-28 23:00:22 +02:00
Florian Kaiser
f5529f1463 Add state for microphone button 2021-03-07 21:40:55 +01:00
24 changed files with 785 additions and 117 deletions

22
.github/workflows/docs_publish.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: publish_docs
on: [push, pull_request, workflow_dispatch]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Install dependencies
run: |
pip install sphinx furo
- name: Sphinx build
run: |
sphinx-build docs/source docs/build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
with:
publish_branch: gh-pages
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/build/
force_orphan: true

View File

@@ -1,28 +1,56 @@
# pydualsense
control your dualsense through python. using the hid library this package implements the report features for controlling your new PS5 controller.
control your dualsense through python. using the hid library this package implements the report features for controlling your PS5 controller.
# install
# Documentation
You can find the documentation at [docs](https://flok.github.io/pydualsense/)
# Installation
## Windows
Download [hidapi](https://github.com/libusb/hidapi/releases) and place the x64 .dll file into your Workspace. After that install the package from [pypi](https://pypi.org/project/pydualsense/).
```bash
pip install pydualsense
pip install --upgrade pydualsense
```
## Linux
On Linux based system you first need to install the hidapi through your package manager of your system.
On an Ubuntu system the package ```libhidapi-dev``` is required.
```bash
sudo apt install libhidapi-dev
```
After that install the package from [pypi](https://pypi.org/project/pydualsense/).
```bash
pip install --upgrade pydualsense
```
# usage
```python
from pydualsense import pydualsense, TriggerModes
def cross_pressed(state):
print(state)
ds = pydualsense() # open controller
ds.init() # initialize controller
ds.cross_pressed += cross_pressed
ds.light.setColorI(255,0,0) # set touchpad color to red
ds.triggerL.setMode(TriggerModes.Rigid)
ds.triggerL.setForce(1, 255)
ds.close() # closing the controller
```
See [examples](https://github.com/flok/pydualsense/tree/master/examples) folder for some more ideas
See [examples](https://github.com/flok/pydualsense/tree/master/examples) or [examples docs](https://flok.github.io/pydualsense/examples.html) folder for some more ideas
# Help wanted
@@ -30,7 +58,7 @@ Help wanted from people that want to use this and have feature requests. Just op
# dependecies
- hidapi-usb >= 0.2.6
- hidapi-usb >= 0.3
# Credits
@@ -45,5 +73,5 @@ Most stuff for this implementation were provided by and used from:
- add bluetooth support
- add multiple controllers
- reading the states of the controller to enable a fully compatibility with python - partially done
- partially done
- add documentation using sphinx

0
docs/.nojekyll Normal file
View File

23
docs/Makefile Normal file
View File

@@ -0,0 +1,23 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
generate_doc:
sphinx-apidoc ../pydualsense -f -o ./source --ext-autodoc --ext-coverage --ext-todo

10
docs/source/api.rst Normal file
View File

@@ -0,0 +1,10 @@
API
===
This is the front page for the API documentation of the **pydualsense** library.
.. toctree::
ds_enum
ds_main
ds_eventsystem

43
docs/source/conf.py Normal file
View File

@@ -0,0 +1,43 @@
import sys
import os
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'pydualsense'
copyright = '2022, Florian (flok) K'
author = 'Florian (flok) K'
release = '0.6.1'
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath('../../'))
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.coverage', 'sphinx.ext.todo']
templates_path = ['templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['static']
autodoc_default_options = {
'members': True,
'member-order': 'bysource',
'special-members': '__init__',
'undoc-members': True,
'exclude-members': '__weakref__'
}
autoclass_content = 'both'
todo_include_todos = True

11
docs/source/ds_enum.rst Normal file
View File

@@ -0,0 +1,11 @@
pydualsense enums classes
=========================
The enum module provides the used `enums` by **pydualsense**. These `enums` are used to update the state of the controller as a parameter to call the used functions.
.. automodule:: pydualsense.enums
:noindex:
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,10 @@
pydualsense event system classes
=========================
The `Event System` implements the event system used for the button callbacks
.. automodule:: pydualsense.event_system
:noindex:
:members:
:undoc-members:
:show-inheritance:

11
docs/source/ds_main.rst Normal file
View File

@@ -0,0 +1,11 @@
pydualsense main class
======================
`pydualsense` is the main class of the library with the same name. It provides access to the states of the controller through manual reading of the :class:`DSState <pydualsense.pydualsense.DSState>`
.. automodule:: pydualsense.pydualsense
:noindex:
:members:
:undoc-members:
:show-inheritance:

82
docs/source/examples.rst Normal file
View File

@@ -0,0 +1,82 @@
Examples
========
This pages displays some examples that on how the library can be used. All the examples can also be found inside the `examples` folder on the github repository.
.. code-block:: python
from pydualsense import *
def cross_down(state):
print(f'cross {state}')
def circle_down(state):
print(f'circle {state}')
def dpad_down(state):
print(f'dpad down {state}')
def joystick(stateX, stateY):
print(f'joystick {stateX} {stateY}')
def gyro_changed(pitch, yaw, roll):
print(f'{pitch}, {yaw}, {roll}')
# create dualsense
dualsense = pydualsense()
# find device and initialize
dualsense.init()
# add events handler functions
dualsense.cross_pressed += cross_down
dualsense.circle_pressed += circle_down
dualsense.dpad_down += dpad_down
dualsense.left_joystick_changed += joystick
dualsense.gyro_changed += gyro_changed
# read controller state until R1 is pressed
while not dualsense.state.R1:
...
# close device
dualsense.close()
The above example demonstrates the newly added c# like event system that makes it possible to trigger an event for the inputs of the controller.
.. code-block:: python
from pydualsense import *
# get dualsense instance
dualsense = pydualsense()
# initialize controller and connect
dualsense.init()
print('Trigger Effect demo started')
# set left and right rumble motors
dualsense.setLeftMotor(255)
dualsense.setRightMotor(100)
# set left l2 trigger to Rigid and set index 1 to force 255
dualsense.triggerL.setMode(TriggerModes.Rigid)
dualsense.triggerL.setForce(1, 255)
# set left r2 trigger to Rigid
dualsense.triggerR.setMode(TriggerModes.Pulse_A)
dualsense.triggerR.setForce(0, 200)
dualsense.triggerR.setForce(1, 255)
dualsense.triggerR.setForce(2, 175)
# loop until r1 is pressed to feel effect
while not dualsense.state.R1:
...
# terminate the thread for message and close the device
dualsense.close()

26
docs/source/index.rst Normal file
View File

@@ -0,0 +1,26 @@
Welcome to pydualsense's documentation!
=======================================
**pydualsense** is a Python library that helps you interact with your PlayStation 5 DualSense controller. It reads the current state of the controller and also allows to update the triggers and other options on the controller.
To get started check out the :doc:`usage` section for more information on how to install.
.. note::
This project is under active development.
Contents
--------
.. toctree::
usage
api
examples
modules
TODOs
-----
.. todolist::

7
docs/source/modules.rst Normal file
View File

@@ -0,0 +1,7 @@
pydualsense
===========
.. toctree::
:maxdepth: 4
pydualsense

View File

@@ -0,0 +1,45 @@
pydualsense package
===================
Submodules
----------
pydualsense.enums module
------------------------
.. automodule:: pydualsense.enums
:members:
:undoc-members:
:show-inheritance:
pydualsense.event\_system module
--------------------------------
.. automodule:: pydualsense.event_system
:members:
:undoc-members:
:show-inheritance:
pydualsense.hidguardian module
------------------------------
.. automodule:: pydualsense.hidguardian
:members:
:undoc-members:
:show-inheritance:
pydualsense.pydualsense module
------------------------------
.. automodule:: pydualsense.pydualsense
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pydualsense
:members:
:undoc-members:
:show-inheritance:

38
docs/source/usage.rst Normal file
View File

@@ -0,0 +1,38 @@
Usage
=====
Installation
------------
To use **pydualsense**, first install it using pip:
.. code-block:: console
(.venv) $ pip install --upgrade pydualsense
This install the needed dependencies and the **pydualsense** library itself.
Windows
-------
If you are on Windows the hidapi need to downloaded from `here <https://github.com/libusb/hidapi/releases>`_.
The downloaded `.dll` file need to be placed in a path that is in your environments variable `path`.
Linux based
-----------
If you are on a linux based system (e.g debian) you need to first need to install the hidapi through your package manager.
On Ubuntu systems the package `libhidapi-dev` is required.
.. code-block:: console
sudo apt install libhidapi-dev
Examples
--------
For code examles on using the library see :doc:`examples`

View File

@@ -16,7 +16,8 @@ dualsense.triggerR.setForce(0, 200)
dualsense.triggerR.setForce(1, 255)
dualsense.triggerR.setForce(2, 175)
import time; time.sleep(3)
# loop until r1 is pressed to feel effect
while not dualsense.state.R1:
...
# terminate the thread for message and close the device
dualsense.close()

View File

@@ -6,7 +6,7 @@ dualsense.init()
# set color around touchpad to red
dualsense.light.setColorI(255,0,0)
# mute microphone
dualsense.audio.setMicrophoneMute(True)
dualsense.audio.setMicrophoneState(True)
# set all player 1 indicator on
dualsense.light.setPlayerID(PlayerID.player1)
# sleep a little to see the result on the controller

View File

@@ -1,15 +1,41 @@
from pydualsense import *
def cross_down(state):
print(f'cross {state}')
def circle_down(state):
print(f'circle {state}')
def dpad_down(state):
print(f'dpad {state}')
def joystick(stateX, stateY):
print(f'lj {stateX} {stateY}')
def gyro_changed(pitch, yaw, roll):
print(f'{pitch}, {yaw}, {roll}')
# create dualsense
dualsense = pydualsense()
# find device and initialize
dualsense.init()
# add events handler functions
dualsense.cross_pressed += cross_down
dualsense.circle_pressed += circle_down
dualsense.dpad_down += dpad_down
dualsense.left_joystick_changed += joystick
dualsense.gyro_changed += gyro_changed
# read controller state until R1 is pressed
while not dualsense.state.R1:
print(f"Circle : {dualsense.state.circle} Cross : {dualsense.state.cross} L Stick X : {dualsense.state.LX} L Stick Y : {dualsense.state.LY}")
...
# close device
dualsense.close()

View File

@@ -1,2 +1,3 @@
from .enums import LedOptions,Brightness,PlayerID,PulseOptions,TriggerModes
from .pydualsense import pydualsense, DSLight, DSState, DSTouchpad, DSTrigger, DSAudio
from .enums import LedOptions, Brightness, PlayerID, PulseOptions, TriggerModes
from .event_system import Event
from .pydualsense import pydualsense, DSLight, DSState, DSTouchpad, DSTrigger, DSAudio

View File

@@ -1,41 +1,46 @@
from enum import IntFlag
class ConnectionType(IntFlag):
BT = 0x0,
BT = 0x0
USB = 0x1
class LedOptions(IntFlag):
Off=0x0,
PlayerLedBrightness=0x1,
UninterrumpableLed=0x2,
Both=0x01 | 0x02
Off = 0x0
PlayerLedBrightness = 0x1
UninterrumpableLed = 0x2
Both = 0x01 | 0x02
class PulseOptions(IntFlag):
Off=0x0,
FadeBlue=0x1,
FadeOut=0x2
Off = 0x0
FadeBlue = 0x1
FadeOut = 0x2
class Brightness(IntFlag):
high = 0x0,
medium = 0x1,
high = 0x0
medium = 0x1
low = 0x2
class PlayerID(IntFlag):
player1 = 4,
player2 = 10,
player3 = 21,
player4 = 27,
all = 31
PLAYER_1 = 4
PLAYER_2 = 10
PLAYER_3 = 21
PLAYER_4 = 27
ALL = 31
class TriggerModes(IntFlag):
Off = 0x0, # no resistance
Rigid = 0x1, # continous resistance
Pulse = 0x2, # section resistance
Rigid_A = 0x1 | 0x20,
Rigid_B = 0x1 | 0x04,
Rigid_AB = 0x1 | 0x20 | 0x04,
Pulse_A = 0x2 | 0x20,
Pulse_B = 0x2 | 0x04,
Pulse_AB = 0x2 | 0x20 | 0x04,
Calibration= 0xFC
Off = 0x0 # no resistance
Rigid = 0x1 # continous resistance
Pulse = 0x2 # section resistance
Rigid_A = 0x1 | 0x20
Rigid_B = 0x1 | 0x04
Rigid_AB = 0x1 | 0x20 | 0x04
Pulse_A = 0x2 | 0x20
Pulse_B = 0x2 | 0x04
Pulse_AB = 0x2 | 0x20 | 0x04
Calibration = 0xFC

View File

@@ -0,0 +1,60 @@
from collections import defaultdict
class Event(object):
"""
Base class for the event driven system
"""
def __init__(self) -> None:
"""
initialise event system
"""
self._event_handler = []
def subscribe(self, fn):
"""
add a event subscription
Args:
fn (function): _description_
"""
self._event_handler.append(fn)
return self
def unsubscribe(self, fn):
"""
delete event subscription fn
Args:
fn (function): _description_
"""
self._event_handler.remove(fn)
return self
def __iadd__(self, fn):
"""
add event subscription fn
Args:
fn (function): _description_
"""
self._event_handler.append(fn)
return self
def __isub__(self, fn):
"""
delete event subscription fn
Args:
fn (function): _description_
"""
self._event_handler.remove(fn)
return self
def __call__(self, *args, **keywargs):
"""
calls all event subscription functions
"""
for eventhandler in self._event_handler:
eventhandler(*args, **keywargs)

View File

@@ -1,18 +1,20 @@
import winreg
import sys
def check_hide() -> bool:
"""check if hidguardian is used and controller is hidden
"""
if sys.platform.startswith('win32'):
try:
access_reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
access_key = winreg.OpenKey(access_reg, 'SYSTEM\CurrentControlSet\Services\HidGuardian\Parameters', 0, winreg.KEY_READ)
affected_devices = winreg.QueryValueEx(access_key, 'AffectedDevices')[0]
if "054C" in affected_devices and "0CE6" in affected_devices:
return True
return False
except OSError as e:
pass
return False
def check_hide() -> bool:
"""
check if hidguardian is used and controller is hidden
"""
if sys.platform.startswith('win32'):
try:
access_reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
access_key = winreg.OpenKey(access_reg, r'SYSTEM\CurrentControlSet\Services\HidGuardian\Parameters', 0, winreg.KEY_READ)
affected_devices = winreg.QueryValueEx(access_key, 'AffectedDevices')[0]
if "054C" in affected_devices and "0CE6" in affected_devices:
return True
return False
except OSError as e:
pass
return False

View File

@@ -1,65 +1,147 @@
import logging
import os
import sys
from sys import platform
# needed for python > 3.8
import os, sys
if sys.platform.startswith('win32') and sys.version_info >= (3,8):
if platform.startswith('Windows') and sys.version_info >= (3, 8):
os.add_dll_directory(os.getcwd())
import hidapi
from .enums import (LedOptions, PlayerID, PulseOptions, TriggerModes, Brightness, ConnectionType) # type: ignore
import threading
from .event_system import Event
from copy import deepcopy
logger = logging.getLogger()
FORMAT = '%(asctime)s %(message)s'
logging.basicConfig(format=FORMAT)
logger.setLevel(logging.INFO)
class pydualsense:
def __init__(self, verbose: bool = False) -> None:#
def __init__(self, verbose: bool = False) -> None:
"""
initialise the library but dont connect to the controller. call :func:`init() <pydualsense.pydualsense.init>` to connect to the controller
Args:
verbose (bool, optional): display verbose out (debug prints of input and output). Defaults to False.
"""
# TODO: maybe add a init function to not automatically allocate controller when class is declared
self.verbose = verbose
if self.verbose:
logger.setLevel(logging.DEBUG)
self.leftMotor = 0
self.rightMotor = 0
self.last_states = None
def init(self):
"""initialize module and device states
self.register_available_events()
def register_available_events(self) -> None:
"""
register all available events that can be used for the controller
"""
# button events
self.triangle_pressed = Event()
self.circle_pressed = Event()
self.cross_pressed = Event()
self.square_pressed = Event()
# dpad events
# TODO: add a event that sends the pressed key if any key is pressed
# self.dpad_changed = Event()
self.dpad_up = Event()
self.dpad_down = Event()
self.dpad_left = Event()
self.dpad_right = Event()
# joystick
self.left_joystick_changed = Event()
self.right_joystick_changed = Event()
# trigger back buttons
self.r1_changed = Event()
self.r2_changed = Event()
self.r3_changed = Event()
self.l1_changed = Event()
self.l2_changed = Event()
self.l3_changed = Event()
# misc
self.ps_pressed = Event()
self.touch_pressed = Event()
self.microphone_pressed = Event()
self.share_pressed = Event()
self.option_pressed = Event()
# trackpad touch
# handles 1 or 2 fingers
#self.trackpad_frame_reported = Event()
# gyrometer events
self.gyro_changed = Event()
self.accelerometer_changed = Event()
def init(self) -> None:
"""
initialize module and device states. Starts the sendReport background thread at the end
"""
self.device: hidapi.Device = self.__find_device()
self.light = DSLight() # control led light of ds
self.audio = DSAudio() # ds audio setting
self.triggerL = DSTrigger() # left trigger
self.triggerR = DSTrigger() # right trigger
self.state = DSState() # controller states
self.conType = self.determineConnectionType() # determine USB or BT connection
if platform.startswith('Windows'):
self.conType = self.determineConnectionType() # determine USB or BT connection
else:
# set for usb manually
self.input_report_length = 64
self.output_report_length = 64
# thread for receiving and sending
self.ds_thread = True
self.report_thread = threading.Thread(target=self.sendReport)
self.report_thread.start()
def determineConnectionType(self) -> ConnectionType:
"""
Determine the connection type of the controller. eg USB or BT.
Currently only USB is supported.
Returns:
ConnectionType: Detected connection type of the controller.
"""
if self.device._device.input_report_length == 64:
self.input_report_length = 64
self.output_report_length = 64
return ConnectionType.USB
elif self.device._device.input_report_length == 78:
elif self.device._device.input_report_length == 78:
self.input_report_length = 78
self.output_report_length = 78
return ConnectionType.BT
def close(self):
def close(self) -> None:
"""
Stops the report thread and closes the HID device
"""
# TODO: reset trigger effect to default
self.ds_thread = False
self.report_thread.join()
self.device.close()
def __find_device(self) -> hidapi.Device:
"""
find HID device and open it
find HID dualsense device and open it
Raises:
Exception: HIDGuardian detected
@@ -80,14 +162,13 @@ class pydualsense:
if device.vendor_id == 0x054c and device.product_id == 0x0CE6:
detected_device = device
if detected_device == None:
if detected_device is None:
raise Exception('No device detected')
dual_sense = hidapi.Device(vendor_id=detected_device.vendor_id, product_id=detected_device.product_id)
return dual_sense
def setLeftMotor(self, intensity: int):
def setLeftMotor(self, intensity: int) -> None:
"""
set left motor rumble
@@ -105,8 +186,7 @@ class pydualsense:
raise Exception('maximum intensity is 255')
self.leftMotor = intensity
def setRightMotor(self, intensity: int):
def setRightMotor(self, intensity: int) -> None:
"""
set right motor rumble
@@ -124,26 +204,24 @@ class pydualsense:
raise Exception('maximum intensity is 255')
self.rightMotor = intensity
def sendReport(self):
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:
print(inReport)
logger.debug(inReport)
# decrypt the packet and bind the inputs
self.readInput(inReport)
# prepare new report for device
outReport = self.prepareReport()
# write the report to the device
self.writeReport(outReport)
def readInput(self, inReport):
def readInput(self, inReport) -> None:
"""
read the input from the controller and assign the states
@@ -167,7 +245,6 @@ class pydualsense:
self.state.cross = (buttonState & (1 << 5)) != 0
self.state.square = (buttonState & (1 << 4)) != 0
# dpad
dpad_state = buttonState & 0x0F
self.state.setDPadState(dpad_state)
@@ -185,7 +262,7 @@ class pydualsense:
misc2 = states[10]
self.state.ps = (misc2 & (1 << 0)) != 0
self.state.touchBtn = (misc2 & 0x02) != 0
self.state.micBtn = (misc2 & 0x04) != 0
# trackpad touch
self.state.trackPadTouch0.ID = inReport[33] & 0x7F
@@ -199,18 +276,104 @@ class pydualsense:
self.state.trackPadTouch1.X = ((inReport[39] & 0x0f) << 8) | (inReport[38])
self.state.trackPadTouch1.Y = ((inReport[40]) << 4) | ((inReport[39] & 0xf0) >> 4)
# print(f'1Active = {self.state.trackPadTouch0.isActive}')
# print(f'X1: {self.state.trackPadTouch0.X} Y2: {self.state.trackPadTouch0.Y}')
# accelerometer
self.state.accelerometer.X = int.from_bytes(([inReport[16], inReport[17]]), byteorder='little', signed=True)
self.state.accelerometer.Y = int.from_bytes(([inReport[18], inReport[19]]), byteorder='little', signed=True)
self.state.accelerometer.Z = int.from_bytes(([inReport[20], inReport[21]]), byteorder='little', signed=True)
# print(f'2Active = {self.state.trackPadTouch1.isActive}')
# print(f'X2: {self.state.trackPadTouch1.X} Y2: {self.state.trackPadTouch1.Y}')
# print(f'DPAD {self.state.DpadLeft} {self.state.DpadUp} {self.state.DpadRight} {self.state.DpadDown}')
# gyrometer
self.state.gyro.Pitch = int.from_bytes(([inReport[22], inReport[23]]), byteorder='little', signed=True)
self.state.gyro.Yaw = int.from_bytes(([inReport[24], inReport[25]]), byteorder='little', signed=True)
self.state.gyro.Roll = int.from_bytes(([inReport[26], inReport[27]]), byteorder='little', signed=True)
# 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)
return
# send all events if neede
if self.state.circle != self.last_states.circle:
self.circle_pressed(self.state.circle)
if self.state.cross != self.last_states.cross:
self.cross_pressed(self.state.cross)
if self.state.triangle != self.last_states.triangle:
self.triangle_pressed(self.state.triangle)
if self.state.square != self.last_states.square:
self.square_pressed(self.state.square)
if self.state.DpadDown != self.last_states.DpadDown:
self.dpad_down(self.state.DpadDown)
if self.state.DpadLeft != self.last_states.DpadLeft:
self.dpad_left(self.state.DpadLeft)
if self.state.DpadRight != self.last_states.DpadRight:
self.dpad_right(self.state.DpadRight)
if self.state.DpadUp != self.last_states.DpadUp:
self.dpad_up(self.state.DpadUp)
if self.state.LX != self.last_states.LX or self.state.LY != self.last_states.LY:
self.left_joystick_changed(self.state.LX, self.state.LY)
if self.state.RX != self.last_states.RX or self.state.RY != self.last_states.RY:
self.right_joystick_changed(self.state.RX, self.state.RY)
if self.state.R1 != self.last_states.R1:
self.r1_changed(self.state.R1)
if self.state.R2 != self.last_states.R2:
self.r2_changed(self.state.R2)
if self.state.L1 != self.last_states.L1:
self.l1_changed(self.state.L1)
if self.state.L2 != self.last_states.L2:
self.l2_changed(self.state.L2)
if self.state.R3 != self.last_states.R3:
self.r3_changed(self.state.R3)
if self.state.L3 != self.last_states.L3:
self.l3_changed(self.state.L3)
if self.state.ps != self.last_states.ps:
self.ps_pressed(self.state.ps)
if self.state.touchBtn != self.last_states.touchBtn:
self.touch_pressed(self.state.touchBtn)
if self.state.micBtn != self.last_states.micBtn:
self.microphone_pressed(self.state.micBtn)
if self.state.share != self.last_states.share:
self.share_pressed(self.state.share)
if self.state.options != self.last_states.options:
self.option_pressed(self.state.options)
if self.state.accelerometer.X != self.last_states.accelerometer.X or \
self.state.accelerometer.Y != self.last_states.accelerometer.Y or \
self.state.accelerometer.Z != self.last_states.accelerometer.Z:
self.accelerometer_changed(self.state.accelerometer.X, self.state.accelerometer.Y, self.state.accelerometer.Z)
if self.state.gyro.Pitch != self.last_states.gyro.Pitch or \
self.state.gyro.Yaw != self.last_states.gyro.Yaw or \
self.state.gyro.Roll != self.last_states.gyro.Roll:
self.gyro_changed(self.state.gyro.Pitch, self.state.gyro.Yaw, self.state.gyro.Roll)
"""
copy current state into temp object to check next cycle if a change occuret
and event trigger is needed
"""
self.last_states = deepcopy(self.state) # copy current state into object to check next time
# TODO: implement gyrometer and accelerometer
# TODO: control mouse with touchpad for fun as DS4Windows
def writeReport(self, outReport):
def writeReport(self, outReport) -> None:
"""
write the report to the device
@@ -219,8 +382,7 @@ class pydualsense:
"""
self.device.write(bytes(outReport))
def prepareReport(self):
def prepareReport(self) -> None:
"""
prepare the output to be send to the controller
@@ -232,7 +394,6 @@ class pydualsense:
# packet type
outReport[0] = 0x2
# flags determing what changes this packet will perform
# 0x01 set the main motors (also requires flag 0x02); setting this by itself will allow rumble to gracefully terminate and then re-enable audio haptics, whereas not setting it will kill the rumble instantly and re-enable audio haptics.
# 0x02 set the main motors (also requires flag 0x01; without bit 0x01 motors are allowed to time out without re-enabling audio haptics)
@@ -254,15 +415,15 @@ class pydualsense:
# 0x80 ???
outReport[2] = 0x1 | 0x2 | 0x4 | 0x10 | 0x40 # [2]
outReport[3] = self.leftMotor # left low freq motor 0-255 # [3]
outReport[4] = self.rightMotor # right low freq motor 0-255 # [4]
outReport[3] = self.rightMotor # right low freq motor 0-255 # [3]
outReport[4] = self.leftMotor # left low freq motor 0-255 # [4]
# outReport[5] - outReport[8] audio related
# set Micrphone LED, setting doesnt effect microphone settings
outReport[9] = self.audio.microphone_led # [9]
outReport[10] = 0x10 if self.audio.microphone_state == True else 0x00
outReport[10] = 0x10 if self.audio.microphone_mute is True else 0x00
# add right trigger mode + parameters to packet
outReport[11] = self.triggerR.mode.value
@@ -290,11 +451,17 @@ class pydualsense:
outReport[45] = self.light.TouchpadColor[0]
outReport[46] = self.light.TouchpadColor[1]
outReport[47] = self.light.TouchpadColor[2]
if self.verbose:
print(outReport)
logger.debug(outReport)
return outReport
class DSTouchpad:
"""
Dualsense Touchpad class. Contains X and Y position of touch and if the touch isActive
"""
def __init__(self) -> None:
"""
Class represents the Touchpad of the controller
@@ -304,19 +471,31 @@ class DSTouchpad:
self.X = 0
self.Y = 0
class DSState:
def __init__(self) -> None:
self.packerC = 0
"""
All dualsense states (inputs) that can be read. Second method to check if a input is pressed.
"""
self.square, self.triangle, self.circle, self.cross = False, False, False, False
self.DpadUp, self.DpadDown, self.DpadLeft, self.DpadRight = False, False, False, False
self.L1, self.L2, self.L3, self.R1, self.R2, self.R3, self.R2Btn, self.L2Btn = False, False, False, False, False, False, False, False
self.share, self.options, self.ps, self.touch1, self.touch2, self.touchBtn, self.touchRight, self.touchLeft = False, False, False, False, False, False, False, False
self.touchFinger1, self.touchFinger2 = False, False
self.RX, self.RY, self.LX, self.LY = 128,128,128,128
self.micBtn = False
self.RX, self.RY, self.LX, self.LY = 128, 128, 128, 128
self.trackPadTouch0, self.trackPadTouch1 = DSTouchpad(), DSTouchpad()
self.gyro = DSGyro()
self.accelerometer = DSAccelerometer()
def setDPadState(self, dpad_state):
def setDPadState(self, dpad_state: int):
"""
Sets the dpad state variables according to the integers that was read from the controller
Args:
dpad_state (int): integer number representing the dpad state
"""
if dpad_state == 0:
self.DpadUp = True
self.DpadDown = False
@@ -370,10 +549,10 @@ class DSLight:
"""
def __init__(self) -> None:
self.brightness: Brightness = Brightness.low # sets
self.playerNumber: PlayerID = PlayerID.player1
self.ledOption : LedOptions = LedOptions.Both
self.pulseOptions : PulseOptions = PulseOptions.Off
self.TouchpadColor = (0,0,255)
self.playerNumber: PlayerID = PlayerID.PLAYER_1
self.ledOption: LedOptions = LedOptions.Both
self.pulseOptions: PulseOptions = PulseOptions.Off
self.TouchpadColor = (0, 0, 255)
def setLEDOption(self, option: LedOptions):
"""
@@ -417,7 +596,7 @@ class DSLight:
raise TypeError('Need Brightness type')
self.brightness = brightness
def setPlayerID(self, player : PlayerID):
def setPlayerID(self, player: PlayerID):
"""
Sets the PlayerID of the controller with the choosen LEDs.
The controller has 4 Player states
@@ -432,7 +611,7 @@ class DSLight:
raise TypeError('Need PlayerID type')
self.playerNumber = player
def setColorI(self, r: int , g: int, b: int) -> None:
def setColorI(self, r: int, g: int, b: int) -> None:
"""
Sets the Color around the Touchpad of the controller
@@ -450,8 +629,7 @@ class DSLight:
# check if color is out of bounds
if (r > 255 or g > 255 or b > 255) or (r < 0 or g < 0 or b < 0):
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:
"""
@@ -467,15 +645,18 @@ class DSLight:
if not isinstance(color, tuple):
raise TypeError('Color type is tuple')
# unpack for out of bounds check
r,g,b = map(int, color)
r, g, b = map(int, color)
# check if color is out of bounds
if (r > 255 or g > 255 or b > 255) or (r < 0 or g < 0 or b < 0):
raise Exception('colors have values from 0 to 255 only')
self.TouchpadColor = (r,g,b)
self.TouchpadColor = (r, g, b)
class DSAudio:
def __init__(self) -> None:
"""
initialize the limited Audio features of the controller
"""
self.microphone_mute = 0
self.microphone_led = 0
@@ -491,22 +672,36 @@ class DSAudio:
Exception: false state for the led
"""
if not isinstance(value, bool):
raise TypeError('MicrophoneLED can only be a bool')
raise TypeError('MicrophoneLED can only be a bool')
self.microphone_led = value
def setMicrophoneMute(self, state):
def setMicrophoneState(self, state: bool):
"""
Set the microphone state and also sets the microphone led accordingle
Args:
state (bool): desired state of the microphone
Raises:
TypeError: state was not a bool
"""
if not isinstance(state, bool):
raise TypeError('state needs to be bool')
self.setMicrophoneLED(state) # set led accordingly
self.microphone_state = state
self.microphone_mute = state
class DSTrigger:
"""
Dualsense trigger class. Allowes for multiple :class:`TriggerModes <pydualsense.enums.TriggerModes>` and multiple forces
# TODO: make this interface more userfriendly so a developer knows what he is doing
"""
def __init__(self) -> None:
# trigger modes
self.mode : TriggerModes = TriggerModes.Off
self.mode: TriggerModes = TriggerModes.Off
# force parameters for the triggers
self.forces = [0 for i in range(7)]
@@ -545,3 +740,23 @@ class DSTrigger:
raise TypeError('Trigger mode parameter needs to be of type `TriggerModes`')
self.mode = mode
class DSGyro:
"""
Class representing the Gyro2 of the controller
"""
def __init__(self) -> None:
self.Pitch = 0
self.Yaw = 0
self.Roll = 0
class DSAccelerometer:
"""
Class representing the Accelerometer of the controller
"""
def __init__(self) -> None:
self.X = 0
self.Y = 0
self.Z = 0

View File

@@ -1 +1,3 @@
hid==1.0.4
hid-usb
sphinx
furo

View File

@@ -6,13 +6,13 @@ with open("README.md", "r") as fh:
setup(
name='pydualsense',
version='0.5.2',
version='0.6.3',
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 K',
author='Florian (flok) K',
license='MIT License',
packages=setuptools.find_packages(),
install_requires=['hidapi-usb', 'cffi']
install_requires=['hidapi-usb>=0.3', 'cffi']
)