swbt-python 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
swbt/__init__.py CHANGED
@@ -15,8 +15,13 @@ from swbt.errors import (
15
15
  )
16
16
  from swbt.gamepad import (
17
17
  ConnectionResult,
18
+ DirectJoyConL,
19
+ DirectJoyConR,
20
+ DirectProController,
21
+ DirectSwitchGamepad,
18
22
  JoyConL,
19
23
  JoyConR,
24
+ PeriodicSwitchGamepad,
20
25
  ProController,
21
26
  SwitchGamepad,
22
27
  )
@@ -33,6 +38,10 @@ __all__ = (
33
38
  "ConnectionTimeoutError",
34
39
  "ControllerColors",
35
40
  "DiagnosticsConfig",
41
+ "DirectJoyConL",
42
+ "DirectJoyConR",
43
+ "DirectProController",
44
+ "DirectSwitchGamepad",
36
45
  "GamepadStatus",
37
46
  "IMUFrame",
38
47
  "InputState",
@@ -40,6 +49,7 @@ __all__ = (
40
49
  "InvalidKeyStoreError",
41
50
  "JoyConL",
42
51
  "JoyConR",
52
+ "PeriodicSwitchGamepad",
43
53
  "ProController",
44
54
  "Stick",
45
55
  "SwbtError",
swbt/_testing/gamepad.py CHANGED
@@ -1,6 +1,15 @@
1
1
  """Internal gamepad constructors for tests that inject fake transports."""
2
2
 
3
- from swbt import ControllerColors, DiagnosticsConfig, JoyConL, JoyConR, ProController
3
+ from swbt import (
4
+ ControllerColors,
5
+ DiagnosticsConfig,
6
+ DirectJoyConL,
7
+ DirectJoyConR,
8
+ DirectProController,
9
+ JoyConL,
10
+ JoyConR,
11
+ ProController,
12
+ )
4
13
  from swbt.gamepad._config import _SwitchGamepadConfig
5
14
  from swbt.protocol.profiles.joycon import JoyConLeftProfile, JoyConRightProfile
6
15
  from swbt.transport.base import HidDeviceTransport
@@ -72,3 +81,65 @@ def make_joycon_r(
72
81
  diagnostics=diagnostics,
73
82
  transport=transport,
74
83
  )
84
+
85
+
86
+ def make_direct_pro_controller(
87
+ *,
88
+ transport: HidDeviceTransport,
89
+ adapter: str | None = None,
90
+ key_store_path: str | None = None,
91
+ controller_colors: ControllerColors | None = None,
92
+ diagnostics: DiagnosticsConfig | None = None,
93
+ ) -> DirectProController:
94
+ """Create a Direct Pro Controller with an injected transport for tests."""
95
+ return DirectProController._from_config(
96
+ _SwitchGamepadConfig(
97
+ adapter=adapter,
98
+ key_store_path=key_store_path,
99
+ controller_colors=controller_colors,
100
+ ),
101
+ diagnostics=diagnostics,
102
+ transport=transport,
103
+ )
104
+
105
+
106
+ def make_direct_joycon_l(
107
+ *,
108
+ transport: HidDeviceTransport,
109
+ adapter: str | None = None,
110
+ key_store_path: str | None = None,
111
+ controller_colors: ControllerColors | None = None,
112
+ diagnostics: DiagnosticsConfig | None = None,
113
+ ) -> DirectJoyConL:
114
+ """Create a Direct Joy-Con L with an injected transport for tests."""
115
+ return DirectJoyConL._from_config(
116
+ _SwitchGamepadConfig(
117
+ adapter=adapter,
118
+ key_store_path=key_store_path,
119
+ controller_colors=controller_colors,
120
+ profile=JoyConLeftProfile(),
121
+ ),
122
+ diagnostics=diagnostics,
123
+ transport=transport,
124
+ )
125
+
126
+
127
+ def make_direct_joycon_r(
128
+ *,
129
+ transport: HidDeviceTransport,
130
+ adapter: str | None = None,
131
+ key_store_path: str | None = None,
132
+ controller_colors: ControllerColors | None = None,
133
+ diagnostics: DiagnosticsConfig | None = None,
134
+ ) -> DirectJoyConR:
135
+ """Create a Direct Joy-Con R with an injected transport for tests."""
136
+ return DirectJoyConR._from_config(
137
+ _SwitchGamepadConfig(
138
+ adapter=adapter,
139
+ key_store_path=key_store_path,
140
+ controller_colors=controller_colors,
141
+ profile=JoyConRightProfile(),
142
+ ),
143
+ diagnostics=diagnostics,
144
+ transport=transport,
145
+ )
swbt/gamepad/__init__.py CHANGED
@@ -3,9 +3,16 @@
3
3
  from swbt.gamepad.connection import ConnectionResult, ConnectionStatus
4
4
  from swbt.gamepad.constants import DISCONNECT_REQUEST_TIMEOUT_SECONDS
5
5
  from swbt.gamepad.core import (
6
+ DirectJoyConL,
7
+ DirectJoyConR,
8
+ DirectProController,
6
9
  JoyConL,
7
10
  JoyConR,
8
11
  ProController,
12
+ )
13
+ from swbt.gamepad.interface import (
14
+ DirectSwitchGamepad,
15
+ PeriodicSwitchGamepad,
9
16
  SwitchGamepad,
10
17
  )
11
18
 
@@ -13,8 +20,13 @@ __all__ = (
13
20
  "DISCONNECT_REQUEST_TIMEOUT_SECONDS",
14
21
  "ConnectionResult",
15
22
  "ConnectionStatus",
23
+ "DirectJoyConL",
24
+ "DirectJoyConR",
25
+ "DirectProController",
26
+ "DirectSwitchGamepad",
16
27
  "JoyConL",
17
28
  "JoyConR",
29
+ "PeriodicSwitchGamepad",
18
30
  "ProController",
19
31
  "SwitchGamepad",
20
32
  )
swbt/gamepad/core.py CHANGED
@@ -1,12 +1,15 @@
1
1
  """Public gamepad API."""
2
2
 
3
- from typing import Self
3
+ from typing import ClassVar, Literal, Self
4
4
 
5
5
  from swbt.diagnostics import DiagnosticsConfig, GamepadStatus
6
6
  from swbt.errors import InvalidInputError
7
7
  from swbt.gamepad._config import _ControllerSpec, _SwitchGamepadConfig
8
8
  from swbt.gamepad.connection import ConnectionResult
9
- from swbt.gamepad.interface import SwitchGamepad
9
+ from swbt.gamepad.interface import (
10
+ DirectSwitchGamepad,
11
+ PeriodicSwitchGamepad,
12
+ )
10
13
  from swbt.gamepad.output import OutputReportDispatcher
11
14
  from swbt.gamepad.runtime import ControllerRuntime
12
15
  from swbt.input import Button, IMUFrame, InputState, Stick
@@ -17,7 +20,7 @@ from swbt.state_store import InputStateStore
17
20
  from swbt.transport.base import HidDeviceTransport
18
21
 
19
22
 
20
- class _RuntimeBackedGamepad(SwitchGamepad):
23
+ class _RuntimeBackedGamepad:
21
24
  """Runtime-backed concrete gamepad base.
22
25
 
23
26
  The object owns the public API surface and delegates stateful
@@ -25,6 +28,7 @@ class _RuntimeBackedGamepad(SwitchGamepad):
25
28
  """
26
29
 
27
30
  _controller_spec = _ControllerSpec(profile=default_controller_profile())
31
+ _reporting_mode: ClassVar[Literal["periodic", "direct"]] = "periodic"
28
32
 
29
33
  def __init__(
30
34
  self,
@@ -65,6 +69,7 @@ class _RuntimeBackedGamepad(SwitchGamepad):
65
69
  self._runtime = ControllerRuntime.from_config(
66
70
  config,
67
71
  diagnostics=diagnostics,
72
+ reporting_mode=self._reporting_mode,
68
73
  transport=transport,
69
74
  )
70
75
 
@@ -113,8 +118,9 @@ class _RuntimeBackedGamepad(SwitchGamepad):
113
118
  async def open(self) -> None:
114
119
  """Open the configured transport.
115
120
 
116
- Opening prepares transport callbacks, diagnostics metadata, and the report
117
- loop. It does not start HID advertising, pairing, or active reconnect.
121
+ Opening prepares transport callbacks, diagnostics metadata, and the
122
+ reporting-type resources. It does not start HID advertising, pairing,
123
+ or active reconnect.
118
124
 
119
125
  Raises:
120
126
  TransportOpenError: Raised by the transport when the adapter cannot be opened.
@@ -218,22 +224,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
218
224
  Args:
219
225
  buttons: Buttons to add to the current button set.
220
226
 
221
- This updates local state only and does not send an immediate input report.
227
+ A periodic controller commits local state. A direct controller sends one
228
+ input report and commits only after transmission succeeds.
222
229
  """
223
230
  await self._runtime.press(*buttons)
224
231
 
225
- async def apply(self, state: InputState) -> None:
226
- """Replace the current input state without immediate transmission.
227
-
228
- Args:
229
- state: Complete input state to commit.
230
-
231
- This updates local state only and does not send an immediate input report.
232
- """
233
- await self._runtime.apply(state)
234
-
235
232
  async def sticks(self, *, left: Stick | None = None, right: Stick | None = None) -> None:
236
- """Replace one or both stick positions without immediate transmission.
233
+ """Replace one or both stick positions according to the reporting type.
237
234
 
238
235
  Args:
239
236
  left: Optional replacement for the left stick.
@@ -242,12 +239,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
242
239
  Raises:
243
240
  InvalidInputError: ``left`` or ``right`` is not a ``Stick``.
244
241
 
245
- This updates local state only and does not send an immediate input report.
242
+ A periodic controller commits local state. A direct controller sends one
243
+ input report and commits only after transmission succeeds.
246
244
  """
247
245
  await self._runtime.sticks(left=left, right=right)
248
246
 
249
247
  async def lstick(self, stick: Stick) -> None:
250
- """Replace the left stick position without immediate transmission.
248
+ """Replace the left stick position according to the reporting type.
251
249
 
252
250
  Args:
253
251
  stick: Replacement for the left stick.
@@ -255,12 +253,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
255
253
  Raises:
256
254
  InvalidInputError: ``stick`` is not a ``Stick``.
257
255
 
258
- This updates local state only and does not send an immediate input report.
256
+ A periodic controller commits local state. A direct controller sends one
257
+ input report and commits only after transmission succeeds.
259
258
  """
260
259
  await self._runtime.lstick(stick)
261
260
 
262
261
  async def rstick(self, stick: Stick) -> None:
263
- """Replace the right stick position without immediate transmission.
262
+ """Replace the right stick position according to the reporting type.
264
263
 
265
264
  Args:
266
265
  stick: Replacement for the right stick.
@@ -268,12 +267,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
268
267
  Raises:
269
268
  InvalidInputError: ``stick`` is not a ``Stick``.
270
269
 
271
- This updates local state only and does not send an immediate input report.
270
+ A periodic controller commits local state. A direct controller sends one
271
+ input report and commits only after transmission succeeds.
272
272
  """
273
273
  await self._runtime.rstick(stick)
274
274
 
275
275
  async def imu(self, *frames: IMUFrame) -> None:
276
- """Replace IMU frames without immediate transmission.
276
+ """Replace IMU frames according to the reporting type.
277
277
 
278
278
  Args:
279
279
  frames: One ``IMUFrame`` to repeat across all three IMU slots, or exactly
@@ -293,12 +293,13 @@ class _RuntimeBackedGamepad(SwitchGamepad):
293
293
  Args:
294
294
  buttons: Buttons to remove from the current button set.
295
295
 
296
- This updates local state only and does not send an immediate input report.
296
+ A periodic controller commits local state. A direct controller sends one
297
+ input report and commits only after transmission succeeds.
297
298
  """
298
299
  await self._runtime.release(*buttons)
299
300
 
300
301
  async def neutral(self) -> None:
301
- """Return local input state to ``InputState.neutral()`` without immediate transmission."""
302
+ """Apply ``InputState.neutral()`` according to the reporting type."""
302
303
  await self._runtime.neutral()
303
304
 
304
305
  async def tap(self, *buttons: Button, duration: float = 0.08) -> None:
@@ -327,19 +328,77 @@ class _RuntimeBackedGamepad(SwitchGamepad):
327
328
  def snapshot(self) -> InputState:
328
329
  """Return the latest committed input state.
329
330
 
331
+ A periodic controller returns its latest local state. A direct
332
+ controller returns the last state sent successfully.
333
+
330
334
  Returns:
331
335
  InputState: Immutable snapshot of the current input state.
332
336
  """
333
337
  return self._runtime.snapshot()
334
338
 
335
339
 
336
- class ProController(_RuntimeBackedGamepad):
340
+ class _PeriodicRuntimeBackedGamepad(_RuntimeBackedGamepad, PeriodicSwitchGamepad):
341
+ """Runtime-backed gamepad with library-owned periodic input transmission."""
342
+
343
+ async def apply(self, state: InputState) -> None:
344
+ """Replace the current input state without immediate transmission.
345
+
346
+ Args:
347
+ state: Complete input state to commit.
348
+
349
+ This updates local state only and does not send an immediate input report.
350
+ """
351
+ await self._runtime.apply(state)
352
+
353
+
354
+ class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
355
+ """Runtime-backed gamepad with caller-owned input transmission."""
356
+
357
+ _reporting_mode = "direct"
358
+
359
+ def __init__(
360
+ self,
361
+ *,
362
+ adapter: str | None = None,
363
+ key_store_path: str | None = None,
364
+ controller_colors: ControllerColors | None = None,
365
+ diagnostics: DiagnosticsConfig | None = None,
366
+ ) -> None:
367
+ """Create a direct-reporting gamepad object.
368
+
369
+ Args:
370
+ adapter: Bumble adapter moniker used for the Bluetooth backend.
371
+ key_store_path: Optional path used by the Bluetooth backend to persist keys.
372
+ controller_colors: Optional fixed controller body, button, and grip colors.
373
+ diagnostics: Optional diagnostics configuration for trace output.
374
+
375
+ Raises:
376
+ InvalidInputError: ``adapter`` is omitted.
377
+ """
378
+ config = self._controller_spec.build_config(
379
+ adapter=adapter,
380
+ key_store_path=key_store_path,
381
+ report_period_us=None,
382
+ controller_colors=controller_colors,
383
+ )
384
+ self._init_from_config(config, diagnostics=diagnostics, transport=None)
385
+
386
+ async def send(self, state: InputState) -> None:
387
+ """Send one complete input state and commit it after transmission.
388
+
389
+ Args:
390
+ state: Complete input state to send.
391
+ """
392
+ await self._runtime.send(state)
393
+
394
+
395
+ class ProController(_PeriodicRuntimeBackedGamepad):
337
396
  """Runtime-backed Pro Controller-compatible gamepad."""
338
397
 
339
398
  _controller_spec = _ControllerSpec(profile=default_controller_profile())
340
399
 
341
400
 
342
- class JoyConL(_RuntimeBackedGamepad):
401
+ class JoyConL(_PeriodicRuntimeBackedGamepad):
343
402
  """Runtime-backed Joy-Con L-compatible gamepad."""
344
403
 
345
404
  _controller_spec = _ControllerSpec(profile=JoyConLeftProfile())
@@ -374,7 +433,7 @@ class JoyConL(_RuntimeBackedGamepad):
374
433
  self._init_from_config(config, diagnostics=diagnostics, transport=None)
375
434
 
376
435
 
377
- class JoyConR(_RuntimeBackedGamepad):
436
+ class JoyConR(_PeriodicRuntimeBackedGamepad):
378
437
  """Runtime-backed Joy-Con R-compatible gamepad."""
379
438
 
380
439
  _controller_spec = _ControllerSpec(profile=JoyConRightProfile())
@@ -407,3 +466,21 @@ class JoyConR(_RuntimeBackedGamepad):
407
466
  controller_colors=controller_colors,
408
467
  )
409
468
  self._init_from_config(config, diagnostics=diagnostics, transport=None)
469
+
470
+
471
+ class DirectProController(_DirectRuntimeBackedGamepad):
472
+ """Direct-reporting Pro Controller-compatible gamepad."""
473
+
474
+ _controller_spec = _ControllerSpec(profile=default_controller_profile())
475
+
476
+
477
+ class DirectJoyConL(_DirectRuntimeBackedGamepad):
478
+ """Direct-reporting Joy-Con L-compatible gamepad."""
479
+
480
+ _controller_spec = _ControllerSpec(profile=JoyConLeftProfile())
481
+
482
+
483
+ class DirectJoyConR(_DirectRuntimeBackedGamepad):
484
+ """Direct-reporting Joy-Con R-compatible gamepad."""
485
+
486
+ _controller_spec = _ControllerSpec(profile=JoyConRightProfile())
swbt/gamepad/interface.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from types import TracebackType
5
+ from typing import Self
5
6
 
6
7
  from swbt.diagnostics import GamepadStatus
7
8
  from swbt.gamepad.connection import ConnectionResult
@@ -9,14 +10,15 @@ from swbt.input import Button, IMUFrame, InputState, Stick
9
10
 
10
11
 
11
12
  class SwitchGamepad(ABC):
12
- """Shared public interface for NX-compatible virtual gamepads.
13
+ """Shared abstract public interface for NX-compatible virtual gamepads.
13
14
 
14
- Use this abstract base class for type annotations. Construct
15
- ``ProController``, ``JoyConL``, or ``JoyConR`` for a concrete virtual
16
- controller.
15
+ Use ``PeriodicSwitchGamepad`` or ``DirectSwitchGamepad`` when a type
16
+ annotation must express who owns the input-report schedule. Common input
17
+ operations commit local state on periodic gamepads; on direct gamepads they
18
+ send one input report and commit only after transmission succeeds.
17
19
  """
18
20
 
19
- async def __aenter__(self) -> "SwitchGamepad":
21
+ async def __aenter__(self) -> Self:
20
22
  """Open the gamepad for an async context manager.
21
23
 
22
24
  Returns:
@@ -45,8 +47,9 @@ class SwitchGamepad(ABC):
45
47
  async def open(self) -> None:
46
48
  """Open the configured transport.
47
49
 
48
- Opening prepares transport callbacks, diagnostics metadata, and the report
49
- loop. It does not start HID advertising, pairing, or active reconnect.
50
+ Opening prepares transport callbacks, diagnostics metadata, and the
51
+ reporting-type resources. It does not start HID advertising, pairing,
52
+ or active reconnect.
50
53
 
51
54
  Raises:
52
55
  TransportOpenError: The configured transport cannot be opened.
@@ -160,25 +163,13 @@ class SwitchGamepad(ABC):
160
163
  InvalidInputError: Any value is not a ``Button``.
161
164
  UnsupportedInputError: The controller profile does not support a button.
162
165
 
163
- This updates local state only and does not send an immediate input report.
164
- """
165
-
166
- @abstractmethod
167
- async def apply(self, state: InputState) -> None:
168
- """Replace the current input state without immediate transmission.
169
-
170
- Args:
171
- state: Complete input state to commit.
172
-
173
- Raises:
174
- InvalidInputError: ``state`` is not an ``InputState``.
175
- UnsupportedInputError: The controller profile does not support part of
176
- the supplied state.
166
+ Completion follows the reporting type: periodic gamepads commit local
167
+ state, while direct gamepads send one input report and then commit.
177
168
  """
178
169
 
179
170
  @abstractmethod
180
171
  async def sticks(self, *, left: Stick | None = None, right: Stick | None = None) -> None:
181
- """Replace one or both stick positions without immediate transmission.
172
+ """Replace one or both stick positions according to the reporting type.
182
173
 
183
174
  Args:
184
175
  left: Optional replacement for the left stick.
@@ -191,7 +182,7 @@ class SwitchGamepad(ABC):
191
182
 
192
183
  @abstractmethod
193
184
  async def lstick(self, stick: Stick) -> None:
194
- """Replace the left stick position without immediate transmission.
185
+ """Replace the left stick position according to the reporting type.
195
186
 
196
187
  Args:
197
188
  stick: Replacement for the left stick.
@@ -203,7 +194,7 @@ class SwitchGamepad(ABC):
203
194
 
204
195
  @abstractmethod
205
196
  async def rstick(self, stick: Stick) -> None:
206
- """Replace the right stick position without immediate transmission.
197
+ """Replace the right stick position according to the reporting type.
207
198
 
208
199
  Args:
209
200
  stick: Replacement for the right stick.
@@ -215,7 +206,7 @@ class SwitchGamepad(ABC):
215
206
 
216
207
  @abstractmethod
217
208
  async def imu(self, *frames: IMUFrame) -> None:
218
- """Replace IMU frames without immediate transmission.
209
+ """Replace IMU frames according to the reporting type.
219
210
 
220
211
  Args:
221
212
  frames: One ``IMUFrame`` to repeat across all three IMU slots, or exactly
@@ -240,7 +231,7 @@ class SwitchGamepad(ABC):
240
231
 
241
232
  @abstractmethod
242
233
  async def neutral(self) -> None:
243
- """Return local input state to ``InputState.neutral()`` without immediate transmission."""
234
+ """Apply ``InputState.neutral()`` according to the reporting type."""
244
235
 
245
236
  @abstractmethod
246
237
  async def tap(self, *buttons: Button, duration: float = 0.08) -> None:
@@ -268,6 +259,44 @@ class SwitchGamepad(ABC):
268
259
  def snapshot(self) -> InputState:
269
260
  """Return the latest committed input state.
270
261
 
262
+ A periodic gamepad returns its latest local state. A direct gamepad
263
+ returns the last state whose input report was sent successfully.
264
+
271
265
  Returns:
272
266
  InputState: Immutable snapshot of the current input state.
273
267
  """
268
+
269
+
270
+ class PeriodicSwitchGamepad(SwitchGamepad):
271
+ """Abstract gamepad whose input report schedule is owned by the library."""
272
+
273
+ @abstractmethod
274
+ async def apply(self, state: InputState) -> None:
275
+ """Replace the current local input state without immediate transmission.
276
+
277
+ Args:
278
+ state: Complete input state to commit.
279
+
280
+ Raises:
281
+ InvalidInputError: ``state`` is not an ``InputState``.
282
+ UnsupportedInputError: The controller profile does not support part of
283
+ the supplied state.
284
+ """
285
+
286
+
287
+ class DirectSwitchGamepad(SwitchGamepad):
288
+ """Abstract gamepad whose input report schedule is owned by the caller."""
289
+
290
+ @abstractmethod
291
+ async def send(self, state: InputState) -> None:
292
+ """Send one complete input state and commit it after transmission.
293
+
294
+ Args:
295
+ state: Complete input state to send.
296
+
297
+ Raises:
298
+ ClosedError: The gamepad is not connected.
299
+ InvalidInputError: ``state`` is not an ``InputState``.
300
+ UnsupportedInputError: The controller profile does not support part of
301
+ the supplied state.
302
+ """
swbt/gamepad/output.py CHANGED
@@ -13,7 +13,7 @@ from swbt.protocol.subcommand import (
13
13
  )
14
14
  from swbt.state_store import InputStateStore
15
15
 
16
- ReplyBuilder = Callable[[], bytes]
16
+ ReplyBuilder = Callable[[], bytes | Awaitable[bytes]]
17
17
  ReplySender = Callable[[ReplyBuilder], Awaitable[bytes]]
18
18
  ReplySenderRequirement = Callable[[], None]
19
19
 
@@ -50,15 +50,17 @@ class OutputReportDispatcher:
50
50
  subcommand_id=output_report.subcommand_id,
51
51
  )
52
52
  self.require_reply_sender()
53
- state = await self.state_store.snapshot()
54
53
  try:
55
- reply = await self.send_subcommand_reply(
56
- lambda: self.subcommand_responder.respond(
54
+
55
+ async def build_reply() -> bytes:
56
+ state = await self.state_store.snapshot()
57
+ return self.subcommand_responder.respond(
57
58
  output_report,
58
59
  state=state,
59
60
  session=self.session,
60
61
  )
61
- )
62
+
63
+ reply = await self.send_subcommand_reply(build_reply)
62
64
  except UnsupportedSubcommandError:
63
65
  self.diagnostics.record_event(
64
66
  "unsupported_subcommand",
swbt/gamepad/runtime.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """Stateful runtime for gamepad lifecycle and input behavior."""
2
2
 
3
3
  import asyncio
4
- from collections.abc import Callable
4
+ from collections.abc import Awaitable, Callable
5
5
  from dataclasses import replace
6
6
  from types import TracebackType
7
+ from typing import Literal
7
8
 
8
9
  import swbt.gamepad as gamepad_module
9
10
  from swbt.diagnostics import DiagnosticsConfig, DiagnosticsRecorder, GamepadStatus
@@ -29,7 +30,7 @@ from swbt.protocol.input_report import InputReportBuilder
29
30
  from swbt.protocol.profiles.base import ControllerColors
30
31
  from swbt.protocol.session import SwitchHidSession
31
32
  from swbt.protocol.subcommand import SubcommandResponder
32
- from swbt.report_loop import ReportLoop
33
+ from swbt.report_loop import ReportLoop, ReportSender
33
34
  from swbt.state_store import InputStateStore
34
35
  from swbt.transport.base import DisconnectRequestResult, HidDeviceTransport
35
36
 
@@ -83,6 +84,7 @@ class ControllerRuntime:
83
84
  config: _SwitchGamepadConfig,
84
85
  *,
85
86
  diagnostics: DiagnosticsConfig | None,
87
+ reporting_mode: Literal["periodic", "direct"] = "periodic",
86
88
  transport: HidDeviceTransport | None,
87
89
  ) -> None:
88
90
  runtime_config = _RuntimeConfig.from_public_config(config)
@@ -90,6 +92,7 @@ class ControllerRuntime:
90
92
  msg = "adapter is required when no custom transport is supplied"
91
93
  raise InvalidInputError(msg)
92
94
  self._config = runtime_config
95
+ self._reporting_mode = reporting_mode
93
96
  self._transport = transport
94
97
  self._transport_was_injected = transport is not None
95
98
  if transport is None:
@@ -117,6 +120,8 @@ class ControllerRuntime:
117
120
  ),
118
121
  )
