swbt-python 0.1.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.
@@ -0,0 +1,311 @@
1
+ """Lifecycle and connection diagnostics helpers for Bumble HID transport."""
2
+
3
+ from collections.abc import Callable
4
+ from contextlib import suppress
5
+ from typing import Any, Protocol, cast
6
+
7
+
8
+ class EventRecorder(Protocol):
9
+ def __call__(self, event: str, **fields: object) -> None:
10
+ """Record one diagnostics event."""
11
+
12
+
13
+ class ConnectionDiagnostics:
14
+ """Register diagnostics callbacks on a Bumble connection object."""
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ adapter: str,
20
+ handle_disconnection: Callable[[int | None], None],
21
+ record_event: EventRecorder,
22
+ ) -> None:
23
+ self._adapter = adapter
24
+ self._handle_disconnection = handle_disconnection
25
+ self._record_event = record_event
26
+
27
+ def register(self, connection: object) -> None:
28
+ """Register all connection diagnostics callbacks supported by the connection."""
29
+ on_event = getattr(connection, "on", None)
30
+ if not callable(on_event):
31
+ return
32
+ on_event(
33
+ getattr(connection, "EVENT_DISCONNECTION", "disconnection"),
34
+ self._handle_disconnection,
35
+ )
36
+ on_event(
37
+ getattr(connection, "EVENT_CLASSIC_PAIRING", "classic_pairing"),
38
+ lambda *_args: self._record_event("classic_pairing", adapter=self._adapter),
39
+ )
40
+ on_event(
41
+ getattr(connection, "EVENT_CLASSIC_PAIRING_FAILURE", "classic_pairing_failure"),
42
+ lambda reason=None, *_args: self._record_event(
43
+ "classic_pairing_failure",
44
+ adapter=self._adapter,
45
+ reason=reason,
46
+ ),
47
+ )
48
+ on_event(
49
+ getattr(connection, "EVENT_PAIRING_START", "pairing_start"),
50
+ lambda *_args: self._record_event("pairing_start", adapter=self._adapter),
51
+ )
52
+ on_event(
53
+ getattr(connection, "EVENT_PAIRING", "pairing"),
54
+ lambda keys=None, *_args: self._record_pairing_complete(connection, keys),
55
+ )
56
+ on_event(
57
+ getattr(connection, "EVENT_PAIRING_FAILURE", "pairing_failure"),
58
+ lambda reason=None, *_args: self._record_event(
59
+ "pairing_failure",
60
+ adapter=self._adapter,
61
+ reason=reason,
62
+ ),
63
+ )
64
+ on_event(
65
+ getattr(connection, "EVENT_CONNECTION_AUTHENTICATION", "connection_authentication"),
66
+ lambda *_args: self._record_connection_authentication(connection),
67
+ )
68
+ on_event(
69
+ getattr(
70
+ connection,
71
+ "EVENT_CONNECTION_AUTHENTICATION_FAILURE",
72
+ "connection_authentication_failure",
73
+ ),
74
+ lambda error=None, *_args: self._record_event(
75
+ "connection_authentication_failure",
76
+ adapter=self._adapter,
77
+ error=safe_event_value(error),
78
+ ),
79
+ )
80
+ on_event(
81
+ getattr(
82
+ connection,
83
+ "EVENT_CONNECTION_ENCRYPTION_CHANGE",
84
+ "connection_encryption_change",
85
+ ),
86
+ lambda *_args: self._record_connection_encryption_change(connection),
87
+ )
88
+ on_event(
89
+ getattr(
90
+ connection,
91
+ "EVENT_CONNECTION_ENCRYPTION_FAILURE",
92
+ "connection_encryption_failure",
93
+ ),
94
+ lambda error=None, *_args: self._record_event(
95
+ "connection_encryption_failure",
96
+ adapter=self._adapter,
97
+ error=safe_event_value(error),
98
+ ),
99
+ )
100
+ on_event(
101
+ getattr(
102
+ connection,
103
+ "EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH",
104
+ "connection_encryption_key_refresh",
105
+ ),
106
+ lambda *_args: self._record_connection_encryption_refresh(connection),
107
+ )
108
+ on_event(
109
+ getattr(connection, "EVENT_LINK_KEY", "link_key"),
110
+ lambda *_args: self._record_event("link_key_available", adapter=self._adapter),
111
+ )
112
+ on_event(
113
+ getattr(connection, "EVENT_MODE_CHANGE", "mode_change"),
114
+ lambda *_args: self._record_classic_mode_change(connection),
115
+ )
116
+ on_event(
117
+ getattr(connection, "EVENT_MODE_CHANGE_FAILURE", "mode_change_failure"),
118
+ lambda status=None, *_args: self._record_event(
119
+ "classic_mode_change_failure",
120
+ adapter=self._adapter,
121
+ status=status,
122
+ ),
123
+ )
124
+
125
+ def _record_pairing_complete(self, connection: object, keys: object | None) -> None:
126
+ fields = connection_security_fields(connection)
127
+ fields["has_link_key"] = keys_include_link_key(keys)
128
+ self._record_event("pairing_complete", adapter=self._adapter, **fields)
129
+
130
+ def _record_connection_authentication(self, connection: object) -> None:
131
+ self._record_event(
132
+ "connection_authentication",
133
+ adapter=self._adapter,
134
+ authenticated=getattr(connection, "authenticated", None),
135
+ )
136
+
137
+ def _record_connection_encryption_change(self, connection: object) -> None:
138
+ self._record_event(
139
+ "connection_encryption_change",
140
+ adapter=self._adapter,
141
+ **connection_security_fields(connection),
142
+ )
143
+
144
+ def _record_connection_encryption_refresh(self, connection: object) -> None:
145
+ self._record_event(
146
+ "connection_encryption_key_refresh",
147
+ adapter=self._adapter,
148
+ **connection_security_fields(connection),
149
+ )
150
+
151
+ def _record_classic_mode_change(self, connection: object) -> None:
152
+ self._record_event(
153
+ "classic_mode_change",
154
+ adapter=self._adapter,
155
+ mode=getattr(connection, "classic_mode", None),
156
+ interval=getattr(connection, "classic_interval", None),
157
+ )
158
+
159
+
160
+ def register_connection_diagnostics(
161
+ *,
162
+ adapter: str,
163
+ connection: object,
164
+ handle_disconnection: Callable[[int | None], None],
165
+ record_event: EventRecorder,
166
+ ) -> None:
167
+ """Register connection-level diagnostics callbacks."""
168
+ ConnectionDiagnostics(
169
+ adapter=adapter,
170
+ handle_disconnection=handle_disconnection,
171
+ record_event=record_event,
172
+ ).register(connection)
173
+
174
+
175
+ def register_connection_request_bridge(
176
+ *,
177
+ adapter: str,
178
+ device: object,
179
+ record_event: EventRecorder,
180
+ ) -> None:
181
+ """Wrap Bumble's connection request callback while avoiding deprecated sync APIs."""
182
+ original_connection_request = cast("Any", device).on_connection_request
183
+
184
+ def on_connection_request(
185
+ bd_addr: object,
186
+ class_of_device: int,
187
+ link_type: int,
188
+ ) -> None:
189
+ record_event(
190
+ "connection_request",
191
+ adapter=adapter,
192
+ class_of_device=f"0x{class_of_device:06x}",
193
+ link_type=link_type,
194
+ peer_address=str(bd_addr),
195
+ )
196
+ call_connection_request_without_deprecated_sync_command(
197
+ device,
198
+ original_connection_request,
199
+ bd_addr,
200
+ class_of_device,
201
+ link_type,
202
+ )
203
+
204
+ device_with_attrs = cast("Any", device)
205
+ device_with_attrs.on_connection_request = on_connection_request
206
+ replace_host_connection_request_listener(
207
+ device,
208
+ original_connection_request,
209
+ on_connection_request,
210
+ )
211
+
212
+
213
+ def register_l2cap_lifecycle_bridge(
214
+ *,
215
+ hid_device: object,
216
+ notify_connected_if_ready: Callable[[], None],
217
+ notify_disconnected_if_channels_closed: Callable[[], None],
218
+ record_l2cap_channel_event: Callable[[str, object], None],
219
+ set_l2cap_connected_emitted: Callable[[bool], None],
220
+ ) -> None:
221
+ """Wrap HID L2CAP lifecycle callbacks and emit transport diagnostics."""
222
+ hid_device_with_attrs = cast("Any", hid_device)
223
+ original_open = hid_device_with_attrs.on_l2cap_channel_open
224
+ original_close = hid_device_with_attrs.on_l2cap_channel_close
225
+
226
+ def on_l2cap_channel_open(l2cap_channel: object) -> None:
227
+ original_open(l2cap_channel)
228
+ record_l2cap_channel_event("l2cap_channel_open", l2cap_channel)
229
+ notify_connected_if_ready()
230
+
231
+ def on_l2cap_channel_close(l2cap_channel: object) -> None:
232
+ original_close(l2cap_channel)
233
+ record_l2cap_channel_event("l2cap_channel_close", l2cap_channel)
234
+ set_l2cap_connected_emitted(False)
235
+ notify_disconnected_if_channels_closed()
236
+
237
+ hid_device_with_attrs.on_l2cap_channel_open = on_l2cap_channel_open
238
+ hid_device_with_attrs.on_l2cap_channel_close = on_l2cap_channel_close
239
+
240
+
241
+ def connection_security_fields(connection: object) -> dict[str, object]:
242
+ return {
243
+ "authenticated": getattr(connection, "authenticated", None),
244
+ "encryption": getattr(connection, "encryption", None),
245
+ "encryption_key_size": getattr(connection, "encryption_key_size", None),
246
+ "secure_connections": getattr(connection, "sc", None),
247
+ }
248
+
249
+
250
+ def keys_include_link_key(keys: object | None) -> bool | None:
251
+ if keys is None:
252
+ return None
253
+ return getattr(keys, "link_key", None) is not None
254
+
255
+
256
+ def safe_event_value(value: object) -> object:
257
+ if value is None or isinstance(value, int | str | bool | float):
258
+ return value
259
+ return str(value)
260
+
261
+
262
+ def call_connection_request_without_deprecated_sync_command(
263
+ device: object,
264
+ connection_request: Callable[[object, int, int], None],
265
+ bd_addr: object,
266
+ class_of_device: int,
267
+ link_type: int,
268
+ ) -> None:
269
+ """Run Bumble's connection request handler without its deprecated sync helper."""
270
+ host = getattr(device, "host", None)
271
+ send_async_command = getattr(host, "send_async_command", None)
272
+ if host is None or not callable(send_async_command):
273
+ connection_request(bd_addr, class_of_device, link_type)
274
+ return
275
+
276
+ from bumble import utils # noqa: PLC0415
277
+
278
+ missing = object()
279
+ host_dict = getattr(host, "__dict__", None)
280
+ previous_instance_attr = (
281
+ host_dict.get("send_command_sync", missing) if isinstance(host_dict, dict) else missing
282
+ )
283
+
284
+ def send_command_sync(command: object) -> None:
285
+ utils.AsyncRunner.spawn(send_async_command(command))
286
+
287
+ host_with_attrs = cast("Any", host)
288
+ host_with_attrs.send_command_sync = send_command_sync
289
+ try:
290
+ connection_request(bd_addr, class_of_device, link_type)
291
+ finally:
292
+ if previous_instance_attr is missing:
293
+ del host_with_attrs.send_command_sync
294
+ else:
295
+ host_with_attrs.send_command_sync = previous_instance_attr
296
+
297
+
298
+ def replace_host_connection_request_listener(
299
+ device: object,
300
+ original_connection_request: Callable[[object, int, int], None],
301
+ replacement_connection_request: Callable[[object, int, int], None],
302
+ ) -> None:
303
+ host = getattr(device, "host", None)
304
+ remove_listener = getattr(host, "remove_listener", None)
305
+ on = getattr(host, "on", None)
306
+ if not callable(remove_listener) or not callable(on):
307
+ return
308
+
309
+ with suppress(KeyError, ValueError):
310
+ remove_listener("connection_request", original_connection_request)
311
+ on("connection_request", replacement_connection_request)
@@ -0,0 +1,203 @@
1
+ """SDP service record builder for Bumble HID transport."""
2
+
3
+ from swbt.transport._bumble_hidp import HID_CONTROL_PSM, HID_INTERRUPT_PSM
4
+
5
+ _HID_SERVICE_RECORD_HANDLE = 0x00010001
6
+ _HID_REPORT_DESCRIPTOR_TYPE = 0x22
7
+ _DEFAULT_DEVICE_NAME = "Pro Controller"
8
+
9
+ _SDP_HID_PARSER_VERSION_ATTRIBUTE_ID = 0x0201
10
+ _SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID = 0x0202
11
+ _SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID = 0x0203
12
+ _SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID = 0x0204
13
+ _SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID = 0x0205
14
+ _SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0x0206
15
+ _SDP_HID_LANG_ID_BASE_LIST_ATTRIBUTE_ID = 0x0207
16
+ _SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID = 0x020A
17
+ _SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID = 0x020B
18
+ _SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID = 0x020C
19
+ _SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID = 0x020D
20
+ _SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID = 0x020E
21
+ _SDP_HIDSSR_HOST_MAX_LATENCY_ATTRIBUTE_ID = 0x020F
22
+ _SDP_HIDSSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210
23
+ _SDP_PRIMARY_LANGUAGE_SERVICE_NAME_ATTRIBUTE_ID = 0x0100
24
+
25
+ _LANGUAGE_BASE_EN_US = 0x0100
26
+ _SDP_LANGUAGE_CODE_ENGLISH = 0x656E
27
+ _SDP_CHARACTER_ENCODING_UTF8 = 0x006A
28
+ _LANGUAGE_ID_EN_US = 0x0409
29
+ _REFERENCE_HID_COUNTRY_CODE = 0x21
30
+ _REFERENCE_HID_SUPERVISION_TIMEOUT = 0x0C80
31
+ _REFERENCE_HIDSSR_HOST_MAX_LATENCY = 0xFFFF
32
+ _REFERENCE_HIDSSR_HOST_MIN_TIMEOUT = 0xFFFF
33
+
34
+
35
+ def build_hid_service_records(
36
+ hid_descriptor: bytes,
37
+ *,
38
+ device_name: str = _DEFAULT_DEVICE_NAME,
39
+ ) -> dict[int, list[object]]:
40
+ """Build the Classic HID SDP service record used by the reference controller."""
41
+ from bumble import core # noqa: PLC0415
42
+ from bumble.sdp import ( # noqa: PLC0415
43
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
44
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
45
+ SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
46
+ SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID,
47
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
48
+ SDP_PUBLIC_BROWSE_ROOT,
49
+ SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
50
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
51
+ DataElement,
52
+ ServiceAttribute,
53
+ )
54
+
55
+ return {
56
+ _HID_SERVICE_RECORD_HANDLE: [
57
+ ServiceAttribute(
58
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
59
+ DataElement.unsigned_integer_32(_HID_SERVICE_RECORD_HANDLE),
60
+ ),
61
+ ServiceAttribute(
62
+ SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
63
+ DataElement.sequence([DataElement.uuid(core.BT_HUMAN_INTERFACE_DEVICE_SERVICE)]),
64
+ ),
65
+ ServiceAttribute(
66
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
67
+ DataElement.sequence(
68
+ [
69
+ DataElement.sequence(
70
+ [
71
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
72
+ DataElement.unsigned_integer_16(HID_CONTROL_PSM),
73
+ ]
74
+ ),
75
+ DataElement.sequence([DataElement.uuid(core.BT_HIDP_PROTOCOL_ID)]),
76
+ ]
77
+ ),
78
+ ),
79
+ ServiceAttribute(
80
+ SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
81
+ DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
82
+ ),
83
+ ServiceAttribute(
84
+ SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID,
85
+ DataElement.sequence(
86
+ [
87
+ DataElement.unsigned_integer_16(_SDP_LANGUAGE_CODE_ENGLISH),
88
+ DataElement.unsigned_integer_16(_SDP_CHARACTER_ENCODING_UTF8),
89
+ DataElement.unsigned_integer_16(_LANGUAGE_BASE_EN_US),
90
+ ]
91
+ ),
92
+ ),
93
+ ServiceAttribute(
94
+ _SDP_PRIMARY_LANGUAGE_SERVICE_NAME_ATTRIBUTE_ID,
95
+ DataElement.text_string(device_name.encode("utf-8")),
96
+ ),
97
+ ServiceAttribute(
98
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
99
+ DataElement.sequence(
100
+ [
101
+ DataElement.sequence(
102
+ [
103
+ DataElement.uuid(core.BT_HUMAN_INTERFACE_DEVICE_SERVICE),
104
+ DataElement.unsigned_integer_16(0x0101),
105
+ ]
106
+ )
107
+ ]
108
+ ),
109
+ ),
110
+ ServiceAttribute(
111
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
112
+ DataElement.sequence(
113
+ [
114
+ DataElement.sequence(
115
+ [
116
+ DataElement.sequence(
117
+ [
118
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
119
+ DataElement.unsigned_integer_16(HID_INTERRUPT_PSM),
120
+ ]
121
+ ),
122
+ DataElement.sequence([DataElement.uuid(core.BT_HIDP_PROTOCOL_ID)]),
123
+ ]
124
+ )
125
+ ]
126
+ ),
127
+ ),
128
+ ServiceAttribute(
129
+ _SDP_HID_PARSER_VERSION_ATTRIBUTE_ID,
130
+ DataElement.unsigned_integer_16(0x0111),
131
+ ),
132
+ ServiceAttribute(
133
+ _SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID,
134
+ DataElement.unsigned_integer_8(0x08),
135
+ ),
136
+ ServiceAttribute(
137
+ _SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID,
138
+ DataElement.unsigned_integer_8(_REFERENCE_HID_COUNTRY_CODE),
139
+ ),
140
+ ServiceAttribute(
141
+ _SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID,
142
+ DataElement.boolean(True),
143
+ ),
144
+ ServiceAttribute(
145
+ _SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID,
146
+ DataElement.boolean(True),
147
+ ),
148
+ ServiceAttribute(
149
+ _SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID,
150
+ DataElement.sequence(
151
+ [
152
+ DataElement.sequence(
153
+ [
154
+ DataElement.unsigned_integer_8(_HID_REPORT_DESCRIPTOR_TYPE),
155
+ DataElement.text_string(hid_descriptor),
156
+ ]
157
+ )
158
+ ]
159
+ ),
160
+ ),
161
+ ServiceAttribute(
162
+ _SDP_HID_LANG_ID_BASE_LIST_ATTRIBUTE_ID,
163
+ DataElement.sequence(
164
+ [
165
+ DataElement.sequence(
166
+ [
167
+ DataElement.unsigned_integer_16(_LANGUAGE_ID_EN_US),
168
+ DataElement.unsigned_integer_16(_LANGUAGE_BASE_EN_US),
169
+ ]
170
+ )
171
+ ]
172
+ ),
173
+ ),
174
+ ServiceAttribute(
175
+ _SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID,
176
+ DataElement.boolean(True),
177
+ ),
178
+ ServiceAttribute(
179
+ _SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID,
180
+ DataElement.unsigned_integer_16(0x0101),
181
+ ),
182
+ ServiceAttribute(
183
+ _SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID,
184
+ DataElement.unsigned_integer_16(_REFERENCE_HID_SUPERVISION_TIMEOUT),
185
+ ),
186
+ ServiceAttribute(
187
+ _SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID,
188
+ DataElement.boolean(True),
189
+ ),
190
+ ServiceAttribute(
191
+ _SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID,
192
+ DataElement.boolean(False),
193
+ ),
194
+ ServiceAttribute(
195
+ _SDP_HIDSSR_HOST_MAX_LATENCY_ATTRIBUTE_ID,
196
+ DataElement.unsigned_integer_16(_REFERENCE_HIDSSR_HOST_MAX_LATENCY),
197
+ ),
198
+ ServiceAttribute(
199
+ _SDP_HIDSSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID,
200
+ DataElement.unsigned_integer_16(_REFERENCE_HIDSSR_HOST_MIN_TIMEOUT),
201
+ ),
202
+ ]
203
+ }
swbt/transport/base.py ADDED
@@ -0,0 +1,81 @@
1
+ """Transport protocol shared by gamepad and transport implementations."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from typing import Literal, Protocol
6
+
7
+ InterruptDataCallback = Callable[[bytes], Awaitable[None]]
8
+ ControlDataCallback = Callable[[bytes], Awaitable[None]]
9
+ ConnectedCallback = Callable[[], Awaitable[None]]
10
+ DisconnectedCallback = Callable[[int | None], Awaitable[None]]
11
+ DisconnectRequestStatus = Literal["requested", "unavailable", "failed"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DisconnectRequestResult:
16
+ """Result of a best-effort remote HID disconnect request."""
17
+
18
+ status: DisconnectRequestStatus
19
+ channels: tuple[str, ...] = ()
20
+ reason: str | None = None
21
+ error_type: str | None = None
22
+ message: str | None = None
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class BondedPeer:
27
+ """Peer address discovered from a transport key store."""
28
+
29
+ address: str
30
+
31
+
32
+ class HidDeviceTransport(Protocol):
33
+ """Abstract HID device transport used by SwitchGamepad."""
34
+
35
+ async def open(self) -> None:
36
+ """Open transport resources without waiting for a host connection."""
37
+
38
+ async def start_advertising(self) -> None:
39
+ """Enter the host-discoverable state."""
40
+
41
+ async def close(self) -> None:
42
+ """Close transport resources."""
43
+
44
+ async def request_disconnect(self) -> DisconnectRequestResult:
45
+ """Request a remote HID/L2CAP disconnect when the transport supports it."""
46
+
47
+ async def list_bonded_peers(self) -> tuple[BondedPeer, ...]:
48
+ """Return current reconnect candidates.
49
+
50
+ Implementations must return zero or one peer. A transport that stores
51
+ multiple historical peers must expose only the current reconnect target
52
+ here. Multiple current peers are an invalid transport or key-store state
53
+ and should raise InvalidKeyStoreError rather than returning multiple
54
+ BondedPeer values.
55
+ """
56
+
57
+ async def connect_bonded_peer(
58
+ self,
59
+ peer_address: str,
60
+ *,
61
+ connect_timeout: float | None,
62
+ ) -> None:
63
+ """Start an active reconnect attempt to a bonded peer."""
64
+
65
+ async def send_interrupt(self, payload: bytes) -> None:
66
+ """Send one HID interrupt report."""
67
+
68
+ async def send_control(self, payload: bytes) -> None:
69
+ """Send one HID control report."""
70
+
71
+ def on_interrupt_data(self, callback: InterruptDataCallback) -> None:
72
+ """Register a callback for interrupt-channel host data."""
73
+
74
+ def on_control_data(self, callback: ControlDataCallback) -> None:
75
+ """Register a callback for control-channel host data."""
76
+
77
+ def on_connected(self, callback: ConnectedCallback) -> None:
78
+ """Register a callback for transport connection completion."""
79
+
80
+ def on_disconnected(self, callback: DisconnectedCallback) -> None:
81
+ """Register a callback for transport disconnection."""