bumble 0.0.219__py3-none-any.whl → 0.0.221__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.
Files changed (104) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -479
  4. bumble/apps/bench.py +4 -5
  5. bumble/apps/console.py +5 -10
  6. bumble/apps/controller_info.py +12 -7
  7. bumble/apps/controller_loopback.py +1 -2
  8. bumble/apps/device_info.py +2 -3
  9. bumble/apps/gatt_dump.py +0 -1
  10. bumble/apps/lea_unicast/app.py +1 -1
  11. bumble/apps/pair.py +49 -46
  12. bumble/apps/pandora_server.py +2 -2
  13. bumble/apps/player/player.py +10 -12
  14. bumble/apps/rfcomm_bridge.py +10 -11
  15. bumble/apps/scan.py +1 -3
  16. bumble/apps/speaker/speaker.py +3 -4
  17. bumble/at.py +4 -5
  18. bumble/att.py +91 -25
  19. bumble/audio/io.py +8 -6
  20. bumble/avc.py +1 -2
  21. bumble/avctp.py +2 -3
  22. bumble/avdtp.py +53 -57
  23. bumble/avrcp.py +25 -27
  24. bumble/codecs.py +15 -15
  25. bumble/colors.py +7 -8
  26. bumble/controller.py +1201 -643
  27. bumble/core.py +41 -49
  28. bumble/crypto/__init__.py +2 -1
  29. bumble/crypto/builtin.py +2 -8
  30. bumble/data_types.py +2 -1
  31. bumble/decoder.py +2 -3
  32. bumble/device.py +278 -325
  33. bumble/drivers/__init__.py +3 -2
  34. bumble/drivers/intel.py +6 -8
  35. bumble/drivers/rtk.py +1 -1
  36. bumble/gatt.py +9 -9
  37. bumble/gatt_adapters.py +6 -6
  38. bumble/gatt_client.py +110 -60
  39. bumble/gatt_server.py +209 -139
  40. bumble/hci.py +87 -74
  41. bumble/helpers.py +5 -5
  42. bumble/hfp.py +27 -26
  43. bumble/hid.py +9 -9
  44. bumble/host.py +44 -50
  45. bumble/keys.py +17 -17
  46. bumble/l2cap.py +1015 -218
  47. bumble/link.py +54 -284
  48. bumble/ll.py +200 -0
  49. bumble/lmp.py +324 -0
  50. bumble/pairing.py +14 -15
  51. bumble/pandora/__init__.py +2 -2
  52. bumble/pandora/device.py +6 -4
  53. bumble/pandora/host.py +19 -10
  54. bumble/pandora/l2cap.py +8 -9
  55. bumble/pandora/security.py +18 -16
  56. bumble/pandora/utils.py +4 -4
  57. bumble/profiles/aics.py +6 -8
  58. bumble/profiles/ams.py +3 -5
  59. bumble/profiles/ancs.py +11 -11
  60. bumble/profiles/ascs.py +5 -5
  61. bumble/profiles/asha.py +10 -9
  62. bumble/profiles/bass.py +9 -3
  63. bumble/profiles/battery_service.py +1 -2
  64. bumble/profiles/csip.py +9 -10
  65. bumble/profiles/device_information_service.py +16 -17
  66. bumble/profiles/gap.py +3 -4
  67. bumble/profiles/gatt_service.py +0 -1
  68. bumble/profiles/gmap.py +12 -13
  69. bumble/profiles/hap.py +3 -3
  70. bumble/profiles/heart_rate_service.py +7 -8
  71. bumble/profiles/le_audio.py +1 -1
  72. bumble/profiles/mcp.py +28 -28
  73. bumble/profiles/pacs.py +13 -17
  74. bumble/profiles/pbp.py +16 -0
  75. bumble/profiles/vcs.py +2 -2
  76. bumble/profiles/vocs.py +6 -9
  77. bumble/rfcomm.py +19 -18
  78. bumble/sdp.py +12 -11
  79. bumble/smp.py +20 -30
  80. bumble/snoop.py +12 -5
  81. bumble/tools/generate_company_id_list.py +1 -1
  82. bumble/tools/intel_util.py +2 -2
  83. bumble/tools/rtk_fw_download.py +1 -1
  84. bumble/tools/rtk_util.py +1 -1
  85. bumble/transport/__init__.py +1 -2
  86. bumble/transport/android_emulator.py +2 -3
  87. bumble/transport/android_netsim.py +49 -40
  88. bumble/transport/common.py +9 -9
  89. bumble/transport/file.py +1 -2
  90. bumble/transport/hci_socket.py +2 -3
  91. bumble/transport/pty.py +3 -5
  92. bumble/transport/pyusb.py +8 -5
  93. bumble/transport/serial.py +1 -2
  94. bumble/transport/vhci.py +1 -2
  95. bumble/transport/ws_server.py +2 -3
  96. bumble/utils.py +23 -14
  97. bumble/vendor/android/hci.py +4 -2
  98. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/METADATA +4 -3
  99. bumble-0.0.221.dist-info/RECORD +185 -0
  100. bumble-0.0.219.dist-info/RECORD +0 -183
  101. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
  102. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
  103. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
  104. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/lmp.py ADDED
@@ -0,0 +1,324 @@
1
+ # Copyright 2021-2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ from __future__ import annotations
19
+
20
+ import struct
21
+ from dataclasses import dataclass, field
22
+ from typing import TypeVar
23
+
24
+ from bumble import hci, utils
25
+
26
+
27
+ class Opcode(utils.OpenIntEnum):
28
+ '''
29
+ See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary.
30
+
31
+ Follow the alphabetical order defined there.
32
+ '''
33
+
34
+ # fmt: off
35
+ LMP_ACCEPTED = 3
36
+ LMP_ACCEPTED_EXT = 127 << 8 + 1
37
+ LMP_AU_RAND = 11
38
+ LMP_AUTO_RATE = 35
39
+ LMP_CHANNEL_CLASSIFICATION = 127 << 8 + 17
40
+ LMP_CHANNEL_CLASSIFICATION_REQ = 127 << 8 + 16
41
+ LMP_CLK_ADJ = 127 << 8 + 5
42
+ LMP_CLK_ADJ_ACK = 127 << 8 + 6
43
+ LMP_CLK_ADJ_REQ = 127 << 8 + 7
44
+ LMP_CLKOFFSET_REQ = 5
45
+ LMP_CLKOFFSET_RES = 6
46
+ LMP_COMB_KEY = 9
47
+ LMP_DECR_POWER_REQ = 32
48
+ LMP_DETACH = 7
49
+ LMP_DHKEY_CHECK = 65
50
+ LMP_ENCAPSULATED_HEADER = 61
51
+ LMP_ENCAPSULATED_PAYLOAD = 62
52
+ LMP_ENCRYPTION_KEY_SIZE_MASK_REQ= 58
53
+ LMP_ENCRYPTION_KEY_SIZE_MASK_RES= 59
54
+ LMP_ENCRYPTION_KEY_SIZE_REQ = 16
55
+ LMP_ENCRYPTION_MODE_REQ = 15
56
+ LMP_ESCO_LINK_REQ = 127 << 8 + 12
57
+ LMP_FEATURES_REQ = 39
58
+ LMP_FEATURES_REQ_EXT = 127 << 8 + 3
59
+ LMP_FEATURES_RES = 40
60
+ LMP_FEATURES_RES_EXT = 127 << 8 + 4
61
+ LMP_HOLD = 20
62
+ LMP_HOLD_REQ = 21
63
+ LMP_HOST_CONNECTION_REQ = 51
64
+ LMP_IN_RAND = 8
65
+ LMP_INCR_POWER_REQ = 31
66
+ LMP_IO_CAPABILITY_REQ = 127 << 8 + 25
67
+ LMP_IO_CAPABILITY_RES = 127 << 8 + 26
68
+ LMP_KEYPRESS_NOTIFICATION = 127 << 8 + 30
69
+ LMP_MAX_POWER = 33
70
+ LMP_MAX_SLOT = 45
71
+ LMP_MAX_SLOT_REQ = 46
72
+ LMP_MIN_POWER = 34
73
+ LMP_NAME_REQ = 1
74
+ LMP_NAME_RES = 2
75
+ LMP_NOT_ACCEPTED = 4
76
+ LMP_NOT_ACCEPTED_EXT = 127 << 8 + 2
77
+ LMP_NUMERIC_COMPARISON_FAILED = 127 << 8 + 27
78
+ LMP_OOB_FAILED = 127 << 8 + 29
79
+ LMP_PACKET_TYPE_TABLE_REQ = 127 << 8 + 11
80
+ LMP_PAGE_MODE_REQ = 53
81
+ LMP_PAGE_SCAN_MODE_REQ = 54
82
+ LMP_PASSKEY_FAILED = 127 << 8 + 28
83
+ LMP_PAUSE_ENCRYPTION_AES_REQ = 66
84
+ LMP_PAUSE_ENCRYPTION_REQ = 127 << 8 + 23
85
+ LMP_PING_REQ = 127 << 8 + 33
86
+ LMP_PING_RES = 127 << 8 + 34
87
+ LMP_POWER_CONTROL_REQ = 127 << 8 + 31
88
+ LMP_POWER_CONTROL_RES = 127 << 8 + 32
89
+ LMP_PREFERRED_RATE = 36
90
+ LMP_QUALITY_OF_SERVICE = 41
91
+ LMP_QUALITY_OF_SERVICE_REQ = 42
92
+ LMP_REMOVE_ESCO_LINK_REQ = 127 << 8 + 13
93
+ LMP_REMOVE_SCO_LINK_REQ = 44
94
+ LMP_RESUME_ENCRYPTION_REQ = 127 << 8 + 24
95
+ LMP_SAM_DEFINE_MAP = 127 << 8 + 36
96
+ LMP_SAM_SET_TYPE0 = 127 << 8 + 35
97
+ LMP_SAM_SWITCH = 127 << 8 + 37
98
+ LMP_SCO_LINK_REQ = 43
99
+ LMP_SET_AFH = 60
100
+ LMP_SETUP_COMPLETE = 49
101
+ LMP_SIMPLE_PAIRING_CONFIRM = 63
102
+ LMP_SIMPLE_PAIRING_NUMBER = 64
103
+ LMP_SLOT_OFFSET = 52
104
+ LMP_SNIFF_REQ = 23
105
+ LMP_SNIFF_SUBRATING_REQ = 127 << 8 + 21
106
+ LMP_SNIFF_SUBRATING_RES = 127 << 8 + 22
107
+ LMP_SRES = 12
108
+ LMP_START_ENCRYPTION_REQ = 17
109
+ LMP_STOP_ENCRYPTION_REQ = 18
110
+ LMP_SUPERVISION_TIMEOUT = 55
111
+ LMP_SWITCH_REQ = 19
112
+ LMP_TEMP_KEY = 14
113
+ LMP_TEMP_RAND = 13
114
+ LMP_TEST_ACTIVATE = 56
115
+ LMP_TEST_CONTROL = 57
116
+ LMP_TIMING_ACCURACY_REQ = 47
117
+ LMP_TIMING_ACCURACY_RES = 48
118
+ LMP_UNIT_KEY = 10
119
+ LMP_UNSNIFF_REQ = 24
120
+ LMP_USE_SEMI_PERMANENT_KEY = 50
121
+ LMP_VERSION_REQ = 37
122
+ LMP_VERSION_RES = 38
123
+ # fmt: on
124
+
125
+ @classmethod
126
+ def parse_from(cls, data: bytes, offset: int = 0) -> tuple[int, Opcode]:
127
+ opcode = data[offset]
128
+ if opcode in (124, 127):
129
+ opcode = struct.unpack('>H', data)[0]
130
+ return offset + 2, Opcode(opcode)
131
+ return offset + 1, Opcode(opcode)
132
+
133
+ def __bytes__(self) -> bytes:
134
+ if self.value >> 8:
135
+ return struct.pack('>H', self.value)
136
+ return bytes([self.value])
137
+
138
+ @classmethod
139
+ def type_metadata(cls):
140
+ return hci.metadata(
141
+ {
142
+ 'serializer': bytes,
143
+ 'parser': lambda data, offset: (Opcode.parse_from(data, offset)),
144
+ }
145
+ )
146
+
147
+
148
+ class Packet:
149
+ '''
150
+ See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary
151
+ '''
152
+
153
+ subclasses: dict[int, type[Packet]] = {}
154
+ opcode: Opcode
155
+ fields: hci.Fields = ()
156
+ _payload: bytes = b''
157
+
158
+ _Packet = TypeVar("_Packet", bound="Packet")
159
+
160
+ @classmethod
161
+ def subclass(cls, subclass: type[_Packet]) -> type[_Packet]:
162
+ # Register a factory for this class
163
+ cls.subclasses[subclass.opcode] = subclass
164
+ subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
165
+
166
+ return subclass
167
+
168
+ @classmethod
169
+ def from_bytes(cls, data: bytes) -> Packet:
170
+ offset, opcode = Opcode.parse_from(data)
171
+ if not (subclass := cls.subclasses.get(opcode)):
172
+ instance = Packet()
173
+ instance.opcode = opcode
174
+ else:
175
+ instance = subclass(
176
+ **hci.HCI_Object.dict_from_bytes(data, offset, subclass.fields)
177
+ )
178
+ instance.payload = data[offset:]
179
+ return instance
180
+
181
+ @property
182
+ def payload(self) -> bytes:
183
+ if self._payload is None:
184
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
185
+ return self._payload
186
+
187
+ @payload.setter
188
+ def payload(self, value: bytes) -> None:
189
+ self._payload = value
190
+
191
+ def __bytes__(self) -> bytes:
192
+ return bytes(self.opcode) + self.payload
193
+
194
+
195
+ @Packet.subclass
196
+ @dataclass
197
+ class LmpAccepted(Packet):
198
+ opcode = Opcode.LMP_ACCEPTED
199
+
200
+ response_opcode: Opcode = field(metadata=Opcode.type_metadata())
201
+
202
+
203
+ @Packet.subclass
204
+ @dataclass
205
+ class LmpNotAccepted(Packet):
206
+ opcode = Opcode.LMP_NOT_ACCEPTED
207
+
208
+ response_opcode: Opcode = field(metadata=Opcode.type_metadata())
209
+ error_code: int = field(metadata=hci.metadata(1))
210
+
211
+
212
+ @Packet.subclass
213
+ @dataclass
214
+ class LmpAcceptedExt(Packet):
215
+ opcode = Opcode.LMP_ACCEPTED_EXT
216
+
217
+ response_opcode: Opcode = field(metadata=Opcode.type_metadata())
218
+
219
+
220
+ @Packet.subclass
221
+ @dataclass
222
+ class LmpNotAcceptedExt(Packet):
223
+ opcode = Opcode.LMP_NOT_ACCEPTED_EXT
224
+
225
+ response_opcode: Opcode = field(metadata=Opcode.type_metadata())
226
+ error_code: int = field(metadata=hci.metadata(1))
227
+
228
+
229
+ @Packet.subclass
230
+ @dataclass
231
+ class LmpAuRand(Packet):
232
+ opcode = Opcode.LMP_AU_RAND
233
+
234
+ random_number: bytes = field(metadata=hci.metadata(16))
235
+
236
+
237
+ @Packet.subclass
238
+ @dataclass
239
+ class LmpDetach(Packet):
240
+ opcode = Opcode.LMP_DETACH
241
+
242
+ error_code: int = field(metadata=hci.metadata(1))
243
+
244
+
245
+ @Packet.subclass
246
+ @dataclass
247
+ class LmpEscoLinkReq(Packet):
248
+ opcode = Opcode.LMP_ESCO_LINK_REQ
249
+
250
+ esco_handle: int = field(metadata=hci.metadata(1))
251
+ esco_lt_addr: int = field(metadata=hci.metadata(1))
252
+ timing_control_flags: int = field(metadata=hci.metadata(1))
253
+ d_esco: int = field(metadata=hci.metadata(1))
254
+ t_esco: int = field(metadata=hci.metadata(1))
255
+ w_esco: int = field(metadata=hci.metadata(1))
256
+ esco_packet_type_c_to_p: int = field(metadata=hci.metadata(1))
257
+ esco_packet_type_p_to_c: int = field(metadata=hci.metadata(1))
258
+ packet_length_c_to_p: int = field(metadata=hci.metadata(2))
259
+ packet_length_p_to_c: int = field(metadata=hci.metadata(2))
260
+ air_mode: int = field(metadata=hci.metadata(1))
261
+ negotiation_state: int = field(metadata=hci.metadata(1))
262
+
263
+
264
+ @Packet.subclass
265
+ @dataclass
266
+ class LmpHostConnectionReq(Packet):
267
+ opcode = Opcode.LMP_HOST_CONNECTION_REQ
268
+
269
+
270
+ @Packet.subclass
271
+ @dataclass
272
+ class LmpRemoveEscoLinkReq(Packet):
273
+ opcode = Opcode.LMP_REMOVE_ESCO_LINK_REQ
274
+
275
+ esco_handle: int = field(metadata=hci.metadata(1))
276
+ error_code: int = field(metadata=hci.metadata(1))
277
+
278
+
279
+ @Packet.subclass
280
+ @dataclass
281
+ class LmpRemoveScoLinkReq(Packet):
282
+ opcode = Opcode.LMP_REMOVE_SCO_LINK_REQ
283
+
284
+ sco_handle: int = field(metadata=hci.metadata(1))
285
+ error_code: int = field(metadata=hci.metadata(1))
286
+
287
+
288
+ @Packet.subclass
289
+ @dataclass
290
+ class LmpScoLinkReq(Packet):
291
+ opcode = Opcode.LMP_SCO_LINK_REQ
292
+
293
+ sco_handle: int = field(metadata=hci.metadata(1))
294
+ timing_control_flags: int = field(metadata=hci.metadata(1))
295
+ d_sco: int = field(metadata=hci.metadata(1))
296
+ t_sco: int = field(metadata=hci.metadata(1))
297
+ sco_packet: int = field(metadata=hci.metadata(1))
298
+ air_mode: int = field(metadata=hci.metadata(1))
299
+
300
+
301
+ @Packet.subclass
302
+ @dataclass
303
+ class LmpSwitchReq(Packet):
304
+ opcode = Opcode.LMP_SWITCH_REQ
305
+
306
+ switch_instant: int = field(metadata=hci.metadata(4), default=0)
307
+
308
+
309
+ @Packet.subclass
310
+ @dataclass
311
+ class LmpNameReq(Packet):
312
+ opcode = Opcode.LMP_NAME_REQ
313
+
314
+ name_offset: int = field(metadata=hci.metadata(2))
315
+
316
+
317
+ @Packet.subclass
318
+ @dataclass
319
+ class LmpNameRes(Packet):
320
+ opcode = Opcode.LMP_NAME_RES
321
+
322
+ name_offset: int = field(metadata=hci.metadata(2))
323
+ name_length: int = field(metadata=hci.metadata(3))
324
+ name_fregment: bytes = field(metadata=hci.metadata('*'))
bumble/pairing.py CHANGED
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
  import enum
21
21
  import secrets
22
22
  from dataclasses import dataclass
23
- from typing import Optional
24
23
 
25
24
  from bumble import hci
26
25
  from bumble.core import AdvertisingData, LeRole
@@ -45,16 +44,16 @@ from bumble.smp import (
45
44
  class OobData:
46
45
  """OOB data that can be sent from one device to another."""
47
46
 
48
- address: Optional[hci.Address] = None
49
- role: Optional[LeRole] = None
50
- shared_data: Optional[OobSharedData] = None
51
- legacy_context: Optional[OobLegacyContext] = None
47
+ address: hci.Address | None = None
48
+ role: LeRole | None = None
49
+ shared_data: OobSharedData | None = None
50
+ legacy_context: OobLegacyContext | None = None
52
51
 
53
52
  @classmethod
54
53
  def from_ad(cls, ad: AdvertisingData) -> OobData:
55
54
  instance = cls()
56
- shared_data_c: Optional[bytes] = None
57
- shared_data_r: Optional[bytes] = None
55
+ shared_data_c: bytes | None = None
56
+ shared_data_r: bytes | None = None
58
57
  for ad_type, ad_data in ad.ad_structures:
59
58
  if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
60
59
  instance.address = hci.Address(ad_data)
@@ -181,14 +180,14 @@ class PairingDelegate:
181
180
  """Compare two numbers."""
182
181
  return True
183
182
 
184
- async def get_number(self) -> Optional[int]:
183
+ async def get_number(self) -> int | None:
185
184
  """
186
185
  Return an optional number as an answer to a passkey request.
187
186
  Returning `None` will result in a negative reply.
188
187
  """
189
188
  return 0
190
189
 
191
- async def get_string(self, max_length: int) -> Optional[str]:
190
+ async def get_string(self, max_length: int) -> str | None:
192
191
  """
193
192
  Return a string whose utf-8 encoding is up to max_length bytes.
194
193
  """
@@ -239,18 +238,18 @@ class PairingConfig:
239
238
  class OobConfig:
240
239
  """Config for OOB pairing."""
241
240
 
242
- our_context: Optional[OobContext]
243
- peer_data: Optional[OobSharedData]
244
- legacy_context: Optional[OobLegacyContext]
241
+ our_context: OobContext | None
242
+ peer_data: OobSharedData | None
243
+ legacy_context: OobLegacyContext | None
245
244
 
246
245
  def __init__(
247
246
  self,
248
247
  sc: bool = True,
249
248
  mitm: bool = True,
250
249
  bonding: bool = True,
251
- delegate: Optional[PairingDelegate] = None,
252
- identity_address_type: Optional[AddressType] = None,
253
- oob: Optional[OobConfig] = None,
250
+ delegate: PairingDelegate | None = None,
251
+ identity_address_type: AddressType | None = None,
252
+ oob: OobConfig | None = None,
254
253
  ) -> None:
255
254
  self.sc = sc
256
255
  self.mitm = mitm
@@ -19,7 +19,7 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
19
19
 
20
20
  __version__ = "0.0.1"
21
21
 
22
- from typing import Callable, List, Optional
22
+ from collections.abc import Callable
23
23
 
24
24
  import grpc
25
25
  import grpc.aio
@@ -58,7 +58,7 @@ def register_servicer_hook(
58
58
  async def serve(
59
59
  bumble: PandoraDevice,
60
60
  config: Config = Config(),
61
- grpc_server: Optional[grpc.aio.Server] = None,
61
+ grpc_server: grpc.aio.Server | None = None,
62
62
  port: int = 0,
63
63
  ) -> None:
64
64
  # initialize a gRPC server if not provided.
bumble/pandora/device.py CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- from typing import Any, Optional
19
+ from typing import Any
20
20
 
21
21
  from bumble import transport
22
22
  from bumble.core import (
@@ -54,7 +54,7 @@ class PandoraDevice:
54
54
 
55
55
  # HCI transport name & instance.
56
56
  _hci_name: str
57
- _hci: Optional[transport.Transport] # type: ignore[name-defined]
57
+ _hci: transport.Transport | None # type: ignore[name-defined]
58
58
 
59
59
  def __init__(self, config: dict[str, Any]) -> None:
60
60
  self.config = config
@@ -74,7 +74,9 @@ class PandoraDevice:
74
74
 
75
75
  # open HCI transport & set device host.
76
76
  self._hci = await transport.open_transport(self._hci_name)
77
- self.device.host = Host(controller_source=self._hci.source, controller_sink=self._hci.sink) # type: ignore[no-untyped-call]
77
+ self.device.host = Host(
78
+ controller_source=self._hci.source, controller_sink=self._hci.sink
79
+ ) # type: ignore[no-untyped-call]
78
80
 
79
81
  # power-on.
80
82
  await self.device.power_on()
@@ -96,7 +98,7 @@ class PandoraDevice:
96
98
  await self.close()
97
99
  await self.open()
98
100
 
99
- def info(self) -> Optional[dict[str, str]]:
101
+ def info(self) -> dict[str, str] | None:
100
102
  return {
101
103
  'public_bd_address': str(self.device.public_address),
102
104
  'random_address': str(self.device.random_address),
bumble/pandora/host.py CHANGED
@@ -17,12 +17,15 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import logging
19
19
  import struct
20
- from typing import AsyncGenerator, Optional, cast
20
+ from collections.abc import AsyncGenerator
21
+ from typing import cast
21
22
 
22
23
  import grpc
23
24
  import grpc.aio
24
- from google.protobuf import any_pb2 # pytype: disable=pyi-error
25
- from google.protobuf import empty_pb2 # pytype: disable=pyi-error
25
+ from google.protobuf import (
26
+ any_pb2, # pytype: disable=pyi-error
27
+ empty_pb2, # pytype: disable=pyi-error
28
+ )
26
29
  from pandora import host_pb2
27
30
  from pandora.host_grpc_aio import HostServicer
28
31
  from pandora.host_pb2 import (
@@ -302,7 +305,9 @@ class HostService(HostServicer):
302
305
  await disconnection_future
303
306
  self.log.debug("Disconnected")
304
307
  finally:
305
- connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
308
+ connection.remove_listener(
309
+ connection.EVENT_DISCONNECTION, on_disconnection
310
+ ) # type: ignore
306
311
 
307
312
  return empty_pb2.Empty()
308
313
 
@@ -539,7 +544,7 @@ class HostService(HostServicer):
539
544
  await bumble.utils.cancel_on_event(
540
545
  self.device, 'flush', self.device.stop_advertising()
541
546
  )
542
- except:
547
+ except Exception:
543
548
  pass
544
549
 
545
550
  @utils.rpc
@@ -609,7 +614,7 @@ class HostService(HostServicer):
609
614
  await bumble.utils.cancel_on_event(
610
615
  self.device, 'flush', self.device.stop_scanning()
611
616
  )
612
- except:
617
+ except Exception:
613
618
  pass
614
619
 
615
620
  @utils.rpc
@@ -619,7 +624,7 @@ class HostService(HostServicer):
619
624
  self.log.debug('Inquiry')
620
625
 
621
626
  inquiry_queue: asyncio.Queue[
622
- Optional[tuple[Address, int, AdvertisingData, int]]
627
+ tuple[Address, int, AdvertisingData, int] | None
623
628
  ] = asyncio.Queue()
624
629
  complete_handler = self.device.on(
625
630
  self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
@@ -644,14 +649,18 @@ class HostService(HostServicer):
644
649
  )
645
650
 
646
651
  finally:
647
- self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
648
- self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
652
+ self.device.remove_listener(
653
+ self.device.EVENT_INQUIRY_COMPLETE, complete_handler
654
+ ) # type: ignore
655
+ self.device.remove_listener(
656
+ self.device.EVENT_INQUIRY_RESULT, result_handler
657
+ ) # type: ignore
649
658
  try:
650
659
  self.log.debug('Stop inquiry')
651
660
  await bumble.utils.cancel_on_event(
652
661
  self.device, 'flush', self.device.stop_discovery()
653
662
  )
654
- except:
663
+ except Exception:
655
664
  pass
656
665
 
657
666
  @utils.rpc
bumble/pandora/l2cap.py CHANGED
@@ -18,15 +18,15 @@ import json
18
18
  import logging
19
19
  from asyncio import Future
20
20
  from asyncio import Queue as AsyncQueue
21
+ from collections.abc import AsyncGenerator
21
22
  from dataclasses import dataclass
22
- from typing import AsyncGenerator, Optional, Union
23
23
 
24
24
  import grpc
25
25
  from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
26
26
  from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
27
- from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
28
- from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
29
27
  from pandora.l2cap_pb2 import (
28
+ COMMAND_NOT_UNDERSTOOD,
29
+ INVALID_CID_IN_REQUEST,
30
30
  ConnectRequest,
31
31
  ConnectResponse,
32
32
  CreditBasedChannelRequest,
@@ -41,6 +41,7 @@ from pandora.l2cap_pb2 import (
41
41
  WaitDisconnectionRequest,
42
42
  WaitDisconnectionResponse,
43
43
  )
44
+ from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
44
45
 
45
46
  from bumble.core import InvalidArgumentError, OutOfResourcesError
46
47
  from bumble.device import Device
@@ -55,7 +56,7 @@ from bumble.l2cap import (
55
56
  from bumble.pandora import utils
56
57
  from bumble.pandora.config import Config
57
58
 
58
- L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
59
+ L2capChannel = ClassicChannel | LeCreditBasedChannel
59
60
 
60
61
 
61
62
  @dataclass
@@ -106,10 +107,8 @@ class L2CAPService(L2CAPServicer):
106
107
  oneof = request.WhichOneof('type')
107
108
  self.log.debug(f'WaitConnection channel request type: {oneof}.')
108
109
  channel_type = getattr(request, oneof)
109
- spec: Optional[Union[ClassicChannelSpec, LeCreditBasedChannelSpec]] = None
110
- l2cap_server: Optional[
111
- Union[ClassicChannelServer, LeCreditBasedChannelServer]
112
- ] = None
110
+ spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
111
+ l2cap_server: ClassicChannelServer | LeCreditBasedChannelServer | None = None
113
112
  if isinstance(channel_type, CreditBasedChannelRequest):
114
113
  spec = LeCreditBasedChannelSpec(
115
114
  psm=channel_type.spsm,
@@ -216,7 +215,7 @@ class L2CAPService(L2CAPServicer):
216
215
  oneof = request.WhichOneof('type')
217
216
  self.log.debug(f'Channel request type: {oneof}.')
218
217
  channel_type = getattr(request, oneof)
219
- spec: Optional[Union[ClassicChannelSpec, LeCreditBasedChannelSpec]] = None
218
+ spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
220
219
  if isinstance(channel_type, CreditBasedChannelRequest):
221
220
  spec = LeCreditBasedChannelSpec(
222
221
  psm=channel_type.spsm,
@@ -17,13 +17,15 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import contextlib
19
19
  import logging
20
- from collections.abc import Awaitable
21
- from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
20
+ from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
21
+ from typing import Any
22
22
 
23
23
  import grpc
24
- from google.protobuf import any_pb2 # pytype: disable=pyi-error
25
- from google.protobuf import empty_pb2 # pytype: disable=pyi-error
26
- from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
24
+ from google.protobuf import (
25
+ any_pb2, # pytype: disable=pyi-error
26
+ empty_pb2, # pytype: disable=pyi-error
27
+ wrappers_pb2, # pytype: disable=pyi-error
28
+ )
27
29
  from pandora.host_pb2 import Connection
28
30
  from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer
29
31
  from pandora.security_pb2 import (
@@ -64,7 +66,7 @@ class PairingDelegate(BasePairingDelegate):
64
66
  def __init__(
65
67
  self,
66
68
  connection: BumbleConnection,
67
- service: "SecurityService",
69
+ service: SecurityService,
68
70
  io_capability: BasePairingDelegate.IoCapability = BasePairingDelegate.NO_OUTPUT_NO_INPUT,
69
71
  local_initiator_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
70
72
  local_responder_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
@@ -130,7 +132,7 @@ class PairingDelegate(BasePairingDelegate):
130
132
  assert answer.answer_variant() == 'confirm' and answer.confirm is not None
131
133
  return answer.confirm
132
134
 
133
- async def get_number(self) -> Optional[int]:
135
+ async def get_number(self) -> int | None:
134
136
  self.log.debug(
135
137
  f"Pairing event: `passkey_entry_request` (io_capability: {self.io_capability})"
136
138
  )
@@ -147,7 +149,7 @@ class PairingDelegate(BasePairingDelegate):
147
149
  assert answer.answer_variant() == 'passkey'
148
150
  return answer.passkey
149
151
 
150
- async def get_string(self, max_length: int) -> Optional[str]:
152
+ async def get_string(self, max_length: int) -> str | None:
151
153
  self.log.debug(
152
154
  f"Pairing event: `pin_code_request` (io_capability: {self.io_capability})"
153
155
  )
@@ -195,8 +197,8 @@ class SecurityService(SecurityServicer):
195
197
  self.log = utils.BumbleServerLoggerAdapter(
196
198
  logging.getLogger(), {'service_name': 'Security', 'device': device}
197
199
  )
198
- self.event_queue: Optional[asyncio.Queue[PairingEvent]] = None
199
- self.event_answer: Optional[AsyncIterator[PairingEventAnswer]] = None
200
+ self.event_queue: asyncio.Queue[PairingEvent] | None = None
201
+ self.event_answer: AsyncIterator[PairingEventAnswer] | None = None
200
202
  self.device = device
201
203
  self.config = config
202
204
 
@@ -231,7 +233,7 @@ class SecurityService(SecurityServicer):
231
233
  if level == LEVEL2:
232
234
  return connection.encryption != 0 and connection.authenticated
233
235
 
234
- link_key_type: Optional[int] = None
236
+ link_key_type: int | None = None
235
237
  if (keystore := connection.device.keystore) and (
236
238
  keys := await keystore.get(str(connection.peer_address))
237
239
  ):
@@ -410,8 +412,8 @@ class SecurityService(SecurityServicer):
410
412
  wait_for_security: asyncio.Future[str] = (
411
413
  asyncio.get_running_loop().create_future()
412
414
  )
413
- authenticate_task: Optional[asyncio.Future[None]] = None
414
- pair_task: Optional[asyncio.Future[None]] = None
415
+ authenticate_task: asyncio.Future[None] | None = None
416
+ pair_task: asyncio.Future[None] | None = None
415
417
 
416
418
  async def authenticate() -> None:
417
419
  if (encryption := connection.encryption) != 0:
@@ -455,9 +457,9 @@ class SecurityService(SecurityServicer):
455
457
 
456
458
  def pair(*_: Any) -> None:
457
459
  if self.need_pairing(connection, level):
458
- pair_task = asyncio.create_task(connection.pair())
460
+ bumble.utils.AsyncRunner.spawn(connection.pair())
459
461
 
460
- listeners: dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
462
+ listeners: dict[str, Callable[..., None | Awaitable[None]]] = {
461
463
  'disconnection': set_failure('connection_died'),
462
464
  'pairing_failure': set_failure('pairing_failure'),
463
465
  'connection_authentication_failure': set_failure('authentication_failure'),
@@ -500,7 +502,7 @@ class SecurityService(SecurityServicer):
500
502
  return WaitSecurityResponse(**kwargs)
501
503
 
502
504
  async def reached_security_level(
503
- self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
505
+ self, connection: BumbleConnection, level: SecurityLevel | LESecurityLevel
504
506
  ) -> bool:
505
507
  self.log.debug(
506
508
  str(