bumble 0.0.220__py3-none-any.whl → 0.0.222__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 +1070 -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.222.dist-info}/METADATA +3 -2
  98. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/RECORD +102 -101
  99. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/WHEEL +0 -0
  100. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/entry_points.txt +0 -0
  101. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/licenses/LICENSE +0 -0
  102. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/top_level.txt +0 -0
bumble/host.py CHANGED
@@ -22,7 +22,8 @@ import collections
22
22
  import dataclasses
23
23
  import logging
24
24
  import struct
25
- from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast
25
+ from collections.abc import Awaitable, Callable
26
+ from typing import TYPE_CHECKING, Any, cast
26
27
 
27
28
  from bumble import drivers, hci, utils
28
29
  from bumble.colors import color
@@ -108,8 +109,7 @@ class DataPacketQueue(utils.EventEmitter):
108
109
 
109
110
  if self._packets:
110
111
  logger.debug(
111
- f'{self._in_flight} packets in flight, '
112
- f'{len(self._packets)} in queue'
112
+ f'{self._in_flight} packets in flight, {len(self._packets)} in queue'
113
113
  )
114
114
 
115
115
  def flush(self, connection_handle: int) -> None:
@@ -199,7 +199,7 @@ class Connection:
199
199
  self.peer_address = peer_address
200
200
  self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
201
201
  self.transport = transport
202
- acl_packet_queue: Optional[DataPacketQueue] = (
202
+ acl_packet_queue: DataPacketQueue | None = (
203
203
  host.le_acl_packet_queue
204
204
  if transport == PhysicalTransport.LE
205
205
  else host.acl_packet_queue
@@ -242,20 +242,18 @@ class Host(utils.EventEmitter):
242
242
  bis_links: dict[int, IsoLink]
243
243
  sco_links: dict[int, ScoLink]
244
244
  bigs: dict[int, set[int]]
245
- acl_packet_queue: Optional[DataPacketQueue] = None
246
- le_acl_packet_queue: Optional[DataPacketQueue] = None
247
- iso_packet_queue: Optional[DataPacketQueue] = None
248
- hci_sink: Optional[TransportSink] = None
245
+ acl_packet_queue: DataPacketQueue | None = None
246
+ le_acl_packet_queue: DataPacketQueue | None = None
247
+ iso_packet_queue: DataPacketQueue | None = None
248
+ hci_sink: TransportSink | None = None
249
249
  hci_metadata: dict[str, Any]
250
- long_term_key_provider: Optional[
251
- Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
252
- ]
253
- link_key_provider: Optional[Callable[[hci.Address], Awaitable[Optional[bytes]]]]
250
+ long_term_key_provider: Callable[[int, bytes, int], Awaitable[bytes | None]] | None
251
+ link_key_provider: Callable[[hci.Address], Awaitable[bytes | None]] | None
254
252
 
255
253
  def __init__(
256
254
  self,
257
- controller_source: Optional[TransportSource] = None,
258
- controller_sink: Optional[TransportSink] = None,
255
+ controller_source: TransportSource | None = None,
256
+ controller_sink: TransportSink | None = None,
259
257
  ) -> None:
260
258
  super().__init__()
261
259
 
@@ -267,7 +265,7 @@ class Host(utils.EventEmitter):
267
265
  self.sco_links = {} # SCO links, by connection handle
268
266
  self.bigs = {} # BIG Handle to BIS Handles
269
267
  self.pending_command = None
270
- self.pending_response: Optional[asyncio.Future[Any]] = None
268
+ self.pending_response: asyncio.Future[Any] | None = None
271
269
  self.number_of_supported_advertising_sets = 0
272
270
  self.maximum_advertising_data_length = 31
273
271
  self.local_version = None
@@ -280,7 +278,7 @@ class Host(utils.EventEmitter):
280
278
  self.long_term_key_provider = None
281
279
  self.link_key_provider = None
282
280
  self.pairing_io_capability_provider = None # Classic only
283
- self.snooper: Optional[Snooper] = None
281
+ self.snooper: Snooper | None = None
284
282
 
285
283
  # Connect to the source and sink if specified
286
284
  if controller_source:
@@ -291,9 +289,9 @@ class Host(utils.EventEmitter):
291
289
  def find_connection_by_bd_addr(
292
290
  self,
293
291
  bd_addr: hci.Address,
294
- transport: Optional[int] = None,
292
+ transport: int | None = None,
295
293
  check_address_type: bool = False,
296
- ) -> Optional[Connection]:
294
+ ) -> Connection | None:
297
295
  for connection in self.connections.values():
298
296
  if bytes(connection.peer_address) == bytes(bd_addr):
299
297
  if (
@@ -633,7 +631,7 @@ class Host(utils.EventEmitter):
633
631
  )
634
632
 
635
633
  @property
636
- def controller(self) -> Optional[TransportSink]:
634
+ def controller(self) -> TransportSink | None:
637
635
  return self.hci_sink
638
636
 
639
637
  @controller.setter
@@ -642,7 +640,7 @@ class Host(utils.EventEmitter):
642
640
  if controller:
643
641
  self.set_packet_source(controller)
644
642
 
645
- def set_packet_sink(self, sink: Optional[TransportSink]) -> None:
643
+ def set_packet_sink(self, sink: TransportSink | None) -> None:
646
644
  self.hci_sink = sink
647
645
 
648
646
  def set_packet_source(self, source: TransportSource) -> None:
@@ -657,7 +655,7 @@ class Host(utils.EventEmitter):
657
655
  self.hci_sink.on_packet(bytes(packet))
658
656
 
659
657
  async def send_command(
660
- self, command, check_result=False, response_timeout: Optional[int] = None
658
+ self, command, check_result=False, response_timeout: int | None = None
661
659
  ):
662
660
  # Wait until we can send (only one pending command at a time)
663
661
  async with self.command_semaphore:
@@ -707,7 +705,7 @@ class Host(utils.EventEmitter):
707
705
 
708
706
  asyncio.create_task(send_command(command))
709
707
 
710
- def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
708
+ def send_acl_sdu(self, connection_handle: int, sdu: bytes) -> None:
711
709
  if not (connection := self.connections.get(connection_handle)):
712
710
  logger.warning(f'connection 0x{connection_handle:04X} not found')
713
711
  return
@@ -718,27 +716,24 @@ class Host(utils.EventEmitter):
718
716
  )
