9 Commits

Author SHA1 Message Date
Florian Kaiser
d76717c163 Fix missing import 2021-01-01 20:34:42 +01:00
Florian Kaiser
786657cc90 Merge branch 'master' of https://github.com/flok/pydualsense into master 2021-01-01 20:33:39 +01:00
Florian Kaiser
11e78fbece Fix Python > 3.8 dll import 2021-01-01 20:32:55 +01:00
Florian K
a3f697866b Changed place for hidapi.dll
Adding dlls to your System32 is not a good idea. Place the dll into your Workspace
2021-01-01 19:01:30 +01:00
Florian K
1530c79dd7 Update install instructions
Updated the install instructions with the hidapi download and placement.
2021-01-01 11:36:35 +01:00
Florian Kaiser
93b5e38e6e v0.4.1
- Fix mypy errors
2020-12-31 23:53:23 +01:00
Florian Kaiser
94cb09dbdd v0.4.0
- refactored code structure
- fixed playerID led display
- added Color function with tuple support
- added type checking in every function
- added more Exceptions for out of bound values
2020-12-31 23:48:34 +01:00
Florian Kaiser
8fb31f86ba added mypy static analyzer action on push 2020-12-31 23:09:03 +01:00
Florian Kaiser
e04766d48d Add requirements.txt for dependabot 2020-12-27 15:06:23 +01:00
9 changed files with 262 additions and 183 deletions

21
.github/workflows/python-mypy.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Mypy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
name: Mypy
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install mypy
- name: mypy
run: |
mypy pydualsense/

View File

