bumble 0.0.220__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 (102) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -473
  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 +5 -3
  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 +663 -391
  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 +171 -142
  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 +26 -159
  48. bumble/ll.py +200 -0
  49. bumble/pairing.py +14 -15
  50. bumble/pandora/__init__.py +2 -2
  51. bumble/pandora/device.py +6 -4
  52. bumble/pandora/host.py +19 -10
  53. bumble/pandora/l2cap.py +8 -9
  54. bumble/pandora/security.py +18 -16
  55. bumble/pandora/utils.py +4 -4
  56. bumble/profiles/aics.py +6 -8
  57. bumble/profiles/ams.py +3 -5
  58. bumble/profiles/ancs.py +11 -11
  59. bumble/profiles/ascs.py +5 -5
  60. bumble/profiles/asha.py +10 -9
  61. bumble/profiles/bass.py +9 -3
  62. bumble/profiles/battery_service.py +1 -2
  63. bumble/profiles/csip.py +9 -10
  64. bumble/profiles/device_information_service.py +16 -17
  65. bumble/profiles/gap.py +3 -4
  66. bumble/profiles/gatt_service.py +0 -1
  67. bumble/profiles/gmap.py +12 -13
  68. bumble/profiles/hap.py +3 -3
  69. bumble/profiles/heart_rate_service.py +7 -8
  70. bumble/profiles/le_audio.py +1 -1
  71. bumble/profiles/mcp.py +28 -28
  72. bumble/profiles/pacs.py +13 -17
  73. bumble/profiles/pbp.py +16 -0
  74. bumble/profiles/vcs.py +2 -2
  75. bumble/profiles/vocs.py +6 -9
  76. bumble/rfcomm.py +19 -18
  77. bumble/sdp.py +12 -11
  78. bumble/smp.py +20 -30
  79. bumble/snoop.py +2 -1
  80. bumble/tools/generate_company_id_list.py +1 -1
  81. bumble/tools/intel_util.py +2 -2
  82. bumble/tools/rtk_fw_download.py +1 -1
  83. bumble/tools/rtk_util.py +1 -1
  84. bumble/transport/__init__.py +1 -2
  85. bumble/transport/android_emulator.py +2 -3
  86. bumble/transport/android_netsim.py +49 -40
  87. bumble/transport/common.py +9 -9
  88. bumble/transport/file.py +1 -2
  89. bumble/transport/hci_socket.py +2 -3
  90. bumble/transport/pty.py +3 -5
  91. bumble/transport/pyusb.py +8 -5
  92. bumble/transport/serial.py +1 -2
  93. bumble/transport/vhci.py +1 -2
  94. bumble/transport/ws_server.py +2 -3
  95. bumble/utils.py +22 -9
  96. bumble/vendor/android/hci.py +4 -2
  97. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/METADATA +3 -2
  98. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/RECORD +102 -101
  99. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
  100. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
  101. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
  102. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/smp.py CHANGED
@@ -27,17 +27,9 @@ from __future__ import annotations
27
27
  import asyncio
28
28
  import enum
29
29
  import logging
30
+ from collections.abc import Awaitable, Callable
30
31
  from dataclasses import dataclass, field
31
- from typing import (
32
- TYPE_CHECKING,
33
- Any,
34
- Awaitable,
35
- Callable,
36
- ClassVar,
37
- Optional,
38
- TypeVar,
39
- cast,
40
- )
32
+ from typing import TYPE_CHECKING, ClassVar, TypeVar, cast
41
33
 
42
34
  from bumble import crypto, utils
43
35
  from bumble.colors import color
@@ -213,10 +205,10 @@ class SMP_Command:
213
205
  fields: ClassVar[Fields]
214
206
  code: int = field(default=0, init=False)
215
207
  name: str = field(default='', init=False)
216
- _payload: Optional[bytes] = field(default=None, init=False)
208
+ _payload: bytes | None = field(default=None, init=False)
217
209
 
218
210
  @classmethod
219
- def from_bytes(cls, pdu: bytes) -> "SMP_Command":
211
+ def from_bytes(cls, pdu: bytes) -> SMP_Command:
220
212
  code = pdu[0]
221
213
 
222
214
  subclass = SMP_Command.smp_classes.get(code)
@@ -554,7 +546,7 @@ class OobContext:
554
546
  r: bytes
555
547
 
