Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Bluetooth implementation" #47

Merged
merged 1 commit into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions pydualsense/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,3 @@ class TriggerModes(IntFlag):
Pulse_B = 0x2 | 0x04
Pulse_AB = 0x2 | 0x20 | 0x04
Calibration = 0xFC


class BatteryState(IntFlag):
POWER_SUPPLY_STATUS_DISCHARGING = 0x0
POWER_SUPPLY_STATUS_CHARGING = 0x1
POWER_SUPPLY_STATUS_FULL = 0x2
POWER_SUPPLY_STATUS_NOT_CHARGING = 0xb
POWER_SUPPLY_STATUS_ERROR = 0xf
POWER_SUPPLY_TEMP_OR_VOLTAGE_OUT_OF_RANGE = 0xa
POWER_SUPPLY_STATUS_UNKNOWN = 0x0


183 changes: 47 additions & 136 deletions pydualsense/pydualsense.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
os.add_dll_directory(os.getcwd())

import hidapi
from .enums import (LedOptions, PlayerID, PulseOptions, TriggerModes, Brightness, ConnectionType, BatteryState) # type: ignore
from .enums import (LedOptions, PlayerID, PulseOptions, TriggerModes, Brightness, ConnectionType) # type: ignore
import threading
from .event_system import Event
from copy import deepcopy
Expand All @@ -19,17 +19,6 @@
logger.setLevel(logging.INFO)

class pydualsense:
# Bluetooth Checksum seeds
INPUT_CRC_SEED = b'\xa1'
OUTPUT_CRC_SEED = b'\xa2'
# Feature report not implemented (I don't think?)
FEATURE_CRC_SEED = b'\xa2'

# Report ID Constants
INPUT_REPORT_USB = 0x01
INPUT_REPORT_BT = 0x31
OUTPUT_REPORT_USB = 0x02
OUTPUT_REPORT_BT = 0x31

def __init__(self, verbose: bool = False) -> None:
"""
Expand Down Expand Up @@ -110,7 +99,6 @@ def init(self) -> None:
self.triggerR = DSTrigger() # right trigger
self.state = DSState() # controller states
self.conType = self.determineConnectionType() # determine USB or BT connection
self.battery = DSBattery()
self.ds_thread = True
self.report_thread = threading.Thread(target=self.sendReport)
self.report_thread.start()
Expand Down Expand Up @@ -140,7 +128,6 @@ def determineConnectionType(self) -> ConnectionType:
elif input_report_length == 78:
self.input_report_length = 78
self.output_report_length = 78
self.output_report_seq_id = 0x0
return ConnectionType.BT

def close(self) -> None:
Expand Down Expand Up @@ -182,23 +169,6 @@ def __find_device(self) -> hidapi.Device:
dual_sense = hidapi.Device(vendor_id=detected_device.vendor_id, product_id=detected_device.product_id)
return dual_sense

def add_checksum(self, data: list) -> list:
from binascii import crc32
crc32_result = crc32(pydualsense.OUTPUT_CRC_SEED)
crc32_result = crc32(bytearray(data[:-4]), crc32_result)
checksum_bytes = crc32_result.to_bytes(4, byteorder='little')
data[-4:] = checksum_bytes
return data

def validate_checksum(self, data: list | bytearray) -> bool:
from binascii import crc32
if isinstance(data, list):
data = bytearray(data)
crc32_result = crc32(pydualsense.INPUT_CRC_SEED)
crc32_result = crc32(data[:-4], crc32_result)
checksum_bytes = crc32_result.to_bytes(4, byteorder='little')
return checksum_bytes == bytes(data[-4:])

def setLeftMotor(self, intensity: int) -> None:
"""
set left motor rumble
Expand Down Expand Up @@ -253,27 +223,22 @@ def sendReport(self) -> None:
self.writeReport(outReport)

def readInput(self, inReport) -> None:
if self.conType == ConnectionType.BT:
# First validate the report and skip if it's invalid
if not self.validate_checksum(inReport):
logger.warning('checksum failed')
return
# Bluetooth report comes prefixed with a tag byte. The meaning is unclear.
# Dropping the byte shifts us to the same common report format as USB
states = list(inReport)[1:] # convert bytes to list

else: # USB
states = list(inReport) # convert bytes to list

# Common report size is USB report size less the report ID (i.e. 62 bytes)
self.states = states
# states 0 is always 1
"""
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] - 127
self.state.LY = states[2] - 127
self.state.RX = states[3] - 127
Expand Down Expand Up @@ -330,10 +295,6 @@ def readInput(self, inReport) -> None:
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)

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)
Expand Down Expand Up @@ -430,28 +391,6 @@ def writeReport(self, outReport) -> None:
"""
self.device.write(bytes(outReport))

