Package: dualsense-controller
DualSense™ controller for PythonUse the Sony DualSense™ controller (PlayStation 5 controller) with Python.FeaturesPython support for Windows and Linux. SeeTested withsection.Connection via USB or BluetoothSimple, flexible and intuitive event-based APIRead and listen to all analog and digital inputsRead and listen to battery stateRead and listen to touchpadSet lights (Microphone light and Player LEDs) and its brightness,Set the lightbar's color,Set haptic feedbackSet adaptive triggers (experimental - work in progress)ContentsRequirementsInstallationPrerequisites for WindowsPrerequisites for LinuxHIDAPI on Linuxudev-rulesInstall the libraryUsageGetting started - Simple exampleList available devicesInitializationLifecycleErrors during operationRead and listen to BatteryDigital buttonsAnalog buttonsTouchpadGyroscope, Accelerotmeter and OrientationLightbar colorPlayer LEDsHaptic feedback (Rumble)Adaptive TriggersBehavioral OptionsValue MappingExamplesDevelopment NotesUSB Sniffing on Windows with Wireshark/TShark and USBPcapProtocolTested withMacOS supportSpecial thanks toContributionTrademarks NotesPhoto CreditsLicenseRequirementsPython 3.10+Kernel 5.12+ (Linux only)hidapi libInstallationSome preparations have to be done before depending on your operating system:Prerequisites for WindowsJust download thelatest release of HIDAPI.
Unzip the release zip file und then place the accordinghidapi.dllin your Workspace (i.e.C:\Windows\System32)
folder.
(fromx64folder for 64-bit Windows or fromx86folder for 32-bit Windows)Prerequisites for LinuxHIDAPI on LinuxYou needHIDAPI libraryinstalled on your system.For example on Ubuntu install it via:sudoaptinstalllibhidapi-devudev rulesFor use the controller in Python without root privileges add the udev rule.sudocpres/70-dualsense.rules/etc/udev/rules.dor create a file/etd/udev/rules.d/70-dualsense.ruleswith following content.# USB
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0666"
# Bluetooth
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", KERNELS=="0005:054C:0CE6.*", MODE="0666"Then u have to activate the rule.sudoudevadmcontrol--reload-rules
sudoudevadmtriggerInstall the libraryYou can now go ahead and use the library within your projects.
Add either viapip:pipinstall--upgradedualsense-controlleror when you preferPython Poetryas packaging and dependency management solution:
then add viapoetryadddualsense-controllerUsageGetting started - Simple exampleFollowing example enables rumble on cross button press and disables rumble on release.
Example program stops on PS button pressed.fromtimeimportsleepfromdualsense_controllerimportDualSenseController# list availabe devices and throw exception when tzhere is no device detecteddevice_infos=DualSenseController.enumerate_devices()iflen(device_infos)<1:raiseException('No DualSense Controller available.')# flag, which keeps program aliveis_running=True# create an instance, use fiŕst available devicecontroller=DualSenseController()# switches the keep alive flag, which stops the below loopdefstop():globalis_runningis_running=False# callback, when cross button is pressed, which enables rumbledefon_cross_btn_pressed():print('cross button pressed')controller.left_rumble.set(255)controller.right_rumble.set(255)# callback, when cross button is released, which disables rumbledefon_cross_btn_released():print('cross button released')controller.left_rumble.set(0)controller.right_rumble.set(0)# callback, when PlayStation button is pressed# stop programdefon_ps_btn_pressed():print('PS button released -> stop')stop()# callback, when unintended error occurs,# i.e. physically disconnecting the controller during operation# stop programdefon_error(error):print(f'Opps! an error occured:{error}')stop()# register the button callbackscontroller.btn_cross.on_down(on_cross_btn_pressed)controller.btn_cross.on_up(on_cross_btn_released)controller.btn_ps.on_down(on_ps_btn_pressed)# register the error callbackcontroller.on_error(on_error)# enable/connect the devicecontroller.activate()# start keep alive loop, controller inputs and callbacks are handled in a second threadwhileis_running:sleep(0.001)# disable/disconnect controller devicecontroller.deactivate()List available devicesCheck whether devices are connected and detecteddevice_infos=DualSenseController.enumerate_devices()iflen(device_infos)<1:raiseException('No DualSense Controller available.')InitializationYou can initialize Controllers on multiple ways:Variant 1Initialize by passing an index, (amount of devices - 1).controller=DualSenseController(device_index_or_device_info=0)Variant 2Pass a DeviceInfo object, obtained from the devices list.controller=DualSenseController(device_index_or_device_info=device_infos[0])Variant 3Just pass nothing. It will try to use the first device.controller=DualSenseController()LifecycleActivation -> Operation -> DeactivationThe controller has to be activated at first.Since the controller communication now takes place in a separate thread,
you must ensure that your program remains alive,
e.g. by using a while loop (possibly with a termination condition).
At the end you have to deactivate the controller.is_running=Truecontroller.activate()whileis_running:sleep(0.001)controller.deactivate()Alternatively, you can also use the controller with a context manager,
which activates and deactivates controllers automatically.is_running=Truewithactive_dualsense_controller()ascontroller:whileis_running:sleep(0.001)Errors during operationIn order to be able to react to unforeseen errors during operation,
such as a physical disconnect, you can handle such events in a callback.defon_error(error):print(f'an unforseen error occured{error}')# handle error# ...Register the callback viacontroller.on_error(on_error)Read and listen to batteryYou can read out the charge level and charge status of the battery as followsbatt=controller.battery.valueprint(batt)or listen to changes (especially while conneted via Bluetooth):defon_battery_change(battery)->None:print(f'on battery change:{battery}')defon_battery_lower_than(battery_level)->None:print(f'on battery low:{battery_level}')defon_battery_charging(battery_level)->None:print(f'on battery charging:{battery_level}')defon_battery_discharging(battery_level)->None:print(f'on battery discharging:{battery_level}')controller.battery.on_change(on_battery_change)controller.battery.on_lower_than(20,on_battery_lower_than)controller.battery.on_charging(on_battery_charging)controller.battery.on_discharging(on_battery_discharging)Digital buttonsThis lib reads and reacts on the following digital buttons of the DualSense Controller:Up,Down,Left,Right,Cross,Square,Circle,Triangle,L1,L2,L3,R1,R2,R3,Touchpad click,PlayStation,Mute,CreateandOptionsYou can listen to each button seperately if it ispressed,releasedor its valuechanged.defon_cross_btn_pressed():print('cross button pressed')defon_cross_btn_released():print('cross button_released')defon_cross_btn_changed(pressed):print(f'cross button is pressed:{pressed}')controller.btn_cross.on_down(on_cross_btn_pressed)controller.btn_cross.on_up(on_cross_btn_released)controller.btn_cross.on_change(on_cross_btn_changed)Analog buttonsThe DualSense controller has serveral analog buttons: two analogsticksand two analogtriggers(L2andR2).Note: trigger keys can be used analog and digital.L2andR2are digital buttons toodefon_left_trigger(value):print(f'left trigger changed:{value}')defon_left_stick_x_changed(left_stick_x):print(f'on_left_stick_x_changed:{left_stick_x}')defon_left_stick_y_changed(left_stick_y):print(f'on_left_stick_y_changed:{left_stick_y}')defon_left_stick_changed(left_stick):print(f'on_left_stick_changed:{left_stick}')controller.left_trigger.on_change(on_left_trigger)controller.left_stick_x.on_change(on_left_stick_x_changed)controller.left_stick_y.on_change(on_left_stick_y_changed)controller.left_stick.on_change(on_left_stick_changed)TouchpadThe DualSense Controller has a 2-point capacitive touchpad with click mechanism.
The Click of the Touchpad is handled like a digital button. SeeDigtal buttonsYou can listen and react on both touch events:defon_touch_finger_1(value):print(f'touch finger 1 changed:{value}')defon_touch_finger_2(value):print(f'touch finger 2 changed:{value}')controller.touch_finger_1.on_change(on_touch_finger_1)controller.touch_finger_2.on_change(on_touch_finger_2)Gyroscope, Accelerometer and OrientationYou can listen on all events of the 3 axis gyroscope, the 3 axis accelerometer and the calculated orientation.defon_gyroscope_change(gyroscope):print(f'on_gyroscope_change:{gyroscope}')defon_accelerometer_change(accelerometer):print(f'on_accelerometer_change:{accelerometer}')defon_orientation_change(orientation):print(f'on_orientation_change:{orientation}')controller.gyroscope.on_change(on_gyroscope_change)controller.accelerometer.on_change(on_accelerometer_change)controller.orientation.on_change(on_orientation_change)Lightbar colorThe color of the lightbar can be setted with predefined valuescontroller.lightbar.set_color_red()# controller.lightbar.set_color_green()# controller.lightbar.set_color_blue()# controller.lightbar.set_color_white()# controller.lightbar.set_color_black()or with custom RGB values.controller.lightbar.set_color(88,10,200)Player LEDsTurn on all LEDscontroller.player_leds.set_all()# controller.player_leds.set_inner()# controller.player_leds.set_outer()# controller.player_leds.set_center_and_outer()# controller.player_leds.set_center()or specific onescontroller.player_leds.set_inner()# controller.player_leds.set_outer()# controller.player_leds.set_center_and_outer()# controller.player_leds.set_center()or turn off allcontroller.player_leds.set_off()and modify their brightness.controller.player_leds.set_brightness_high()# controller.player_leds.set_brightness_medium()# controller.player_leds.set_brightness_low()Haptic feedback (Rumble)The haptic feedback is controlled by the left and right builtin rumble motors. You can set both of them independently.Attention:the according rumble values depend on the chosenValue Mapping.
By default it is a value between 0 and 255.controller.left_rumble.set(0)# no rumble# controller.left_rumble.set(128)  # medium rumble# controller.left_rumble.set(255) # strong rumbleAdaptive TriggersThis feature is a work in progress and not all possibilities have been exhausted yet.
However, this library provides a few essential effects.
Contribution is explicitly desired at this point.The following effects are implemented and can be used:Off (No effect)This is the default trigger effect and means that there is no trigger effect :P
Recommended to unset all trigger effects or apply no effect.controller.left_trigger.effect.off()No resistanceThis effect is similar to theOffeffect. (Not recommended, UseOffinstead)controller.left_trigger.effect.no_resistance()Continuous resistanceThis effect means there is a continuous resistance, starting at a defined position and is applied with following values:start_positionbetween 0 and 255 inclusiveforcestrength of the effect, between 0 and 255 inclusivecontroller.left_trigger.effect.continuous_resistance(start_position=0,force=255)# full resistance# controller.left_trigger.effect.continuous_resistance(start_position=127, force=255) # full resist. starts at middle pos# controller.left_trigger.effect.continuous_resistance(start_position=0, force=128)  # medium resistanceFeedbackThis effect is similar toContinuous resistanceTrigger will resist movement beyond the start position and applied with following values:start_position: The starting zone of the trigger effect. Must be between 0 and 9 inclusive.strength: The force of the resistance. Must be between 0 and 8 inclusive.controller.left_trigger.effect.feedback(start_position=0,strength=8)# full resistance# controller.left_trigger.effect.feedback(start_position=0, strength=3)  # less resistance# controller.left_trigger.effect.feedback(start_position=4, strength=3)  # less resistance, starting at middle positionMultiple position feedbackThis effect is like theFeedbackeffect it will resist movement at varrying strengths in 10 regions
and is applied with following values:strengths: List of 10 resistance values for zones 0 through 9. Each value must be between 0 and 8 inclusive.controller.left_trigger.effect.feedback((0,0,0,0,8,8,0,0,0,0))# controller.left_trigger.effect.feedback((8, 8, 0, 0, 0, 0, 0, 0, 8, 8))Slope feedbackThis effect will resist movement at a linear range of strengthsstart_position: The starting zone of the trigger effect. Must be between 0 and 8 inclusive.end_position: The ending zone of the trigger effect. Must be betweenstart_position+ 1 and 9 inclusive.start_strength: The force of the resistance at the start. Must be between 1 and 8 inclusive.end_strength: The force of the resistance at the end. Must be between 1 and 8 inclusive.controller.left_trigger.effect.slope_feedback(start_position=0,end_position=9,start_strength=1,end_strength=4)Section resistanceThis effect means only a custom section has resistance and is applied with following values:start_positionbetween 0 and 255 inclusiveend_positionbetween greather thanstart_positionand 255 inclusiveforcestrength of the effect, between 0 and 255 inclusivecontroller.left_trigger.effect.section_resistance(start_position=70,end_position=100,force=255)# full# controller.left_trigger.effect.set_section_resistance(start_pos=70,end_pos=100,force=10) # low resistanceWeaponThis effect is similar toSection resistancemeans resist movement beyond
the start position until the end position.start_position: The starting zone of the trigger effect. Must be between 2 and 7 inclusive.end_position: The ending zone of the trigger effect. Must be betweenstart_position+ 1 and 8 inclusive.strength: The force of the resistance. Must be between 0 and 8 inclusive.controller.left_trigger.effect.weapon(start_position=2,end_position=5,strength=8)BowThis effect is similar toWeaponeffect. There is a snap-back force that attempts to reset the trigger.start_position: The starting zone of the trigger effect. Must be between 0 and 8 inclusive.end_position: The ending zone of the trigger effect. Must be betweenstart_position+ 1 and 8 inclusive.strength: The force of the resistance. Must be between 0 and 8 inclusive.snap_force: The force of the snap-back. Must be between 0 and 8 inclusive.controller.left_trigger.effect.bow(start_position=1,end_position=4,strength=1,snap_force=8)GallopingTrigger will oscillate in a rythmic pattern resembling galloping.
Note that the effect is only discernable at low frequency values.start_position: The starting zone of the trigger effect. Must be between 0 and 8 inclusive.end_position: The ending zone of the trigger effect. Must be betweenstart_position+ 1 and 9 inclusive.first_foot: Position of second foot in cycle. Must be between 0 and 6 inclusive.second_foot: Position of second foot in cycle. Must be betweenfirst_foot+ 1 and 7 inclusive.frequency: Frequency of the automatic cycling action in hertz.controller.left_trigger.effect.bow(start_position=0,end_position=9,first_foot=4,second_foot=7,frequency=2)MachineThis effect resembles Vibration but will oscilate between two amplitudes.start_position: The starting zone of the trigger effect. Must be between 0 and 8 inclusiveend_position: The ending zone of the trigger effect. Must be betweenstart_position+ 1 and 9 inclusiveamplitude_a: Primary strength of cycling action. Must be between 0 and 7 inclusiveamplitude_b: Secondary strength of cycling action. Must be between 0 and 7 inclusivefrequency: Frequency of the automatic cycling action in hertzperiod: Period of the oscillation betweenamplitude_aandamplitude_bin tenths of a secondcontroller.left_trigger.effect.machine(start_position=1,end_position=9,amplitude_a=2,amplitude_b=7,frequency=5,period=3)Simple vibrationThis effect starts to vibrate at a given position.start_position: The starting zone of the trigger effect.amplitude: Strength of the automatic cycling action.frequency: Frequency of the automatic cycling action in hertz.controller.left_trigger.simple_vibration(start_position=0,amplitude=255,frequency=8)More effect presetsThere are more non-parametrized effects. Try them out:controller.left_trigger.full_press()controller.left_trigger.soft_press()controller.left_trigger.medium_press()controller.left_trigger.hard_press()controller.left_trigger.pulse()controller.left_trigger.choppy()controller.left_trigger.soft_rigidity()controller.left_trigger.medium_rigidity()controller.left_trigger.max_rigidity()controller.left_trigger.half_press()Behavioral OptionsThe behaviour of some aspects can be adjusted via the following optional parameters during initialization.Value MappingYou can change the value mapping for analog values, like stick axis, trigger values and rumble intensity
By default the stick axis values are mapped from -128 to 127 (default mapping)
and the trigger values from 0 to 255, which means the stick axis default position values are 0
and trigers default position values are 0.Optional stick deadzones should be adjusted properly depending on the mapping, i.e. value 3 is fine
when the stick axis values range from -128 to 127. But it is too high,
when stick range is interpreted as -1.0 to 1.0 (normalized mapping),
then you should use a deadzone which is smaller than 1.To apply a custom mapping, i.e. normalized mapping (-1.0 to 1.0) pass it on initialization:controller=DualSenseController(# ...mapping=Mapping.NORMALIZED,# ...)Available mappings are:Mapping.RAW: stick x axis values from 0 to 255, stick y axis values from 0 to 255, trigger values from 0 to 255,
rumble from 0 to 255Mapping.RAW_INVERTED: same asMapping.RAWbut stick y axis values inverted.Mapping.DEFAULT: stick x axis values from -128 to 127, stick y axis values from 127 to -128, trigger values from 0
to 255, rumble from 0 to 255Mapping.DEFAULT_INVERTED: same asMapping.DEFAULTbut stick y axis values inverted.Mapping.NORMALIZED: stick x axis values from -1.0 to 1.0, stick y axis values from 1.0 to -1.0, trigger values from
0.0 to 1.0, rumble from 0 to 1.0Mapping.NORMALIZED_INVERTED: same asMapping.NORMALIZEDbut stick y axis values inverted.Mapping.HUNDRED:ExamplesNot all funcionality is explicitly explained here, so take a look at the example files here,
to see more use cases take a look into the./src/examples:Development NotesWe tried different ways to get all the secrets out of the controller. We have documented some of the procedures here.USB Sniffing on Windows with Wireshark/TShark and USBPcapWireshark with USBPcap install is required. Ensure that your Wireshark and USBPcapCMD binaries are in the
Windows Path variable.find controller in USB tree to detect Root Device, i.e.\\.\USBPcap3USBPcapCMD.exeRun Capture in Wireshark for that device, start an app which permanently sends Data to controller
likenondebug Dualsense Explorer(Chrome browser
required)In Wireshark find Destination Number for appropriate device (out) i.e.3.8.3Run script:python tools_dev/shark/shark.py USBPcap3 3.8.3The output should look like:02 ff f7 00 00 00 00 00 00 00 10 26 90 a0 ff 00 00 00 00 00 00 00 26 90 a0 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 02 00 00 ff ff ff
operating_mode: 02
flags_physics: ff
flags_controls: f7
motor_right: 00
motor_left: 00
microphone_led: 00
microphone_mute: 10
right_trigger: 26 90 a0 ff 00 00 00 00
left_trigger: 26 90 a0 ff 00 00 00 00
led_options: 00
lightbar_pulse_options: 02
player_leds_brightness: 00
player_leds_enable: 00
color: ff ff ffProtocolFor communication between PC and controller there is a byte-based protocol, which has been deciphered to a large extent.
The meaning of individual bytes and byte sequences in both direction - from and to the controller - is documented in the
filesdocs/dualsense-controller.odsandREADME_PROTOCOL.mdTested withWindows:Windows 10 ProfessionalLinux:Manjaro Linux (6.1.38-1-MANJARO (64-bit)), Python 3.11.xUbuntu 22.04 Linux 64-bit, Python 3.10.xMacOS supportMacOS is not supported currently.
We are discussing the viability of implementation and there may also be macos support in the future.Special thanks toThis project's was heavily inspired by the following projects.
A lot of implementation details were borrowed and know-how were extracted from them.pydualsense: Another good python lib for DualSense controllerDualSense explorer tool: Another good python lib for DualSense controllerds5ctl: A GUI tool for configuring a DualSense 5 controllerPS5 Library of USB_Host_Shield_2.0: A DualSense Controller
library for arduinoDualSense on Windows [API]: Windows API for the DualSense DualSense
controller written in C++ for C++.TriggerEffectGenerator.cs:
Factories for all DualSense trigger effectsExtendInput: C# implementation of DualSenseGame Controller Collective Wiki: Sony DualSense:
Helpful informations about DualSense controller internalsLibshidapi: HIDAPI library for Windows, Linux, FreeBSD and macOShidapi-usb: Python bindings for hidapi via CFFIpyhidapi: Python bindings for hidapi via CFFIcython-hidapi: A Cython interface to HIDAPI libraryhidapitester: Simple command-line program to exercise HIDAPIContributionWe welcome any input from others to help us improve this software.
Feel free to send us suggestions for improvements.
In particular, collaboration on the documentation,
the haptic feedback (Rumble) and the adaptive triggers APIs are especially welcome.Trademarks Notes"PlayStation", "PlayStation Family Mark", "PS5 logo", "PS5", "DualSense" and "DUALSHOCK"
are registered trademarks or trademarks of Sony Interactive Entertainment Inc.
"SONY" is a registered trademark of Sony Corporation.
The authors are not affiliated in any kind with Sony Interactive Entertainment Inc.Photo CreditsTeaser Image: Original photo
byMarcel
StraußonUnsplashLicenseMIT