119
122
  self._report_loop: ReportLoop | None = None
123
+ self._report_sender: ReportSender | None = None
124
+ self._input_operation_lock = asyncio.Lock()
120
125
  self._lifecycle_lock = asyncio.Lock()
121
126
  self._connected_event = asyncio.Event()
122
127
  self._disconnect_event = asyncio.Event()
@@ -143,6 +148,7 @@ class ControllerRuntime:
143
148
  config: _SwitchGamepadConfig,
144
149
  *,
145
150
  diagnostics: DiagnosticsConfig | None = None,
151
+ reporting_mode: Literal["periodic", "direct"] = "periodic",
146
152
  transport: HidDeviceTransport | None = None,
147
153
  ) -> "ControllerRuntime":
148
154
  """Create a runtime from an explicit resource configuration.
@@ -150,6 +156,7 @@ class ControllerRuntime:
150
156
  Args:
151
157
  config: Resource configuration for the runtime.
152
158
  diagnostics: Optional diagnostics configuration for trace output.
159
+ reporting_mode: Internal owner of normal input report scheduling.
153
160
  transport: Optional HID transport instance.
154
161
 
155
162
  Returns:
@@ -159,6 +166,7 @@ class ControllerRuntime:
159
166
  runtime._init_from_config(
160
167
  config,
161
168
  diagnostics=diagnostics,
169
+ reporting_mode=reporting_mode,
162
170
  transport=transport,
163
171
  )
164
172
  return runtime
@@ -210,22 +218,36 @@ class ControllerRuntime:
210
218
  try:
211
219
  await transport.open()
212
220
  self._configure_device_info_bluetooth_address(transport)
213
- self._report_loop = ReportLoop(
221
+ report_sender = ReportSender(
214
222
  transport=transport,
215
- state_store=self._state_store,
216
- report_period_us=self._config.report_period_us,
217
223
  input_report_builder=InputReportBuilder(
218
224
  self._controller_profile,
219
225
  ),
220
226
  session=self._protocol_session,
221
227
  diagnostics=self._diagnostics,
222
228
  )
229
+ self._report_sender = report_sender
230
+ if self._reporting_mode == "periodic":
231
+ self._report_loop = ReportLoop(
232
+ transport=transport,
233
+ state_store=self._state_store,
234
+ report_period_us=self._config.report_period_us,
235
+ input_report_builder=InputReportBuilder(
236
+ self._controller_profile,
237
+ ),
238
+ session=self._protocol_session,
239
+ diagnostics=self._diagnostics,
240
+ sender=report_sender,
241
+ )
242
+ else:
243
+ self._report_loop = None
223
244
  self._connection_state = "opened"
224
245
  self._is_open = True
225
246
  except Exception:
226
247
  self._connection_state = "failed"
227
248
  await transport.close()
228
249
  self._report_loop = None
250
+ self._report_sender = None
229
251
  self._is_open = False
230
252
  raise
231
253
 
@@ -393,6 +415,7 @@ class ControllerRuntime:
393
415
  )
394
416
  await self._transport.close()
395
417
  self._report_loop = None
418
+ self._report_sender = None
396
419
  self._is_open = False
397
420
  self._connection_state = "closed"
398
421
  finally:
@@ -406,8 +429,15 @@ class ControllerRuntime:
406
429
 
407
430
  This updates local state only and does not send an immediate input report.
408
431
  """
