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.
- dualsense_py-0.3.0/.github/workflows/publish.yml +64 -0
- dualsense_py-0.3.0/.gitignore +5 -0
- dualsense_py-0.3.0/LICENSE.txt +21 -0
- dualsense_py-0.3.0/PKG-INFO +245 -0
- dualsense_py-0.3.0/README.md +216 -0
- dualsense_py-0.3.0/dualsense_py/__init__.py +7 -0
- dualsense_py-0.3.0/dualsense_py/_version.py +24 -0
- dualsense_py-0.3.0/dualsense_py/backends/__init__.py +3 -0
- dualsense_py-0.3.0/dualsense_py/backends/backend.py +67 -0
- dualsense_py-0.3.0/dualsense_py/backends/device_infos.py +270 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/hidapi_backend.py +25 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/hidapi_device_info.py +113 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/__init__.py +6 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/bt_01_in_report.py +11 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/bt_31_in_report.py +25 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/enums.py +8 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/exceptions.py +21 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/in_report.py +281 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/in_report/usb_01_in_report.py +23 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/__init__.py +4 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/bt_01_out_report.py +8 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/bt_31_out_report.py +70 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/crc32.py +50 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/enums.py +186 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/out_report.py +65 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/usb_01_out_report.py +96 -0
- dualsense_py-0.3.0/dualsense_py/backends/hidapi/out_report/utils.py +6 -0
- dualsense_py-0.3.0/dualsense_py/backends/sdl3/sdl3_backend.py +37 -0
- dualsense_py-0.3.0/dualsense_py/backends/sdl3/sdl3_device_info.py +133 -0
- dualsense_py-0.3.0/dualsense_py/change_detection.py +65 -0
- dualsense_py-0.3.0/dualsense_py/dual_sense_controller.py +249 -0
- dualsense_py-0.3.0/dualsense_py/mapping.py +139 -0
- dualsense_py-0.3.0/dualsense_py/py.typed +0 -0
- dualsense_py-0.3.0/dualsense_py/readable_value.py +114 -0
- dualsense_py-0.3.0/dualsense_py/states/__init__.py +7 -0
- dualsense_py-0.3.0/dualsense_py/states/accelerometer.py +7 -0
- dualsense_py-0.3.0/dualsense_py/states/battery.py +7 -0
- dualsense_py-0.3.0/dualsense_py/states/gyroscope.py +7 -0
- dualsense_py-0.3.0/dualsense_py/states/joy_stick.py +6 -0
- dualsense_py-0.3.0/dualsense_py/states/orientation.py +7 -0
- dualsense_py-0.3.0/dualsense_py/states/touch_finger.py +8 -0
- dualsense_py-0.3.0/dualsense_py/states/trigger_feedback.py +7 -0
- dualsense_py-0.3.0/dualsense_py/utils.py +26 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/PKG-INFO +245 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/SOURCES.txt +56 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/dependency_links.txt +1 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/requires.txt +6 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/scm_file_list.json +52 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/scm_version.json +8 -0
- dualsense_py-0.3.0/dualsense_py.egg-info/top_level.txt +1 -0
- dualsense_py-0.3.0/examples/basic_usage.py +64 -0
- dualsense_py-0.3.0/pyproject.toml +52 -0
- dualsense_py-0.3.0/requirements.txt +3 -0
- dualsense_py-0.3.0/setup.cfg +4 -0
- dualsense_py-0.3.0/tests/conftest.py +41 -0
- dualsense_py-0.3.0/tests/test_dual_sense_controller.py +86 -0
- dualsense_py-0.3.0/tests/test_readable_value.py +70 -0
- 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,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,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,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
|