dualsense-py 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. dualsense_py-0.3.0/.github/workflows/publish.yml +64 -0
  2. dualsense_py-0.3.0/.gitignore +5 -0
  3. dualsense_py-0.3.0/LICENSE.txt +21 -0
  4. dualsense_py-0.3.0/PKG-INFO +245 -0
  5. dualsense_py-0.3.0/README.md +216 -0
  6. dualsense_py-0.3.0/dualsense_py/__init__.py +7 -0
  7. dualsense_py-0.3.0/dualsense_py/_version.py +24 -0
  8. dualsense_py-0.3.0/dualsense_py/backends/__init__.py +3 -0
  9. dualsense_py-0.3.0/dualsense_py/backends/backend.py +67 -0
  10. dualsense_py-0.3.0/dualsense_py/backends/device_infos.py +270 -0
  11. dualsense_py-0.3.0/dualsense_py/backends/hidapi/hidapi_backend.py +25 -0
  12. dualsense_py-0.3.0/dualsense_py/backends/hidapi/hidapi_device_info.py +113 -0
  13. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/__init__.py +6 -0
  14. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/bt_01_in_report.py +11 -0
  15. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/bt_31_in_report.py +25 -0
  16. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/enums.py +8 -0
  17. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/exceptions.py +21 -0
  18. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/in_report.py +281 -0
  19. dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/usb_01_in_report.py +23 -0
  20. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/__init__.py +4 -0
  21. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/bt_01_out_report.py +8 -0
  22. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/bt_31_out_report.py +70 -0
  23. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/crc32.py +50 -0
  24. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/enums.py +186 -0
  25. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/out_report.py +65 -0
  26. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/usb_01_out_report.py +96 -0
  27. dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/utils.py +6 -0
  28. dualsense_py-0.3.0/dualsense_py/backends/sdl3/sdl3_backend.py +37 -0
  29. dualsense_py-0.3.0/dualsense_py/backends/sdl3/sdl3_device_info.py +133 -0
  30. dualsense_py-0.3.0/dualsense_py/change_detection.py +65 -0
  31. dualsense_py-0.3.0/dualsense_py/dual_sense_controller.py +249 -0
  32. dualsense_py-0.3.0/dualsense_py/mapping.py +139 -0
  33. dualsense_py-0.3.0/dualsense_py/py.typed +0 -0
  34. dualsense_py-0.3.0/dualsense_py/readable_value.py +114 -0
  35. dualsense_py-0.3.0/dualsense_py/states/__init__.py +7 -0
  36. dualsense_py-0.3.0/dualsense_py/states/accelerometer.py +7 -0
  37. dualsense_py-0.3.0/dualsense_py/states/battery.py +7 -0
  38. dualsense_py-0.3.0/dualsense_py/states/gyroscope.py +7 -0
  39. dualsense_py-0.3.0/dualsense_py/states/joy_stick.py +6 -0
  40. dualsense_py-0.3.0/dualsense_py/states/orientation.py +7 -0
  41. dualsense_py-0.3.0/dualsense_py/states/touch_finger.py +8 -0
  42. dualsense_py-0.3.0/dualsense_py/states/trigger_feedback.py +7 -0
  43. dualsense_py-0.3.0/dualsense_py/utils.py +26 -0
  44. dualsense_py-0.3.0/dualsense_py.egg-info/PKG-INFO +245 -0
  45. dualsense_py-0.3.0/dualsense_py.egg-info/SOURCES.txt +56 -0
  46. dualsense_py-0.3.0/dualsense_py.egg-info/dependency_links.txt +1 -0
  47. dualsense_py-0.3.0/dualsense_py.egg-info/requires.txt +6 -0
  48. dualsense_py-0.3.0/dualsense_py.egg-info/scm_file_list.json +52 -0
  49. dualsense_py-0.3.0/dualsense_py.egg-info/scm_version.json +8 -0
  50. dualsense_py-0.3.0/dualsense_py.egg-info/top_level.txt +1 -0
  51. dualsense_py-0.3.0/examples/basic_usage.py +64 -0
  52. dualsense_py-0.3.0/pyproject.toml +52 -0
  53. dualsense_py-0.3.0/requirements.txt +3 -0
  54. dualsense_py-0.3.0/setup.cfg +4 -0
  55. dualsense_py-0.3.0/tests/conftest.py +41 -0
  56. dualsense_py-0.3.0/tests/test_dual_sense_controller.py +86 -0
  57. dualsense_py-0.3.0/tests/test_readable_value.py +70 -0
  58. dualsense_py-0.3.0/tests/test_utils.py +69 -0