432
+
433
+ def transform(current: InputState) -> InputState:
434
+ return current.with_buttons((*current.buttons, *buttons))
435
+
436
+ if self._reporting_mode == "direct":
437
+ await self._send_direct_update(transform)
438
+ return
409
439
  await self._state_store.update(
410
- lambda current: current.with_buttons((*current.buttons, *buttons)),
440
+ transform,
411
441
  validate=self._controller_profile.validate_input_state,
412
442
  )
413
443
 
@@ -422,6 +452,14 @@ class ControllerRuntime:
422
452
  self._controller_profile.validate_input_state(state)
423
453
  await self._state_store.apply(state)
424
454
 
455
+ async def send(self, state: InputState) -> None:
456
+ """Send one complete state through a direct-reporting runtime."""
457
+ if self._reporting_mode != "direct":
458
+ msg = "send is only available for direct reporting"
459
+ raise InvalidInputError(msg)
460
+ self._validate_input_state(state)
461
+ await self._send_direct_update(lambda _current: state)
462
+
425
463
  async def sticks(self, *, left: Stick | None = None, right: Stick | None = None) -> None:
426
464
  """Replace one or both stick positions without immediate transmission.
427
465
 
@@ -440,8 +478,15 @@ class ControllerRuntime:
440
478
  left=left is not None,
441
479
  right=right is not None,
442
480
  )
481
+
482
+ def transform(current: InputState) -> InputState:
483
+ return current.with_sticks(left_stick=left, right_stick=right)
484
+
485
+ if self._reporting_mode == "direct":
486
+ await self._send_direct_update(transform)
487
+ return
443
488
  await self._state_store.update(
444
- lambda current: current.with_sticks(left_stick=left, right_stick=right),
489
+ transform,
445
490
  validate=self._controller_profile.validate_input_state,
446
491
  )
447
492
 
@@ -484,6 +529,9 @@ class ControllerRuntime:
484
529
 
485
530
  This updates local IMU state only and does not send an immediate input report.
486
531
  """
