swbt-python 0.5.3__py3-none-any.whl → 0.6.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/gamepad/interface.py +18 -14
- swbt/gamepad/runtime.py +93 -23
- swbt/report_loop.py +11 -0
- {swbt_python-0.5.3.dist-info → swbt_python-0.6.0.dist-info}/METADATA +4 -4
- {swbt_python-0.5.3.dist-info → swbt_python-0.6.0.dist-info}/RECORD +8 -8
- {swbt_python-0.5.3.dist-info → swbt_python-0.6.0.dist-info}/WHEEL +0 -0
- {swbt_python-0.5.3.dist-info → swbt_python-0.6.0.dist-info}/entry_points.txt +0 -0
- {swbt_python-0.5.3.dist-info → swbt_python-0.6.0.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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swbt-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -14,14 +14,14 @@ Classifier: Operating System :: MacOS
|
|
|
14
14
|
Classifier: Operating System :: Microsoft :: Windows
|
|
15
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Dist: bumble==0.0.233
|
|
22
22
|
Maintainer: niart120
|
|
23
23
|
Maintainer-email: niart120 <38847256+niart120@users.noreply.github.com>
|
|
24
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.13
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
27
|
# swbt-python
|
|
@@ -32,7 +32,7 @@ NX 向けの仮想 Bluetooth HID 入力デバイスを Python から扱うため
|
|
|
32
32
|
|
|
33
33
|
## 必要なもの
|
|
34
34
|
|
|
35
|
-
- Python 3.
|
|
35
|
+
- Python 3.13 以降
|
|
36
36
|
- uv
|
|
37
37
|
- Bumble が利用可能な専用 USB Bluetooth ドングル
|
|
38
38
|
|
|
@@ -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,7 +30,7 @@ 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
|
|
@@ -46,8 +46,8 @@ swbt/transport/_pairing_profile.py,sha256=95tISeYHnllHfFpNs0oBsaYb5uAffXfi9eFkjb
|
|
|
46
46
|
swbt/transport/base.py,sha256=eI0hs1wTwJf2ppzqda4ok-tUXF1hE9dwpb0p-lpvwmo,5163
|
|
47
47
|
swbt/transport/bumble.py,sha256=VufUEo4aUeVebSFvZPJaVe_G7nNz5fKQMCbjgqmBbRs,33414
|
|
48
48
|
swbt/transport/fake.py,sha256=4O5zlsg1TO61Ai54FGCn_xM8TEA9yi938Fso-XHdh1w,12891
|
|
49
|
-
swbt_python-0.
|
|
50
|
-
swbt_python-0.
|
|
51
|
-
swbt_python-0.
|
|
52
|
-
swbt_python-0.
|
|
53
|
-
swbt_python-0.
|
|
49
|
+
swbt_python-0.6.0.dist-info/licenses/LICENSE,sha256=FzzUoYjuIh5DzpeaujIbDUTylYnyzcHRVU-_ZMyowyk,1065
|
|
50
|
+
swbt_python-0.6.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
|
|
51
|
+
swbt_python-0.6.0.dist-info/entry_points.txt,sha256=YYQc7VgoSy74iZ6s8Qr6iJpnvFoDpp8G4-v0KJH5TGo,48
|
|
52
|
+
swbt_python-0.6.0.dist-info/METADATA,sha256=uYO6HjCv9WQ4YTzUKV4gIn-L4LvMOeVWAzEh37GL6RA,5902
|
|
53
|
+
swbt_python-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|