diff --git a/pydualsense/__init__.py b/pydualsense/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pydualsense/enums.py b/pydualsense/enums.py new file mode 100644 index 0000000..50ed12b --- /dev/null +++ b/pydualsense/enums.py @@ -0,0 +1,39 @@ +from enum import Enum, IntFlag +from flags import Flags # bitflag + +class LedOptions(IntFlag): + Off=0x0, + PlayerLedBrightness=0x1, + UninterrumpableLed=0x2, + Both=0x01 | 0x02 + +class PulseOptions(IntFlag): + Off=0x0, + FadeBlue=0x1, + FadeOut=0x2 + +class Brightness(IntFlag): + high = 0x0, + medium = 0x1, + low = 0x2 + +class PlayerID(IntFlag): + player1 = 1, + player2 = 2, + player3 = 4, + player4 = 8, + player5 = 16, + 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 + diff --git a/pydualsense/pydualsense-demo.py b/pydualsense/pydualsense-demo.py new file mode 100644 index 0000000..6f5a9f5 --- /dev/null +++ b/pydualsense/pydualsense-demo.py @@ -0,0 +1,34 @@ +from PyQt5 import QtCore, QtGui, QtWidgets +import sys +from interface import Ui_MainWindow +from pydualsense import pydualsense + +def colorR(value): + global colorR + colorR = value + +def colorG(value): + global colorG + colorG = value + +def colorB(value): + global colorB + colorB = value + +def send(): + ds.setColor(colorR, colorG, colorB) + ds.sendReport() +if __name__ == "__main__": + global ds + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + ds = pydualsense() + # connect interface to + ui.slider_r.valueChanged.connect(colorR) + ui.slider_g.valueChanged.connect(colorG) + ui.slider_b.valueChanged.connect(colorB) + ui.pushButton.clicked.connect(send) + MainWindow.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/pydualsense/pydualsense.py b/pydualsense/pydualsense.py new file mode 100644 index 0000000..4ffe72b --- /dev/null +++ b/pydualsense/pydualsense.py @@ -0,0 +1,261 @@ +import hid +from enums import (LedOptions, PlayerID, + PulseOptions, TriggerModes, Brightness) +import threading + +class pydualsense: + + def __init__(self) -> None: + # TODO: maybe add a init function to not automatically allocate controller when class is declared + self.device = self.__find_device() + self.light = DSLight() # control led light of ds + self.audio = DSAudio() + self.triggerL = DSTrigger() + self.triggerR = DSTrigger() + + # set default for the controller + self.color = (0,0,255) # set dualsense color around the touchpad to blue + + self.send_thread = True + send_report = threading.Thread(target=self.sendReport) + #send_report.start() + # create thread for sending + + def __find_device(self): + devices = hid.enumerate(vid=0x054c) + found_devices = [] + for device in devices: + if device['vendor_id'] == 0x54c and device['product_id'] == 0xCE6: + found_devices.append(device) + + # TODO: detect connection mode, bluetooth has a bigger write buffer + # TODO: implement multiple controllers working + if len(found_devices) != 1: + raise Exception('no dualsense controller detected') + + + dual_sense = hid.Device(vid=found_devices[0]['vendor_id'], pid=found_devices[0]['product_id']) + return dual_sense + + + # color stuff + def setColor(self, r: int, g:int, b:int): + if r > 255 or g > 255 or b > 255: + raise Exception('colors have values from 0 to 255 only') + self.color = (r,g,b) + + + # 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) + + + # left trigger + 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.microphoneLED = 0x1 + + + def sendReport(self): + # while self.send_thread: + outReport = [0] * 48 # create empty list with range of output report + # 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) + # 0x04 set the right trigger motor + # 0x08 set the left trigger motor + # 0x10 modification of audio volume + # 0x20 toggling of internal speaker while headset is connected + # 0x40 modification of microphone volume + outReport[1] = 0xff # [1] + + # further flags determining what changes this packet will perform + # 0x01 toggling microphone LED + # 0x02 toggling audio/mic mute + # 0x04 toggling LED strips on the sides of the touchpad + # 0x08 will actively turn all LEDs off? Convenience flag? (if so, third parties might not support it properly) + # 0x10 toggling white player indicator LEDs below touchpad + # 0x20 ??? + # 0x40 adjustment of overall motor/effect power (index 37 - read note on triggers) + # 0x80 ??? + outReport[2] = 0x1 | 0x2 | 0x4 | 0x10 | 0x40 # [2] + + outReport[3]= 0 # left low freq motor 0-255 # [3] + outReport[4] = 0 # right 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] + + # set microphone muting + + + + # add right trigger mode + parameters to packet + outReport[11] = self.triggerR.mode.value + outReport[12] = self.triggerR.forces[0] + outReport[13] = self.triggerR.forces[1] + outReport[14] = self.triggerR.forces[2] + outReport[15] = self.triggerR.forces[3] + outReport[16] = self.triggerR.forces[4] + outReport[17] = self.triggerR.forces[5] + outReport[20] = self.triggerR.forces[6] + + outReport[22] = self.triggerL.mode.value + outReport[23] = self.triggerL.forces[0] + outReport[24] = self.triggerL.forces[1] + outReport[25] = self.triggerL.forces[2] + outReport[26] = self.triggerL.forces[3] + outReport[27] = self.triggerL.forces[4] + outReport[28] = self.triggerL.forces[5] + outReport[31] = self.triggerL.forces[6] + + + """ + outReport.append(self.light.ledOption.value[0]) # + outReport.append(self.light.pulseOptions.value[0]) + outReport.append(self.light.brightness.value[0]) + outReport.append(self.light.playerNumber.value[0]) + outReport.append(self.color[0]) # r + outReport.append(self.color[1]) # g + outReport.append(self.color[2]) # b + """ + outReport[39] = self.light.ledOption.value + outReport[42] = self.light.pulseOptions.value + outReport[43] = self.light.brightness.value + outReport[44] = self.light.playerNumber.value + outReport[45] = self.color[0] + outReport[46] = self.color[1] + outReport[47] = self.color[2] + self.device.write(bytes(outReport)) # send to controller + + + +class DSLight: + """DualSense Light class + + make it simple, no get or set functions. quick and dirty + """ + 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 + + def setBrightness(self, brightness: Brightness): + self._brightness = brightness + + def setPlayerNumer(self, player): + if player > 5: + raise Exception('only 5 players supported. choose 1-5') + + + +class DSAudio: + def __init__(self) -> None: + self.microphone_mute = 0 + self.microphone_led = 0 + +class DSTrigger: + def __init__(self) -> None: + # trigger modes + self.mode : TriggerModes = TriggerModes.Off + + # force parameters for the triggers + self.forces = [0 for i in range(7)] + + def setForce(self, id: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: + raise Exception('only trigger parameters 0 to 6 available') + self.forces[id] = force + + def setMode(self, mode: TriggerModes): + """set mode on the trigger + + :param mode: mode for trigger + :type mode: TriggerModes + """ + self.mode = mode + + def getTriggerPacket(self): + """returns array of the trigger modes and its parameters + + :return: packet of the trigger settings + :rtype: list + """ + # create packet + packet = [self.mode.value] + packet += [self.forces[i] for i in range(6)] + packet += [0,0] # unknown what these do ? + 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 + +if __name__ == "__main__": + ds = pydualsense() + import time + # ds.triggerR.setMode(TriggerModes.Rigid) + # ds.triggerR.setForce(0, 255) + ds.setLeftTriggerMode(TriggerModes.Pulse) + ds.setLeftTriggerForce(1, 255) + ds.setRightTriggerMode(TriggerModes.Rigid) + ds.setRightTriggerForce(1, 255) +# ds.triggerL.setForce(6,255) + ds.sendReport() + time.sleep(2) + time.sleep(3) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..39804e2 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name='pydualsense', + version='0.0.1', + description='control your dualsense controller with python', + long_description=long_description, + long_description_content_type="text/markdown", + url='https://github.com/flok/pydualsense', + author='Florian Kaiser', + author_email='shudson@anl.gov', + license='BSD 2-clause', + packages=setuptools.find_packages(), + install_requires=['hid>=1.0.4'] +)