bumble 0.0.213__py3-none-any.whl → 0.0.215__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 (123) hide show
  1. bumble/_version.py +16 -3
  2. bumble/a2dp.py +15 -16
  3. bumble/apps/auracast.py +14 -38
  4. bumble/apps/bench.py +10 -15
  5. bumble/apps/ble_rpa_tool.py +1 -0
  6. bumble/apps/console.py +22 -25
  7. bumble/apps/controller_info.py +20 -25
  8. bumble/apps/controller_loopback.py +6 -10
  9. bumble/apps/controllers.py +2 -3
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +3 -3
  12. bumble/apps/gg_bridge.py +7 -8
  13. bumble/apps/hci_bridge.py +4 -3
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +16 -26
  16. bumble/apps/pair.py +30 -43
  17. bumble/apps/pandora_server.py +5 -4
  18. bumble/apps/player/player.py +20 -24
  19. bumble/apps/rfcomm_bridge.py +4 -10
  20. bumble/apps/scan.py +17 -8
  21. bumble/apps/show.py +4 -5
  22. bumble/apps/speaker/speaker.py +23 -27
  23. bumble/apps/unbond.py +3 -3
  24. bumble/apps/usb_probe.py +2 -4
  25. bumble/att.py +241 -246
  26. bumble/audio/io.py +5 -9
  27. bumble/avc.py +2 -2
  28. bumble/avctp.py +6 -7
  29. bumble/avdtp.py +19 -22
  30. bumble/avrcp.py +1097 -589
  31. bumble/codecs.py +2 -0
  32. bumble/controller.py +142 -35
  33. bumble/core.py +567 -248
  34. bumble/crypto/__init__.py +2 -2
  35. bumble/crypto/builtin.py +1 -1
  36. bumble/crypto/cryptography.py +2 -4
  37. bumble/data_types.py +1025 -0
  38. bumble/device.py +319 -267
  39. bumble/drivers/__init__.py +3 -2
  40. bumble/drivers/intel.py +3 -4
  41. bumble/drivers/rtk.py +26 -9
  42. bumble/gap.py +4 -4
  43. bumble/gatt.py +3 -2
  44. bumble/gatt_adapters.py +3 -11
  45. bumble/gatt_client.py +69 -81
  46. bumble/gatt_server.py +124 -124
  47. bumble/hci.py +114 -18
  48. bumble/helpers.py +19 -26
  49. bumble/hfp.py +10 -21
  50. bumble/hid.py +22 -16
  51. bumble/host.py +191 -103
  52. bumble/keys.py +5 -3
  53. bumble/l2cap.py +138 -104
  54. bumble/link.py +18 -19
  55. bumble/logging.py +65 -0
  56. bumble/pairing.py +7 -6
  57. bumble/pandora/__init__.py +9 -8
  58. bumble/pandora/config.py +3 -1
  59. bumble/pandora/device.py +3 -2
  60. bumble/pandora/host.py +38 -36
  61. bumble/pandora/l2cap.py +22 -21
  62. bumble/pandora/security.py +15 -15
  63. bumble/pandora/utils.py +5 -3
  64. bumble/profiles/aics.py +11 -11
  65. bumble/profiles/ams.py +403 -0
  66. bumble/profiles/ancs.py +6 -7
  67. bumble/profiles/ascs.py +14 -9
  68. bumble/profiles/asha.py +8 -12
  69. bumble/profiles/bap.py +11 -23
  70. bumble/profiles/bass.py +2 -7
  71. bumble/profiles/battery_service.py +3 -4
  72. bumble/profiles/cap.py +1 -2
  73. bumble/profiles/csip.py +2 -6
  74. bumble/profiles/device_information_service.py +2 -2
  75. bumble/profiles/gap.py +4 -4
  76. bumble/profiles/gatt_service.py +1 -4
  77. bumble/profiles/gmap.py +5 -5
  78. bumble/profiles/hap.py +62 -59
  79. bumble/profiles/heart_rate_service.py +5 -4
  80. bumble/profiles/le_audio.py +3 -1
  81. bumble/profiles/mcp.py +3 -7
  82. bumble/profiles/pacs.py +3 -6
  83. bumble/profiles/pbp.py +2 -0
  84. bumble/profiles/tmap.py +2 -3
  85. bumble/profiles/vcs.py +2 -8
  86. bumble/profiles/vocs.py +8 -8
  87. bumble/rfcomm.py +11 -14
  88. bumble/rtp.py +1 -0
  89. bumble/sdp.py +10 -8
  90. bumble/smp.py +151 -159
  91. bumble/snoop.py +5 -5
  92. bumble/tools/generate_company_id_list.py +1 -0
  93. bumble/tools/intel_fw_download.py +3 -3
  94. bumble/tools/intel_util.py +5 -4
  95. bumble/tools/rtk_fw_download.py +6 -3
  96. bumble/tools/rtk_util.py +26 -8
  97. bumble/transport/__init__.py +19 -15
  98. bumble/transport/android_emulator.py +8 -13
  99. bumble/transport/android_netsim.py +19 -18
  100. bumble/transport/common.py +12 -15
  101. bumble/transport/file.py +1 -1
  102. bumble/transport/hci_socket.py +4 -6
  103. bumble/transport/pty.py +5 -6
  104. bumble/transport/pyusb.py +7 -10
  105. bumble/transport/serial.py +2 -1
  106. bumble/transport/tcp_client.py +2 -2
  107. bumble/transport/tcp_server.py +11 -14
  108. bumble/transport/udp.py +3 -3
  109. bumble/transport/unix.py +67 -1
  110. bumble/transport/usb.py +6 -6
  111. bumble/transport/vhci.py +0 -1
  112. bumble/transport/ws_client.py +2 -1
  113. bumble/transport/ws_server.py +3 -2
  114. bumble/utils.py +20 -5
  115. bumble/vendor/android/hci.py +1 -2
  116. bumble/vendor/zephyr/hci.py +0 -1
  117. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/METADATA +4 -2
  118. bumble-0.0.215.dist-info/RECORD +183 -0
  119. bumble-0.0.213.dist-info/RECORD +0 -180
  120. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/WHEEL +0 -0
  121. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/entry_points.txt +0 -0
  122. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/licenses/LICENSE +0 -0
  123. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/top_level.txt +0 -0
bumble/l2cap.py CHANGED
@@ -16,32 +16,32 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
+
19
20
  import asyncio
20
21
  import dataclasses
21
22
  import enum
22
23
  import logging
23
24
  import struct
24
-
25
25
  from collections import deque
26
+ from collections.abc import Sequence
26
27
  from typing import (
27
- Optional,
28
- Callable,
28
+ TYPE_CHECKING,
29
29
  Any,
30
- Union,
30
+ Callable,
31
+ ClassVar,
31
32
  Iterable,
33
+ Optional,
32
34
  SupportsBytes,
33
35
  TypeVar,
34
- ClassVar,
35
- TYPE_CHECKING,
36
+ Union,
36
37
  )
37
38
 
38
- from bumble import utils
39
- from bumble import hci
39
+ from bumble import hci, utils
40
40
  from bumble.colors import color
41
41
  from bumble.core import (
42
- InvalidStateError,
43
42
  InvalidArgumentError,
44
43
  InvalidPacketError,
44
+ InvalidStateError,
45
45
  OutOfResourcesError,
46
46
  ProtocolError,
47
47
  )
@@ -112,6 +112,10 @@ class CommandCode(hci.SpecableEnum):
112
112
  L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14
113
113
  L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15
114
114
  L2CAP_LE_FLOW_CONTROL_CREDIT = 0x16
115
+ L2CAP_CREDIT_BASED_CONNECTION_REQUEST = 0x17
116
+ L2CAP_CREDIT_BASED_CONNECTION_RESPONSE = 0x18
117
+ L2CAP_CREDIT_BASED_RECONFIGURE_REQUEST = 0x19
118
+ L2CAP_CREDIT_BASED_RECONFIGURE_RESPONSE = 0x1A
115
119
 
116
120
  L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT = 0x0000
117
121
  L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001
@@ -213,7 +217,7 @@ class L2CAP_Control_Frame:
213
217
  fields: ClassVar[hci.Fields] = ()
214
218
  code: int = dataclasses.field(default=0, init=False)
215
219
  name: str = dataclasses.field(default='', init=False)
216
- _data: Optional[bytes] = dataclasses.field(default=None, init=False)
220
+ _payload: Optional[bytes] = dataclasses.field(default=None, init=False)
217
221
 
218
222
  identifier: int
219
223
 
@@ -223,7 +227,8 @@ class L2CAP_Control_Frame:
223
227
 
224
228
  subclass = L2CAP_Control_Frame.classes.get(code)
225
229
  if subclass is None:
226
- instance = L2CAP_Control_Frame(pdu)
230
+ instance = L2CAP_Control_Frame(identifier=identifier)
231
+ instance.payload = pdu[4:]
227
232
  instance.code = CommandCode(code)
228
233
  instance.name = instance.code.name
229
234
  return instance
@@ -232,11 +237,11 @@ class L2CAP_Control_Frame:
232
237
  identifier=identifier,
233
238
  )
234
239
  frame.identifier = identifier
235
- frame.data = pdu[4:]
236
- if length != len(pdu):
240
+ frame.payload = pdu[4:]
241
+ if length != len(frame.payload):
237
242
  logger.warning(
238
243
  color(
239
- f'!!! length mismatch: expected {len(pdu) - 4} but got {length}',
244
+ f'!!! length mismatch: expected {length} but got {len(frame.payload)}',
240
245
  'red',
241
246
  )
242
247
  )
@@ -273,34 +278,20 @@ class L2CAP_Control_Frame:
273
278
 
274
279
  return subclass
275
280
 
276
- def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
277
- self.identifier = kwargs.get('identifier', 0)
278
- if self.fields:
279
- if kwargs:
280
- hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
281
- if pdu is None:
282
- data = hci.HCI_Object.dict_to_bytes(kwargs, self.fields)
283
- pdu = (
284
- bytes([self.code, self.identifier])
285
- + struct.pack('<H', len(data))
286
- + data
287
- )
288
- self.data = pdu[4:] if pdu else b''
289
-
290
281
  @property
291
- def data(self) -> bytes:
292
- if self._data is None:
293
- self._data = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
294
- return self._data
282
+ def payload(self) -> bytes:
283
+ if self._payload is None:
284
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
285
+ return self._payload
295
286
 
296
- @data.setter
297
- def data(self, parameters: bytes) -> None:
298
- self._data = parameters
287
+ @payload.setter
288
+ def payload(self, payload: bytes) -> None:
289
+ self._payload = payload
299
290
 
300
291
  def __bytes__(self) -> bytes:
301
292
  return (
302
- struct.pack('<BBH', self.code, self.identifier, len(self.data) + 4)
303
- + self.data
293
+ struct.pack('<BBH', self.code, self.identifier, len(self.payload))
294
+ + self.payload
304
295
  )
305
296
 
306
297
  def __str__(self) -> str:
@@ -308,8 +299,8 @@ class L2CAP_Control_Frame:
308
299
  if fields := getattr(self, 'fields', None):
309
300
  result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
310
301
  else:
311
- if len(self.data) > 1:
312
- result += f': {self.data.hex()}'
302
+ if len(self.payload) > 1:
303
+ result += f': {self.payload.hex()}'
313
304
  return result
314
305
 
315
306
 
@@ -608,6 +599,109 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
608
599
  credits: int = dataclasses.field(metadata=hci.metadata(2))
609
600
 
610
601
 
602
+ # -----------------------------------------------------------------------------
603
+ @L2CAP_Control_Frame.subclass
604
+ @dataclasses.dataclass
605
+ class L2CAP_Credit_Based_Connection_Request(L2CAP_Control_Frame):
606
+ '''
607
+ See Bluetooth spec @ Vol 3, Part A - 4.25 L2CAP_CREDIT_BASED_CONNECTION_REQ (0x17).
608
+ '''
609
+
610
+ @classmethod
611
+ def parse_cid_list(cls, data: bytes, offset: int) -> tuple[int, list[int]]:
612
+ count = (len(data) - offset) // 2
613
+ return len(data), list(struct.unpack_from("<" + ("H" * count), data, offset))
614
+
615
+ @classmethod
616
+ def serialize_cid_list(cls, cids: Sequence[int]) -> bytes:
617
+ return b"".join([struct.pack("<H", cid) for cid in cids])
618
+
619
+ CID_METADATA: ClassVar[dict[str, Any]] = hci.metadata(
620
+ {
621
+ 'parser': lambda data, offset: L2CAP_Credit_Based_Connection_Request.parse_cid_list(
622
+ data, offset
623
+ ),
624
+ 'serializer': lambda value: L2CAP_Credit_Based_Connection_Request.serialize_cid_list(
625
+ value
626
+ ),
627
+ }
628
+ )
629
+
630
+ spsm: int = dataclasses.field(metadata=hci.metadata(2))
631
+ mtu: int = dataclasses.field(metadata=hci.metadata(2))
632
+ mps: int = dataclasses.field(metadata=hci.metadata(2))
633
+ initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
634
+ source_cid: Sequence[int] = dataclasses.field(metadata=CID_METADATA)
635
+
636
+
637
+ # -----------------------------------------------------------------------------
638
+ @L2CAP_Control_Frame.subclass
639
+ @dataclasses.dataclass
640
+ class L2CAP_Credit_Based_Connection_Response(L2CAP_Control_Frame):
641
+ '''
642
+ See Bluetooth spec @ Vol 3, Part A - 4.26 L2CAP_CREDIT_BASED_CONNECTION_RSP (0x18).
643
+ '''
644
+
645
+ class Result(hci.SpecableEnum):
646
+ ALL_CONNECTIONS_SUCCESSFUL = 0x0000
647
+ ALL_CONNECTIONS_REFUSED_SPSM_NOT_SUPPORTED = 0x0002
648
+ SOME_CONNECTIONS_REFUSED_INSUFFICIENT_RESOURCES_AVAILABLE = 0x0004
649
+ ALL_CONNECTIONS_REFUSED_INSUFFICIENT_AUTHENTICATION = 0x0005
650
+ ALL_CONNECTIONS_REFUSED_INSUFFICIENT_AUTHORIZATION = 0x0006
651
+ ALL_CONNECTIONS_REFUSED_ENCRYPTION_KEY_SIZE_TOO_SHORT = 0x0007
652
+ ALL_CONNECTIONS_REFUSED_INSUFFICIENT_ENCRYPTION = 0x0008
653
+ SOME_CONNECTIONS_REFUSED_INVALID_SOURCE_CID = 0x0009
654
+ SOME_CONNECTIONS_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A
655
+ ALL_CONNECTIONS_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
656
+ ALL_CONNECTIONS_REFUSED_INVALID_PARAMETERS = 0x000C
657
+ ALL_CONNECTIONS_PENDING_NO_FURTHER_INFORMATION_AVAILABLE = 0x000D
658
+ ALL_CONNECTIONS_PENDING_AUTHENTICATION_PENDING = 0x000E
659
+ ALL_CONNECTIONS_PENDING_AUTHORIZATION_PENDING = 0x000F
660
+
661
+ mtu: int = dataclasses.field(metadata=hci.metadata(2))
662
+ mps: int = dataclasses.field(metadata=hci.metadata(2))
663
+ initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
664
+ result: int = dataclasses.field(metadata=Result.type_metadata(2))
665
+ destination_cid: Sequence[int] = dataclasses.field(
666
+ metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
667
+ )
668
+
669
+
670
+ # -----------------------------------------------------------------------------
671
+ @L2CAP_Control_Frame.subclass
672
+ @dataclasses.dataclass
673
+ class L2CAP_Credit_Based_Reconfigure_Request(L2CAP_Control_Frame):
674
+ '''
675
+ See Bluetooth spec @ Vol 3, Part A - 4.27 L2CAP_CREDIT_BASED_RECONFIGURE_REQ (0x19).
676
+ '''
677
+
678
+ mtu: int = dataclasses.field(metadata=hci.metadata(2))
679
+ mps: int = dataclasses.field(metadata=hci.metadata(2))
680
+ destination_cid: Sequence[int] = dataclasses.field(
681
+ metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
682
+ )
683
+
684
+
685
+ # -----------------------------------------------------------------------------
686
+ @L2CAP_Control_Frame.subclass
687
+ @dataclasses.dataclass
688
+ class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
689
+ '''
690
+ See Bluetooth spec @ Vol 3, Part A - 4.28 L2CAP_CREDIT_BASED_RECONFIGURE_RSP (0x1A).
691
+ '''
692
+
693
+ class Result(hci.SpecableEnum):
694
+ RECONFIGURATION_SUCCESSFUL = 0x0000
695
+ RECONFIGURATION_FAILED_REDUCTION_IN_SIZE_OF_MTU_NOT_ALLOWED = 0x0001
696
+ RECONFIGURATION_FAILED_REDUCTION_IN_SIZE_OF_MPS_NOT_ALLOWED_FOR_MORE_THAN_ONE_CHANNEL_AT_A_TIME = (
697
+ 0x0002
698
+ )
699
+ RECONFIGURATION_FAILED_ONE_OR_MORE_DESTINATION_CIDS_INVALID = 0x0003
700
+ RECONFIGURATION_FAILED_OTHER_UNACCEPTABLE_PARAMETERS = 0x0004
701
+
702
+ result: int = dataclasses.field(metadata=Result.type_metadata(2))
703
+
704
+
611
705
  # -----------------------------------------------------------------------------
612
706
  class ClassicChannel(utils.EventEmitter):
613
707
  class State(enum.IntEnum):
@@ -1437,16 +1531,6 @@ class ChannelManager:
1437
1531
  if cid in self.fixed_channels:
1438
1532
  del self.fixed_channels[cid]
1439
1533
 
1440
- @utils.deprecated("Please use create_classic_server")
1441
- def register_server(
1442
- self,
1443
- psm: int,
1444
- server: Callable[[ClassicChannel], Any],
1445
- ) -> int:
1446
- return self.create_classic_server(
1447
- handler=server, spec=ClassicChannelSpec(psm=psm)
1448
- ).psm
1449
-
1450
1534
  def create_classic_server(
1451
1535
  self,
1452
1536
  spec: ClassicChannelSpec,
@@ -1483,22 +1567,6 @@ class ChannelManager:
1483
1567
 
1484
1568
  return self.servers[spec.psm]
1485
1569
 
1486
- @utils.deprecated("Please use create_le_credit_based_server()")
1487
- def register_le_coc_server(
1488
- self,
1489
- psm: int,
1490
- server: Callable[[LeCreditBasedChannel], Any],
1491
- max_credits: int,
1492
- mtu: int,
1493
- mps: int,
1494
- ) -> int:
1495
- return self.create_le_credit_based_server(
1496
- spec=LeCreditBasedChannelSpec(
1497
- psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
1498
- ),
1499
- handler=server,
1500
- ).psm
1501
-
1502
1570
  def create_le_credit_based_server(
1503
1571
  self,
1504
1572
  spec: LeCreditBasedChannelSpec,
@@ -1600,8 +1668,8 @@ class ChannelManager:
1600
1668
  if handler:
1601
1669
  try:
1602
1670
  handler(connection, cid, control_frame)
1603
- except Exception as error:
1604
- logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
1671
+ except Exception:
1672
+ logger.exception(color("!!! Exception in handler:", "red"))
1605
1673
  self.send_control_frame(
1606
1674
  connection,
1607
1675
  cid,
@@ -1611,7 +1679,7 @@ class ChannelManager:
1611
1679
  data=b'',
1612
1680
  ),
1613
1681
  )
1614
- raise error
1682
+ raise
1615
1683
  else:
1616
1684
  logger.error(color('Channel Manager command not handled???', 'red'))
1617
1685
  self.send_control_frame(
@@ -2051,17 +2119,6 @@ class ChannelManager:
2051
2119
  if channel.source_cid in connection_channels:
2052
2120
  del connection_channels[channel.source_cid]
2053
2121
 
2054
- @utils.deprecated("Please use create_le_credit_based_channel()")
2055
- async def open_le_coc(
2056
- self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
2057
- ) -> LeCreditBasedChannel:
2058
- return await self.create_le_credit_based_channel(
2059
- connection=connection,
2060
- spec=LeCreditBasedChannelSpec(
2061
- psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
2062
- ),
2063
- )
2064
-
2065
2122
  async def create_le_credit_based_channel(
2066
2123
  self,
2067
2124
  connection: Connection,
@@ -2097,8 +2154,8 @@ class ChannelManager:
2097
2154
  # Connect
2098
2155
  try:
2099
2156
  await channel.connect()
2100
- except Exception as error:
2101
- logger.warning(f'connection failed: {error}')
2157
+ except Exception:
2158
+ logger.exception('connection failed')
2102
2159
  del connection_channels[source_cid]
2103
2160
  raise
2104
2161
 
@@ -2108,12 +2165,6 @@ class ChannelManager:
2108
2165
 
2109
2166
  return channel
2110
2167
 
2111
- @utils.deprecated("Please use create_classic_channel()")
2112
- async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
2113
- return await self.create_classic_channel(
2114
- connection=connection, spec=ClassicChannelSpec(psm=psm)
2115
- )
2116
-
2117
2168
  async def create_classic_channel(
2118
2169
  self, connection: Connection, spec: ClassicChannelSpec
2119
2170
  ) -> ClassicChannel:
@@ -2150,20 +2201,3 @@ class ChannelManager:
2150
2201
  raise e
2151
2202
 
2152
2203
  return channel
2153
-
2154
-
2155
- # -----------------------------------------------------------------------------
2156
- # Deprecated Classes
2157
- # -----------------------------------------------------------------------------
2158
-
2159
-
2160
- class Channel(ClassicChannel):
2161
- @utils.deprecated("Please use ClassicChannel")
2162
- def __init__(self, *args, **kwargs) -> None:
2163
- super().__init__(*args, **kwargs)
2164
-
2165
-
2166
- class LeConnectionOrientedChannel(LeCreditBasedChannel):
2167
- @utils.deprecated("Please use LeCreditBasedChannel")
2168
- def __init__(self, *args, **kwargs) -> None:
2169
- super().__init__(*args, **kwargs)
bumble/link.py CHANGED
@@ -12,25 +12,24 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import asyncio
16
+
15
17
  # -----------------------------------------------------------------------------
16
18
  # Imports
17
19
  # -----------------------------------------------------------------------------
18
20
  import logging
19
- import asyncio
21
+ from typing import Optional
20
22
 
21
- from bumble import core
23
+ from bumble import controller, core
22
24
  from bumble.hci import (
23
- Address,
24
- Role,
25
- HCI_SUCCESS,
26
25
  HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
27
- HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
28
26
  HCI_PAGE_TIMEOUT_ERROR,
27
+ HCI_SUCCESS,
28
+ HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
29
+ Address,
29
30
  HCI_Connection_Complete_Event,
31
+ Role,
30
32
  )
31
- from bumble import controller
32
-
33
- from typing import Optional
34
33
 
35
34
  # -----------------------------------------------------------------------------
36
35
  # Logging
@@ -159,29 +158,29 @@ class LocalLink:
159
158
  asyncio.get_running_loop().call_soon(self.on_connection_complete)
160
159
 
161
160
  def on_disconnection_complete(
162
- self, central_address, peripheral_address, disconnect_command
161
+ self, initiating_address, target_address, disconnect_command
163
162
  ):
164
163
  # Find the controller that initiated the disconnection
165
- if not (central_controller := self.find_controller(central_address)):
164
+ if not (initiating_controller := self.find_controller(initiating_address)):
166
165
  logger.warning('!!! Initiating controller not found')
167
166
  return
168
167
 
169
168
  # Disconnect from the first controller with a matching address
170
- if peripheral_controller := self.find_controller(peripheral_address):
171
- peripheral_controller.on_link_central_disconnected(
172
- central_address, disconnect_command.reason
169
+ if target_controller := self.find_controller(target_address):
170
+ target_controller.on_link_disconnected(
171
+ initiating_address, disconnect_command.reason
173
172
  )
174
173
 
175
- central_controller.on_link_peripheral_disconnection_complete(
174
+ initiating_controller.on_link_disconnection_complete(
176
175
  disconnect_command, HCI_SUCCESS
177
176
  )
178
177
 
179
- def disconnect(self, central_address, peripheral_address, disconnect_command):
178
+ def disconnect(self, initiating_address, target_address, disconnect_command):
180
179
  logger.debug(
181
- f'$$$ DISCONNECTION {central_address} -> '
182
- f'{peripheral_address}: reason = {disconnect_command.reason}'
180
+ f'$$$ DISCONNECTION {initiating_address} -> '
181
+ f'{target_address}: reason = {disconnect_command.reason}'
183
182
  )
184
- args = [central_address, peripheral_address, disconnect_command]
183
+ args = [initiating_address, target_address, disconnect_command]
185
184
  asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
186
185
 
187
186
  # pylint: disable=too-many-arguments
bumble/logging.py ADDED
@@ -0,0 +1,65 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ import functools
19
+ import logging
20
+ import os
21
+
22
+ from bumble import colors
23
+
24
+
25
+ # -----------------------------------------------------------------------------
26
+ class ColorFormatter(logging.Formatter):
27
+ _colorizers = {
28
+ logging.DEBUG: functools.partial(colors.color, fg="white"),
29
+ logging.INFO: functools.partial(colors.color, fg="green"),
30
+ logging.WARNING: functools.partial(colors.color, fg="yellow"),
31
+ logging.ERROR: functools.partial(colors.color, fg="red"),
32
+ logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
33
+ }
34
+
35
+ _formatters = {
36
+ level: logging.Formatter(
37
+ fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
38
+ + "{message}",
39
+ datefmt="%H:%M:%S",
40
+ style="{",
41
+ )
42
+ for level, colorizer in _colorizers.items()
43
+ }
44
+
45
+ def format(self, record: logging.LogRecord) -> str:
46
+ return self._formatters[record.levelno].format(record)
47
+
48
+
49
+ def setup_basic_logging(default_level: str = "INFO") -> None:
50
+ """
51
+ Set up basic logging with logging.basicConfig, configured with a simple formatter
52
+ that prints out the date and log level in color.
53
+ If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
54
+ is used. Otherwise the default_level argument is used.
55
+
56
+ Args:
57
+ default_level: default logging level
58
+
59
+ """
60
+ handler = logging.StreamHandler()
61
+ handler.setFormatter(ColorFormatter())
62
+ logging.basicConfig(
63
+ level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
64
+ handlers=[handler],
65
+ )
bumble/pairing.py CHANGED
@@ -16,27 +16,28 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
+
19
20
  import enum
20
- from dataclasses import dataclass
21
21
  import secrets
22
+ from dataclasses import dataclass
22
23
  from typing import Optional
23
24
 
24
25
  from bumble import hci
26
+ from bumble.core import AdvertisingData, LeRole
25
27
  from bumble.smp import (
26
- SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
27
- SMP_KEYBOARD_ONLY_IO_CAPABILITY,
28
28
  SMP_DISPLAY_ONLY_IO_CAPABILITY,
29
29
  SMP_DISPLAY_YES_NO_IO_CAPABILITY,
30
- SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
31
30
  SMP_ENC_KEY_DISTRIBUTION_FLAG,
32
31
  SMP_ID_KEY_DISTRIBUTION_FLAG,
33
- SMP_SIGN_KEY_DISTRIBUTION_FLAG,
32
+ SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
33
+ SMP_KEYBOARD_ONLY_IO_CAPABILITY,
34
34
  SMP_LINK_KEY_DISTRIBUTION_FLAG,
35
+ SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
36
+ SMP_SIGN_KEY_DISTRIBUTION_FLAG,
35
37
  OobContext,
36
38
  OobLegacyContext,
37
39
  OobSharedData,
38
40
  )
39
- from bumble.core import AdvertisingData, LeRole
40
41
 
41
42
 
42
43
  # -----------------------------------------------------------------------------
@@ -19,21 +19,22 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
19
19
 
20
20
  __version__ = "0.0.1"
21
21
 
22
+ from typing import Callable, List, Optional
23
+
22
24
  import grpc
23
25
  import grpc.aio
24
-
25
- from bumble.pandora.config import Config
26
- from bumble.pandora.device import PandoraDevice
27
- from bumble.pandora.host import HostService
28
- from bumble.pandora.l2cap import L2CAPService
29
- from bumble.pandora.security import SecurityService, SecurityStorageService
30
26
  from pandora.host_grpc_aio import add_HostServicer_to_server
31
27
  from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
32
28
  from pandora.security_grpc_aio import (
33
29
  add_SecurityServicer_to_server,
34
30
  add_SecurityStorageServicer_to_server,
35
31
  )
36
- from typing import Callable, List, Optional
32
+
33
+ from bumble.pandora.config import Config
34
+ from bumble.pandora.device import PandoraDevice
35
+ from bumble.pandora.host import HostService
36
+ from bumble.pandora.l2cap import L2CAPService
37
+ from bumble.pandora.security import SecurityService, SecurityStorageService
37
38
 
38
39
  # public symbols
39
40
  __all__ = [
@@ -49,7 +50,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
49
50
 
50
51
 
51
52
  def register_servicer_hook(
52
- hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
53
+ hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
53
54
  ) -> None:
54
55
  _SERVICERS_HOOKS.append(hook)
55
56
 
bumble/pandora/config.py CHANGED
@@ -13,10 +13,12 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from __future__ import annotations
16
- from bumble.pairing import PairingConfig, PairingDelegate
16
+
17
17
  from dataclasses import dataclass
18
18
  from typing import Any
19
19
 
20
+ from bumble.pairing import PairingConfig, PairingDelegate
21
+
20
22
 
21
23
  @dataclass
22
24
  class Config:
bumble/pandora/device.py CHANGED
@@ -15,6 +15,9 @@
15
15
  """Generic & dependency free Bumble (reference) device."""
16
16
 
17
17
  from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
18
21
  from bumble import transport
19
22
  from bumble.core import (
20
23
  BT_GENERIC_AUDIO_SERVICE,
@@ -32,8 +35,6 @@ from bumble.sdp import (
32
35
  DataElement,
33
36
  ServiceAttribute,
34
37
  )
35
- from typing import Any, Optional
36
-
37
38
 
38
39
  # Default rootcanal HCI TCP address
39
40
  ROOTCANAL_HCI_ADDRESS = "localhost:6402"