swbt-python 0.5.2__py3-none-any.whl → 0.5.4__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/gamepad/interface.py +18 -14
- swbt/gamepad/runtime.py +93 -23
- swbt/report_loop.py +11 -0
- swbt/transport/_bumble_acl.py +9 -25
- swbt/transport/_bumble_key_store.py +2 -28
- swbt/transport/_bumble_lifecycle.py +28 -54
- swbt/transport/_pairing_profile.py +7 -11
- swbt/transport/bumble.py +8 -7
- {swbt_python-0.5.2.dist-info → swbt_python-0.5.4.dist-info}/METADATA +2 -2
- {swbt_python-0.5.2.dist-info → swbt_python-0.5.4.dist-info}/RECORD +13 -13
- {swbt_python-0.5.2.dist-info → swbt_python-0.5.4.dist-info}/WHEEL +0 -0
- {swbt_python-0.5.2.dist-info → swbt_python-0.5.4.dist-info}/entry_points.txt +0 -0
- {swbt_python-0.5.2.dist-info → swbt_python-0.5.4.dist-info}/licenses/LICENSE +0 -0
swbt/gamepad/interface.py
CHANGED
|
@@ -121,11 +121,11 @@ class SwitchGamepad(ABC):
|
|
|
121
121
|
await self._runtime.open()
|
|
122
122
|
|
|
123
123
|
async def pair(self, timeout: float | None = None) -> None: # noqa: ASYNC109
|
|
124
|
-
"""Start pairing
|
|
124
|
+
"""Start pairing and wait until the controller can accept normal input.
|
|
125
125
|
|
|
126
126
|
Args:
|
|
127
|
-
timeout: Maximum seconds for link connection
|
|
128
|
-
``None`` waits without a deadline.
|
|
127
|
+
timeout: Maximum seconds for link connection, protocol initialization,
|
|
128
|
+
and normal-input readiness. ``None`` waits without a deadline.
|
|
129
129
|
|
|
130
130
|
Raises:
|
|
131
131
|
ConnectionTimeoutError: The timeout elapsed before a connection completed.
|
|
@@ -134,15 +134,15 @@ class SwitchGamepad(ABC):
|
|
|
134
134
|
await self._runtime.pair(timeout=timeout)
|
|
135
135
|
|
|
136
136
|
async def reconnect(self, timeout: float | None = None) -> None: # noqa: ASYNC109
|
|
137
|
-
"""Reconnect with
|
|
137
|
+
"""Reconnect with one bonded peer and wait until normal input can begin.
|
|
138
138
|
|
|
139
139
|
Args:
|
|
140
|
-
timeout: Maximum seconds for the active reconnect attempt
|
|
141
|
-
the transport default.
|
|
140
|
+
timeout: Maximum seconds for the active reconnect attempt and
|
|
141
|
+
normal-input readiness. ``None`` uses the transport default.
|
|
142
142
|
|
|
143
143
|
Raises:
|
|
144
144
|
ConnectionFailedError: No single bonded peer was available or reconnect failed.
|
|
145
|
-
ConnectionTimeoutError: The
|
|
145
|
+
ConnectionTimeoutError: The reconnect or normal-input readiness timed out.
|
|
146
146
|
InvalidKeyStoreError: The key store cannot identify one current peer.
|
|
147
147
|
"""
|
|
148
148
|
await self._runtime.reconnect(timeout=timeout)
|
|
@@ -154,11 +154,12 @@ class SwitchGamepad(ABC):
|
|
|
154
154
|
"""Try active reconnect with exactly one bonded peer.
|
|
155
155
|
|
|
156
156
|
Args:
|
|
157
|
-
timeout: Maximum seconds for the active reconnect attempt
|
|
158
|
-
the transport default.
|
|
157
|
+
timeout: Maximum seconds for the active reconnect attempt and
|
|
158
|
+
normal-input readiness. ``None`` uses the transport default.
|
|
159
159
|
|
|
160
160
|
Returns:
|
|
161
|
-
Reconnect route, status, selected peer, and peer count.
|
|
161
|
+
Reconnect route, status, selected peer, and peer count. ``connected``
|
|
162
|
+
means that normal input can begin.
|
|
162
163
|
|
|
163
164
|
Raises:
|
|
164
165
|
InvalidKeyStoreError: The key store cannot identify one current peer.
|
|
@@ -171,7 +172,7 @@ class SwitchGamepad(ABC):
|
|
|
171
172
|
timeout: float | None = None, # noqa: ASYNC109
|
|
172
173
|
allow_pairing: bool = False,
|
|
173
174
|
) -> None:
|
|
174
|
-
"""Connect
|
|
175
|
+
"""Connect by bonded reconnect or pairing and wait for normal-input readiness.
|
|
175
176
|
|
|
176
177
|
Args:
|
|
177
178
|
timeout: Maximum seconds for each connection attempt. ``None`` uses the
|
|
@@ -199,7 +200,8 @@ class SwitchGamepad(ABC):
|
|
|
199
200
|
allow_pairing: If ``True``, run pairing when no bonded peer is available.
|
|
200
201
|
|
|
201
202
|
Returns:
|
|
202
|
-
Route and status chosen by reconnect or pairing fallback.
|
|
203
|
+
Route and status chosen by reconnect or pairing fallback. ``connected``
|
|
204
|
+
means that normal input can begin.
|
|
203
205
|
|
|
204
206
|
Raises:
|
|
205
207
|
InvalidKeyStoreError: The key store cannot identify one current peer.
|
|
@@ -375,7 +377,8 @@ class PeriodicSwitchGamepad(SwitchGamepad):
|
|
|
375
377
|
diagnostics: Optional diagnostics configuration for trace output.
|
|
376
378
|
|
|
377
379
|
Returns:
|
|
378
|
-
|
|
380
|
+
A periodic controller ready to accept normal input. The caller owns
|
|
381
|
+
its lifetime.
|
|
379
382
|
|
|
380
383
|
Raises:
|
|
381
384
|
ValueError: ``local_address`` is invalid.
|
|
@@ -440,7 +443,8 @@ class DirectSwitchGamepad(SwitchGamepad):
|
|
|
440
443
|
diagnostics: Optional diagnostics configuration for trace output.
|
|
441
444
|
|
|
442
445
|
Returns:
|
|
443
|
-
|
|
446
|
+
A direct controller ready to accept normal input. The caller owns
|
|
447
|
+
its lifetime.
|
|
444
448
|
|
|
445
449
|
Raises:
|
|
446
450
|
ValueError: ``local_address`` is invalid.
|
swbt/gamepad/runtime.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
+
from contextlib import suppress
|
|
5
6
|
from dataclasses import replace
|
|
7
|
+
from time import monotonic
|
|
6
8
|
from typing import Literal
|
|
7
9
|
|
|
8
10
|
import swbt.gamepad as gamepad_module
|
|
@@ -48,6 +50,8 @@ class ControllerRuntime:
|
|
|
48
50
|
diagnostics: DiagnosticsConfig | None = None,
|
|
49
51
|
reporting_mode: Literal["periodic", "direct"] = "periodic",
|
|
50
52
|
transport: HidDeviceTransport | None = None,
|
|
53
|
+
_report_sender_monotonic_time: Callable[[], float] = monotonic,
|
|
54
|
+
_report_sender_sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
51
55
|
) -> None:
|
|
52
56
|
"""Create a fully initialized gamepad runtime.
|
|
53
57
|
|
|
@@ -57,6 +61,8 @@ class ControllerRuntime:
|
|
|
57
61
|
reporting_mode: Owner of normal input report scheduling.
|
|
58
62
|
transport: Optional HID transport instance. When supplied, no Bumble
|
|
59
63
|
transport is created by the constructor.
|
|
64
|
+
_report_sender_monotonic_time: Internal holdoff clock override for tests.
|
|
65
|
+
_report_sender_sleep: Internal holdoff sleep override for tests.
|
|
60
66
|
|
|
61
67
|
Raises:
|
|
62
68
|
InvalidInputError: ``adapter`` is omitted for the default transport.
|
|
@@ -68,6 +74,8 @@ class ControllerRuntime:
|
|
|
68
74
|
self._reporting_mode = reporting_mode
|
|
69
75
|
self._transport = transport
|
|
70
76
|
self._transport_was_injected = transport is not None
|
|
77
|
+
self._report_sender_monotonic_time = _report_sender_monotonic_time
|
|
78
|
+
self._report_sender_sleep = _report_sender_sleep
|
|
71
79
|
self._pairing_profile: PairingProfile | None = None
|
|
72
80
|
self._state_store = InputStateStore()
|
|
73
81
|
self._diagnostics = DiagnosticsRecorder(
|
|
@@ -94,6 +102,7 @@ class ControllerRuntime:
|
|
|
94
102
|
self._report_loop: ReportLoop | None = None
|
|
95
103
|
self._report_sender: ReportSender | None = None
|
|
96
104
|
self._protocol_handshake: ProtocolHandshake | None = None
|
|
105
|
+
self._input_readiness_task: asyncio.Task[None] | None = None
|
|
97
106
|
self._input_operation_lock = asyncio.Lock()
|
|
98
107
|
self._lifecycle_lock = asyncio.Lock()
|
|
99
108
|
self._connected_event = asyncio.Event()
|
|
@@ -139,6 +148,8 @@ class ControllerRuntime:
|
|
|
139
148
|
),
|
|
140
149
|
session=self._protocol_session,
|
|
141
150
|
diagnostics=self._diagnostics,
|
|
151
|
+
monotonic_time=self._report_sender_monotonic_time,
|
|
152
|
+
_sleep=self._report_sender_sleep,
|
|
142
153
|
)
|
|
143
154
|
self._report_sender = report_sender
|
|
144
155
|
self._connection_state = "opened"
|
|
@@ -161,14 +172,14 @@ class ControllerRuntime:
|
|
|
161
172
|
self._configured_device_info_bluetooth_address = None
|
|
162
173
|
|
|
163
174
|
async def pair(self, timeout: float | None = None) -> None: # noqa: ASYNC109
|
|
164
|
-
"""Start pairing
|
|
175
|
+
"""Start pairing and wait until the controller can accept normal input.
|
|
165
176
|
|
|
166
177
|
Args:
|
|
167
|
-
timeout: Maximum seconds for link connection
|
|
168
|
-
``None`` waits without a deadline.
|
|
178
|
+
timeout: Maximum seconds for link connection, protocol initialization,
|
|
179
|
+
and normal-input readiness. ``None`` waits without a deadline.
|
|
169
180
|
|
|
170
181
|
Raises:
|
|
171
|
-
ConnectionTimeoutError: The deadline elapsed before
|
|
182
|
+
ConnectionTimeoutError: The deadline elapsed before normal-input readiness.
|
|
172
183
|
ConnectionFailedError: Protocol initialization or the link failed.
|
|
173
184
|
ClosedError: The transport was unavailable after opening.
|
|
174
185
|
"""
|
|
@@ -186,7 +197,7 @@ class ControllerRuntime:
|
|
|
186
197
|
raise ClosedError(msg)
|
|
187
198
|
await self._transport.start_advertising()
|
|
188
199
|
self._configure_device_info_bluetooth_address(self._transport)
|
|
189
|
-
await self.
|
|
200
|
+
await self._wait_for_input_ready(None)
|
|
190
201
|
|
|
191
202
|
try:
|
|
192
203
|
if timeout is None:
|
|
@@ -194,6 +205,9 @@ class ControllerRuntime:
|
|
|
194
205
|
else:
|
|
195
206
|
async with asyncio.timeout(timeout):
|
|
196
207
|
await advertise_and_wait()
|
|
208
|
+
except asyncio.CancelledError:
|
|
209
|
+
await self._stop_input_readiness_wait()
|
|
210
|
+
raise
|
|
197
211
|
except TimeoutError as error:
|
|
198
212
|
msg = "connection timed out"
|
|
199
213
|
connection_error = ConnectionTimeoutError(msg)
|
|
@@ -204,7 +218,7 @@ class ControllerRuntime:
|
|
|
204
218
|
),
|
|
205
219
|
player_lights=_format_optional_byte(self._protocol_session.state.player_lights),
|
|
206
220
|
report_mode=_format_optional_byte(self._protocol_session.state.report_mode),
|
|
207
|
-
stage=
|
|
221
|
+
stage=self._connection_readiness_stage(),
|
|
208
222
|
state=self._connection_state,
|
|
209
223
|
timeout=timeout,
|
|
210
224
|
)
|
|
@@ -216,15 +230,15 @@ class ControllerRuntime:
|
|
|
216
230
|
raise
|
|
217
231
|
|
|
218
232
|
async def reconnect(self, timeout: float | None = None) -> None: # noqa: ASYNC109
|
|
219
|
-
"""Reconnect with
|
|
233
|
+
"""Reconnect with one bonded peer and wait until normal input can begin.
|
|
220
234
|
|
|
221
235
|
Args:
|
|
222
|
-
timeout: Maximum seconds for the active reconnect attempt
|
|
223
|
-
the transport default.
|
|
236
|
+
timeout: Maximum seconds for the active reconnect attempt and
|
|
237
|
+
normal-input readiness. ``None`` uses the transport default.
|
|
224
238
|
|
|
225
239
|
Raises:
|
|
226
240
|
ConnectionFailedError: No single bonded peer was available or reconnect failed.
|
|
227
|
-
ConnectionTimeoutError: The
|
|
241
|
+
ConnectionTimeoutError: The reconnect or normal-input readiness timed out.
|
|
228
242
|
"""
|
|
229
243
|
result = await self.try_reconnect(timeout=timeout)
|
|
230
244
|
self._raise_if_connection_failed(result)
|
|
@@ -236,8 +250,8 @@ class ControllerRuntime:
|
|
|
236
250
|
"""Try active reconnect with exactly one bonded peer.
|
|
237
251
|
|
|
238
252
|
Args:
|
|
239
|
-
timeout: Maximum seconds for the active reconnect attempt
|
|
240
|
-
the transport default.
|
|
253
|
+
timeout: Maximum seconds for the active reconnect attempt and
|
|
254
|
+
normal-input readiness. ``None`` uses the transport default.
|
|
241
255
|
|
|
242
256
|
Returns:
|
|
243
257
|
ConnectionResult: Reconnect route, status, selected peer, and peer count.
|
|
@@ -308,6 +322,7 @@ class ControllerRuntime:
|
|
|
308
322
|
)
|
|
309
323
|
except asyncio.CancelledError as error:
|
|
310
324
|
if _current_task_is_cancelling():
|
|
325
|
+
await self._stop_input_readiness_wait()
|
|
311
326
|
raise
|
|
312
327
|
return await self._record_active_reconnect_transport_error(
|
|
313
328
|
error,
|
|
@@ -406,6 +421,7 @@ class ControllerRuntime:
|
|
|
406
421
|
except Exception as error: # noqa: BLE001
|
|
407
422
|
self._diagnostics.record_error(error, recoverable=True)
|
|
408
423
|
await self._stop_protocol_handshake()
|
|
424
|
+
await self._stop_input_readiness_wait()
|
|
409
425
|
if self._report_loop is not None:
|
|
410
426
|
await self._report_loop.stop()
|
|
411
427
|
self._disconnect_event.clear()
|
|
@@ -820,7 +836,7 @@ class ControllerRuntime:
|
|
|
820
836
|
|
|
821
837
|
async def _wait_for_reconnect_connected(self, *, max_wait: float | None) -> None:
|
|
822
838
|
try:
|
|
823
|
-
await self.
|
|
839
|
+
await self._wait_for_input_ready(max_wait)
|
|
824
840
|
except TimeoutError as error:
|
|
825
841
|
self._diagnostics.record_event(
|
|
826
842
|
"connection_timeout",
|
|
@@ -830,13 +846,13 @@ class ControllerRuntime:
|
|
|
830
846
|
player_lights=_format_optional_byte(self._protocol_session.state.player_lights),
|
|
831
847
|
report_mode=_format_optional_byte(self._protocol_session.state.report_mode),
|
|
832
848
|
route="active_reconnect",
|
|
833
|
-
stage=
|
|
849
|
+
stage=self._connection_readiness_stage(),
|
|
834
850
|
state=self._connection_state,
|
|
835
851
|
timeout=max_wait,
|
|
836
852
|
)
|
|
837
853
|
raise TimeoutError from error
|
|
838
854
|
|
|
839
|
-
async def
|
|
855
|
+
async def _wait_for_input_ready(self, max_wait: float | None) -> None:
|
|
840
856
|
if max_wait is None:
|
|
841
857
|
await self._connection_attempt_event.wait()
|
|
842
858
|
else:
|
|
@@ -844,9 +860,14 @@ class ControllerRuntime:
|
|
|
844
860
|
await self._connection_attempt_event.wait()
|
|
845
861
|
if self._connected_event.is_set():
|
|
846
862
|
return
|
|
847
|
-
msg = "
|
|
863
|
+
msg = "connection initialization failed"
|
|
848
864
|
raise ConnectionFailedError(msg) from self._connection_attempt_error
|
|
849
865
|
|
|
866
|
+
def _connection_readiness_stage(self) -> str:
|
|
867
|
+
if self._protocol_session.state.protocol_ready:
|
|
868
|
+
return "input_readiness"
|
|
869
|
+
return "protocol_initialization"
|
|
870
|
+
|
|
850
871
|
def _record_disconnect_request_result(self, result: DisconnectRequestResult) -> None:
|
|
851
872
|
fields: dict[str, object] = {"status": result.status}
|
|
852
873
|
if result.channels:
|
|
@@ -917,6 +938,15 @@ class ControllerRuntime:
|
|
|
917
938
|
if handshake is not None:
|
|
918
939
|
await handshake.stop()
|
|
919
940
|
|
|
941
|
+
async def _stop_input_readiness_wait(self) -> None:
|
|
942
|
+
task = self._input_readiness_task
|
|
943
|
+
self._input_readiness_task = None
|
|
944
|
+
if task is None or task is asyncio.current_task():
|
|
945
|
+
return
|
|
946
|
+
task.cancel()
|
|
947
|
+
with suppress(asyncio.CancelledError):
|
|
948
|
+
await task
|
|
949
|
+
|
|
920
950
|
def _start_periodic_report_loop(self) -> None:
|
|
921
951
|
if self._transport is None or self._report_loop is not None:
|
|
922
952
|
return
|
|
@@ -950,12 +980,7 @@ class ControllerRuntime:
|
|
|
950
980
|
return
|
|
951
981
|
if self._connected_event.is_set():
|
|
952
982
|
return
|
|
953
|
-
self._connection_state = "connected"
|
|
954
|
-
self._connected_event.set()
|
|
955
|
-
self._connection_attempt_event.set()
|
|
956
983
|
self._protocol_handshake = None
|
|
957
|
-
if self._reporting_mode == "periodic":
|
|
958
|
-
self._start_periodic_report_loop()
|
|
959
984
|
session_state = self._protocol_session.state
|
|
960
985
|
self._diagnostics.record_event(
|
|
961
986
|
"protocol_ready",
|
|
@@ -965,6 +990,50 @@ class ControllerRuntime:
|
|
|
965
990
|
route=self._connection_route,
|
|
966
991
|
observed_subcommands=_format_subcommands(session_state.observed_subcommands),
|
|
967
992
|
)
|
|
993
|
+
if self._reporting_mode == "direct":
|
|
994
|
+
self._publish_input_ready()
|
|
995
|
+
return
|
|
996
|
+
if self._input_readiness_task is not None:
|
|
997
|
+
return
|
|
998
|
+
self._input_readiness_task = asyncio.create_task(
|
|
999
|
+
self._complete_periodic_input_readiness(),
|
|
1000
|
+
name="swbt-input-readiness",
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
async def _complete_periodic_input_readiness(self) -> None:
|
|
1004
|
+
task = asyncio.current_task()
|
|
1005
|
+
sender = self._require_report_sender()
|
|
1006
|
+
try:
|
|
1007
|
+
await sender.wait_until_automatic_input_ready()
|
|
1008
|
+
if (
|
|
1009
|
+
self._close_in_progress
|
|
1010
|
+
or self._connection_attempt_event.is_set()
|
|
1011
|
+
or self._report_sender is not sender
|
|
1012
|
+
):
|
|
1013
|
+
return
|
|
1014
|
+
self._start_periodic_report_loop()
|
|
1015
|
+
self._publish_input_ready()
|
|
1016
|
+
except asyncio.CancelledError:
|
|
1017
|
+
raise
|
|
1018
|
+
except Exception as error: # noqa: BLE001
|
|
1019
|
+
if not self._close_in_progress:
|
|
1020
|
+
self._record_protocol_initialization_failure(error)
|
|
1021
|
+
finally:
|
|
1022
|
+
if self._input_readiness_task is task:
|
|
1023
|
+
self._input_readiness_task = None
|
|
1024
|
+
|
|
1025
|
+
def _publish_input_ready(self) -> None:
|
|
1026
|
+
if self._connected_event.is_set() or self._connection_attempt_event.is_set():
|
|
1027
|
+
return
|
|
1028
|
+
self._connection_state = "connected"
|
|
1029
|
+
self._connected_event.set()
|
|
1030
|
+
self._connection_attempt_event.set()
|
|
1031
|
+
self._diagnostics.record_event(
|
|
1032
|
+
"input_ready",
|
|
1033
|
+
profile_kind=self._controller_profile.kind.value,
|
|
1034
|
+
reporting_mode=self._reporting_mode,
|
|
1035
|
+
route=self._connection_route,
|
|
1036
|
+
)
|
|
968
1037
|
|
|
969
1038
|
async def _fail_protocol_initialization(self, error: Exception) -> None:
|
|
970
1039
|
if self._protocol_handshake is not None:
|
|
@@ -987,7 +1056,7 @@ class ControllerRuntime:
|
|
|
987
1056
|
profile_kind=self._controller_profile.kind.value,
|
|
988
1057
|
report_mode=_format_optional_byte(self._protocol_session.state.report_mode),
|
|
989
1058
|
route=self._connection_route,
|
|
990
|
-
stage=
|
|
1059
|
+
stage=self._connection_readiness_stage(),
|
|
991
1060
|
)
|
|
992
1061
|
self._diagnostics.record_error(error, recoverable=False)
|
|
993
1062
|
|
|
@@ -999,7 +1068,7 @@ class ControllerRuntime:
|
|
|
999
1068
|
and not self._connection_attempt_event.is_set()
|
|
1000
1069
|
and not self._close_in_progress
|
|
1001
1070
|
):
|
|
1002
|
-
msg = "disconnected before
|
|
1071
|
+
msg = "disconnected before connection initialization completed"
|
|
1003
1072
|
handshake_failure = ConnectionFailedError(msg)
|
|
1004
1073
|
if self._protocol_handshake is not None:
|
|
1005
1074
|
self._protocol_handshake.fail(handshake_failure)
|
|
@@ -1014,6 +1083,7 @@ class ControllerRuntime:
|
|
|
1014
1083
|
self._protocol_handshake = None
|
|
1015
1084
|
else:
|
|
1016
1085
|
await self._stop_protocol_handshake()
|
|
1086
|
+
await self._stop_input_readiness_wait()
|
|
1017
1087
|
if self._report_loop is not None:
|
|
1018
1088
|
await self._report_loop.stop()
|
|
1019
1089
|
self._report_loop = None
|
swbt/report_loop.py
CHANGED
|
@@ -34,6 +34,7 @@ class ReportSender:
|
|
|
34
34
|
diagnostics: DiagnosticsRecorder | None = None,
|
|
35
35
|
clock_ns: Callable[[], int] = monotonic_ns,
|
|
36
36
|
monotonic_time: Callable[[], float] = monotonic,
|
|
37
|
+
_sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
37
38
|
) -> None:
|
|
38
39
|
"""Create a session-scoped serialized report sender."""
|
|
39
40
|
self._transport = transport
|
|
@@ -42,6 +43,7 @@ class ReportSender:
|
|
|
42
43
|
self._diagnostics = diagnostics
|
|
43
44
|
self._clock_ns = clock_ns
|
|
44
45
|
self._monotonic_time = monotonic_time
|
|
46
|
+
self._sleep = _sleep
|
|
45
47
|
self._timer = 0
|
|
46
48
|
self._send_lock = asyncio.Lock()
|
|
47
49
|
self._automatic_input_holdoff_until = 0.0
|
|
@@ -80,6 +82,15 @@ class ReportSender:
|
|
|
80
82
|
await self._send_input_locked(state, reason=reason)
|
|
81
83
|
return True
|
|
82
84
|
|
|
85
|
+
async def wait_until_automatic_input_ready(self) -> None:
|
|
86
|
+
"""Wait until automatic input is no longer held off by an accepted reply."""
|
|
87
|
+
while True:
|
|
88
|
+
async with self._send_lock:
|
|
89
|
+
remaining = self._automatic_input_holdoff_until - self._monotonic_time()
|
|
90
|
+
if remaining <= 0:
|
|
91
|
+
return
|
|
92
|
+
await self._sleep(remaining)
|
|
93
|
+
|
|
83
94
|
async def send_subcommand_reply(self, build_report: ReplyBuilder) -> bytes:
|
|
84
95
|
"""Build and send a subcommand reply under the shared send lock."""
|
|
85
96
|
async with self._send_lock:
|
swbt/transport/_bumble_acl.py
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
1
|
-
"""ACL queue drain helper for Bumble HID transport."""
|
|
1
|
+
"""ACL queue drain helper for the pinned Bumble HID transport."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import Any, cast
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
async def drain_bumble_acl_queue(l2cap_channel: object) -> None:
|
|
7
|
-
"""Wait
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
acl_packet_queue =
|
|
11
|
-
if acl_packet_queue is None
|
|
12
|
-
device = getattr(connection, "device", None)
|
|
13
|
-
host = getattr(device, "host", None)
|
|
14
|
-
get_data_packet_queue = getattr(host, "get_data_packet_queue", None)
|
|
15
|
-
if callable(get_data_packet_queue):
|
|
16
|
-
acl_packet_queue = get_data_packet_queue(connection_handle)
|
|
17
|
-
drain = getattr(acl_packet_queue, "drain", None)
|
|
18
|
-
if not isinstance(connection_handle, int) or not callable(drain):
|
|
7
|
+
"""Wait for the pinned Bumble connection's pending ACL packets to complete."""
|
|
8
|
+
channel = cast("Any", l2cap_channel)
|
|
9
|
+
connection = channel.connection
|
|
10
|
+
acl_packet_queue = connection.device.host.get_data_packet_queue(connection.handle)
|
|
11
|
+
if acl_packet_queue is None:
|
|
19
12
|
return
|
|
20
13
|
try:
|
|
21
|
-
|
|
22
|
-
while True:
|
|
23
|
-
pending = getattr(acl_packet_queue, "pending", 0)
|
|
24
|
-
if not isinstance(pending, int) or pending <= 0 or pending == last_pending:
|
|
25
|
-
return
|
|
26
|
-
last_pending = pending
|
|
27
|
-
drain_result = drain(connection_handle)
|
|
28
|
-
if isinstance(drain_result, Awaitable):
|
|
29
|
-
await drain_result
|
|
30
|
-
continue
|
|
31
|
-
return
|
|
14
|
+
await acl_packet_queue.drain(connection.handle)
|
|
32
15
|
except ValueError:
|
|
16
|
+
# Bumble raises when the queue has no state for a channel that was already drained.
|
|
33
17
|
return
|
|
@@ -9,11 +9,9 @@ from swbt.diagnostics import DiagnosticsRecorder
|
|
|
9
9
|
from swbt.errors import InvalidKeyStoreError
|
|
10
10
|
from swbt.transport._pairing_profile import KeyStoreNamespaces, PairingProfile
|
|
11
11
|
|
|
12
|
-
PREVIOUS_NAMESPACE_PREFIX = "swbt.previous::"
|
|
13
|
-
|
|
14
12
|
|
|
15
13
|
class _PairingProfileKeyStore:
|
|
16
|
-
"""Bumble-compatible profile key store
|
|
14
|
+
"""Bumble-compatible profile key store for the current namespace."""
|
|
17
15
|
|
|
18
16
|
def __init__(
|
|
19
17
|
self,
|
|
@@ -24,7 +22,6 @@ class _PairingProfileKeyStore:
|
|
|
24
22
|
self._profile_path = Path(profile_path)
|
|
25
23
|
self._namespace = namespace if isinstance(namespace, str) else None
|
|
26
24
|
self._namespace_resolver = None if isinstance(namespace, str) else namespace
|
|
27
|
-
self.last_update_previous_saved = False
|
|
28
25
|
|
|
29
26
|
def _resolve_namespace(self) -> str:
|
|
30
27
|
namespace = (
|
|
@@ -36,16 +33,9 @@ class _PairingProfileKeyStore:
|
|
|
36
33
|
return namespace
|
|
37
34
|
|
|
38
35
|
async def update(self, name: str, keys: object) -> None:
|
|
39
|
-
"""
|
|
36
|
+
"""Replace the current namespace with one peer's keys."""
|
|
40
37
|
current_store = self._current_store()
|
|
41
38
|
db, current_key_map = await current_store.load()
|
|
42
|
-
previous_namespace = self._previous_namespace(current_store)
|
|
43
|
-
previous_key_map = copy.deepcopy(current_key_map)
|
|
44
|
-
self.last_update_previous_saved = bool(previous_key_map)
|
|
45
|
-
if previous_key_map:
|
|
46
|
-
db[previous_namespace] = previous_key_map
|
|
47
|
-
else:
|
|
48
|
-
db.pop(previous_namespace, None)
|
|
49
39
|
current_key_map.clear()
|
|
50
40
|
current_key_map[name] = cast("Any", keys).to_dict()
|
|
51
41
|
await current_store.save(db)
|
|
@@ -84,10 +74,6 @@ class _PairingProfileKeyStore:
|
|
|
84
74
|
adapter_default=self._namespace_resolver is not None,
|
|
85
75
|
)
|
|
86
76
|
|
|
87
|
-
@staticmethod
|
|
88
|
-
def _previous_namespace(current_store: "_PairingProfileNamespaceStore") -> str:
|
|
89
|
-
return f"{PREVIOUS_NAMESPACE_PREFIX}{current_store.namespace}"
|
|
90
|
-
|
|
91
77
|
|
|
92
78
|
class _PairingProfileNamespaceStore:
|
|
93
79
|
"""JsonKeyStore-compatible view over one profile namespace map."""
|
|
@@ -169,20 +155,16 @@ class _DiagnosticKeyStore:
|
|
|
169
155
|
try:
|
|
170
156
|
await cast("Any", self._key_store).update(name, keys)
|
|
171
157
|
except Exception as error:
|
|
172
|
-
fields = self._generation_fields()
|
|
173
158
|
self._diagnostics.record_event(
|
|
174
159
|
"key_store_update",
|
|
175
|
-
**fields,
|
|
176
160
|
error_type=type(error).__name__,
|
|
177
161
|
message=str(error),
|
|
178
162
|
peer_address=name,
|
|
179
163
|
status="failed",
|
|
180
164
|
)
|
|
181
165
|
raise
|
|
182
|
-
fields = self._generation_fields()
|
|
183
166
|
self._diagnostics.record_event(
|
|
184
167
|
"key_store_update",
|
|
185
|
-
**fields,
|
|
186
168
|
peer_address=name,
|
|
187
169
|
status="succeeded",
|
|
188
170
|
)
|
|
@@ -209,11 +191,3 @@ class _DiagnosticKeyStore:
|
|
|
209
191
|
|
|
210
192
|
def __getattr__(self, name: str) -> object:
|
|
211
193
|
return getattr(self._key_store, name)
|
|
212
|
-
|
|
213
|
-
def _generation_fields(self) -> dict[str, object]:
|
|
214
|
-
if not isinstance(self._key_store, _PairingProfileKeyStore):
|
|
215
|
-
return {}
|
|
216
|
-
return {
|
|
217
|
-
"generation": "current",
|
|
218
|
-
"previous_saved": self._key_store.last_update_previous_saved,
|
|
219
|
-
}
|
|
@@ -25,20 +25,19 @@ class ConnectionDiagnostics:
|
|
|
25
25
|
self._record_event = record_event
|
|
26
26
|
|
|
27
27
|
def register(self, connection: object) -> None:
|
|
28
|
-
"""Register all
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
28
|
+
"""Register all diagnostics callbacks provided by Bumble 0.0.233."""
|
|
29
|
+
connection_with_attrs = cast("Any", connection)
|
|
30
|
+
on_event = connection_with_attrs.on
|
|
32
31
|
on_event(
|
|
33
|
-
|
|
32
|
+
connection_with_attrs.EVENT_DISCONNECTION,
|
|
34
33
|
self._handle_disconnection,
|
|
35
34
|
)
|
|
36
35
|
on_event(
|
|
37
|
-
|
|
36
|
+
connection_with_attrs.EVENT_CLASSIC_PAIRING,
|
|
38
37
|
lambda *_args: self._record_event("classic_pairing", adapter=self._adapter),
|
|
39
38
|
)
|
|
40
39
|
on_event(
|
|
41
|
-
|
|
40
|
+
connection_with_attrs.EVENT_CLASSIC_PAIRING_FAILURE,
|
|
42
41
|
lambda reason=None, *_args: self._record_event(
|
|
43
42
|
"classic_pairing_failure",
|
|
44
43
|
adapter=self._adapter,
|
|
@@ -46,15 +45,15 @@ class ConnectionDiagnostics:
|
|
|
46
45
|
),
|
|
47
46
|
)
|
|
48
47
|
on_event(
|
|
49
|
-
|
|
48
|
+
connection_with_attrs.EVENT_PAIRING_START,
|
|
50
49
|
lambda *_args: self._record_event("pairing_start", adapter=self._adapter),
|
|
51
50
|
)
|
|
52
51
|
on_event(
|
|
53
|
-
|
|
52
|
+
connection_with_attrs.EVENT_PAIRING,
|
|
54
53
|
lambda keys=None, *_args: self._record_pairing_complete(connection, keys),
|
|
55
54
|
)
|
|
56
55
|
on_event(
|
|
57
|
-
|
|
56
|
+
connection_with_attrs.EVENT_PAIRING_FAILURE,
|
|
58
57
|
lambda reason=None, *_args: self._record_event(
|
|
59
58
|
"pairing_failure",
|
|
60
59
|
adapter=self._adapter,
|
|
@@ -62,15 +61,11 @@ class ConnectionDiagnostics:
|
|
|
62
61
|
),
|
|
63
62
|
)
|
|
64
63
|
on_event(
|
|
65
|
-
|
|
64
|
+
connection_with_attrs.EVENT_CONNECTION_AUTHENTICATION,
|
|
66
65
|
lambda *_args: self._record_connection_authentication(connection),
|
|
67
66
|
)
|
|
68
67
|
on_event(
|
|
69
|
-
|
|
70
|
-
connection,
|
|
71
|
-
"EVENT_CONNECTION_AUTHENTICATION_FAILURE",
|
|
72
|
-
"connection_authentication_failure",
|
|
73
|
-
),
|
|
68
|
+
connection_with_attrs.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
|
|
74
69
|
lambda error=None, *_args: self._record_event(
|
|
75
70
|
"connection_authentication_failure",
|
|
76
71
|
adapter=self._adapter,
|
|
@@ -78,19 +73,11 @@ class ConnectionDiagnostics:
|
|
|
78
73
|
),
|
|
79
74
|
)
|
|
80
75
|
on_event(
|
|
81
|
-
|
|
82
|
-
connection,
|
|
83
|
-
"EVENT_CONNECTION_ENCRYPTION_CHANGE",
|
|
84
|
-
"connection_encryption_change",
|
|
85
|
-
),
|
|
76
|
+
connection_with_attrs.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
|
86
77
|
lambda *_args: self._record_connection_encryption_change(connection),
|
|
87
78
|
)
|
|
88
79
|
on_event(
|
|
89
|
-
|
|
90
|
-
connection,
|
|
91
|
-
"EVENT_CONNECTION_ENCRYPTION_FAILURE",
|
|
92
|
-
"connection_encryption_failure",
|
|
93
|
-
),
|
|
80
|
+
connection_with_attrs.EVENT_CONNECTION_ENCRYPTION_FAILURE,
|
|
94
81
|
lambda error=None, *_args: self._record_event(
|
|
95
82
|
"connection_encryption_failure",
|
|
96
83
|
adapter=self._adapter,
|
|
@@ -98,23 +85,19 @@ class ConnectionDiagnostics:
|
|
|
98
85
|
),
|
|
99
86
|
)
|
|
100
87
|
on_event(
|
|
101
|
-
|
|
102
|
-
connection,
|
|
103
|
-
"EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH",
|
|
104
|
-
"connection_encryption_key_refresh",
|
|
105
|
-
),
|
|
88
|
+
connection_with_attrs.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH,
|
|
106
89
|
lambda *_args: self._record_connection_encryption_refresh(connection),
|
|
107
90
|
)
|
|
108
91
|
on_event(
|
|
109
|
-
|
|
92
|
+
connection_with_attrs.EVENT_LINK_KEY,
|
|
110
93
|
lambda *_args: self._record_event("link_key_available", adapter=self._adapter),
|
|
111
94
|
)
|
|
112
95
|
on_event(
|
|
113
|
-
|
|
96
|
+
connection_with_attrs.EVENT_MODE_CHANGE,
|
|
114
97
|
lambda *_args: self._record_classic_mode_change(connection),
|
|
115
98
|
)
|
|
116
99
|
on_event(
|
|
117
|
-
|
|
100
|
+
connection_with_attrs.EVENT_MODE_CHANGE_FAILURE,
|
|
118
101
|
lambda status=None, *_args: self._record_event(
|
|
119
102
|
"classic_mode_change_failure",
|
|
120
103
|
adapter=self._adapter,
|
|
@@ -179,7 +162,8 @@ def register_connection_request_bridge(
|
|
|
179
162
|
record_event: EventRecorder,
|
|
180
163
|
) -> None:
|
|
181
164
|
"""Wrap Bumble's connection request callback while avoiding deprecated sync APIs."""
|
|
182
|
-
|
|
165
|
+
device_with_attrs = cast("Any", device)
|
|
166
|
+
original_connection_request = device_with_attrs.on_connection_request
|
|
183
167
|
|
|
184
168
|
def on_connection_request(
|
|
185
169
|
bd_addr: object,
|
|
@@ -201,7 +185,6 @@ def register_connection_request_bridge(
|
|
|
201
185
|
link_type,
|
|
202
186
|
)
|
|
203
187
|
|
|
204
|
-
device_with_attrs = cast("Any", device)
|
|
205
188
|
device_with_attrs.on_connection_request = on_connection_request
|
|
206
189
|
replace_host_connection_request_listener(
|
|
207
190
|
device,
|
|
@@ -267,24 +250,18 @@ def call_connection_request_without_deprecated_sync_command(
|
|
|
267
250
|
link_type: int,
|
|
268
251
|
) -> None:
|
|
269
252
|
"""Run Bumble's connection request handler without its deprecated sync helper."""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if host is None or not callable(send_async_command):
|
|
273
|
-
connection_request(bd_addr, class_of_device, link_type)
|
|
274
|
-
return
|
|
253
|
+
device_with_attrs = cast("Any", device)
|
|
254
|
+
host = device_with_attrs.host
|
|
275
255
|
|
|
276
256
|
from bumble import utils # noqa: PLC0415
|
|
277
257
|
|
|
278
258
|
missing = object()
|
|
279
|
-
|
|
280
|
-
previous_instance_attr = (
|
|
281
|
-
host_dict.get("send_command_sync", missing) if isinstance(host_dict, dict) else missing
|
|
282
|
-
)
|
|
259
|
+
previous_instance_attr = host.__dict__.get("send_command_sync", missing)
|
|
283
260
|
|
|
284
261
|
def send_command_sync(command: object) -> None:
|
|
285
|
-
utils.AsyncRunner.spawn(send_async_command(command))
|
|
262
|
+
utils.AsyncRunner.spawn(host.send_async_command(command))
|
|
286
263
|
|
|
287
|
-
host_with_attrs =
|
|
264
|
+
host_with_attrs = host
|
|
288
265
|
host_with_attrs.send_command_sync = send_command_sync
|
|
289
266
|
try:
|
|
290
267
|
connection_request(bd_addr, class_of_device, link_type)
|
|
@@ -300,12 +277,9 @@ def replace_host_connection_request_listener(
|
|
|
300
277
|
original_connection_request: Callable[[object, int, int], None],
|
|
301
278
|
replacement_connection_request: Callable[[object, int, int], None],
|
|
302
279
|
) -> None:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
on = getattr(host, "on", None)
|
|
306
|
-
if not callable(remove_listener) or not callable(on):
|
|
307
|
-
return
|
|
280
|
+
device_with_attrs = cast("Any", device)
|
|
281
|
+
host = device_with_attrs.host
|
|
308
282
|
|
|
309
283
|
with suppress(KeyError, ValueError):
|
|
310
|
-
remove_listener("connection_request", original_connection_request)
|
|
311
|
-
on("connection_request", replacement_connection_request)
|
|
284
|
+
host.remove_listener("connection_request", original_connection_request)
|
|
285
|
+
host.on("connection_request", replacement_connection_request)
|
|
@@ -16,7 +16,7 @@ _ADDRESS_PATTERN = re.compile(r"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
|
|
16
16
|
_RESERVED_INQUIRY_LAP_MIN = 0x9E8B00
|
|
17
17
|
_RESERVED_INQUIRY_LAP_MAX = 0x9E8B3F
|
|
18
18
|
_PROFILE_FORMAT = "swbt.profile"
|
|
19
|
-
_PROFILE_SCHEMA_VERSION =
|
|
19
|
+
_PROFILE_SCHEMA_VERSION = 2
|
|
20
20
|
_PROFILE_LOCAL_ADDRESS_IDENTITY_KIND = "exp-local-address"
|
|
21
21
|
_PROFILE_ADAPTER_DEFAULT_IDENTITY_KIND = "adapter-default"
|
|
22
22
|
_CONTROLLER_KINDS_BY_PROFILE_VALUE: dict[str, ControllerKind] = {
|
|
@@ -79,7 +79,7 @@ class LocalAddress:
|
|
|
79
79
|
|
|
80
80
|
@dataclass(frozen=True)
|
|
81
81
|
class PairingProfile:
|
|
82
|
-
"""Validated version
|
|
82
|
+
"""Validated version 2 envelope for one controller shape and its pairing data."""
|
|
83
83
|
|
|
84
84
|
local_address: LocalAddress | None
|
|
85
85
|
key_store_namespaces: KeyStoreNamespaces
|
|
@@ -97,14 +97,7 @@ class PairingProfile:
|
|
|
97
97
|
address = None if local_address is None else str(local_address)
|
|
98
98
|
profile = cls(
|
|
99
99
|
local_address=local_address,
|
|
100
|
-
key_store_namespaces=(
|
|
101
|
-
{}
|
|
102
|
-
if address is None
|
|
103
|
-
else {
|
|
104
|
-
address: {},
|
|
105
|
-
f"swbt.previous::{address}": {},
|
|
106
|
-
}
|
|
107
|
-
),
|
|
100
|
+
key_store_namespaces=({} if address is None else {address: {}}),
|
|
108
101
|
controller_kind=controller_kind,
|
|
109
102
|
)
|
|
110
103
|
target = Path(path)
|
|
@@ -200,7 +193,10 @@ class PairingProfile:
|
|
|
200
193
|
if payload.get("format") != _PROFILE_FORMAT:
|
|
201
194
|
_invalid_profile("profile format is unsupported")
|
|
202
195
|
if payload.get("schema_version") != _PROFILE_SCHEMA_VERSION:
|
|
203
|
-
_invalid_profile(
|
|
196
|
+
_invalid_profile(
|
|
197
|
+
"profile schema_version is unsupported; create a new schema v2 profile "
|
|
198
|
+
"and pair again"
|
|
199
|
+
)
|
|
204
200
|
controller_kind_value = payload.get("controller_kind")
|
|
205
201
|
if not isinstance(controller_kind_value, str):
|
|
206
202
|
_invalid_profile("profile controller_kind is unsupported")
|
swbt/transport/bumble.py
CHANGED
|
@@ -132,6 +132,12 @@ class _BumbleHidRuntime(Protocol):
|
|
|
132
132
|
def send_data(self, data: bytes) -> None:
|
|
133
133
|
"""Send an interrupt-channel HID data message."""
|
|
134
134
|
|
|
135
|
+
def register_set_report_cb(
|
|
136
|
+
self,
|
|
137
|
+
callback: Callable[[int, int, int, bytes], _BumbleGetSetStatus],
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Register a HID SET_REPORT callback."""
|
|
140
|
+
|
|
135
141
|
async def connect_control_channel(self) -> None:
|
|
136
142
|
"""Request control-channel L2CAP connection."""
|
|
137
143
|
|
|
@@ -455,10 +461,6 @@ class BumbleHidTransport:
|
|
|
455
461
|
self._register_set_report_callback(hid_device)
|
|
456
462
|
|
|
457
463
|
def _register_set_report_callback(self, hid_device: _BumbleHidRuntime) -> None:
|
|
458
|
-
register_set_report = getattr(hid_device, "register_set_report_cb", None)
|
|
459
|
-
if not callable(register_set_report):
|
|
460
|
-
return
|
|
461
|
-
|
|
462
464
|
def on_set_report(
|
|
463
465
|
report_id: int,
|
|
464
466
|
report_type: int,
|
|
@@ -475,7 +477,7 @@ class BumbleHidTransport:
|
|
|
475
477
|
return _BumbleGetSetStatus(status=HID_GET_SET_UNSUPPORTED_REQUEST)
|
|
476
478
|
return _BumbleGetSetStatus(status=HID_GET_SET_SUCCESS)
|
|
477
479
|
|
|
478
|
-
|
|
480
|
+
hid_device.register_set_report_cb(on_set_report)
|
|
479
481
|
|
|
480
482
|
def _register_device_callbacks(self, device: _BumbleDeviceRuntime) -> None:
|
|
481
483
|
device.on(device.EVENT_CONNECTION, self._handle_device_connection)
|
|
@@ -840,8 +842,7 @@ def _device_info_bluetooth_address_from_bumble_address(address: object) -> bytes
|
|
|
840
842
|
"""Return Device Info address bytes from a Bumble address object."""
|
|
841
843
|
if address is None:
|
|
842
844
|
return None
|
|
843
|
-
|
|
844
|
-
address_text = str(to_string(False)) if callable(to_string) else str(address)
|
|
845
|
+
address_text = str(cast("Any", address).to_string(False))
|
|
845
846
|
address_text = address_text.split("/", 1)[0]
|
|
846
847
|
parts = address_text.split(":")
|
|
847
848
|
if len(parts) != 6 or any(len(part) != 2 for part in parts):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swbt-python
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Python library for presenting an NX-compatible virtual Bluetooth HID input device.
|
|
5
5
|
Keywords: bluetooth,bluetooth-hid,controller,gamepad,hid,nx
|
|
6
6
|
Author: niart120
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Dist: bumble
|
|
21
|
+
Requires-Dist: bumble==0.0.233
|
|
22
22
|
Maintainer: niart120
|
|
23
23
|
Maintainer-email: niart120 <38847256+niart120@users.noreply.github.com>
|
|
24
24
|
Requires-Python: >=3.12
|
|
@@ -7,10 +7,10 @@ swbt/gamepad/_config.py,sha256=VjYrR4erPhEohjZ1Pw5ElxryNEa8dRfsKhz2EsjpEUs,2050
|
|
|
7
7
|
swbt/gamepad/connection.py,sha256=8EUeKYvdV_n1OIq11MFAVNhKmIbJHUoijaK6xUx6Y4g,753
|
|
8
8
|
swbt/gamepad/constants.py,sha256=gDesRFvVxZ3SMSiP4qzd42em_Dt9M5hIu3-2exgKEus,84
|
|
9
9
|
swbt/gamepad/controllers.py,sha256=G6EMi82F_36wSi2pm5QMFLJSZSakRogVcDeygqJJ8gI,7487
|
|
10
|
-
swbt/gamepad/interface.py,sha256=
|
|
10
|
+
swbt/gamepad/interface.py,sha256=HWABzdzrVPAumTZPOrUjiku-mKvAPIhqTE-pvqAIgSU,17793
|
|
11
11
|
swbt/gamepad/output.py,sha256=NeOp-NwYshEkuelge_POwk_i7orqOQc4SXm9t9Njs1s,5361
|
|
12
12
|
swbt/gamepad/protocol_handshake.py,sha256=9vAdE0PIpSpWk9oXhStOTq0XxDlE8DejE1SLBh21rx4,6217
|
|
13
|
-
swbt/gamepad/runtime.py,sha256=
|
|
13
|
+
swbt/gamepad/runtime.py,sha256=HVuhQxPtpVpcyxVo2RnWOM5Vkmn2kc9NjhJ9PPWwG4o,47170
|
|
14
14
|
swbt/gamepad/transport_factory.py,sha256=0eiC-HqtNG0CJuHR8TlSPWtpInW03A4ojnBccqpryN0,921
|
|
15
15
|
swbt/imu.py,sha256=sT1_MsYECQlQ-pSfayD8gojD9l3w9zEtOI3xP1IovQM,4237
|
|
16
16
|
swbt/input.py,sha256=7gv75lzrcmoV8Sijz7f1pvsheRrmUi_TCIMW3DjUVjw,22275
|
|
@@ -30,24 +30,24 @@ swbt/protocol/session.py,sha256=SVkF0jaJPbnKCAWswRQsrdoCxZwjHPEMz4lktVBcXc4,4811
|
|
|
30
30
|
swbt/protocol/spi.py,sha256=buKyNYYOHOSYbpVh3Wa2968FL4VIO_43QcSXNowNfJs,2594
|
|
31
31
|
swbt/protocol/subcommand.py,sha256=NRtp9-lB8IBimC0kO8oyJnel_qSSPx65XlmGWTARB0E,8662
|
|
32
32
|
swbt/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
33
|
-
swbt/report_loop.py,sha256=
|
|
33
|
+
swbt/report_loop.py,sha256=zGaKStCsupzkmSTF8YZOL9H2DbH1Nf15yHtI3POJiTY,8350
|
|
34
34
|
swbt/state_store.py,sha256=jE1TeNpqynbVpxFp0Xt3m1Dfl1LUKR-uqV4f14qntuM,1402
|
|
35
35
|
swbt/transport/__init__.py,sha256=b_Vqsg0k_g7XclN9XHQrcJxqCwGIklxG5NqzISal4_I,68
|
|
36
36
|
swbt/transport/_adapter_identity.py,sha256=xOECQBlGAZJoyKrfxrIPt88kXoksHWmWzzVo1-x5XmA,6621
|
|
37
|
-
swbt/transport/_bumble_acl.py,sha256=
|
|
37
|
+
swbt/transport/_bumble_acl.py,sha256=tZalNxNSKEsTB6WD4wEOyqEY-Td6s5wg2s_3Ve5UOqI,653
|
|
38
38
|
swbt/transport/_bumble_hidp.py,sha256=mL_eT-otopHzg5G0p7uhqTYsqSuDwdXAiAwakQiHbCY,835
|
|
39
|
-
swbt/transport/_bumble_key_store.py,sha256=
|
|
40
|
-
swbt/transport/_bumble_lifecycle.py,sha256=
|
|
39
|
+
swbt/transport/_bumble_key_store.py,sha256=d7IJ9537QRruvy4aOyVtK7RsOOnjuJ7hKmiYTt7gC_I,6938
|
|
40
|
+
swbt/transport/_bumble_lifecycle.py,sha256=9NgXzyW3g2s91gPcsDuyzS9-rAp64ahDKk9fb3VJLDg,10174
|
|
41
41
|
swbt/transport/_bumble_sdp.py,sha256=RtVgNI3EFi0-cIbiNqrLUi0A2Ju513kF-JWmFlkuvi4,9254
|
|
42
42
|
swbt/transport/_bumble_usb_devices.py,sha256=soAKiHTzeeTYdb7M357yaac-RxG2c_BiKWH9NK9zJFY,894
|
|
43
43
|
swbt/transport/_csr_bd_addr.py,sha256=FNc7y9SvEXhHyChZ8I4zgC-LyduiddLqt-HM_Ux2QXA,7949
|
|
44
44
|
swbt/transport/_csr_bd_addr_harness.py,sha256=aSuFCVbneRENveN_b3CuamD7R9Wqf3CKRNU6Zcdxvpc,10297
|
|
45
|
-
swbt/transport/_pairing_profile.py,sha256=
|
|
45
|
+
swbt/transport/_pairing_profile.py,sha256=95tISeYHnllHfFpNs0oBsaYb5uAffXfi9eFkjb-EeFQ,9342
|
|
46
46
|
swbt/transport/base.py,sha256=eI0hs1wTwJf2ppzqda4ok-tUXF1hE9dwpb0p-lpvwmo,5163
|
|
47
|
-
swbt/transport/bumble.py,sha256=
|
|
47
|
+
swbt/transport/bumble.py,sha256=VufUEo4aUeVebSFvZPJaVe_G7nNz5fKQMCbjgqmBbRs,33414
|
|
48
48
|
swbt/transport/fake.py,sha256=4O5zlsg1TO61Ai54FGCn_xM8TEA9yi938Fso-XHdh1w,12891
|
|
49
|
-
swbt_python-0.5.
|
|
50
|
-
swbt_python-0.5.
|
|
51
|
-
swbt_python-0.5.
|
|
52
|
-
swbt_python-0.5.
|
|
53
|
-
swbt_python-0.5.
|
|
49
|
+
swbt_python-0.5.4.dist-info/licenses/LICENSE,sha256=FzzUoYjuIh5DzpeaujIbDUTylYnyzcHRVU-_ZMyowyk,1065
|
|
50
|
+
swbt_python-0.5.4.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
|
|
51
|
+
swbt_python-0.5.4.dist-info/entry_points.txt,sha256=YYQc7VgoSy74iZ6s8Qr6iJpnvFoDpp8G4-v0KJH5TGo,48
|
|
52
|
+
swbt_python-0.5.4.dist-info/METADATA,sha256=Av__75M0-iYeeo-cBzL2FB2zO1ktEN4CCw6YZHA_ufA,5902
|
|
53
|
+
swbt_python-0.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|