├── .gitignore ├── .python-version ├── .run ├── contextmanager_usage_example.run.xml ├── core_example.run.xml ├── example.run.xml ├── example_simple.run.xml ├── example_trigger .run.xml ├── example_trigger_deprecated.run.xml ├── shark.run.xml ├── tests.run.xml ├── tests_test_deadzone.run.xml └── tests_test_update_level.run.xml ├── LICENSE ├── Makefile ├── README.md ├── README_PROTOCOL.md ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── requirements.txt ├── res └── 70-dualsense.rules ├── research ├── ADAPTIVE_TRIGGER_EFFECTS.md ├── DSX_Resistance.csv ├── ExtendInput.DataTools.DualSense.TriggerEffectGenerator.cs └── dualsense-controller.ods ├── src ├── dualsense_controller │ ├── __init__.py │ ├── api │ │ ├── DualSenseController.py │ │ ├── Properties.py │ │ ├── __init__.py │ │ ├── contextmanager.py │ │ ├── enum.py │ │ ├── property │ │ │ ├── AccelerometerProperty.py │ │ │ ├── BatteryProperty.py │ │ │ ├── BenchmarkProperty.py │ │ │ ├── ButtonProperty.py │ │ │ ├── ConnectionProperty.py │ │ │ ├── ExceptionProperty.py │ │ │ ├── GyroscopeProperty.py │ │ │ ├── JoyStickProperty.py │ │ │ ├── LightbarProperty.py │ │ │ ├── MicrophoneProperty.py │ │ │ ├── OrientationProperty.py │ │ │ ├── PlayerLedsProperty.py │ │ │ ├── RumbleProperty.py │ │ │ ├── TouchFingerProperty.py │ │ │ ├── TriggerEffectProperty.py │ │ │ ├── TriggerFeedbackProperty.py │ │ │ ├── TriggerProperty.py │ │ │ ├── __init__.py │ │ │ └── base.py │ │ └── typedef.py │ └── core │ │ ├── Benchmarker.py │ │ ├── DualSenseControllerCore.py │ │ ├── HidControllerDevice.py │ │ ├── __init__.py │ │ ├── core │ │ ├── Lockable.py │ │ └── __init__.py │ │ ├── enum.py │ │ ├── exception.py │ │ ├── hidapi │ │ ├── LICENSE.txt │ │ ├── __init__.py │ │ └── hidapi.py │ │ ├── log.py │ │ ├── report │ │ ├── __init__.py │ │ ├── in_report │ │ │ ├── Bt01InReport.py │ │ │ ├── Bt31InReport.py │ │ │ ├── InReport.py │ │ │ ├── Usb01InReport.py │ │ │ ├── __init__.py │ │ │ ├── enum.py │ │ │ └── typedef.py │ │ └── out_report │ │ │ ├── Bt01OutReport.py │ │ │ ├── Bt31OutReport.py │ │ │ ├── OutReport.py │ │ │ ├── Usb01OutReport.py │ │ │ ├── __init__.py │ │ │ ├── crc32.py │ │ │ ├── enum.py │ │ │ └── util.py │ │ ├── state │ │ ├── BaseStates.py │ │ ├── State.py │ │ ├── StateValueCallbackManager.py │ │ ├── ValueCompare.py │ │ ├── __init__.py │ │ ├── enum.py │ │ ├── mapping │ │ │ ├── StateValueMapper.py │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── enum.py │ │ │ └── typedef.py │ │ ├── read_state │ │ │ ├── ReadState.py │ │ │ ├── ReadStates.py │ │ │ ├── ValueCalc.py │ │ │ ├── ValueCompare.py │ │ │ ├── __init__.py │ │ │ ├── enum.py │ │ │ └── value_type.py │ │ ├── typedef.py │ │ └── write_state │ │ │ ├── WriteStates.py │ │ │ ├── __init__.py │ │ │ ├── enum.py │ │ │ └── value_type.py │ │ ├── typedef.py │ │ └── util.py └── examples │ ├── __init__.py │ ├── contextmanager_usage_example.py │ ├── core_example.py │ ├── example.py │ ├── example_simple.py │ ├── example_trigger.py │ └── example_trigger_deprecated.py ├── teaser.jpg ├── tests ├── __init__.py ├── common.py ├── conftest.py ├── mock │ ├── MockedHidapiMockedHidapiDevice.py │ ├── __init__.py │ └── common.py ├── test_creation.py ├── test_deadzone.py ├── test_mapping.py └── test_update_level.py └── tools_dev ├── __init__.py └── shark ├── TSharkCapture.py ├── __init__.py ├── dsadasdas ├── shark.py ├── shark.sh └── watch.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .idea 3 | /dist 4 | __pycache__ 5 | .~lock* -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.run/contextmanager_usage_example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/core_example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/example_simple.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/example_trigger .run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/example_trigger_deprecated.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | -------------------------------------------------------------------------------- /.run/shark.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | -------------------------------------------------------------------------------- /.run/tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | -------------------------------------------------------------------------------- /.run/tests_test_deadzone.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | -------------------------------------------------------------------------------- /.run/tests_test_update_level.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 yesbotics (Albrecht Nitsche, Jens Kabisch) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | This software contains Code from third party developers. 25 | You can find the license in src/dualsense_controller/core/hidapi/LICENSE.txt. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | .ONESHELL: 4 | 5 | all: build 6 | 7 | build: FORCE setup 8 | poetry build 9 | 10 | setup: FORCE 11 | poetry install 12 | 13 | test: FORCE 14 | pytest -v 15 | 16 | shell: FORCE 17 | poetry shell 18 | 19 | requirements: FORCE 20 | poetry export --output requirements.txt 21 | 22 | publish: FORCE build 23 | poetry publish 24 | 25 | .PHONY: FORCE 26 | FORCE: 27 | -------------------------------------------------------------------------------- /README_PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # DualSense™ protocol 2 | 3 | ## Sent data to controller 4 | 5 | ### Via USB 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 94 | 95 | 96 | 97 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | 108 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 323 | 327 | 328 | 329 | 330 | 331 | 332 | 341 | 346 | 347 | 348 | 349 | 350 | 351 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 372 | 373 | 374 |
Byte DecByte HexNameValuesDescription
00x00Report ID
10x01Feature flags (physical effects)
20x02Feature flags (lights) 32 | bit 0: MIC_MUTE_LED_CONTROL_ENABLE
33 | bit 1: POWER_SAVE_CONTROL_ENABLE
34 | bit 2: LIGHTBAR_CONTROL_ENABLE
35 | bit 3: RELEASE_LEDS
36 | bit 4: PLAYER_INDICATOR_CONTROL_ENABLE
37 | bit 5: UNKNOWN_FLAG_5
38 | bit 6: OVERALL_EFFECT_POWER
39 | bit 7: UNKNOWN_FLAG_7 40 |
42 | Flag RELEASE_LEDS makes trouble and is currently disabled. All other known flags are enabled. 43 |
30x03Motor rumble right0 - 255 A.K.A. "small rumble"
40x04Motor rumble left0 - 255 A.K.A. "big rumble"
50x05headphone, speaker, mic volume, audio flags (USB_Host_Shield_2.0)
60x06headphone, speaker, mic volume, audio flags (USB_Host_Shield_2.0)
70x07headphone, speaker, mic volume, audio flags (USB_Host_Shield_2.0)
80x08
90x09Mute button led 91 | 0x00 - Off
92 | 0x01 - On 93 |
100x0APower save control 100 | 0x00 - Unmute mic
101 | 0x10 - Mute mic 102 |
110x0BRight trigger - effect mode 109 | 0x01 - CONTINUOUS_RESISTANCE
110 | 0x02 - SECTION_RESISTANCE
111 | 0x06 - VIBRATING
112 | 0x23 - EFFECT_EXTENDED
113 | 0xFC - CALIBRATE
114 |
120x0CRight trigger - Parameter 1Start of resistance section
130x0DRight trigger - Parameter 2
140x0ERight trigger - Parameter 3
150x0FRight trigger - Parameter 4
160x10Right trigger - Parameter 5
170x11Right trigger - Parameter 6
180x12Right trigger - Parameter 7
190x13
200x14
210x15
220x16Left trigger - effect mode 182 | 0x01 - CONTINUOUS_RESISTANCE
183 | 0x02 - SECTION_RESISTANCE
184 | 0x06 - VIBRATING
185 | 0x23 - EFFECT_EXTENDED
186 | 0xFC - CALIBRATE
187 |
230x17Left trigger - Parameter 1Start of resistance section
240x18Left trigger - Parameter 2
250x19Left trigger - Parameter 3
260x1ALeft trigger - Parameter 4
270x1BLeft trigger - Parameter 5
280x1CLeft trigger - Parameter 6
290x1DLeft trigger - Parameter 7
300x1E
310x1F
320x20
330x21
340x22
350x23
360x24
370x25Trigger motor effect strengths? (USB_Host_Shield_2.0)
380x26Speaker volume? (USB_Host_Shield_2.0)
390x27Led brightness, pulse? (USB_Host_Shield_2.0)
400x28LIGHTBAR_SETUP_CONTROL_ENABLE? (dualsense (Javascript))
410x29Lightbar control 293 | 294 | 0b100 - LIGHTBAR_CONTROL_ENABLE
295 | 0b000 - LIGHTBAR_CONTROL_DISABLE? 296 |
297 |
420x2ALightbar setup 305 | 306 | 0x01 - LIGHT_ON
307 | 0x02 - LIGHT_OFF 308 |
309 |
430x2BLed brightness 317 | 318 | 0x01 - FULL
319 | 0x02 - MEDIUM
320 | 0x03 - LOW 321 |
322 |
Sets brightness of mute button and player leds.
324 | Only works when light-effects flag MIC_MUTE_LED_CONTROL_ENABLE is NOT set. This is done interally when 325 | setting brightness. 326 |
440x2CPlayer leds 333 | 334 | 0b00000 - OFF
335 | 0b00100 - CENTER
336 | 0b01010 - INNER
337 | 0b10001 - OUTER
338 | 0b11111 - ALL 339 |
340 |
342 | CENTER: The single, center LED.
343 | INNER: The two LEDs adjacent to and directly surrounding the CENTER LED.
344 | OUTER: The two outermost LEDs surrounding the INNER LEDs. 345 |
450x2DLightbar led color red 352 | 0 - 255 353 |
460x2ELightbar led color green 361 | 0 - 255 362 |
470x2FLightbar led color blue 370 | 0 - 255 371 |
375 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | create = true 4 | prefer-active-python = true 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dualsense-controller" 3 | version = "0.3.1" 4 | description = "Use DualSense Controller with Python." 5 | authors = ["Albrecht Nitsche ", "Jens Kabisch "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [ 9 | { include = "examples", from = "src" }, 10 | { include = "dualsense_controller", from = "src" }, 11 | ] 12 | repository = 'https://github.com/yesbotics/dualsense-controller-python' 13 | homepage = 'https://github.com/yesbotics/dualsense-controller-python' 14 | keywords = ["DualSense Controller", "PS5", "PlayStation 5", "Game Controller", "Gaming", "Robotics"] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: Microsoft :: Windows", 21 | "Operating System :: POSIX :: Linux", 22 | "Topic :: System :: Hardware :: Universal Serial Bus (USB) :: Human Interface Device (HID)", 23 | "Topic :: System :: Hardware :: Universal Serial Bus (USB) :: Wireless Controller", 24 | ] 25 | 26 | [tool.poetry.dependencies] 27 | python = "^3.10" 28 | pyee = "^11.0.0" 29 | cffi = "^1.15.1" 30 | deprecated = "^1.2.14" 31 | 32 | [tool.poetry.group.test.dependencies] 33 | pytest = "^7.4.0" 34 | pytest-mock = "^3.11.1" 35 | 36 | [tool.poetry.group.dev.dependencies] 37 | setuptools = "^70.0.0" 38 | windows-curses = { version = "^2.3.1", platform = "win32" } 39 | 40 | [tool.poetry.scripts] 41 | dualsense-controller-example = 'examples.example:main' 42 | dualsense-controller-example-trigger = 'examples.example_trigger:main' 43 | dualsense-controller-core-example = 'examples.core_example:main' 44 | dualsense-controller-contextmanager-usage-example = 'examples.contextmanager_usage_example:main' 45 | 46 | [build-system] 47 | requires = ["poetry-core"] 48 | build-backend = "poetry.core.masonry.api" 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ 2 | --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ 3 | --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ 4 | --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ 5 | --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ 6 | --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ 7 | --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ 8 | --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ 9 | --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ 10 | --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ 11 | --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ 12 | --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ 13 | --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ 14 | --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ 15 | --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ 16 | --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ 17 | --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ 18 | --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ 19 | --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ 20 | --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ 21 | --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ 22 | --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ 23 | --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ 24 | --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ 25 | --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ 26 | --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ 27 | --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ 28 | --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ 29 | --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ 30 | --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ 31 | --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ 32 | --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ 33 | --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ 34 | --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ 35 | --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ 36 | --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ 37 | --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ 38 | --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ 39 | --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ 40 | --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ 41 | --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ 42 | --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ 43 | --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ 44 | --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ 45 | --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ 46 | --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ 47 | --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ 48 | --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ 49 | --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ 50 | --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ 51 | --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ 52 | --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ 53 | --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 54 | pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0" \ 55 | --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ 56 | --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 57 | pyee==11.1.0 ; python_version >= "3.10" and python_version < "4.0" \ 58 | --hash=sha256:5d346a7d0f861a4b2e6c47960295bd895f816725b27d656181947346be98d7c1 \ 59 | --hash=sha256:b53af98f6990c810edd9b56b87791021a8f54fd13db4edd1142438d44ba2263f 60 | typing-extensions==4.8.0 ; python_version >= "3.10" and python_version < "4.0" \ 61 | --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ 62 | --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef 63 | -------------------------------------------------------------------------------- /res/70-dualsense.rules: -------------------------------------------------------------------------------- 1 | # USB 2 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0666" 3 | # Bluetooth 4 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", KERNELS=="0005:054C:0CE6.*", MODE="0666" -------------------------------------------------------------------------------- /research/DSX_Resistance.csv: -------------------------------------------------------------------------------- 1 | Start\Force,0,1,2,3,4,5,6,7,8 2 | 0,05 00 00 00 00 00 00 00,21 ff 03 00 00 00 00 00,21 ff 03 49 92 24 09 00,21 ff 03 92 24 49 12 00,21 ff 03 db b6 6d 1b 00,21 ff 03 24 49 92 24 00,21 ff 03 6d db b6 2d 00,21 ff 03 b6 6d db 36 00,21 ff 03 ff ff ff 3f 00 3 | 1,05 00 00 00 00 00 00 00,21 fe 03 00 00 00 00 00,21 fe 03 48 92 24 09 00,21 fe 03 90 24 49 12 00,21 fe 03 d8 b6 6d 1b 00,21 fe 03 20 49 92 24 00,21 fe 03 68 db b6 2d 00,21 fe 03 b0 6d db 36 00,21 fe 03 f8 ff ff 3f 00 4 | 2,05 00 00 00 00 00 00 00,21 fc 03 00 00 00 00 00,21 fc 03 40 92 24 09 00,21 fc 03 80 24 49 12 00,21 fc 03 c0 b6 6d 1b 00,21 fc 03 00 49 92 24 00,21 fc 03 40 db b6 2d 00,21 fc 03 80 6d db 36 00,21 fc 03 c0 ff ff 3f 00 5 | 3,05 00 00 00 00 00 00 00,21 f8 03 00 00 00 00 00,21 f8 03 00 92 24 09 00,21 f8 03 00 24 49 12 00,21 f8 03 00 b6 6d 1b 00,21 f8 03 00 48 92 24 00,21 f8 03 00 da b6 2d 00,21 f8 03 00 6c db 36 00,21 f8 03 00 fe ff 3f 00 6 | 4,05 00 00 00 00 00 00 00,21 f0 03 00 00 00 00 00,21 f0 03 00 90 24 09 00,21 f0 03 00 20 49 12 00,21 f0 03 00 b0 6d 1b 00,21 f0 03 00 40 92 24 00,21 f0 03 00 d0 b6 2d 00,21 f0 03 00 60 db 36 00,21 f0 03 00 f0 ff 3f 00 7 | 5,05 00 00 00 00 00 00 00,21 e0 03 00 00 00 00 00,21 e0 03 00 80 24 09 00,21 e0 03 00 00 49 12 00,21 e0 03 00 80 6d 1b 00,21 e0 03 00 00 92 24 00,21 e0 03 00 80 b6 2d 00,21 e0 03 00 00 db 36 00,21 e0 03 00 80 ff 3f 00 8 | 6,05 00 00 00 00 00 00 00,21 c0 03 00 00 00 00 00,21 c0 03 00 00 24 09 00,21 c0 03 00 00 48 12 00,21 c0 03 00 00 6c 1b 00,21 c0 03 00 00 90 24 00,21 c0 03 00 00 b4 2d 00,21 c0 03 00 00 d8 36 00,21 c0 03 00 00 fc 3f 00 9 | 7,05 00 00 00 00 00 00 00,21 80 03 00 00 00 00 00,21 80 03 00 00 20 09 00,21 80 03 00 00 40 12 00,21 80 03 00 00 60 1b 00,21 80 03 00 00 80 24 00,21 80 03 00 00 a0 2d 00,21 80 03 00 00 c0 36 00,21 80 03 00 00 e0 3f 00 10 | 8,05 00 00 00 00 00 00 00,21 00 03 00 00 00 00 00,21 00 03 00 00 00 09 00,21 00 03 00 00 00 12 00,21 00 03 00 00 00 1b 00,21 00 03 00 00 00 24 00,21 00 03 00 00 00 2d 00,21 00 03 00 00 00 36 00,21 00 03 00 00 00 3f 00 11 | 9,05 00 00 00 00 00 00 00,21 00 02 00 00 00 00 00,21 00 02 00 00 00 08 00,21 00 02 00 00 00 10 00,21 00 02 00 00 00 18 00,21 00 02 00 00 00 20 00,21 00 02 00 00 00 28 00,21 00 02 00 00 00 30 00,21 00 02 00 00 00 38 00 -------------------------------------------------------------------------------- /src/dualsense_controller/__init__.py: -------------------------------------------------------------------------------- 1 | from .api.DualSenseController import DualSenseController, Mapping, DeviceInfo, ConnectionType 2 | from .api.contextmanager import active_dualsense_controller 3 | from .api.enum import UpdateLevel 4 | from .api.property import TriggerProperty 5 | from .core.Benchmarker import Benchmark 6 | from .core.exception import InvalidDeviceIndexException 7 | from .core.state.read_state.value_type import Accelerometer, Battery, Connection, Gyroscope, JoyStick, Orientation, \ 8 | TouchFinger 9 | from .core.state.typedef import Number 10 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/Properties.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.api.property.AccelerometerProperty import AccelerometerProperty 4 | from dualsense_controller.api.property.BatteryProperty import BatteryProperty 5 | from dualsense_controller.api.property.BenchmarkProperty import BenchmarkProperty 6 | from dualsense_controller.api.property.ButtonProperty import ButtonProperty 7 | from dualsense_controller.api.property.ConnectionProperty import ConnectionProperty 8 | from dualsense_controller.api.property.ExceptionProperty import ExceptionProperty 9 | from dualsense_controller.api.property.GyroscopeProperty import GyroscopeProperty 10 | from dualsense_controller.api.property.JoyStickProperty import JoyStickProperty 11 | from dualsense_controller.api.property.LightbarProperty import LightbarProperty 12 | from dualsense_controller.api.property.MicrophoneProperty import MicrophoneProperty 13 | from dualsense_controller.api.property.OrientationProperty import OrientationProperty 14 | from dualsense_controller.api.property.PlayerLedsProperty import PlayerLedsProperty 15 | from dualsense_controller.api.property.RumbleProperty import RumbleProperty 16 | from dualsense_controller.api.property.TouchFingerProperty import TouchFingerProperty 17 | from dualsense_controller.api.property.TriggerEffectProperty import TriggerEffectProperty 18 | from dualsense_controller.api.property.TriggerFeedbackProperty import TriggerFeedbackProperty 19 | from dualsense_controller.api.property.TriggerProperty import TriggerProperty 20 | from dualsense_controller.core.Benchmarker import Benchmark 21 | from dualsense_controller.core.state.State import State 22 | from dualsense_controller.core.state.read_state.ReadStates import ReadStates 23 | from dualsense_controller.core.state.read_state.value_type import Connection 24 | from dualsense_controller.core.state.write_state.WriteStates import WriteStates 25 | 26 | 27 | class Properties: 28 | def __init__( 29 | self, 30 | # STATES 31 | connection_state: State[Connection], 32 | update_benchmark_state: State[Benchmark], 33 | exception_state: State[Exception], 34 | read_states: ReadStates, 35 | write_states: WriteStates, 36 | # OPTS 37 | microphone_invert_led: bool = False, 38 | ): 39 | # MAIN 40 | self.exceptions: Final[ExceptionProperty] = ExceptionProperty(exception_state) 41 | self.benchmark: Final[BenchmarkProperty] = BenchmarkProperty(update_benchmark_state) 42 | self.connection: Final[ConnectionProperty] = ConnectionProperty(connection_state) 43 | self.battery: Final[BatteryProperty] = BatteryProperty(read_states.battery) 44 | 45 | # BTN MISC 46 | self.btn_ps: Final[ButtonProperty] = ButtonProperty(read_states.btn_ps) 47 | self.btn_options: Final[ButtonProperty] = ButtonProperty(read_states.btn_options) 48 | self.btn_create: Final[ButtonProperty] = ButtonProperty(read_states.btn_create) 49 | self.btn_mute: Final[ButtonProperty] = ButtonProperty(read_states.btn_mute) 50 | self.btn_touchpad: Final[ButtonProperty] = ButtonProperty(read_states.btn_touchpad) 51 | 52 | # BTN SYMBOL 53 | self.btn_cross: Final[ButtonProperty] = ButtonProperty(read_states.btn_cross) 54 | self.btn_square: Final[ButtonProperty] = ButtonProperty(read_states.btn_square) 55 | self.btn_triangle: Final[ButtonProperty] = ButtonProperty(read_states.btn_triangle) 56 | self.btn_circle: Final[ButtonProperty] = ButtonProperty(read_states.btn_circle) 57 | 58 | # BTN DPAD 59 | self.btn_left: Final[ButtonProperty] = ButtonProperty(read_states.btn_left) 60 | self.btn_up: Final[ButtonProperty] = ButtonProperty(read_states.btn_up) 61 | self.btn_right: Final[ButtonProperty] = ButtonProperty(read_states.btn_right) 62 | self.btn_down: Final[ButtonProperty] = ButtonProperty(read_states.btn_down) 63 | 64 | # BTN L AND R 65 | self.btn_l1: Final[ButtonProperty] = ButtonProperty(read_states.btn_l1) 66 | self.btn_r1: Final[ButtonProperty] = ButtonProperty(read_states.btn_r1) 67 | self.btn_l2: Final[ButtonProperty] = ButtonProperty(read_states.btn_l2) 68 | self.btn_r2: Final[ButtonProperty] = ButtonProperty(read_states.btn_r2) 69 | self.btn_l3: Final[ButtonProperty] = ButtonProperty(read_states.btn_l3) 70 | self.btn_r3: Final[ButtonProperty] = ButtonProperty(read_states.btn_r3) 71 | 72 | # TRIGGERS 73 | self.left_trigger: Final[TriggerProperty] = TriggerProperty( 74 | trigger_value_state=read_states.left_trigger_value, 75 | trigger_feedback_property=TriggerFeedbackProperty(read_states.left_trigger_feedback), 76 | trigger_effect_property=TriggerEffectProperty(write_states.left_trigger_effect), 77 | ) 78 | self.right_trigger: Final[TriggerProperty] = TriggerProperty( 79 | trigger_value_state=read_states.right_trigger_value, 80 | trigger_feedback_property=TriggerFeedbackProperty(read_states.right_trigger_feedback), 81 | trigger_effect_property=TriggerEffectProperty(write_states.right_trigger_effect), 82 | ) 83 | 84 | # STICKS 85 | self.left_stick_x: Final[JoyStickProperty] = JoyStickProperty(read_states.left_stick_x) 86 | self.left_stick_y: Final[JoyStickProperty] = JoyStickProperty(read_states.left_stick_y) 87 | self.left_stick: Final[JoyStickProperty] = JoyStickProperty(read_states.left_stick) 88 | self.right_stick_x: Final[JoyStickProperty] = JoyStickProperty(read_states.right_stick_x) 89 | self.right_stick_y: Final[JoyStickProperty] = JoyStickProperty(read_states.right_stick_y) 90 | self.right_stick: Final[JoyStickProperty] = JoyStickProperty(read_states.right_stick) 91 | 92 | # TOUCH 93 | self.touch_finger_1: Final[TouchFingerProperty] = TouchFingerProperty(read_states.touch_finger_1) 94 | self.touch_finger_2: Final[TouchFingerProperty] = TouchFingerProperty(read_states.touch_finger_2) 95 | self.gyroscope: Final[GyroscopeProperty] = GyroscopeProperty(read_states.gyroscope) 96 | self.accelerometer: Final[AccelerometerProperty] = AccelerometerProperty(read_states.accelerometer) 97 | self.orientation: Final[OrientationProperty] = OrientationProperty(read_states.orientation) 98 | 99 | # WRITE 100 | self.left_rumble: Final[RumbleProperty] = RumbleProperty(write_states.left_motor) 101 | self.right_rumble: Final[RumbleProperty] = RumbleProperty(write_states.right_motor) 102 | self.player_leds: Final[PlayerLedsProperty] = PlayerLedsProperty(write_states.player_leds) 103 | self.microphone: Final[MicrophoneProperty] = MicrophoneProperty( 104 | write_states.microphone, 105 | invert_led=microphone_invert_led, 106 | ) 107 | self.lightbar: Final[LightbarProperty] = LightbarProperty(write_states.lightbar) 108 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/api/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/api/contextmanager.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from typing import Generator 3 | 4 | from dualsense_controller.api.DualSenseController import DualSenseController, Mapping 5 | from dualsense_controller.api.enum import UpdateLevel 6 | from dualsense_controller.core.hidapi import DeviceInfo 7 | from dualsense_controller.core.state.typedef import Number 8 | 9 | 10 | @contextmanager 11 | def active_dualsense_controller( 12 | # CORE 13 | device_index_or_device_info: int | DeviceInfo = 0, 14 | left_joystick_deadzone: Number = 0.05, 15 | right_joystick_deadzone: Number = 0.05, 16 | left_trigger_deadzone: Number = 0, 17 | right_trigger_deadzone: Number = 0, 18 | gyroscope_threshold: int = 0, 19 | accelerometer_threshold: int = 0, 20 | orientation_threshold: int = 0, 21 | mapping: Mapping = Mapping.NORMALIZED, 22 | update_level: UpdateLevel = UpdateLevel.DEFAULT, 23 | # OPTS 24 | microphone_initially_muted: bool = True, 25 | microphone_invert_led: bool = False, 26 | ) -> Generator[DualSenseController, None, None]: 27 | controller: DualSenseController = DualSenseController( 28 | device_index_or_device_info, 29 | left_joystick_deadzone=left_joystick_deadzone, 30 | right_joystick_deadzone=right_joystick_deadzone, 31 | left_trigger_deadzone=left_trigger_deadzone, 32 | right_trigger_deadzone=right_trigger_deadzone, 33 | gyroscope_threshold=gyroscope_threshold, 34 | accelerometer_threshold=accelerometer_threshold, 35 | orientation_threshold=orientation_threshold, 36 | mapping=mapping, 37 | update_level=update_level, 38 | microphone_initially_muted=microphone_initially_muted, 39 | microphone_invert_led=microphone_invert_led, 40 | ) 41 | controller.activate() 42 | try: 43 | yield controller 44 | finally: 45 | controller.deactivate() 46 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/enum.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | @dataclass(frozen=True, slots=True) 6 | class _UpdateLevelData: 7 | enforce_update: bool 8 | can_update_itself: bool 9 | 10 | 11 | class UpdateLevel(Enum): 12 | # reactive, only update value if requested or has listeners 13 | LAZY = _UpdateLevelData( 14 | enforce_update=False, 15 | can_update_itself=True, 16 | ) 17 | # proactive, always update all values 18 | PAINSTAKING = _UpdateLevelData( 19 | enforce_update=True, 20 | can_update_itself=False, 21 | ) 22 | # passive, only update values, which are listened 23 | HAENGBLIEM = _UpdateLevelData( 24 | enforce_update=False, 25 | can_update_itself=False, 26 | ) 27 | DEFAULT = LAZY 28 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/AccelerometerProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import Accelerometer 3 | 4 | 5 | class AccelerometerProperty(Property[Accelerometer]): 6 | pass 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/BatteryProperty.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from dualsense_controller.api.property.base import Property 4 | from dualsense_controller.api.typedef import PropertyChangeCallback 5 | from dualsense_controller.core.state.read_state.value_type import Battery 6 | 7 | 8 | class BatteryProperty(Property[Battery]): 9 | 10 | @property 11 | def value(self) -> Battery: 12 | return self._get_value() 13 | 14 | def on_lower_than(self, percentage: float, callback: PropertyChangeCallback): 15 | self.on_change(partial(self._check_low, callback, percentage)) 16 | 17 | def on_charging(self, callback: PropertyChangeCallback): 18 | self.on_change(partial(self._check_charging, callback, True)) 19 | 20 | def on_discharging(self, callback: PropertyChangeCallback): 21 | self.on_change(partial(self._check_charging, callback, False)) 22 | 23 | def _check_low(self, callback: PropertyChangeCallback, expected_value: float, actual_value: Battery): 24 | if ( 25 | actual_value.level_percentage <= expected_value 26 | and (self._get_last_value() is None 27 | or actual_value.level_percentage != self._get_last_value().level_percentage) 28 | ): 29 | callback(actual_value.level_percentage) 30 | 31 | def _check_charging(self, callback: PropertyChangeCallback, expected_value: bool, actual_value: Battery): 32 | if ( 33 | expected_value == actual_value.charging 34 | and (self._get_last_value() is None 35 | or actual_value.charging != self._get_last_value().charging) 36 | ): 37 | callback(actual_value.level_percentage) 38 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/BenchmarkProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.Benchmarker import Benchmark 3 | 4 | 5 | class BenchmarkProperty(Property[Benchmark]): 6 | 7 | @property 8 | def value(self) -> Benchmark: 9 | return self._get_value() 10 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/ButtonProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import BoolProperty 2 | from dualsense_controller.api.typedef import PropertyChangeCallback 3 | 4 | 5 | class ButtonProperty(BoolProperty): 6 | 7 | def on_down(self, callback: PropertyChangeCallback): 8 | self._on_true(callback) 9 | 10 | def on_up(self, callback: PropertyChangeCallback): 11 | self._on_false(callback) 12 | 13 | @property 14 | def pressed(self) -> bool: 15 | return self._get_value() -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/ConnectionProperty.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from dualsense_controller.api.property.base import Property 4 | from dualsense_controller.api.typedef import PropertyChangeCallback 5 | from dualsense_controller.core.state.read_state.value_type import Connection 6 | 7 | 8 | class ConnectionProperty(Property[Connection]): 9 | 10 | @property 11 | def value(self) -> Connection: 12 | return self._get_value() 13 | 14 | def on_connected(self, callback: PropertyChangeCallback): 15 | self.on_change(partial(self._on_changed, callback, True)) 16 | 17 | def on_disconnected(self, callback: PropertyChangeCallback): 18 | self.on_change(partial(self._on_changed, callback, False)) 19 | 20 | @staticmethod 21 | def _on_changed(callback: PropertyChangeCallback, expected_value: bool, actual_value: Connection): 22 | if expected_value == actual_value.connected: 23 | callback(actual_value.connection_type) 24 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/ExceptionProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | 3 | 4 | class ExceptionProperty(Property[Exception]): 5 | 6 | @property 7 | def value(self) -> Exception: 8 | return self._get_value() 9 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/GyroscopeProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import Gyroscope 3 | 4 | 5 | class GyroscopeProperty(Property[Gyroscope]): 6 | pass 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/JoyStickProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import JoyStick 3 | 4 | 5 | class JoyStickProperty(Property[JoyStick]): 6 | 7 | @property 8 | def value(self) -> JoyStick: 9 | return self._get_value() 10 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/LightbarProperty.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from dualsense_controller.api.property.base import Property 4 | from dualsense_controller.core.state.write_state.enum import LightbarPulseOptions 5 | from dualsense_controller.core.state.write_state.value_type import Lightbar 6 | 7 | 8 | class LightbarProperty(Property[Lightbar]): 9 | 10 | @property 11 | def color(self) -> tuple[int, int, int]: 12 | current: Lightbar = self._get_value() 13 | return current.red, current.green, current.blue 14 | 15 | @property 16 | def is_on(self) -> bool: 17 | return self._get_value().is_on 18 | 19 | def fade_in_blue(self) -> None: 20 | self._set(pulse_options=LightbarPulseOptions.FADE_IN_BLUE) 21 | 22 | def fade_out_blue(self) -> None: 23 | self._set(pulse_options=LightbarPulseOptions.FADE_OUT_BLUE) 24 | 25 | def set_on(self) -> None: 26 | self.set_is_on(True) 27 | 28 | def set_off(self) -> None: 29 | self.set_is_on(False) 30 | 31 | def toggle_on_off(self) -> None: 32 | self.set_is_on(not self.is_on) 33 | 34 | def set_is_on(self, is_on: bool) -> None: 35 | self._set(is_on=is_on) 36 | 37 | def set_color(self, red: int, green: int, blue: int) -> None: 38 | self._set(red=red, green=green, blue=blue) 39 | 40 | def set_color_black(self) -> None: 41 | self.set_color(0, 0, 0) 42 | 43 | def set_color_white(self) -> None: 44 | self.set_color(255, 255, 255) 45 | 46 | def set_color_red(self) -> None: 47 | self.set_color(255, 0, 0) 48 | 49 | def set_color_green(self) -> None: 50 | self.set_color(0, 255, 0) 51 | 52 | def set_color_blue(self) -> None: 53 | self.set_color(0, 0, 255) 54 | 55 | def _set( 56 | self, 57 | red: int = None, 58 | green: int = None, 59 | blue: int = None, 60 | is_on: int = None, 61 | pulse_options: int = None, 62 | ): 63 | before: Lightbar = self._get_value() 64 | if ( 65 | before.pulse_options == LightbarPulseOptions.FADE_IN_BLUE 66 | and pulse_options is None 67 | ): 68 | warnings.warn('currently lightbar set to fade_in_blue. ' 69 | 'other actions like changing color are not possible. ' 70 | 'set fade_out_blue to change other colors') 71 | self._set_value(Lightbar( 72 | red=red if red is not None else before.red, 73 | green=green if green is not None else before.green, 74 | blue=blue if blue is not None else before.blue, 75 | is_on=is_on if is_on is not None else before.is_on, 76 | pulse_options=pulse_options if pulse_options is not None else before.pulse_options, 77 | )) 78 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/MicrophoneProperty.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Final 3 | 4 | from dualsense_controller.api.property.base import Property 5 | from dualsense_controller.core.state.State import State 6 | from dualsense_controller.core.state.write_state.value_type import Microphone 7 | 8 | 9 | class MicrophoneProperty(Property[Microphone]): 10 | 11 | def __init__( 12 | self, 13 | state: State[Microphone], 14 | invert_led: bool = False 15 | ): 16 | super().__init__(state) 17 | self._invert_led: Final[bool] = invert_led 18 | 19 | def toggle_muted(self) -> None: 20 | if self.is_muted: 21 | self.set_unmuted() 22 | else: 23 | self.set_muted() 24 | 25 | def set_muted(self) -> None: 26 | self._set_mute(True) 27 | 28 | def set_unmuted(self) -> None: 29 | self._set_mute(False) 30 | 31 | def refresh_workaround(self) -> None: 32 | warnings.warn("Microphone state initially not set properly. workaround enforces it", UserWarning) 33 | self.toggle_muted() 34 | self.toggle_muted() 35 | 36 | def _set_mute(self, mute: bool): 37 | self._set_value(Microphone( 38 | mute=mute, 39 | led=(mute if not self._invert_led else not mute) 40 | )) 41 | 42 | @property 43 | def is_muted(self) -> bool: 44 | return self._get_value().mute 45 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/OrientationProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import Orientation 3 | 4 | 5 | class OrientationProperty(Property[Orientation]): 6 | pass 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/PlayerLedsProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.write_state.enum import PlayerLedsBrightness, \ 3 | PlayerLedsEnable 4 | from dualsense_controller.core.state.write_state.value_type import PlayerLeds 5 | 6 | 7 | class PlayerLedsProperty(Property[PlayerLeds]): 8 | 9 | def set_off(self) -> None: 10 | self._set_enable(PlayerLedsEnable.OFF) 11 | 12 | def set_center(self) -> None: 13 | self._set_enable(PlayerLedsEnable.CENTER) 14 | 15 | def set_inner(self) -> None: 16 | self._set_enable(PlayerLedsEnable.INNER) 17 | 18 | def set_outer(self) -> None: 19 | self._set_enable(PlayerLedsEnable.OUTER) 20 | 21 | def set_all(self) -> None: 22 | self._set_enable(PlayerLedsEnable.ALL) 23 | 24 | def set_center_and_outer(self) -> None: 25 | self._set_enable(PlayerLedsEnable.CENTER | PlayerLedsEnable.OUTER) 26 | 27 | def set_brightness_high(self) -> None: 28 | self._set_brightness(PlayerLedsBrightness.HIGH) 29 | 30 | def set_brightness_medium(self) -> None: 31 | self._set_brightness(PlayerLedsBrightness.MEDIUM) 32 | 33 | def set_brightness_low(self) -> None: 34 | self._set_brightness(PlayerLedsBrightness.LOW) 35 | 36 | def _set_enable(self, enable: PlayerLedsEnable): 37 | before: PlayerLeds = self._get_value() 38 | self._set_value(PlayerLeds(enable=enable, brightness=before.brightness)) 39 | 40 | def _set_brightness(self, brightness: PlayerLedsBrightness): 41 | before: PlayerLeds = self._get_value() 42 | self._set_value(PlayerLeds(enable=before.enable, brightness=brightness)) 43 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/RumbleProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import GetSetNumberProperty 2 | 3 | 4 | class RumbleProperty(GetSetNumberProperty): 5 | pass 6 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/TouchFingerProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import TouchFinger 3 | 4 | 5 | class TouchFingerProperty(Property[TouchFinger]): 6 | pass 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/TriggerFeedbackProperty.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.api.property.base import Property 2 | from dualsense_controller.core.state.read_state.value_type import TriggerFeedback 3 | 4 | 5 | class TriggerFeedbackProperty(Property[TriggerFeedback]): 6 | pass 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/TriggerProperty.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.api.property.TriggerEffectProperty import TriggerEffectProperty 4 | from dualsense_controller.api.property.TriggerFeedbackProperty import TriggerFeedbackProperty 5 | from dualsense_controller.api.property.base import GetNumberProperty 6 | from dualsense_controller.core.state.State import State 7 | from dualsense_controller.core.state.typedef import Number 8 | 9 | 10 | class TriggerProperty(GetNumberProperty): 11 | def __init__( 12 | self, 13 | trigger_value_state: State[Number], 14 | trigger_feedback_property: TriggerFeedbackProperty, 15 | trigger_effect_property: TriggerEffectProperty 16 | ): 17 | super().__init__(state=trigger_value_state) 18 | self._trigger_feedback_property: Final[TriggerFeedbackProperty] = trigger_feedback_property 19 | self._trigger_effect_property: Final[TriggerEffectProperty] = trigger_effect_property 20 | 21 | @property 22 | def feedback(self) -> TriggerFeedbackProperty: 23 | return self._trigger_feedback_property 24 | 25 | @property 26 | def effect(self) -> TriggerEffectProperty: 27 | return self._trigger_effect_property 28 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/api/property/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/api/property/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from functools import partial 3 | from typing import Final, Generic 4 | 5 | from dualsense_controller.api.typedef import PropertyChangeCallback, PropertyType 6 | from dualsense_controller.core.state.State import State 7 | from dualsense_controller.core.state.typedef import Number 8 | 9 | 10 | class Property(Generic[PropertyType], ABC): 11 | 12 | def __init__(self, state: State[PropertyType]): 13 | self._state: Final[State[PropertyType]] = state 14 | 15 | def on_change(self, callback: PropertyChangeCallback): 16 | self._state.on_change(callback) 17 | 18 | def once_change(self, callback: PropertyChangeCallback): 19 | self._state.once_change(callback) 20 | 21 | @property 22 | def changed(self) -> bool: 23 | return self._state.has_changed_since_last_set_value 24 | 25 | def _get_value(self) -> PropertyType: 26 | return self._state.value 27 | 28 | def _get_last_value(self) -> PropertyType: 29 | return self._state.last_value 30 | 31 | def _set_value(self, value: Number) -> None: 32 | self._state.value = value 33 | 34 | 35 | class GetNumberProperty(Property[Number], ABC): 36 | 37 | @property 38 | def value(self) -> Number: 39 | return self._get_value() 40 | 41 | 42 | class GetSetNumberProperty(Property[Number], ABC): 43 | 44 | @property 45 | def value(self) -> Number: 46 | return self._get_value() 47 | 48 | def set(self, value: Number): 49 | self._set_value(value) 50 | 51 | 52 | class BoolProperty(Property[bool], ABC): 53 | 54 | def _on_true(self, callback: PropertyChangeCallback): 55 | self.on_change(partial(self._on_changed, callback, True)) 56 | 57 | def _on_false(self, callback: PropertyChangeCallback): 58 | self.on_change(partial(self._on_changed, callback, False)) 59 | 60 | @staticmethod 61 | def _on_changed(callback: PropertyChangeCallback, expected_value: bool, actual_value: bool): 62 | if expected_value == actual_value: 63 | callback() 64 | -------------------------------------------------------------------------------- /src/dualsense_controller/api/typedef.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | 3 | PropertyType = TypeVar('PropertyType') 4 | _PrChCb0 = Callable[[], None] 5 | _PrChCb1 = Callable[[PropertyType], None] 6 | _PrChCb2 = Callable[[PropertyType, int], None] 7 | _PrChCb3 = Callable[[PropertyType, PropertyType, int], None] 8 | PropertyChangeCallback = _PrChCb0 | _PrChCb1 | _PrChCb2 | _PrChCb3 9 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/Benchmarker.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from dataclasses import dataclass 3 | from time import perf_counter_ns 4 | from typing import Final 5 | 6 | 7 | @dataclass 8 | class Benchmark: 9 | duration: float 10 | per_second: int 11 | 12 | 13 | _ONE_SECOND_NS: Final[float] = 1e+9 14 | 15 | 16 | class Benchmarker: 17 | def __init__(self, maxsize: int = 50): 18 | self._durations_queue: Final[deque] = deque(maxlen=maxsize) 19 | self._last_time: int | None = None 20 | 21 | def update(self) -> Benchmark | None: 22 | current: int = perf_counter_ns() 23 | if self._last_time is None: 24 | self._last_time = current 25 | return None 26 | duration: int = perf_counter_ns() - current 27 | self._last_time = current 28 | self._durations_queue.append(duration) 29 | 30 | sum_dur: int = 0 31 | for dur in self._durations_queue: 32 | sum_dur += dur 33 | 34 | duration_mean: float = sum_dur / len(self._durations_queue) 35 | return Benchmark( 36 | duration=duration_mean, 37 | per_second=int(_ONE_SECOND_NS / duration_mean) 38 | ) 39 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/DualSenseControllerCore.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.core.Benchmarker import Benchmark, Benchmarker 4 | from dualsense_controller.core.HidControllerDevice import HidControllerDevice 5 | from dualsense_controller.core.enum import ConnectionType, EventType 6 | from dualsense_controller.core.hidapi.hidapi import DeviceInfo 7 | from dualsense_controller.core.log import Log 8 | from dualsense_controller.core.report.in_report.InReport import InReport 9 | from dualsense_controller.core.state.State import State 10 | from dualsense_controller.core.state.mapping.StateValueMapper import StateValueMapper 11 | from dualsense_controller.core.state.mapping.enum import StateValueMapping 12 | from dualsense_controller.core.state.read_state.ReadStates import ReadStates 13 | from dualsense_controller.core.state.read_state.enum import ReadStateName 14 | from dualsense_controller.core.state.read_state.value_type import Connection 15 | from dualsense_controller.core.state.typedef import Number, StateChangeCallback 16 | from dualsense_controller.core.state.write_state.WriteStates import WriteStates 17 | from dualsense_controller.core.state.write_state.enum import WriteStateName 18 | from dualsense_controller.core.typedef import EmptyCallback 19 | from dualsense_controller.core.util import format_exception 20 | 21 | 22 | class DualSenseControllerCore: 23 | 24 | # ######################################### STATIC ##########################################v 25 | @staticmethod 26 | def enumerate_devices() -> list[DeviceInfo]: 27 | return HidControllerDevice.enumerate_devices() 28 | 29 | # ######################################### BASE ##########################################v 30 | @property 31 | def is_initialized(self) -> bool: 32 | return self._hid_controller_device.is_opened 33 | 34 | @property 35 | def read_states(self) -> ReadStates: 36 | return self._read_states 37 | 38 | @property 39 | def write_states(self) -> WriteStates: 40 | return self._write_states 41 | 42 | @property 43 | def connection_type(self) -> ConnectionType: 44 | return self._hid_controller_device.connection_type 45 | 46 | # ######################################### SPECIAL STATES ##########################################v 47 | 48 | @property 49 | def connection_state(self) -> State[Connection]: 50 | return self._connection_state 51 | 52 | @property 53 | def update_benchmark_state(self) -> State[Benchmark]: 54 | return self._update_benchmark_state 55 | 56 | @property 57 | def exception_state(self) -> State[Exception]: 58 | return self._exception_state 59 | 60 | # ######################################### MAIN ##########################################v 61 | def __init__( 62 | self, 63 | # ##### BASE ##### 64 | device_index_or_device_info: int | DeviceInfo = 0, 65 | # ##### FEELING ##### 66 | left_joystick_deadzone: Number = 0, 67 | right_joystick_deadzone: Number = 0, 68 | left_trigger_deadzone: Number = 0, 69 | right_trigger_deadzone: Number = 0, 70 | gyroscope_threshold: int = 0, 71 | accelerometer_threshold: int = 0, 72 | orientation_threshold: int = 0, 73 | state_value_mapping: StateValueMapping = StateValueMapping.DEFAULT, 74 | # ##### CORE ##### 75 | enforce_update: bool = False, 76 | can_update_itself: bool = True, 77 | ): 78 | 79 | # HARDWARE 80 | self._hid_controller_device: HidControllerDevice = HidControllerDevice(device_index_or_device_info) 81 | 82 | # SPECIAL STATES 83 | self._connection_state: Final[State[Connection]] = State( 84 | name=EventType.CONNECTION_CHANGE, ignore_none=False 85 | ) 86 | 87 | self._update_benchmark_state: Final[State[Benchmark]] = State( 88 | name=EventType.UPDATE_BENCHMARK, ignore_none=True 89 | ) 90 | 91 | self._exception_state: Final[State[Exception]] = State( 92 | name=EventType.EXCEPTION, ignore_none=False 93 | ) 94 | 95 | # MAIN 96 | self._update_benchmark: Final[Benchmarker] = Benchmarker() 97 | 98 | state_value_mapper: StateValueMapper = StateValueMapper( 99 | mapping=state_value_mapping, 100 | left_joystick_deadzone=left_joystick_deadzone, 101 | right_joystick_deadzone=right_joystick_deadzone, 102 | left_trigger_deadzone=left_trigger_deadzone, 103 | right_trigger_deadzone=right_trigger_deadzone, 104 | gyroscope_threshold=gyroscope_threshold, 105 | accelerometer_threshold=accelerometer_threshold, 106 | orientation_threshold=orientation_threshold, 107 | ) 108 | 109 | self._read_states: Final[ReadStates] = ReadStates( 110 | state_value_mapper=state_value_mapper, 111 | enforce_update=enforce_update, 112 | can_update_itself=can_update_itself, 113 | ) 114 | 115 | self._write_states: Final[WriteStates] = WriteStates( 116 | state_value_mapper=state_value_mapper, 117 | ) 118 | 119 | self._hid_controller_device.on_exception(self._on_thread_exception) 120 | self._hid_controller_device.on_in_report(self._on_in_report) 121 | 122 | def on_updated(self, callback: EmptyCallback) -> None: 123 | self._read_states.on_updated(callback) 124 | 125 | def once_updated(self, callback: EmptyCallback) -> None: 126 | self._read_states.once_updated(callback) 127 | 128 | def wait_until_updated(self) -> None: 129 | 130 | wait: bool = True 131 | 132 | def on_updated() -> None: 133 | nonlocal wait 134 | wait = False 135 | 136 | self._read_states.once_updated(on_updated) 137 | while wait: 138 | pass 139 | 140 | def on_connection_change(self, callback: StateChangeCallback): 141 | self._connection_state.on_change(callback) 142 | 143 | def once_connection_change(self, callback: StateChangeCallback): 144 | self._connection_state.once_change(callback) 145 | 146 | def on_state_change(self, state_name: ReadStateName | StateChangeCallback, callback: StateChangeCallback = None): 147 | self._read_states.on_change(state_name, callback) 148 | 149 | def once_state_change(self, state_name: ReadStateName | StateChangeCallback, callback: StateChangeCallback = None): 150 | self._read_states.once_change(state_name, callback) 151 | 152 | def on_any_state_change(self, callback: StateChangeCallback): 153 | self._read_states.on_any_change(callback) 154 | 155 | def once_any_state_change(self, callback: StateChangeCallback): 156 | self._read_states.once_any_change(callback) 157 | 158 | def set_state(self, state_name: WriteStateName, value: Number): 159 | self._write_states.set_value(state_name, value) 160 | 161 | def init(self) -> None: 162 | assert not self._hid_controller_device.is_opened, 'already opened' 163 | self._hid_controller_device.open() 164 | self._connection_state.value = Connection(True, self._hid_controller_device.connection_type) 165 | 166 | def deinit(self) -> None: 167 | assert self._hid_controller_device.is_opened, 'not opened yet' 168 | self._hid_controller_device.close() 169 | self._connection_state.value = Connection(False, self._hid_controller_device.connection_type) 170 | 171 | def _on_in_report(self, in_report: InReport) -> None: 172 | 173 | self._read_states.update(in_report, self._hid_controller_device.connection_type) 174 | 175 | if self._write_states.has_changed: 176 | # print(f'Sending report.') 177 | self._write_states.update_out_report(self._hid_controller_device.out_report) 178 | self._write_states.set_unchanged() 179 | self._hid_controller_device.write() 180 | 181 | if self._update_benchmark_state.has_listeners: 182 | self._update_benchmark_state.value = self._update_benchmark.update() 183 | 184 | def _on_thread_exception(self, exception: Exception) -> None: 185 | self._exception_state.value = exception 186 | Log.error('An Exception in the loop thread occured:', format_exception(exception)) 187 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/HidControllerDevice.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from threading import Thread 3 | from typing import Final 4 | 5 | import pyee 6 | 7 | from dualsense_controller.core.core.Lockable import Lockable 8 | from dualsense_controller.core.enum import ConnectionType, EventType 9 | from dualsense_controller.core.exception import InvalidDeviceIndexException, InvalidInReportLengthException 10 | from dualsense_controller.core.hidapi import Device, DeviceInfo, enumerate 11 | from dualsense_controller.core.log import Log 12 | from dualsense_controller.core.report.in_report.Bt01InReport import Bt01InReport 13 | from dualsense_controller.core.report.in_report.Bt31InReport import Bt31InReport 14 | from dualsense_controller.core.report.in_report.InReport import InReport 15 | from dualsense_controller.core.report.in_report.Usb01InReport import Usb01InReport 16 | from dualsense_controller.core.report.in_report.enum import InReportLength 17 | from dualsense_controller.core.report.in_report.typedef import InReportCallback 18 | from dualsense_controller.core.report.out_report.Bt01OutReport import Bt01OutReport 19 | from dualsense_controller.core.report.out_report.Bt31OutReport import Bt31OutReport 20 | from dualsense_controller.core.report.out_report.OutReport import OutReport 21 | from dualsense_controller.core.report.out_report.Usb01OutReport import Usb01OutReport 22 | from dualsense_controller.core.typedef import ExceptionCallback 23 | 24 | 25 | class HidControllerDevice: 26 | VENDOR_ID: Final[int] = 0x054c 27 | PRODUCT_ID: Final[int] = 0x0ce6 28 | 29 | @staticmethod 30 | def enumerate_devices() -> list[DeviceInfo]: 31 | return enumerate(vendor_id=HidControllerDevice.VENDOR_ID, product_id=HidControllerDevice.PRODUCT_ID) 32 | 33 | @property 34 | def connection_type(self) -> ConnectionType: 35 | return self._connection_type 36 | 37 | @property 38 | def out_report(self) -> OutReport: 39 | return self._out_report_lockable.value 40 | 41 | @property 42 | def is_opened(self) -> bool: 43 | return self._hid_device is not None 44 | 45 | def __init__(self, device_index_or_device_info: int | DeviceInfo = 0): 46 | self._connection_type: ConnectionType = ConnectionType.UNDEFINED 47 | self._event_emitter: Final[pyee.EventEmitter] = pyee.EventEmitter() 48 | self._loop_thread: Thread | None = None 49 | self._stop_thread_event: threading.Event | None = None 50 | self._thread_started_event: threading.Event | None = None 51 | 52 | device_info: DeviceInfo 53 | if device_index_or_device_info is None or isinstance(device_index_or_device_info, int): 54 | device_index: int = device_index_or_device_info if device_index_or_device_info is not None else 0 55 | hid_device_infos: list[DeviceInfo] = HidControllerDevice.enumerate_devices() 56 | num_hid_device_infos: int = len(hid_device_infos) 57 | if num_hid_device_infos < device_index + 1: 58 | raise InvalidDeviceIndexException(device_index) 59 | device_info = hid_device_infos[device_index] 60 | else: 61 | device_info = device_index_or_device_info 62 | 63 | self._serial_number: Final[str] = device_info.serial_number 64 | self._path: Final[bytes] = device_info.path 65 | self._hid_device: Device | None = None 66 | 67 | self._in_report_length: InReportLength = InReportLength.DUMMY 68 | self._in_report_lockable: Final[Lockable[InReport]] = Lockable() 69 | self._out_report_lockable: Final[Lockable[OutReport]] = Lockable() 70 | 71 | def open(self): 72 | assert self._hid_device is None, "Device already opened" 73 | self._hid_device: Device = self._create() 74 | self._detect() 75 | self._start_loop_thread() 76 | 77 | def close(self) -> None: 78 | assert self._hid_device is not None, "Device already opened" 79 | self._stop_loop_thread() 80 | self._hid_device.close() 81 | self._hid_device = None 82 | 83 | def write(self) -> None: 84 | data = self._out_report_lockable.value.to_bytes() 85 | Log.verbose(data.hex(' ')) 86 | self._hid_device.write(data) 87 | 88 | def on_exception(self, callback: ExceptionCallback) -> None: 89 | self._event_emitter.on(EventType.EXCEPTION, callback) 90 | 91 | def on_in_report(self, callback: InReportCallback) -> None: 92 | self._event_emitter.on(EventType.IN_REPORT, callback) 93 | 94 | def _create(self) -> Device: 95 | return Device( 96 | vendor_id=HidControllerDevice.VENDOR_ID, 97 | product_id=HidControllerDevice.PRODUCT_ID, 98 | serial_number=self._serial_number, 99 | path=self._path, 100 | ) 101 | 102 | def _detect(self) -> None: 103 | dummy_report_bytes: bytes = self._hid_device.read(InReportLength.DUMMY) 104 | self._in_report_length: int = len(dummy_report_bytes) 105 | match self._in_report_length: 106 | case InReportLength.USB_01: 107 | self._connection_type = ConnectionType.USB_01 108 | self._in_report_lockable.value = Usb01InReport() 109 | self._out_report_lockable.value = Usb01OutReport() 110 | case InReportLength.BT_31: 111 | self._connection_type = ConnectionType.BT_31 112 | self._in_report_lockable.value = Bt31InReport() 113 | self._out_report_lockable.value = Bt31OutReport() 114 | case InReportLength.BT_01: 115 | self._connection_type = ConnectionType.BT_01 116 | self._in_report_lockable.value = Bt01InReport() 117 | self._out_report_lockable.value = Bt01OutReport() 118 | case _: 119 | raise InvalidInReportLengthException 120 | 121 | def _start_loop_thread(self) -> None: 122 | self._stop_thread_event = threading.Event() 123 | self._thread_started_event = threading.Event() 124 | self._loop_thread = Thread( 125 | target=self._loop, 126 | daemon=True, 127 | ) 128 | self._loop_thread.start() 129 | while not self._thread_started_event.is_set(): 130 | pass 131 | 132 | def _stop_loop_thread(self) -> None: 133 | self._stop_thread_event.set() 134 | self._loop_thread.join() 135 | self._loop_thread = None 136 | self._stop_thread_event = None 137 | self._thread_started_event = None 138 | 139 | def _loop(self) -> None: 140 | if not self._thread_started_event.is_set(): 141 | self._thread_started_event.set() 142 | try: 143 | while not self._stop_thread_event.is_set(): 144 | raw_bytes: bytes = self._hid_device.read(self._in_report_length) 145 | self._in_report_lockable.value.update(raw_bytes) 146 | self._event_emitter.emit(EventType.IN_REPORT, self._in_report_lockable.value) 147 | except Exception as exception: 148 | self._event_emitter.emit(EventType.EXCEPTION, exception) 149 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/core/Lockable.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | from typing import Generic, Final 3 | 4 | from dualsense_controller.core.typedef import LockableValue 5 | 6 | 7 | class Lockable(Generic[LockableValue], object): 8 | __slots__ = ['_value', '_lock'] 9 | 10 | @property 11 | def value(self) -> LockableValue | None: 12 | with self._lock: 13 | return self._value 14 | 15 | @value.setter 16 | def value(self, value: LockableValue | None) -> None: 17 | with self._lock: 18 | self._value = value 19 | 20 | def __init__(self, lock: Lock = None, value: LockableValue = None): 21 | self._lock: Final[Lock] = lock if lock is not None else Lock() 22 | self._value: LockableValue = value 23 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/core/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EventType(str, Enum): 5 | UPDATE_BENCHMARK = "UPDATE_BENCHMARK" 6 | EXCEPTION = 'EXCEPTION' 7 | CONNECTION_CHANGE = 'CONNECTION_CHANGE' 8 | IN_REPORT = 'IN_REPORT' 9 | 10 | 11 | class ConnectionType(Enum): 12 | UNDEFINED = r"¯\_(ツ)_/¯", 13 | USB_01 = "USB", 14 | BT_31 = "Bluetooth", 15 | BT_01 = "Bluetooth (minimum features)" 16 | 17 | def __str__(self) -> str: 18 | return str(self.value[0]) if isinstance(self.value, tuple) else self.value 19 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/exception.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class AbstractBaseException(ABC, Exception): 5 | def __init__(self, msg: str): 6 | super().__init__(msg) 7 | 8 | 9 | class NoDeviceDetectedException(AbstractBaseException): 10 | def __init__(self): 11 | super().__init__('No DualSense device detected') 12 | 13 | 14 | class InvalidDeviceIndexException(AbstractBaseException): 15 | def __init__(self, idx: int): 16 | super().__init__(f'Invalid DualSense device index given {idx}') 17 | 18 | 19 | class InvalidInReportLengthException(AbstractBaseException): 20 | def __init__(self): 21 | super().__init__(f'Invalid connection type') 22 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/hidapi/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Johannes Baiter 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Signal 11 Software nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/dualsense_controller/core/hidapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .hidapi import * 2 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/log.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | import logging 5 | from typing import Any, Final, Iterable 6 | 7 | NAME_LOGGER: Final[str] = "DSC_LOGGER" 8 | 9 | 10 | class LogLevel(enum.IntEnum): 11 | CRITICAL = logging.CRITICAL 12 | FATAL = logging.FATAL 13 | ERROR = logging.ERROR 14 | WARNING = logging.WARNING 15 | INFO = logging.INFO 16 | VERBOSE = 15 17 | DEBUG = logging.DEBUG 18 | TRACE = 5 19 | NOTSET = logging.NOTSET 20 | 21 | 22 | class Log: 23 | __DEFAULT_FORMATTER: Final[logging.Formatter] = logging.Formatter('[%(asctime)s - %(levelname)s] %(message)s') 24 | 25 | __instance: Log = None 26 | 27 | def __init__(self): 28 | logging.addLevelName(LogLevel.TRACE, LogLevel.TRACE.name) 29 | logging.addLevelName(LogLevel.VERBOSE, LogLevel.VERBOSE.name) 30 | 31 | self.__logger: logging.Logger = logging.getLogger(NAME_LOGGER) 32 | self.__logger.setLevel(LogLevel.INFO) 33 | 34 | stream_handler: logging.StreamHandler = logging.StreamHandler() 35 | stream_handler.formatter = self.__DEFAULT_FORMATTER 36 | self.__logger.addHandler(stream_handler) 37 | 38 | @classmethod 39 | def __get_instance(cls): 40 | if cls.__instance is None: 41 | cls.__instance = Log() 42 | return cls.__instance 43 | 44 | @classmethod 45 | def set_level(cls, level: LogLevel): 46 | cls.__get_instance().__logger.setLevel(level) 47 | 48 | @classmethod 49 | def critical(cls, msg, *args: Any): 50 | cls.__get_instance().__logger.critical(cls.__args_to_str(msg, *args)) 51 | 52 | @classmethod 53 | def fatal(cls, msg, *args: Any): 54 | cls.__get_instance().__logger.fatal(cls.__args_to_str(msg, *args)) 55 | 56 | @classmethod 57 | def error(cls, msg, *args: Any): 58 | cls.__get_instance().__logger.error(cls.__args_to_str(msg, *args)) 59 | 60 | @classmethod 61 | def exception(cls, exception: BaseException, *args: Any): 62 | cls.__get_instance().__logger.exception(exception) 63 | 64 | @classmethod 65 | def warning(cls, msg, *args: Any): 66 | cls.__get_instance().__logger.warning(cls.__args_to_str(msg, *args)) 67 | 68 | @classmethod 69 | def info(cls, msg, *args: Any): 70 | cls.__get_instance().__logger.info(cls.__args_to_str(msg, *args)) 71 | 72 | @classmethod 73 | def verbose(cls, msg, *args: Any): 74 | cls.__get_instance().__logger.log(LogLevel.VERBOSE, cls.__args_to_str(msg, *args)) 75 | 76 | @classmethod 77 | def debug(cls, msg, *args: Any): 78 | cls.__get_instance().__logger.debug(cls.__args_to_str(msg, *args)) 79 | 80 | @classmethod 81 | def trace(cls, msg, *args: Any): 82 | cls.__get_instance().__logger.log(LogLevel.TRACE, cls.__args_to_str(msg, *args)) 83 | 84 | @classmethod 85 | def __args_to_str(cls, msg, *args: Any) -> str: 86 | args_as_str: Iterable[str] = map(lambda arg: str(arg), args) 87 | msg = str(msg) 88 | msg = f'{msg}, ' if len(args) else msg 89 | return f'{msg}{", ".join(args_as_str)}' 90 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/report/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/Bt01InReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.in_report.InReport import InReport 2 | 3 | 4 | class Bt01InReport(InReport): 5 | def __init__(self, raw_bytes: bytearray = None): 6 | super().__init__({ 7 | "axes_0": 0, "axes_1": 1, "axes_2": 2, "axes_3": 3, 8 | "buttons_0": 4, "buttons_1": 5, "buttons_2": 6, 9 | "axes_4": 7, "axes_5": 8 10 | }, raw_bytes=raw_bytes) 11 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/Bt31InReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.in_report.InReport import InReport 2 | 3 | 4 | # ??? byte 0 5 | # ??? byte 7? 6 | # ??? byte 11 7 | # ??? bytes 28-32 8 | # ??? byte 41 9 | # ??? bytes 44-52 10 | # ??? bytes 55-76 11 | class Bt31InReport(InReport): 12 | 13 | def __init__(self, raw_bytes: bytearray = None): 14 | super().__init__({ 15 | "axes_0": 1, "axes_1": 2, "axes_2": 3, "axes_3": 4, "axes_4": 5, "axes_5": 6, 16 | "buttons_0": 8, "buttons_1": 9, "buttons_2": 10, 17 | "timestamp_0": 12, "timestamp_1": 13, "timestamp_2": 14, "timestamp_3": 15, 18 | "gyro_x_0": 16, "gyro_x_1": 17, "gyro_y_0": 18, "gyro_y_1": 19, "gyro_z_0": 20, "gyro_z_1": 21, 19 | "accel_x_0": 22, "accel_x_1": 23, "accel_y_0": 24, "accel_y_1": 25, "accel_z_0": 26, "accel_z_1": 27, 20 | "touch_1_0": 33, "touch_1_1": 34, "touch_1_2": 35, "touch_1_3": 36, 21 | "touch_2_0": 37, "touch_2_1": 38, "touch_2_2": 39, "touch_2_3": 40, 22 | "right_trigger_feedback": 42, "left_trigger_feedback": 43, 23 | "battery_0": 53, "battery_1": 54, 24 | }, raw_bytes=raw_bytes) 25 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/Usb01InReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.in_report.InReport import InReport 2 | 3 | 4 | # ??? byte 31 5 | # ??? byte 40 6 | # ??? bytes 43-51 7 | class Usb01InReport(InReport): 8 | def __init__(self, raw_bytes: bytearray = None): 9 | super().__init__({ 10 | "axes_0": 0, "axes_1": 1, "axes_2": 2, "axes_3": 3, "axes_4": 4, "axes_5": 5, 11 | "seq_num": 6, 12 | "buttons_0": 7, "buttons_1": 8, "buttons_2": 9, "buttons_3": 10, 13 | "timestamp_0": 11, "timestamp_1": 12, "timestamp_2": 13, "timestamp_3": 14, 14 | "gyro_x_0": 15, "gyro_x_1": 16, "gyro_y_0": 17, "gyro_y_1": 18, "gyro_z_0": 19, "gyro_z_1": 20, 15 | "accel_x_0": 21, "accel_x_1": 22, "accel_y_0": 23, "accel_y_1": 24, "accel_z_0": 25, "accel_z_1": 26, 16 | "sensor_timestamp_0": 27, "sensor_timestamp_1": 28, "sensor_timestamp_2": 29, "sensor_timestamp_3": 30, 17 | "touch_1_0": 32, "touch_1_1": 33, "touch_1_2": 34, "touch_1_3": 35, 18 | "touch_2_0": 36, "touch_2_1": 37, "touch_2_2": 38, "touch_2_3": 39, 19 | "right_trigger_feedback": 41, "left_trigger_feedback": 42, 20 | "battery_0": 52, "battery_1": 53 21 | }, raw_bytes=raw_bytes) 22 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/report/in_report/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class InReportLength(int, Enum): 5 | DUMMY = 100 6 | USB_01 = 64 7 | BT_31 = 78 8 | BT_01 = 10 9 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/in_report/typedef.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from dualsense_controller.core.report.in_report.InReport import InReport 4 | 5 | InReportCallback = Callable[[InReport], None] 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/Bt01OutReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.out_report.OutReport import OutReport 2 | from dualsense_controller.core.report.out_report.enum import OutReportLength 3 | 4 | 5 | class Bt01OutReport(OutReport): 6 | def to_bytes(self) -> bytes: 7 | out_report_bytes: bytes = bytearray(OutReportLength.BT_31) 8 | return out_report_bytes 9 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/Bt31OutReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.out_report.OutReport import OutReport 2 | from dualsense_controller.core.report.out_report.crc32 import compute_crc32_checksum 3 | from dualsense_controller.core.report.out_report.enum import OutReportLength 4 | from dualsense_controller.core.report.out_report.util import clamp_byte 5 | from dualsense_controller.core.state.write_state.enum import LightbarMode 6 | 7 | 8 | class Bt31OutReport(OutReport): 9 | def to_bytes(self) -> bytes: 10 | # print("-----------> BT31") 11 | 12 | out_report_bytes: bytearray = bytearray(OutReportLength.BT_31) 13 | 14 | # report type 15 | out_report_bytes[0] = 0x31 16 | out_report_bytes[1] = self.operating_mode 17 | 18 | # report data 19 | out_report_bytes[2] = self.flags_physics 20 | out_report_bytes[3] = self.flags_controls 21 | 22 | # DualShock 4 compatibility update_level. 23 | out_report_bytes[4] = clamp_byte(self.motor_right) 24 | out_report_bytes[5] = clamp_byte(self.motor_left) 25 | 26 | out_report_bytes[10] = self.microphone_led 27 | out_report_bytes[11] = 0x10 if self.microphone_mute else 0x00 28 | 29 | out_report_bytes[12] = self.right_trigger_effect_mode 30 | out_report_bytes[13] = self.right_trigger_effect_param1 31 | out_report_bytes[14] = self.right_trigger_effect_param2 32 | out_report_bytes[15] = self.right_trigger_effect_param3 33 | out_report_bytes[16] = self.right_trigger_effect_param4 34 | out_report_bytes[17] = self.right_trigger_effect_param5 35 | out_report_bytes[18] = self.right_trigger_effect_param6 36 | out_report_bytes[19] = self.right_trigger_effect_param7 37 | out_report_bytes[20] = self.right_trigger_effect_param8 38 | out_report_bytes[21] = self.right_trigger_effect_param9 39 | out_report_bytes[22] = self.right_trigger_effect_param10 40 | 41 | out_report_bytes[23] = self.left_trigger_effect_mode 42 | out_report_bytes[24] = self.left_trigger_effect_param1 43 | out_report_bytes[25] = self.left_trigger_effect_param2 44 | out_report_bytes[26] = self.left_trigger_effect_param3 45 | out_report_bytes[27] = self.left_trigger_effect_param4 46 | out_report_bytes[28] = self.left_trigger_effect_param5 47 | out_report_bytes[29] = self.left_trigger_effect_param6 48 | out_report_bytes[30] = self.left_trigger_effect_param7 49 | out_report_bytes[31] = self.left_trigger_effect_param8 50 | out_report_bytes[32] = self.left_trigger_effect_param9 51 | out_report_bytes[33] = self.left_trigger_effect_param10 52 | 53 | out_report_bytes[40] = self.led_options 54 | out_report_bytes[41] = LightbarMode.LIGHT_ON if self.lightbar_on_off else LightbarMode.LIGHT_OFF 55 | 56 | out_report_bytes[43] = self.lightbar_pulse_options 57 | out_report_bytes[44] = self.player_leds_brightness 58 | out_report_bytes[45] = self.player_leds_enable 59 | 60 | out_report_bytes[46] = self.lightbar_red 61 | out_report_bytes[47] = self.lightbar_green 62 | out_report_bytes[48] = self.lightbar_blue 63 | 64 | crc32_checksum_result: int = compute_crc32_checksum(out_report_bytes) 65 | 66 | out_report_bytes[74] = (crc32_checksum_result & 0x000000FF) 67 | out_report_bytes[75] = (crc32_checksum_result & 0x0000FF00) >> 8 68 | out_report_bytes[76] = (crc32_checksum_result & 0x00FF0000) >> 16 69 | out_report_bytes[77] = (crc32_checksum_result & 0xFF000000) >> 24 70 | 71 | return out_report_bytes 72 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/OutReport.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | 4 | from dualsense_controller.core.state.write_state.enum import FlagsPhysics, FlagsControls, LedOptions, \ 5 | LightbarPulseOptions, OperatingMode, PlayerLedsBrightness, PlayerLedsEnable 6 | 7 | 8 | @dataclass(slots=True) 9 | class OutReport(ABC): 10 | 11 | @abstractmethod 12 | def to_bytes(self) -> bytes: 13 | pass 14 | 15 | operating_mode: int = OperatingMode.DS5_MODE 16 | flags_physics: int = FlagsPhysics.ALL 17 | flags_controls: int = FlagsControls.ALL 18 | 19 | lightbar_red: int = 0x00 20 | lightbar_green: int = 0x00 21 | lightbar_blue: int = 0x00 22 | 23 | motor_left: int = 0x00 24 | motor_right: int = 0x00 25 | 26 | left_trigger_effect_mode: int = 0x00 27 | left_trigger_effect_param1: int = 0x00 28 | left_trigger_effect_param2: int = 0x00 29 | left_trigger_effect_param3: int = 0x00 30 | left_trigger_effect_param4: int = 0x00 31 | left_trigger_effect_param5: int = 0x00 32 | left_trigger_effect_param6: int = 0x00 33 | left_trigger_effect_param7: int = 0x00 34 | left_trigger_effect_param8: int = 0x00 35 | left_trigger_effect_param9: int = 0x00 36 | left_trigger_effect_param10: int = 0x00 37 | 38 | right_trigger_effect_mode: int = 0x00 39 | right_trigger_effect_param1: int = 0x00 40 | right_trigger_effect_param2: int = 0x00 41 | right_trigger_effect_param3: int = 0x00 42 | right_trigger_effect_param4: int = 0x00 43 | right_trigger_effect_param5: int = 0x00 44 | right_trigger_effect_param6: int = 0x00 45 | right_trigger_effect_param7: int = 0x00 46 | right_trigger_effect_param8: int = 0x00 47 | right_trigger_effect_param9: int = 0x00 48 | right_trigger_effect_param10: int = 0x00 49 | 50 | lightbar_on_off: bool = True 51 | 52 | microphone_led: bool = False 53 | microphone_mute: bool = True 54 | 55 | led_options: LedOptions = LedOptions.ALL 56 | lightbar_pulse_options: LightbarPulseOptions = LightbarPulseOptions.OFF 57 | player_leds_brightness: PlayerLedsBrightness = PlayerLedsBrightness.HIGH 58 | player_leds_enable: int = PlayerLedsEnable.OFF 59 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/Usb01OutReport.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.report.out_report.OutReport import OutReport 2 | from dualsense_controller.core.report.out_report.enum import OutReportLength 3 | from dualsense_controller.core.report.out_report.util import clamp_byte 4 | from dualsense_controller.core.state.write_state.enum import LightbarMode 5 | 6 | 7 | class Usb01OutReport(OutReport): 8 | def to_bytes(self) -> bytes: 9 | # print("-----------> usb") 10 | 11 | # reportId = 0x02 12 | 13 | out_report_bytes: bytearray = bytearray(OutReportLength.USB_01) 14 | 15 | # report type 16 | out_report_bytes[0] = self.operating_mode 17 | 18 | # report data 19 | out_report_bytes[1] = self.flags_physics 20 | 21 | # 22 | # valid_flag1 - further flags determining what changes this packet will perform 23 | # or: LightEffectMode 24 | # 25 | # bit 0: MIC_MUTE_LED_CONTROL_ENABLE - toggling microphone LED 26 | # bit 1: POWER_SAVE_CONTROL_ENABLE - toggling audio/mic mute 27 | # bit 2: LIGHTBAR_CONTROL_ENABLE - toggling LED strips on the sides of the touchpad 28 | # bit 3: RELEASE_LEDS - will actively turn all LEDs off? Convenience flag? 29 | # bit 4: PLAYER_INDICATOR_CONTROL_ENABLE - toggling white player indicator LEDs below touchpad 30 | # bit 5: ??? 31 | # bit 6: adjustment of overall motor/effect power (index 37 - read note on triggers) 32 | # bit 7: ??? 33 | out_report_bytes[2] = self.flags_controls 34 | 35 | # DualShock 4 compatibility update_level. 36 | out_report_bytes[3] = clamp_byte(self.motor_right) 37 | out_report_bytes[4] = clamp_byte(self.motor_left) 38 | 39 | # mute_button_led 40 | # 0: mute LED off 41 | # 1: mute LED on 42 | out_report_bytes[9] = self.microphone_led 43 | 44 | # power_save_control 45 | # bit 4: POWER_SAVE_CONTROL_MIC_MUTE 46 | out_report_bytes[10] = 0x10 if self.microphone_mute else 0x00 47 | 48 | # Right trigger effect 49 | # UpdateLevel 50 | # 0x00: off 51 | # 0x01: mode1 52 | # 0x02: mode2 53 | # 0x05: mode1 + mode4 54 | # 0x06: mode2 + mode4 55 | # 0x21: mode1 + mode20 56 | # 0x25: mode1 + mode4 + mode20 57 | # 0x26: mode2 + mode4 + mode20 58 | # 0xFC: calibration 59 | out_report_bytes[11] = self.right_trigger_effect_mode 60 | out_report_bytes[12] = self.right_trigger_effect_param1 61 | out_report_bytes[13] = self.right_trigger_effect_param2 62 | out_report_bytes[14] = self.right_trigger_effect_param3 63 | out_report_bytes[15] = self.right_trigger_effect_param4 64 | out_report_bytes[16] = self.right_trigger_effect_param5 65 | out_report_bytes[17] = self.right_trigger_effect_param6 66 | out_report_bytes[18] = self.right_trigger_effect_param7 67 | out_report_bytes[19] = self.right_trigger_effect_param8 68 | out_report_bytes[20] = self.right_trigger_effect_param9 69 | out_report_bytes[21] = self.right_trigger_effect_param10 70 | 71 | out_report_bytes[22] = self.left_trigger_effect_mode 72 | out_report_bytes[23] = self.left_trigger_effect_param1 73 | out_report_bytes[24] = self.left_trigger_effect_param2 74 | out_report_bytes[25] = self.left_trigger_effect_param3 75 | out_report_bytes[26] = self.left_trigger_effect_param4 76 | out_report_bytes[27] = self.left_trigger_effect_param5 77 | out_report_bytes[28] = self.left_trigger_effect_param6 78 | out_report_bytes[29] = self.left_trigger_effect_param7 79 | out_report_bytes[30] = self.left_trigger_effect_param8 80 | out_report_bytes[31] = self.left_trigger_effect_param9 81 | out_report_bytes[32] = self.left_trigger_effect_param10 82 | 83 | out_report_bytes[39] = self.led_options 84 | 85 | # Lightbar on/off 86 | out_report_bytes[41] = LightbarMode.LIGHT_ON if self.lightbar_on_off else LightbarMode.LIGHT_OFF 87 | 88 | # Disable/Endable LEDs or Pulse/Fade-Options? 89 | out_report_bytes[42] = self.lightbar_pulse_options 90 | out_report_bytes[43] = self.player_leds_brightness 91 | out_report_bytes[44] = self.player_leds_enable 92 | 93 | out_report_bytes[45] = self.lightbar_red 94 | out_report_bytes[46] = self.lightbar_green 95 | out_report_bytes[47] = self.lightbar_blue 96 | 97 | return out_report_bytes 98 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/report/out_report/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/crc32.py: -------------------------------------------------------------------------------- 1 | import array 2 | from typing import Final 3 | 4 | _CRC32_HASH_TABLE: Final[array.array] = array.array('I', [ 5 | 0xd202ef8d, 0xa505df1b, 0x3c0c8ea1, 0x4b0bbe37, 0xd56f2b94, 0xa2681b02, 0x3b614ab8, 0x4c667a2e, 6 | 0xdcd967bf, 0xabde5729, 0x32d70693, 0x45d03605, 0xdbb4a3a6, 0xacb39330, 0x35bac28a, 0x42bdf21c, 7 | 0xcfb5ffe9, 0xb8b2cf7f, 0x21bb9ec5, 0x56bcae53, 0xc8d83bf0, 0xbfdf0b66, 0x26d65adc, 0x51d16a4a, 8 | 0xc16e77db, 0xb669474d, 0x2f6016f7, 0x58672661, 0xc603b3c2, 0xb1048354, 0x280dd2ee, 0x5f0ae278, 9 | 0xe96ccf45, 0x9e6bffd3, 0x762ae69, 0x70659eff, 0xee010b5c, 0x99063bca, 0xf6a70, 0x77085ae6, 10 | 0xe7b74777, 0x90b077e1, 0x9b9265b, 0x7ebe16cd, 0xe0da836e, 0x97ddb3f8, 0xed4e242, 0x79d3d2d4, 11 | 0xf4dbdf21, 0x83dcefb7, 0x1ad5be0d, 0x6dd28e9b, 0xf3b61b38, 0x84b12bae, 0x1db87a14, 0x6abf4a82, 12 | 0xfa005713, 0x8d076785, 0x140e363f, 0x630906a9, 0xfd6d930a, 0x8a6aa39c, 0x1363f226, 0x6464c2b0, 13 | 0xa4deae1d, 0xd3d99e8b, 0x4ad0cf31, 0x3dd7ffa7, 0xa3b36a04, 0xd4b45a92, 0x4dbd0b28, 0x3aba3bbe, 14 | 0xaa05262f, 0xdd0216b9, 0x440b4703, 0x330c7795, 0xad68e236, 0xda6fd2a0, 0x4366831a, 0x3461b38c, 15 | 0xb969be79, 0xce6e8eef, 0x5767df55, 0x2060efc3, 0xbe047a60, 0xc9034af6, 0x500a1b4c, 0x270d2bda, 16 | 0xb7b2364b, 0xc0b506dd, 0x59bc5767, 0x2ebb67f1, 0xb0dff252, 0xc7d8c2c4, 0x5ed1937e, 0x29d6a3e8, 17 | 0x9fb08ed5, 0xe8b7be43, 0x71beeff9, 0x6b9df6f, 0x98dd4acc, 0xefda7a5a, 0x76d32be0, 0x1d41b76, 18 | 0x916b06e7, 0xe66c3671, 0x7f6567cb, 0x862575d, 0x9606c2fe, 0xe101f268, 0x7808a3d2, 0xf0f9344, 19 | 0x82079eb1, 0xf500ae27, 0x6c09ff9d, 0x1b0ecf0b, 0x856a5aa8, 0xf26d6a3e, 0x6b643b84, 0x1c630b12, 20 | 0x8cdc1683, 0xfbdb2615, 0x62d277af, 0x15d54739, 0x8bb1d29a, 0xfcb6e20c, 0x65bfb3b6, 0x12b88320, 21 | 0x3fba6cad, 0x48bd5c3b, 0xd1b40d81, 0xa6b33d17, 0x38d7a8b4, 0x4fd09822, 0xd6d9c998, 0xa1def90e, 22 | 0x3161e49f, 0x4666d409, 0xdf6f85b3, 0xa868b525, 0x360c2086, 0x410b1010, 0xd80241aa, 0xaf05713c, 23 | 0x220d7cc9, 0x550a4c5f, 0xcc031de5, 0xbb042d73, 0x2560b8d0, 0x52678846, 0xcb6ed9fc, 0xbc69e96a, 24 | 0x2cd6f4fb, 0x5bd1c46d, 0xc2d895d7, 0xb5dfa541, 0x2bbb30e2, 0x5cbc0074, 0xc5b551ce, 0xb2b26158, 25 | 0x4d44c65, 0x73d37cf3, 0xeada2d49, 0x9ddd1ddf, 0x3b9887c, 0x74beb8ea, 0xedb7e950, 0x9ab0d9c6, 26 | 0xa0fc457, 0x7d08f4c1, 0xe401a57b, 0x930695ed, 0xd62004e, 0x7a6530d8, 0xe36c6162, 0x946b51f4, 27 | 0x19635c01, 0x6e646c97, 0xf76d3d2d, 0x806a0dbb, 0x1e0e9818, 0x6909a88e, 0xf000f934, 0x8707c9a2, 28 | 0x17b8d433, 0x60bfe4a5, 0xf9b6b51f, 0x8eb18589, 0x10d5102a, 0x67d220bc, 0xfedb7106, 0x89dc4190, 29 | 0x49662d3d, 0x3e611dab, 0xa7684c11, 0xd06f7c87, 0x4e0be924, 0x390cd9b2, 0xa0058808, 0xd702b89e, 30 | 0x47bda50f, 0x30ba9599, 0xa9b3c423, 0xdeb4f4b5, 0x40d06116, 0x37d75180, 0xaede003a, 0xd9d930ac, 31 | 0x54d13d59, 0x23d60dcf, 0xbadf5c75, 0xcdd86ce3, 0x53bcf940, 0x24bbc9d6, 0xbdb2986c, 0xcab5a8fa, 32 | 0x5a0ab56b, 0x2d0d85fd, 0xb404d447, 0xc303e4d1, 0x5d677172, 0x2a6041e4, 0xb369105e, 0xc46e20c8, 33 | 0x72080df5, 0x50f3d63, 0x9c066cd9, 0xeb015c4f, 0x7565c9ec, 0x262f97a, 0x9b6ba8c0, 0xec6c9856, 34 | 0x7cd385c7, 0xbd4b551, 0x92dde4eb, 0xe5dad47d, 0x7bbe41de, 0xcb97148, 0x95b020f2, 0xe2b71064, 35 | 0x6fbf1d91, 0x18b82d07, 0x81b17cbd, 0xf6b64c2b, 0x68d2d988, 0x1fd5e91e, 0x86dcb8a4, 0xf1db8832, 36 | 0x616495a3, 0x1663a535, 0x8f6af48f, 0xf86dc419, 0x660951ba, 0x110e612c, 0x88073096, 0xff000000 37 | ]) 38 | _CRC32_SEED: Final[int] = 0xeada2d49 39 | 40 | BT_31_CRC32_REPORT_LEN: Final[int] = 74 41 | 42 | 43 | def compute_crc32_checksum(out_report_bytes: bytes): 44 | checksum: int = 0xeada2d49 45 | 46 | for i in range(0, BT_31_CRC32_REPORT_LEN): 47 | checksum = _CRC32_HASH_TABLE[(checksum & 0xFF) ^ (out_report_bytes[i] & 0xFF)] ^ (checksum >> 8) 48 | 49 | return checksum 50 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OutReportLength(int, Enum): 5 | USB_01 = 48 6 | BT_31 = 78 7 | BT_01 = 10 8 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/report/out_report/util.py: -------------------------------------------------------------------------------- 1 | def clamp(value: int, val_min: int, val_max: int) -> int: 2 | return min(val_max, max(val_min, value)) 3 | 4 | 5 | def clamp_byte(value: int) -> int: 6 | return min(255, max(0, value)) 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/BaseStates.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.core.state.State import State 4 | from dualsense_controller.core.state.mapping.StateValueMapper import StateValueMapper 5 | from dualsense_controller.core.state.typedef import StateChangeCallback, StateName, StateValue 6 | 7 | 8 | class BaseStates: 9 | 10 | def __init__(self, state_value_mapper: StateValueMapper): 11 | self._states_dict: Final[dict[StateName, State]] = {} 12 | self._state_value_mapper: Final[StateValueMapper] = state_value_mapper 13 | 14 | @property 15 | def has_changed_states(self) -> bool: 16 | return any(True for key, state in self._states_dict.items() if state.has_changed_since_last_set_value) 17 | 18 | def once_change( 19 | self, name_or_callback: StateName | StateChangeCallback, callback: StateChangeCallback | None = None 20 | ): 21 | if callback is None: 22 | self.once_any_change(name_or_callback) 23 | else: 24 | self._get_state_by_name(name_or_callback).once_change(callback) 25 | 26 | def on_change( 27 | self, name_or_callback: StateName | StateChangeCallback, callback: StateChangeCallback | None = None 28 | ): 29 | if callback is None: 30 | self.on_any_change(name_or_callback) 31 | else: 32 | self._get_state_by_name(name_or_callback).on_change(callback) 33 | 34 | def on_any_change(self, callback: StateChangeCallback): 35 | for state_name, state in self._states_dict.items(): 36 | state.on_change(callback) 37 | 38 | def once_any_change(self, callback: StateChangeCallback): 39 | for state_name, state in self._states_dict.items(): 40 | state.once_change(callback) 41 | 42 | def remove_change_listener( 43 | self, name_or_callback: StateName | StateChangeCallback, callback: StateChangeCallback | None = None 44 | ) -> None: 45 | if isinstance(name_or_callback, StateName): 46 | self._get_state_by_name(name_or_callback).remove_change_listener(callback) 47 | elif callable(name_or_callback): 48 | self.remove_any_change_listener(name_or_callback) 49 | else: 50 | self.remove_all_change_listeners() 51 | 52 | def remove_all_change_listeners(self) -> None: 53 | for state_name, state in self._states_dict.items(): 54 | state.remove_all_change_listeners() 55 | 56 | def remove_any_change_listener(self, callback: StateChangeCallback) -> None: 57 | for state_name, state in self._states_dict.items(): 58 | state.remove_change_listener(callback) 59 | 60 | def _register_state(self, name: StateName, state: State[StateValue]) -> None: 61 | self._states_dict[name] = state 62 | 63 | def _get_state_by_name(self, name: StateName) -> State[StateValue]: 64 | return self._states_dict[name] 65 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/State.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | from threading import Lock 5 | from typing import Final, Generic 6 | 7 | from dualsense_controller.core.core.Lockable import Lockable 8 | from dualsense_controller.core.state.StateValueCallbackManager import StateValueCallbackManager 9 | from dualsense_controller.core.state.mapping.typedef import MapFn 10 | from dualsense_controller.core.state.typedef import CompareFn, CompareResult, StateChangeCallback, StateName, \ 11 | StateValue 12 | 13 | 14 | class State(Generic[StateValue]): 15 | 16 | @staticmethod 17 | def _compare(before: StateValue, after: StateValue) -> CompareResult: 18 | return (True, after) if before != after else (False, after) 19 | 20 | def __repr__(self) -> str: 21 | return f'State[{type(self._value_raw).__name__}]({self.name}: {self._value_raw} -> {self.value})' 22 | 23 | @property 24 | def value(self) -> StateValue: 25 | value_raw: StateValue = self.value_raw 26 | return value_raw if not callable(self._raw_to_mapped_fn) else self._raw_to_mapped_fn(value_raw) 27 | 28 | @value.setter 29 | def value(self, value_mapped: StateValue) -> None: 30 | self._set_value(value_mapped) 31 | 32 | @property 33 | def last_value(self) -> StateValue: 34 | last_value_raw: StateValue = self.last_value_raw 35 | return last_value_raw if not callable(self._raw_to_mapped_fn) else self._raw_to_mapped_fn(last_value_raw) 36 | 37 | @property 38 | def value_raw(self) -> StateValue: 39 | return self._value_raw 40 | 41 | @property 42 | def last_value_raw(self) -> StateValue: 43 | return self._last_value_raw 44 | 45 | @property 46 | def has_changed_since_last_set_value(self) -> bool: 47 | if self._disable_change_detection: 48 | return False 49 | return self._changed_since_last_set_value 50 | 51 | @property 52 | def has_listeners(self) -> bool: 53 | return self._callback_manager.has_listeners 54 | 55 | # LOCKED GETTERS AND SETTERS 56 | 57 | @property 58 | def _value_raw(self) -> StateValue: 59 | return self.__value.value 60 | 61 | @_value_raw.setter 62 | def _value_raw(self, _value: StateValue) -> None: 63 | self.__value.value = _value 64 | 65 | @property 66 | def _last_value_raw(self) -> StateValue: 67 | return self.__last_value.value 68 | 69 | @_last_value_raw.setter 70 | def _last_value_raw(self, _last_value: StateValue) -> None: 71 | self.__last_value.value = _last_value 72 | 73 | @property 74 | def _change_timestamp(self) -> int: 75 | return self.__change_timestamp.value 76 | 77 | @_change_timestamp.setter 78 | def _change_timestamp(self, _change_timestamp: int) -> None: 79 | self.__change_timestamp.value = _change_timestamp 80 | 81 | @property 82 | def _changed_since_last_set_value(self) -> bool: 83 | return self.__changed_since_last_update.value 84 | 85 | @_changed_since_last_set_value.setter 86 | def _changed_since_last_set_value(self, _changed_since_last_update: bool) -> None: 87 | self.__changed_since_last_update.value = _changed_since_last_update 88 | 89 | def __init__( 90 | self, 91 | name: StateName, 92 | value: StateValue = None, 93 | default_value: StateValue = None, 94 | ignore_none: bool = True, 95 | mapped_to_raw_fn: MapFn = None, 96 | raw_to_mapped_fn: MapFn = None, 97 | compare_fn: CompareFn = None, 98 | disable_change_detection: bool = False, 99 | ): 100 | # CONST 101 | self.name: Final[StateName] = name 102 | self._lock: Final[Lock] = Lock() 103 | self._callback_manager: Final[StateValueCallbackManager[StateValue]] = StateValueCallbackManager(name) 104 | self._compare_fn: Final[CompareFn] = compare_fn if compare_fn is not None else State._compare 105 | self._mapped_to_raw_fn: Final[MapFn] = mapped_to_raw_fn 106 | self._raw_to_mapped_fn: Final[MapFn] = raw_to_mapped_fn 107 | self._ignore_none: Final[bool] = ignore_none 108 | self._default_value: Final[StateValue | None] = default_value 109 | self._disable_change_detection: Final[bool] = disable_change_detection 110 | 111 | # VAR 112 | self.__value: Lockable[StateValue | None] = Lockable( 113 | lock=self._lock, 114 | value=value if value is not None else default_value 115 | ) 116 | self.__last_value: Lockable[StateValue | None] = Lockable( 117 | lock=self._lock, 118 | value=None 119 | ) 120 | self.__change_timestamp: Lockable[int] = Lockable( 121 | lock=self._lock, 122 | value=0 123 | ) 124 | self.__changed_since_last_update: Lockable[bool] = Lockable( 125 | lock=self._lock, 126 | value=False 127 | ) 128 | 129 | def set_value_raw_without_triggering_change(self, new_value: StateValue | None): 130 | self._set_value_raw(new_value, trigger_change_on_changed=False) 131 | 132 | def set_value_without_triggering_change(self, new_value: StateValue | None): 133 | self._set_value(new_value, trigger_change_on_changed=False) 134 | 135 | def _set_value( 136 | self, 137 | value_mapped: StateValue | None, 138 | trigger_change_on_changed: bool = True, 139 | ) -> None: 140 | value_raw: StateValue | None = ( 141 | value_mapped if self._mapped_to_raw_fn is None else self._mapped_to_raw_fn(value_mapped) 142 | ) 143 | # print(f'{self.name}: {value_mapped} -> {raw_val}') 144 | self._set_value_raw(value_raw, trigger_change_on_changed=trigger_change_on_changed) 145 | 146 | def trigger_change_if_changed(self) -> None: 147 | if self.has_changed_since_last_set_value: 148 | self._trigger_change() 149 | 150 | def on_change(self, callback: StateChangeCallback) -> None: 151 | self._callback_manager.on_change(callback) 152 | 153 | def once_change(self, callback: StateChangeCallback) -> None: 154 | self._callback_manager.once_change(callback) 155 | 156 | def remove_change_listener(self, callback: StateChangeCallback | None = None) -> None: 157 | self._callback_manager.remove_change_listener(callback) 158 | 159 | def remove_all_change_listeners(self) -> None: 160 | self._callback_manager.remove_all_change_listeners() 161 | 162 | # ################# GETTERS AND SETTERS ############### 163 | 164 | def _set_value_raw(self, value_raw: StateValue | None, trigger_change_on_changed: bool = True) -> None: 165 | old_value: StateValue = self._value_raw 166 | new_value: StateValue = value_raw 167 | if old_value is None and self._default_value is not None: 168 | old_value = self._default_value 169 | if new_value is None and self._default_value is not None: 170 | new_value = self._default_value 171 | if self._ignore_none and (old_value is None or new_value is None): 172 | self._change_value( 173 | old_value=new_value, 174 | new_value=new_value, 175 | changed=False, 176 | trigger_change=False, 177 | ) 178 | return 179 | changed, new_value = self._compare_fn(old_value, new_value) 180 | self._change_value( 181 | old_value=old_value, 182 | new_value=new_value, 183 | changed=changed, 184 | trigger_change=(changed if trigger_change_on_changed else False), 185 | ) 186 | 187 | def _change_value( 188 | self, 189 | old_value: StateValue, 190 | new_value: StateValue, 191 | changed: bool, 192 | trigger_change: bool = True, 193 | ) -> None: 194 | self._last_value_raw = old_value 195 | self._value_raw = new_value 196 | self._change_timestamp = time.perf_counter_ns() 197 | self._changed_since_last_set_value = changed 198 | if not self._disable_change_detection and trigger_change: 199 | self._trigger_change() 200 | 201 | def _trigger_change(self): 202 | self._callback_manager.emit_change(self.last_value, self.value, self._change_timestamp) 203 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/StateValueCallbackManager.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Final, Generic 3 | 4 | import pyee 5 | 6 | from dualsense_controller.core.state.typedef import StateChangeCallback, StateName, StateValue 7 | 8 | 9 | class StateValueCallbackManager(Generic[StateValue]): 10 | 11 | @property 12 | def has_listeners(self) -> bool: 13 | return len(self._event_emitter.event_names()) > 0 14 | 15 | def __init__(self, name: StateName): 16 | self._name: Final[StateName] = name 17 | self._event_emitter: Final[pyee.EventEmitter] = pyee.EventEmitter() 18 | 19 | self._event_name_0_args: Final[str] = f'{name}_0' 20 | self._event_name_1_args: Final[str] = f'{name}_1' 21 | self._event_name_2_args: Final[str] = f'{name}_2' 22 | self._event_name_3_args: Final[str] = f'{name}_3' 23 | self._event_name_4_args: Final[str] = f'{name}_4' 24 | 25 | def on_change(self, callback: StateChangeCallback) -> None: 26 | self._event_emitter.on(self._get_event_name_by_callable(callback), callback) 27 | 28 | def once_change(self, callback: StateChangeCallback) -> None: 29 | self._event_emitter.once(self._get_event_name_by_callable(callback), callback) 30 | 31 | def remove_change_listener(self, callback: StateChangeCallback | None = None) -> None: 32 | if callback is None: 33 | self.remove_all_change_listeners() 34 | else: 35 | self._event_emitter.remove_listener( 36 | self._get_event_name_by_callable(callback), 37 | callback 38 | ) 39 | 40 | def remove_all_change_listeners(self) -> None: 41 | self._event_emitter.remove_all_listeners() 42 | 43 | def emit_change(self, old_value: StateValue, new_value: StateValue, timestamp: int): 44 | if self._event_name_0_args in self._event_emitter.event_names(): 45 | self._event_emitter.emit(self._event_name_0_args) 46 | if self._event_name_1_args in self._event_emitter.event_names(): 47 | self._event_emitter.emit(self._event_name_1_args, new_value) 48 | if self._event_name_2_args in self._event_emitter.event_names(): 49 | self._event_emitter.emit(self._event_name_2_args, new_value, timestamp) 50 | if self._event_name_3_args in self._event_emitter.event_names(): 51 | self._event_emitter.emit(self._event_name_3_args, old_value, new_value, timestamp) 52 | if self._event_name_4_args in self._event_emitter.event_names(): 53 | self._event_emitter.emit(self._event_name_4_args, self._name, old_value, new_value, timestamp) 54 | 55 | def _get_event_name_by_callable(self, callable_: StateChangeCallback) -> str: 56 | num_params: int = len(inspect.signature(callable_).parameters) 57 | match num_params: 58 | case 0: 59 | return self._event_name_0_args 60 | case 1: 61 | return self._event_name_1_args 62 | case 2: 63 | return self._event_name_2_args 64 | case 3: 65 | return self._event_name_3_args 66 | case 4: 67 | return self._event_name_4_args 68 | raise Exception(f'invalid arg count {callable_}') 69 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/ValueCompare.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.core.state.read_state.value_type import Accelerometer, Battery, TriggerFeedback, Gyroscope, \ 4 | JoyStick, \ 5 | Orientation, TouchFinger 6 | from dualsense_controller.core.state.typedef import CompareResult, Number 7 | from dualsense_controller.core.state.write_state.value_type import Lightbar, Microphone, PlayerLeds, TriggerEffect 8 | 9 | _HALF_255: Final[Number] = 127.5 10 | 11 | 12 | class ValueCompare: 13 | 14 | @staticmethod 15 | def compare_joystick( 16 | before: JoyStick | None, 17 | after: JoyStick, 18 | deadzone_raw: Number = 0, 19 | ) -> CompareResult: 20 | if before is None: 21 | return True, after 22 | if deadzone_raw > 0 and (((after.x - _HALF_255) ** 2) + ((after.y - _HALF_255) ** 2)) <= (deadzone_raw ** 2): 23 | after = JoyStick(_HALF_255, _HALF_255) 24 | 25 | changed: bool = after.x != before.x or after.y != before.y 26 | return changed, after 27 | 28 | @staticmethod 29 | def compare_trigger( 30 | before: int | None, 31 | after: int, 32 | deadzone_raw: Number = 0, 33 | ) -> CompareResult: 34 | if before is None: 35 | return True, after 36 | if deadzone_raw > 0 and after <= deadzone_raw: 37 | after = 0 38 | changed: bool = after != before 39 | return changed, after 40 | 41 | @staticmethod 42 | # TODO: refact that compare fcts 43 | def compare_gyroscope( 44 | before: Gyroscope, 45 | after: Gyroscope, 46 | threshold_raw: Number = 0 47 | ) -> CompareResult: 48 | if before is None: 49 | return True, after 50 | if threshold_raw > 0: 51 | if abs(after.x - before.x) < threshold_raw \ 52 | and abs(after.y - before.y) < threshold_raw \ 53 | and abs(after.z - before.z) < threshold_raw: 54 | after = Gyroscope(before.x, before.y, before.z) 55 | changed: bool = after.x != before.x or after.y != before.y or after.z != before.z 56 | return changed, after 57 | 58 | @staticmethod 59 | def compare_accelerometer( 60 | before: Accelerometer, 61 | after: Accelerometer, 62 | threshold_raw: Number = 0 63 | ) -> CompareResult: 64 | if before is None: 65 | return True, after 66 | if threshold_raw > 0: 67 | if abs(after.x - before.x) < threshold_raw \ 68 | and abs(after.y - before.y) < threshold_raw \ 69 | and abs(after.z - before.z) < threshold_raw: 70 | after = Gyroscope(before.x, before.y, before.z) 71 | changed: bool = after.x != before.x or after.y != before.y or after.z != before.z 72 | return changed, after 73 | 74 | @staticmethod 75 | def compare_touch_finger( 76 | before: TouchFinger, 77 | after: TouchFinger 78 | ) -> CompareResult: 79 | if before is None: 80 | return True, after 81 | changed: bool = ( 82 | after.active != before.active 83 | or after.x != before.x 84 | or after.y != before.y 85 | or after.id != before.id 86 | ) 87 | return changed, after 88 | 89 | @staticmethod 90 | def compare_battery( 91 | before: Battery, 92 | after: Battery 93 | ) -> CompareResult: 94 | if before is None: 95 | return True, after 96 | changed: bool = ( 97 | after.level_percentage != before.level_percentage 98 | or after.full != before.full 99 | or after.charging != before.charging 100 | ) 101 | return changed, after 102 | 103 | @staticmethod 104 | def compare_feedback( 105 | before: TriggerFeedback, 106 | after: TriggerFeedback 107 | ) -> CompareResult: 108 | if before is None: 109 | return True, after 110 | changed: bool = after.active != before.active or after.value != before.value 111 | return changed, after 112 | 113 | @staticmethod 114 | def compare_orientation( 115 | before: Orientation, 116 | after: Orientation, 117 | threshold_raw: Number = 0 118 | ) -> CompareResult: 119 | if before is None: 120 | return True, after 121 | if threshold_raw > 0: 122 | if abs(after.yaw - before.yaw) < threshold_raw \ 123 | and abs(after.pitch - before.pitch) < threshold_raw \ 124 | and abs(after.roll - before.roll) < threshold_raw: 125 | after = Orientation(before.yaw, before.pitch, before.roll) 126 | changed: bool = after.yaw != before.yaw or after.pitch != before.pitch or after.roll != before.roll 127 | return changed, after 128 | 129 | @staticmethod 130 | def compare_microphone( 131 | before: Microphone, 132 | after: Microphone, 133 | ) -> CompareResult: 134 | if before is None: 135 | return True, after 136 | changed: bool = after.mute != before.mute or after.led != before.led 137 | return changed, after 138 | 139 | @staticmethod 140 | def compare_lightbar( 141 | before: Lightbar, 142 | after: Lightbar, 143 | ) -> CompareResult: 144 | if before is None: 145 | return True, after 146 | changed: bool = ( 147 | after.is_on != before.is_on 148 | or after.red != before.red 149 | or after.green != before.green 150 | or after.blue != before.blue 151 | or after.pulse_options != before.pulse_options 152 | ) 153 | return changed, after 154 | 155 | @staticmethod 156 | def compare_player_leds( 157 | before: PlayerLeds, 158 | after: PlayerLeds, 159 | ) -> CompareResult: 160 | if before is None: 161 | return True, after 162 | changed: bool = (after.enable != before.enable or after.brightness != before.brightness) 163 | return changed, after 164 | 165 | @staticmethod 166 | def compare_trigger_effect( 167 | before: TriggerEffect, 168 | after: TriggerEffect, 169 | ) -> CompareResult: 170 | if before is None: 171 | return True, after 172 | changed: bool = ( 173 | after.mode != before.mode 174 | or after.param1 != before.param1 175 | or after.param2 != before.param2 176 | or after.param3 != before.param3 177 | or after.param4 != before.param4 178 | or after.param5 != before.param5 179 | or after.param6 != before.param6 180 | or after.param7 != before.param7 181 | ) 182 | return changed, after 183 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/state/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MixedStateName(str, Enum): 5 | LEFT_TRIGGER = "LEFT_TRIGGER" 6 | RIGHT_TRIGGER = "RIGHT_TRIGGER" 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/mapping/StateValueMapper.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Final 3 | 4 | from dualsense_controller.core.state.mapping.common import Float, FromTo, Integer, StateValueMappingData 5 | from dualsense_controller.core.state.mapping.enum import StateValueMapping 6 | from dualsense_controller.core.state.mapping.typedef import FromToTuple, MapFn 7 | from dualsense_controller.core.state.read_state.value_type import JoyStick 8 | from dualsense_controller.core.state.typedef import Number 9 | 10 | _NumberType = Float | Integer 11 | 12 | 13 | class StateValueMapper: 14 | 15 | @staticmethod 16 | def _number_map(value: float, in_min: float, in_max: float, out_min: float, out_max: float) -> float | None: 17 | if value is None: 18 | return None 19 | if in_min == out_min and in_max == out_max: 20 | return value 21 | return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min 22 | 23 | @classmethod 24 | def _number_raw_to_mapped(cls, from_to: FromTo | None, value: Number | None) -> Number: 25 | if value is None: 26 | return None 27 | if from_to is None: 28 | return value 29 | to_type: _NumberType = from_to.to_type 30 | value_type: Number = to_type.value_type 31 | from_to_tuple: FromToTuple = from_to.as_tuple 32 | mapped_value: Number = value_type(cls._number_map(value, *from_to_tuple)) 33 | if isinstance(mapped_value, float): 34 | mapped_value = round(mapped_value, to_type.round_digits) 35 | return mapped_value 36 | 37 | @classmethod 38 | def _number_mapped_to_raw(cls, from_to: FromTo, value: Number) -> Number: 39 | if from_to is None: 40 | return value 41 | raw_value: int = int(cls._number_map(value, *from_to.swapped.as_tuple)) 42 | return raw_value 43 | 44 | @classmethod 45 | def _joystick_mapped_to_raw(cls, from_to: FromTo, value: JoyStick) -> JoyStick: 46 | return JoyStick( 47 | x=cls._number_mapped_to_raw(from_to, value.x), 48 | y=cls._number_mapped_to_raw(from_to, value.y) 49 | ) 50 | 51 | @classmethod 52 | def _joystick_raw_to_mapped( 53 | cls, 54 | from_to_x: FromTo, 55 | from_to_y: FromTo, 56 | value: JoyStick, 57 | ) -> JoyStick: 58 | return JoyStick( 59 | x=cls._number_raw_to_mapped(from_to_x, value.x), 60 | y=cls._number_raw_to_mapped(from_to_y, value.y), 61 | ) 62 | 63 | @property 64 | def mapping_data(self) -> StateValueMappingData | None: 65 | return self._mapping_data 66 | 67 | def __init__( 68 | self, 69 | mapping: StateValueMapping, 70 | left_joystick_deadzone: Number = 0, 71 | right_joystick_deadzone: Number = 0, 72 | left_trigger_deadzone: Number = 0, 73 | right_trigger_deadzone: Number = 0, 74 | gyroscope_threshold: int = 0, 75 | accelerometer_threshold: int = 0, 76 | orientation_threshold: int = 0, 77 | ): 78 | 79 | self.left_stick_deadzone_mapped: Final[Number] = left_joystick_deadzone 80 | self.right_stick_deadzone_mapped: Final[Number] = right_joystick_deadzone 81 | self.left_trigger_deadzone_mapped: Final[Number] = left_trigger_deadzone 82 | self.right_trigger_deadzone_mapped: Final[Number] = right_trigger_deadzone 83 | self.gyroscope_threshold_mapped: Final[int] = gyroscope_threshold 84 | self.accelerometer_threshold_mapped: Final[int] = accelerometer_threshold 85 | self.orientation_threshold_mapped: Final[int] = orientation_threshold 86 | 87 | self._mapping_data: StateValueMappingData = mapping.value 88 | if isinstance(self._mapping_data, tuple): 89 | self._mapping_data = self._mapping_data[0] 90 | 91 | # #################################################### DEADZONE AND THRESHOLD ############################### 92 | self.left_stick_deadzone_mapped_to_raw: Number = ( 93 | left_joystick_deadzone if self._mapping_data is None else self._number_mapped_to_raw( 94 | self._mapping_data.left_stick_deadzone, 95 | left_joystick_deadzone 96 | ) 97 | ) 98 | self.right_stick_deadzone_mapped_to_raw: Number = ( 99 | right_joystick_deadzone if self._mapping_data is None else self._number_mapped_to_raw( 100 | self._mapping_data.right_stick_deadzone, 101 | right_joystick_deadzone 102 | ) 103 | ) 104 | self.left_trigger_deadzone_mapped_to_raw: Number = ( 105 | left_trigger_deadzone if self._mapping_data is None else self._number_mapped_to_raw( 106 | self._mapping_data.left_trigger_deadzone, 107 | left_trigger_deadzone 108 | ) 109 | ) 110 | self.right_trigger_deadzone_mapped_to_raw: Number = ( 111 | right_trigger_deadzone if self._mapping_data is None else self._number_mapped_to_raw( 112 | self._mapping_data.right_trigger_deadzone, 113 | right_trigger_deadzone 114 | ) 115 | ) 116 | self.gyroscope_threshold_mapped_to_raw: Number = ( 117 | gyroscope_threshold if self._mapping_data is None else self._number_mapped_to_raw( 118 | self._mapping_data.right_trigger_deadzone, 119 | gyroscope_threshold 120 | ) 121 | ) 122 | self.accelerometer_threshold_mapped_to_raw: Number = ( 123 | accelerometer_threshold if self._mapping_data is None else self._number_mapped_to_raw( 124 | self._mapping_data.right_trigger_deadzone, 125 | accelerometer_threshold 126 | ) 127 | ) 128 | self.orientation_threshold_mapped_to_raw: Number = ( 129 | orientation_threshold if self._mapping_data is None else self._number_mapped_to_raw( 130 | self._mapping_data.right_trigger_deadzone, 131 | orientation_threshold 132 | ) 133 | ) 134 | 135 | # #################################################### JOYSTICKS ############################### 136 | self.left_stick_x_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 137 | self._number_raw_to_mapped, 138 | self._mapping_data.left_stick_x, 139 | ) 140 | self.left_stick_x_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 141 | self._number_mapped_to_raw, 142 | self._mapping_data.left_stick_x 143 | ) 144 | self.left_stick_y_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 145 | self._number_raw_to_mapped, 146 | self._mapping_data.left_stick_y, 147 | ) 148 | self.left_stick_y_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 149 | self._number_mapped_to_raw, 150 | self._mapping_data.left_stick_y 151 | ) 152 | self.left_stick_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 153 | self._joystick_raw_to_mapped, 154 | self._mapping_data.left_stick_x, 155 | self._mapping_data.left_stick_y, 156 | ) 157 | self.left_stick_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 158 | self._joystick_mapped_to_raw, 159 | self._mapping_data.left_stick_x, 160 | self._mapping_data.left_stick_y, 161 | ) 162 | self.right_stick_x_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 163 | self._number_raw_to_mapped, 164 | self._mapping_data.right_stick_x, 165 | ) 166 | self.right_stick_x_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 167 | self._number_mapped_to_raw, 168 | self._mapping_data.right_stick_x 169 | ) 170 | self.right_stick_y_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 171 | self._number_raw_to_mapped, 172 | self._mapping_data.right_stick_y, 173 | ) 174 | self.right_stick_y_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 175 | self._number_mapped_to_raw, 176 | self._mapping_data.right_stick_y 177 | ) 178 | self.right_stick_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 179 | self._joystick_raw_to_mapped, 180 | self._mapping_data.right_stick_x, 181 | self._mapping_data.right_stick_y, 182 | ) 183 | self.right_stick_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 184 | self._joystick_mapped_to_raw, 185 | self._mapping_data.right_stick_x, 186 | self._mapping_data.right_stick_y, 187 | ) 188 | 189 | # #################################################### TRIGGERS ############################### 190 | self.left_trigger_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 191 | self._number_raw_to_mapped, 192 | self._mapping_data.left_trigger 193 | ) 194 | self.left_trigger_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 195 | self._number_mapped_to_raw, 196 | self._mapping_data.left_trigger 197 | ) 198 | self.right_trigger_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 199 | self._number_raw_to_mapped, 200 | self._mapping_data.right_trigger 201 | ) 202 | self.right_trigger_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 203 | self._number_mapped_to_raw, 204 | self._mapping_data.right_trigger 205 | ) 206 | 207 | # #################################################### MOTORS ############################### 208 | self.set_left_motor_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 209 | self._number_mapped_to_raw, 210 | self._mapping_data.set_motor_left 211 | ) 212 | self.set_left_motor_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 213 | self._number_raw_to_mapped, 214 | self._mapping_data.set_motor_left 215 | ) 216 | self.set_right_motor_mapped_to_raw: MapFn | None = None if self._mapping_data is None else partial( 217 | self._number_mapped_to_raw, 218 | self._mapping_data.set_motor_right 219 | ) 220 | self.set_right_motor_raw_to_mapped: MapFn | None = None if self._mapping_data is None else partial( 221 | self._number_raw_to_mapped, 222 | self._mapping_data.set_motor_right 223 | ) 224 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/mapping/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/state/mapping/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/mapping/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | from dualsense_controller.core.state.mapping.typedef import FromToTuple 6 | from dualsense_controller.core.state.typedef import Number 7 | 8 | 9 | @dataclass(frozen=True, slots=True) 10 | class Integer: 11 | value_type: type[Number] = int 12 | 13 | 14 | @dataclass(frozen=True, slots=True) 15 | class Float: 16 | value_type: type[Number] = float 17 | round_digits: int = 2 18 | 19 | 20 | @dataclass(frozen=True, slots=True) 21 | class FromTo: 22 | from_min: int 23 | from_max: int 24 | to_min: Number 25 | to_max: Number 26 | from_type: Integer | Float = Integer() 27 | to_type: Integer | Float = Integer() 28 | 29 | @property 30 | def swapped(self) -> FromTo: 31 | return FromTo(self.to_min, self.to_max, self.from_min, self.from_max, self.to_type, self.from_type) 32 | 33 | @property 34 | def as_tuple(self) -> FromToTuple: 35 | return self.from_min, self.from_max, self.to_min, self.to_max 36 | 37 | 38 | @dataclass(frozen=True, slots=True) 39 | class StateValueMappingData: 40 | left_stick_x: FromTo = None 41 | left_stick_y: FromTo = None 42 | left_stick_deadzone: FromTo = None 43 | 44 | right_stick_x: FromTo = None 45 | right_stick_y: FromTo = None 46 | right_stick_deadzone: FromTo = None 47 | 48 | left_trigger: FromTo = None 49 | left_trigger_deadzone: FromTo = None 50 | 51 | right_trigger: FromTo = None 52 | right_trigger_deadzone: FromTo = None 53 | 54 | set_motor_left: FromTo = None 55 | set_motor_right: FromTo = None 56 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/mapping/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from dualsense_controller.core.state.mapping.common import Float, FromTo, StateValueMappingData 4 | 5 | 6 | class StateValueMapping(Enum): 7 | # # no need to fill StateValueMapping.RAW, only for illustration 8 | # # stick y-axis: 0 ... 255, trigger: 0 ... 255 9 | # RAW = StateValueMappingData( 10 | # left_stick_x=FromTo(0, 255, 0, 255), 11 | # left_stick_y=FromTo(0, 255, 0, 255), 12 | # left_stick_deadzone=FromTo(0, 255, 0, 255), 13 | # right_stick_x=FromTo(0, 255, 0, 255), 14 | # right_stick_y=FromTo(0, 255, 0, 255), 15 | # right_stick_deadzone=FromTo(0, 255, 0, 255), 16 | # left_trigger=FromTo(0, 255, 0, 255), 17 | # left_trigger_deadzone=FromTo(0, 255, 0, 255), 18 | # right_trigger=FromTo(0, 255, 0, 255), 19 | # right_trigger_deadzone=FromTo(0, 255, 0, 255), 20 | # set_motor_left=FromTo(0, 255, 0, 255), 21 | # set_motor_right=FromTo(0, 255, 0, 255), 22 | # ), 23 | # # thats why 24 | RAW = None 25 | 26 | # stick y-axis: -100 ... 100, trigger: 0 ... 100 27 | HUNDRED = StateValueMappingData( 28 | left_stick_x=FromTo(0, 255, -100, 100), 29 | left_stick_y=FromTo(0, 255, 100, -100), 30 | left_stick_deadzone=FromTo(0, 255, 0, 100), 31 | right_stick_x=FromTo(0, 255, -100, 100), 32 | right_stick_y=FromTo(0, 255, 100, -100), 33 | right_stick_deadzone=FromTo(0, 255, 0, 100), 34 | left_trigger=FromTo(0, 255, 0, 100), 35 | left_trigger_deadzone=FromTo(0, 255, 0, 100), 36 | right_trigger=FromTo(0, 255, 0, 100), 37 | right_trigger_deadzone=FromTo(0, 255, 0, 100), 38 | set_motor_left=FromTo(0, 255, 0, 100), 39 | set_motor_right=FromTo(0, 255, 0, 100), 40 | ) 41 | 42 | # stick y-axis: 255 ... 0, trigger: 0 ... 255 43 | RAW_INVERTED = StateValueMappingData( 44 | left_stick_x=FromTo(0, 255, 0, 255), 45 | left_stick_y=FromTo(0, 255, 255, 0), 46 | right_stick_x=FromTo(0, 255, 0, 255), 47 | right_stick_y=FromTo(0, 255, 255, 0), 48 | # undefined maps handled like StateValueMapping.RAW 49 | ) 50 | 51 | DEFAULT = StateValueMappingData( 52 | left_stick_x=FromTo(0, 255, -128, 127), 53 | left_stick_y=FromTo(0, 255, 127, -128), 54 | right_stick_x=FromTo(0, 255, -128, 127), 55 | right_stick_y=FromTo(0, 255, 127, -128), 56 | # undefined maps handled like StateValueMapping.RAW 57 | ) 58 | 59 | DEFAULT_INVERTED = StateValueMappingData( 60 | left_stick_x=FromTo(0, 255, -128, 127), 61 | left_stick_y=FromTo(0, 255, -128, 127), 62 | right_stick_x=FromTo(0, 255, -128, 127), 63 | right_stick_y=FromTo(0, 255, -128, 127), 64 | # undefined maps handled like StateValueMapping.RAW 65 | ) 66 | 67 | NORMALIZED = StateValueMappingData( 68 | left_stick_x=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 69 | left_stick_y=FromTo(0, 255, 1.0, -1.0, to_type=Float()), 70 | left_stick_deadzone=FromTo(0, 127, 0, 1.0, to_type=Float()), 71 | right_stick_x=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 72 | right_stick_y=FromTo(0, 255, 1.0, -1.0, to_type=Float()), 73 | right_stick_deadzone=FromTo(0, 127, 0, 1.0, to_type=Float()), 74 | left_trigger=FromTo(0, 255, 0, 1.0, to_type=Float()), 75 | left_trigger_deadzone=FromTo(0, 255, 0, 1.0, to_type=Float()), 76 | right_trigger=FromTo(0, 255, 0, 1.0, to_type=Float()), 77 | right_trigger_deadzone=FromTo(0, 255, 0, 1.0, to_type=Float()), 78 | set_motor_left=FromTo(0, 255, 0, 1.0, to_type=Float()), 79 | set_motor_right=FromTo(0, 255, 0, 1.0, to_type=Float()), 80 | ) 81 | 82 | NORMALIZED_INVERTED = StateValueMappingData( 83 | left_stick_x=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 84 | left_stick_y=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 85 | left_stick_deadzone=FromTo(0, 127, 0, 1.0, to_type=Float()), 86 | right_stick_x=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 87 | right_stick_y=FromTo(0, 255, -1.0, 1.0, to_type=Float()), 88 | right_stick_deadzone=FromTo(0, 127, 0, 1.0, to_type=Float()), 89 | left_trigger=FromTo(0, 255, 0, 1.0, to_type=Float()), 90 | left_trigger_deadzone=FromTo(0, 255, 0, 1.0, to_type=Float()), 91 | right_trigger=FromTo(0, 255, 0, 1.0, to_type=Float()), 92 | right_trigger_deadzone=FromTo(0, 255, 0, 1.0, to_type=Float()), 93 | set_motor_left=FromTo(0, 255, 0, 1.0, to_type=Float()), 94 | set_motor_right=FromTo(0, 255, 0, 1.0, to_type=Float()), 95 | ) 96 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/mapping/typedef.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | 3 | from dualsense_controller.core.state.typedef import Number 4 | 5 | FromToTuple = tuple[Number, Number, Number, Number] 6 | MapFn = Callable[[Any], Any] 7 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/read_state/ReadState.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Final, Generic 4 | 5 | from dualsense_controller.core.core.Lockable import Lockable 6 | from dualsense_controller.core.report.in_report.InReport import InReport 7 | from dualsense_controller.core.state.State import State 8 | from dualsense_controller.core.state.mapping.typedef import MapFn 9 | from dualsense_controller.core.state.read_state.enum import ReadStateName 10 | from dualsense_controller.core.state.typedef import CompareFn, StateValue, StateValueFn 11 | 12 | 13 | class ReadState(Generic[StateValue], State[StateValue]): 14 | 15 | @property 16 | def has_changed_dependencies(self) -> bool: 17 | return any(state.has_changed_since_last_set_value for state in self._depends_on) 18 | 19 | @property 20 | def has_changed_dependents(self) -> bool: 21 | return any(state.has_changed_since_last_set_value for state in self._is_dependency_of) 22 | 23 | @property 24 | def has_changed_dependencies_or_dependents(self) -> bool: 25 | return ( 26 | any(state.has_changed_since_last_set_value for state in self._is_dependency_of) 27 | or any(state.has_changed_since_last_set_value for state in self._depends_on) 28 | ) 29 | 30 | @property 31 | def has_listened_dependencies_or_dependents(self) -> bool: 32 | return ( 33 | any(state.has_listeners for state in self._is_dependency_of) 34 | or any(state.has_listeners for state in self._depends_on) 35 | ) 36 | 37 | @property 38 | def has_listened_dependents(self) -> bool: 39 | return any(state.has_listeners for state in self._is_dependency_of) 40 | 41 | @property 42 | def has_listened_dependencies(self) -> bool: 43 | return any(state.has_listeners for state in self._depends_on) 44 | 45 | @property 46 | def value_raw(self) -> StateValue: 47 | if self.is_self_updatable: 48 | return self.calc_value() 49 | return super().value_raw 50 | 51 | @property 52 | def is_self_updatable(self) -> bool: 53 | return self._can_update_itself and (self._cycle_timestamp > self._change_timestamp) 54 | 55 | @property 56 | def is_updatable_from_outside(self) -> bool: 57 | return ( 58 | self._enforce_update 59 | or self.has_listeners 60 | or self.has_listened_dependents 61 | or self.has_changed_dependencies 62 | ) 63 | 64 | def __init__( 65 | self, 66 | # BASE 67 | name: ReadStateName, 68 | value: StateValue = None, 69 | default_value: StateValue = None, 70 | ignore_none: bool = True, 71 | mapped_to_raw_fn: MapFn = None, 72 | raw_to_mapped_fn: MapFn = None, 73 | compare_fn: CompareFn = None, 74 | 75 | # READ STATE 76 | value_calc_fn: StateValueFn = None, 77 | in_report_lockable: Lockable[InReport] = None, 78 | enforce_update: bool = False, 79 | can_update_itself: bool = True, 80 | depends_on: list[State[Any]] = None, 81 | is_dependency_of: list[State[Any]] = None, 82 | ): 83 | State.__init__( 84 | self, 85 | name=name, 86 | value=value, 87 | default_value=default_value, 88 | ignore_none=ignore_none, 89 | mapped_to_raw_fn=mapped_to_raw_fn, 90 | raw_to_mapped_fn=raw_to_mapped_fn, 91 | compare_fn=compare_fn, 92 | ) 93 | # CONST 94 | self._depends_on: Final[list[ReadState[StateValue]]] = depends_on if depends_on is not None else [] 95 | self._is_dependency_of: Final[list[ReadState[StateValue]]] = ( 96 | is_dependency_of if is_dependency_of is not None else [] 97 | ) 98 | self._enforce_update: Final[bool] = enforce_update 99 | self._value_calc_fn: Final[StateValueFn] = value_calc_fn 100 | self._in_report_lockable: Final[Lockable[InReport]] = in_report_lockable 101 | self._can_update_itself: Final[bool] = can_update_itself 102 | 103 | # VAR 104 | self._cycle_timestamp: int = 0 105 | 106 | # AFTER 107 | for depends_on_state in self._depends_on: 108 | depends_on_state.add_as_dependecy_of(self) 109 | for is_dependency_of_state in self._is_dependency_of: 110 | is_dependency_of_state.add_depends_on(self) 111 | 112 | def calc_value(self, trigger_change_on_changed: bool = True) -> StateValue: 113 | value_raw: StateValue = self._value_calc_fn( 114 | self._in_report_lockable.value, 115 | *self._depends_on 116 | ) 117 | self._set_value_raw(value_raw, trigger_change_on_changed) 118 | return self._value_raw 119 | 120 | def set_cycle_timestamp(self, timestamp: int): 121 | self._cycle_timestamp = timestamp 122 | 123 | def add_as_dependecy_of(self, state: ReadState[Any]): 124 | self._is_dependency_of.append(state) 125 | 126 | def add_depends_on(self, state: ReadState[Any]): 127 | self._depends_on.append(state) 128 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/read_state/ValueCompare.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | 3 | from dualsense_controller.core.state.read_state.value_type import Accelerometer, Battery, TriggerFeedback, Gyroscope, \ 4 | JoyStick, \ 5 | Orientation, TouchFinger, Trigger 6 | from dualsense_controller.core.state.typedef import CompareResult, Number 7 | 8 | _HALF_255: Final[Number] = 127.5 9 | 10 | 11 | class ValueCompare: 12 | 13 | @staticmethod 14 | def compare_joystick( 15 | before: JoyStick | None, 16 | after: JoyStick, 17 | deadzone_raw: Number = 0, 18 | ) -> CompareResult: 19 | 20 | if before is None: 21 | return True, after 22 | 23 | if deadzone_raw > 0 and (((before.x - _HALF_255) ** 2) + ((before.y - _HALF_255) ** 2)) <= (deadzone_raw ** 2): 24 | before = JoyStick(_HALF_255, _HALF_255) 25 | 26 | if deadzone_raw > 0 and (((after.x - _HALF_255) ** 2) + ((after.y - _HALF_255) ** 2)) <= (deadzone_raw ** 2): 27 | after = JoyStick(_HALF_255, _HALF_255) 28 | 29 | changed: bool = after.x != before.x or after.y != before.y 30 | return changed, after 31 | 32 | @staticmethod 33 | # TODO: refact that compare fcts 34 | def compare_gyroscope( 35 | before: Gyroscope, 36 | after: Gyroscope, 37 | threshold_raw: Number = 0 38 | ) -> CompareResult: 39 | if before is None: 40 | return True, after 41 | if threshold_raw > 0: 42 | if abs(after.x - before.x) < threshold_raw \ 43 | and abs(after.y - before.y) < threshold_raw \ 44 | and abs(after.z - before.z) < threshold_raw: 45 | after = Gyroscope(before.x, before.y, before.z) 46 | changed: bool = after.x != before.x or after.y != before.y or after.z != before.z 47 | return changed, after 48 | 49 | @staticmethod 50 | def compare_accelerometer( 51 | before: Accelerometer, 52 | after: Accelerometer, 53 | threshold_raw: Number = 0 54 | ) -> CompareResult: 55 | if before is None: 56 | return True, after 57 | if threshold_raw > 0: 58 | if abs(after.x - before.x) < threshold_raw \ 59 | and abs(after.y - before.y) < threshold_raw \ 60 | and abs(after.z - before.z) < threshold_raw: 61 | after = Gyroscope(before.x, before.y, before.z) 62 | changed: bool = after.x != before.x or after.y != before.y or after.z != before.z 63 | return changed, after 64 | 65 | @staticmethod 66 | def compare_touch_finger( 67 | before: TouchFinger, 68 | after: TouchFinger 69 | ) -> CompareResult: 70 | if before is None: 71 | return True, after 72 | changed: bool = after.active != before.active or after.x != before.x or after.y != before.y or after.id != before.id 73 | return changed, after 74 | 75 | @staticmethod 76 | def compare_battery( 77 | before: Battery, 78 | after: Battery 79 | ) -> CompareResult: 80 | if before is None: 81 | return True, after 82 | changed: bool = ( 83 | after.level_percentage != before.level_percentage 84 | or after.full != before.full 85 | or after.charging != before.charging 86 | ) 87 | return changed, after 88 | 89 | @staticmethod 90 | def compare_orientation( 91 | before: Orientation, 92 | after: Orientation, 93 | threshold_raw: Number = 0 94 | ) -> CompareResult: 95 | if before is None: 96 | return True, after 97 | if threshold_raw > 0: 98 | if abs(after.yaw - before.yaw) < threshold_raw \ 99 | and abs(after.pitch - before.pitch) < threshold_raw \ 100 | and abs(after.roll - before.roll) < threshold_raw: 101 | after = Orientation(before.yaw, before.pitch, before.roll) 102 | changed: bool = after.yaw != before.yaw or after.pitch != before.pitch or after.roll != before.roll 103 | return changed, after 104 | 105 | @staticmethod 106 | def compare_trigger_feedback( 107 | before: TriggerFeedback, 108 | after: TriggerFeedback 109 | ) -> CompareResult: 110 | if before is None: 111 | return True, after 112 | changed: bool = after.active != before.active or after.value != before.value 113 | return changed, after 114 | 115 | @staticmethod 116 | def compare_trigger_value( 117 | before: int | None, 118 | after: int, 119 | deadzone_raw: Number = 0, 120 | ) -> CompareResult: 121 | if before is None: 122 | return True, after 123 | if deadzone_raw > 0 and before <= deadzone_raw: 124 | before = 0 125 | if deadzone_raw > 0 and after <= deadzone_raw: 126 | after = 0 127 | changed: bool = after != before 128 | return changed, after 129 | 130 | # @staticmethod 131 | # def compare_trigger( 132 | # before: Trigger, 133 | # after: Trigger, 134 | # deadzone_raw: Number = 0, 135 | # ) -> CompareResult: 136 | # if before is None: 137 | # return True, after 138 | # value_changed, _ = ValueCompare.compare_trigger_value(before.value, after.value, deadzone_raw) 139 | # feedback_changed, _ = ValueCompare.compare_trigger_feedback(before.feedback, after.feedback) 140 | # changed: bool = value_changed or feedback_changed 141 | # return changed, after 142 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/read_state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/state/read_state/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/read_state/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ReadStateName(str, Enum): 5 | DPAD = 'DPAD' 6 | 7 | BTN_UP = 'BTN_UP' 8 | BTN_LEFT = 'BTN_LEFT' 9 | BTN_DOWN = 'BTN_DOWN' 10 | BTN_RIGHT = 'BTN_RIGHT' 11 | 12 | BTN_SQUARE = "BTN_SQUARE" 13 | BTN_CROSS = "BTN_CROSS" 14 | BTN_CIRCLE = "BTN_CIRCLE" 15 | BTN_TRIANGLE = "BTN_TRIANGLE" 16 | BTN_L1 = "BTN_L1" 17 | BTN_R1 = "BTN_R1" 18 | BTN_L2 = "BTN_L2" 19 | BTN_R2 = "BTN_R2" 20 | BTN_CREATE = "BTN_CREATE" 21 | BTN_OPTIONS = "BTN_OPTIONS" 22 | BTN_L3 = "BTN_L3" 23 | BTN_R3 = "BTN_R3" 24 | BTN_PS = "BTN_PS" 25 | BTN_TOUCHPAD = "BTN_TOUCHPAD" 26 | BTN_MUTE = "BTN_MUTE" 27 | 28 | LEFT_STICK = 'LEFT_STICK' 29 | LEFT_STICK_X = 'LEFT_STICK_X' 30 | LEFT_STICK_Y = 'LEFT_STICK_Y' 31 | 32 | RIGHT_STICK = 'RIGHT_STICK' 33 | RIGHT_STICK_X = 'RIGHT_STICK_X' 34 | RIGHT_STICK_Y = 'RIGHT_STICK_Y' 35 | 36 | GYROSCOPE = 'GYROSCOPE' 37 | GYROSCOPE_X = "GYROSCOPE_X" 38 | GYROSCOPE_Y = "GYROSCOPE_Y" 39 | GYROSCOPE_Z = "GYROSCOPE_Z" 40 | 41 | ACCELEROMETER = 'ACCELEROMETER' 42 | ACCELEROMETER_X = "ACCELEROMETER_X" 43 | ACCELEROMETER_Y = "ACCELEROMETER_Y" 44 | ACCELEROMETER_Z = "ACCELEROMETER_Z" 45 | 46 | ORIENTATION = 'ORIENTATION' 47 | 48 | TOUCH_FINGER_1 = 'TOUCH_FINGER_1' 49 | TOUCH_FINGER_1_ACTIVE = 'TOUCH_FINGER_1_ACTIVE' 50 | TOUCH_FINGER_1_ID = 'TOUCH_FINGER_1_ID' 51 | TOUCH_FINGER_1_X = 'TOUCH_FINGER_1_X' 52 | TOUCH_FINGER_1_Y = 'TOUCH_FINGER_1_Y' 53 | TOUCH_FINGER_2 = 'TOUCH_FINGER_2' 54 | TOUCH_FINGER_2_ACTIVE = 'TOUCH_FINGER_2_ACTIVE' 55 | TOUCH_FINGER_2_ID = 'TOUCH_FINGER_2_ID' 56 | TOUCH_FINGER_2_X = 'TOUCH_FINGER_2_X' 57 | TOUCH_FINGER_2_Y = 'TOUCH_FINGER_2_Y' 58 | 59 | LEFT_TRIGGER = "LEFT_TRIGGER" 60 | LEFT_TRIGGER_VALUE = 'LEFT_TRIGGER_VALUE' 61 | LEFT_TRIGGER_FEEDBACK = "LEFT_TRIGGER_FEEDBACK" 62 | LEFT_TRIGGER_FEEDBACK_ACTIVE = 'LEFT_TRIGGER_FEEDBACK_ACTIVE' 63 | LEFT_TRIGGER_FEEDBACK_VALUE = 'LEFT_TRIGGER_FEEDBACK_VALUE' 64 | 65 | RIGHT_TRIGGER = "RIGHT_TRIGGER" 66 | RIGHT_TRIGGER_VALUE = 'RIGHT_TRIGGER_VALUE' 67 | RIGHT_TRIGGER_FEEDBACK = "RIGHT_TRIGGER_FEEDBACK" 68 | RIGHT_TRIGGER_FEEDBACK_ACTIVE = 'RIGHT_TRIGGER_FEEDBACK_ACTIVE' 69 | RIGHT_TRIGGER_FEEDBACK_VALUE = 'RIGHT_TRIGGER_FEEDBACK_VALUE' 70 | 71 | BATTERY = "BATTERY" 72 | BATTERY_LEVEL_PERCENT = 'BATTERY_LEVEL_PERCENT' 73 | BATTERY_FULL = 'BATTERY_FULL' 74 | BATTERY_CHARGING = 'BATTERY_CHARGING' 75 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/read_state/value_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Final 5 | 6 | from dualsense_controller.core.enum import ConnectionType 7 | from dualsense_controller.core.state.typedef import Number 8 | 9 | _DEFAULT_NUMBER: Final[Number] = -99999 10 | 11 | 12 | @dataclass(frozen=True, slots=True) 13 | class Connection: 14 | connected: bool = False 15 | connection_type: ConnectionType = None 16 | 17 | 18 | @dataclass(frozen=True, slots=True) 19 | class JoyStick: 20 | x: Number = _DEFAULT_NUMBER 21 | y: Number = _DEFAULT_NUMBER 22 | 23 | 24 | @dataclass(frozen=True, slots=True) 25 | class Gyroscope: 26 | x: int = _DEFAULT_NUMBER 27 | y: int = _DEFAULT_NUMBER 28 | z: int = _DEFAULT_NUMBER 29 | 30 | 31 | @dataclass(frozen=True, slots=True) 32 | class Accelerometer: 33 | x: int = _DEFAULT_NUMBER 34 | y: int = _DEFAULT_NUMBER 35 | z: int = _DEFAULT_NUMBER 36 | 37 | 38 | @dataclass(frozen=True, slots=True) 39 | class TouchFinger: 40 | active: bool = False 41 | id: int = _DEFAULT_NUMBER 42 | x: int = _DEFAULT_NUMBER 43 | y: int = _DEFAULT_NUMBER 44 | 45 | 46 | @dataclass(frozen=True, slots=True) 47 | class Battery: 48 | level_percentage: float = _DEFAULT_NUMBER 49 | full: bool = False 50 | charging: bool = False 51 | 52 | 53 | @dataclass(frozen=True, slots=True) 54 | class TriggerFeedback: 55 | active: bool = False 56 | value: int = _DEFAULT_NUMBER 57 | 58 | 59 | @dataclass(frozen=True, slots=True) 60 | class Trigger: 61 | value: Number = _DEFAULT_NUMBER 62 | feedback: TriggerFeedback = TriggerFeedback() 63 | 64 | 65 | @dataclass(frozen=True, slots=True) 66 | class Orientation: 67 | pitch: float = _DEFAULT_NUMBER 68 | roll: float = _DEFAULT_NUMBER 69 | yaw: float | None = None 70 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/typedef.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Callable, TypeVar 4 | 5 | from dualsense_controller.core.report.in_report import InReport 6 | from dualsense_controller.core.state.enum import MixedStateName 7 | from dualsense_controller.core.state.read_state.enum import ReadStateName 8 | from dualsense_controller.core.state.write_state.enum import WriteStateName 9 | 10 | StateValue = TypeVar('StateValue') 11 | MappedStateValue = TypeVar('MappedStateValue') 12 | StateName = ReadStateName | WriteStateName | MixedStateName | str 13 | 14 | _StChCb0 = Callable[[], None] 15 | _StChCb1 = Callable[[Any], None] 16 | _StChCb2 = Callable[[Any, int | None], None] 17 | _StChCb3 = Callable[[Any, Any, int | None], None] 18 | _StChCb4 = Callable[[StateName, Any, Any, int | None], None] 19 | StateChangeCallback = _StChCb0 | _StChCb1 | _StChCb2 | _StChCb3 | _StChCb4 20 | 21 | Number = int | float 22 | CompareResult = tuple[bool, StateValue] 23 | _WrappedCompareFn = Callable[[StateValue, StateValue, ...], CompareResult] 24 | _CompareFn = Callable[[StateValue, StateValue], CompareResult] 25 | CompareFn = _WrappedCompareFn | _CompareFn 26 | StateValueFn = Callable[[InReport, ...], StateValue] 27 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/write_state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/dualsense_controller/core/state/write_state/__init__.py -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/write_state/enum.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | 5 | 6 | class WriteStateName(str, Enum): 7 | FLAGS_CONTROLS = 'FLAGS_CONTROLS' 8 | FLAGS_PHYSICS = 'FLAGS_PHYSICS' 9 | 10 | MOTOR_LEFT = 'MOTOR_LEFT' 11 | MOTOR_RIGHT = 'MOTOR_RIGHT' 12 | 13 | LEFT_TRIGGER_EFFECT = 'LEFT_TRIGGER_EFFECT' 14 | LEFT_TRIGGER_EFFECT_MODE = 'LEFT_TRIGGER_EFFECT_MODE' 15 | LEFT_TRIGGER_EFFECT_PARAM1 = 'LEFT_TRIGGER_EFFECT_PARAM1' 16 | LEFT_TRIGGER_EFFECT_PARAM2 = 'LEFT_TRIGGER_EFFECT_PARAM2' 17 | LEFT_TRIGGER_EFFECT_PARAM3 = 'LEFT_TRIGGER_EFFECT_PARAM3' 18 | LEFT_TRIGGER_EFFECT_PARAM4 = 'LEFT_TRIGGER_EFFECT_PARAM4' 19 | LEFT_TRIGGER_EFFECT_PARAM5 = 'LEFT_TRIGGER_EFFECT_PARAM5' 20 | LEFT_TRIGGER_EFFECT_PARAM6 = 'LEFT_TRIGGER_EFFECT_PARAM6' 21 | LEFT_TRIGGER_EFFECT_PARAM7 = 'LEFT_TRIGGER_EFFECT_PARAM7' 22 | 23 | RIGHT_TRIGGER_EFFECT = 'RIGHT_TRIGGER_EFFECT' 24 | RIGHT_TRIGGER_EFFECT_MODE = 'RIGHT_TRIGGER_EFFECT_MODE' 25 | RIGHT_TRIGGER_EFFECT_PARAM1 = 'RIGHT_TRIGGER_EFFECT_PARAM1' 26 | RIGHT_TRIGGER_EFFECT_PARAM2 = 'RIGHT_TRIGGER_EFFECT_PARAM2' 27 | RIGHT_TRIGGER_EFFECT_PARAM3 = 'RIGHT_TRIGGER_EFFECT_PARAM3' 28 | RIGHT_TRIGGER_EFFECT_PARAM4 = 'RIGHT_TRIGGER_EFFECT_PARAM4' 29 | RIGHT_TRIGGER_EFFECT_PARAM5 = 'RIGHT_TRIGGER_EFFECT_PARAM5' 30 | RIGHT_TRIGGER_EFFECT_PARAM6 = 'RIGHT_TRIGGER_EFFECT_PARAM6' 31 | RIGHT_TRIGGER_EFFECT_PARAM7 = 'RIGHT_TRIGGER_EFFECT_PARAM7' 32 | 33 | LIGHTBAR = 'LIGHTBAR' 34 | LIGHTBAR_RED = 'LIGHTBAR_RED' 35 | LIGHTBAR_GREEN = 'LIGHTBAR_GREEN' 36 | LIGHTBAR_BLUE = 'LIGHTBAR_BLUE' 37 | LIGHTBAR_ON_OFF = 'LIGHTBAR_ON_OFF' 38 | LIGHTBAR_PULSE_OPTIONS = 'LIGHTBAR_PULSE_OPTIONS' 39 | 40 | LED_OPTIONS = 'LED_OPTIONS' 41 | 42 | PLAYER_LEDS = "PLAYER_LEDS" 43 | PLAYER_LEDS_BRIGHTNESS = 'PLAYER_LEDS_BRIGHTNESS' 44 | PLAYER_LEDS_ENABLE = 'PLAYER_LEDS_ENABLE' 45 | 46 | MICROPHONE = "MICROPHONE" 47 | MICROPHONE_LED = 'MICROPHONE_LED' 48 | MICROPHONE_MUTE = 'MICROPHONE_MUTE' 49 | 50 | 51 | # 52 | # Not clear 53 | 54 | # #### pydualsense says: 55 | # flags determing what changes this packet will perform 56 | # 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. 57 | # 0x02 set the main motors (also requires flag 0x01; without bit 0x01 motors are allowed to time out without re-enabling audio haptics) 58 | # 0x04 set the right trigger motor 59 | # 0x08 set the left trigger motor 60 | # 0x10 modification of audio volume 61 | # 0x20 toggling of internal speaker while headset is connected 62 | # 0x40 modification of microphone volume 63 | # #### ds5ctl says: 64 | 65 | 66 | class OperatingMode(int, Enum): 67 | DS4_COMPATIBILITY_MODE = 1 << 0 68 | DS5_MODE = 1 << 1 69 | 70 | 71 | class FlagsPhysics(int, Enum): 72 | ENABLE_HAPTICS = 1 << 0 | 1 << 1 73 | TRIGGER_EFFECTS_RIGHT = 1 << 2 74 | TRIGGER_EFFECTS_LEFT = 1 << 3 75 | ALL = 0xff 76 | 77 | 78 | class FlagsControls(int, Enum): 79 | MIC_MUTE_LED_CONTROL_ENABLE = 1 << 0 80 | POWER_SAVE_CONTROL_ENABLE = 1 << 1 81 | LIGHTBAR_CONTROL_ENABLE = 1 << 2 82 | RELEASE_LEDS = 1 << 3 83 | PLAYER_INDICATOR_CONTROL_ENABLE = 1 << 4 84 | UNKNOWN_FLAG_5 = 1 << 5 85 | OVERALL_EFFECT_POWER = 1 << 6 86 | UNKNOWN_FLAG_7 = 1 << 7 87 | ALL = ( 88 | # RELEASE_LEDS | 89 | MIC_MUTE_LED_CONTROL_ENABLE | 90 | POWER_SAVE_CONTROL_ENABLE | 91 | LIGHTBAR_CONTROL_ENABLE | 92 | PLAYER_INDICATOR_CONTROL_ENABLE | 93 | OVERALL_EFFECT_POWER 94 | ) 95 | ALL_BUT_MUTE_LED = ( 96 | # RELEASE_LEDS | 97 | # MIC_MUTE_LED_CONTROL_ENABLE | 98 | POWER_SAVE_CONTROL_ENABLE | 99 | LIGHTBAR_CONTROL_ENABLE | 100 | PLAYER_INDICATOR_CONTROL_ENABLE | 101 | OVERALL_EFFECT_POWER 102 | ) 103 | ALL_FORCE = 0xff 104 | 105 | 106 | class PlayerLedsEnable(int, Enum): 107 | OFF = 0 108 | # Enables the single, center LED 109 | CENTER = 0b00100 110 | # Enables the two LEDs adjacent to and directly surrounding the CENTER LED 111 | INNER = 0b01010 112 | # Enables the two outermost LEDs surrounding the INNER LEDs 113 | OUTER = 0b10001 114 | ALL = CENTER | INNER | OUTER 115 | 116 | 117 | class PlayerLedsBrightness(int, Enum): 118 | HIGH = 0 119 | MEDIUM = 0x01 120 | LOW = 0x02 121 | 122 | 123 | class LightbarMode(int, Enum): 124 | LIGHT_ON = 1 << 0 125 | LIGHT_OFF = 1 << 1 126 | 127 | 128 | class LightbarPulseOptions(int, Enum): 129 | OFF = 0 130 | FADE_IN_BLUE = 1 << 0 131 | FADE_OUT_BLUE = 1 << 1 132 | 133 | 134 | class LedOptions(int, Enum): 135 | OFF = 0 136 | PLAYER_LED_BRIGHTNESS = 1 << 0 137 | UNINTERRUMPABLE_LED = 1 << 1 138 | ALL = PLAYER_LED_BRIGHTNESS | UNINTERRUMPABLE_LED 139 | 140 | 141 | class TriggerEffectMode(int, Enum): 142 | # Offically recognized modes 143 | # These are 100% safe and are the only effects that modify the trigger status nybble 144 | OFF = 0x05, # 00 00 0 101 145 | FEEDBACK = 0x21, # 00 10 0 001 146 | WEAPON = 0x25, # 00 10 0 101 147 | VIBRATION = 0x26, # 00 10 0 110 148 | 149 | # Unofficial but unique effects left in the firmware 150 | # These might be removed in the future 151 | BOW = 0x22, # 00 10 0 010 152 | GALLOPING = 0x23, # 00 10 0 011 153 | MACHINE = 0x27, # 00 10 0 111 154 | 155 | # Leftover versions of offical modes with simpler logic and no paramater protections 156 | # These should not be used 157 | SIMPLE_FEEDBACK = 0x01, # 00 00 0 001 158 | SIMPLE_WEAPON = 0x02, # 00 00 0 010 159 | SIMPLE_VIBRATION = 0x06, # 00 00 0 110 160 | 161 | # Leftover versions of offical modes with limited paramater ranges 162 | # These should not be used 163 | LIMITED_FEEDBACK = 0x11, # 00 01 0 001 164 | LIMITED_WEAPON = 0x12, # 00 01 0 010 165 | 166 | # Debug or Calibration functions 167 | # Don't use these as they will courrupt the trigger state until the reset button is pressed 168 | DEBUG_FC = 0xFC, # 11 11 1 100 169 | DEBUG_FD = 0xFD, # 11 11 1 101 170 | DEBUG_FE = 0xFE, # 11 11 1 110 171 | 172 | # MY OLD - DONT USE 173 | # TODO: REPLACE 174 | NO_RESISTANCE = 0x00 175 | CONTINUOUS_RESISTANCE = SIMPLE_FEEDBACK 176 | SECTION_RESISTANCE = SIMPLE_WEAPON 177 | VIBRATING = SIMPLE_VIBRATION 178 | EFFECT_EXTENDED = VIBRATION 179 | CALIBRATE = DEBUG_FC 180 | SEMI_AUTOMATIC_GUN = WEAPON 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/state/write_state/value_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | 6 | from dualsense_controller.core.state.typedef import Number 7 | from dualsense_controller.core.state.write_state.enum import LightbarPulseOptions, PlayerLedsEnable, \ 8 | PlayerLedsBrightness, TriggerEffectMode 9 | 10 | 11 | @dataclass(frozen=True, slots=True) 12 | class Microphone: 13 | mute: bool = True 14 | led: bool = True 15 | 16 | 17 | @dataclass(frozen=True, slots=True) 18 | class Lightbar: 19 | red: int = 0 20 | green: int = 0 21 | blue: int = 0 22 | is_on: bool = False 23 | pulse_options: int = LightbarPulseOptions.FADE_OUT_BLUE 24 | 25 | 26 | @dataclass(frozen=True, slots=True) 27 | class PlayerLeds: 28 | enable: PlayerLedsEnable = PlayerLedsEnable.OFF 29 | brightness: PlayerLedsBrightness = PlayerLedsBrightness.MEDIUM 30 | 31 | 32 | @dataclass(frozen=True, slots=True) 33 | class TriggerEffect: 34 | mode: int | TriggerEffectMode = TriggerEffectMode.OFF 35 | param1: int = 0x00 36 | param2: int = 0x00 37 | param3: int = 0x00 38 | param4: int = 0x00 39 | param5: int = 0x00 40 | param6: int = 0x00 41 | param7: int = 0x00 42 | param8: int = 0x00 43 | param9: int = 0x00 44 | param10: int = 0x00 45 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/typedef.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | 3 | from dualsense_controller.core.Benchmarker import Benchmark 4 | 5 | ExceptionCallback = Callable[[Exception], None] 6 | UpdateBenchmarkCallback = Callable[[Benchmark], None] 7 | EmptyCallback = Callable[[], None] 8 | BatteryLowCallback = Callable[[float], None] 9 | LockableValue = TypeVar('LockableValue') 10 | -------------------------------------------------------------------------------- /src/dualsense_controller/core/util.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import statistics 3 | import traceback 4 | import warnings 5 | from types import FrameType 6 | from typing import Any 7 | 8 | 9 | def flag(bit: int) -> int: 10 | return 1 << bit 11 | 12 | 13 | def format_exception(exception: Exception) -> str: 14 | traceback_list = traceback.format_exception(type(exception), exception, exception.__traceback__) 15 | formatted_traceback = ''.join(traceback_list) 16 | return formatted_traceback 17 | 18 | 19 | def get_referencing_class() -> Any: 20 | current_frame: FrameType | None = inspect.currentframe() 21 | calling_frame: FrameType | None = current_frame.f_back 22 | return calling_frame.f_locals.get("self") 23 | 24 | 25 | _Num = int | float 26 | 27 | 28 | def _abs_list(values: list[_Num]) -> list[_Num]: 29 | return [abs(v) for v in values] 30 | 31 | 32 | def _min_val(mapped_min_max_values: list[_Num]) -> _Num: 33 | return statistics.mean(_abs_list(mapped_min_max_values)) 34 | 35 | 36 | def _max_val(mapped_min_max_values: list[_Num]) -> _Num: 37 | return max(_abs_list(mapped_min_max_values)) 38 | 39 | 40 | def check_value_restrictions( 41 | name: str, 42 | mapped_min_max_values: list[_Num] = None, 43 | middle_deadzone: _Num = None, 44 | deadzone: _Num = None, 45 | threshold: _Num = None, 46 | ) -> None: 47 | if mapped_min_max_values is None: 48 | return 49 | if deadzone is not None or middle_deadzone is not None or threshold is not None: 50 | mapped_min_max_values = [v for v in mapped_min_max_values if v is not None] 51 | 52 | if len(mapped_min_max_values) >= 1 and deadzone is not None: 53 | if deadzone < 0: 54 | raise ValueError('Deadzone value must not be negative') 55 | max_map_val: _Num = _max_val(mapped_min_max_values) 56 | if deadzone >= max_map_val: 57 | msg: str = ( 58 | "\nWarning:\n" 59 | f"Deadzone value for \"{name.split('.')[-1].lower()}\" is very big related to chosen mapping.\n" 60 | "Maybe changes are not reconized properly.\n" 61 | f"Deadzone for value should be lower than {max_map_val}. actual value is: {deadzone}" 62 | ) 63 | warnings.warn(msg, UserWarning) 64 | 65 | if len(mapped_min_max_values) >= 1 and middle_deadzone is not None: 66 | if middle_deadzone < 0: 67 | raise ValueError('Deadzone value must not be negative') 68 | min_map_val: _Num = _min_val(mapped_min_max_values) 69 | if middle_deadzone >= min_map_val: 70 | msg: str = ( 71 | "\nWarning:\n" 72 | f"Deadzone value for \"{name.split('.')[-1].lower()}\" is very big related to chosen mapping.\n" 73 | "Maybe changes are not reconized properly.\n" 74 | f"Deadzone for value should be lower than {min_map_val}. actual value is: {middle_deadzone}" 75 | ) 76 | warnings.warn(msg, UserWarning) 77 | 78 | if len(mapped_min_max_values) >= 1 and threshold is not None: 79 | if threshold < 0: 80 | raise ValueError('Threshold value must not be negative') 81 | min_map_val: _Num = _min_val(mapped_min_max_values) 82 | if threshold >= min_map_val: 83 | msg: str = ( 84 | "\nWarning:\n" 85 | f"Threshold value for \"{name.split('.')[-1].lower()}\" is very big related to chosen mapping.\n" 86 | "Maybe changes are not reconized.\n" 87 | f"Threshold for value should be lower than {min_map_val}. actual value is: {threshold}" 88 | ) 89 | warnings.warn(msg, UserWarning) 90 | -------------------------------------------------------------------------------- /src/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/src/examples/__init__.py -------------------------------------------------------------------------------- /src/examples/contextmanager_usage_example.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from dualsense_controller import DualSenseController, UpdateLevel, active_dualsense_controller, Mapping 4 | 5 | 6 | class ContextManagerUsageExample: 7 | 8 | def run(self) -> None: 9 | controller: DualSenseController 10 | with active_dualsense_controller( 11 | left_joystick_deadzone=0.2, 12 | right_joystick_deadzone=0.2, 13 | left_trigger_deadzone=0.05, 14 | right_trigger_deadzone=0.05, 15 | gyroscope_threshold=0, 16 | accelerometer_threshold=0, 17 | orientation_threshold=0, 18 | mapping=Mapping.NORMALIZED, 19 | update_level=UpdateLevel.DEFAULT, 20 | microphone_initially_muted=True, 21 | microphone_invert_led=False, 22 | ) as controller: 23 | for i in range(0, 10): 24 | controller.wait_until_updated() 25 | print(controller.left_stick.value) 26 | time.sleep(1) 27 | 28 | 29 | # ############################################# RUN EXAMPLE ################################################## 30 | 31 | def main(): 32 | ContextManagerUsageExample().run() 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /src/examples/example_simple.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from dualsense_controller import DualSenseController 4 | 5 | # list availabe devices and throw exception when tzhere is no device detected 6 | device_infos = DualSenseController.enumerate_devices() 7 | if len(device_infos) < 1: 8 | raise Exception('No DualSense Controller available.') 9 | 10 | # flag, which keeps program alive 11 | is_running = True 12 | 13 | # create an instance, use fiŕst available device 14 | controller = DualSenseController() 15 | 16 | 17 | # switches the keep alive flag, which stops the below loop 18 | def stop(): 19 | global is_running 20 | is_running = False 21 | 22 | 23 | # callback, when cross button is pressed, which enables rumble 24 | def on_cross_btn_pressed(): 25 | print('cross button pressed') 26 | controller.left_rumble.set(255) 27 | controller.right_rumble.set(255) 28 | 29 | 30 | # callback, when cross button is released, which disables rumble 31 | def on_cross_btn_released(): 32 | print('cross button released') 33 | controller.left_rumble.set(0) 34 | controller.right_rumble.set(0) 35 | 36 | 37 | # callback, when left button is pressed, set led bar color to red 38 | def on_left_btn_pressed(): 39 | print('left button pressed') 40 | controller.lightbar.set_color_red() 41 | 42 | 43 | # callback, when PlayStation button is pressed 44 | # stop program 45 | def on_ps_btn_pressed(): 46 | print('PS button released -> stop') 47 | stop() 48 | 49 | 50 | # callback, when unintended error occurs, 51 | # i.e. physically disconnecting the controller during operation 52 | # stop program 53 | def on_error(error): 54 | print(f'Opps! an error occured: {error}') 55 | stop() 56 | 57 | 58 | # register the button callbacks 59 | controller.btn_cross.on_down(on_cross_btn_pressed) 60 | controller.btn_cross.on_up(on_cross_btn_released) 61 | controller.btn_left.on_down(on_left_btn_pressed) 62 | controller.btn_ps.on_down(on_ps_btn_pressed) 63 | 64 | # register the error callback 65 | controller.on_error(on_error) 66 | 67 | # enable/connect the device 68 | controller.activate() 69 | 70 | # start keep alive loop, controller inputs and callbacks are handled in a second thread 71 | while is_running: 72 | sleep(1) 73 | 74 | # disable/disconnect controller device 75 | controller.deactivate() 76 | -------------------------------------------------------------------------------- /src/examples/example_trigger.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from dualsense_controller import DualSenseController, TriggerProperty 4 | 5 | 6 | class ExampleTrigger: 7 | 8 | def __init__(self): 9 | self.controller: DualSenseController = DualSenseController( 10 | device_index_or_device_info=0, 11 | ) 12 | 13 | self.trigger_effects = [ 14 | lambda trigger: ( 15 | print(f"Effect: no_resistance"), 16 | trigger.effect.no_resistance() 17 | ), 18 | lambda trigger: ( 19 | print(f"Effect: off"), 20 | trigger.effect.off() 21 | ), 22 | lambda trigger: ( 23 | print(f"Effect: continuous_resistance"), 24 | trigger.effect.continuous_resistance() 25 | ), 26 | lambda trigger: ( 27 | print(f"Effect: feedback (full)"), 28 | trigger.effect.feedback() 29 | ), 30 | lambda trigger: ( 31 | print(f"Effect: feedback (half)"), 32 | trigger.effect.feedback(strength=3) 33 | ), 34 | lambda trigger: ( 35 | print(f"Effect: feedback (half,middle)"), 36 | trigger.effect.feedback(start_position=4, strength=3) 37 | ), 38 | lambda trigger: ( 39 | print(f"Effect: section_resistance"), 40 | trigger.effect.section_resistance() 41 | ), 42 | lambda trigger: ( 43 | print(f"Effect: weapon"), 44 | trigger.effect.weapon() 45 | ), 46 | lambda trigger: ( 47 | print(f"Effect: multiple_position_feedback"), 48 | trigger.effect.multiple_position_feedback() 49 | ), 50 | lambda trigger: ( 51 | print(f"Effect: slope_feedback"), 52 | trigger.effect.slope_feedback() 53 | ), 54 | lambda trigger: ( 55 | print(f"Effect: bow"), 56 | trigger.effect.bow() 57 | ), 58 | lambda trigger: ( 59 | print(f"Effect: galloping"), 60 | trigger.effect.galloping() 61 | ), 62 | lambda trigger: ( 63 | print(f"Effect: machine"), 64 | trigger.effect.machine() 65 | ), 66 | lambda trigger: ( 67 | print(f"Effect: simple_vibration"), 68 | trigger.effect.simple_vibration() 69 | ), 70 | lambda trigger: ( 71 | print(f"Effect: full_press"), 72 | trigger.effect.full_press() 73 | ), 74 | lambda trigger: ( 75 | print(f"Effect: soft_press"), 76 | trigger.effect.soft_press() 77 | ), 78 | lambda trigger: ( 79 | print(f"Effect: medium_press"), 80 | trigger.effect.medium_press() 81 | ), 82 | lambda trigger: ( 83 | print(f"Effect: hard_press"), 84 | trigger.effect.hard_press() 85 | ), 86 | lambda trigger: ( 87 | print(f"Effect: pulse"), 88 | trigger.effect.pulse() 89 | ), 90 | lambda trigger: ( 91 | print(f"Effect: choppy"), 92 | trigger.effect.choppy() 93 | ), 94 | lambda trigger: ( 95 | print(f"Effect: soft_rigidity"), 96 | trigger.effect.soft_rigidity() 97 | ), 98 | lambda trigger: ( 99 | print(f"Effect: medium_rigidity"), 100 | trigger.effect.medium_rigidity() 101 | ), 102 | lambda trigger: ( 103 | print(f"Effect: max_rigidity"), 104 | trigger.effect.max_rigidity() 105 | ), 106 | lambda trigger: ( 107 | print(f"Effect: half_press"), 108 | trigger.effect.half_press() 109 | ), 110 | ] 111 | self.trigger_effects_num: int = len(self.trigger_effects) 112 | self.left_trigger_effect_index: int = 0 113 | self.right_trigger_effect_index: int = 0 114 | 115 | self.controller.btn_left.on_up(self.left_trigger_effect_previous) 116 | self.controller.btn_right.on_up(self.left_trigger_effect_next) 117 | self.controller.btn_square.on_up(self.right_trigger_effect_previous) 118 | self.controller.btn_circle.on_up(self.right_trigger_effect_next) 119 | 120 | def run(self) -> None: 121 | self.controller.activate() 122 | 123 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 124 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 125 | 126 | while True: 127 | time.sleep(1) 128 | 129 | # ############################################# MAIN ################################################## 130 | 131 | def left_trigger_effect_previous(self): 132 | self.left_trigger_effect_index = (self.left_trigger_effect_index - 1) % self.trigger_effects_num 133 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 134 | 135 | def left_trigger_effect_next(self): 136 | self.left_trigger_effect_index = (self.left_trigger_effect_index + 1) % self.trigger_effects_num 137 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 138 | 139 | def right_trigger_effect_previous(self): 140 | self.right_trigger_effect_index = (self.right_trigger_effect_index - 1) % self.trigger_effects_num 141 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 142 | 143 | def right_trigger_effect_next(self): 144 | self.right_trigger_effect_index = (self.right_trigger_effect_index + 1) % self.trigger_effects_num 145 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 146 | 147 | def set_trigger_effect(self, trigger: TriggerProperty, trigger_index: int): 148 | self.trigger_effects[trigger_index](trigger) 149 | 150 | 151 | # ############################################# RUN EXAMPLE ################################################## 152 | 153 | def main(): 154 | ExampleTrigger().run() 155 | 156 | 157 | if __name__ == "__main__": 158 | main() 159 | -------------------------------------------------------------------------------- /src/examples/example_trigger_deprecated.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from dualsense_controller import DualSenseController, TriggerProperty 4 | 5 | 6 | class ExampleTrigger: 7 | 8 | def __init__(self): 9 | self.controller: DualSenseController = DualSenseController( 10 | device_index_or_device_info=0, 11 | ) 12 | 13 | self.trigger_effects = [ 14 | lambda trigger: ( 15 | print(f"Effect: set_no_resistance"), 16 | trigger.effect.set_no_resistance() 17 | ), 18 | lambda trigger: ( 19 | print(f"Effect: set_continuous_resistance, start_pos: 0, force: 255"), 20 | trigger.effect.set_continuous_resistance(0, 255) 21 | ), 22 | lambda trigger: ( 23 | print(f"Effect: set_continuous_resistance, start_pos: 127, force: 255"), 24 | trigger.effect.set_continuous_resistance(127, 255) 25 | ), 26 | lambda trigger: ( 27 | print(f"Effect: set_section_resistance, start_pos: 0, endpos: 100, force: 255"), 28 | trigger.effect.set_section_resistance(0, 100, 255) 29 | ), 30 | ] 31 | self.trigger_effects_num: int = len(self.trigger_effects) 32 | self.left_trigger_effect_index: int = 0 33 | self.right_trigger_effect_index: int = 0 34 | 35 | self.controller.btn_left.on_up(self.left_trigger_effect_previous) 36 | self.controller.btn_right.on_up(self.left_trigger_effect_next) 37 | self.controller.btn_square.on_up(self.right_trigger_effect_previous) 38 | self.controller.btn_circle.on_up(self.right_trigger_effect_next) 39 | 40 | def run(self) -> None: 41 | self.controller.activate() 42 | 43 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 44 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 45 | 46 | while True: 47 | time.sleep(1) 48 | 49 | # ############################################# MAIN ################################################## 50 | 51 | def left_trigger_effect_previous(self): 52 | self.left_trigger_effect_index = (self.left_trigger_effect_index - 1) % self.trigger_effects_num 53 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 54 | 55 | def left_trigger_effect_next(self): 56 | self.left_trigger_effect_index = (self.left_trigger_effect_index + 1) % self.trigger_effects_num 57 | self.set_trigger_effect(self.controller.left_trigger, self.left_trigger_effect_index) 58 | 59 | def right_trigger_effect_previous(self): 60 | self.right_trigger_effect_index = (self.right_trigger_effect_index - 1) % self.trigger_effects_num 61 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 62 | 63 | def right_trigger_effect_next(self): 64 | self.right_trigger_effect_index = (self.right_trigger_effect_index + 1) % self.trigger_effects_num 65 | self.set_trigger_effect(self.controller.right_trigger, self.right_trigger_effect_index) 66 | 67 | def set_trigger_effect(self, trigger: TriggerProperty, trigger_index: int): 68 | self.trigger_effects[trigger_index](trigger) 69 | 70 | 71 | # ############################################# RUN EXAMPLE ################################################## 72 | 73 | def main(): 74 | ExampleTrigger().run() 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /teaser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/teaser.jpg -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/tests/__init__.py -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from dualsense_controller.api.DualSenseController import DualSenseController, Mapping 4 | from dualsense_controller.api.enum import UpdateLevel 5 | from dualsense_controller.core.state.typedef import Number 6 | from tests.mock.MockedHidapiMockedHidapiDevice import MockedHidapiMockedHidapiDevice 7 | 8 | 9 | @dataclass 10 | class ControllerInstanceParams: 11 | mapping: Mapping = Mapping.DEFAULT 12 | update_level: UpdateLevel = UpdateLevel.DEFAULT 13 | left_joystick_deadzone: Number = 0 14 | right_joystick_deadzone: Number = 0 15 | left_trigger_deadzone: Number = 0 16 | right_trigger_deadzone: Number = 0 17 | gyroscope_threshold: int = 0 18 | orientation_threshold: int = 0 19 | accelerometer_threshold: int = 0 20 | 21 | 22 | @dataclass 23 | class ControllerInstanceData: 24 | controller: DualSenseController 25 | mocked_hidapi_device: MockedHidapiMockedHidapiDevice 26 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from unittest.mock import MagicMock, Mock, patch 3 | 4 | import pytest as pytest 5 | from _pytest.fixtures import SubRequest 6 | from pytest_mock import MockerFixture 7 | 8 | from dualsense_controller.api.DualSenseController import DualSenseController 9 | from dualsense_controller.core.enum import ConnectionType 10 | from tests.common import ControllerInstanceData, ControllerInstanceParams 11 | from tests.mock.MockedHidapiMockedHidapiDevice import MockedHidapiMockedHidapiDevice 12 | from tests.mock.common import DeviceInfoMock 13 | 14 | 15 | @pytest.fixture 16 | def fixture_device_info_mock() -> DeviceInfoMock: 17 | return DeviceInfoMock() 18 | 19 | 20 | @pytest.fixture 21 | def fixture_enumerate_devices_mock( 22 | request: SubRequest, 23 | fixture_device_info_mock: DeviceInfoMock 24 | ) -> Generator[MagicMock, None, None]: 25 | num_infos: int = request.param if hasattr(request, 'param') else 1 26 | with patch( 27 | "dualsense_controller.core.HidControllerDevice.HidControllerDevice.enumerate_devices", 28 | return_value=([fixture_device_info_mock] * num_infos) if num_infos > 0 else [] 29 | ) as enumerate_devices_mock: 30 | yield enumerate_devices_mock 31 | 32 | 33 | @pytest.fixture 34 | def fixture_params_for_mocked_hidapi_device(request: SubRequest) -> ConnectionType: 35 | return request.param if hasattr(request, 'param') else None 36 | 37 | 38 | @pytest.fixture 39 | def fixture_mocked_hidapi_device( 40 | fixture_params_for_mocked_hidapi_device, 41 | request: SubRequest, 42 | mocker: MockerFixture 43 | ) -> MockedHidapiMockedHidapiDevice: 44 | conn_type: ConnectionType = request.param if hasattr(request, 'param') else fixture_params_for_mocked_hidapi_device 45 | mocked_hidapi_device: MockedHidapiMockedHidapiDevice = MockedHidapiMockedHidapiDevice(conn_type=conn_type) 46 | create_mock: Mock = mocker.patch('dualsense_controller.core.HidControllerDevice.HidControllerDevice._create') 47 | create_mock.return_value = mocked_hidapi_device 48 | return mocked_hidapi_device 49 | 50 | 51 | @pytest.fixture 52 | def fixture_params_for_controller_instance(request: SubRequest) -> ControllerInstanceParams: 53 | return request.param if hasattr(request, 'param') else ControllerInstanceParams() 54 | 55 | 56 | @pytest.fixture 57 | def fixture_controller_instance( 58 | fixture_params_for_controller_instance: ControllerInstanceParams, 59 | request: SubRequest, 60 | fixture_enumerate_devices_mock: MagicMock, 61 | fixture_mocked_hidapi_device: MockedHidapiMockedHidapiDevice 62 | ) -> Generator[ControllerInstanceData, None, None]: 63 | # prepare 64 | params: ControllerInstanceParams = ( 65 | request.param if hasattr(request, 'param') else fixture_params_for_controller_instance 66 | ) 67 | # create 68 | yield ControllerInstanceData( 69 | controller=DualSenseController( 70 | device_index_or_device_info=fixture_enumerate_devices_mock, 71 | mapping=params.mapping, 72 | update_level=params.update_level, 73 | left_joystick_deadzone=params.left_joystick_deadzone, 74 | right_joystick_deadzone=params.right_joystick_deadzone, 75 | left_trigger_deadzone=params.left_trigger_deadzone, 76 | right_trigger_deadzone=params.right_trigger_deadzone, 77 | gyroscope_threshold=params.gyroscope_threshold, 78 | orientation_threshold=params.orientation_threshold, 79 | accelerometer_threshold=params.accelerometer_threshold, 80 | ), 81 | 82 | mocked_hidapi_device=fixture_mocked_hidapi_device 83 | ) 84 | 85 | 86 | @pytest.fixture 87 | def fixture_activated_instance( 88 | request: SubRequest, 89 | fixture_controller_instance: ControllerInstanceData, 90 | ) -> Generator[ControllerInstanceData, None, None]: 91 | # activate 92 | fixture_controller_instance.controller.activate() 93 | fixture_controller_instance.controller.wait_until_updated() 94 | # pass and do test stuff 95 | yield fixture_controller_instance 96 | # deactivate 97 | fixture_controller_instance.controller.deactivate() 98 | -------------------------------------------------------------------------------- /tests/mock/MockedHidapiMockedHidapiDevice.py: -------------------------------------------------------------------------------- 1 | from dualsense_controller.core.enum import ConnectionType 2 | from dualsense_controller.core.report.in_report.InReport import InReport 3 | from dualsense_controller.core.report.in_report.Bt01InReport import Bt01InReport 4 | from dualsense_controller.core.report.in_report.Bt31InReport import Bt31InReport 5 | from dualsense_controller.core.report.in_report.Usb01InReport import Usb01InReport 6 | from dualsense_controller.core.state.read_state.ValueCalc import ValueCalc 7 | from dualsense_controller.core.state.read_state.value_type import JoyStick 8 | 9 | 10 | class _BaseMockedHidapiDevice: 11 | 12 | def __init__(self, conn_type: ConnectionType = ConnectionType.USB_01): 13 | self._in_report: InReport | None = None 14 | match conn_type: 15 | case ConnectionType.USB_01: 16 | self._in_report = Usb01InReport(raw_bytes=bytearray( 17 | b'\x01\x80\x80\x82\x83\x00\x00\x9c\x08\x00\x00\x00\x1c\x8f\xcc ' 18 | b'\x03\x00\xfc\xff\x03\x00\xb0\xff\xeb\x1e\x82\x06\xbf@\xb1\t\x02' 19 | b'\x80\x00\x00\x00\x80\x00\x00\x00\x00\t\t\x00\x00\x00\x00\x00\x03Z' 20 | b'\xb1\t)\x08\x00\xdd\xedU\xeb%{\x97\xc3' 21 | )) 22 | case ConnectionType.BT_31: 23 | self._in_report = Bt31InReport(raw_bytes=bytearray( 24 | b'1\x01\x80\x81\x82\x83\x00\x00\x01\x08\x00\x00\x00\xae\xab\x8b\xf2' 25 | b'\x02\x00\xfc\xff\x02\x00\xdc\xff\xda\x1e\x9e\x06\xc7\xb5\xe4\x00\x08' 26 | b'\x80\x00\x00\x00\x80\x00\x00\x00\x00\t\t\x00\x00\x00\x00\x00\xc3\xc9\xe4' 27 | b'\x00\t\x00\x00\xaf\xc5\x1af\xc2\xf3\x16\xbd\x00\x00\x00\x00\x00\x00\x00\x00\x00B\xd7\xd8\r' 28 | )) 29 | case ConnectionType.BT_01: 30 | self._in_report = Bt01InReport(raw_bytes=bytearray( 31 | b'\x01\x80\x80\x82\x83\x00\x00\x9c\x08\x00' 32 | )) 33 | 34 | def write(self, data: bytes): 35 | pass 36 | 37 | def read(self, _: int, **kwargs) -> bytes: 38 | return self._in_report.raw_bytes 39 | 40 | def close(self): 41 | pass 42 | 43 | 44 | class MockedHidapiMockedHidapiDevice(_BaseMockedHidapiDevice): 45 | 46 | def set_left_stick_raw(self, value: JoyStick): 47 | ValueCalc.set_left_stick(self._in_report, value) 48 | 49 | def set_left_stick_x_raw(self, value: int): 50 | ValueCalc.set_left_stick_x(self._in_report, value) 51 | 52 | def set_left_stick_y_raw(self, value: int): 53 | ValueCalc.set_left_stick_y(self._in_report, value) 54 | 55 | def set_right_stick_raw(self, value: JoyStick): 56 | ValueCalc.set_right_stick(self._in_report, value) 57 | 58 | def set_right_stick_x_raw(self, value: int): 59 | ValueCalc.set_right_stick_x(self._in_report, value) 60 | 61 | def set_right_stick_y_raw(self, value: int): 62 | ValueCalc.set_right_stick_y(self._in_report, value) 63 | 64 | def set_left_trigger_raw(self, value: int): 65 | ValueCalc.set_left_trigger(self._in_report, value) 66 | 67 | def set_right_trigger_raw(self, value: int): 68 | ValueCalc.set_right_trigger(self._in_report, value) 69 | 70 | def set_btn_square(self, value: bool): 71 | ValueCalc.set_btn_square(self._in_report, value) 72 | 73 | def set_btn_cross(self, value: bool): 74 | ValueCalc.set_btn_cross(self._in_report, value) 75 | -------------------------------------------------------------------------------- /tests/mock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/tests/mock/__init__.py -------------------------------------------------------------------------------- /tests/mock/common.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | @dataclass 6 | class DeviceInfoMock: 7 | path: str = '/dev/hidraw0' 8 | vendor_id: int = 0x054c 9 | product_id: int = 0x0ce6 10 | serial_number: str = 'a0:ab:51:a2:8c:1b' 11 | release_number: int = 256 12 | manufacturer_string: str = 'Sony Interactive Entertainment' 13 | product_string: str = 'Wireless Controller' 14 | usage_page: int = 1 15 | usage: int = 5 16 | interface_number: int = 3 17 | -------------------------------------------------------------------------------- /tests/test_creation.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest as pytest 4 | 5 | from dualsense_controller.api.DualSenseController import DualSenseController 6 | from dualsense_controller.core.enum import ConnectionType 7 | from dualsense_controller.core.exception import InvalidDeviceIndexException 8 | from tests.common import ControllerInstanceData 9 | 10 | 11 | # # @pytest.mark.skip(reason="temp disabled") 12 | @pytest.mark.parametrize( 13 | 'fixture_enumerate_devices_mock,device_index,expect_is_raising_exception', 14 | [ 15 | [0, None, True], 16 | [0, 0, True], 17 | [0, 1, True], 18 | [0, 999, True], 19 | [1, None, False], 20 | [1, 0, False], 21 | [1, 1, True], 22 | [1, 2, True], 23 | [1, 999, True], 24 | [2, None, False], 25 | [2, 0, False], 26 | [2, 1, False], 27 | [2, 2, True], 28 | [2, 3, True], 29 | [2, 999, True], 30 | [3, None, False], 31 | [3, 0, False], 32 | [3, 1, False], 33 | [3, 2, False], 34 | [3, 3, True], 35 | [3, 4, True], 36 | [4, 999, True], 37 | ], 38 | indirect=['fixture_enumerate_devices_mock'] 39 | ) 40 | def test_device_index( 41 | fixture_enumerate_devices_mock: MagicMock, 42 | device_index: int, 43 | expect_is_raising_exception: bool 44 | ) -> None: 45 | if expect_is_raising_exception: 46 | with pytest.raises(InvalidDeviceIndexException): 47 | DualSenseController(device_index) 48 | else: 49 | assert isinstance(DualSenseController(device_index), DualSenseController) 50 | 51 | 52 | # @pytest.mark.skip(reason="temp disabled") 53 | @pytest.mark.parametrize( 54 | 'fixture_params_for_mocked_hidapi_device', 55 | [ 56 | ConnectionType.USB_01, 57 | ConnectionType.BT_31, 58 | ConnectionType.BT_01, 59 | ], 60 | indirect=['fixture_params_for_mocked_hidapi_device'] 61 | ) 62 | def test_instance( 63 | fixture_params_for_mocked_hidapi_device: ConnectionType, 64 | fixture_controller_instance: ControllerInstanceData 65 | ) -> None: 66 | assert isinstance(fixture_controller_instance.controller, DualSenseController) 67 | 68 | 69 | # @pytest.mark.skip(reason="temp disabled") 70 | @pytest.mark.parametrize( 71 | 'fixture_params_for_mocked_hidapi_device,expected_conn_type', 72 | [ 73 | [ConnectionType.USB_01, ConnectionType.USB_01], 74 | [ConnectionType.BT_31, ConnectionType.BT_31], 75 | [ConnectionType.BT_01, ConnectionType.BT_01], 76 | ], 77 | indirect=['fixture_params_for_mocked_hidapi_device'] 78 | ) 79 | def test_instance_conntype( 80 | fixture_params_for_mocked_hidapi_device: ConnectionType, 81 | fixture_activated_instance: ControllerInstanceData, 82 | expected_conn_type: ConnectionType, 83 | ) -> None: 84 | assert isinstance(fixture_activated_instance.controller, DualSenseController) 85 | assert fixture_activated_instance.controller.is_active 86 | assert fixture_activated_instance.controller.connection_type.name == expected_conn_type.name 87 | -------------------------------------------------------------------------------- /tests/test_update_level.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dualsense_controller.api.enum import UpdateLevel 4 | from dualsense_controller.core.enum import ConnectionType 5 | from tests.common import ControllerInstanceData, ControllerInstanceParams 6 | 7 | 8 | # @pytest.mark.skip(reason="temp disabled") 9 | @pytest.mark.parametrize( 10 | 'fixture_params_for_mocked_hidapi_device,fixture_params_for_controller_instance', 11 | [ 12 | [ConnectionType.USB_01, ControllerInstanceParams(update_level=UpdateLevel.HAENGBLIEM)], 13 | [ConnectionType.BT_31, ControllerInstanceParams(update_level=UpdateLevel.HAENGBLIEM)], 14 | [ConnectionType.BT_01, ControllerInstanceParams(update_level=UpdateLevel.HAENGBLIEM)], 15 | ], 16 | indirect=['fixture_params_for_mocked_hidapi_device'] 17 | ) 18 | def test_update_level_haengbliem( 19 | fixture_params_for_mocked_hidapi_device: ConnectionType, 20 | fixture_params_for_controller_instance: ControllerInstanceData, 21 | fixture_activated_instance: ControllerInstanceData, 22 | ) -> None: 23 | fixture_activated_instance.controller.btn_cross.on_change(lambda _: _) 24 | fixture_activated_instance.controller.wait_until_updated() 25 | fixture_activated_instance.mocked_hidapi_device.set_btn_square(True) 26 | fixture_activated_instance.mocked_hidapi_device.set_btn_cross(True) 27 | fixture_activated_instance.controller.wait_until_updated() 28 | 29 | assert fixture_activated_instance.controller.btn_cross.pressed is True 30 | assert fixture_activated_instance.controller.btn_square.pressed is None 31 | 32 | 33 | # @pytest.mark.skip(reason="temp disabled") 34 | @pytest.mark.parametrize( 35 | 'fixture_params_for_mocked_hidapi_device,fixture_params_for_controller_instance', 36 | [ 37 | [ConnectionType.USB_01, ControllerInstanceParams(update_level=UpdateLevel.PAINSTAKING)], 38 | [ConnectionType.BT_31, ControllerInstanceParams(update_level=UpdateLevel.PAINSTAKING)], 39 | [ConnectionType.BT_01, ControllerInstanceParams(update_level=UpdateLevel.PAINSTAKING)], 40 | ], 41 | indirect=['fixture_params_for_mocked_hidapi_device'] 42 | ) 43 | def test_update_level_painstaking( 44 | fixture_params_for_mocked_hidapi_device: ConnectionType, 45 | fixture_params_for_controller_instance: ControllerInstanceData, 46 | fixture_activated_instance: ControllerInstanceData, 47 | ) -> None: 48 | fixture_activated_instance.mocked_hidapi_device.set_btn_square(True) 49 | fixture_activated_instance.mocked_hidapi_device.set_btn_cross(True) 50 | fixture_activated_instance.controller.wait_until_updated() 51 | 52 | assert fixture_activated_instance.controller.btn_cross.pressed is True 53 | assert fixture_activated_instance.controller.btn_square.pressed is True 54 | 55 | 56 | # @pytest.mark.skip(reason="temp disabled") 57 | @pytest.mark.parametrize( 58 | 'fixture_params_for_mocked_hidapi_device,fixture_params_for_controller_instance', 59 | [ 60 | [ConnectionType.USB_01, ControllerInstanceParams(update_level=UpdateLevel.DEFAULT)], 61 | [ConnectionType.BT_31, ControllerInstanceParams(update_level=UpdateLevel.DEFAULT)], 62 | [ConnectionType.BT_01, ControllerInstanceParams(update_level=UpdateLevel.DEFAULT)], 63 | ], 64 | indirect=['fixture_params_for_mocked_hidapi_device'] 65 | ) 66 | def test_update_level_default( 67 | fixture_params_for_mocked_hidapi_device: ConnectionType, 68 | fixture_params_for_controller_instance: ControllerInstanceData, 69 | fixture_activated_instance: ControllerInstanceData, 70 | ) -> None: 71 | fixture_activated_instance.mocked_hidapi_device.set_btn_square(True) 72 | fixture_activated_instance.mocked_hidapi_device.set_btn_cross(True) 73 | fixture_activated_instance.controller.wait_until_updated() 74 | 75 | assert fixture_activated_instance.controller.btn_cross.pressed is True 76 | assert fixture_activated_instance.controller.btn_square.pressed is True 77 | -------------------------------------------------------------------------------- /tools_dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/tools_dev/__init__.py -------------------------------------------------------------------------------- /tools_dev/shark/TSharkCapture.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from subprocess import Popen 3 | from threading import Thread, Event 4 | from typing import Final, Callable 5 | 6 | 7 | class TSharkCapture: 8 | 9 | def __init__(self, interface: str, destination: str): 10 | self.__interface: Final[str] = interface 11 | self.__destination: Final[str] = destination 12 | self.__capture_thread: Thread | None = None 13 | self.__stop_event: Final[Event] = Event() 14 | 15 | def sniff(self, callback: Callable[[str], None]) -> None: 16 | self.__stop_event.clear() 17 | self.__capture_thread = Thread(target=self._sniff, daemon=True, args=(callback,)) 18 | self.__capture_thread.start() 19 | 20 | def stop(self) -> None: 21 | self.__stop_event.set() 22 | self.__capture_thread.join() 23 | self.__capture_thread = None 24 | 25 | def _sniff(self, callback: Callable[[str], None]) -> None: 26 | cmd: str = 'tshark' \ 27 | f' -i {self.__interface}' \ 28 | f' -Y "usb.dst == {self.__destination}"' \ 29 | ' --hexdump frames' \ 30 | ' --hexdump delimit' \ 31 | ' --hexdump noascii' 32 | process: Popen[str] = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) 33 | while not self.__stop_event.is_set(): 34 | output: str = process.stdout.readline() 35 | if output == '' and process.poll() is not None: 36 | break 37 | if output: 38 | callback(output.strip()) 39 | process.kill() 40 | -------------------------------------------------------------------------------- /tools_dev/shark/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yesbotics/dualsense-controller-python/a897d1999445215c7050ed4c69c821ff1419d8f8/tools_dev/shark/__init__.py -------------------------------------------------------------------------------- /tools_dev/shark/dsadasdas: -------------------------------------------------------------------------------- 1 | 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 45 46 47 48 2 | ID FP FC MR ML ----------- ML MM 02 00 05 00 00 00 00 00 00 00 00 00 00 21 c0 03 00 00 d8 36 00 00 00 00 00 00 00 00 00 00 07 01 00 02 00 2a COLOR 3 | 02 0f 55 00 00 00 00 00 00 02 00 05 00 00 00 00 00 00 00 00 00 00 21 c0 03 00 00 d8 36 00 00 00 00 00 00 00 00 00 00 07 01 00 02 00 2a b0 13 55 -------------------------------------------------------------------------------- /tools_dev/shark/shark.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from argparse import ArgumentParser, Namespace 3 | from dataclasses import dataclass 4 | from time import sleep 5 | from typing import Final 6 | 7 | import _curses 8 | 9 | from TSharkCapture import TSharkCapture 10 | 11 | 12 | class Shark: 13 | 14 | def __init__(self, stdscr: _curses.window, capture_interface_name: str, destination: str): 15 | self.__stdscr = stdscr 16 | self.__capture: Final[TSharkCapture] = TSharkCapture( 17 | interface=capture_interface_name, 18 | destination=destination 19 | ) 20 | self.__buff: list[str] | None = [] 21 | self.__last_frame_data: str | None = None 22 | 23 | def __on_data(self, data: str) -> None: 24 | if 38 <= len(data) <= 53: 25 | line: str = data[6:] # cut leading numbers 26 | self.__buff.append(line) 27 | if len(self.__buff) == 5: 28 | single_line: str = " ".join(self.__buff[1:]) # glue lines except first line 29 | frame_data: str = single_line[33:] # cut leading numbers 30 | self.__on_frame_data(frame_data) 31 | self.__buff.clear() 32 | 33 | def __on_frame_data(self, frame_data: str) -> None: 34 | if self.__last_frame_data != frame_data: 35 | self.__on_frame_data_change( 36 | self.__last_frame_data if self.__last_frame_data is not None else frame_data, 37 | frame_data 38 | ) 39 | self.__last_frame_data = frame_data 40 | 41 | def __on_frame_data_change(self, old_data: str | None, new_data: str) -> None: 42 | # posi: str = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47" 43 | # head: str = "ID|FP|FC|MR|ML| |ML|MM|TRMTR1TR2 3 4 5 6 7 |TL | |LO|LS|PO|BR|PL|R |G |B " 44 | # data: str = "02 0f 55 00 00 00 00 00 00 02 00 05 00 00 00 00 00 00 00 00 00 00 21 c0 03 00 00 d8 36 00 00 00 00 00 00 00 00 00 00 07 01 00 02 00 2a b0 13 55" 45 | 46 | bytez: list[str] = new_data.split(' ') 47 | 48 | @dataclass(frozen=True) 49 | class Bubs: 50 | name: str 51 | indexes: list[int] 52 | 53 | bubse: list[Bubs] = [ 54 | Bubs(name='operating_mode', indexes=[0]), 55 | Bubs(name='flags_physics', indexes=[1]), 56 | Bubs(name='flags_controls', indexes=[2]), 57 | Bubs(name='motor_right', indexes=[3]), 58 | Bubs(name='motor_left', indexes=[4]), 59 | Bubs(name='microphone_led', indexes=[9]), 60 | Bubs(name='microphone_mute', indexes=[10]), 61 | Bubs(name='right_trigger', indexes=[11, 12, 13, 14, 15, 16, 17, 20]), 62 | Bubs(name='left_trigger', indexes=[22, 23, 24, 25, 26, 27, 28, 31]), 63 | Bubs(name='led_options', indexes=[39]), 64 | Bubs(name='lightbar_pulse_options', indexes=[42]), 65 | Bubs(name='player_leds_brightness', indexes=[43]), 66 | Bubs(name='player_leds_enable', indexes=[44]), 67 | Bubs(name='color', indexes=[45, 46, 47]), 68 | ] 69 | 70 | self.__stdscr.clear() 71 | self.__stdscr.addstr(f'{new_data}\n') 72 | for bubs in bubse: 73 | self.__stdscr.addstr(f'{bubs.name}: {" ".join([bytez[idx] for idx in bubs.indexes])}\n') 74 | 75 | self.__stdscr.refresh() 76 | 77 | def run(self) -> None: 78 | self.__capture.sniff(self.__on_data) 79 | while True: 80 | sleep(0.0000000001) 81 | 82 | 83 | def main(stdscr: _curses.window): 84 | curses.curs_set(0) # Hide the cursor 85 | stdscr.nodelay(True) # Non-blocking input 86 | 87 | parser: ArgumentParser = ArgumentParser() 88 | parser.add_argument("capture_interface_name") 89 | parser.add_argument("destination") 90 | args: Namespace = parser.parse_args() 91 | 92 | Shark( 93 | stdscr, 94 | capture_interface_name=args.capture_interface_name, 95 | destination=args.destination, 96 | ).run() 97 | 98 | 99 | if __name__ == "__main__": 100 | curses.wrapper(main) 101 | -------------------------------------------------------------------------------- /tools_dev/shark/shark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tshark -i USBPcap3 -Y "usb.dst == 3.8.3" --hexdump frames --hexdump delimit --hexdump noascii | ./watch.sh -------------------------------------------------------------------------------- /tools_dev/shark/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | count=0 4 | prints=0 5 | text="" 6 | oldtext="" 7 | 8 | while IFS= read -r line; do 9 | 10 | if [ "$count" -gt 0 ]; then 11 | text="$text \n$line" 12 | fi 13 | 14 | count=$((count + 1)) 15 | 16 | if [ "$count" -gt 5 ]; then 17 | if ! [ "$text" = "$oldtext" ]; then 18 | clear 19 | echo -e "$text" 20 | echo -e "Prints: ${prints}" 21 | oldtext="$text" 22 | prints=$((prints + 1)) 23 | fi 24 | 25 | text="" 26 | count=0 27 | fi 28 | done --------------------------------------------------------------------------------