@@ -0,0 +1,64 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ name: Build distribution
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Install build tooling
23
+ run: python -m pip install --upgrade build twine setuptools-scm
24
+
25
+ - name: Build sdist and wheel
26
+ run: python -m build
27
+
28
+ - name: Check distribution metadata
29
+ run: python -m twine check dist/*
30
+
31
+ - name: Verify built version matches tag
32
+ if: startsWith(github.ref, 'refs/tags/v')
33
+ run: |
34
+ built_version="$(python -m setuptools_scm)"
35
+ tag_version="${GITHUB_REF_NAME#v}"
36
+ if [ "$built_version" != "$tag_version" ]; then
37
+ echo "::error::Built version ($built_version) does not match tag ($tag_version)"
38
+ exit 1
39
+ fi
40
+
41
+ - name: Upload build artifacts
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+
47
+ publish:
48
+ name: Publish to PyPI
49
+ needs: build
50
+ runs-on: ubuntu-latest
51
+ environment:
52
+ name: pypi
53
+ url: https://pypi.org/project/dualsense-py/
54
+ permissions:
55
+ id-token: write
56
+ steps:
57
+ - name: Download build artifacts
58
+ uses: actions/download-artifact@v4
59
+ with:
60
+ name: dist
61
+ path: dist/
62
+
63
+ - name: Publish to PyPI
64
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ dualsense_py/_version.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Benjamin Heuberger
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.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: dualsense-py
3
+ Version: 0.3.0
4
+ Summary: Python library for interacting with DualSense controllers (input states, LEDs, adaptive triggers) via HIDAPI or SDL3 backends.
5
+ Author-email: Benjamin Heuberger <beni.heu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Rothen/dualsensepy
8
+ Project-URL: Repository, https://github.com/Rothen/dualsensepy
9
+ Project-URL: Issues, https://github.com/Rothen/dualsensepy/issues
10
+ Keywords: dualsense,ps5,playstation,controller,gamepad,hidapi,sdl3
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Games/Entertainment
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.txt
23
+ Requires-Dist: PySDL3==0.9.12b1
24
+ Requires-Dist: hidapi-py==0.1.2
25
+ Requires-Dist: reactivex==5.1.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # dualsense-py
31
+
32
+ A Python library for reading state from a DualSense controller and reacting to input events in real time.
33
+
34
+ This package exposes the controller as a live stream of events and state updates, with support for:
35
+
36
+ - button press/release callbacks
37
+ - analog stick changes
38
+ - trigger values
39
+ - accelerometer and gyroscope data
40
+ - battery and orientation state
41
+ - LED control
42
+ - HID-backed controller discovery
43
+
44
+ ## Features
45
+
46
+ - Read DualSense button states with callback hooks such as `square_pressed()` and `circle_released()`
47
+ - Track joystick, trigger, accelerometer, gyroscope, and orientation values as they change
48
+ - Detect available controllers using built-in utility functions
49
+ - Set the controller LED color with `set_led(red, green, blue)`
50
+ - Use the library with native HID device support through `hidapi` and SDL-based discovery helpers
51
+
52
+ ## Installation
53
+
54
+ Install from PyPI:
55
+
56
+ ```bash
57
+ pip install dualsense-py
58
+ ```
59
+
60
+ Install from the repository source:
61
+
62
+ ```bash
63
+ git clone https://github.com/Rothen/dualsensepy.git
64
+ cd dualsensepy
65
+ pip install .
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import time
72
+
73
+ from dualsense_py.utils import get_all_dual_sense_controllers
74
+
75
+ controllers = get_all_dual_sense_controllers()
76
+ if not controllers:
77
+ raise RuntimeError("No DualSense controller found")
78
+
79
+ controller = controllers[0]
80
+ controller.open()
81
+
82
+ controller.square_pressed(lambda _: print("Square pressed"))
83
+ controller.square_released(lambda _: print("Square released"))
84
+ controller.left_joy_stick_changed(lambda stick: print(f"Left stick: ({stick.x}, {stick.y})"))
85
+ controller.battery_changed(lambda battery: print(f"Battery: {battery.level_percentage}%"))
86
+ controller.set_led(0, 255, 0)
87
+
88
+ try:
89
+ while True:
90
+ time.sleep(0.1)
91
+ finally:
92
+ controller.close()
93
+ ```
94
+
95
+ A fuller, runnable version of this (button/stick/battery logging plus a light bar color cycle) lives at [examples/basic_usage.py](examples/basic_usage.py):
96
+
97
+ ```bash
98
+ python examples/basic_usage.py
99
+ ```
100
+
101
+ ## Controller discovery
102
+
103
+ The package includes helper functions for locating controllers:
104
+
105
+ ```python
106
+ from dualsense_py.utils import (
107
+ get_all_controllers,
108
+ get_all_dual_sense_controllers,
109
+ get_available_controllers,
110
+ )
111
+
112
+ controllers = get_all_dual_sense_controllers()
113
+ for controller in controllers:
114
+ print(controller)
115
+
116
+ # Generic discovery by vendor/product IDs
117
+ all_controllers = get_all_controllers(0x054C, 0x0CE6)
118
+ ```
119
+
120
+ ## Backends
121
+
122
+ `dualsense-py` ships two backends for talking to the controller:
123
+
124
+ - **hidapi** (`get_all_dual_sense_controllers()`, `get_all_controllers()`) — reads raw HID reports directly. This is the default used above and needs no extra setup. LED control (`set_led`) is not implemented on this backend yet and is a no-op.
125
+ - **SDL3** (`get_available_controllers()`) — uses SDL3's gamepad API for broader controller support and working LED control, at the cost of an explicit init step:
126
+
127
+ ```python
128
+ from dualsense_py.backends import SDL3Backend
129
+ from dualsense_py.utils import get_available_controllers
130
+
131
+ SDL3Backend.init()
132
+ controllers = get_available_controllers()
133
+ ```
134
+
135
+ Call `SDL3Backend.init()` once before discovering controllers with `get_available_controllers()`. Controller state is read by polling (`SDL_UpdateGamepads()` plus the `SDL_GetGamepad*()` getters) rather than draining SDL's event queue, since `DualSenseController` reads each device from its own background thread and SDL only allows event pumping from the thread that called `SDL_Init`.
136
+
137
+ ## Event API
138
+
139
+ The controller exposes a set of event subscription methods. The naming follows the hardware input names and callback type.
140
+
141
+ ### Button press callbacks
142
+
143
+ ```python
144
+ controller.square_pressed(lambda _: print("Square pressed"))
145
+ controller.cross_pressed(lambda _: print("Cross pressed"))
146
+ controller.circle_pressed(lambda _: print("Circle pressed"))
147
+ controller.triangle_pressed(lambda _: print("Triangle pressed"))
148
+ controller.l1_pressed(lambda _: print("L1 pressed"))
149
+ controller.r1_pressed(lambda _: print("R1 pressed"))
150
+ controller.share_pressed(lambda _: print("Share pressed"))
151
+ controller.options_pressed(lambda _: print("Options pressed"))
152
+ ```
153
+
154
+ ### Button release callbacks
155
+
156
+ ```python
157
+ controller.square_released(lambda _: print("Square released"))
158
+ controller.dpad_up_released(lambda _: print("D-pad up released"))
159
+ controller.dpad_down_released(lambda _: print("D-pad down released"))
160
+ controller.ps_released(lambda _: print("PS released"))
161
+ ```
162
+
163
+ ### State change callbacks
164
+
165
+ ```python
166
+ controller.left_joy_stick_changed(lambda stick: print(stick))
167
+ controller.right_joy_stick_changed(lambda stick: print(stick))
168
+ controller.l2_trigger_changed(lambda value: print(f"L2: {value}"))
169
+ controller.r2_trigger_changed(lambda value: print(f"R2: {value}"))
170
+ controller.accelerometer_changed(lambda accel: print(accel))
171
+ controller.gyroscope_changed(lambda gyro: print(gyro))
172
+ controller.battery_changed(lambda battery: print(battery))
173
+ controller.orientation_changed(lambda orientation: print(orientation))
174
+ controller.touch_finger_1_changed(lambda finger: print(finger))
175
+ controller.touch_finger_2_changed(lambda finger: print(finger))
176
+ ```
177
+
178
+ ## State objects
179
+
180
+ The library emits dataclasses representing controller state values. These are the actual payloads produced by the change listeners:
181
+
182
+ - `JoyStick(x, y)`
183
+ - `Battery(level_percentage, full, charging)`
184
+ - `Accelerometer(x, y, z)`
185
+ - `Gyroscope(x, y, z)`
186
+ - `Orientation(pitch, roll, yaw)`
187
+ - `TouchFinger(active, id, x, y)`
188
+ - `TriggerFeedback(active, value)`
189
+
190
+ Example:
191
+
192
+ ```python
193
+ controller.left_joy_stick_changed(
194
+ lambda stick: print(f"X={stick.x}, Y={stick.y}")
195
+ )
196
+ ```
197
+
198
+ ## LED control
199
+
200
+ Set the light bar color on the controller:
201
+
202
+ ```python
203
+ controller.set_led(255, 0, 0) # red
204
+ controller.set_led(0, 255, 0) # green
205
+ controller.set_led(0, 0, 255) # blue
206
+ controller.set_led(255, 255, 0) # yellow
207
+ ```
208
+
209
+ The values are standard 8-bit channel values from 0 to 255.
210
+
211
+ > LED control currently requires the SDL3 backend — see [Backends](#backends). On the `hidapi` backend, `set_led` is a no-op.
212
+
213
+ ## Runtime properties
214
+
215
+ The controller instance also exposes timing metadata while the read loop is running:
216
+
217
+ ```python
218
+ print(controller.read_time)
219
+ print(controller.loop_time)
220
+ ```
221
+
222
+ ## Notes
223
+
224
+ - The project is currently in an early stage and is best treated as a low-level controller library.
225
+ - Controller access depends on the underlying HID / backend support in the current environment.
226
+ - Event callbacks are subscription-based and can be used to build reactive input loops or game automation logic.
227
+
228
+ ## Development
229
+
230
+ Install the package with its test dependencies and run the test suite with `pytest`:
231
+
232
+ ```bash
233
+ pip install -e .[dev]
234
+ pytest
235
+ ```
236
+
237
+ The tests drive `DualSenseController` against an in-memory fake device (see `tests/conftest.py`), so no physical controller is required to run them.
238
+
239
+ ## License
240
+
241
+ This project is licensed under the [MIT License](LICENSE.txt).
242
+
243
+ ## Contributing
244
+
245
+ Contributions are welcome. If you want to improve the library, open an issue or submit a pull request with a clear description of the change and any validation steps.
@@ -0,0 +1,216 @@
1
+ # dualsense-py
2
+
3
+ A Python library for reading state from a DualSense controller and reacting to input events in real time.
4
+
5
+ This package exposes the controller as a live stream of events and state updates, with support for:
6
+
7
+ - button press/release callbacks
8
+ - analog stick changes
9
+ - trigger values
10
+ - accelerometer and gyroscope data
11
+ - battery and orientation state
12
+ - LED control
13
+ - HID-backed controller discovery
14
+
15
+ ## Features
16
+
17
+ - Read DualSense button states with callback hooks such as `square_pressed()` and `circle_released()`
18
+ - Track joystick, trigger, accelerometer, gyroscope, and orientation values as they change
19
+ - Detect available controllers using built-in utility functions
20
+ - Set the controller LED color with `set_led(red, green, blue)`
21
+ - Use the library with native HID device support through `hidapi` and SDL-based discovery helpers
22
+
23
+ ## Installation
24
+
25
+ Install from PyPI:
26
+
27
+ ```bash
28
+ pip install dualsense-py
29
+ ```
30
+
31
+ Install from the repository source:
32
+
33
+ ```bash
34
+ git clone https://github.com/Rothen/dualsensepy.git
35
+ cd dualsensepy
36
+ pip install .
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ import time
43
+
44
+ from dualsense_py.utils import get_all_dual_sense_controllers
45
+
46
+ controllers = get_all_dual_sense_controllers()
47
+ if not controllers:
48
+ raise RuntimeError("No DualSense controller found")
49
+
50
+ controller = controllers[0]
51
+ controller.open()
52
+
53
+ controller.square_pressed(lambda _: print("Square pressed"))
54
+ controller.square_released(lambda _: print("Square released"))
55
+ controller.left_joy_stick_changed(lambda stick: print(f"Left stick: ({stick.x}, {stick.y})"))
56
+ controller.battery_changed(lambda battery: print(f"Battery: {battery.level_percentage}%"))
57
+ controller.set_led(0, 255, 0)
58
+
59
+ try:
60
+ while True:
61
+ time.sleep(0.1)
62
+ finally:
63
+ controller.close()
64
+ ```
65
+
66
+ A fuller, runnable version of this (button/stick/battery logging plus a light bar color cycle) lives at [examples/basic_usage.py](examples/basic_usage.py):
67
+
68
+ ```bash
69
+ python examples/basic_usage.py
70
+ ```
71
+
72
+ ## Controller discovery
73
+
74
+ The package includes helper functions for locating controllers:
75
+
76
+ ```python
77
+ from dualsense_py.utils import (
78
+ get_all_controllers,
79
+ get_all_dual_sense_controllers,
80
+ get_available_controllers,
81
+ )
82
+
83
+ controllers = get_all_dual_sense_controllers()
84
+ for controller in controllers:
85
+ print(controller)
86
+
87
+ # Generic discovery by vendor/product IDs
88
+ all_controllers = get_all_controllers(0x054C, 0x0CE6)
89
+ ```
90
+
91
+ ## Backends
92
+
93
+ `dualsense-py` ships two backends for talking to the controller:
94
+
95
+ - **hidapi** (`get_all_dual_sense_controllers()`, `get_all_controllers()`) — reads raw HID reports directly. This is the default used above and needs no extra setup. LED control (`set_led`) is not implemented on this backend yet and is a no-op.
96
+ - **SDL3** (`get_available_controllers()`) — uses SDL3's gamepad API for broader controller support and working LED control, at the cost of an explicit init step:
97
+
98
+ ```python
99
+ from dualsense_py.backends import SDL3Backend
100
+ from dualsense_py.utils import get_available_controllers
101
+
102
+ SDL3Backend.init()
103
+ controllers = get_available_controllers()
104
+ ```
105
+
106
+ Call `SDL3Backend.init()` once before discovering controllers with `get_available_controllers()`. Controller state is read by polling (`SDL_UpdateGamepads()` plus the `SDL_GetGamepad*()` getters) rather than draining SDL's event queue, since `DualSenseController` reads each device from its own background thread and SDL only allows event pumping from the thread that called `SDL_Init`.
107
+
108
+ ## Event API
109
+
110
+ The controller exposes a set of event subscription methods. The naming follows the hardware input names and callback type.
111
+
112
+ ### Button press callbacks
113
+
114
+ ```python
115
+ controller.square_pressed(lambda _: print("Square pressed"))
116
+ controller.cross_pressed(lambda _: print("Cross pressed"))
117
+ controller.circle_pressed(lambda _: print("Circle pressed"))
118
+ controller.triangle_pressed(lambda _: print("Triangle pressed"))
119
+ controller.l1_pressed(lambda _: print("L1 pressed"))
120
+ controller.r1_pressed(lambda _: print("R1 pressed"))
121
+ controller.share_pressed(lambda _: print("Share pressed"))
122
+ controller.options_pressed(lambda _: print("Options pressed"))
123
+ ```
124
+
125
+ ### Button release callbacks
126
+
127
+ ```python
128
+ controller.square_released(lambda _: print("Square released"))
129
+ controller.dpad_up_released(lambda _: print("D-pad up released"))
130
+ controller.dpad_down_released(lambda _: print("D-pad down released"))
131
+ controller.ps_released(lambda _: print("PS released"))
132
+ ```
133
+
134
+ ### State change callbacks
135
+
136
+ ```python
137
+ controller.left_joy_stick_changed(lambda stick: print(stick))
138
+ controller.right_joy_stick_changed(lambda stick: print(stick))
139
+ controller.l2_trigger_changed(lambda value: print(f"L2: {value}"))
140
+ controller.r2_trigger_changed(lambda value: print(f"R2: {value}"))
141
+ controller.accelerometer_changed(lambda accel: print(accel))
142
+ controller.gyroscope_changed(lambda gyro: print(gyro))
143
+ controller.battery_changed(lambda battery: print(battery))
144
+ controller.orientation_changed(lambda orientation: print(orientation))
145
+ controller.touch_finger_1_changed(lambda finger: print(finger))
146
+ controller.touch_finger_2_changed(lambda finger: print(finger))
147
+ ```
148
+
149
+ ## State objects
150
+
151
+ The library emits dataclasses representing controller state values. These are the actual payloads produced by the change listeners:
152
+
153
+ - `JoyStick(x, y)`
154
+ - `Battery(level_percentage, full, charging)`
155
+ - `Accelerometer(x, y, z)`
156
+ - `Gyroscope(x, y, z)`
157
+ - `Orientation(pitch, roll, yaw)`
158
+ - `TouchFinger(active, id, x, y)`
159
+ - `TriggerFeedback(active, value)`
160
+
161
+ Example:
162
+
163
+ ```python
164
+ controller.left_joy_stick_changed(
165
+ lambda stick: print(f"X={stick.x}, Y={stick.y}")
166
+ )
167
+ ```
168
+
169
+ ## LED control
170
+
171
+ Set the light bar color on the controller:
172
+
173
+ ```python
174
+ controller.set_led(255, 0, 0) # red
175
+ controller.set_led(0, 255, 0) # green
176
+ controller.set_led(0, 0, 255) # blue
177
+ controller.set_led(255, 255, 0) # yellow
178
+ ```
179
+
180
+ The values are standard 8-bit channel values from 0 to 255.
181
+
182
+ > LED control currently requires the SDL3 backend — see [Backends](#backends). On the `hidapi` backend, `set_led` is a no-op.
183
+
184
+ ## Runtime properties
185
+
186
+ The controller instance also exposes timing metadata while the read loop is running:
187
+
188
+ ```python
189
+ print(controller.read_time)
190
+ print(controller.loop_time)
191
+ ```
192
+
193
+ ## Notes
194
+
195
+ - The project is currently in an early stage and is best treated as a low-level controller library.
196
+ - Controller access depends on the underlying HID / backend support in the current environment.
197
+ - Event callbacks are subscription-based and can be used to build reactive input loops or game automation logic.
198
+
199
+ ## Development
200
+
201
+ Install the package with its test dependencies and run the test suite with `pytest`:
202
+
203
+ ```bash
204
+ pip install -e .[dev]
205
+ pytest
206
+ ```
207
+
208
+ The tests drive `DualSenseController` against an in-memory fake device (see `tests/conftest.py`), so no physical controller is required to run them.
209
+
210
+ ## License
211
+
212
+ This project is licensed under the [MIT License](LICENSE.txt).
213
+
214
+ ## Contributing
215
+
216
+ Contributions are welcome. If you want to improve the library, open an issue or submit a pull request with a clear description of the change and any validation steps.
@@ -0,0 +1,7 @@
1
+ from .dual_sense_controller import DualSenseController
2
+ from .utils import get_all_controllers, get_all_dual_sense_controllers, get_all_xbox_360_controllers
3
+
4
+ try:
5
+ from ._version import __version__
6
+ except ImportError:
7
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
+
24
+ __commit_id__ = commit_id = 'gfde1784d8'
@@ -0,0 +1,3 @@
1
+ from .backend import DeviceInfo
2
+ from .hidapi.hidapi_backend import HidAPIBackend
3
+ from .sdl3.sdl3_backend import SDL3Backend
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import Generic, TypeVar, Any, Type
4
+
5
+
6
+ from .device_infos import DeviceInfo
7
+
8
+
9
+ _DeviceInfoType = TypeVar("_DeviceInfoType", bound=DeviceInfo[Any, Any])
10
+
11
+
12
+ class Backend(ABC, Generic[_DeviceInfoType]):
13
+ """
14
+ Abstract base class for backend implementations.
15
+ """
16
+
17
+ ActiveBackend: Type[Backend[Any]] | None = None
18
+
19
+ @staticmethod
20
+ @abstractmethod
21
+ def _get_available_devices() -> list[_DeviceInfoType]:
22
+ """
23
+ Open a connection to the device at the specified path.
24
+ """
25
+
26
+ @staticmethod
27
+ @abstractmethod
28
+ def _init() -> Type[Backend[_DeviceInfoType]]:
29
+ pass
30
+
31
+ @staticmethod
32
+ @abstractmethod
33
+ def _quit() -> None:
34
+ pass
35
+
36
+ __slots__ = ( )
37
+
38
+ @classmethod
39
+ def init(cls) -> None:
40
+ """
41
+ Initialize the backend.
42
+ """
43
+ if cls == Backend:
44
+ raise TypeError("Cannot initialize the base Backend class directly.")
45
+
46
+ Backend.ActiveBackend = cls._init()
47
+
48
+ @staticmethod
49
+ def get_available_devices() -> list[_DeviceInfoType]:
50
+ """
51
+ Open a connection to the device at the specified path.
52
+ """
53
+ if Backend.ActiveBackend is None:
54
+ raise TypeError("Cannot call get_available_devices() without initializing the backend.")
55
+
56
+ return Backend.ActiveBackend._get_available_devices()
57
+
58
+ @staticmethod
59
+ def quit() -> None:
60
+ """
61
+ Close the backend.
62
+ """
63
+ if Backend.ActiveBackend is None:
64
+ return
65
+
66
+ Backend.ActiveBackend._quit()
67
+ Backend.ActiveBackend = None