719
717
  return
720
718
 
721
- # Create a PDU
722
- l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
723
-
724
719
  # Send the data to the controller via ACL packets
725
- bytes_remaining = len(l2cap_pdu)
726
- offset = 0
727
- pb_flag = 0
728
- while bytes_remaining:
729
- data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
720
+ max_packet_size = packet_queue.max_packet_size
721
+ for offset in range(0, len(sdu), max_packet_size):
722
+ pdu = sdu[offset : offset + max_packet_size]
730
723
  acl_packet = hci.HCI_AclDataPacket(
731
724
  connection_handle=connection_handle,
732
- pb_flag=pb_flag,
725
+ pb_flag=1 if offset > 0 else 0,
733
726
  bc_flag=0,
734
- data_total_length=data_total_length,
735
- data=l2cap_pdu[offset : offset + data_total_length],
727
+ data_total_length=len(pdu),
728
+ data=pdu,
729
+ )
730
+ logger.debug(
731
+ '>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
736
732
  )
737
- logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
738
733
  packet_queue.enqueue(acl_packet, connection_handle)
739
- pb_flag = 1
740
- offset += data_total_length
741
- bytes_remaining -= data_total_length
734
+
735
+ def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
736
+ self.send_acl_sdu(connection_handle, bytes(L2CAP_PDU(cid, pdu)))
742
737
 
743
738
  def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
744
739
  if connection := self.connections.get(connection_handle):
@@ -903,7 +898,7 @@ class Host(utils.EventEmitter):
903
898
  self.emit('l2cap_pdu', connection.handle, cid, pdu)
904
899
 
905
900
  def on_command_processed(
906
- self, event: Union[hci.HCI_Command_Complete_Event, hci.HCI_Command_Status_Event]
901
+ self, event: hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event
907
902
  ):
908
903
  if self.pending_response:
909
904
  # Check that it is what we were expecting
@@ -966,11 +961,11 @@ class Host(utils.EventEmitter):
966
961
 
967
962
  def on_hci_le_connection_complete_event(
968
963
  self,
969
- event: Union[
970
- hci.HCI_LE_Connection_Complete_Event,
971
- hci.HCI_LE_Enhanced_Connection_Complete_Event,
972
- hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
973
- ],
964
+ event: (
965
+ hci.HCI_LE_Connection_Complete_Event
966
+ | hci.HCI_LE_Enhanced_Connection_Complete_Event
967
+ | hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
968
+ ),
974
969
  ):
975
970
  # Check if this is a cancellation
976
971
  if event.status == hci.HCI_SUCCESS:
@@ -1015,10 +1010,10 @@ class Host(utils.EventEmitter):
1015
1010
 
1016
1011
  def on_hci_le_enhanced_connection_complete_event(
1017
1012
  self,
1018
- event: Union[
1019
- hci.HCI_LE_Enhanced_Connection_Complete_Event,
1020
- hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
1021
- ],
1013
+ event: (
1014
+ hci.HCI_LE_Enhanced_Connection_Complete_Event
1015
+ | hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
1016
+ ),
1022
1017
  ):
1023
1018
  # Just use the same implementation as for the non-enhanced event for now
1024
1019
  self.on_hci_le_connection_complete_event(event)