@@ -1,29 +1,28 @@
# pydualsense # pydualsense
control your dualsense through python. using the hid library this module implements the sending report for controlling you new PS5 controller. It creates a background thread to constantly receive and update the controller. control your dualsense through python. using the hid library this package implements the report features for controlling your new PS5 controller.
# install # install
Just install the package from [pypi](https://pypi.org/project/pydualsense/) 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 ```bash
pip install pydualsense pip install pydualsense
``` ```
# usage # usage
```python ```python
from pydualsense import pydualsense from pydualsense import pydualsense
ds = pydualsense() # open controller ds = pydualsense() # open controller
ds.setColor(255,0,0) # set touchpad color to red ds.init() # initialize controller
ds.setLeftTriggerMode(TriggerModes.Rigid) ds.light.setColorI(255,0,0) # set touchpad color to red
ds.setLeftTriggerForce(1, 255) ds.triggerL.setMode(TriggerModes.Rigid)
ds.triggerL.setForce(1, 255)
ds.close() # closing the controller ds.close() # closing the controller
``` ```
See ``examples`` folder for some more ideas See [examples](https://github.com/flok/pydualsense/tree/master/examples) folder for some more ideas
# Help wanted # Help wanted
@@ -44,5 +43,7 @@ Most stuff for this implementation were provided by and used from:
# Coming soon # Coming soon
- add bluetooth support
- add multiple controllers
- reading the states of the controller to enable a fully compatibility with python - partially done - reading the states of the controller to enable a fully compatibility with python - partially done
- add documentation using sphinx - add documentation using sphinx

View File

@@ -8,13 +8,13 @@ print('Trigger Effect demo started')
dualsense.setLeftMotor(255) dualsense.setLeftMotor(255)
dualsense.setRightMotor(100) dualsense.setRightMotor(100)
dualsense.setLeftTriggerMode(TriggerModes.Rigid) dualsense.triggerL.setMode(TriggerModes.Rigid)
dualsense.setLeftTriggerForce(1, 255) dualsense.triggerL.setForce(1, 255)
dualsense.setRightTriggerMode(TriggerModes.Pulse_A) dualsense.triggerR.setMode(TriggerModes.Pulse_A)
dualsense.setRightTriggerForce(0, 200) dualsense.triggerR.setForce(0, 200)
dualsense.setRightTriggerForce(1, 255) dualsense.triggerR.setForce(1, 255)
dualsense.setRightTriggerForce(2, 175) dualsense.triggerR.setForce(2, 175)
import time; time.sleep(3) import time; time.sleep(3)

View File

@@ -4,11 +4,11 @@ from pydualsense import *
dualsense = pydualsense() dualsense = pydualsense()
dualsense.init() dualsense.init()
# set color around touchpad to red # set color around touchpad to red
dualsense.setColor(0,0,255) dualsense.light.setColorI(255,0,0)
# enable microphone indicator # enable microphone indicator
dualsense.setMicrophoneLED(1) dualsense.audio.setMicrophoneLED(1)
# set all player indicators on # set all player 1 indicator on
dualsense.setPlayerID(PlayerID.all) dualsense.light.setPlayerID(PlayerID.player1)
# sleep a little to see the result on the controller # sleep a little to see the result on the controller
# this is not needed in normal usage # this is not needed in normal usage
import time; time.sleep(2) import time; time.sleep(2)

View File

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

View File

@@ -17,13 +17,13 @@ class Brightness(IntFlag):
low = 0x2 low = 0x2
class PlayerID(IntFlag): class PlayerID(IntFlag):
player1 = 1, player1 = 4,
player2 = 2, player2 = 10,
player3 = 4, player3 = 21,
player4 = 8, player4 = 27,
player5 = 16,
all = 31 all = 31
class TriggerModes(IntFlag): class TriggerModes(IntFlag):
Off = 0x0, # no resistance Off = 0x0, # no resistance
Rigid = 0x1, # continous resistance Rigid = 0x1, # continous resistance

View File

@@ -1,25 +1,28 @@
from os import device_encoding
import hid # needed for python > 3.8
import os, sys
if sys.version_info >= (3,8):
os.add_dll_directory(os.getcwd())
import hid # type: ignore
from .enums import (LedOptions, PlayerID, from .enums import (LedOptions, PlayerID,
PulseOptions, TriggerModes, Brightness) PulseOptions, TriggerModes, Brightness)
import threading import threading
import sys
import winreg import winreg
class pydualsense: class pydualsense:
def __init__(self, verbose: bool = False) -> None: def __init__(self, verbose: bool = False) -> None:#
# TODO: maybe add a init function to not automatically allocate controller when class is declared # TODO: maybe add a init function to not automatically allocate controller when class is declared
self.verbose = verbose self.verbose = verbose
self.receive_buffer_size = 64 self.receive_buffer_size = 64
self.send_report_size = 48 self.send_report_size = 48
self.color = (0,0,255) # set color around touchpad to blue
self.leftMotor = 0 self.leftMotor = 0
self.rightMotor = 0 self.rightMotor = 0
def init(self): def init(self):
"""initialize module and device """initialize module and device states
""" """
self.device: hid.Device = self.__find_device() self.device: hid.Device = self.__find_device()
self.light = DSLight() # control led light of ds self.light = DSLight() # control led light of ds
@@ -38,11 +41,14 @@ class pydualsense:
self.init = True self.init = True
def close(self): def close(self):
"""
Stops the report thread and closes the HID device
"""
self.ds_thread = False self.ds_thread = False
self.report_thread.join() self.report_thread.join()
self.device.close() self.device.close()
def _check_hide(self): def _check_hide(self) -> bool:
"""check if hidguardian is used and controller is hidden """check if hidguardian is used and controller is hidden
""" """
if sys.platform.startswith('win32'): if sys.platform.startswith('win32'):
@@ -55,17 +61,26 @@ class pydualsense:
return False return False
except OSError as e: except OSError as e:
print(e) print(e)
else:
# TODO: find something for other platforms. Maybe not even needed on linux return False
return False
def __find_device(self): def __find_device(self) -> hid.Device:
"""
find HID device and open it
Raises:
Exception: HIDGuardian detected
Exception: No device detected
Returns:
hid.Device: returns opened controller device
"""
# TODO: detect connection mode, bluetooth has a bigger write buffer # TODO: detect connection mode, bluetooth has a bigger write buffer
# TODO: implement multiple controllers working # TODO: implement multiple controllers working
if self._check_hide(): if self._check_hide():
raise Exception('HIDGuardian detected. Delete the controller from HIDGuardian and restart PC to connect to controller') raise Exception('HIDGuardian detected. Delete the controller from HIDGuardian and restart PC to connect to controller')
detected_device = None detected_device: hid.Device = None
devices = hid.enumerate(vid=0x054c) devices = hid.enumerate(vid=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'] == 0x0CE6:
@@ -79,117 +94,42 @@ class pydualsense:
return dual_sense return dual_sense
def setLeftMotor(self, intensity: int): def setLeftMotor(self, intensity: int):
if intensity > 255: """
set left motor rumble
Args:
intensity (int): rumble intensity
Raises:
TypeError: intensity false type
Exception: intensity out of bounds 0..255
"""
if not isinstance(intensity, int):
raise TypeError('left motor intensity needs to be an int')
if intensity > 255 or intensity < 0:
raise Exception('maximum intensity is 255') raise Exception('maximum intensity is 255')
self.leftMotor = intensity self.leftMotor = intensity
def setRightMotor(self, intensity: int): def setRightMotor(self, intensity: int):
if intensity > 255: """
set right motor rumble
Args:
intensity (int): rumble intensity
Raises:
TypeError: intensity false type
Exception: intensity out of bounds 0..255
"""
if not isinstance(intensity, int):
raise TypeError('right motor intensity needs to be an int')
if intensity > 255 or intensity < 0:
raise Exception('maximum intensity is 255') raise Exception('maximum intensity is 255')
self.rightMotor = intensity self.rightMotor = intensity
# right trigger
def setRightTriggerMode(self, mode: TriggerModes):
"""set the trigger mode for R2
:param mode: enum of Trigger mode
:type mode: TriggerModes
"""
self.triggerR.mode = mode
def setRightTriggerForce(self, forceID: int, force: int):
"""set the right trigger force. trigger consist of 7 parameter
:param forceID: parameter id from 0 to 6
:type forceID: int
:param force: force from 0..ff (0..255) applied to the trigger
:type force: int
"""
if forceID > 6:
raise Exception('only 7 parameters available')
self.triggerR.setForce(id=forceID, force=force)
def setLeftTriggerMode(self, mode: TriggerModes):
"""set the trigger mode for L2
:param mode: enum of Trigger mode
:type mode: TriggerModes
"""
self.triggerL.mode = mode
def setLeftTriggerForce(self, forceID: int, force: int):
"""set the left trigger force. trigger consist of 7 parameter
:param forceID: parameter id from 0 to 6
:type forceID: int
:param force: force from 0..ff (0..255) applied to the trigger
:type force: int
"""
if forceID > 6:
raise Exception('only 7 parameters available')
self.triggerL.setForce(id=forceID, force=force)
# TODO: audio
# audio stuff
def setMicrophoneLED(self, value):
self.audio.microphone_led = value
# color stuff
def setColor(self, r: int, g:int, b:int):
"""sets the led colour around the touchpad
:param r: red channel, 0..255
:type r: int
:param g: green channel, 0..255
:type g: int
:param b: blue channel, 0..255
:type b: int
:raises Exception: wron color values
"""
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.color = (r,g,b)
def setLEDOption(self, option: LedOptions):
"""set led option
:param option: led option
:type option: LedOptions
"""
self.light.ledOption = option
def setPulseOption(self, option: PulseOptions):
"""set the pulse option for the leds
:param option: [description]
:type option: PulseOptions
"""
self.light.pulseOptions = option
def setBrightness(self, brightness: Brightness):
"""set the brightness of the player leds
:param brightness: brightness for the leds
:type brightness: Brightness
"""
self.light.brightness = brightness
def setPlayerID(self, player : PlayerID):
"""set the player ID. The controller has 5 white LED which signals
which player the controller is
:param player: the player id from 1 to 5
:type player: PlayerID
"""
self.light.playerNumber = player
def sendReport(self): def sendReport(self):
"""background thread handling the reading of the device and updating its states """background thread handling the reading of the device and updating its states
@@ -210,10 +150,11 @@ class pydualsense:
self.writeReport(outReport) self.writeReport(outReport)
def readInput(self, inReport): def readInput(self, inReport):
"""read the reported data from the controller """
read the input from the controller and assign the states
:param inReport: report of the controller Args:
:type inReport: bytes inReport (bytearray): read bytearray containing the state of the whole controller
""" """
states = list(inReport) # convert bytes to list states = list(inReport) # convert bytes to list
# states 0 is always 1 # states 0 is always 1
@@ -276,19 +217,21 @@ class pydualsense:
def writeReport(self, outReport): def writeReport(self, outReport):
"""Write the given report to the device """
write the report to the device
:param outReport: report with data for the controller Args:
:type outReport: list outReport (list): report to be written to device
""" """
self.device.write(bytes(outReport)) self.device.write(bytes(outReport))
def prepareReport(self): def prepareReport(self):
"""prepare the report for the controller with all the settings set since the previous update """
prepare the output to be send to the controller
:return: report for the controller with all infos Returns:
:rtype: list list: report to send to controller
""" """
outReport = [0] * 48 # create empty list with range of output report outReport = [0] * 48 # create empty list with range of output report
# packet type # packet type
@@ -347,15 +290,18 @@ class pydualsense:
outReport[42] = self.light.pulseOptions.value outReport[42] = self.light.pulseOptions.value
outReport[43] = self.light.brightness.value outReport[43] = self.light.brightness.value
outReport[44] = self.light.playerNumber.value outReport[44] = self.light.playerNumber.value
outReport[45] = self.color[0] outReport[45] = self.light.TouchpadColor[0]
outReport[46] = self.color[1] outReport[46] = self.light.TouchpadColor[1]
outReport[47] = self.color[2] outReport[47] = self.light.TouchpadColor[2]
if self.verbose: if self.verbose:
print(outReport) print(outReport)
return outReport return outReport
class DSTouchpad: class DSTouchpad:
def __init__(self) -> None: def __init__(self) -> None:
"""
Class represents the Touchpad of the controller
"""
self.isActive = False self.isActive = False
self.ID = 0 self.ID = 0
self.X = 0 self.X = 0
@@ -422,19 +368,113 @@ class DSState:
class DSLight: class DSLight:
"""DualSense Light class """
Represents all features of lights on the controller
make it simple, no get or set functions. quick and dirty
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.brightness: Brightness = Brightness.low # sets self.brightness: Brightness = Brightness.low # sets
self.playerNumber: PlayerID = PlayerID.player1 self.playerNumber: PlayerID = PlayerID.player1
self.ledOption : LedOptions = LedOptions.Both self.ledOption : LedOptions = LedOptions.Both
self.pulseOptions : PulseOptions = PulseOptions.Off self.pulseOptions : PulseOptions = PulseOptions.Off
self.TouchpadColor = (0,0,255)
def setLEDOption(self, option: LedOptions):
"""
Sets the LED Option
Args:
option (LedOptions): Led option
Raises:
TypeError: LedOption is false type
"""
if not isinstance(option, LedOptions):
raise TypeError('Need LEDOption type')
self.ledOption = option
def setPulseOption(self, option: PulseOptions):
"""
Sets the Pulse Option of the LEDs
Args:
option (PulseOptions): pulse option of the LEDs
Raises:
TypeError: Pulse option is false type
"""
if not isinstance(option, PulseOptions):
raise TypeError('Need PulseOption type')
self.pulseOptions = option
def setBrightness(self, brightness: Brightness): def setBrightness(self, brightness: Brightness):
self._brightness = brightness """
Defines the brightness of the Player LEDs
Args:
brightness (Brightness): brightness of LEDS
Raises:
TypeError: brightness false type
"""
if not isinstance(brightness, Brightness):
raise TypeError('Need Brightness type')
self.brightness = brightness
def setPlayerID(self, player : PlayerID):
"""
Sets the PlayerID of the controller with the choosen LEDs.
The controller has 4 Player states
Args:
player (PlayerID): chosen PlayerID for the Controller
Raises:
TypeError: [description]
"""
if not isinstance(player, PlayerID):
raise TypeError('Need PlayerID type')
self.playerNumber = player
def setColorI(self, r: int , g: int, b: int) -> None:
"""
Sets the Color around the Touchpad of the controller
Args:
r (int): red channel
g (int): green channel
b (int): blue channel
Raises:
TypeError: color channels have wrong type
Exception: color channels are out of bounds
"""
if not isinstance(r, int) or not isinstance(g, int) or not isinstance(b, int):
raise TypeError('Color parameter need to be int')
# 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)
def setColorT(self, color: tuple) -> None:
"""
Sets the Color around the Touchpad as a tuple
Args:
color (tuple): color as tuple
Raises:
TypeError: color has wrong type
Exception: color channels are out of bounds
"""
if not isinstance(color, tuple):
raise TypeError('Color type is tuple')
# unpack for out of bounds check
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)
class DSAudio: class DSAudio:
@@ -442,6 +482,21 @@ class DSAudio:
self.microphone_mute = 0 self.microphone_mute = 0
self.microphone_led = 0 self.microphone_led = 0
def setMicrophoneLED(self, value):
"""
Activates or disables the microphone led.
This doesnt change the mute/unmutes the microphone itself.
Args:
value (int): On or off microphone LED
Raises:
Exception: false state for the led
"""
if value > 1 or value < 0:
raise Exception('Microphone LED can only be on or off (0 .. 1)')
self.microphone_led = value
class DSTrigger: class DSTrigger:
def __init__(self) -> None: def __init__(self) -> None:
# trigger modes # trigger modes
@@ -450,36 +505,37 @@ 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, id:int = 0, force:int = 0): def setForce(self, forceID: int = 0, force: int = 0):
"""set the force of the trigger
:param id: id of the trigger parameters. 6 possible, defaults to 0
:type id: int, optional
:param force: force 0 to 255, defaults to 0
:type force: int, optional
:raises Exception: false trigger parameter accessed. only available trigger parameters from 0 to 6
""" """
if id > 6 or id < 0: Sets the forces of the choosen force parameter
raise Exception('only trigger parameters 0 to 6 available')
self.forces[id] = force Args:
forceID (int, optional): force parameter. Defaults to 0.
force (int, optional): applied force to the parameter. Defaults to 0.
Raises:
TypeError: wrong type of forceID or force
Exception: choosen a false force parameter
"""
if not isinstance(forceID, int) or not isinstance(force, int):
raise TypeError('forceID and force needs to be type int')
if forceID > 6 or forceID < 0:
raise Exception('only 7 parameters available')
self.forces[forceID] = force
def setMode(self, mode: TriggerModes): def setMode(self, mode: TriggerModes):
"""set mode on the trigger
:param mode: mode for trigger
:type mode: TriggerModes
""" """
self.mode = mode Set the Mode for the Trigger
def getTriggerPacket(self): Args:
"""returns array of the trigger modes and its parameters mode (TriggerModes): Trigger mode
:return: packet of the trigger settings Raises:
:rtype: list TypeError: false Trigger mode type
""" """
# create packet if not isinstance(mode, TriggerModes):
packet = [self.mode.value] raise TypeError('Trigger mode parameter needs to be of type `TriggerModes`')
packet += [self.forces[i] for i in range(6)]
packet += [0,0] # unknown what these do ? self.mode = mode
packet.append(self.forces[-1]) # last force has a offset of 2 from the other forces. this is the frequency of the actuation
return packet

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
hid==1.0.4

View File

@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
setup( setup(
name='pydualsense', name='pydualsense',
version='0.3.0', version='0.4.2',
description='use your DualSense (PS5) controller with python', description='use your DualSense (PS5) controller with python',
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",