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.
- bumble/_version.py +16 -3
- bumble/a2dp.py +15 -16
- bumble/apps/auracast.py +14 -38
- bumble/apps/bench.py +10 -15
- bumble/apps/ble_rpa_tool.py +1 -0
- bumble/apps/console.py +22 -25
- bumble/apps/controller_info.py +20 -25
- bumble/apps/controller_loopback.py +6 -10
- bumble/apps/controllers.py +2 -3
- bumble/apps/device_info.py +4 -5
- bumble/apps/gatt_dump.py +3 -3
- bumble/apps/gg_bridge.py +7 -8
- bumble/apps/hci_bridge.py +4 -3
- bumble/apps/l2cap_bridge.py +5 -5
- bumble/apps/lea_unicast/app.py +16 -26
- bumble/apps/pair.py +30 -43
- bumble/apps/pandora_server.py +5 -4
- bumble/apps/player/player.py +20 -24
- bumble/apps/rfcomm_bridge.py +4 -10
- bumble/apps/scan.py +17 -8
- bumble/apps/show.py +4 -5
- bumble/apps/speaker/speaker.py +23 -27
- bumble/apps/unbond.py +3 -3
- bumble/apps/usb_probe.py +2 -4
- bumble/att.py +241 -246
- bumble/audio/io.py +5 -9
- bumble/avc.py +2 -2
- bumble/avctp.py +6 -7
- bumble/avdtp.py +19 -22
- bumble/avrcp.py +1097 -589
- bumble/codecs.py +2 -0
- bumble/controller.py +142 -35
- bumble/core.py +567 -248
- bumble/crypto/__init__.py +2 -2
- bumble/crypto/builtin.py +1 -1
- bumble/crypto/cryptography.py +2 -4
- bumble/data_types.py +1025 -0
- bumble/device.py +319 -267
- bumble/drivers/__init__.py +3 -2
- bumble/drivers/intel.py +3 -4
- bumble/drivers/rtk.py +26 -9
- bumble/gap.py +4 -4
- bumble/gatt.py +3 -2
- bumble/gatt_adapters.py +3 -11
- bumble/gatt_client.py +69 -81
- bumble/gatt_server.py +124 -124
- bumble/hci.py +114 -18
- bumble/helpers.py +19 -26
- bumble/hfp.py +10 -21
- bumble/hid.py +22 -16
- bumble/host.py +191 -103
- bumble/keys.py +5 -3
- bumble/l2cap.py +138 -104
- bumble/link.py +18 -19
- bumble/logging.py +65 -0
- bumble/pairing.py +7 -6
- bumble/pandora/__init__.py +9 -8
- bumble/pandora/config.py +3 -1
- bumble/pandora/device.py +3 -2
- bumble/pandora/host.py +38 -36
- bumble/pandora/l2cap.py +22 -21
- bumble/pandora/security.py +15 -15
- bumble/pandora/utils.py +5 -3
- bumble/profiles/aics.py +11 -11
- bumble/profiles/ams.py +403 -0
- bumble/profiles/ancs.py +6 -7
- bumble/profiles/ascs.py +14 -9
- bumble/profiles/asha.py +8 -12
- bumble/profiles/bap.py +11 -23
- bumble/profiles/bass.py +2 -7
- bumble/profiles/battery_service.py +3 -4
- bumble/profiles/cap.py +1 -2
- bumble/profiles/csip.py +2 -6
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +4 -4
- bumble/profiles/gatt_service.py +1 -4
- bumble/profiles/gmap.py +5 -5
- bumble/profiles/hap.py +62 -59
- bumble/profiles/heart_rate_service.py +5 -4
- bumble/profiles/le_audio.py +3 -1
- bumble/profiles/mcp.py +3 -7
- bumble/profiles/pacs.py +3 -6
- bumble/profiles/pbp.py +2 -0
- bumble/profiles/tmap.py +2 -3
- bumble/profiles/vcs.py +2 -8
- bumble/profiles/vocs.py +8 -8
- bumble/rfcomm.py +11 -14
- bumble/rtp.py +1 -0
- bumble/sdp.py +10 -8
- bumble/smp.py +151 -159
- bumble/snoop.py +5 -5
- bumble/tools/generate_company_id_list.py +1 -0
- bumble/tools/intel_fw_download.py +3 -3
- bumble/tools/intel_util.py +5 -4
- bumble/tools/rtk_fw_download.py +6 -3
- bumble/tools/rtk_util.py +26 -8
- bumble/transport/__init__.py +19 -15
- bumble/transport/android_emulator.py +8 -13
- bumble/transport/android_netsim.py +19 -18
- bumble/transport/common.py +12 -15
- bumble/transport/file.py +1 -1
- bumble/transport/hci_socket.py +4 -6
- bumble/transport/pty.py +5 -6
- bumble/transport/pyusb.py +7 -10
- bumble/transport/serial.py +2 -1
- bumble/transport/tcp_client.py +2 -2
- bumble/transport/tcp_server.py +11 -14
- bumble/transport/udp.py +3 -3
- bumble/transport/unix.py +67 -1
- bumble/transport/usb.py +6 -6
- bumble/transport/vhci.py +0 -1
- bumble/transport/ws_client.py +2 -1
- bumble/transport/ws_server.py +3 -2
- bumble/utils.py +20 -5
- bumble/vendor/android/hci.py +1 -2
- bumble/vendor/zephyr/hci.py +0 -1
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/METADATA +4 -2
- bumble-0.0.215.dist-info/RECORD +183 -0
- bumble-0.0.213.dist-info/RECORD +0 -180
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/WHEEL +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
28
|
-
Callable,
|
|
28
|
+
TYPE_CHECKING,
|
|
29
29
|
Any,
|
|
30
|
-
|
|
30
|
+
Callable,
|
|
31
|
+
ClassVar,
|
|
31
32
|
Iterable,
|
|
33
|
+
Optional,
|
|
32
34
|
SupportsBytes,
|
|
33
35
|
TypeVar,
|
|
34
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
236
|
-
if length != len(
|
|
240
|
+
frame.payload = pdu[4:]
|
|
241
|
+
if length != len(frame.payload):
|
|
237
242
|
logger.warning(
|
|
238
243
|
color(
|
|
239
|
-
f'!!! length mismatch: expected {
|
|
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
|
|
292
|
-
if self.
|
|
293
|
-
self.
|
|
294
|
-
return self.
|
|
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
|
-
@
|
|
297
|
-
def
|
|
298
|
-
self.
|
|
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.
|
|
303
|
-
+ self.
|
|
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.
|
|
312
|
-
result += f': {self.
|
|
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
|
|
1604
|
-
logger.
|
|
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
|
|
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
|
|
2101
|
-
logger.
|
|
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
|
|
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,
|
|
161
|
+
self, initiating_address, target_address, disconnect_command
|
|
163
162
|
):
|
|
164
163
|
# Find the controller that initiated the disconnection
|
|
165
|
-
if not (
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
174
|
+
initiating_controller.on_link_disconnection_complete(
|
|
176
175
|
disconnect_command, HCI_SUCCESS
|
|
177
176
|
)
|
|
178
177
|
|
|
179
|
-
def disconnect(self,
|
|
178
|
+
def disconnect(self, initiating_address, target_address, disconnect_command):
|
|
180
179
|
logger.debug(
|
|
181
|
-
f'$$$ DISCONNECTION {
|
|
182
|
-
f'{
|
|
180
|
+
f'$$$ DISCONNECTION {initiating_address} -> '
|
|
181
|
+
f'{target_address}: reason = {disconnect_command.reason}'
|
|
183
182
|
)
|
|
184
|
-
args = [
|
|
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
|
-
|
|
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
|
# -----------------------------------------------------------------------------
|
bumble/pandora/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|