swbt-python 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- swbt/__init__.py +10 -0
- swbt/_testing/gamepad.py +72 -1
- swbt/gamepad/__init__.py +12 -0
- swbt/gamepad/core.py +105 -28
- swbt/gamepad/interface.py +55 -26
- swbt/gamepad/output.py +16 -5
- swbt/gamepad/runtime.py +161 -20
- swbt/imu.py +116 -0
- swbt/input.py +122 -0
- swbt/protocol/imu_report.py +186 -0
- swbt/protocol/input_report.py +21 -18
- swbt/protocol/profiles/base.py +8 -0
- swbt/protocol/profiles/joycon.py +3 -3
- swbt/protocol/profiles/pro_controller.py +1 -1
- swbt/protocol/session.py +112 -0
- swbt/protocol/spi.py +12 -0
- swbt/protocol/subcommand.py +45 -40
- swbt/report_loop.py +128 -43
- {swbt_python-0.2.0.dist-info → swbt_python-0.4.0.dist-info}/METADATA +2 -3
- {swbt_python-0.2.0.dist-info → swbt_python-0.4.0.dist-info}/RECORD +23 -20
- {swbt_python-0.2.0.dist-info → swbt_python-0.4.0.dist-info}/WHEEL +0 -0
- {swbt_python-0.2.0.dist-info → swbt_python-0.4.0.dist-info}/entry_points.txt +0 -0
- {swbt_python-0.2.0.dist-info → swbt_python-0.4.0.dist-info}/licenses/LICENSE +0 -0
swbt/__init__.py
CHANGED
|
@@ -15,8 +15,13 @@ from swbt.errors import (
|
|
|
15
15
|
)
|
|
16
16
|
from swbt.gamepad import (
|
|
17
17
|
ConnectionResult,
|
|
18
|
+
DirectJoyConL,
|
|
19
|
+
DirectJoyConR,
|
|
20
|
+
DirectProController,
|
|
21
|
+
DirectSwitchGamepad,
|
|
18
22
|
JoyConL,
|
|
19
23
|
JoyConR,
|
|
24
|
+
PeriodicSwitchGamepad,
|
|
20
25
|
ProController,
|
|
21
26
|
SwitchGamepad,
|
|
22
27
|
)
|
|
@@ -33,6 +38,10 @@ __all__ = (
|
|
|
33
38
|
"ConnectionTimeoutError",
|
|
34
39
|
"ControllerColors",
|
|
35
40
|
"DiagnosticsConfig",
|
|
41
|
+
"DirectJoyConL",
|
|
42
|
+
"DirectJoyConR",
|
|
43
|
+
"DirectProController",
|
|
44
|
+
"DirectSwitchGamepad",
|
|
36
45
|
"GamepadStatus",
|
|
37
46
|
"IMUFrame",
|
|
38
47
|
"InputState",
|
|
@@ -40,6 +49,7 @@ __all__ = (
|
|
|
40
49
|
"InvalidKeyStoreError",
|
|
41
50
|
"JoyConL",
|
|
42
51
|
"JoyConR",
|
|
52
|
+
"PeriodicSwitchGamepad",
|
|
43
53
|
"ProController",
|
|
44
54
|
"Stick",
|
|
45
55
|
"SwbtError",
|
swbt/_testing/gamepad.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
"""Internal gamepad constructors for tests that inject fake transports."""
|
|
2
2
|
|
|
3
|
-
from swbt import
|
|
3
|
+
from swbt import (
|
|
4
|
+
ControllerColors,
|
|
5
|
+
DiagnosticsConfig,
|
|
6
|
+
DirectJoyConL,
|
|
7
|
+
DirectJoyConR,
|
|
8
|
+
DirectProController,
|
|
9
|
+
JoyConL,
|
|
10
|
+
JoyConR,
|
|
11
|
+
ProController,
|
|
12
|
+
)
|
|
4
13
|
from swbt.gamepad._config import _SwitchGamepadConfig
|
|
5
14
|
from swbt.protocol.profiles.joycon import JoyConLeftProfile, JoyConRightProfile
|
|
6
15
|
from swbt.transport.base import HidDeviceTransport
|
|
@@ -72,3 +81,65 @@ def make_joycon_r(
|
|
|
72
81
|
diagnostics=diagnostics,
|
|
73
82
|
transport=transport,
|
|
74
83
|
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def make_direct_pro_controller(
|
|
87
|
+
*,
|
|
88
|
+
transport: HidDeviceTransport,
|
|
89
|
+
adapter: str | None = None,
|
|
90
|
+
key_store_path: str | None = None,
|
|
91
|
+
controller_colors: ControllerColors | None = None,
|
|
92
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
93
|
+
) -> DirectProController:
|
|
94
|
+
"""Create a Direct Pro Controller with an injected transport for tests."""
|
|
95
|
+
return DirectProController._from_config(
|
|
96
|
+
_SwitchGamepadConfig(
|
|
97
|
+
adapter=adapter,
|
|
98
|
+
key_store_path=key_store_path,
|
|
99
|
+
controller_colors=controller_colors,
|
|
100
|
+
),
|
|
101
|
+
diagnostics=diagnostics,
|
|
102
|
+
transport=transport,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def make_direct_joycon_l(
|
|
107
|
+
*,
|
|
108
|
+
transport: HidDeviceTransport,
|
|
109
|
+
adapter: str | None = None,
|
|
110
|
+
key_store_path: str | None = None,
|
|
111
|
+
controller_colors: ControllerColors | None = None,
|
|
112
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
113
|
+
) -> DirectJoyConL:
|
|
114
|
+
"""Create a Direct Joy-Con L with an injected transport for tests."""
|
|
115
|
+
return DirectJoyConL._from_config(
|
|
116
|
+
_SwitchGamepadConfig(
|
|
117
|
+
adapter=adapter,
|
|
118
|
+
key_store_path=key_store_path,
|
|
119
|
+
controller_colors=controller_colors,
|
|
120
|
+
profile=JoyConLeftProfile(),
|
|
121
|
+
),
|
|
122
|
+
diagnostics=diagnostics,
|
|
123
|
+
transport=transport,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def make_direct_joycon_r(
|
|
128
|
+
*,
|
|
129
|
+
transport: HidDeviceTransport,
|
|
130
|
+
adapter: str | None = None,
|
|
131
|
+
key_store_path: str | None = None,
|
|
132
|
+
controller_colors: ControllerColors | None = None,
|
|
133
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
134
|
+
) -> DirectJoyConR:
|
|
135
|
+
"""Create a Direct Joy-Con R with an injected transport for tests."""
|
|
136
|
+
return DirectJoyConR._from_config(
|
|
137
|
+
_SwitchGamepadConfig(
|
|
138
|
+
adapter=adapter,
|
|
139
|
+
key_store_path=key_store_path,
|
|
140
|
+
controller_colors=controller_colors,
|
|
141
|
+
profile=JoyConRightProfile(),
|
|
142
|
+
),
|
|
143
|
+
diagnostics=diagnostics,
|
|
144
|
+
transport=transport,
|
|
145
|
+
)
|
swbt/gamepad/__init__.py
CHANGED
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
from swbt.gamepad.connection import ConnectionResult, ConnectionStatus
|
|
4
4
|
from swbt.gamepad.constants import DISCONNECT_REQUEST_TIMEOUT_SECONDS
|
|
5
5
|
from swbt.gamepad.core import (
|
|
6
|
+
DirectJoyConL,
|
|
7
|
+
DirectJoyConR,
|
|
8
|
+
DirectProController,
|
|
6
9
|
JoyConL,
|
|
7
10
|
JoyConR,
|
|
8
11
|
ProController,
|
|
12
|
+
)
|
|
13
|
+
from swbt.gamepad.interface import (
|
|
14
|
+
DirectSwitchGamepad,
|
|
15
|
+
PeriodicSwitchGamepad,
|
|
9
16
|
SwitchGamepad,
|
|
10
17
|
)
|
|
11
18
|
|
|
@@ -13,8 +20,13 @@ __all__ = (
|
|
|
13
20
|
"DISCONNECT_REQUEST_TIMEOUT_SECONDS",
|
|
14
21
|
"ConnectionResult",
|
|
15
22
|
"ConnectionStatus",
|
|
23
|
+
"DirectJoyConL",
|
|
24
|
+
"DirectJoyConR",
|
|
25
|
+
"DirectProController",
|
|
26
|
+
"DirectSwitchGamepad",
|
|
16
27
|
"JoyConL",
|
|
17
28
|
"JoyConR",
|
|
29
|
+
"PeriodicSwitchGamepad",
|
|
18
30
|
"ProController",
|
|
19
31
|
"SwitchGamepad",
|
|
20
32
|
)
|
swbt/gamepad/core.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""Public gamepad API."""
|
|
2
2
|
|
|
3
|
-
from typing import Self
|
|
3
|
+
from typing import ClassVar, Literal, Self
|
|
4
4
|
|
|
5
5
|
from swbt.diagnostics import DiagnosticsConfig, GamepadStatus
|
|
6
6
|
from swbt.errors import InvalidInputError
|
|
7
7
|
from swbt.gamepad._config import _ControllerSpec, _SwitchGamepadConfig
|
|
8
8
|
from swbt.gamepad.connection import ConnectionResult
|
|
9
|
-
from swbt.gamepad.interface import
|
|
9
|
+
from swbt.gamepad.interface import (
|
|
10
|
+
DirectSwitchGamepad,
|
|
11
|
+
PeriodicSwitchGamepad,
|
|
12
|
+
)
|
|
10
13
|
from swbt.gamepad.output import OutputReportDispatcher
|
|
11
14
|
from swbt.gamepad.runtime import ControllerRuntime
|
|
12
15
|
from swbt.input import Button, IMUFrame, InputState, Stick
|
|
@@ -17,7 +20,7 @@ from swbt.state_store import InputStateStore
|
|
|
17
20
|
from swbt.transport.base import HidDeviceTransport
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
class _RuntimeBackedGamepad
|
|
23
|
+
class _RuntimeBackedGamepad:
|
|
21
24
|
"""Runtime-backed concrete gamepad base.
|
|
22
25
|
|
|
23
26
|
The object owns the public API surface and delegates stateful
|
|
@@ -25,6 +28,7 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
_controller_spec = _ControllerSpec(profile=default_controller_profile())
|
|
31
|
+
_reporting_mode: ClassVar[Literal["periodic", "direct"]] = "periodic"
|
|
28
32
|
|
|
29
33
|
def __init__(
|
|
30
34
|
self,
|
|
@@ -65,6 +69,7 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
65
69
|
self._runtime = ControllerRuntime.from_config(
|
|
66
70
|
config,
|
|
67
71
|
diagnostics=diagnostics,
|
|
72
|
+
reporting_mode=self._reporting_mode,
|
|
68
73
|
transport=transport,
|
|
69
74
|
)
|
|
70
75
|
|
|
@@ -113,8 +118,9 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
113
118
|
async def open(self) -> None:
|
|
114
119
|
"""Open the configured transport.
|
|
115
120
|
|
|
116
|
-
Opening prepares transport callbacks, diagnostics metadata, and the
|
|
117
|
-
|
|
121
|
+
Opening prepares transport callbacks, diagnostics metadata, and the
|
|
122
|
+
reporting-type resources. It does not start HID advertising, pairing,
|
|
123
|
+
or active reconnect.
|
|
118
124
|
|
|
119
125
|
Raises:
|
|
120
126
|
TransportOpenError: Raised by the transport when the adapter cannot be opened.
|
|
@@ -218,22 +224,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
218
224
|
Args:
|
|
219
225
|
buttons: Buttons to add to the current button set.
|
|
220
226
|
|
|
221
|
-
|
|
227
|
+
A periodic controller commits local state. A direct controller sends one
|
|
228
|
+
input report and commits only after transmission succeeds.
|
|
222
229
|
"""
|
|
223
230
|
await self._runtime.press(*buttons)
|
|
224
231
|
|
|
225
|
-
async def apply(self, state: InputState) -> None:
|
|
226
|
-
"""Replace the current input state without immediate transmission.
|
|
227
|
-
|
|
228
|
-
Args:
|
|
229
|
-
state: Complete input state to commit.
|
|
230
|
-
|
|
231
|
-
This updates local state only and does not send an immediate input report.
|
|
232
|
-
"""
|
|
233
|
-
await self._runtime.apply(state)
|
|
234
|
-
|
|
235
232
|
async def sticks(self, *, left: Stick | None = None, right: Stick | None = None) -> None:
|
|
236
|
-
"""Replace one or both stick positions
|
|
233
|
+
"""Replace one or both stick positions according to the reporting type.
|
|
237
234
|
|
|
238
235
|
Args:
|
|
239
236
|
left: Optional replacement for the left stick.
|
|
@@ -242,12 +239,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
242
239
|
Raises:
|
|
243
240
|
InvalidInputError: ``left`` or ``right`` is not a ``Stick``.
|
|
244
241
|
|
|
245
|
-
|
|
242
|
+
A periodic controller commits local state. A direct controller sends one
|
|
243
|
+
input report and commits only after transmission succeeds.
|
|
246
244
|
"""
|
|
247
245
|
await self._runtime.sticks(left=left, right=right)
|
|
248
246
|
|
|
249
247
|
async def lstick(self, stick: Stick) -> None:
|
|
250
|
-
"""Replace the left stick position
|
|
248
|
+
"""Replace the left stick position according to the reporting type.
|
|
251
249
|
|
|
252
250
|
Args:
|
|
253
251
|
stick: Replacement for the left stick.
|
|
@@ -255,12 +253,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
255
253
|
Raises:
|
|
256
254
|
InvalidInputError: ``stick`` is not a ``Stick``.
|
|
257
255
|
|
|
258
|
-
|
|
256
|
+
A periodic controller commits local state. A direct controller sends one
|
|
257
|
+
input report and commits only after transmission succeeds.
|
|
259
258
|
"""
|
|
260
259
|
await self._runtime.lstick(stick)
|
|
261
260
|
|
|
262
261
|
async def rstick(self, stick: Stick) -> None:
|
|
263
|
-
"""Replace the right stick position
|
|
262
|
+
"""Replace the right stick position according to the reporting type.
|
|
264
263
|
|
|
265
264
|
Args:
|
|
266
265
|
stick: Replacement for the right stick.
|
|
@@ -268,12 +267,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
268
267
|
Raises:
|
|
269
268
|
InvalidInputError: ``stick`` is not a ``Stick``.
|
|
270
269
|
|
|
271
|
-
|
|
270
|
+
A periodic controller commits local state. A direct controller sends one
|
|
271
|
+
input report and commits only after transmission succeeds.
|
|
272
272
|
"""
|
|
273
273
|
await self._runtime.rstick(stick)
|
|
274
274
|
|
|
275
275
|
async def imu(self, *frames: IMUFrame) -> None:
|
|
276
|
-
"""Replace IMU frames
|
|
276
|
+
"""Replace IMU frames according to the reporting type.
|
|
277
277
|
|
|
278
278
|
Args:
|
|
279
279
|
frames: One ``IMUFrame`` to repeat across all three IMU slots, or exactly
|
|
@@ -293,12 +293,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
293
293
|
Args:
|
|
294
294
|
buttons: Buttons to remove from the current button set.
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
A periodic controller commits local state. A direct controller sends one
|
|
297
|
+
input report and commits only after transmission succeeds.
|
|
297
298
|
"""
|
|
298
299
|
await self._runtime.release(*buttons)
|
|
299
300
|
|
|
300
301
|
async def neutral(self) -> None:
|
|
301
|
-
"""
|
|
302
|
+
"""Apply ``InputState.neutral()`` according to the reporting type."""
|
|
302
303
|
await self._runtime.neutral()
|
|
303
304
|
|
|
304
305
|
async def tap(self, *buttons: Button, duration: float = 0.08) -> None:
|
|
@@ -327,19 +328,77 @@ class _RuntimeBackedGamepad(SwitchGamepad):
|
|
|
327
328
|
def snapshot(self) -> InputState:
|
|
328
329
|
"""Return the latest committed input state.
|
|
329
330
|
|
|
331
|
+
A periodic controller returns its latest local state. A direct
|
|
332
|
+
controller returns the last state sent successfully.
|
|
333
|
+
|
|
330
334
|
Returns:
|
|
331
335
|
InputState: Immutable snapshot of the current input state.
|
|
332
336
|
"""
|
|
333
337
|
return self._runtime.snapshot()
|
|
334
338
|
|
|
335
339
|
|
|
336
|
-
class
|
|
340
|
+
class _PeriodicRuntimeBackedGamepad(_RuntimeBackedGamepad, PeriodicSwitchGamepad):
|
|
341
|
+
"""Runtime-backed gamepad with library-owned periodic input transmission."""
|
|
342
|
+
|
|
343
|
+
async def apply(self, state: InputState) -> None:
|
|
344
|
+
"""Replace the current input state without immediate transmission.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
state: Complete input state to commit.
|
|
348
|
+
|
|
349
|
+
This updates local state only and does not send an immediate input report.
|
|
350
|
+
"""
|
|
351
|
+
await self._runtime.apply(state)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
|
|
355
|
+
"""Runtime-backed gamepad with caller-owned input transmission."""
|
|
356
|
+
|
|
357
|
+
_reporting_mode = "direct"
|
|
358
|
+
|
|
359
|
+
def __init__(
|
|
360
|
+
self,
|
|
361
|
+
*,
|
|
362
|
+
adapter: str | None = None,
|
|
363
|
+
key_store_path: str | None = None,
|
|
364
|
+
controller_colors: ControllerColors | None = None,
|
|
365
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Create a direct-reporting gamepad object.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
371
|
+
key_store_path: Optional path used by the Bluetooth backend to persist keys.
|
|
372
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
373
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
InvalidInputError: ``adapter`` is omitted.
|
|
377
|
+
"""
|
|
378
|
+
config = self._controller_spec.build_config(
|
|
379
|
+
adapter=adapter,
|
|
380
|
+
key_store_path=key_store_path,
|
|
381
|
+
report_period_us=None,
|
|
382
|
+
controller_colors=controller_colors,
|
|
383
|
+
)
|
|
384
|
+
self._init_from_config(config, diagnostics=diagnostics, transport=None)
|
|
385
|
+
|
|
386
|
+
async def send(self, state: InputState) -> None:
|
|
387
|
+
"""Send one complete input state and commit it after transmission.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
state: Complete input state to send.
|
|
391
|
+
"""
|
|
392
|
+
await self._runtime.send(state)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class ProController(_PeriodicRuntimeBackedGamepad):
|
|
337
396
|
"""Runtime-backed Pro Controller-compatible gamepad."""
|
|
338
397
|
|
|
339
398
|
_controller_spec = _ControllerSpec(profile=default_controller_profile())
|
|
340
399
|
|
|
341
400
|
|
|
342
|
-
class JoyConL(
|
|
401
|
+
class JoyConL(_PeriodicRuntimeBackedGamepad):
|
|
343
402
|
"""Runtime-backed Joy-Con L-compatible gamepad."""
|
|
344
403
|
|
|
345
404
|
_controller_spec = _ControllerSpec(profile=JoyConLeftProfile())
|
|
@@ -374,7 +433,7 @@ class JoyConL(_RuntimeBackedGamepad):
|
|
|
374
433
|
self._init_from_config(config, diagnostics=diagnostics, transport=None)
|
|
375
434
|
|
|
376
435
|
|
|
377
|
-
class JoyConR(
|
|
436
|
+
class JoyConR(_PeriodicRuntimeBackedGamepad):
|
|
378
437
|
"""Runtime-backed Joy-Con R-compatible gamepad."""
|
|
379
438
|
|
|
380
439
|
_controller_spec = _ControllerSpec(profile=JoyConRightProfile())
|
|
@@ -407,3 +466,21 @@ class JoyConR(_RuntimeBackedGamepad):
|
|
|
407
466
|
controller_colors=controller_colors,
|
|
408
467
|
)
|
|
409
468
|
self._init_from_config(config, diagnostics=diagnostics, transport=None)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class DirectProController(_DirectRuntimeBackedGamepad):
|
|
472
|
+
"""Direct-reporting Pro Controller-compatible gamepad."""
|
|
473
|
+
|
|
474
|
+
_controller_spec = _ControllerSpec(profile=default_controller_profile())
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class DirectJoyConL(_DirectRuntimeBackedGamepad):
|
|
478
|
+
"""Direct-reporting Joy-Con L-compatible gamepad."""
|
|
479
|
+
|
|
480
|
+
_controller_spec = _ControllerSpec(profile=JoyConLeftProfile())
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class DirectJoyConR(_DirectRuntimeBackedGamepad):
|
|
484
|
+
"""Direct-reporting Joy-Con R-compatible gamepad."""
|
|
485
|
+
|
|
486
|
+
_controller_spec = _ControllerSpec(profile=JoyConRightProfile())
|
swbt/gamepad/interface.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from types import TracebackType
|
|
5
|
+
from typing import Self
|
|
5
6
|
|
|
6
7
|
from swbt.diagnostics import GamepadStatus
|
|
7
8
|
from swbt.gamepad.connection import ConnectionResult
|
|
@@ -9,14 +10,15 @@ from swbt.input import Button, IMUFrame, InputState, Stick
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class SwitchGamepad(ABC):
|
|
12
|
-
"""Shared public interface for NX-compatible virtual gamepads.
|
|
13
|
+
"""Shared abstract public interface for NX-compatible virtual gamepads.
|
|
13
14
|
|
|
14
|
-
Use
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Use ``PeriodicSwitchGamepad`` or ``DirectSwitchGamepad`` when a type
|
|
16
|
+
annotation must express who owns the input-report schedule. Common input
|
|
17
|
+
operations commit local state on periodic gamepads; on direct gamepads they
|
|
18
|
+
send one input report and commit only after transmission succeeds.
|
|
17
19
|
"""
|
|
18
20
|
|
|
19
|
-
async def __aenter__(self) ->
|
|
21
|
+
async def __aenter__(self) -> Self:
|
|
20
22
|
"""Open the gamepad for an async context manager.
|
|
21
23
|
|
|
22
24
|
Returns:
|
|
@@ -45,8 +47,9 @@ class SwitchGamepad(ABC):
|
|
|
45
47
|
async def open(self) -> None:
|
|
46
48
|
"""Open the configured transport.
|
|
47
49
|
|
|
48
|
-
Opening prepares transport callbacks, diagnostics metadata, and the
|
|
49
|
-
|
|
50
|
+
Opening prepares transport callbacks, diagnostics metadata, and the
|
|
51
|
+
reporting-type resources. It does not start HID advertising, pairing,
|
|
52
|
+
or active reconnect.
|
|
50
53
|
|
|
51
54
|
Raises:
|
|
52
55
|
TransportOpenError: The configured transport cannot be opened.
|
|
@@ -160,25 +163,13 @@ class SwitchGamepad(ABC):
|
|
|
160
163
|
InvalidInputError: Any value is not a ``Button``.
|
|
161
164
|
UnsupportedInputError: The controller profile does not support a button.
|
|
162
165
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@abstractmethod
|
|
167
|
-
async def apply(self, state: InputState) -> None:
|
|
168
|
-
"""Replace the current input state without immediate transmission.
|
|
169
|
-
|
|
170
|
-
Args:
|
|
171
|
-
state: Complete input state to commit.
|
|
172
|
-
|
|
173
|
-
Raises:
|
|
174
|
-
InvalidInputError: ``state`` is not an ``InputState``.
|
|
175
|
-
UnsupportedInputError: The controller profile does not support part of
|
|
176
|
-
the supplied state.
|
|
166
|
+
Completion follows the reporting type: periodic gamepads commit local
|
|
167
|
+
state, while direct gamepads send one input report and then commit.
|
|
177
168
|
"""
|
|
178
169
|
|
|
179
170
|
@abstractmethod
|
|
180
171
|
async def sticks(self, *, left: Stick | None = None, right: Stick | None = None) -> None:
|
|
181
|
-
"""Replace one or both stick positions
|
|
172
|
+
"""Replace one or both stick positions according to the reporting type.
|
|
182
173
|
|
|
183
174
|
Args:
|
|
184
175
|
left: Optional replacement for the left stick.
|
|
@@ -191,7 +182,7 @@ class SwitchGamepad(ABC):
|
|
|
191
182
|
|
|
192
183
|
@abstractmethod
|
|
193
184
|
async def lstick(self, stick: Stick) -> None:
|
|
194
|
-
"""Replace the left stick position
|
|
185
|
+
"""Replace the left stick position according to the reporting type.
|
|
195
186
|
|
|
196
187
|
Args:
|
|
197
188
|
stick: Replacement for the left stick.
|
|
@@ -203,7 +194,7 @@ class SwitchGamepad(ABC):
|
|
|
203
194
|
|
|
204
195
|
@abstractmethod
|
|
205
196
|
async def rstick(self, stick: Stick) -> None:
|
|
206
|
-
"""Replace the right stick position
|
|
197
|
+
"""Replace the right stick position according to the reporting type.
|
|
207
198
|
|
|
208
199
|
Args:
|
|
209
200
|
stick: Replacement for the right stick.
|
|
@@ -215,7 +206,7 @@ class SwitchGamepad(ABC):
|
|
|
215
206
|
|
|
216
207
|
@abstractmethod
|
|
217
208
|
async def imu(self, *frames: IMUFrame) -> None:
|
|
218
|
-
"""Replace IMU frames
|
|
209
|
+
"""Replace IMU frames according to the reporting type.
|
|
219
210
|
|
|
220
211
|
Args:
|
|
221
212
|
frames: One ``IMUFrame`` to repeat across all three IMU slots, or exactly
|
|
@@ -240,7 +231,7 @@ class SwitchGamepad(ABC):
|
|
|
240
231
|
|
|
241
232
|
@abstractmethod
|
|
242
233
|
async def neutral(self) -> None:
|
|
243
|
-
"""
|
|
234
|
+
"""Apply ``InputState.neutral()`` according to the reporting type."""
|
|
244
235
|
|
|
245
236
|
@abstractmethod
|
|
246
237
|
async def tap(self, *buttons: Button, duration: float = 0.08) -> None:
|
|
@@ -268,6 +259,44 @@ class SwitchGamepad(ABC):
|
|
|
268
259
|
def snapshot(self) -> InputState:
|
|
269
260
|
"""Return the latest committed input state.
|
|
270
261
|
|
|
262
|
+
A periodic gamepad returns its latest local state. A direct gamepad
|
|
263
|
+
returns the last state whose input report was sent successfully.
|
|
264
|
+
|
|
271
265
|
Returns:
|
|
272
266
|
InputState: Immutable snapshot of the current input state.
|
|
273
267
|
"""
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class PeriodicSwitchGamepad(SwitchGamepad):
|
|
271
|
+
"""Abstract gamepad whose input report schedule is owned by the library."""
|
|
272
|
+
|
|
273
|
+
@abstractmethod
|
|
274
|
+
async def apply(self, state: InputState) -> None:
|
|
275
|
+
"""Replace the current local input state without immediate transmission.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
state: Complete input state to commit.
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
InvalidInputError: ``state`` is not an ``InputState``.
|
|
282
|
+
UnsupportedInputError: The controller profile does not support part of
|
|
283
|
+
the supplied state.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class DirectSwitchGamepad(SwitchGamepad):
|
|
288
|
+
"""Abstract gamepad whose input report schedule is owned by the caller."""
|
|
289
|
+
|
|
290
|
+
@abstractmethod
|
|
291
|
+
async def send(self, state: InputState) -> None:
|
|
292
|
+
"""Send one complete input state and commit it after transmission.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
state: Complete input state to send.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ClosedError: The gamepad is not connected.
|
|
299
|
+
InvalidInputError: ``state`` is not an ``InputState``.
|
|
300
|
+
UnsupportedInputError: The controller profile does not support part of
|
|
301
|
+
the supplied state.
|
|
302
|
+
"""
|
swbt/gamepad/output.py
CHANGED
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
|
|
6
6
|
from swbt.diagnostics import DiagnosticsRecorder
|
|
7
7
|
from swbt.protocol.output_report import OutputReportParser
|
|
8
|
+
from swbt.protocol.session import SwitchHidSession
|
|
8
9
|
from swbt.protocol.subcommand import (
|
|
9
10
|
SESSION_STATE_SUBCOMMANDS,
|
|
10
11
|
SubcommandResponder,
|
|
@@ -12,7 +13,8 @@ from swbt.protocol.subcommand import (
|
|
|
12
13
|
)
|
|
13
14
|
from swbt.state_store import InputStateStore
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
ReplyBuilder = Callable[[], bytes | Awaitable[bytes]]
|
|
17
|
+
ReplySender = Callable[[ReplyBuilder], Awaitable[bytes]]
|
|
16
18
|
ReplySenderRequirement = Callable[[], None]
|
|
17
19
|
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ class OutputReportDispatcher:
|
|
|
23
25
|
diagnostics: DiagnosticsRecorder
|
|
24
26
|
require_reply_sender: ReplySenderRequirement
|
|
25
27
|
send_subcommand_reply: ReplySender
|
|
28
|
+
session: SwitchHidSession
|
|
26
29
|
state_store: InputStateStore
|
|
27
30
|
output_report_parser: OutputReportParser = field(default_factory=OutputReportParser)
|
|
28
31
|
subcommand_responder: SubcommandResponder = field(default_factory=SubcommandResponder)
|
|
@@ -47,9 +50,17 @@ class OutputReportDispatcher:
|
|
|
47
50
|
subcommand_id=output_report.subcommand_id,
|
|
48
51
|
)
|
|
49
52
|
self.require_reply_sender()
|
|
50
|
-
state = await self.state_store.snapshot()
|
|
51
53
|
try:
|
|
52
|
-
|
|
54
|
+
|
|
55
|
+
async def build_reply() -> bytes:
|
|
56
|
+
state = await self.state_store.snapshot()
|
|
57
|
+
return self.subcommand_responder.respond(
|
|
58
|
+
output_report,
|
|
59
|
+
state=state,
|
|
60
|
+
session=self.session,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
reply = await self.send_subcommand_reply(build_reply)
|
|
53
64
|
except UnsupportedSubcommandError:
|
|
54
65
|
self.diagnostics.record_event(
|
|
55
66
|
"unsupported_subcommand",
|
|
@@ -59,10 +70,11 @@ class OutputReportDispatcher:
|
|
|
59
70
|
)
|
|
60
71
|
raise
|
|
61
72
|
if output_report.subcommand_id in SESSION_STATE_SUBCOMMANDS:
|
|
62
|
-
session_state = self.
|
|
73
|
+
session_state = self.session.state
|
|
63
74
|
self.diagnostics.record_event(
|
|
64
75
|
"subcommand_session_state",
|
|
65
76
|
imu_enabled=session_state.imu_enabled,
|
|
77
|
+
imu_encoding_format=session_state.imu_encoding_format,
|
|
66
78
|
imu_mode=_format_optional_byte(session_state.imu_mode),
|
|
67
79
|
packet_id=output_report.packet_id,
|
|
68
80
|
report_mode=_format_optional_byte(session_state.report_mode),
|
|
@@ -79,7 +91,6 @@ class OutputReportDispatcher:
|
|
|
79
91
|
report_id=_format_report_id(reply[0]),
|
|
80
92
|
subcommand_id=subcommand_id,
|
|
81
93
|
)
|
|
82
|
-
await self.send_subcommand_reply(reply)
|
|
83
94
|
|
|
84
95
|
|
|
85
96
|
def _format_report_id(report_id: int) -> str:
|