wsljoy 0.1.1__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.
- wsljoy-0.1.1/.github/workflows/publish.yml +32 -0
- wsljoy-0.1.1/.gitignore +9 -0
- wsljoy-0.1.1/.python-version +1 -0
- wsljoy-0.1.1/.vscode/c_cpp_properties.json +20 -0
- wsljoy-0.1.1/.vscode/settings.json +13 -0
- wsljoy-0.1.1/LICENSE +21 -0
- wsljoy-0.1.1/PKG-INFO +176 -0
- wsljoy-0.1.1/README.md +160 -0
- wsljoy-0.1.1/pyproject.toml +33 -0
- wsljoy-0.1.1/src/wsljoy/__init__.py +4 -0
- wsljoy-0.1.1/src/wsljoy/__main__.py +56 -0
- wsljoy-0.1.1/src/wsljoy/cli.py +68 -0
- wsljoy-0.1.1/src/wsljoy/controllers/__init__.py +2 -0
- wsljoy-0.1.1/src/wsljoy/controllers/common.py +62 -0
- wsljoy-0.1.1/src/wsljoy/controllers/ds4_hid.py +54 -0
- wsljoy-0.1.1/src/wsljoy/controllers/sdl.py +137 -0
- wsljoy-0.1.1/src/wsljoy/ds4.py +97 -0
- wsljoy-0.1.1/src/wsljoy/linux.py +215 -0
- wsljoy-0.1.1/src/wsljoy/protocol.py +109 -0
- wsljoy-0.1.1/src/wsljoy/setup_uinput.py +164 -0
- wsljoy-0.1.1/src/wsljoy/windows.py +162 -0
- wsljoy-0.1.1/tests/test_common.py +13 -0
- wsljoy-0.1.1/tests/test_ds4.py +75 -0
- wsljoy-0.1.1/tests/test_hid_connection.py +11 -0
- wsljoy-0.1.1/tests/test_main_cli.py +9 -0
- wsljoy-0.1.1/tests/test_protocol.py +20 -0
- wsljoy-0.1.1/tests/test_sdl.py +5 -0
- wsljoy-0.1.1/tests/test_setup_uinput.py +59 -0
- wsljoy-0.1.1/tests/test_windows_target.py +45 -0
- wsljoy-0.1.1/uv.lock +159 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Build and publish
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: pypi
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Check out repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v5
|
|
24
|
+
|
|
25
|
+
- name: Set up Python 3.10
|
|
26
|
+
run: uv python install 3.10
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
run: uv publish
|
wsljoy-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"configurations": [
|
|
3
|
+
{
|
|
4
|
+
"browse": {
|
|
5
|
+
"databaseFilename": "${workspaceFolder}/.vscode/browse.vc.db",
|
|
6
|
+
"limitSymbolsToIncludedHeaders": false
|
|
7
|
+
},
|
|
8
|
+
"includePath": [
|
|
9
|
+
"/opt/ros/humble/include/**",
|
|
10
|
+
"/usr/include/**"
|
|
11
|
+
],
|
|
12
|
+
"name": "ros2",
|
|
13
|
+
"intelliSenseMode": "gcc-x64",
|
|
14
|
+
"compilerPath": "/usr/bin/gcc",
|
|
15
|
+
"cStandard": "gnu11",
|
|
16
|
+
"cppStandard": "c++17"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"version": 4
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ROS2.distro": "humble",
|
|
3
|
+
"python.autoComplete.extraPaths": [
|
|
4
|
+
"/home/avula/workspaces/waywiser_ws/.venv/lib/python3.10/site-packages",
|
|
5
|
+
"/opt/ros/humble/lib/python3.10/site-packages",
|
|
6
|
+
"/opt/ros/humble/local/lib/python3.10/dist-packages"
|
|
7
|
+
],
|
|
8
|
+
"python.analysis.extraPaths": [
|
|
9
|
+
"/home/avula/workspaces/waywiser_ws/.venv/lib/python3.10/site-packages",
|
|
10
|
+
"/opt/ros/humble/lib/python3.10/site-packages",
|
|
11
|
+
"/opt/ros/humble/local/lib/python3.10/dist-packages"
|
|
12
|
+
]
|
|
13
|
+
}
|
wsljoy-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramana Avula
|
|
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.
|
wsljoy-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wsljoy
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Mirror a Windows USB or Bluetooth game controller into WSL2 as a Linux joystick.
|
|
5
|
+
Author: wsljoy contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <3.11,>=3.10
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
11
|
+
Provides-Extra: linux
|
|
12
|
+
Provides-Extra: windows
|
|
13
|
+
Requires-Dist: hidapi>=0.14; extra == 'windows'
|
|
14
|
+
Requires-Dist: pygame>=2.5; extra == 'windows'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# wsljoy
|
|
18
|
+
|
|
19
|
+
`wsljoy` mirrors a controller connected to Windows over USB or Bluetooth into WSL2 as a normal Linux joystick device.
|
|
20
|
+
|
|
21
|
+
## Python
|
|
22
|
+
|
|
23
|
+
Use Python 3.10. The project is pinned to Python 3.10 because the Windows controller dependencies have the most reliable wheel support there.
|
|
24
|
+
|
|
25
|
+
## Windows Host
|
|
26
|
+
|
|
27
|
+
With `uv`:
|
|
28
|
+
|
|
29
|
+
```cmd
|
|
30
|
+
uv python install 3.10
|
|
31
|
+
uv sync --extra windows
|
|
32
|
+
uv run python -m wsljoy list
|
|
33
|
+
uv run python -m wsljoy host
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With standard `venv` and `pip`:
|
|
37
|
+
|
|
38
|
+
```cmd
|
|
39
|
+
py -3.10 -m venv .venv
|
|
40
|
+
.venv\Scripts\activate.bat
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
python -m pip install -e ".[windows]"
|
|
43
|
+
python -m wsljoy list
|
|
44
|
+
python -m wsljoy host
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## WSL2 Guest
|
|
48
|
+
|
|
49
|
+
With `uv`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync --extra linux
|
|
53
|
+
uv run python -m wsljoy setup-uinput
|
|
54
|
+
uv run python -m wsljoy guest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
With standard `venv` and `pip`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python3.10 -m venv .venv
|
|
61
|
+
. .venv/bin/activate
|
|
62
|
+
python -m pip install --upgrade pip
|
|
63
|
+
python -m pip install -e ".[linux]"
|
|
64
|
+
python -m wsljoy setup-uinput
|
|
65
|
+
python -m wsljoy guest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`setup-uinput` first checks whether `/dev/uinput` is already writable. It only asks for sudo when your user needs to be added to the `input` group; after that, start a new WSL shell or run `newgrp input`, then run the guest without sudo.
|
|
69
|
+
|
|
70
|
+
It creates a virtual Linux input device through `/dev/uinput` using the Linux uinput API. The `/dev/input/event*` device appears after the guest receives the first packet from Windows. The `/dev/input/js*` device appears when the Linux `joydev` module is available and loaded.
|
|
71
|
+
|
|
72
|
+
Check the guest side:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ls -l /dev/uinput
|
|
76
|
+
groups
|
|
77
|
+
python -m wsljoy guest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In a second WSL terminal, after the Windows host is running:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
ls -l /dev/input/event* /dev/input/js*
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If `event*` exists but `js*` does not, try:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
sudo modprobe joydev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Network Setup
|
|
93
|
+
|
|
94
|
+
By default the Windows host targets WSL2. On Windows, `wsljoy` resolves the current WSL2 IP by running `wsl.exe hostname -I`.
|
|
95
|
+
|
|
96
|
+
With `uv`:
|
|
97
|
+
|
|
98
|
+
```cmd
|
|
99
|
+
uv run python -m wsljoy host
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run python -m wsljoy guest --listen 0.0.0.0 --port 27414
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
With an activated venv:
|
|
107
|
+
|
|
108
|
+
```cmd
|
|
109
|
+
python -m wsljoy host
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m wsljoy guest --listen 0.0.0.0 --port 27414
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you have multiple WSL distros, name the one running the guest:
|
|
117
|
+
|
|
118
|
+
```cmd
|
|
119
|
+
python -m wsljoy host --wsl-distro Ubuntu-22.04
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To bypass WSL auto-resolution, pass an explicit IP address:
|
|
123
|
+
|
|
124
|
+
```cmd
|
|
125
|
+
python -m wsljoy host --target 172.25.121.7
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Controller Support
|
|
129
|
+
|
|
130
|
+
The Windows host has two reader backends:
|
|
131
|
+
|
|
132
|
+
- `ds4-hid`: exact DualShock 4 HID parser for USB and Bluetooth, preferred automatically for DS4 devices.
|
|
133
|
+
- `sdl`: generic SDL/pygame joystick reader, used for common Xbox, PlayStation, 8BitDo, Nintendo, Logitech, PowerA, Razer, and compatible controllers.
|
|
134
|
+
|
|
135
|
+
If the same controller is visible through both APIs, `list` shows the exact backend and hides the duplicate SDL entry. `host --backend auto` prefers `ds4-hid`; use `host --backend sdl` to force SDL.
|
|
136
|
+
|
|
137
|
+
Auto detection is the default:
|
|
138
|
+
|
|
139
|
+
```cmd
|
|
140
|
+
python -m wsljoy list
|
|
141
|
+
python -m wsljoy host --backend auto
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Force the generic backend:
|
|
145
|
+
|
|
146
|
+
```cmd
|
|
147
|
+
python -m wsljoy host --backend sdl
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The exact DS4 path supports:
|
|
151
|
+
|
|
152
|
+
- Sony vendor ID `054c`
|
|
153
|
+
- Product IDs `05c4` and `09cc`
|
|
154
|
+
- USB reports and Bluetooth reports
|
|
155
|
+
|
|
156
|
+
The SDL path covers many usual-suspect controllers but depends on the mapping SDL exposes for that device. The WSL side creates a virtual Linux gamepad with the detected vendor/product IDs when available and Linux-style axes/buttons available through `/dev/input/event*` and `/dev/input/js*`.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
With `uv`:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv python install 3.10
|
|
164
|
+
uv sync --all-extras --dev
|
|
165
|
+
uv run pytest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
With standard `venv` and `pip`:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python3.10 -m venv .venv
|
|
172
|
+
. .venv/bin/activate
|
|
173
|
+
python -m pip install --upgrade pip
|
|
174
|
+
python -m pip install -e ".[dev]"
|
|
175
|
+
python -m pytest
|
|
176
|
+
```
|
wsljoy-0.1.1/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# wsljoy
|
|
2
|
+
|
|
3
|
+
`wsljoy` mirrors a controller connected to Windows over USB or Bluetooth into WSL2 as a normal Linux joystick device.
|
|
4
|
+
|
|
5
|
+
## Python
|
|
6
|
+
|
|
7
|
+
Use Python 3.10. The project is pinned to Python 3.10 because the Windows controller dependencies have the most reliable wheel support there.
|
|
8
|
+
|
|
9
|
+
## Windows Host
|
|
10
|
+
|
|
11
|
+
With `uv`:
|
|
12
|
+
|
|
13
|
+
```cmd
|
|
14
|
+
uv python install 3.10
|
|
15
|
+
uv sync --extra windows
|
|
16
|
+
uv run python -m wsljoy list
|
|
17
|
+
uv run python -m wsljoy host
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
With standard `venv` and `pip`:
|
|
21
|
+
|
|
22
|
+
```cmd
|
|
23
|
+
py -3.10 -m venv .venv
|
|
24
|
+
.venv\Scripts\activate.bat
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
python -m pip install -e ".[windows]"
|
|
27
|
+
python -m wsljoy list
|
|
28
|
+
python -m wsljoy host
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## WSL2 Guest
|
|
32
|
+
|
|
33
|
+
With `uv`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync --extra linux
|
|
37
|
+
uv run python -m wsljoy setup-uinput
|
|
38
|
+
uv run python -m wsljoy guest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With standard `venv` and `pip`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python3.10 -m venv .venv
|
|
45
|
+
. .venv/bin/activate
|
|
46
|
+
python -m pip install --upgrade pip
|
|
47
|
+
python -m pip install -e ".[linux]"
|
|
48
|
+
python -m wsljoy setup-uinput
|
|
49
|
+
python -m wsljoy guest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`setup-uinput` first checks whether `/dev/uinput` is already writable. It only asks for sudo when your user needs to be added to the `input` group; after that, start a new WSL shell or run `newgrp input`, then run the guest without sudo.
|
|
53
|
+
|
|
54
|
+
It creates a virtual Linux input device through `/dev/uinput` using the Linux uinput API. The `/dev/input/event*` device appears after the guest receives the first packet from Windows. The `/dev/input/js*` device appears when the Linux `joydev` module is available and loaded.
|
|
55
|
+
|
|
56
|
+
Check the guest side:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ls -l /dev/uinput
|
|
60
|
+
groups
|
|
61
|
+
python -m wsljoy guest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In a second WSL terminal, after the Windows host is running:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ls -l /dev/input/event* /dev/input/js*
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If `event*` exists but `js*` does not, try:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sudo modprobe joydev
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Network Setup
|
|
77
|
+
|
|
78
|
+
By default the Windows host targets WSL2. On Windows, `wsljoy` resolves the current WSL2 IP by running `wsl.exe hostname -I`.
|
|
79
|
+
|
|
80
|
+
With `uv`:
|
|
81
|
+
|
|
82
|
+
```cmd
|
|
83
|
+
uv run python -m wsljoy host
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv run python -m wsljoy guest --listen 0.0.0.0 --port 27414
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
With an activated venv:
|
|
91
|
+
|
|
92
|
+
```cmd
|
|
93
|
+
python -m wsljoy host
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m wsljoy guest --listen 0.0.0.0 --port 27414
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If you have multiple WSL distros, name the one running the guest:
|
|
101
|
+
|
|
102
|
+
```cmd
|
|
103
|
+
python -m wsljoy host --wsl-distro Ubuntu-22.04
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
To bypass WSL auto-resolution, pass an explicit IP address:
|
|
107
|
+
|
|
108
|
+
```cmd
|
|
109
|
+
python -m wsljoy host --target 172.25.121.7
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Controller Support
|
|
113
|
+
|
|
114
|
+
The Windows host has two reader backends:
|
|
115
|
+
|
|
116
|
+
- `ds4-hid`: exact DualShock 4 HID parser for USB and Bluetooth, preferred automatically for DS4 devices.
|
|
117
|
+
- `sdl`: generic SDL/pygame joystick reader, used for common Xbox, PlayStation, 8BitDo, Nintendo, Logitech, PowerA, Razer, and compatible controllers.
|
|
118
|
+
|
|
119
|
+
If the same controller is visible through both APIs, `list` shows the exact backend and hides the duplicate SDL entry. `host --backend auto` prefers `ds4-hid`; use `host --backend sdl` to force SDL.
|
|
120
|
+
|
|
121
|
+
Auto detection is the default:
|
|
122
|
+
|
|
123
|
+
```cmd
|
|
124
|
+
python -m wsljoy list
|
|
125
|
+
python -m wsljoy host --backend auto
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Force the generic backend:
|
|
129
|
+
|
|
130
|
+
```cmd
|
|
131
|
+
python -m wsljoy host --backend sdl
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The exact DS4 path supports:
|
|
135
|
+
|
|
136
|
+
- Sony vendor ID `054c`
|
|
137
|
+
- Product IDs `05c4` and `09cc`
|
|
138
|
+
- USB reports and Bluetooth reports
|
|
139
|
+
|
|
140
|
+
The SDL path covers many usual-suspect controllers but depends on the mapping SDL exposes for that device. The WSL side creates a virtual Linux gamepad with the detected vendor/product IDs when available and Linux-style axes/buttons available through `/dev/input/event*` and `/dev/input/js*`.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
With `uv`:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
uv python install 3.10
|
|
148
|
+
uv sync --all-extras --dev
|
|
149
|
+
uv run pytest
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
With standard `venv` and `pip`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python3.10 -m venv .venv
|
|
156
|
+
. .venv/bin/activate
|
|
157
|
+
python -m pip install --upgrade pip
|
|
158
|
+
python -m pip install -e ".[dev]"
|
|
159
|
+
python -m pytest
|
|
160
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wsljoy"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Mirror a Windows USB or Bluetooth game controller into WSL2 as a Linux joystick."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "wsljoy contributors" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
windows = ["hidapi>=0.14", "pygame>=2.5"]
|
|
17
|
+
linux = []
|
|
18
|
+
dev = ["pytest>=8"]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = ["pytest>=8"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
wsljoy-host = "wsljoy.cli:host_main"
|
|
25
|
+
wsljoy-guest = "wsljoy.cli:guest_main"
|
|
26
|
+
wsljoy-list = "wsljoy.cli:list_main"
|
|
27
|
+
wsljoy-setup-uinput = "wsljoy.setup_uinput:main"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/wsljoy"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
COMMANDS = {
|
|
9
|
+
"list": "List supported Windows controllers.",
|
|
10
|
+
"host": "Send Windows controller state to WSL2.",
|
|
11
|
+
"guest": "Create a virtual gamepad in Linux/WSL2.",
|
|
12
|
+
"setup-uinput": "Configure /dev/uinput permissions.",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_handler(command: str) -> Callable[[list[str] | None], None]:
|
|
17
|
+
if command == "list":
|
|
18
|
+
from .cli import list_main
|
|
19
|
+
|
|
20
|
+
return list_main
|
|
21
|
+
if command == "host":
|
|
22
|
+
from .cli import host_main
|
|
23
|
+
|
|
24
|
+
return host_main
|
|
25
|
+
if command == "guest":
|
|
26
|
+
from .cli import guest_main
|
|
27
|
+
|
|
28
|
+
return guest_main
|
|
29
|
+
if command == "setup-uinput":
|
|
30
|
+
from .setup_uinput import main as setup_uinput_main
|
|
31
|
+
|
|
32
|
+
return setup_uinput_main
|
|
33
|
+
choices = ", ".join(COMMANDS)
|
|
34
|
+
raise SystemExit(f"unknown command: {command}\nchoose one of: {choices}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> None:
|
|
38
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
39
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
40
|
+
parser = argparse.ArgumentParser(prog="python -m wsljoy")
|
|
41
|
+
subcommands = parser.add_subparsers(dest="command")
|
|
42
|
+
for name, help_text in COMMANDS.items():
|
|
43
|
+
subcommands.add_parser(name, help=help_text)
|
|
44
|
+
parser.print_help()
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
command = args.pop(0)
|
|
48
|
+
handler = _load_handler(command)
|
|
49
|
+
try:
|
|
50
|
+
handler(args)
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
print("\nStopped.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_main(argv: list[str] | None = None) -> None:
|
|
7
|
+
from .controllers.common import decode_hid_path
|
|
8
|
+
from .windows import list_controllers
|
|
9
|
+
|
|
10
|
+
devices = list_controllers()
|
|
11
|
+
if not devices:
|
|
12
|
+
print("No supported game controllers found.")
|
|
13
|
+
return
|
|
14
|
+
for index, device in enumerate(devices):
|
|
15
|
+
product = device.get("product_string") or "Wireless Controller"
|
|
16
|
+
path = decode_hid_path(device.get("path"))
|
|
17
|
+
vendor_id = int(device.get("vendor_id") or 0)
|
|
18
|
+
product_id = int(device.get("product_id") or 0)
|
|
19
|
+
print(
|
|
20
|
+
f"{index}: [{device.get('backend', 'unknown')}] {product} "
|
|
21
|
+
f"vid={vendor_id:04x} pid={product_id:04x} "
|
|
22
|
+
f"path={path}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def host_main(argv: list[str] | None = None) -> None:
|
|
27
|
+
parser = argparse.ArgumentParser(description="Send Windows controller state to WSL2.")
|
|
28
|
+
parser.add_argument("--target", default="wsl", help="WSL/Linux UDP target address. Defaults to `wsl`, resolved via wsl.exe; explicit IPs are also allowed.")
|
|
29
|
+
parser.add_argument("--wsl-distro", help="WSL distro name to use when --target is wsl/wsl2.")
|
|
30
|
+
parser.add_argument("--port", type=int, default=27414, help="UDP target port.")
|
|
31
|
+
parser.add_argument("--path", help="Controller path from wsljoy-list.")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--backend",
|
|
34
|
+
choices=("auto", "ds4-hid", "sdl"),
|
|
35
|
+
default="auto",
|
|
36
|
+
help="Controller reader backend.",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument("--rate", type=float, default=250.0, help="Maximum send rate in Hz.")
|
|
39
|
+
args = parser.parse_args(argv)
|
|
40
|
+
|
|
41
|
+
from .windows import run_host
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
run_host(
|
|
45
|
+
target=args.target,
|
|
46
|
+
port=args.port,
|
|
47
|
+
path=args.path,
|
|
48
|
+
backend=args.backend,
|
|
49
|
+
rate_limit_hz=args.rate,
|
|
50
|
+
wsl_distro=args.wsl_distro,
|
|
51
|
+
)
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
print("\nStopped.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def guest_main(argv: list[str] | None = None) -> None:
|
|
57
|
+
parser = argparse.ArgumentParser(description="Create a virtual gamepad in Linux/WSL2.")
|
|
58
|
+
parser.add_argument("--listen", default="0.0.0.0", help="UDP listen address.")
|
|
59
|
+
parser.add_argument("--port", type=int, default=27414, help="UDP listen port.")
|
|
60
|
+
parser.add_argument("--stale-after", type=float, default=1.0, help="Neutralize after silence.")
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
from .linux import run_guest
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
run_guest(listen=args.listen, port=args.port, stale_after=args.stale_after)
|
|
67
|
+
except KeyboardInterrupt:
|
|
68
|
+
print("\nStopped.")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
KNOWN_GAMEPAD_VENDORS = {
|
|
5
|
+
0x044F: "Thrustmaster",
|
|
6
|
+
0x045E: "Microsoft",
|
|
7
|
+
0x046D: "Logitech",
|
|
8
|
+
0x054C: "Sony",
|
|
9
|
+
0x057E: "Nintendo",
|
|
10
|
+
0x0738: "Mad Catz",
|
|
11
|
+
0x0E6F: "PDP",
|
|
12
|
+
0x1532: "Razer",
|
|
13
|
+
0x20D6: "PowerA",
|
|
14
|
+
0x24C6: "PowerA",
|
|
15
|
+
0x2DC8: "8BitDo",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_known_gamepad(device: dict) -> bool:
|
|
20
|
+
vendor_id = int(device.get("vendor_id") or 0)
|
|
21
|
+
usage_page = int(device.get("usage_page") or 0)
|
|
22
|
+
usage = int(device.get("usage") or 0)
|
|
23
|
+
product = str(device.get("product_string") or "").lower()
|
|
24
|
+
manufacturer = str(device.get("manufacturer_string") or "").lower()
|
|
25
|
+
|
|
26
|
+
if vendor_id in KNOWN_GAMEPAD_VENDORS:
|
|
27
|
+
return True
|
|
28
|
+
if usage_page == 0x01 and usage in {0x04, 0x05}:
|
|
29
|
+
return True
|
|
30
|
+
return any(
|
|
31
|
+
token in f"{manufacturer} {product}"
|
|
32
|
+
for token in (
|
|
33
|
+
"xbox",
|
|
34
|
+
"dualshock",
|
|
35
|
+
"dualsense",
|
|
36
|
+
"wireless controller",
|
|
37
|
+
"8bitdo",
|
|
38
|
+
"gamepad",
|
|
39
|
+
"controller",
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def decode_hid_path(path: object) -> str:
|
|
45
|
+
if isinstance(path, bytes):
|
|
46
|
+
return path.decode(errors="replace")
|
|
47
|
+
return str(path or "")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def hid_connection(device: dict) -> str:
|
|
51
|
+
bus_type = int(device.get("bus_type") or 0)
|
|
52
|
+
if bus_type == 1:
|
|
53
|
+
return "usb"
|
|
54
|
+
if bus_type == 2:
|
|
55
|
+
return "bluetooth"
|
|
56
|
+
|
|
57
|
+
path = decode_hid_path(device.get("path")).lower()
|
|
58
|
+
if "bth" in path or "bluetooth" in path:
|
|
59
|
+
return "bluetooth"
|
|
60
|
+
if "usb" in path or "vid_" in path:
|
|
61
|
+
return "usb"
|
|
62
|
+
return "unknown"
|