532
+ if self._reporting_mode == "direct":
533
+ await self._send_direct_update(lambda current: current.with_imu(*frames))
534
+ return
487
535
  await self._state_store.imu(*frames)
488
536
 
489
537
  async def release(self, *buttons: Button) -> None:
@@ -495,13 +543,23 @@ class ControllerRuntime:
495
543
  This updates local state only and does not send an immediate input report.
496
544
  """
497
545
  self._controller_profile.validate_buttons(buttons)
546
+
547
+ def transform(current: InputState) -> InputState:
548
+ return current.with_buttons(current.buttons.difference(buttons))
549
+
550
+ if self._reporting_mode == "direct":
551
+ await self._send_direct_update(transform)
552
+ return
498
553
  await self._state_store.update(
499
- lambda current: current.with_buttons(current.buttons.difference(buttons)),
554
+ transform,
500
555
  validate=self._controller_profile.validate_input_state,
501
556
  )
502
557
 
503
558
  async def neutral(self) -> None:
504
559
  """Return local input state to ``InputState.neutral()`` without immediate transmission."""
560
+ if self._reporting_mode == "direct":
561
+ await self._send_direct_update(lambda _current: InputState.neutral())
562
+ return
505
563
  await self._state_store.neutral()
506
564
 
507
565
  async def tap(self, *buttons: Button, duration: float = 0.08) -> None:
@@ -517,6 +575,9 @@ class ControllerRuntime:
517
575
  The tap sends immediate press and release input reports. The release step
518
576
  removes only the buttons supplied to this call, preserving other held buttons.
519
577
  """