@@ -1397,8 +1392,7 @@ class Host(utils.EventEmitter):
1397
1392
  if event.status == hci.HCI_SUCCESS:
1398
1393
  # Create/update the connection
1399
1394
  logger.debug(
1400
- f'### SCO CONNECTION: [0x{event.connection_handle:04X}] '
1401
- f'{event.bd_addr}'
1395
+ f'### SCO CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
1402
1396
  )
1403
1397
 
1404
1398
  self.sco_links[event.connection_handle] = ScoLink(
@@ -1450,7 +1444,7 @@ class Host(utils.EventEmitter):
1450
1444
  def on_hci_le_data_length_change_event(
1451
1445
  self, event: hci.HCI_LE_Data_Length_Change_Event
1452
1446
  ):
1453
- if (connection := self.connections.get(event.connection_handle)) is None:
1447
+ if event.connection_handle not in self.connections:
1454
1448
  logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
1455
1449
  return
1456
1450
 
bumble/keys.py CHANGED
@@ -27,7 +27,7 @@ import dataclasses
27
27
  import json
28
28
  import logging
29
29
  import os
30
- from typing import TYPE_CHECKING, Any, Optional
30
+ from typing import TYPE_CHECKING, Any
31
31
 
32
32
  from typing_extensions import Self
33
33
 
@@ -51,8 +51,8 @@ class PairingKeys:
51
51
  class Key:
52
52
  value: bytes
53
53
  authenticated: bool = False
54
- ediv: Optional[int] = None
55
- rand: Optional[bytes] = None
54
+ ediv: int | None = None
55
+ rand: bytes | None = None
56
56
 
57
57
  @classmethod
58
58
  def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
@@ -74,17 +74,17 @@ class PairingKeys:
74
74
 
75
75
  return key_dict
76
76
 
77
- address_type: Optional[hci.AddressType] = None
78
- ltk: Optional[Key] = None
79
- ltk_central: Optional[Key] = None
80
- ltk_peripheral: Optional[Key] = None
81
- irk: Optional[Key] = None
82
- csrk: Optional[Key] = None
83
- link_key: Optional[Key] = None # Classic
84
- link_key_type: Optional[int] = None # Classic
77
+ address_type: hci.AddressType | None = None
78
+ ltk: Key | None = None
79
+ ltk_central: Key | None = None
80
+ ltk_peripheral: Key | None = None
81
+ irk: Key | None = None
82
+ csrk: Key | None = None
83
+ link_key: Key | None = None # Classic
84
+ link_key_type: int | None = None # Classic
85
85
 
86
86
  @classmethod
87
- def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
87
+ def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Key | None:
88
88
  key_dict = keys_dict.get(key_name)
89
89
  if key_dict is None:
90
90
  return None
@@ -156,7 +156,7 @@ class KeyStore:
156
156
  async def update(self, name: str, keys: PairingKeys) -> None:
157
157
  pass
158
158
 
159
- async def get(self, _name: str) -> Optional[PairingKeys]:
159
+ async def get(self, _name: str) -> PairingKeys | None:
160
160
  return None
161
161
 
162
162
  async def get_all(self) -> list[tuple[str, PairingKeys]]:
@@ -274,7 +274,7 @@ class JsonKeyStore(KeyStore):
274
274
 
275
275
  @classmethod
276
276
  def from_device(
277
- cls: type[Self], device: Device, filename: Optional[str] = None
277
+ cls: type[Self], device: Device, filename: str | None = None
278
278
  ) -> Self:
279
279
  if not filename:
280
280
  # Extract the filename from the config if there is one
@@ -297,7 +297,7 @@ class JsonKeyStore(KeyStore):
297
297
  # Try to open the file, without failing. If the file does not exist, it
298
298
  # will be created upon saving.
299
299
  try:
300
- with open(self.filename, 'r', encoding='utf-8') as json_file:
300
+ with open(self.filename, encoding='utf-8') as json_file:
301
301
  db = json.load(json_file)
302
302
  except FileNotFoundError:
303
303
  db = {}
@@ -348,7 +348,7 @@ class JsonKeyStore(KeyStore):
348
348
  key_map.clear()
349
349
  await self.save(db)
350
350
 
351
- async def get(self, name: str) -> Optional[PairingKeys]:
351
+ async def get(self, name: str) -> PairingKeys | None:
352
352
  _, key_map = await self.load()
353
353
  if name not in key_map:
354
354
  return None
@@ -370,7 +370,7 @@ class MemoryKeyStore(KeyStore):
370
370
  async def update(self, name: str, keys: PairingKeys) -> None:
371
371
  self.all_keys[name] = keys
372
372
 
373
- async def get(self, name: str) -> Optional[PairingKeys]:
373
+ async def get(self, name: str) -> PairingKeys | None:
374
374
  return self.all_keys.get(name)
375
375
 
376
376
  async def get_all(self) -> list[tuple[str, PairingKeys]]: