swbt-python 0.3.0__py3-none-any.whl → 0.5.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 +16 -0
- swbt/_testing/gamepad.py +78 -7
- swbt/diagnostics.py +4 -10
- swbt/errors.py +37 -0
- swbt/gamepad/__init__.py +12 -0
- swbt/gamepad/_config.py +6 -6
- swbt/gamepad/connection.py +4 -4
- swbt/gamepad/core.py +360 -37
- swbt/gamepad/interface.py +55 -26
- swbt/gamepad/output.py +7 -5
- swbt/gamepad/runtime.py +182 -37
- swbt/gamepad/transport_factory.py +34 -7
- swbt/probe.py +7 -6
- swbt/protocol/profiles/base.py +1 -1
- swbt/report_loop.py +136 -53
- swbt/transport/_adapter_identity.py +220 -0
- swbt/transport/_bumble_key_store.py +108 -57
- swbt/transport/_csr_bd_addr.py +249 -0
- swbt/transport/_csr_bd_addr_harness.py +279 -0
- swbt/transport/_pairing_profile.py +243 -0
- swbt/transport/base.py +5 -2
- swbt/transport/bumble.py +110 -15
- {swbt_python-0.3.0.dist-info → swbt_python-0.5.0.dist-info}/METADATA +16 -19
- {swbt_python-0.3.0.dist-info → swbt_python-0.5.0.dist-info}/RECORD +27 -23
- {swbt_python-0.3.0.dist-info → swbt_python-0.5.0.dist-info}/WHEEL +0 -0
- {swbt_python-0.3.0.dist-info → swbt_python-0.5.0.dist-info}/entry_points.txt +0 -0
- {swbt_python-0.3.0.dist-info → swbt_python-0.5.0.dist-info}/licenses/LICENSE +0 -0
swbt/__init__.py
CHANGED
|
@@ -4,19 +4,27 @@ from swbt.adapter_discovery import AdapterInfo, list_adapters
|
|
|
4
4
|
from swbt.diagnostics import DiagnosticsConfig, GamepadStatus
|
|
5
5
|
from swbt.errors import (
|
|
6
6
|
AdapterDiscoveryError,
|
|
7
|
+
AdapterIdentityRecoveryRequired,
|
|
7
8
|
ClosedError,
|
|
8
9
|
ConnectionFailedError,
|
|
9
10
|
ConnectionTimeoutError,
|
|
10
11
|
InvalidInputError,
|
|
11
12
|
InvalidKeyStoreError,
|
|
13
|
+
InvalidProfileError,
|
|
14
|
+
ProfileControllerMismatchError,
|
|
12
15
|
SwbtError,
|
|
13
16
|
TransportOpenError,
|
|
14
17
|
UnsupportedInputError,
|
|
15
18
|
)
|
|
16
19
|
from swbt.gamepad import (
|
|
17
20
|
ConnectionResult,
|
|
21
|
+
DirectJoyConL,
|
|
22
|
+
DirectJoyConR,
|
|
23
|
+
DirectProController,
|
|
24
|
+
DirectSwitchGamepad,
|
|
18
25
|
JoyConL,
|
|
19
26
|
JoyConR,
|
|
27
|
+
PeriodicSwitchGamepad,
|
|
20
28
|
ProController,
|
|
21
29
|
SwitchGamepad,
|
|
22
30
|
)
|
|
@@ -25,6 +33,7 @@ from swbt.protocol.profiles.base import ControllerColors
|
|
|
25
33
|
|
|
26
34
|
__all__ = (
|
|
27
35
|
"AdapterDiscoveryError",
|
|
36
|
+
"AdapterIdentityRecoveryRequired",
|
|
28
37
|
"AdapterInfo",
|
|
29
38
|
"Button",
|
|
30
39
|
"ClosedError",
|
|
@@ -33,14 +42,21 @@ __all__ = (
|
|
|
33
42
|
"ConnectionTimeoutError",
|
|
34
43
|
"ControllerColors",
|
|
35
44
|
"DiagnosticsConfig",
|
|
45
|
+
"DirectJoyConL",
|
|
46
|
+
"DirectJoyConR",
|
|
47
|
+
"DirectProController",
|
|
48
|
+
"DirectSwitchGamepad",
|
|
36
49
|
"GamepadStatus",
|
|
37
50
|
"IMUFrame",
|
|
38
51
|
"InputState",
|
|
39
52
|
"InvalidInputError",
|
|
40
53
|
"InvalidKeyStoreError",
|
|
54
|
+
"InvalidProfileError",
|
|
41
55
|
"JoyConL",
|
|
42
56
|
"JoyConR",
|
|
57
|
+
"PeriodicSwitchGamepad",
|
|
43
58
|
"ProController",
|
|
59
|
+
"ProfileControllerMismatchError",
|
|
44
60
|
"Stick",
|
|
45
61
|
"SwbtError",
|
|
46
62
|
"SwitchGamepad",
|
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
|
|
@@ -10,7 +19,7 @@ def make_pro_controller(
|
|
|
10
19
|
*,
|
|
11
20
|
transport: HidDeviceTransport,
|
|
12
21
|
adapter: str | None = None,
|
|
13
|
-
|
|
22
|
+
profile_path: str | None = None,
|
|
14
23
|
report_period_us: int | None = None,
|
|
15
24
|
controller_colors: ControllerColors | None = None,
|
|
16
25
|
diagnostics: DiagnosticsConfig | None = None,
|
|
@@ -19,7 +28,7 @@ def make_pro_controller(
|
|
|
19
28
|
return ProController._from_config(
|
|
20
29
|
_SwitchGamepadConfig(
|
|
21
30
|
adapter=adapter,
|
|
22
|
-
|
|
31
|
+
profile_path=profile_path,
|
|
23
32
|
report_period_us=report_period_us,
|
|
24
33
|
controller_colors=controller_colors,
|
|
25
34
|
),
|
|
@@ -32,7 +41,7 @@ def make_joycon_l(
|
|
|
32
41
|
*,
|
|
33
42
|
transport: HidDeviceTransport,
|
|
34
43
|
adapter: str | None = None,
|
|
35
|
-
|
|
44
|
+
profile_path: str | None = None,
|
|
36
45
|
report_period_us: int | None = None,
|
|
37
46
|
controller_colors: ControllerColors | None = None,
|
|
38
47
|
diagnostics: DiagnosticsConfig | None = None,
|
|
@@ -41,7 +50,7 @@ def make_joycon_l(
|
|
|
41
50
|
return JoyConL._from_config(
|
|
42
51
|
_SwitchGamepadConfig(
|
|
43
52
|
adapter=adapter,
|
|
44
|
-
|
|
53
|
+
profile_path=profile_path,
|
|
45
54
|
report_period_us=report_period_us,
|
|
46
55
|
controller_colors=controller_colors,
|
|
47
56
|
profile=JoyConLeftProfile(),
|
|
@@ -55,7 +64,7 @@ def make_joycon_r(
|
|
|
55
64
|
*,
|
|
56
65
|
transport: HidDeviceTransport,
|
|
57
66
|
adapter: str | None = None,
|
|
58
|
-
|
|
67
|
+
profile_path: str | None = None,
|
|
59
68
|
report_period_us: int | None = None,
|
|
60
69
|
controller_colors: ControllerColors | None = None,
|
|
61
70
|
diagnostics: DiagnosticsConfig | None = None,
|
|
@@ -64,7 +73,7 @@ def make_joycon_r(
|
|
|
64
73
|
return JoyConR._from_config(
|
|
65
74
|
_SwitchGamepadConfig(
|
|
66
75
|
adapter=adapter,
|
|
67
|
-
|
|
76
|
+
profile_path=profile_path,
|
|
68
77
|
report_period_us=report_period_us,
|
|
69
78
|
controller_colors=controller_colors,
|
|
70
79
|
profile=JoyConRightProfile(),
|
|
@@ -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
|
+
profile_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
|
+
profile_path=profile_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
|
+
profile_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
|
+
profile_path=profile_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
|
+
profile_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
|
+
profile_path=profile_path,
|
|
140
|
+
controller_colors=controller_colors,
|
|
141
|
+
profile=JoyConRightProfile(),
|
|
142
|
+
),
|
|
143
|
+
diagnostics=diagnostics,
|
|
144
|
+
transport=transport,
|
|
145
|
+
)
|
swbt/diagnostics.py
CHANGED
|
@@ -112,7 +112,7 @@ class DiagnosticsRecorder:
|
|
|
112
112
|
return diagnostics_event
|
|
113
113
|
|
|
114
114
|
def record_report_tx(self, *, report_id: int, reason: str) -> DiagnosticsEvent:
|
|
115
|
-
"""Record one
|
|
115
|
+
"""Record one transport-accepted report and increment its counter."""
|
|
116
116
|
counter = self._report_counters.get(report_id, 0) + 1
|
|
117
117
|
self._report_counters[report_id] = counter
|
|
118
118
|
return self.record_event(
|
|
@@ -139,9 +139,7 @@ class DiagnosticsRecorder:
|
|
|
139
139
|
self,
|
|
140
140
|
*,
|
|
141
141
|
adapter: str,
|
|
142
|
-
|
|
143
|
-
key_store_path: str | None = None,
|
|
144
|
-
key_store_previous_exists: bool | None = None,
|
|
142
|
+
profile_path: str | None = None,
|
|
145
143
|
) -> DiagnosticsEvent:
|
|
146
144
|
"""Record environment metadata for one diagnostics run."""
|
|
147
145
|
fields: dict[str, object] = {
|
|
@@ -150,12 +148,8 @@ class DiagnosticsRecorder:
|
|
|
150
148
|
"package_version": self._package_version(),
|
|
151
149
|
"python_version": platform.python_version(),
|
|
152
150
|
}
|
|
153
|
-
if
|
|
154
|
-
fields["
|
|
155
|
-
if key_store_exists is not None:
|
|
156
|
-
fields["key_store_exists"] = key_store_exists
|
|
157
|
-
if key_store_previous_exists is not None:
|
|
158
|
-
fields["key_store_previous_exists"] = key_store_previous_exists
|
|
151
|
+
if profile_path is not None:
|
|
152
|
+
fields["profile_path"] = profile_path
|
|
159
153
|
return self.record_event(
|
|
160
154
|
"run_metadata",
|
|
161
155
|
**fields,
|
swbt/errors.py
CHANGED
|
@@ -63,6 +63,43 @@ class InvalidKeyStoreError(SwbtError):
|
|
|
63
63
|
"""Raised when a key store has an unsupported or invalid shape."""
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
class InvalidProfileError(SwbtError):
|
|
67
|
+
"""Raised when a pairing profile is invalid or unsupported."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProfileControllerMismatchError(InvalidProfileError):
|
|
71
|
+
"""Raised when a profile belongs to a different concrete controller kind."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
expected_controller_kind: str,
|
|
77
|
+
actual_controller_kind: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize expected and actual profile controller kinds."""
|
|
80
|
+
message = (
|
|
81
|
+
"profile controller_kind mismatch: "
|
|
82
|
+
f"expected {expected_controller_kind!r}, got {actual_controller_kind!r}"
|
|
83
|
+
)
|
|
84
|
+
super().__init__(message)
|
|
85
|
+
self.expected_controller_kind = expected_controller_kind
|
|
86
|
+
self.actual_controller_kind = actual_controller_kind
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AdapterIdentityRecoveryRequired(SwbtError): # noqa: N818
|
|
90
|
+
"""Raised when a volatile write may have changed the adapter identity."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, *, target_address: str, stage: str) -> None:
|
|
93
|
+
"""Initialize the target and failure stage requiring physical recovery."""
|
|
94
|
+
message = (
|
|
95
|
+
"adapter identity preparation became uncertain after write started; "
|
|
96
|
+
"unplug and reconnect the USB dongle before retrying"
|
|
97
|
+
)
|
|
98
|
+
super().__init__(message)
|
|
99
|
+
self.target_address = target_address
|
|
100
|
+
self.stage = stage
|
|
101
|
+
|
|
102
|
+
|
|
66
103
|
class ProtocolError(SwbtError):
|
|
67
104
|
"""Raised when protocol bytes cannot be parsed or produced."""
|
|
68
105
|
|
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/_config.py
CHANGED
|
@@ -13,7 +13,7 @@ class _SwitchGamepadConfig:
|
|
|
13
13
|
|
|
14
14
|
Attributes:
|
|
15
15
|
adapter: Bumble adapter moniker, such as ``"usb:0"``.
|
|
16
|
-
|
|
16
|
+
profile_path: Path to a swbt-owned pairing profile.
|
|
17
17
|
profile: Fixed controller identity and protocol profile.
|
|
18
18
|
report_period_us: Periodic input report interval in microseconds.
|
|
19
19
|
device_name: HID device name advertised to the host.
|
|
@@ -21,7 +21,7 @@ class _SwitchGamepadConfig:
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
adapter: str | None = None
|
|
24
|
-
|
|
24
|
+
profile_path: str | None = None
|
|
25
25
|
profile: ControllerProfile = field(default_factory=default_controller_profile)
|
|
26
26
|
report_period_us: int | None = None
|
|
27
27
|
device_name: str | None = None
|
|
@@ -60,14 +60,14 @@ class _ControllerSpec:
|
|
|
60
60
|
self,
|
|
61
61
|
*,
|
|
62
62
|
adapter: str | None,
|
|
63
|
-
key_store_path: str | None,
|
|
64
63
|
report_period_us: int | None,
|
|
65
64
|
controller_colors: ControllerColors | None,
|
|
65
|
+
profile_path: str | None = None,
|
|
66
66
|
) -> _SwitchGamepadConfig:
|
|
67
67
|
"""Create internal construction config from public constructor options."""
|
|
68
68
|
return _SwitchGamepadConfig(
|
|
69
69
|
adapter=adapter,
|
|
70
|
-
|
|
70
|
+
profile_path=profile_path,
|
|
71
71
|
profile=self.profile,
|
|
72
72
|
report_period_us=report_period_us,
|
|
73
73
|
controller_colors=controller_colors,
|
|
@@ -79,7 +79,7 @@ class _RuntimeConfig:
|
|
|
79
79
|
"""Normalized internal configuration for ControllerRuntime."""
|
|
80
80
|
|
|
81
81
|
adapter: str | None
|
|
82
|
-
|
|
82
|
+
profile_path: str | None
|
|
83
83
|
profile: ControllerProfile
|
|
84
84
|
report_period_us: int
|
|
85
85
|
device_name: str
|
|
@@ -93,7 +93,7 @@ class _RuntimeConfig:
|
|
|
93
93
|
raise InvalidInputError(msg)
|
|
94
94
|
return cls(
|
|
95
95
|
adapter=config.adapter,
|
|
96
|
-
|
|
96
|
+
profile_path=config.profile_path,
|
|
97
97
|
profile=config.profile,
|
|
98
98
|
report_period_us=config.report_period_us,
|
|
99
99
|
device_name=config.device_name,
|
swbt/gamepad/connection.py
CHANGED
|
@@ -58,7 +58,7 @@ class ConnectionWorkflow:
|
|
|
58
58
|
diagnostics: DiagnosticsRecorder
|
|
59
59
|
ensure_open: EnsureOpen
|
|
60
60
|
get_transport: TransportProvider
|
|
61
|
-
|
|
61
|
+
profile_path: str | None
|
|
62
62
|
pair: PairWithTimeout
|
|
63
63
|
set_connection_state: StateSetter
|
|
64
64
|
transport_was_injected: bool
|
|
@@ -71,10 +71,10 @@ class ConnectionWorkflow:
|
|
|
71
71
|
"""Try active reconnect with exactly one bonded peer."""
|
|
72
72
|
await self.ensure_open()
|
|
73
73
|
transport = self._transport()
|
|
74
|
-
if self.
|
|
74
|
+
if self.profile_path is None and not self.transport_was_injected:
|
|
75
75
|
self.diagnostics.record_event(
|
|
76
|
-
"
|
|
77
|
-
reason="
|
|
76
|
+
"reconnect_profile_unavailable",
|
|
77
|
+
reason="profile_path_none",
|
|
78
78
|
route="active_reconnect",
|
|
79
79
|
)
|
|
80
80
|
peers = await transport.list_bonded_peers()
|