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
@@ -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)
@@ -17,7 +17,6 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
19
  import logging
20
- from typing import Optional
21
20
 
22
21
  import serial_asyncio
23
22
 
@@ -52,7 +51,7 @@ class SerialPacketSource(StreamPacketSource):
52
51
  logger.debug('connection made')
53
52
  self._ready.set()
54
53
 
55
- def connection_lost(self, exc: Optional[Exception]) -> None:
54
+ def connection_lost(self, exc: Exception | None) -> None:
56
55
  logger.debug('connection lost')
57
56
  self.on_transport_lost()
58
57
 
bumble/transport/vhci.py CHANGED
@@ -16,7 +16,6 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import logging
19
- from typing import Optional
20
19
 
21
20
  from bumble.transport.common import Transport
22
21
  from bumble.transport.file import open_file_transport
@@ -28,7 +27,7 @@ logger = logging.getLogger(__name__)
28
27
 
29
28
 
30
29
  # -----------------------------------------------------------------------------
31
- async def open_vhci_transport(spec: Optional[str]) -> Transport:
30
+ async def open_vhci_transport(spec: str | None) -> Transport:
32
31
  '''
33
32
  Open a VHCI transport (only available on some platforms).
34
33
  The parameter string is either empty (to use the default VHCI device
@@ -16,7 +16,6 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import logging
19
- from typing import Optional
20
19
 
21
20
  import websockets.asyncio.server
22
21
 
@@ -43,8 +42,8 @@ async def open_ws_server_transport(spec: str) -> Transport:
43
42
  class WsServerTransport(Transport):
44
43
  sink: PumpedPacketSink
45
44
  source: ParserSource
46
- connection: Optional[websockets.asyncio.server.ServerConnection]
47
- server: Optional[websockets.asyncio.server.Server]
45
+ connection: websockets.asyncio.server.ServerConnection | None
46
+ server: websockets.asyncio.server.Server | None
48
47
 
49
48
  def __init__(self) -> None:
50
49
  source = ParserSource()
bumble/utils.py CHANGED
@@ -22,16 +22,12 @@ import collections
22
22
  import enum
23
23
  import functools
24
24
  import logging
25
- import sys
26
25
  import warnings
26
+ from collections.abc import Awaitable, Callable
27
27
  from typing import (
28
28
  Any,
29
- Awaitable,
30
- Callable,
31
- Optional,
32
29
  Protocol,
33
30
  TypeVar,
34
- Union,
35
31
  overload,
36
32
  )
37
33
 
@@ -170,8 +166,8 @@ class EventWatcher:
170
166
  ) -> _Handler: ...
171
167
 
172
168
  def on(
173
- self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
174
- ) -> Union[_Handler, Callable[[_Handler], _Handler]]:
169
+ self, emitter: pyee.EventEmitter, event: str, handler: _Handler | None = None
170
+ ) -> _Handler | Callable[[_Handler], _Handler]:
175
171
  '''Watch an event until the context is closed.
176
172
 
177
173
  Args:
@@ -199,8 +195,8 @@ class EventWatcher:
199
195
  ) -> _Handler: ...
200
196
 
201
197
  def once(
202
- self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
203
- ) -> Union[_Handler, Callable[[_Handler], _Handler]]:
198
+ self, emitter: pyee.EventEmitter, event: str, handler: _Handler | None = None
199
+ ) -> _Handler | Callable[[_Handler], _Handler]:
204
200
  '''Watch an event for once.
205
201
 
206
202
  Args:
@@ -241,11 +237,7 @@ def cancel_on_event(
241
237
  return
242
238
  msg = f'abort: {event} event occurred.'
243
239
  if isinstance(future, asyncio.Task):
244
- # python < 3.9 does not support passing a message on `Task.cancel`
245
- if sys.version_info < (3, 9, 0):
246
- future.cancel()
247
- else:
248
- future.cancel(msg)
240
+ future.cancel(msg)
249
241
  else:
250
242
  future.set_exception(asyncio.CancelledError(msg))
251
243
 
@@ -537,3 +529,20 @@ class IntConvertible(Protocol):
537
529
 
538
530
  def __init__(self, value: int) -> None: ...
539
531
  def __int__(self) -> int: ...
532
+
533
+
534
+ # -----------------------------------------------------------------------------
535
+ def crc_16(data: bytes) -> int:
536
+ """Calculate CRC-16-IBM of given data.
537
+
538
+ Polynomial = x^16 + x^15 + x^2 + 1 = 0x8005 or 0xA001(Reversed)
539
+ """
540
+ crc = 0x0000
541
+ for byte in data:
542
+ crc ^= byte
543
+ for _ in range(8):
544
+ if (crc & 0x0001) > 0:
545
+ crc = (crc >> 1) ^ 0xA001
546
+ else:
547
+ crc = crc >> 1
548
+ return crc
@@ -18,7 +18,6 @@
18
18
  import dataclasses
19
19
  import struct
20
20
  from dataclasses import field
21
- from typing import Optional
22
21
 
23
22
  from bumble import hci
24
23
 
@@ -51,6 +50,7 @@ class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
51
50
  '''
52
51
  See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
53
52
  '''
53
+
54
54
  return_parameters_fields = [
55
55
  ('status', hci.STATUS_SPEC),
56
56
  ('max_advt_instances', 1),
@@ -137,6 +137,7 @@ class HCI_Get_Controller_Activity_Energy_Info_Command(hci.HCI_Command):
137
137
  '''
138
138
  See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info
139
139
  '''
140
+
140
141
  return_parameters_fields = [
141
142
  ('status', hci.STATUS_SPEC),
142
143
  ('total_tx_time_ms', 4),
@@ -207,7 +208,7 @@ class HCI_Android_Vendor_Event(hci.HCI_Extended_Event):
207
208
  @classmethod
208
209
  def subclass_from_parameters(
209
210
  cls, parameters: bytes
210
- ) -> Optional[hci.HCI_Extended_Event]:
211
+ ) -> hci.HCI_Extended_Event | None:
211
212
  subevent_code = parameters[0]
212
213
  if subevent_code == HCI_BLUETOOTH_QUALITY_REPORT_EVENT:
213
214
  quality_report_id = parameters[1]
@@ -229,6 +230,7 @@ class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event):
229
230
  '''
230
231
  See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#bluetooth-quality-report-sub-event
231
232
  '''
233
+
232
234
  quality_report_id: int = field(metadata=hci.metadata(1))
233
235
  packet_types: int = field(metadata=hci.metadata(1))
234
236
  connection_handle: int = field(metadata=hci.metadata(2))
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bumble
3
- Version: 0.0.219
3
+ Version: 0.0.221
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Author-email: Google <bumble-dev@google.com>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/google/bumble
8
- Requires-Python: >=3.9
8
+ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: aiohttp~=3.8; platform_system != "Emscripten"
@@ -25,6 +25,7 @@ Requires-Dist: pyee>=13.0.0
25
25
  Requires-Dist: pyserial-asyncio>=0.5; platform_system != "Emscripten"
26
26
  Requires-Dist: pyserial>=3.5; platform_system != "Emscripten"
27
27
  Requires-Dist: pyusb>=1.2; platform_system != "Emscripten"
28
+ Requires-Dist: tomli~=2.2.1; platform_system != "Emscripten"
28
29
  Requires-Dist: websockets>=15.0.1; platform_system != "Emscripten"
29
30
  Provides-Extra: build
30
31
  Requires-Dist: build>=0.7; extra == "build"
@@ -38,12 +39,12 @@ Requires-Dist: black~=25.1; extra == "development"
38
39
  Requires-Dist: bt-test-interfaces>=0.0.6; extra == "development"
39
40
  Requires-Dist: grpcio-tools>=1.62.1; extra == "development"
40
41
  Requires-Dist: invoke>=1.7.3; extra == "development"
41
- Requires-Dist: isort~=5.13.2; extra == "development"
42
42
  Requires-Dist: mobly>=1.12.2; extra == "development"
43
43
  Requires-Dist: mypy==1.12.0; extra == "development"
44
44
  Requires-Dist: nox>=2022; extra == "development"
45
45
  Requires-Dist: pylint==3.3.1; extra == "development"
46
46
  Requires-Dist: pyyaml>=6.0; extra == "development"
47
+ Requires-Dist: ruff==0.14.10; extra == "development"
47
48
  Requires-Dist: types-appdirs>=1.4.3; extra == "development"
48
49
  Requires-Dist: types-invoke>=1.7.3; extra == "development"
49
50
  Requires-Dist: types-protobuf>=4.21.0; extra == "development"