937 lines
32 KiB
Python
937 lines
32 KiB
Python
import logging
|
|
import os
|
|
import sys
|
|
from sys import platform
|
|
|
|
if platform.startswith("win32") and sys.version_info >= (3, 8):
|
|
os.environ["PATH"] += os.pathsep + os.path.dirname(__file__)
|
|
|
|
|
|
import hidapi
|
|
from .enums import (
|
|
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"
|
|
logging.basicConfig(format=FORMAT)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
class pydualsense:
|
|
OUTPUT_REPORT_USB = 0x02
|
|
OUTPUT_REPORT_BT = 0x31
|
|
|
|
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.
|
|
"""
|
|
|
|
self.verbose = verbose
|
|
|
|
if self.verbose:
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
self.leftMotor = 0
|
|
self.rightMotor = 0
|
|
|
|
self.last_states = None
|
|
|
|
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.battery = DSBattery()
|
|
self.conType = self.determineConnectionType() # determine USB or BT connection
|
|
self.ds_thread = True
|
|
self.report_thread = threading.Thread(target=self.sendReport)
|
|
self.report_thread.start()
|
|
self.states = None
|
|
|
|
def determineConnectionType(self) -> ConnectionType:
|
|
"""
|
|
Determine the connection type of the controller. eg USB or BT.
|
|
|
|
We ask the controller for an input report with a length up to 100 bytes
|
|
and afterwords check the lenght of the received input report.
|
|
The connection type determines the length of the report.
|
|
|
|
This way of determining is not pretty but it works..
|
|
|
|
Returns:
|
|
ConnectionType: Detected connection type of the controller.
|
|
"""
|
|
|
|
dummy_report = self.device.read(100)
|
|
input_report_length = len(dummy_report)
|
|
|
|
if input_report_length == 64:
|
|
self.input_report_length = 64
|
|
self.output_report_length = 64
|
|
return ConnectionType.USB
|
|
elif input_report_length == 78:
|
|
self.input_report_length = 78
|
|
self.output_report_length = 78
|
|
return ConnectionType.BT
|
|
|
|
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 dualsense 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: implement multiple controllers working
|
|
if sys.platform.startswith("win32"):
|
|
import pydualsense.hidguardian as hidguardian
|
|
|
|
if hidguardian.check_hide():
|
|
raise Exception(
|
|
"HIDGuardian detected. Delete the controller from HIDGuardian and restart PC to connect to controller"
|
|
)
|
|
detected_device: hidapi.Device = None
|
|
devices = hidapi.enumerate(vendor_id=0x054C)
|
|
for device in devices:
|
|
if device.vendor_id == 0x054C and device.product_id in (0x0CE6, 0x0DF2):
|
|
detected_device = device
|
|
|
|
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) -> None:
|
|
"""
|
|
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")
|
|
self.leftMotor = intensity
|
|
|
|
def setRightMotor(self, intensity: int) -> None:
|
|
"""
|
|
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")
|
|
self.rightMotor = intensity
|
|
|
|
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)
|
|
|
|
# prepare new report for device
|
|
outReport = self.prepareReport()
|
|
|
|
# write the report to the device
|
|
self.writeReport(outReport)
|
|
|
|
def readInput(self, inReport) -> 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
|
|
# 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]
|
|
|
|
# state 7 always increments -> not used anywhere
|
|
|
|
buttonState = states[8]
|
|
self.state.triangle = (buttonState & (1 << 7)) != 0
|
|
self.state.circle = (buttonState & (1 << 6)) != 0
|
|
self.state.cross = (buttonState & (1 << 5)) != 0
|
|
self.state.square = (buttonState & (1 << 4)) != 0
|
|
|
|
# dpad
|
|
dpad_state = buttonState & 0x0F
|
|
self.state.setDPadState(dpad_state)
|
|
|
|
misc = states[9]
|
|
self.state.R3 = (misc & (1 << 7)) != 0
|
|
self.state.L3 = (misc & (1 << 6)) != 0
|
|
self.state.options = (misc & (1 << 5)) != 0
|
|
self.state.share = (misc & (1 << 4)) != 0
|
|
self.state.R2Btn = (misc & (1 << 3)) != 0
|
|
self.state.L2Btn = (misc & (1 << 2)) != 0
|
|
self.state.R1 = (misc & (1 << 1)) != 0
|
|
self.state.L1 = (misc & (1 << 0)) != 0
|
|
|
|
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
|
|
self.state.trackPadTouch0.isActive = (inReport[33] & 0x80) == 0
|
|
self.state.trackPadTouch0.X = ((inReport[35] & 0x0F) << 8) | (inReport[34])
|
|
self.state.trackPadTouch0.Y = ((inReport[36]) << 4) | (
|
|
(inReport[35] & 0xF0) >> 4
|
|
)
|
|
|
|
# trackpad touch
|
|
self.state.trackPadTouch1.ID = inReport[37] & 0x7F
|
|
self.state.trackPadTouch1.isActive = (inReport[37] & 0x80) == 0
|
|
self.state.trackPadTouch1.X = ((inReport[39] & 0x0F) << 8) | (inReport[38])
|
|
self.state.trackPadTouch1.Y = ((inReport[40]) << 4) | (
|
|
(inReport[39] & 0xF0) >> 4
|
|
)
|
|
|
|
# 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
|
|
)
|
|
|
|
# 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
|
|
)
|
|
|
|
# from kit-nya
|
|
battery = states[53]
|
|
self.battery.State = BatteryState((battery & 0xF0) >> 4)
|
|
self.battery.Level = min((battery & 0x0F) * 10 + 5, 100)
|
|
|
|
# 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: control mouse with touchpad for fun as DS4Windows
|
|
|
|
def writeReport(self, outReport) -> None:
|
|
"""
|
|
write the report to the device
|
|
|
|
Args:
|
|
outReport (list): report to be written to device
|
|
"""
|
|
self.device.write(bytes(outReport))
|
|
|
|
def prepareReport(self) -> None:
|
|
"""
|
|
prepare the output to be send to the controller
|
|
|
|
Returns:
|
|
list: report to send to controller
|
|
"""
|
|
|
|
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
|
|
|
|
# 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] = 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_mute is True else 0x00
|
|
|
|
# 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[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.light.TouchpadColor[0]
|
|
outReport[46] = self.light.TouchpadColor[1]
|
|
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
|
|
|
|
outReport[1] = 0x02
|
|
|
|
# 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[2] = 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[3] = 0x1 | 0x2 | 0x4 | 0x10 | 0x40 # [2]
|
|
|
|
outReport[4] = self.rightMotor # right low freq motor 0-255 # [3]
|
|
outReport[5] = self.leftMotor # left low freq motor 0-255 # [4]
|
|
|
|
# outReport[5] - outReport[8] audio related
|
|
|
|
# set Micrphone LED, setting doesnt effect microphone settings
|
|
outReport[10] = self.audio.microphone_led # [9]
|
|
|
|
outReport[11] = 0x10 if self.audio.microphone_mute is True else 0x00
|
|
|
|
# add right trigger mode + parameters to packet
|
|
outReport[12] = self.triggerR.mode.value
|
|
outReport[13] = self.triggerR.forces[0]
|
|
outReport[14] = self.triggerR.forces[1]
|
|
outReport[15] = self.triggerR.forces[2]
|
|
outReport[16] = self.triggerR.forces[3]
|
|
outReport[17] = self.triggerR.forces[4]
|
|
outReport[18] = self.triggerR.forces[5]
|
|
outReport[21] = self.triggerR.forces[6]
|
|
|
|
outReport[23] = self.triggerL.mode.value
|
|
outReport[24] = self.triggerL.forces[0]
|
|
outReport[25] = self.triggerL.forces[1]
|
|
outReport[26] = self.triggerL.forces[2]
|
|
outReport[27] = self.triggerL.forces[3]
|
|
outReport[28] = self.triggerL.forces[4]
|
|
outReport[29] = self.triggerL.forces[5]
|
|
outReport[32] = self.triggerL.forces[6]
|
|
|
|
outReport[40] = self.light.ledOption.value
|
|
outReport[43] = self.light.pulseOptions.value
|
|
outReport[44] = self.light.brightness.value
|
|
outReport[45] = self.light.playerNumber.value
|
|
outReport[46] = self.light.TouchpadColor[0]
|
|
outReport[47] = self.light.TouchpadColor[1]
|
|
outReport[48] = self.light.TouchpadColor[2]
|
|
|
|
crcChecksum = compute(outReport)
|
|
|
|
outReport[74] = crcChecksum & 0x000000FF
|
|
outReport[75] = (crcChecksum & 0x0000FF00) >> 8
|
|
outReport[76] = (crcChecksum & 0x00FF0000) >> 16
|
|
outReport[77] = (crcChecksum & 0xFF000000) >> 24
|
|
|
|
if self.verbose:
|
|
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
|
|
"""
|
|
self.isActive = False
|
|
self.ID = 0
|
|
self.X = 0
|
|
self.Y = 0
|
|
|
|
|
|
class DSState:
|
|
def __init__(self) -> None:
|
|
"""
|
|
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.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: 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
|
|
self.DpadLeft = False
|
|
self.DpadRight = False
|
|
elif dpad_state == 1:
|
|
self.DpadUp = True
|
|
self.DpadDown = False
|
|
self.DpadLeft = False
|
|
self.DpadRight = True
|
|
elif dpad_state == 2:
|
|
self.DpadUp = False
|
|
self.DpadDown = False
|
|
self.DpadLeft = False
|
|
self.DpadRight = True
|
|
elif dpad_state == 3:
|
|
self.DpadUp = False
|
|
self.DpadDown = True
|
|
self.DpadLeft = False
|
|
self.DpadRight = True
|
|
elif dpad_state == 4:
|
|
self.DpadUp = False
|
|
self.DpadDown = True
|
|
self.DpadLeft = False
|
|
self.DpadRight = False
|
|
elif dpad_state == 5:
|
|
self.DpadUp = False
|
|
self.DpadDown = True
|
|
self.DpadLeft = False
|
|
self.DpadRight = False
|
|
elif dpad_state == 6:
|
|
self.DpadUp = False
|
|
self.DpadDown = False
|
|
self.DpadLeft = True
|
|
self.DpadRight = False
|
|
elif dpad_state == 7:
|
|
self.DpadUp = True
|
|
self.DpadDown = False
|
|
self.DpadLeft = True
|
|
self.DpadRight = False
|
|
else:
|
|
self.DpadUp = False
|
|
self.DpadDown = False
|
|
self.DpadLeft = False
|
|
self.DpadRight = False
|
|
|
|
|
|
class DSLight:
|
|
"""
|
|
Represents all features of lights on the controller
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.brightness: Brightness = Brightness.low # sets
|
|
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):
|
|
"""
|
|
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):
|
|
"""
|
|
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:
|
|
def __init__(self) -> None:
|
|
"""
|
|
initialize the limited Audio features of the controller
|
|
"""
|
|
self.microphone_mute = 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 (bool): On or off microphone LED
|
|
|
|
Raises:
|
|
Exception: false state for the led
|
|
"""
|
|
if not isinstance(value, bool):
|
|
raise TypeError("MicrophoneLED can only be a bool")
|
|
self.microphone_led = value
|
|
|
|
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_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
|
|
|
|
# force parameters for the triggers
|
|
self.forces = [0 for i in range(7)]
|
|
|
|
def setForce(self, forceID: int = 0, force: int = 0):
|
|
"""
|
|
Sets the forces of the choosen force parameter
|
|
|
|
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):
|
|
"""
|
|
Set the Mode for the Trigger
|
|
|
|
Args:
|
|
mode (TriggerModes): Trigger mode
|
|
|
|
Raises:
|
|
TypeError: false Trigger mode type
|
|
"""
|
|
if not isinstance(mode, TriggerModes):
|
|
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
|
|
|
|
|
|
class DSBattery:
|
|
"""
|
|
Class representing the Battery of the controller
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.State = BatteryState.POWER_SUPPLY_STATUS_UNKNOWN
|
|
self.Level = 0
|