swbt-python 0.2.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
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
5
5
 
6
6
  from swbt.diagnostics import DiagnosticsRecorder
7
7
  from swbt.protocol.output_report import OutputReportParser
8
+ from swbt.protocol.session import SwitchHidSession
8
9
  from swbt.protocol.subcommand import (
9
10
  SESSION_STATE_SUBCOMMANDS,
10
11
  SubcommandResponder,
@@ -12,7 +13,8 @@ from swbt.protocol.subcommand import (
12
13
  )
13
14
  from swbt.state_store import InputStateStore
14
15
 
15
- ReplySender = Callable[[bytes], Awaitable[None]]
16
+ ReplyBuilder = Callable[[], bytes | Awaitable[bytes]]
17
+ ReplySender = Callable[[ReplyBuilder], Awaitable[bytes]]
16
18
  ReplySenderRequirement = Callable[[], None]
17
19
 
18
20
 
@@ -23,6 +25,7 @@ class OutputReportDispatcher:
23
25
  diagnostics: DiagnosticsRecorder
24
26
  require_reply_sender: ReplySenderRequirement
25
27
  send_subcommand_reply: ReplySender
28
+ session: SwitchHidSession
26
29
  state_store: InputStateStore
27
30
  output_report_parser: OutputReportParser = field(default_factory=OutputReportParser)
28
31
  subcommand_responder: SubcommandResponder = field(default_factory=SubcommandResponder)
@@ -47,9 +50,17 @@ class OutputReportDispatcher:
47
50
  subcommand_id=output_report.subcommand_id,
48
51
  )
49
52
  self.require_reply_sender()
50
- state = await self.state_store.snapshot()
51
53
  try:
52
- reply = self.subcommand_responder.respond(output_report, state=state)
54
+
55
+ async def build_reply() -> bytes:
56
+ state = await self.state_store.snapshot()
57
+ return self.subcommand_responder.respond(
58
+ output_report,
59
+ state=state,
60
+ session=self.session,
61
+ )
62
+
63
+ reply = await self.send_subcommand_reply(build_reply)
53
64
  except UnsupportedSubcommandError:
54
65
  self.diagnostics.record_event(
55
66
  "unsupported_subcommand",
@@ -59,10 +70,11 @@ class OutputReportDispatcher:
59
70
  )
60
71
  raise
61
72
  if output_report.subcommand_id in SESSION_STATE_SUBCOMMANDS:
62
- session_state = self.subcommand_responder.session_state
73
+ session_state = self.session.state
63
74
  self.diagnostics.record_event(
64
75
  "subcommand_session_state",
65
76
  imu_enabled=session_state.imu_enabled,
77
+ imu_encoding_format=session_state.imu_encoding_format,
66
78
  imu_mode=_format_optional_byte(session_state.imu_mode),
67
79
  packet_id=output_report.packet_id,
68
80
  report_mode=_format_optional_byte(session_state.report_mode),
@@ -79,7 +91,6 @@ class OutputReportDispatcher:
79
91
  report_id=_format_report_id(reply[0]),
80
92
  subcommand_id=subcommand_id,
81
93
  )
82
- await self.send_subcommand_reply(reply)
83
94
 
84
95
 
85
96
  def _format_report_id(report_id: int) -> str: