swbt-python 0.5.0__py3-none-any.whl → 0.5.2__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/diagnostics.py +4 -27
- swbt/gamepad/__init__.py +1 -1
- swbt/gamepad/_config.py +37 -85
- swbt/gamepad/connection.py +3 -207
- swbt/gamepad/controllers.py +211 -0
- swbt/gamepad/interface.py +190 -31
- swbt/gamepad/output.py +32 -2
- swbt/gamepad/protocol_handshake.py +167 -0
- swbt/gamepad/runtime.py +392 -211
- swbt/gamepad/transport_factory.py +1 -78
- swbt/protocol/profiles/base.py +1 -0
- swbt/protocol/profiles/joycon.py +3 -0
- swbt/protocol/session.py +26 -0
- swbt/protocol/subcommand.py +36 -5
- swbt/report_loop.py +29 -32
- swbt/state_store.py +1 -31
- swbt/transport/_bumble_key_store.py +15 -67
- swbt/transport/base.py +7 -33
- swbt/transport/bumble.py +9 -17
- swbt/transport/fake.py +12 -15
- {swbt_python-0.5.0.dist-info → swbt_python-0.5.2.dist-info}/METADATA +11 -12
- {swbt_python-0.5.0.dist-info → swbt_python-0.5.2.dist-info}/RECORD +25 -26
- swbt/_testing/__init__.py +0 -1
- swbt/_testing/gamepad.py +0 -145
- swbt/gamepad/core.py +0 -732
- {swbt_python-0.5.0.dist-info → swbt_python-0.5.2.dist-info}/WHEEL +0 -0
- {swbt_python-0.5.0.dist-info → swbt_python-0.5.2.dist-info}/entry_points.txt +0 -0
- {swbt_python-0.5.0.dist-info → swbt_python-0.5.2.dist-info}/licenses/LICENSE +0 -0
swbt/diagnostics.py
CHANGED
|
@@ -67,21 +67,16 @@ class GamepadStatus:
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
class DiagnosticsRecorder:
|
|
70
|
-
"""Record
|
|
70
|
+
"""Record bounded diagnostics aggregates and optional JSON Lines traces."""
|
|
71
71
|
|
|
72
72
|
def __init__(self, trace_writer: TextIO | None = None) -> None:
|
|
73
73
|
"""Create an empty recorder."""
|
|
74
|
-
self._events: list[DiagnosticsEvent] = []
|
|
75
74
|
self._report_counters: dict[int, int] = {}
|
|
76
75
|
self._last_subcommand_id: int | None = None
|
|
77
76
|
self._raw_rumble: bytes | None = None
|
|
77
|
+
self._last_error: DiagnosticsEvent | None = None
|
|
78
78
|
self._trace_writer = trace_writer
|
|
79
79
|
|
|
80
|
-
@property
|
|
81
|
-
def events(self) -> tuple[DiagnosticsEvent, ...]:
|
|
82
|
-
"""Return recorded events in order."""
|
|
83
|
-
return tuple(self._events)
|
|
84
|
-
|
|
85
80
|
@property
|
|
86
81
|
def report_counters(self) -> dict[int, int]:
|
|
87
82
|
"""Return sent report counters keyed by report ID."""
|
|
@@ -100,10 +95,7 @@ class DiagnosticsRecorder:
|
|
|
100
95
|
@property
|
|
101
96
|
def last_error(self) -> DiagnosticsEvent | None:
|
|
102
97
|
"""Return the latest error event."""
|
|
103
|
-
|
|
104
|
-
if event.event == "error":
|
|
105
|
-
return event
|
|
106
|
-
return None
|
|
98
|
+
return self._last_error
|
|
107
99
|
|
|
108
100
|
def record_event(self, event: str, **fields: object) -> DiagnosticsEvent:
|
|
109
101
|
"""Record a diagnostics event with schema fields."""
|
|
@@ -155,21 +147,6 @@ class DiagnosticsRecorder:
|
|
|
155
147
|
**fields,
|
|
156
148
|
)
|
|
157
149
|
|
|
158
|
-
def record_state_transition(
|
|
159
|
-
self,
|
|
160
|
-
*,
|
|
161
|
-
previous: str,
|
|
162
|
-
next_state: str,
|
|
163
|
-
reason: str,
|
|
164
|
-
) -> DiagnosticsEvent:
|
|
165
|
-
"""Record one lifecycle state transition."""
|
|
166
|
-
return self.record_event(
|
|
167
|
-
"state_transition",
|
|
168
|
-
previous=previous,
|
|
169
|
-
next=next_state,
|
|
170
|
-
reason=reason,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
150
|
def record_error(self, error: BaseException, *, recoverable: bool) -> DiagnosticsEvent:
|
|
174
151
|
"""Record an exception as an error event."""
|
|
175
152
|
event = DiagnosticsEvent(
|
|
@@ -178,11 +155,11 @@ class DiagnosticsRecorder:
|
|
|
178
155
|
message=str(error),
|
|
179
156
|
recoverable=recoverable,
|
|
180
157
|
)
|
|
158
|
+
self._last_error = event
|
|
181
159
|
self._append(event)
|
|
182
160
|
return event
|
|
183
161
|
|
|
184
162
|
def _append(self, event: DiagnosticsEvent) -> None:
|
|
185
|
-
self._events.append(event)
|
|
186
163
|
if self._trace_writer is None:
|
|
187
164
|
return
|
|
188
165
|
payload: dict[str, object] = {"event": event.event}
|
swbt/gamepad/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from swbt.gamepad.connection import ConnectionResult, ConnectionStatus
|
|
4
4
|
from swbt.gamepad.constants import DISCONNECT_REQUEST_TIMEOUT_SECONDS
|
|
5
|
-
from swbt.gamepad.
|
|
5
|
+
from swbt.gamepad.controllers import (
|
|
6
6
|
DirectJoyConL,
|
|
7
7
|
DirectJoyConR,
|
|
8
8
|
DirectProController,
|
swbt/gamepad/_config.py
CHANGED
|
@@ -1,82 +1,14 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Normalized configuration for gamepad construction and runtime setup."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass
|
|
4
4
|
|
|
5
5
|
from swbt.errors import InvalidInputError
|
|
6
6
|
from swbt.protocol.profiles.base import ControllerColors, ControllerProfile
|
|
7
|
-
from swbt.protocol.profiles.pro_controller import default_controller_profile
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
@dataclass(frozen=True)
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
Attributes:
|
|
15
|
-
adapter: Bumble adapter moniker, such as ``"usb:0"``.
|
|
16
|
-
profile_path: Path to a swbt-owned pairing profile.
|
|
17
|
-
profile: Fixed controller identity and protocol profile.
|
|
18
|
-
report_period_us: Periodic input report interval in microseconds.
|
|
19
|
-
device_name: HID device name advertised to the host.
|
|
20
|
-
controller_colors: Fixed controller body, button, and grip colors for SPI profile data.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
adapter: str | None = None
|
|
24
|
-
profile_path: str | None = None
|
|
25
|
-
profile: ControllerProfile = field(default_factory=default_controller_profile)
|
|
26
|
-
report_period_us: int | None = None
|
|
27
|
-
device_name: str | None = None
|
|
28
|
-
controller_colors: ControllerColors | None = None
|
|
29
|
-
|
|
30
|
-
def __post_init__(self) -> None:
|
|
31
|
-
"""Validate resource configuration."""
|
|
32
|
-
if not isinstance(self.profile, ControllerProfile):
|
|
33
|
-
msg = "profile must be a ControllerProfile"
|
|
34
|
-
raise InvalidInputError(msg)
|
|
35
|
-
if self.report_period_us is None:
|
|
36
|
-
object.__setattr__(
|
|
37
|
-
self,
|
|
38
|
-
"report_period_us",
|
|
39
|
-
self.profile.default_report_period_us,
|
|
40
|
-
)
|
|
41
|
-
elif self.report_period_us <= 0:
|
|
42
|
-
msg = "report_period_us must be positive"
|
|
43
|
-
raise InvalidInputError(msg)
|
|
44
|
-
if self.device_name is None:
|
|
45
|
-
object.__setattr__(self, "device_name", self.profile.device_name)
|
|
46
|
-
if self.controller_colors is not None and not isinstance(
|
|
47
|
-
self.controller_colors, ControllerColors
|
|
48
|
-
):
|
|
49
|
-
msg = "controller_colors must be a ControllerColors"
|
|
50
|
-
raise InvalidInputError(msg)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@dataclass(frozen=True)
|
|
54
|
-
class _ControllerSpec:
|
|
55
|
-
"""Internal controller identity selected by a concrete controller class."""
|
|
56
|
-
|
|
57
|
-
profile: ControllerProfile
|
|
58
|
-
|
|
59
|
-
def build_config(
|
|
60
|
-
self,
|
|
61
|
-
*,
|
|
62
|
-
adapter: str | None,
|
|
63
|
-
report_period_us: int | None,
|
|
64
|
-
controller_colors: ControllerColors | None,
|
|
65
|
-
profile_path: str | None = None,
|
|
66
|
-
) -> _SwitchGamepadConfig:
|
|
67
|
-
"""Create internal construction config from public constructor options."""
|
|
68
|
-
return _SwitchGamepadConfig(
|
|
69
|
-
adapter=adapter,
|
|
70
|
-
profile_path=profile_path,
|
|
71
|
-
profile=self.profile,
|
|
72
|
-
report_period_us=report_period_us,
|
|
73
|
-
controller_colors=controller_colors,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@dataclass(frozen=True)
|
|
78
|
-
class _RuntimeConfig:
|
|
79
|
-
"""Normalized internal configuration for ControllerRuntime."""
|
|
9
|
+
@dataclass(frozen=True, init=False)
|
|
10
|
+
class _GamepadConfig:
|
|
11
|
+
"""Normalized internal configuration shared by a controller and its runtime."""
|
|
80
12
|
|
|
81
13
|
adapter: str | None
|
|
82
14
|
profile_path: str | None
|
|
@@ -85,17 +17,37 @@ class _RuntimeConfig:
|
|
|
85
17
|
device_name: str
|
|
86
18
|
controller_colors: ControllerColors | None
|
|
87
19
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
profile: ControllerProfile,
|
|
24
|
+
adapter: str | None = None,
|
|
25
|
+
profile_path: str | None = None,
|
|
26
|
+
report_period_us: int | None = None,
|
|
27
|
+
device_name: str | None = None,
|
|
28
|
+
controller_colors: ControllerColors | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Validate and normalize controller construction values."""
|
|
31
|
+
if not isinstance(profile, ControllerProfile):
|
|
32
|
+
msg = "profile must be a ControllerProfile"
|
|
33
|
+
raise InvalidInputError(msg)
|
|
34
|
+
normalized_report_period = (
|
|
35
|
+
profile.default_report_period_us if report_period_us is None else report_period_us
|
|
36
|
+
)
|
|
37
|
+
if normalized_report_period <= 0:
|
|
38
|
+
msg = "report_period_us must be positive"
|
|
93
39
|
raise InvalidInputError(msg)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
40
|
+
if controller_colors is not None and not isinstance(controller_colors, ControllerColors):
|
|
41
|
+
msg = "controller_colors must be a ControllerColors"
|
|
42
|
+
raise InvalidInputError(msg)
|
|
43
|
+
|
|
44
|
+
object.__setattr__(self, "adapter", adapter)
|
|
45
|
+
object.__setattr__(self, "profile_path", profile_path)
|
|
46
|
+
object.__setattr__(self, "profile", profile)
|
|
47
|
+
object.__setattr__(self, "report_period_us", normalized_report_period)
|
|
48
|
+
object.__setattr__(
|
|
49
|
+
self,
|
|
50
|
+
"device_name",
|
|
51
|
+
profile.device_name if device_name is None else device_name,
|
|
101
52
|
)
|
|
53
|
+
object.__setattr__(self, "controller_colors", controller_colors)
|
swbt/gamepad/connection.py
CHANGED
|
@@ -1,30 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Public connection result types."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
3
|
from dataclasses import dataclass
|
|
6
4
|
from typing import Literal
|
|
7
5
|
|
|
8
|
-
from swbt.diagnostics import DiagnosticsRecorder
|
|
9
|
-
from swbt.errors import (
|
|
10
|
-
ClosedError,
|
|
11
|
-
ConnectionFailedError,
|
|
12
|
-
ConnectionTimeoutError,
|
|
13
|
-
InvalidKeyStoreError,
|
|
14
|
-
)
|
|
15
|
-
from swbt.transport.base import HidDeviceTransport
|
|
16
|
-
|
|
17
6
|
ConnectionRoute = Literal["active_reconnect", "pairing"]
|
|
18
7
|
ConnectionStatus = Literal["connected", "no_bond", "timeout", "failed"]
|
|
19
8
|
|
|
20
|
-
EnsureOpen = Callable[[], Awaitable[None]]
|
|
21
|
-
TransportProvider = Callable[[], HidDeviceTransport | None]
|
|
22
|
-
StateSetter = Callable[[str], None]
|
|
23
|
-
EventClearer = Callable[[], None]
|
|
24
|
-
WaitForConnected = Callable[[float | None], Awaitable[None]]
|
|
25
|
-
CloseNeutral = Callable[[], Awaitable[None]]
|
|
26
|
-
PairWithTimeout = Callable[[float | None], Awaitable[None]]
|
|
27
|
-
|
|
28
9
|
|
|
29
10
|
@dataclass(frozen=True)
|
|
30
11
|
class ConnectionResult:
|
|
@@ -33,196 +14,11 @@ class ConnectionResult:
|
|
|
33
14
|
Args:
|
|
34
15
|
route: Connection path that produced the result.
|
|
35
16
|
status: Outcome of the connection attempt.
|
|
36
|
-
peer_address: Address
|
|
37
|
-
peer_count: Number of
|
|
38
|
-
|
|
39
|
-
Attributes:
|
|
40
|
-
route: Connection path that produced the result.
|
|
41
|
-
status: Outcome of the connection attempt.
|
|
42
|
-
peer_address: Address of the bonded peer used for reconnect, when one was selected.
|
|
43
|
-
peer_count: Number of bonded peers observed while selecting a reconnect target.
|
|
17
|
+
peer_address: Address used for active reconnect, when one was selected.
|
|
18
|
+
peer_count: Number of current peers observed while selecting a reconnect target.
|
|
44
19
|
"""
|
|
45
20
|
|
|
46
21
|
route: ConnectionRoute
|
|
47
22
|
status: ConnectionStatus
|
|
48
23
|
peer_address: str | None = None
|
|
49
24
|
peer_count: int | None = None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class ConnectionWorkflow:
|
|
54
|
-
"""Run active reconnect and pairing fallback workflows."""
|
|
55
|
-
|
|
56
|
-
clear_connected: EventClearer
|
|
57
|
-
close_neutral: CloseNeutral
|
|
58
|
-
diagnostics: DiagnosticsRecorder
|
|
59
|
-
ensure_open: EnsureOpen
|
|
60
|
-
get_transport: TransportProvider
|
|
61
|
-
profile_path: str | None
|
|
62
|
-
pair: PairWithTimeout
|
|
63
|
-
set_connection_state: StateSetter
|
|
64
|
-
transport_was_injected: bool
|
|
65
|
-
wait_for_connected: WaitForConnected
|
|
66
|
-
|
|
67
|
-
async def try_reconnect(
|
|
68
|
-
self,
|
|
69
|
-
timeout: float | None = None, # noqa: ASYNC109
|
|
70
|
-
) -> ConnectionResult:
|
|
71
|
-
"""Try active reconnect with exactly one bonded peer."""
|
|
72
|
-
await self.ensure_open()
|
|
73
|
-
transport = self._transport()
|
|
74
|
-
if self.profile_path is None and not self.transport_was_injected:
|
|
75
|
-
self.diagnostics.record_event(
|
|
76
|
-
"reconnect_profile_unavailable",
|
|
77
|
-
reason="profile_path_none",
|
|
78
|
-
route="active_reconnect",
|
|
79
|
-
)
|
|
80
|
-
peers = await transport.list_bonded_peers()
|
|
81
|
-
if len(peers) > 1:
|
|
82
|
-
self.diagnostics.record_event(
|
|
83
|
-
"invalid_key_store",
|
|
84
|
-
peer_count=len(peers),
|
|
85
|
-
reason="multiple_current_peers",
|
|
86
|
-
)
|
|
87
|
-
msg = "key store contains multiple current peers"
|
|
88
|
-
raise InvalidKeyStoreError(msg)
|
|
89
|
-
selection = _bonded_peer_selection(len(peers))
|
|
90
|
-
self.diagnostics.record_event(
|
|
91
|
-
"bonded_peers_discovered",
|
|
92
|
-
peer_count=len(peers),
|
|
93
|
-
selection=selection,
|
|
94
|
-
)
|
|
95
|
-
if not peers:
|
|
96
|
-
self.diagnostics.record_event(
|
|
97
|
-
"active_reconnect_result",
|
|
98
|
-
peer_count=0,
|
|
99
|
-
route="active_reconnect",
|
|
100
|
-
status="no_bond",
|
|
101
|
-
)
|
|
102
|
-
return ConnectionResult(
|
|
103
|
-
route="active_reconnect",
|
|
104
|
-
status="no_bond",
|
|
105
|
-
peer_count=0,
|
|
106
|
-
)
|
|
107
|
-
peer = peers[0]
|
|
108
|
-
self.set_connection_state("reconnecting")
|
|
109
|
-
self.clear_connected()
|
|
110
|
-
self.diagnostics.record_event(
|
|
111
|
-
"active_reconnect_attempt",
|
|
112
|
-
peer_address=peer.address,
|
|
113
|
-
route="active_reconnect",
|
|
114
|
-
)
|
|
115
|
-
try:
|
|
116
|
-
await transport.connect_bonded_peer(
|
|
117
|
-
peer.address,
|
|
118
|
-
connect_timeout=timeout,
|
|
119
|
-
)
|
|
120
|
-
await self.wait_for_connected(timeout)
|
|
121
|
-
except TimeoutError:
|
|
122
|
-
self.diagnostics.record_event(
|
|
123
|
-
"active_reconnect_result",
|
|
124
|
-
failure_reason="connection_timeout",
|
|
125
|
-
peer_address=peer.address,
|
|
126
|
-
route="active_reconnect",
|
|
127
|
-
status="timeout",
|
|
128
|
-
)
|
|
129
|
-
await self.close_neutral()
|
|
130
|
-
return ConnectionResult(
|
|
131
|
-
route="active_reconnect",
|
|
132
|
-
status="timeout",
|
|
133
|
-
peer_address=peer.address,
|
|
134
|
-
peer_count=1,
|
|
135
|
-
)
|
|
136
|
-
except asyncio.CancelledError as error:
|
|
137
|
-
if _current_task_is_cancelling():
|
|
138
|
-
raise
|
|
139
|
-
return await self._record_transport_error(error, peer_address=peer.address)
|
|
140
|
-
except Exception as error: # noqa: BLE001
|
|
141
|
-
return await self._record_transport_error(error, peer_address=peer.address)
|
|
142
|
-
|
|
143
|
-
self.diagnostics.record_event(
|
|
144
|
-
"active_reconnect_result",
|
|
145
|
-
peer_address=peer.address,
|
|
146
|
-
route="active_reconnect",
|
|
147
|
-
status="connected",
|
|
148
|
-
)
|
|
149
|
-
return ConnectionResult(
|
|
150
|
-
route="active_reconnect",
|
|
151
|
-
status="connected",
|
|
152
|
-
peer_address=peer.address,
|
|
153
|
-
peer_count=1,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
async def try_connect(
|
|
157
|
-
self,
|
|
158
|
-
*,
|
|
159
|
-
timeout: float | None = None, # noqa: ASYNC109
|
|
160
|
-
allow_pairing: bool = False,
|
|
161
|
-
) -> ConnectionResult:
|
|
162
|
-
"""Try bonded reconnect first, then optional pairing fallback."""
|
|
163
|
-
reconnect_result = await self.try_reconnect(timeout=timeout)
|
|
164
|
-
if reconnect_result.status != "no_bond" or not allow_pairing:
|
|
165
|
-
return reconnect_result
|
|
166
|
-
self.diagnostics.record_event(
|
|
167
|
-
"connect_pairing_fallback",
|
|
168
|
-
reason="no_bond",
|
|
169
|
-
route="pairing",
|
|
170
|
-
)
|
|
171
|
-
try:
|
|
172
|
-
await self.pair(timeout)
|
|
173
|
-
except ConnectionTimeoutError:
|
|
174
|
-
return ConnectionResult(route="pairing", status="timeout")
|
|
175
|
-
return ConnectionResult(route="pairing", status="connected")
|
|
176
|
-
|
|
177
|
-
def _transport(self) -> HidDeviceTransport:
|
|
178
|
-
transport = self.get_transport()
|
|
179
|
-
if transport is None:
|
|
180
|
-
msg = "gamepad is not open"
|
|
181
|
-
raise ClosedError(msg)
|
|
182
|
-
return transport
|
|
183
|
-
|
|
184
|
-
async def _record_transport_error(
|
|
185
|
-
self,
|
|
186
|
-
error: BaseException,
|
|
187
|
-
*,
|
|
188
|
-
peer_address: str,
|
|
189
|
-
) -> ConnectionResult:
|
|
190
|
-
self.diagnostics.record_event(
|
|
191
|
-
"active_reconnect_result",
|
|
192
|
-
error_type=type(error).__name__,
|
|
193
|
-
failure_reason="transport_error",
|
|
194
|
-
message=str(error),
|
|
195
|
-
peer_address=peer_address,
|
|
196
|
-
route="active_reconnect",
|
|
197
|
-
status="failed",
|
|
198
|
-
)
|
|
199
|
-
self.diagnostics.record_error(error, recoverable=True)
|
|
200
|
-
await self.close_neutral()
|
|
201
|
-
return ConnectionResult(
|
|
202
|
-
route="active_reconnect",
|
|
203
|
-
status="failed",
|
|
204
|
-
peer_address=peer_address,
|
|
205
|
-
peer_count=1,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def raise_if_connection_failed(result: ConnectionResult) -> None:
|
|
210
|
-
"""Raise the public connection error for a non-connected result."""
|
|
211
|
-
if result.status == "connected":
|
|
212
|
-
return
|
|
213
|
-
if result.status == "timeout":
|
|
214
|
-
msg = "connection timed out"
|
|
215
|
-
raise ConnectionTimeoutError(msg)
|
|
216
|
-
msg = f"connection failed: {result.status}"
|
|
217
|
-
raise ConnectionFailedError(msg)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _bonded_peer_selection(peer_count: int) -> str:
|
|
221
|
-
if peer_count == 0:
|
|
222
|
-
return "none"
|
|
223
|
-
return "selected"
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def _current_task_is_cancelling() -> bool:
|
|
227
|
-
task = asyncio.current_task()
|
|
228
|
-
return task is not None and task.cancelling() > 0
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Public concrete gamepad controllers."""
|
|
2
|
+
|
|
3
|
+
from swbt.diagnostics import DiagnosticsConfig
|
|
4
|
+
from swbt.gamepad.interface import DirectSwitchGamepad, PeriodicSwitchGamepad
|
|
5
|
+
from swbt.protocol.profiles.base import ControllerColors
|
|
6
|
+
from swbt.protocol.profiles.joycon import JoyConLeftProfile, JoyConRightProfile
|
|
7
|
+
from swbt.protocol.profiles.pro_controller import default_controller_profile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProController(PeriodicSwitchGamepad):
|
|
11
|
+
"""Runtime-backed Pro Controller-compatible gamepad."""
|
|
12
|
+
|
|
13
|
+
_profile = default_controller_profile()
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
adapter: str | None = None,
|
|
19
|
+
profile_path: str | None = None,
|
|
20
|
+
report_period_us: int | None = None,
|
|
21
|
+
controller_colors: ControllerColors | None = None,
|
|
22
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Create a Pro Controller-compatible gamepad.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
28
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
29
|
+
report_period_us: Optional periodic input report interval in microseconds.
|
|
30
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
31
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
InvalidInputError: adapter is omitted or report_period_us is not positive.
|
|
35
|
+
"""
|
|
36
|
+
self._initialize_runtime(
|
|
37
|
+
adapter=adapter,
|
|
38
|
+
profile_path=profile_path,
|
|
39
|
+
report_period_us=report_period_us,
|
|
40
|
+
controller_colors=controller_colors,
|
|
41
|
+
diagnostics=diagnostics,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JoyConL(PeriodicSwitchGamepad):
|
|
46
|
+
"""Runtime-backed Joy-Con L-compatible gamepad."""
|
|
47
|
+
|
|
48
|
+
_profile = JoyConLeftProfile()
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
adapter: str | None = None,
|
|
54
|
+
profile_path: str | None = None,
|
|
55
|
+
report_period_us: int | None = None,
|
|
56
|
+
controller_colors: ControllerColors | None = None,
|
|
57
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Create a left Joy-Con-compatible gamepad.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
63
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
64
|
+
report_period_us: Optional periodic input report interval in microseconds.
|
|
65
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
66
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
InvalidInputError: ``adapter`` is omitted or ``report_period_us`` is not positive.
|
|
70
|
+
"""
|
|
71
|
+
self._initialize_runtime(
|
|
72
|
+
adapter=adapter,
|
|
73
|
+
profile_path=profile_path,
|
|
74
|
+
report_period_us=report_period_us,
|
|
75
|
+
controller_colors=controller_colors,
|
|
76
|
+
diagnostics=diagnostics,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class JoyConR(PeriodicSwitchGamepad):
|
|
81
|
+
"""Runtime-backed Joy-Con R-compatible gamepad."""
|
|
82
|
+
|
|
83
|
+
_profile = JoyConRightProfile()
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
adapter: str | None = None,
|
|
89
|
+
profile_path: str | None = None,
|
|
90
|
+
report_period_us: int | None = None,
|
|
91
|
+
controller_colors: ControllerColors | None = None,
|
|
92
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Create a right Joy-Con-compatible gamepad.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
98
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
99
|
+
report_period_us: Optional periodic input report interval in microseconds.
|
|
100
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
101
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
InvalidInputError: ``adapter`` is omitted or ``report_period_us`` is not positive.
|
|
105
|
+
"""
|
|
106
|
+
self._initialize_runtime(
|
|
107
|
+
adapter=adapter,
|
|
108
|
+
profile_path=profile_path,
|
|
109
|
+
report_period_us=report_period_us,
|
|
110
|
+
controller_colors=controller_colors,
|
|
111
|
+
diagnostics=diagnostics,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class DirectProController(DirectSwitchGamepad):
|
|
116
|
+
"""Direct-reporting Pro Controller-compatible gamepad."""
|
|
117
|
+
|
|
118
|
+
_profile = default_controller_profile()
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
adapter: str | None = None,
|
|
124
|
+
profile_path: str | None = None,
|
|
125
|
+
controller_colors: ControllerColors | None = None,
|
|
126
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Create a direct-reporting Pro Controller-compatible gamepad.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
132
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
133
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
134
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
InvalidInputError: ``adapter`` is omitted.
|
|
138
|
+
"""
|
|
139
|
+
self._initialize_runtime(
|
|
140
|
+
adapter=adapter,
|
|
141
|
+
profile_path=profile_path,
|
|
142
|
+
report_period_us=None,
|
|
143
|
+
controller_colors=controller_colors,
|
|
144
|
+
diagnostics=diagnostics,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DirectJoyConL(DirectSwitchGamepad):
|
|
149
|
+
"""Direct-reporting Joy-Con L-compatible gamepad."""
|
|
150
|
+
|
|
151
|
+
_profile = JoyConLeftProfile()
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
adapter: str | None = None,
|
|
157
|
+
profile_path: str | None = None,
|
|
158
|
+
controller_colors: ControllerColors | None = None,
|
|
159
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Create a direct-reporting Joy-Con L-compatible gamepad.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
165
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
166
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
167
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
InvalidInputError: ``adapter`` is omitted.
|
|
171
|
+
"""
|
|
172
|
+
self._initialize_runtime(
|
|
173
|
+
adapter=adapter,
|
|
174
|
+
profile_path=profile_path,
|
|
175
|
+
report_period_us=None,
|
|
176
|
+
controller_colors=controller_colors,
|
|
177
|
+
diagnostics=diagnostics,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class DirectJoyConR(DirectSwitchGamepad):
|
|
182
|
+
"""Direct-reporting Joy-Con R-compatible gamepad."""
|
|
183
|
+
|
|
184
|
+
_profile = JoyConRightProfile()
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
adapter: str | None = None,
|
|
190
|
+
profile_path: str | None = None,
|
|
191
|
+
controller_colors: ControllerColors | None = None,
|
|
192
|
+
diagnostics: DiagnosticsConfig | None = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Create a direct-reporting Joy-Con R-compatible gamepad.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
adapter: Bumble adapter moniker used for the Bluetooth backend.
|
|
198
|
+
profile_path: Optional swbt-owned pairing profile path.
|
|
199
|
+
controller_colors: Optional fixed controller body, button, and grip colors.
|
|
200
|
+
diagnostics: Optional diagnostics configuration for trace output.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
InvalidInputError: ``adapter`` is omitted.
|
|
204
|
+
"""
|
|
205
|
+
self._initialize_runtime(
|
|
206
|
+
adapter=adapter,
|
|
207
|
+
profile_path=profile_path,
|
|
208
|
+
report_period_us=None,
|
|
209
|
+
controller_colors=controller_colors,
|
|
210
|
+
diagnostics=diagnostics,
|
|
211
|
+
)
|