578
+ if self._reporting_mode == "direct":
579
+ await self._tap_direct(*buttons, duration=duration)
580
+ return
520
581
  self._require_connected_for_input()
521
582
  await self.press(*buttons)
522
583
  primary_error: BaseException | None = None
@@ -588,34 +649,94 @@ class ControllerRuntime:
588
649
  )
589
650
 
590
651
  async def _send_trailing_neutral_if_connected(self) -> None:
652
+ if self._reporting_mode == "direct":
653
+ async with self._input_operation_lock:
654
+ if self._report_sender is None or not self._connected_event.is_set():
655
+ return
656
+ await self._send_direct_candidate(InputState.neutral())
657
+ return
591
658
  await self._state_store.neutral()
592
659
  if self._report_loop is None or not self._connected_event.is_set():
593
660
  return
594
661
  await self._report_loop.send_current_input()
595
662
 
596
663
  async def _send_current_input(self) -> None:
597
- if self._report_loop is None:
598
- msg = "gamepad is not open"
599
- raise ClosedError(msg)
600
- await self._report_loop.send_current_input()
664
+ state = await self._state_store.snapshot()
665
+ await self._require_report_sender().send_input(state)
601
666
 
602
667
  def _require_subcommand_reply_sender(self) -> None:
603
- _ = self._subcommand_reply_sender()
668
+ _ = self._require_report_sender()
604
669
 