def flag1(self, muteMicEnable=False, powerSaveEnable=False, lightBarControl=False, releaseLEDs=False,
playerIndicatorControl=False):
output = 0
output = output | (muteMicEnable << 0)
output = output | (powerSaveEnable << 1)
output = output | (lightBarControl << 2)
output = output | (releaseLEDs << 3)
output = output | (playerIndicatorControl << 4)
return output

def flag2(self, lightBarSetupEnable=False, compatibleVibration=False):
output = 0
output = output | (lightBarSetupEnable << 0)
output = output | (compatibleVibration << 1)
return output

def flag0(self, compatibleVibration=False, hapticsSelect=False):
output = 0
output = output | (compatibleVibration << 1)
output = output | (hapticsSelect << 2)
return output

def prepareReport(self) -> None:
"""
prepare the output to be send to the controller
Expand All @@ -460,20 +399,10 @@ def prepareReport(self) -> None:
list: report to send to controller
"""

outReport = [0] * self.output_report_length # create empty list with range of output report
outReport = [0] * self.output_report_length # create empty list with range of output report
# packet type
if self.conType == ConnectionType.BT:
# ReportID
outReport[0] = pydualsense.OUTPUT_REPORT_BT
# Sequence Tag
outReport[1] = (self.output_report_seq_id << 4) | 0x00
self.output_report_seq_id = (self.output_report_seq_id + 1) % 16
# Tag. Note: This is just a magic number :(
outReport[2] = 0x10
else:
outReport[0] = pydualsense.OUTPUT_REPORT_USB
outReport[0] = 0x2

outReportCommon = [0] * 47
# 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)
Expand All @@ -482,7 +411,7 @@ def prepareReport(self) -> None:
# 0x10 modification of audio volume
# 0x20 toggling of internal speaker while headset is connected
# 0x40 modification of microphone volume
outReportCommon[0] = self.flag0(True, True)
outReport[1] = 0xff # [1]

# further flags determining what changes this packet will perform
# 0x01 toggling microphone LED
Expand All @@ -493,54 +422,45 @@ def prepareReport(self) -> None:
# 0x20 ???
# 0x40 adjustment of overall motor/effect power (index 37 - read note on triggers)
# 0x80 ???
outReportCommon[1] = 0x1 | 0x2 | 0x4 | 0x10 | 0x40 # [2]
# Function below should control the flags, but probably makes sense to properly implement
# rather than just toggling everything on?
# outReportCommon[1] = self.flag1(True, True, True, False, True)
outReportCommon[2] = self.rightMotor # right low freq motor 0-255 # [3]
outReportCommon[3] = self.leftMotor # left low freq motor 0-255 # [4]
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[4] - outReport[7] Audio Reserved
# outReport[5] - outReport[8] audio related

# set Micrphone LED, setting doesnt effect microphone settings
outReportCommon[8] = self.audio.microphone_led # [9]
# outReportCommon[9] Power save control? Whatever that is...
outReportCommon[11] = 0x10 if self.audio.microphone_mute is True else 0x00
outReport[9] = self.audio.microphone_led # [9]

# add right trigger mode + parameters to packet
outReportCommon[12] = self.triggerR.mode.value
outReportCommon[13] = self.triggerR.forces[0]
outReportCommon[14] = self.triggerR.forces[1]
outReportCommon[15] = self.triggerR.forces[2]
outReportCommon[16] = self.triggerR.forces[3]
outReportCommon[17] = self.triggerR.forces[4]
outReportCommon[18] = self.triggerR.forces[5]
outReportCommon[21] = self.triggerR.forces[6]

outReportCommon[23] = self.triggerL.mode.value
outReportCommon[24] = self.triggerL.forces[0]
outReportCommon[25] = self.triggerL.forces[1]
outReportCommon[26] = self.triggerL.forces[2]
outReportCommon[27] = self.triggerL.forces[3]
outReportCommon[28] = self.triggerL.forces[4]
outReportCommon[29] = self.triggerL.forces[5]
outReportCommon[32] = self.triggerL.forces[6]

outReportCommon[39] = self.flag2(True, True)
outReportCommon[40] = self.light.ledOption.value
outReportCommon[41] = self.light.pulseOptions.value
outReportCommon[42] = self.light.brightness.value
outReportCommon[43] = self.light.playerNumber.value
outReportCommon[44] = self.light.TouchpadColor[0]
outReportCommon[45] = self.light.TouchpadColor[1]
outReportCommon[46] = self.light.TouchpadColor[2]
outReport[10] = 0x10 if self.audio.microphone_mute is True else 0x00

if self.conType == ConnectionType.BT:
outReport[3:50] = outReportCommon
outReport = self.add_checksum(outReport)
# 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]

else:
outReport[1:48] = outReportCommon
if self.verbose:
logger.debug(outReport)

Expand Down Expand Up @@ -845,16 +765,7 @@ 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