bumble 0.0.194__py3-none-any.whl → 0.0.198__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 (54) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +692 -0
  3. bumble/apps/bench.py +77 -23
  4. bumble/apps/console.py +5 -20
  5. bumble/apps/controller_info.py +3 -3
  6. bumble/apps/device_info.py +230 -0
  7. bumble/apps/gatt_dump.py +4 -0
  8. bumble/apps/lea_unicast/app.py +16 -17
  9. bumble/at.py +12 -6
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +5 -1
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +726 -122
  17. bumble/device.py +817 -117
  18. bumble/drivers/rtk.py +13 -8
  19. bumble/gatt.py +6 -1
  20. bumble/gatt_client.py +10 -4
  21. bumble/hci.py +283 -20
  22. bumble/hid.py +24 -28
  23. bumble/host.py +29 -0
  24. bumble/l2cap.py +24 -17
  25. bumble/link.py +8 -3
  26. bumble/pandora/host.py +3 -2
  27. bumble/profiles/ascs.py +739 -0
  28. bumble/profiles/bap.py +85 -862
  29. bumble/profiles/bass.py +440 -0
  30. bumble/profiles/csip.py +4 -4
  31. bumble/profiles/gap.py +110 -0
  32. bumble/profiles/heart_rate_service.py +4 -3
  33. bumble/profiles/le_audio.py +83 -0
  34. bumble/profiles/mcp.py +448 -0
  35. bumble/profiles/pacs.py +210 -0
  36. bumble/profiles/pbp.py +46 -0
  37. bumble/profiles/tmap.py +89 -0
  38. bumble/rfcomm.py +14 -3
  39. bumble/sdp.py +13 -11
  40. bumble/smp.py +20 -8
  41. bumble/snoop.py +5 -4
  42. bumble/transport/__init__.py +8 -2
  43. bumble/transport/android_emulator.py +9 -3
  44. bumble/transport/android_netsim.py +9 -7
  45. bumble/transport/common.py +46 -18
  46. bumble/transport/pyusb.py +2 -2
  47. bumble/transport/unix.py +56 -0
  48. bumble/transport/usb.py +57 -46
  49. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  50. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/RECORD +54 -43
  51. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  52. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  53. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  54. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/hid.py CHANGED
@@ -23,13 +23,12 @@ import struct
23
23
 
24
24
  from abc import ABC, abstractmethod
25
25
  from pyee import EventEmitter
26
- from typing import Optional, Callable, TYPE_CHECKING
26
+ from typing import Optional, Callable
27
27
  from typing_extensions import override
28
28
 
29
29
  from bumble import l2cap, device
30
- from bumble.colors import color
31
30
  from bumble.core import InvalidStateError, ProtocolError
32
- from .hci import Address
31
+ from bumble.hci import Address
33
32
 
34
33
 
35
34
  # -----------------------------------------------------------------------------
@@ -220,31 +219,27 @@ class HID(ABC, EventEmitter):
220
219
  async def connect_control_channel(self) -> None:
221
220
  # Create a new L2CAP connection - control channel
222
221
  try:
223
- self.l2cap_ctrl_channel = await self.device.l2cap_channel_manager.connect(
222
+ channel = await self.device.l2cap_channel_manager.connect(
224
223
  self.connection, HID_CONTROL_PSM
225
224
  )
225
+ channel.sink = self.on_ctrl_pdu
226
+ self.l2cap_ctrl_channel = channel
226
227
  except ProtocolError:
227
228
  logging.exception(f'L2CAP connection failed.')
228
229
  raise
229
230
 
230
- assert self.l2cap_ctrl_channel is not None
231
- # Become a sink for the L2CAP channel
232
- self.l2cap_ctrl_channel.sink = self.on_ctrl_pdu
233
-
234
231
  async def connect_interrupt_channel(self) -> None:
235
232
  # Create a new L2CAP connection - interrupt channel
236
233
  try:
237
- self.l2cap_intr_channel = await self.device.l2cap_channel_manager.connect(
234
+ channel = await self.device.l2cap_channel_manager.connect(
238
235
  self.connection, HID_INTERRUPT_PSM
239
236
  )
237
+ channel.sink = self.on_intr_pdu
238
+ self.l2cap_intr_channel = channel
240
239
  except ProtocolError:
241
240
  logging.exception(f'L2CAP connection failed.')
242
241
  raise
243
242
 
244
- assert self.l2cap_intr_channel is not None
245
- # Become a sink for the L2CAP channel
246
- self.l2cap_intr_channel.sink = self.on_intr_pdu
247
-
248
243
  async def disconnect_interrupt_channel(self) -> None:
249
244
  if self.l2cap_intr_channel is None:
250
245
  raise InvalidStateError('invalid state')
@@ -334,17 +329,18 @@ class Device(HID):
334
329
  ERR_INVALID_PARAMETER = 0x04
335
330
  SUCCESS = 0xFF
336
331
 
332
+ @dataclass
337
333
  class GetSetStatus:
338
- def __init__(self) -> None:
339
- self.data = bytearray()
340
- self.status = 0
334
+ data: bytes = b''
335
+ status: int = 0
336
+
337
+ get_report_cb: Optional[Callable[[int, int, int], GetSetStatus]] = None
338
+ set_report_cb: Optional[Callable[[int, int, int, bytes], GetSetStatus]] = None
339
+ get_protocol_cb: Optional[Callable[[], GetSetStatus]] = None
340
+ set_protocol_cb: Optional[Callable[[int], GetSetStatus]] = None
341
341
 
342
342
  def __init__(self, device: device.Device) -> None:
343
343
  super().__init__(device, HID.Role.DEVICE)
344
- get_report_cb: Optional[Callable[[int, int, int], None]] = None
345
- set_report_cb: Optional[Callable[[int, int, int, bytes], None]] = None
346
- get_protocol_cb: Optional[Callable[[], None]] = None
347
- set_protocol_cb: Optional[Callable[[int], None]] = None
348
344
 
349
345
  @override
350
346
  def on_ctrl_pdu(self, pdu: bytes) -> None:
@@ -410,7 +406,6 @@ class Device(HID):
410
406
  buffer_size = 0
411
407
 
412
408
  ret = self.get_report_cb(report_id, report_type, buffer_size)
413
- assert ret is not None
414
409
  if ret.status == self.GetSetReturn.FAILURE:
415
410
  self.send_handshake_message(Message.Handshake.ERR_UNKNOWN)
416
411
  elif ret.status == self.GetSetReturn.SUCCESS:
@@ -428,7 +423,9 @@ class Device(HID):
428
423
  elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST:
429
424
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
430
425
 
431
- def register_get_report_cb(self, cb: Callable[[int, int, int], None]) -> None:
426
+ def register_get_report_cb(
427
+ self, cb: Callable[[int, int, int], Device.GetSetStatus]
428
+ ) -> None:
432
429
  self.get_report_cb = cb
433
430
  logger.debug("GetReport callback registered successfully")
434
431
 
@@ -442,7 +439,6 @@ class Device(HID):
442
439
  report_data = pdu[2:]
443
440
  report_size = len(report_data) + 1
444
441
  ret = self.set_report_cb(report_id, report_type, report_size, report_data)
445
- assert ret is not None
446
442
  if ret.status == self.GetSetReturn.SUCCESS:
447
443
  self.send_handshake_message(Message.Handshake.SUCCESSFUL)
448
444
  elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER:
@@ -453,7 +449,7 @@ class Device(HID):
453
449
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
454
450
 
455
451
  def register_set_report_cb(
456
- self, cb: Callable[[int, int, int, bytes], None]
452
+ self, cb: Callable[[int, int, int, bytes], Device.GetSetStatus]
457
453
  ) -> None:
458
454
  self.set_report_cb = cb
459
455
  logger.debug("SetReport callback registered successfully")
@@ -464,13 +460,12 @@ class Device(HID):
464
460
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
465
461
  return
466
462
  ret = self.get_protocol_cb()
467
- assert ret is not None
468
463
  if ret.status == self.GetSetReturn.SUCCESS:
469
464
  self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data)
470
465
  else:
471
466
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
472
467
 
473
- def register_get_protocol_cb(self, cb: Callable[[], None]) -> None:
468
+ def register_get_protocol_cb(self, cb: Callable[[], Device.GetSetStatus]) -> None:
474
469
  self.get_protocol_cb = cb
475
470
  logger.debug("GetProtocol callback registered successfully")
476
471
 
@@ -480,13 +475,14 @@ class Device(HID):
480
475
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
481
476
  return
482
477
  ret = self.set_protocol_cb(pdu[0] & 0x01)
483
- assert ret is not None
484
478
  if ret.status == self.GetSetReturn.SUCCESS:
485
479
  self.send_handshake_message(Message.Handshake.SUCCESSFUL)
486
480
  else:
487
481
  self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
488
482
 
489
- def register_set_protocol_cb(self, cb: Callable[[int], None]) -> None:
483
+ def register_set_protocol_cb(
484
+ self, cb: Callable[[int], Device.GetSetStatus]
485
+ ) -> None:
490
486
  self.set_protocol_cb = cb
491
487
  logger.debug("SetProtocol callback registered successfully")
492
488
 
bumble/host.py CHANGED
@@ -772,6 +772,8 @@ class Host(AbortableEventEmitter):
772
772
  event.connection_handle,
773
773
  BT_LE_TRANSPORT,
774
774
  event.peer_address,
775
+ getattr(event, 'local_resolvable_private_address', None),
776
+ getattr(event, 'peer_resolvable_private_address', None),
775
777
  event.role,
776
778
  connection_parameters,
777
779
  )
@@ -787,6 +789,10 @@ class Host(AbortableEventEmitter):
787
789
  # Just use the same implementation as for the non-enhanced event for now
788
790
  self.on_hci_le_connection_complete_event(event)
789
791
 
792
+ def on_hci_le_enhanced_connection_complete_v2_event(self, event):
793
+ # Just use the same implementation as for the v1 event for now
794
+ self.on_hci_le_enhanced_connection_complete_event(event)
795
+
790
796
  def on_hci_connection_complete_event(self, event):
791
797
  if event.status == hci.HCI_SUCCESS:
792
798
  # Create/update the connection
@@ -813,6 +819,8 @@ class Host(AbortableEventEmitter):
813
819
  event.bd_addr,
814
820
  None,
815
821
  None,
822
+ None,
823
+ None,
816
824
  )
817
825
  else:
818
826
  logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
@@ -905,6 +913,27 @@ class Host(AbortableEventEmitter):
905
913
  event.num_completed_extended_advertising_events,
906
914
  )
907
915
 
916
+ def on_hci_le_periodic_advertising_sync_established_event(self, event):
917
+ self.emit(
918
+ 'periodic_advertising_sync_establishment',
919
+ event.status,
920
+ event.sync_handle,
921
+ event.advertising_sid,
922
+ event.advertiser_address,
923
+ event.advertiser_phy,
924
+ event.periodic_advertising_interval,
925
+ event.advertiser_clock_accuracy,
926
+ )
927
+
928
+ def on_hci_le_periodic_advertising_sync_lost_event(self, event):
929
+ self.emit('periodic_advertising_sync_loss', event.sync_handle)
930
+
931
+ def on_hci_le_periodic_advertising_report_event(self, event):
932
+ self.emit('periodic_advertising_report', event.sync_handle, event)
933
+
934
+ def on_hci_le_biginfo_advertising_report_event(self, event):
935
+ self.emit('biginfo_advertising_report', event.sync_handle, event)
936
+
908
937
  def on_hci_le_cis_request_event(self, event):
909
938
  self.emit(
910
939
  'cis_request',
bumble/l2cap.py CHANGED
@@ -41,7 +41,14 @@ from typing import (
41
41
 
42
42
  from .utils import deprecated
43
43
  from .colors import color
44
- from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError
44
+ from .core import (
45
+ BT_CENTRAL_ROLE,
46
+ InvalidStateError,
47
+ InvalidArgumentError,
48
+ InvalidPacketError,
49
+ OutOfResourcesError,
50
+ ProtocolError,
51
+ )
45
52
  from .hci import (
46
53
  HCI_LE_Connection_Update_Command,
47
54
  HCI_Object,
@@ -189,17 +196,17 @@ class LeCreditBasedChannelSpec:
189
196
  self.max_credits < 1
190
197
  or self.max_credits > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS
191
198
  ):
192
- raise ValueError('max credits out of range')
199
+ raise InvalidArgumentError('max credits out of range')
193
200
  if (
194
201
  self.mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU
195
202
  or self.mtu > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU
196
203
  ):
197
- raise ValueError('MTU out of range')
204
+ raise InvalidArgumentError('MTU out of range')
198
205
  if (
199
206
  self.mps < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS
200
207
  or self.mps > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS
201
208
  ):
202
- raise ValueError('MPS out of range')
209
+ raise InvalidArgumentError('MPS out of range')
203
210
 
204
211
 
205
212
  class L2CAP_PDU:
@@ -211,7 +218,7 @@ class L2CAP_PDU:
211
218
  def from_bytes(data: bytes) -> L2CAP_PDU:
212
219
  # Check parameters
213
220
  if len(data) < 4:
214
- raise ValueError('not enough data for L2CAP header')
221
+ raise InvalidPacketError('not enough data for L2CAP header')
215
222
 
216
223
  _, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
217
224
  l2cap_pdu_payload = data[4:]
@@ -816,7 +823,7 @@ class ClassicChannel(EventEmitter):
816
823
 
817
824
  # Check that we can start a new connection
818
825
  if self.connection_result:
819
- raise RuntimeError('connection already pending')
826
+ raise InvalidStateError('connection already pending')
820
827
 
821
828
  self._change_state(self.State.WAIT_CONNECT_RSP)
822
829
  self.send_control_frame(
@@ -1129,7 +1136,7 @@ class LeCreditBasedChannel(EventEmitter):
1129
1136
  # Check that we can start a new connection
1130
1137
  identifier = self.manager.next_identifier(self.connection)
1131
1138
  if identifier in self.manager.le_coc_requests:
1132
- raise RuntimeError('too many concurrent connection requests')
1139
+ raise InvalidStateError('too many concurrent connection requests')
1133
1140
 
1134
1141
  self._change_state(self.State.CONNECTING)
1135
1142
  request = L2CAP_LE_Credit_Based_Connection_Request(
@@ -1516,7 +1523,7 @@ class ChannelManager:
1516
1523
  if cid not in channels:
1517
1524
  return cid
1518
1525
 
1519
- raise RuntimeError('no free CID available')
1526
+ raise OutOfResourcesError('no free CID available')
1520
1527
 
1521
1528
  @staticmethod
1522
1529
  def find_free_le_cid(channels: Iterable[int]) -> int:
@@ -1529,7 +1536,7 @@ class ChannelManager:
1529
1536
  if cid not in channels:
1530
1537
  return cid
1531
1538
 
1532
- raise RuntimeError('no free CID')
1539
+ raise OutOfResourcesError('no free CID')
1533
1540
 
1534
1541
  def next_identifier(self, connection: Connection) -> int:
1535
1542
  identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
@@ -1576,15 +1583,15 @@ class ChannelManager:
1576
1583
  else:
1577
1584
  # Check that the PSM isn't already in use
1578
1585
  if spec.psm in self.servers:
1579
- raise ValueError('PSM already in use')
1586
+ raise InvalidArgumentError('PSM already in use')
1580
1587
 
1581
1588
  # Check that the PSM is valid
1582
1589
  if spec.psm % 2 == 0:
1583
- raise ValueError('invalid PSM (not odd)')
1590
+ raise InvalidArgumentError('invalid PSM (not odd)')
1584
1591
  check = spec.psm >> 8
1585
1592
  while check:
1586
1593
  if check % 2 != 0:
1587
- raise ValueError('invalid PSM')
1594
+ raise InvalidArgumentError('invalid PSM')
1588
1595
  check >>= 8
1589
1596
 
1590
1597
  self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu)
@@ -1626,7 +1633,7 @@ class ChannelManager:
1626
1633
  else:
1627
1634
  # Check that the PSM isn't already in use
1628
1635
  if spec.psm in self.le_coc_servers:
1629
- raise ValueError('PSM already in use')
1636
+ raise InvalidArgumentError('PSM already in use')
1630
1637
 
1631
1638
  self.le_coc_servers[spec.psm] = LeCreditBasedChannelServer(
1632
1639
  self,
@@ -2154,10 +2161,10 @@ class ChannelManager:
2154
2161
  connection_channels = self.channels.setdefault(connection.handle, {})
2155
2162
  source_cid = self.find_free_le_cid(connection_channels)
2156
2163
  if source_cid is None: # Should never happen!
2157
- raise RuntimeError('all CIDs already in use')
2164
+ raise OutOfResourcesError('all CIDs already in use')
2158
2165
 
2159
2166
  if spec.psm is None:
2160
- raise ValueError('PSM cannot be None')
2167
+ raise InvalidArgumentError('PSM cannot be None')
2161
2168
 
2162
2169
  # Create the channel
2163
2170
  logger.debug(f'creating coc channel with cid={source_cid} for psm {spec.psm}')
@@ -2206,10 +2213,10 @@ class ChannelManager:
2206
2213
  connection_channels = self.channels.setdefault(connection.handle, {})
2207
2214
  source_cid = self.find_free_br_edr_cid(connection_channels)
2208
2215
  if source_cid is None: # Should never happen!
2209
- raise RuntimeError('all CIDs already in use')
2216
+ raise OutOfResourcesError('all CIDs already in use')
2210
2217
 
2211
2218
  if spec.psm is None:
2212
- raise ValueError('PSM cannot be None')
2219
+ raise InvalidArgumentError('PSM cannot be None')
2213
2220
 
2214
2221
  # Create the channel
2215
2222
  logger.debug(
bumble/link.py CHANGED
@@ -19,7 +19,12 @@ import logging
19
19
  import asyncio
20
20
  from functools import partial
21
21
 
22
- from bumble.core import BT_PERIPHERAL_ROLE, BT_BR_EDR_TRANSPORT, BT_LE_TRANSPORT
22
+ from bumble.core import (
23
+ BT_PERIPHERAL_ROLE,
24
+ BT_BR_EDR_TRANSPORT,
25
+ BT_LE_TRANSPORT,
26
+ InvalidStateError,
27
+ )
23
28
  from bumble.colors import color
24
29
  from bumble.hci import (
25
30
  Address,
@@ -405,12 +410,12 @@ class RemoteLink:
405
410
 
406
411
  def add_controller(self, controller):
407
412
  if self.controller:
408
- raise ValueError('controller already set')
413
+ raise InvalidStateError('controller already set')
409
414
  self.controller = controller
410
415
 
411
416
  def remove_controller(self, controller):
412
417
  if self.controller != controller:
413
- raise ValueError('controller mismatch')
418
+ raise InvalidStateError('controller mismatch')
414
419
  self.controller = None
415
420
 
416
421
  def get_pending_connection(self):
bumble/pandora/host.py CHANGED
@@ -28,6 +28,7 @@ from bumble.core import (
28
28
  BT_PERIPHERAL_ROLE,
29
29
  UUID,
30
30
  AdvertisingData,
31
+ Appearance,
31
32
  ConnectionError,
32
33
  )
33
34
  from bumble.device import (
@@ -988,8 +989,8 @@ class HostService(HostServicer):
988
989
  dt.random_target_addresses.extend(
989
990
  [data[i * 6 :: i * 6 + 6] for i in range(int(len(data) / 6))]
990
991
  )
991
- if i := cast(int, ad.get(AdvertisingData.APPEARANCE)):
992
- dt.appearance = i
992
+ if appearance := cast(Appearance, ad.get(AdvertisingData.APPEARANCE)):
993
+ dt.appearance = int(appearance)
993
994
  if i := cast(int, ad.get(AdvertisingData.ADVERTISING_INTERVAL)):
994
995
  dt.advertising_interval = i
995
996
  if s := cast(str, ad.get(AdvertisingData.URI)):