605
- async def _send_subcommand_reply(self, build_reply: Callable[[], bytes]) -> bytes:
606
- return await self._subcommand_reply_sender().send_subcommand_reply(build_reply)
670
+ async def _send_subcommand_reply(
671
+ self,
672
+ build_reply: Callable[[], bytes | Awaitable[bytes]],
673
+ ) -> bytes:
674
+ if self._report_loop is not None:
675
+ return await self._report_loop.send_subcommand_reply(build_reply)
676
+ return await self._require_report_sender().send_subcommand_reply(build_reply)
607
677
 
608
- def _subcommand_reply_sender(self) -> ReportLoop:
609
- if self._report_loop is None:
678
+ def _require_report_sender(self) -> ReportSender:
679
+ if self._report_sender is None:
610
680
  msg = "gamepad is not open"
611
681
  raise ClosedError(msg)
612
- return self._report_loop
682
+ return self._report_sender
613
683
 
614
684
  def _require_connected_for_input(self) -> None:
615
- if self._report_loop is None or not self._connected_event.is_set():
685
+ if self._report_sender is None or not self._connected_event.is_set():
616
686
  msg = "gamepad is not connected"
617
687
  raise ClosedError(msg)
618
688
 
689
+ async def _send_direct_update(
690
+ self,
691
+ transform: Callable[[InputState], InputState],
692
+ ) -> None:
693
+ async with self._input_operation_lock:
694
+ self._require_connected_for_input()
695
+ candidate = transform(self._state_store.current)
696
+ await self._send_direct_candidate(candidate)
697
+
698
+ async def _send_direct_candidate(self, candidate: InputState) -> None:
699
+ self._controller_profile.validate_input_state(candidate)
700
+ await self._require_report_sender().send_input(
701
+ candidate,
702
+ reason="direct",
703
+ commit_state_store=self._state_store,
704
+ )
705
+
706
+ async def _tap_direct(self, *buttons: Button, duration: float) -> None:
707
+ async with self._input_operation_lock:
708
+ self._require_connected_for_input()
709
+ pressed = False
710
+ primary_error: BaseException | None = None
711
+ try:
712
+ press_state = self._state_store.current.with_buttons(
713
+ (*self._state_store.current.buttons, *buttons)
714
+ )
715
+ await self._send_direct_candidate(press_state)
716
+ pressed = True
717
+ if duration > 0:
718
+ await asyncio.sleep(duration)
719
+ except BaseException as error:
720
+ primary_error = error
721
+ raise
722
+ finally:
723
+ if pressed:
724
+ release_state = self._state_store.current.with_buttons(
725
+ self._state_store.current.buttons.difference(buttons)
726
+ )
727
+ try:
728
+ await self._send_direct_candidate(release_state)
729
+ except Exception as error:
730
+ self._diagnostics.record_error(error, recoverable=True)
731
+ if primary_error is None:
732
+ raise
733
+
734
+ @staticmethod
735
+ def _validate_input_state(state: InputState) -> None:
736
+ if not isinstance(state, InputState):
737
+ msg = "state must be an InputState"
738
+ raise InvalidInputError(msg)
739
+
619
740
  @staticmethod
620
741
  def _validate_stick(name: str, value: Stick | None) -> None:
621
742
  if value is not None and not isinstance(value, Stick):
@@ -735,6 +856,7 @@ class ControllerRuntime:
735
856
  if self._report_loop is not None:
736
857
  await self._report_loop.stop()
737
858
  self._report_loop = None
859
+ self._report_sender = None
738
860
  if self._close_in_progress:
739
861
  return
740
862
  if self._transport is not None and self._is_open:
swbt/report_loop.py CHANGED
@@ -2,17 +2,119 @@
2
2
 
3
3
  import asyncio
4
4
  from collections import deque
5
- from collections.abc import Callable
5
+ from collections.abc import Awaitable, Callable
6
6
  from contextlib import suppress
7
+ from inspect import isawaitable
7
8
  from time import monotonic_ns
9
+ from typing import cast
8
10
 
9
11
  from swbt.diagnostics import DiagnosticsRecorder
12
+ from swbt.input import InputState
10
13
  from swbt.protocol.input_report import InputReportBuilder
11
14
  from swbt.protocol.session import SwitchHidSession
12
15
  from swbt.state_store import InputStateStore
13
16
  from swbt.transport.base import HidDeviceTransport
14
17
 
15
18
  REPLY_PERIODIC_HOLDOFF_SECONDS = 0.3
19
+ ReplyBuilder = Callable[[], bytes | Awaitable[bytes]]
20
+
21
+
22
+ class ReportSender:
23
+ """Serialize input reports and subcommand replies for one HID session."""
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ transport: HidDeviceTransport,
29
+ input_report_builder: InputReportBuilder,
30
+ session: SwitchHidSession,
31
+ diagnostics: DiagnosticsRecorder | None = None,
32
+ clock_ns: Callable[[], int] = monotonic_ns,
33
+ ) -> None:
34
+ """Create a session-scoped serialized report sender."""
35
+ self._transport = transport
36
+ self._input_report_builder = input_report_builder
37
+ self._session = session
38
+ self._diagnostics = diagnostics
39
+ self._clock_ns = clock_ns
40
+ self._timer = 0
41
+ self._send_lock = asyncio.Lock()
42
+
43
+ async def send_input(
44
+ self,
45
+ state: InputState,
46
+ *,
47
+ reason: str = "input",
48
+ commit_state_store: InputStateStore | None = None,
49
+ ) -> None:
50
+ """Send one input state and optionally commit it before releasing the send lock."""
51
+ async with self._send_lock:
52
+ await self._send_input_locked(
53
+ state,
54
+ reason=reason,
55
+ commit_state_store=commit_state_store,
56
+ )
57
+
58
+ async def send_current_input(
59
+ self,
60
+ state_store: InputStateStore,
61
+ *,
62
+ reason: str = "input",
63
+ ) -> None:
64
+ """Snapshot and send the current input state under the shared send lock."""
65
+ async with self._send_lock:
66
+ state = await state_store.snapshot()
67
+ await self._send_input_locked(state, reason=reason)
68
+
69
+ async def send_subcommand_reply(self, build_report: ReplyBuilder) -> bytes:
70
+ """Build and send a subcommand reply under the shared send lock."""
71
+ async with self._send_lock:
72
+ built = build_report()
73
+ if isawaitable(built):
74
+ report = await cast("Awaitable[bytes]", built)
75
+ else:
76
+ report = built
77
+ await self._send_subcommand_report_locked(report)
78
+ return report
79
+
80
+ async def send_subcommand_report(self, report: bytes) -> None:
81
+ """Send an already-built subcommand reply under the shared send lock."""
82
+ async with self._send_lock:
83
+ await self._send_subcommand_report_locked(report)
84
+
85
+ async def _send_subcommand_report_locked(self, report: bytes) -> None:
86
+ reply = bytearray(report)
87
+ if reply and reply[0] == 0x21:
88
+ reply[1] = self._timer
89
+ await self._send_report(bytes(reply), reason="subcommand_reply")
90
+ if reply and reply[0] == 0x21:
91
+ self._advance_timer()
92
+
93
+ async def _send_input_locked(
94
+ self,
95
+ state: InputState,
96
+ *,
97
+ reason: str,
98
+ commit_state_store: InputStateStore | None = None,
99
+ ) -> None:
100
+ imu_block = self._session.encode_imu(state.imu_frames, now_ns=self._clock_ns())
101
+ report = self._input_report_builder.build_0x30(
102
+ state,
103
+ timer=self._timer,
104
+ imu_block=imu_block,
105
+ )
106
+ await self._send_report(report, reason=reason)
107
+ if commit_state_store is not None:
108
+ await commit_state_store.apply(state)
109
+ self._advance_timer()
110
+
111
+ async def _send_report(self, report: bytes, *, reason: str) -> None:
112
+ await self._transport.send_interrupt(report)
113
+ if self._diagnostics is not None:
114
+ self._diagnostics.record_report_tx(report_id=report[0], reason=reason)
115
+
116
+ def _advance_timer(self) -> None:
117
+ self._timer = (self._timer + 1) & 0xFF
16
118
 
17
119
 
18
120
  class ReportLoop:
@@ -28,19 +130,20 @@ class ReportLoop:
28
130
  report_period_us: int = 8000,
29
131
  diagnostics: DiagnosticsRecorder | None = None,
30
132
  clock_ns: Callable[[], int] = monotonic_ns,
133
+ sender: ReportSender | None = None,
31
134
  ) -> None:
32
135
  """Create a report loop helper."""
33
- self._transport = transport
34
136
  self._state_store = state_store
35
137
  self._report_period_seconds = report_period_us / 1_000_000
36
- self._input_report_builder = input_report_builder
37
- self._session = session
38
- self._clock_ns = clock_ns
39
- self._diagnostics = diagnostics
138
+ self._sender = sender or ReportSender(
139
+ transport=transport,
140
+ input_report_builder=input_report_builder,
141
+ session=session,
142
+ diagnostics=diagnostics,
143
+ clock_ns=clock_ns,
144
+ )
40
145
  self._reply_queue: deque[bytes] = deque()
41
- self._timer = 0
42
146
  self._periodic_holdoff_until = 0.0
43
- self._send_lock = asyncio.Lock()
44
147
  self._task: asyncio.Task[None] | None = None
45
148
 
46
149
  def start(self) -> None:
@@ -61,16 +164,13 @@ class ReportLoop:
61
164
 
62
165
  async def send_current_input(self, *, reason: str = "input") -> None:
63
166
  """Send one 0x30 input report for the current state."""
64
- async with self._send_lock:
65
- await self._send_current_input_locked(reason=reason)
167
+ await self._sender.send_current_input(self._state_store, reason=reason)
66
168
 
67
- async def send_subcommand_reply(self, build_report: Callable[[], bytes]) -> bytes:
169
+ async def send_subcommand_reply(self, build_report: ReplyBuilder) -> bytes:
68
170
  """Apply a subcommand transition and send its reply under the send lock."""
69
- async with self._send_lock:
70
- report = build_report()
71
- await self._send_subcommand_reply_locked(report)
72
- self._holdoff_periodic_after_reply()
73
- return report
171
+ report = await self._sender.send_subcommand_reply(build_report)
172
+ self._holdoff_periodic_after_reply()
173
+ return report
74
174
 
75
175
  def queue_reply(self, report: bytes) -> None:
76
176
  """Queue one subcommand reply for priority transmission."""
@@ -78,48 +178,19 @@ class ReportLoop:
78
178
 
79
179
  async def send_next_report(self) -> None:
80
180
  """Send the next queued reply or current input report."""
81
- async with self._send_lock:
82
- if self._reply_queue:
83
- await self._send_subcommand_reply_locked(self._reply_queue.popleft())
84
- self._holdoff_periodic_after_reply()
85
- return
86
- if self._is_periodic_held_off():
87
- return
88
- await self._send_current_input_locked(reason="periodic")
89
-
90
- async def _send_current_input_locked(self, *, reason: str) -> None:
91
- state = await self._state_store.snapshot()
92
- imu_block = self._session.encode_imu(state.imu_frames, now_ns=self._clock_ns())
93
- report = self._input_report_builder.build_0x30(
94
- state,
95
- timer=self._timer,
96
- imu_block=imu_block,
97
- )
98
- await self._send_report(report, reason=reason)
99
- self._advance_timer()
100
-
101
- async def _send_subcommand_reply_locked(self, report: bytes) -> None:
102
- reply = bytearray(report)
103
- if reply and reply[0] == 0x21:
104
- reply[1] = self._timer
105
- await self._send_report(bytes(reply), reason="subcommand_reply")
106
- if reply and reply[0] == 0x21:
107
- self._advance_timer()
181
+ if self._reply_queue:
182
+ await self._sender.send_subcommand_report(self._reply_queue.popleft())
183
+ self._holdoff_periodic_after_reply()
184
+ return
185
+ if self._is_periodic_held_off():
186
+ return
187
+ await self.send_current_input(reason="periodic")
108
188
 
109
189
  async def _run(self) -> None:
110
190
  while True:
111
191
  await asyncio.sleep(self._report_period_seconds)
112
192
  await self.send_next_report()
113
193
 
114
- async def _send_report(self, report: bytes, *, reason: str) -> None:
115
- await self._transport.send_interrupt(report)
116
- report_id = report[0]
117
- if self._diagnostics is not None:
118
- self._diagnostics.record_report_tx(report_id=report_id, reason=reason)
119
-
120
- def _advance_timer(self) -> None:
121
- self._timer = (self._timer + 1) & 0xFF
122
-
123
194
  def _holdoff_periodic_after_reply(self) -> None:
124
195
  self._periodic_holdoff_until = (
125
196
  asyncio.get_running_loop().time() + REPLY_PERIODIC_HOLDOFF_SECONDS
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swbt-python
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -1,17 +1,17 @@
1
- swbt/__init__.py,sha256=94cScYM8xa8VeoR5OodUuwj3Vq6bOIBxKoMBIQ5Itlc,1170
1
+ swbt/__init__.py,sha256=2IR5hiqRDZCUgDDcnXpzSX8lsGWcEX9gqOGgz1GUmUY,1410
2
2
  swbt/_testing/__init__.py,sha256=RtJg6CXFY42HTLehYp33Ilrliqhp4wOSN6cjIGoRE8A,58
3
- swbt/_testing/gamepad.py,sha256=n9jMFER-EnLwhHBEytyTUpoSMEDF0leghcmVVvoFNjs,2493
3
+ swbt/_testing/gamepad.py,sha256=xibVgsA_Un0C1WZt6pIHOZXefoR27XOEMZxwLbufPJY,4514
4
4
  swbt/adapter_discovery.py,sha256=cvlMQgmvmR-wsB0jPe9bipBBxcIVbhO6NRkTAKnPBYA,7717
5
5
  swbt/diagnostics.py,sha256=HdOiBpZ8eAlESmibgsR_HLq5V0-OEDUpVUwJ1D9eN28,7281
6
6
  swbt/errors.py,sha256=rKQZs-EO_5BQPtrZSRpirc-6_ymyxt1Q04VZi8y29cI,3196
7
- swbt/gamepad/__init__.py,sha256=s3MMJ--NwtxmzlJKIgfiTfEtjYP8JTQv7Qs3OKJcYRM,446
7
+ swbt/gamepad/__init__.py,sha256=4EQM1jG8Ev2UVjb9Zk3idlyYFZ1eLM5RqzHsurkV6z8,725
8
8
  swbt/gamepad/_config.py,sha256=4Tq3Jlzv9IoyhQ0sDEBmIZ-Fz1Upc4koE4vnTQHDgrg,3793
9
9
  swbt/gamepad/connection.py,sha256=My3deMw4L64UoEXjeyRNBs1D6UvrvkeC2P7yf_PLPYQ,7776
10
10
  swbt/gamepad/constants.py,sha256=gDesRFvVxZ3SMSiP4qzd42em_Dt9M5hIu3-2exgKEus,84
11
- swbt/gamepad/core.py,sha256=vGY24NsCxJ37XY3jagk6P1DZzDM1JJwXdggZM7fk1Ug,15042
12
- swbt/gamepad/interface.py,sha256=GgPo5VwFhvcPIDy265R_dOQSYrBN1MYekqyPvIuXWQY,9431
13
- swbt/gamepad/output.py,sha256=Et2efB09tkNZF50_0ANk-JjHTJsJBpbzSdsJeWMDtfA,4101
14
- swbt/gamepad/runtime.py,sha256=28wDzmCWWEihSLnzg2E0MYP73z3fIGoc2Vu98XZuH88,30508
11
+ swbt/gamepad/core.py,sha256=eJei4mccT7cTLmMkR7TnAb4_gVj6aDqgG9tiJp-vTQU,17880
12
+ swbt/gamepad/interface.py,sha256=Q_iU-te54mXumg7LypchVpmJb5qcn8hxGLMwVhKNNaE,10562
13
+ swbt/gamepad/output.py,sha256=dx7xCBL83fs_jjgsdBVfji3pbalGqi92CwlbB7qzNxM,4173
14
+ swbt/gamepad/runtime.py,sha256=1OD9xyPhWg4lFRjM21PAqjJC-hk--eBkHz9PIQRLY68,35584
15
15
  swbt/gamepad/transport_factory.py,sha256=2LLGIF3Bc4ufBNjp1AcdqNkHNP8ZBDFU2PNt898vo8o,2059
16
16
  swbt/imu.py,sha256=sT1_MsYECQlQ-pSfayD8gojD9l3w9zEtOI3xP1IovQM,4237
17
17
  swbt/input.py,sha256=7gv75lzrcmoV8Sijz7f1pvsheRrmUi_TCIMW3DjUVjw,22275
@@ -31,7 +31,7 @@ swbt/protocol/session.py,sha256=TtnnFySalCXsEUwVv0DX9w1E2xq1yFIfmgCrA6wGHzs,3758
31
31
  swbt/protocol/spi.py,sha256=buKyNYYOHOSYbpVh3Wa2968FL4VIO_43QcSXNowNfJs,2594
32
32
  swbt/protocol/subcommand.py,sha256=1Z3Xf8xo7AydhyTa3bHgHM24hN2YS37yp7eioOHTu0A,7633
33
33
  swbt/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
34
- swbt/report_loop.py,sha256=3qGLLWpJo_lWMdYXkkLqajZrQJtTLF2w7Z1z7iruB8c,4903
34
+ swbt/report_loop.py,sha256=HFAPdm9Wx8zBT-sqUn_zmLf6boahl_BsmoxfkifbmYE,7318
35
35
  swbt/state_store.py,sha256=Mf8uKR5CoQdzyMmqflNE_2KUWE_ZgmLaHcGXUcVThW4,2693
36
36
  swbt/transport/__init__.py,sha256=b_Vqsg0k_g7XclN9XHQrcJxqCwGIklxG5NqzISal4_I,68
37
37
  swbt/transport/_bumble_acl.py,sha256=RnueTwEgAd0w3YgxnXkw0AKQXMeECIRope96W-xQ1K8,1425
@@ -43,8 +43,8 @@ swbt/transport/_bumble_usb_devices.py,sha256=soAKiHTzeeTYdb7M357yaac-RxG2c_BiKWH
43
43
  swbt/transport/base.py,sha256=lAGAJjGMg5CwtdAKmefCZglWqT3mWzm7CFOOCGGKul4,5619
44
44
  swbt/transport/bumble.py,sha256=gB0vy1GW-1Mbg_EZuYSrdOKVClKyNEKXMy1jju41Bts,30147
45
45
  swbt/transport/fake.py,sha256=Pmr-9a3qg2v9Rs86wBjINdWmWi5KOZlcFMH3AOmWF58,12903
46
- swbt_python-0.3.0.dist-info/licenses/LICENSE,sha256=FzzUoYjuIh5DzpeaujIbDUTylYnyzcHRVU-_ZMyowyk,1065
47
- swbt_python-0.3.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
48
- swbt_python-0.3.0.dist-info/entry_points.txt,sha256=YYQc7VgoSy74iZ6s8Qr6iJpnvFoDpp8G4-v0KJH5TGo,48
49
- swbt_python-0.3.0.dist-info/METADATA,sha256=Dta7JIdGnIRn3tZ8xqguwesI8Vf94-4CIL5b-UMbHWA,5994
50
- swbt_python-0.3.0.dist-info/RECORD,,
46
+ swbt_python-0.4.0.dist-info/licenses/LICENSE,sha256=FzzUoYjuIh5DzpeaujIbDUTylYnyzcHRVU-_ZMyowyk,1065
47
+ swbt_python-0.4.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
48
+ swbt_python-0.4.0.dist-info/entry_points.txt,sha256=YYQc7VgoSy74iZ6s8Qr6iJpnvFoDpp8G4-v0KJH5TGo,48
49
+ swbt_python-0.4.0.dist-info/METADATA,sha256=CjgPwuJCCm3HlnkVf6vMZUnowaeEA0PxLPp3Eo026iI,5994
50
+ swbt_python-0.4.0.dist-info/RECORD,,