556
548
  def __init__(
557
- self, ecc_key: Optional[crypto.EccKey] = None, r: Optional[bytes] = None
549
+ self, ecc_key: crypto.EccKey | None = None, r: bytes | None = None
558
550
  ) -> None:
559
551
  self.ecc_key = crypto.EccKey.generate() if ecc_key is None else ecc_key
560
552
  self.r = crypto.r() if r is None else r
@@ -570,7 +562,7 @@ class OobLegacyContext:
570
562
 
571
563
  tk: bytes
572
564
 
573
- def __init__(self, tk: Optional[bytes] = None) -> None:
565
+ def __init__(self, tk: bytes | None = None) -> None:
574
566
  self.tk = crypto.r() if tk is None else tk
575
567
 
576
568
 
@@ -677,31 +669,31 @@ class Session:
677
669
  self.stk = None
678
670
  self.ltk_ediv = 0
679
671
  self.ltk_rand = bytes(8)
680
- self.link_key: Optional[bytes] = None
672
+ self.link_key: bytes | None = None
681
673
  self.maximum_encryption_key_size: int = 0
682
674
  self.initiator_key_distribution: int = 0
683
675
  self.responder_key_distribution: int = 0
684
- self.peer_random_value: Optional[bytes] = None
676
+ self.peer_random_value: bytes | None = None
685
677
  self.peer_public_key_x: bytes = bytes(32)
686
678
  self.peer_public_key_y = bytes(32)
687
679
  self.peer_ltk = None
688
680
  self.peer_ediv = None
689
- self.peer_rand: Optional[bytes] = None
681
+ self.peer_rand: bytes | None = None
690
682
  self.peer_identity_resolving_key = None
691
- self.peer_bd_addr: Optional[Address] = None
683
+ self.peer_bd_addr: Address | None = None
692
684
  self.peer_signature_key = None
693
685
  self.peer_expected_distributions: list[type[SMP_Command]] = []
694
686
  self.dh_key = b''
695
687
  self.confirm_value = None
696
- self.passkey: Optional[int] = None
688
+ self.passkey: int | None = None
697
689
  self.passkey_ready = asyncio.Event()
698
690
  self.passkey_step = 0
699
691
  self.passkey_display = False
700
692
  self.pairing_method: PairingMethod = PairingMethod.JUST_WORKS
701
693
  self.pairing_config = pairing_config
702
- self.wait_before_continuing: Optional[asyncio.Future[None]] = None
694
+ self.wait_before_continuing: asyncio.Future[None] | None = None
703
695
  self.completed = False
704
- self.ctkd_task: Optional[Awaitable[None]] = None
696
+ self.ctkd_task: Awaitable[None] | None = None
705
697
 
706
698
  # Decide if we're the initiator or the responder
707
699
  self.is_initiator = is_initiator
@@ -720,7 +712,7 @@ class Session:
720
712
 
721
713
  # Create a future that can be used to wait for the session to complete
722
714
  if self.is_initiator:
723
- self.pairing_result: Optional[asyncio.Future[None]] = (
715
+ self.pairing_result: asyncio.Future[None] | None = (
724
716
  asyncio.get_running_loop().create_future()
725
717
  )
726
718
  else:
@@ -828,7 +820,7 @@ class Session:
828
820
  def auth_req(self) -> int:
829
821
  return smp_auth_req(self.bonding, self.mitm, self.sc, self.keypress, self.ct2)
830
822
 
831
- def get_long_term_key(self, rand: bytes, ediv: int) -> Optional[bytes]:
823
+ def get_long_term_key(self, rand: bytes, ediv: int) -> bytes | None:
832
824
  if not self.sc and not self.completed:
833
825
  if rand == self.ltk_rand and ediv == self.ltk_ediv:
834
826
  return self.stk
@@ -939,7 +931,7 @@ class Session:
939
931
  self.pairing_config.delegate.display_number(self.passkey, digits=6)
940
932
  )
941
933
 
942
- def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
934
+ def input_passkey(self, next_steps: Callable[[], None] | None = None) -> None:
943
935
  # Prompt the user for the passkey displayed on the peer
944
936
  def after_input(passkey: int) -> None:
945
937
  self.passkey = passkey
@@ -956,7 +948,7 @@ class Session:
956
948
  self.prompt_user_for_number(after_input)
957
949
 
958
950
  def display_or_input_passkey(
959
- self, next_steps: Optional[Callable[[], None]] = None
951
+ self, next_steps: Callable[[], None] | None = None
960
952
  ) -> None:
961
953
  if self.passkey_display:
962
954
 
@@ -1006,7 +998,6 @@ class Session:
1006
998
  self.send_command(response)
1007
999
 
1008
1000
  def send_pairing_confirm_command(self) -> None:
1009
-
1010
1001
  if self.pairing_method != PairingMethod.OOB:
1011
1002
  self.r = crypto.r()
1012
1003
  logger.debug(f'generated random: {self.r.hex()}')
@@ -1842,7 +1833,6 @@ class Session:
1842
1833
  self.send_public_key_command()
1843
1834
 
1844
1835
  def next_steps() -> None:
1845
-
1846
1836
  if self.pairing_method in (
1847
1837
  PairingMethod.JUST_WORKS,
1848
1838
  PairingMethod.NUMERIC_COMPARISON,
@@ -1929,7 +1919,7 @@ class Manager(utils.EventEmitter):
1929
1919
  sessions: dict[int, Session]
1930
1920
  pairing_config_factory: Callable[[Connection], PairingConfig]
1931
1921
  session_proxy: type[Session]
1932
- _ecc_key: Optional[crypto.EccKey]
1922
+ _ecc_key: crypto.EccKey | None
1933
1923
 
1934
1924
  def __init__(
1935
1925
  self,
@@ -2022,7 +2012,7 @@ class Manager(utils.EventEmitter):
2022
2012
  self.device.on_pairing_start(session.connection)
2023
2013
 
2024
2014
  async def on_pairing(
2025
- self, session: Session, identity_address: Optional[Address], keys: PairingKeys
2015
+ self, session: Session, identity_address: Address | None, keys: PairingKeys
2026
2016
  ) -> None:
2027
2017
  # Store the keys in the key store
2028
2018
  if self.device.keystore and identity_address is not None:
@@ -2041,7 +2031,7 @@ class Manager(utils.EventEmitter):
2041
2031
 
2042
2032
  def get_long_term_key(
2043
2033
  self, connection: Connection, rand: bytes, ediv: int
2044
- ) -> Optional[bytes]:
2034
+ ) -> bytes | None:
2045
2035
  if session := self.sessions.get(connection.handle):
2046
2036
  return session.get_long_term_key(rand, ediv)
2047
2037
 
bumble/snoop.py CHANGED
@@ -16,13 +16,14 @@ import datetime
16
16
  import logging
17
17
  import os
18
18
  import struct
19
+ from collections.abc import Generator
19
20
 
20
21
  # -----------------------------------------------------------------------------
21
22
  # Imports
22
23
  # -----------------------------------------------------------------------------
23
24
  from contextlib import contextmanager
24
25
  from enum import IntEnum
25
- from typing import BinaryIO, Generator
26
+ from typing import BinaryIO
26
27
 
27
28
  from bumble import core
28
29
  from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
@@ -27,7 +27,7 @@ import sys
27
27
  import yaml
28
28
 
29
29
  # -----------------------------------------------------------------------------
30
- with open(sys.argv[1], "r") as yaml_file:
30
+ with open(sys.argv[1]) as yaml_file:
31
31
  root = yaml.safe_load(yaml_file)
32
32
  companies = {}
33
33
  for company in root["company_identifiers"]:
@@ -18,7 +18,7 @@ import asyncio
18
18
  # Imports
19
19
  # -----------------------------------------------------------------------------
20
20
  import logging
21
- from typing import Any, Optional
21
+ from typing import Any
22
22
 
23
23
  import click
24
24
 
@@ -47,7 +47,7 @@ def print_device_info(device_info: dict[intel.ValueType, Any]) -> None:
47
47
 
48
48
 
49
49
  # -----------------------------------------------------------------------------
50
- async def get_driver(host: Host, force: bool) -> Optional[intel.Driver]:
50
+ async def get_driver(host: Host, force: bool) -> intel.Driver | None:
51
51
  # Create a driver
52
52
  driver = await intel.Driver.for_host(host, force)
53
53
  if driver is None:
@@ -21,11 +21,11 @@ import urllib.error
21
21
  import urllib.request
22
22
 
23
23
  import click
24
+ from bumble.tools import rtk_util
24
25
 
25
26
  import bumble.logging
26
27
  from bumble.colors import color
27
28
  from bumble.drivers import rtk
28
- from bumble.tools import rtk_util
29
29
 
30
30
  # -----------------------------------------------------------------------------
31
31
  # Logging
bumble/tools/rtk_util.py CHANGED
@@ -21,7 +21,7 @@ import logging
21
21
  import click
22
22
 
23
23
  import bumble.logging
24
- from bumble import company_ids, hci, transport
24
+ from bumble import transport
25
25
  from bumble.drivers import rtk
26
26
  from bumble.host import Host
27
27
 
@@ -18,7 +18,6 @@
18
18
  import logging
19
19
  import os
20
20
  import re
21
- from typing import Optional
22
21
 
23
22
  from bumble import utils
24
23
  from bumble.snoop import create_snooper
@@ -111,7 +110,7 @@ async def open_transport(name: str) -> Transport:
111
110
 
112
111
 
113
112
  # -----------------------------------------------------------------------------
114
- async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
113
+ async def _open_transport(scheme: str, spec: str | None) -> Transport:
115
114
  # pylint: disable=import-outside-toplevel
116
115
  # pylint: disable=too-many-return-statements
117
116
 
@@ -16,7 +16,6 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import logging
19
- from typing import Optional, Union
20
19
 
21
20
  import grpc.aio
22
21
 
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
44
43
 
45
44
 
46
45
  # -----------------------------------------------------------------------------
47
- async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
46
+ async def open_android_emulator_transport(spec: str | None) -> Transport:
48
47
  '''
49
48
  Open a transport connection to an Android emulator via its gRPC interface.
50
49
  The parameter string has this syntax:
@@ -89,7 +88,7 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
89
88
  logger.debug('connecting to gRPC server at %s', server_address)
90
89
  channel = grpc.aio.insecure_channel(server_address)
91
90
 
92
- service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
91
+ service: EmulatedBluetoothServiceStub | VhciForwardingServiceStub
93
92
  if mode == 'host':
94
93
  # Connect as a host
95
94
  service = EmulatedBluetoothServiceStub(channel)
@@ -22,7 +22,6 @@ import os
22
22
  import pathlib
23
23
  import platform
24
24
  import sys
25
- from typing import Optional
26
25
 
27
26
  import grpc.aio
28
27
 
@@ -66,7 +65,7 @@ DEFAULT_VARIANT = ''
66
65
 
67
66
 
68
67
  # -----------------------------------------------------------------------------
69
- def get_ini_dir() -> Optional[pathlib.Path]:
68
+ def get_ini_dir() -> pathlib.Path | None:
70
69
  if sys.platform == 'darwin':
71
70
  if tmpdir := os.getenv('TMPDIR', None):
72
71
  return pathlib.Path(tmpdir)
@@ -100,7 +99,7 @@ def find_grpc_port(instance_number: int) -> int:
100
99
  ini_file = ini_dir / ini_file_name(instance_number)
101
100
  logger.debug(f'Looking for .ini file at {ini_file}')
102
101
  if ini_file.is_file():
103
- with open(ini_file, 'r') as ini_file_data:
102
+ with open(ini_file) as ini_file_data:
104
103
  for line in ini_file_data.readlines():
105
104
  if '=' in line:
106
105
  key, value = line.split('=')
@@ -146,7 +145,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
146
145
 
147
146
  # -----------------------------------------------------------------------------
148
147
  async def open_android_netsim_controller_transport(
149
- server_host: Optional[str], server_port: int, options: dict[str, str]
148
+ server_host: str | None, server_port: int, options: dict[str, str]
150
149
  ) -> Transport:
151
150
  if server_host == '_' or not server_host:
152
151
  server_host = 'localhost'
@@ -156,21 +155,26 @@ async def open_android_netsim_controller_transport(
156
155
  logger.warning("unable to publish gRPC port")
157
156
 
158
157
  class HciDevice:
159
- def __init__(self, context, on_data_received):
158
+ def __init__(self, context, server):
160
159
  self.context = context
161
- self.on_data_received = on_data_received
160
+ self.server = server
162
161
  self.name = None
162
+ self.sink = None
163
163
  self.loop = asyncio.get_running_loop()
164
164
  self.done = self.loop.create_future()
165
- self.task = self.loop.create_task(self.pump())
166
165
 
167
166
  async def pump(self):
168
167
  try:
169
168
  await self.pump_loop()
170
169
  except asyncio.CancelledError:
171
170
  logger.debug('Pump task canceled')
172
- if not self.done.done():
173
- self.done.set_result(None)
171
+ finally:
172
+ if self.sink:
173
+ logger.debug('Releasing sink')
174
+ self.server.release_sink()
175
+ self.sink = None
176
+
177
+ logger.debug('Pump task terminated')
174
178
 
175
179
  async def pump_loop(self):
176
180
  while True:
@@ -186,15 +190,26 @@ async def open_android_netsim_controller_transport(
186
190
  if request.WhichOneof('request_type') == 'initial_info':
187
191
  logger.debug(f'Received initial info: {request}')
188
192
 
193
+ self.name = request.initial_info.name
194
+
189
195
  # We only accept BLUETOOTH
190
196
  if request.initial_info.chip.kind != ChipKind.BLUETOOTH:
191
197
  logger.warning('Unsupported chip type')
192
198
  error = PacketResponse(error='Unsupported chip type')
193
199
  await self.context.write(error)
194
- return
200
+ # return
201
+ continue
202
+
203
+ # Lease the sink so that no other device can send
204
+ self.sink = self.server.lease_sink(self)
205
+ if self.sink is None:
206
+ logger.warning('Another device is already connected')
207
+ error = PacketResponse(error='Device busy')
208
+ await self.context.write(error)
209
+ # return
210
+ continue
195
211
 
196
- self.name = request.initial_info.name
197
- continue
212
+ continue
198
213
 
199
214
  # Expect a data packet
200
215
  request_type = request.WhichOneof('request_type')
@@ -205,10 +220,10 @@ async def open_android_netsim_controller_transport(
205
220
  continue
206
221
 
207
222
  # Process the packet
208
- data = (
223
+ assert self.sink is not None
224
+ self.sink(
209
225
  bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
210
226
  )
211
- self.on_data_received(data)
212
227
 
213
228
  async def send_packet(self, data):
214
229
  return await self.context.write(
@@ -217,12 +232,6 @@ async def open_android_netsim_controller_transport(
217
232
  )
218
233
  )
219
234
 
220
- def terminate(self):
221
- self.task.cancel()
222
-
223
- async def wait_for_termination(self):
224
- await self.done
225
-
226
235
  server_address = f'{server_host}:{server_port}'
227
236
 
228
237
  class Server(PacketStreamerServicer, ParserSource):
@@ -258,27 +267,27 @@ async def open_android_netsim_controller_transport(
258
267
 
259
268
  return await self.device.send_packet(packet)
260
269
 
261
- async def StreamPackets(self, _request_iterator, context):
262
- logger.debug('StreamPackets request')
263
-
264
- # Check that we don't already have a device
270
+ def lease_sink(self, device):
265
271
  if self.device:
266
- logger.debug('Busy, already serving a device')
267
- return PacketResponse(error='Busy')
272
+ return None
273
+ self.device = device
274
+ return self.parser.feed_data
275
+
276
+ def release_sink(self):
277
+ self.device = None
278
+
279
+ async def StreamPackets(self, request_iterator, context):
280
+ logger.debug('StreamPackets request')
268
281
 
269
282
  # Instantiate a new device
270
- self.device = HciDevice(context, self.parser.feed_data)
283
+ device = HciDevice(context, self)
271
284
 
272
- # Wait for the device to terminate
273
- logger.debug('Waiting for device to terminate')
285
+ # Pump packets to/from the device
286
+ logger.debug('Pumping device packets')
274
287
  try:
275
- await self.device.wait_for_termination()
276
- except asyncio.CancelledError:
277
- logger.debug('Request canceled')
278
- self.device.terminate()
279
-
280
- logger.debug('Device terminated')
281
- self.device = None
288
+ await device.pump()
289
+ finally:
290
+ logger.debug('Pump terminated')
282
291
 
283
292
  server = Server()
284
293
  await server.start()
@@ -291,9 +300,9 @@ async def open_android_netsim_controller_transport(
291
300
 
292
301
  # -----------------------------------------------------------------------------
293
302
  async def open_android_netsim_host_transport_with_address(
294
- server_host: Optional[str],
303
+ server_host: str | None,
295
304
  server_port: int,
296
- options: Optional[dict[str, str]] = None,
305
+ options: dict[str, str] | None = None,
297
306
  ):
298
307
  if server_host == '_' or not server_host:
299
308
  server_host = 'localhost'
@@ -318,7 +327,7 @@ async def open_android_netsim_host_transport_with_address(
318
327
 
319
328
  # -----------------------------------------------------------------------------
320
329
  async def open_android_netsim_host_transport_with_channel(
321
- channel, options: Optional[dict[str, str]] = None
330
+ channel, options: dict[str, str] | None = None
322
331
  ):
323
332
  # Wrapper for I/O operations
324
333
  class HciDevice:
@@ -398,7 +407,7 @@ async def open_android_netsim_host_transport_with_channel(
398
407
 
399
408
 
400
409
  # -----------------------------------------------------------------------------
401
- async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
410
+ async def open_android_netsim_transport(spec: str | None) -> Transport:
402
411
  '''
403
412
  Open a transport connection as a client or server, implementing Android's `netsim`
404
413
  simulator protocol over gRPC.
@@ -23,7 +23,7 @@ import io
23
23
  import logging
24
24
  import struct
25
25
  from collections.abc import Awaitable, Callable
26
- from typing import Any, ContextManager, Optional, Protocol
26
+ from typing import Any, Protocol
27
27
 
28
28
  from bumble import core, hci
29
29
  from bumble.colors import color
@@ -107,11 +107,11 @@ class PacketParser:
107
107
  NEED_LENGTH = 1
108
108
  NEED_BODY = 2
109
109
 
110
- sink: Optional[TransportSink]
110
+ sink: TransportSink | None
111
111
  extended_packet_info: dict[int, tuple[int, int, str]]
112
- packet_info: Optional[tuple[int, int, str]] = None
112
+ packet_info: tuple[int, int, str] | None = None
113
113
 
114
- def __init__(self, sink: Optional[TransportSink] = None) -> None:
114
+ def __init__(self, sink: TransportSink | None = None) -> None:
115
115
  self.sink = sink
116
116
  self.extended_packet_info = {}
117
117
  self.reset()
@@ -176,7 +176,7 @@ class PacketReader:
176
176
  self.source = source
177
177
  self.at_end = False
178
178
 
179
- def next_packet(self) -> Optional[bytes]:
179
+ def next_packet(self) -> bytes | None:
180
180
  # Get the packet type
181
181
  packet_type = self.source.read(1)
182
182
  if len(packet_type) != 1:
@@ -253,7 +253,7 @@ class BaseSource:
253
253
  """
254
254
 
255
255
  terminated: asyncio.Future[None]
256
- sink: Optional[TransportSink]
256
+ sink: TransportSink | None
257
257
 
258
258
  def __init__(self) -> None:
259
259
  self.terminated = asyncio.get_running_loop().create_future()
@@ -357,7 +357,7 @@ class Transport:
357
357
 
358
358
  # -----------------------------------------------------------------------------
359
359
  class PumpedPacketSource(ParserSource):
360
- pump_task: Optional[asyncio.Task[None]]
360
+ pump_task: asyncio.Task[None] | None
361
361
 
362
362
  def __init__(self, receive) -> None:
363
363
  super().__init__()
@@ -390,7 +390,7 @@ class PumpedPacketSource(ParserSource):
390
390
 
391
391
  # -----------------------------------------------------------------------------
392
392
  class PumpedPacketSink:
393
- pump_task: Optional[asyncio.Task[None]]
393
+ pump_task: asyncio.Task[None] | None
394
394
 
395
395
  def __init__(self, send: Callable[[bytes], Awaitable[Any]]):
396
396
  self.send_function = send
@@ -443,7 +443,7 @@ class SnoopingTransport(Transport):
443
443
 
444
444
  @staticmethod
445
445
  def create_with(
446
- transport: Transport, snooper: ContextManager[Snooper]
446
+ transport: Transport, snooper: contextlib.AbstractContextManager[Snooper]
447
447
  ) -> SnoopingTransport:
448
448
  """
449
449
  Create an instance given a snooper that works as as context manager.
bumble/transport/file.py CHANGED
@@ -16,7 +16,6 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
- import io
20
19
  import logging
21
20
 
22
21
  from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
@@ -36,7 +35,7 @@ async def open_file_transport(spec: str) -> Transport:
36
35
  '''
37
36
 
38
37
  # Open the file
39
- file = io.open(spec, 'r+b', buffering=0)
38
+ file = open(spec, 'r+b', buffering=0)
40
39
 
41
40
  # Setup reading
42
41
  read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
@@ -22,7 +22,6 @@ import logging
22
22
  import os
23
23
  import socket
24
24
  import struct
25
- from typing import Optional
26
25
 
27
26
  from bumble.transport.common import ParserSource, Transport
28
27
 
@@ -33,7 +32,7 @@ logger = logging.getLogger(__name__)
33
32
 
34
33
 
35
34
  # -----------------------------------------------------------------------------
36
- async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
35
+ async def open_hci_socket_transport(spec: str | None) -> Transport:
37
36
  '''
38
37
  Open an HCI Socket (only available on some platforms).
39
38
  The parameter string is either empty (to use the first/default Bluetooth adapter)
@@ -87,7 +86,7 @@ async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
87
86
  )
88
87
  != 0
89
88
  ):
90
- raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
89
+ raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
91
90
 
92
91
  class HciSocketSource(ParserSource):
93
92
  def __init__(self, hci_socket):
bumble/transport/pty.py CHANGED
@@ -17,12 +17,10 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
19
  import atexit
20
- import io
21
20
  import logging
22
21
  import os
23
22
  import pty
24
23
  import tty
25
- from typing import Optional
26
24
 
27
25
  from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
28
26
 
@@ -33,7 +31,7 @@ logger = logging.getLogger(__name__)
33
31
 
34
32
 
35
33
  # -----------------------------------------------------------------------------
36
- async def open_pty_transport(spec: Optional[str]) -> Transport:
34
+ async def open_pty_transport(spec: str | None) -> Transport:
37
35
  '''
38
36
  Open a PTY transport.
39
37
  The parameter string may be empty, or a path name where a symbolic link
@@ -48,11 +46,11 @@ async def open_pty_transport(spec: Optional[str]) -> Transport:
48
46
  tty.setraw(replica)
49
47
 
50
48
  read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
51
- StreamPacketSource, io.open(primary, 'rb', closefd=False)
49
+ StreamPacketSource, open(primary, 'rb', closefd=False)
52
50
  )
53
51
 
54
52
  write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
55
- asyncio.BaseProtocol, io.open(primary, 'wb', closefd=False)
53
+ asyncio.BaseProtocol, open(primary, 'wb', closefd=False)
56
54
  )
57
55
  packet_sink = StreamPacketSink(write_transport)
58
56
 
bumble/transport/pyusb.py CHANGED
@@ -19,7 +19,6 @@ import asyncio
19
19
  import logging
20
20
  import threading
21
21
  import time
22
- from typing import Optional
23
22
 
24
23
  import usb.core
25
24
  import usb.util
@@ -284,7 +283,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
284
283
  device = await _power_cycle(device) # type: ignore
285
284
  except Exception as e:
286
285
  logging.debug(e, stack_info=True)
287
- logging.info(f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}") # type: ignore
286
+ logging.info(
287
+ f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}"
288
+ ) # type: ignore
288
289
 
289
290
  # Collect the metadata
290
291
  device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
@@ -370,7 +371,9 @@ async def _power_cycle(device: UsbDevice) -> UsbDevice:
370
371
  # Device needs to be find again otherwise it will appear as disconnected
371
372
  return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
372
373
  except USBError:
373
- logger.exception(f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition.") # type: ignore
374
+ logger.exception(
375
+ f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition."
376
+ ) # type: ignore
374
377
 
375
378
  return device
376
379
 
@@ -385,7 +388,7 @@ def _set_port_status(device: UsbDevice, port: int, on: bool):
385
388
  )
386
389
 
387
390
 
388
- def _find_device_by_path(sys_path: str) -> Optional[UsbDevice]:
391
+ def _find_device_by_path(sys_path: str) -> UsbDevice | None:
389
392
  """Finds a USB device based on its system path."""
390
393
  bus_num, *port_parts = sys_path.split('-')
391
394
  ports = [int(port) for port in port_parts[0].split('.')]
@@ -398,7 +401,7 @@ def _find_device_by_path(sys_path: str) -> Optional[UsbDevice]:
398
401
  return None
399
402
 
400
403
 
401
- def _find_hub_by_device_path(sys_path: str) -> Optional[UsbDevice]:
404
+ def _find_hub_by_device_path(sys_path: str) -> UsbDevice | None:
402
405
  """Finds the USB hub associated with a specific device path."""
403
406
  hub_sys_path = sys_path.rsplit('.', 1)[0]
404
407
  hub_device = _find_device_by_path(hub_sys_path)