bumble 0.0.212__py3-none-any.whl → 0.0.214__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 +2 -2
- bumble/a2dp.py +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +14 -11
- bumble/apps/bench.py +482 -37
- bumble/apps/console.py +3 -3
- bumble/apps/controller_info.py +44 -12
- bumble/apps/controller_loopback.py +7 -7
- bumble/apps/controllers.py +4 -5
- bumble/apps/device_info.py +4 -5
- bumble/apps/gatt_dump.py +5 -5
- bumble/apps/gg_bridge.py +5 -5
- bumble/apps/hci_bridge.py +5 -4
- bumble/apps/l2cap_bridge.py +5 -5
- bumble/apps/lea_unicast/app.py +8 -3
- bumble/apps/pair.py +19 -11
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +2 -3
- bumble/apps/rfcomm_bridge.py +3 -4
- bumble/apps/scan.py +4 -5
- bumble/apps/show.py +6 -4
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +123 -19
- bumble/apps/unbond.py +2 -3
- bumble/apps/usb_probe.py +2 -3
- bumble/at.py +4 -4
- bumble/att.py +2 -6
- bumble/avc.py +7 -7
- bumble/avctp.py +3 -3
- bumble/avdtp.py +16 -20
- bumble/avrcp.py +42 -54
- bumble/colors.py +2 -2
- bumble/controller.py +174 -45
- bumble/device.py +398 -182
- bumble/drivers/__init__.py +2 -2
- bumble/drivers/common.py +0 -2
- bumble/drivers/intel.py +37 -40
- bumble/drivers/rtk.py +28 -35
- bumble/gatt.py +4 -4
- bumble/gatt_adapters.py +4 -5
- bumble/gatt_client.py +26 -31
- bumble/gatt_server.py +7 -11
- bumble/hci.py +2648 -2909
- bumble/helpers.py +4 -5
- bumble/hfp.py +32 -37
- bumble/host.py +104 -35
- bumble/keys.py +5 -5
- bumble/l2cap.py +312 -409
- bumble/link.py +16 -280
- bumble/logging.py +65 -0
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +2 -2
- bumble/pandora/config.py +2 -2
- bumble/pandora/device.py +6 -6
- bumble/pandora/host.py +27 -28
- bumble/pandora/l2cap.py +2 -2
- bumble/pandora/security.py +6 -6
- bumble/pandora/utils.py +3 -3
- bumble/profiles/ams.py +404 -0
- bumble/profiles/ascs.py +142 -131
- bumble/profiles/asha.py +2 -2
- bumble/profiles/bap.py +3 -4
- bumble/profiles/csip.py +2 -2
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +2 -2
- bumble/profiles/hap.py +34 -33
- bumble/profiles/le_audio.py +4 -4
- bumble/profiles/mcp.py +4 -4
- bumble/profiles/vcs.py +3 -5
- bumble/rfcomm.py +10 -10
- bumble/rtp.py +1 -2
- bumble/sdp.py +2 -2
- bumble/smp.py +62 -63
- bumble/tools/intel_util.py +3 -2
- bumble/tools/rtk_util.py +6 -5
- bumble/transport/__init__.py +2 -16
- bumble/transport/android_netsim.py +5 -5
- bumble/transport/common.py +4 -4
- bumble/transport/pyusb.py +2 -2
- bumble/utils.py +2 -5
- bumble/vendor/android/hci.py +118 -200
- bumble/vendor/zephyr/hci.py +32 -27
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/METADATA +4 -3
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -1
- bumble/apps/link_relay/__init__.py +0 -0
- bumble/apps/link_relay/link_relay.py +0 -289
- bumble/apps/link_relay/logging.yml +0 -21
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/top_level.txt +0 -0
bumble/helpers.py
CHANGED
|
@@ -34,9 +34,8 @@ from bumble.att import ATT_CID, ATT_PDU
|
|
|
34
34
|
from bumble.smp import SMP_CID, SMP_Command
|
|
35
35
|
from bumble.core import name_or_number
|
|
36
36
|
from bumble.l2cap import (
|
|
37
|
+
CommandCode,
|
|
37
38
|
L2CAP_PDU,
|
|
38
|
-
L2CAP_CONNECTION_REQUEST,
|
|
39
|
-
L2CAP_CONNECTION_RESPONSE,
|
|
40
39
|
L2CAP_SIGNALING_CID,
|
|
41
40
|
L2CAP_LE_SIGNALING_CID,
|
|
42
41
|
L2CAP_Control_Frame,
|
|
@@ -106,14 +105,14 @@ class PacketTracer:
|
|
|
106
105
|
self.analyzer.emit(control_frame)
|
|
107
106
|
|
|
108
107
|
# Check if this signals a new channel
|
|
109
|
-
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
|
108
|
+
if control_frame.code == CommandCode.L2CAP_CONNECTION_REQUEST:
|
|
110
109
|
connection_request = cast(L2CAP_Connection_Request, control_frame)
|
|
111
110
|
self.psms[connection_request.source_cid] = connection_request.psm
|
|
112
|
-
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
|
111
|
+
elif control_frame.code == CommandCode.L2CAP_CONNECTION_RESPONSE:
|
|
113
112
|
connection_response = cast(L2CAP_Connection_Response, control_frame)
|
|
114
113
|
if (
|
|
115
114
|
connection_response.result
|
|
116
|
-
== L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
|
|
115
|
+
== L2CAP_Connection_Response.Result.CONNECTION_SUCCESSFUL
|
|
117
116
|
):
|
|
118
117
|
if self.peer and (
|
|
119
118
|
psm := self.peer.psms.get(connection_response.source_cid)
|
bumble/hfp.py
CHANGED
|
@@ -26,14 +26,9 @@ import enum
|
|
|
26
26
|
import traceback
|
|
27
27
|
import re
|
|
28
28
|
from typing import (
|
|
29
|
-
Dict,
|
|
30
|
-
List,
|
|
31
29
|
Union,
|
|
32
|
-
Set,
|
|
33
30
|
Any,
|
|
34
31
|
Optional,
|
|
35
|
-
Type,
|
|
36
|
-
Tuple,
|
|
37
32
|
ClassVar,
|
|
38
33
|
Iterable,
|
|
39
34
|
TYPE_CHECKING,
|
|
@@ -375,7 +370,7 @@ class CallLineIdentification:
|
|
|
375
370
|
cli_validity: Optional[int] = None
|
|
376
371
|
|
|
377
372
|
@classmethod
|
|
378
|
-
def parse_from(cls
|
|
373
|
+
def parse_from(cls, parameters: list[bytes]) -> Self:
|
|
379
374
|
return cls(
|
|
380
375
|
number=parameters[0].decode(),
|
|
381
376
|
type=int(parameters[1]),
|
|
@@ -505,9 +500,9 @@ STATUS_CODES = {
|
|
|
505
500
|
|
|
506
501
|
@dataclasses.dataclass
|
|
507
502
|
class HfConfiguration:
|
|
508
|
-
supported_hf_features:
|
|
509
|
-
supported_hf_indicators:
|
|
510
|
-
supported_audio_codecs:
|
|
503
|
+
supported_hf_features: list[HfFeature]
|
|
504
|
+
supported_hf_indicators: list[HfIndicator]
|
|
505
|
+
supported_audio_codecs: list[AudioCodec]
|
|
511
506
|
|
|
512
507
|
|
|
513
508
|
@dataclasses.dataclass
|
|
@@ -535,7 +530,7 @@ class AtResponse:
|
|
|
535
530
|
parameters: list
|
|
536
531
|
|
|
537
532
|
@classmethod
|
|
538
|
-
def parse_from(cls:
|
|
533
|
+
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
|
|
539
534
|
code_and_parameters = buffer.split(b':')
|
|
540
535
|
parameters = (
|
|
541
536
|
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
|
@@ -563,7 +558,7 @@ class AtCommand:
|
|
|
563
558
|
)
|
|
564
559
|
|
|
565
560
|
@classmethod
|
|
566
|
-
def parse_from(cls:
|
|
561
|
+
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
|
|
567
562
|
if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
|
|
568
563
|
if buffer.startswith(b'ATA'):
|
|
569
564
|
return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
|
|
@@ -598,7 +593,7 @@ class AgIndicatorState:
|
|
|
598
593
|
"""
|
|
599
594
|
|
|
600
595
|
indicator: AgIndicator
|
|
601
|
-
supported_values:
|
|
596
|
+
supported_values: set[int]
|
|
602
597
|
current_status: int
|
|
603
598
|
index: Optional[int] = None
|
|
604
599
|
enabled: bool = True
|
|
@@ -616,14 +611,14 @@ class AgIndicatorState:
|
|
|
616
611
|
return f'(\"{self.indicator.value}\",{supported_values_text})'
|
|
617
612
|
|
|
618
613
|
@classmethod
|
|
619
|
-
def call(cls:
|
|
614
|
+
def call(cls: type[Self]) -> Self:
|
|
620
615
|
"""Default call indicator state."""
|
|
621
616
|
return cls(
|
|
622
617
|
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
|
623
618
|
)
|
|
624
619
|
|
|
625
620
|
@classmethod
|
|
626
|
-
def callsetup(cls:
|
|
621
|
+
def callsetup(cls: type[Self]) -> Self:
|
|
627
622
|
"""Default callsetup indicator state."""
|
|
628
623
|
return cls(
|
|
629
624
|
indicator=AgIndicator.CALL_SETUP,
|
|
@@ -632,7 +627,7 @@ class AgIndicatorState:
|
|
|
632
627
|
)
|
|
633
628
|
|
|
634
629
|
@classmethod
|
|
635
|
-
def callheld(cls:
|
|
630
|
+
def callheld(cls: type[Self]) -> Self:
|
|
636
631
|
"""Default call indicator state."""
|
|
637
632
|
return cls(
|
|
638
633
|
indicator=AgIndicator.CALL_HELD,
|
|
@@ -641,14 +636,14 @@ class AgIndicatorState:
|
|
|
641
636
|
)
|
|
642
637
|
|
|
643
638
|
@classmethod
|
|
644
|
-
def service(cls:
|
|
639
|
+
def service(cls: type[Self]) -> Self:
|
|
645
640
|
"""Default service indicator state."""
|
|
646
641
|
return cls(
|
|
647
642
|
indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
|
|
648
643
|
)
|
|
649
644
|
|
|
650
645
|
@classmethod
|
|
651
|
-
def signal(cls:
|
|
646
|
+
def signal(cls: type[Self]) -> Self:
|
|
652
647
|
"""Default signal indicator state."""
|
|
653
648
|
return cls(
|
|
654
649
|
indicator=AgIndicator.SIGNAL,
|
|
@@ -657,14 +652,14 @@ class AgIndicatorState:
|
|
|
657
652
|
)
|
|
658
653
|
|
|
659
654
|
@classmethod
|
|
660
|
-
def roam(cls:
|
|
655
|
+
def roam(cls: type[Self]) -> Self:
|
|
661
656
|
"""Default roam indicator state."""
|
|
662
657
|
return cls(
|
|
663
658
|
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
|
664
659
|
)
|
|
665
660
|
|
|
666
661
|
@classmethod
|
|
667
|
-
def battchg(cls:
|
|
662
|
+
def battchg(cls: type[Self]) -> Self:
|
|
668
663
|
"""Default battery charge indicator state."""
|
|
669
664
|
return cls(
|
|
670
665
|
indicator=AgIndicator.BATTERY_CHARGE,
|
|
@@ -732,13 +727,13 @@ class HfProtocol(utils.EventEmitter):
|
|
|
732
727
|
"""Termination signal for run() loop."""
|
|
733
728
|
|
|
734
729
|
supported_hf_features: int
|
|
735
|
-
supported_audio_codecs:
|
|
730
|
+
supported_audio_codecs: list[AudioCodec]
|
|
736
731
|
|
|
737
732
|
supported_ag_features: int
|
|
738
|
-
supported_ag_call_hold_operations:
|
|
733
|
+
supported_ag_call_hold_operations: list[CallHoldOperation]
|
|
739
734
|
|
|
740
|
-
ag_indicators:
|
|
741
|
-
hf_indicators:
|
|
735
|
+
ag_indicators: list[AgIndicatorState]
|
|
736
|
+
hf_indicators: dict[HfIndicator, HfIndicatorState]
|
|
742
737
|
|
|
743
738
|
dlc: rfcomm.DLC
|
|
744
739
|
command_lock: asyncio.Lock
|
|
@@ -836,7 +831,7 @@ class HfProtocol(utils.EventEmitter):
|
|
|
836
831
|
cmd: str,
|
|
837
832
|
timeout: float = 1.0,
|
|
838
833
|
response_type: AtResponseType = AtResponseType.NONE,
|
|
839
|
-
) -> Union[None, AtResponse,
|
|
834
|
+
) -> Union[None, AtResponse, list[AtResponse]]:
|
|
840
835
|
"""
|
|
841
836
|
Sends an AT command and wait for the peer response.
|
|
842
837
|
Wait for the AT responses sent by the peer, to the status code.
|
|
@@ -853,7 +848,7 @@ class HfProtocol(utils.EventEmitter):
|
|
|
853
848
|
async with self.command_lock:
|
|
854
849
|
logger.debug(f">>> {cmd}")
|
|
855
850
|
self.dlc.write(cmd + '\r')
|
|
856
|
-
responses:
|
|
851
|
+
responses: list[AtResponse] = []
|
|
857
852
|
|
|
858
853
|
while True:
|
|
859
854
|
result = await asyncio.wait_for(
|
|
@@ -1073,7 +1068,7 @@ class HfProtocol(utils.EventEmitter):
|
|
|
1073
1068
|
# code, with the value indicating (call=0).
|
|
1074
1069
|
await self.execute_command("AT+CHUP")
|
|
1075
1070
|
|
|
1076
|
-
async def query_current_calls(self) ->
|
|
1071
|
+
async def query_current_calls(self) -> list[CallInfo]:
|
|
1077
1072
|
"""4.32.1 Query List of Current Calls in AG.
|
|
1078
1073
|
|
|
1079
1074
|
Return:
|
|
@@ -1204,27 +1199,27 @@ class AgProtocol(utils.EventEmitter):
|
|
|
1204
1199
|
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
|
1205
1200
|
|
|
1206
1201
|
supported_hf_features: int
|
|
1207
|
-
supported_hf_indicators:
|
|
1208
|
-
supported_audio_codecs:
|
|
1202
|
+
supported_hf_indicators: set[HfIndicator]
|
|
1203
|
+
supported_audio_codecs: list[AudioCodec]
|
|
1209
1204
|
|
|
1210
1205
|
supported_ag_features: int
|
|
1211
|
-
supported_ag_call_hold_operations:
|
|
1206
|
+
supported_ag_call_hold_operations: list[CallHoldOperation]
|
|
1212
1207
|
|
|
1213
|
-
ag_indicators:
|
|
1208
|
+
ag_indicators: list[AgIndicatorState]
|
|
1214
1209
|
hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
|
|
1215
1210
|
|
|
1216
1211
|
dlc: rfcomm.DLC
|
|
1217
1212
|
|
|
1218
1213
|
read_buffer: bytearray
|
|
1219
1214
|
active_codec: AudioCodec
|
|
1220
|
-
calls:
|
|
1215
|
+
calls: list[CallInfo]
|
|
1221
1216
|
|
|
1222
1217
|
indicator_report_enabled: bool
|
|
1223
1218
|
inband_ringtone_enabled: bool
|
|
1224
1219
|
cme_error_enabled: bool
|
|
1225
1220
|
cli_notification_enabled: bool
|
|
1226
1221
|
call_waiting_enabled: bool
|
|
1227
|
-
_remained_slc_setup_features:
|
|
1222
|
+
_remained_slc_setup_features: set[HfFeature]
|
|
1228
1223
|
|
|
1229
1224
|
def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
|
|
1230
1225
|
super().__init__()
|
|
@@ -1694,7 +1689,7 @@ def make_hf_sdp_records(
|
|
|
1694
1689
|
rfcomm_channel: int,
|
|
1695
1690
|
configuration: HfConfiguration,
|
|
1696
1691
|
version: ProfileVersion = ProfileVersion.V1_8,
|
|
1697
|
-
) ->
|
|
1692
|
+
) -> list[sdp.ServiceAttribute]:
|
|
1698
1693
|
"""
|
|
1699
1694
|
Generates the SDP record for HFP Hands-Free support.
|
|
1700
1695
|
|
|
@@ -1780,7 +1775,7 @@ def make_ag_sdp_records(
|
|
|
1780
1775
|
rfcomm_channel: int,
|
|
1781
1776
|
configuration: AgConfiguration,
|
|
1782
1777
|
version: ProfileVersion = ProfileVersion.V1_8,
|
|
1783
|
-
) ->
|
|
1778
|
+
) -> list[sdp.ServiceAttribute]:
|
|
1784
1779
|
"""
|
|
1785
1780
|
Generates the SDP record for HFP Audio-Gateway support.
|
|
1786
1781
|
|
|
@@ -1860,7 +1855,7 @@ def make_ag_sdp_records(
|
|
|
1860
1855
|
|
|
1861
1856
|
async def find_hf_sdp_record(
|
|
1862
1857
|
connection: device.Connection,
|
|
1863
|
-
) -> Optional[
|
|
1858
|
+
) -> Optional[tuple[int, ProfileVersion, HfSdpFeature]]:
|
|
1864
1859
|
"""Searches a Hands-Free SDP record from remote device.
|
|
1865
1860
|
|
|
1866
1861
|
Args:
|
|
@@ -1912,7 +1907,7 @@ async def find_hf_sdp_record(
|
|
|
1912
1907
|
|
|
1913
1908
|
async def find_ag_sdp_record(
|
|
1914
1909
|
connection: device.Connection,
|
|
1915
|
-
) -> Optional[
|
|
1910
|
+
) -> Optional[tuple[int, ProfileVersion, AgSdpFeature]]:
|
|
1916
1911
|
"""Searches an Audio-Gateway SDP record from remote device.
|
|
1917
1912
|
|
|
1918
1913
|
Args:
|
|
@@ -2010,7 +2005,7 @@ class EscoParameters:
|
|
|
2010
2005
|
transmit_codec_frame_size: int = 60
|
|
2011
2006
|
receive_codec_frame_size: int = 60
|
|
2012
2007
|
|
|
2013
|
-
def asdict(self) ->
|
|
2008
|
+
def asdict(self) -> dict[str, Any]:
|
|
2014
2009
|
# dataclasses.asdict() will recursively deep-copy the entire object,
|
|
2015
2010
|
# which is expensive and breaks CodingFormat object, so let it simply copy here.
|
|
2016
2011
|
return self.__dict__
|
bumble/host.py
CHANGED
|
@@ -26,10 +26,7 @@ from typing import (
|
|
|
26
26
|
Any,
|
|
27
27
|
Awaitable,
|
|
28
28
|
Callable,
|
|
29
|
-
Deque,
|
|
30
|
-
Dict,
|
|
31
29
|
Optional,
|
|
32
|
-
Set,
|
|
33
30
|
cast,
|
|
34
31
|
TYPE_CHECKING,
|
|
35
32
|
)
|
|
@@ -41,7 +38,6 @@ from bumble.snoop import Snooper
|
|
|
41
38
|
from bumble import drivers
|
|
42
39
|
from bumble import hci
|
|
43
40
|
from bumble.core import (
|
|
44
|
-
PhysicalTransport,
|
|
45
41
|
PhysicalTransport,
|
|
46
42
|
ConnectionPHY,
|
|
47
43
|
ConnectionParameters,
|
|
@@ -75,6 +71,11 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
75
71
|
|
|
76
72
|
max_packet_size: int
|
|
77
73
|
|
|
74
|
+
class PerConnectionState:
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self.in_flight = 0
|
|
77
|
+
self.drained = asyncio.Event()
|
|
78
|
+
|
|
78
79
|
def __init__(
|
|
79
80
|
self,
|
|
80
81
|
max_packet_size: int,
|
|
@@ -85,11 +86,16 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
85
86
|
self.max_packet_size = max_packet_size
|
|
86
87
|
self.max_in_flight = max_in_flight
|
|
87
88
|
self._in_flight = 0 # Total number of packets in flight across all connections
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
)
|
|
89
|
+
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
|
90
|
+
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
|
91
|
+
)
|
|
92
|
+
self._drained_per_connection: dict[int, asyncio.Event] = (
|
|
93
|
+
collections.defaultdict(asyncio.Event)
|
|
94
|
+
)
|
|
91
95
|
self._send = send
|
|
92
|
-
self._packets:
|
|
96
|
+
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
|
97
|
+
collections.deque()
|
|
98
|
+
)
|
|
93
99
|
self._queued = 0
|
|
94
100
|
self._completed = 0
|
|
95
101
|
|
|
@@ -137,36 +143,40 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
137
143
|
self._completed += flushed_count
|
|
138
144
|
self._packets = collections.deque(packets_to_keep)
|
|
139
145
|
|
|
140
|
-
if
|
|
141
|
-
in_flight =
|
|
146
|
+
if connection_state := self._connection_state.pop(connection_handle, None):
|
|
147
|
+
in_flight = connection_state.in_flight
|
|
142
148
|
self._completed += in_flight
|
|
143
149
|
self._in_flight -= in_flight
|
|
144
|
-
|
|
150
|
+
connection_state.drained.set()
|
|
145
151
|
|
|
146
152
|
def _check_queue(self) -> None:
|
|
147
153
|
while self._packets and self._in_flight < self.max_in_flight:
|
|
148
154
|
packet, connection_handle = self._packets.pop()
|
|
149
155
|
self._send(packet)
|
|
150
156
|
self._in_flight += 1
|
|
151
|
-
self.
|
|
157
|
+
connection_state = self._connection_state[connection_handle]
|
|
158
|
+
connection_state.in_flight += 1
|
|
159
|
+
connection_state.drained.clear()
|
|
152
160
|
|
|
153
161
|
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
|
154
162
|
"""Mark one or more packets associated with a connection as completed."""
|
|
155
|
-
if connection_handle not in self.
|
|
163
|
+
if connection_handle not in self._connection_state:
|
|
156
164
|
logger.warning(
|
|
157
165
|
f'received completion for unknown connection {connection_handle}'
|
|
158
166
|
)
|
|
159
167
|
return
|
|
160
168
|
|
|
161
|
-
|
|
162
|
-
if packet_count <=
|
|
163
|
-
|
|
169
|
+
connection_state = self._connection_state[connection_handle]
|
|
170
|
+
if packet_count <= connection_state.in_flight:
|
|
171
|
+
connection_state.in_flight -= packet_count
|
|
164
172
|
else:
|
|
165
173
|
logger.warning(
|
|
166
174
|
f'{packet_count} completed for {connection_handle} '
|
|
167
|
-
f'but only {
|
|
175
|
+
f'but only {connection_state.in_flight} in flight'
|
|
168
176
|
)
|
|
169
|
-
|
|
177
|
+
connection_state.in_flight = 0
|
|
178
|
+
if connection_state.in_flight == 0:
|
|
179
|
+
connection_state.drained.set()
|
|
170
180
|
|
|
171
181
|
if packet_count <= self._in_flight:
|
|
172
182
|
self._in_flight -= packet_count
|
|
@@ -181,6 +191,13 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
181
191
|
self._check_queue()
|
|
182
192
|
self.emit('flow')
|
|
183
193
|
|
|
194
|
+
async def drain(self, connection_handle: int) -> None:
|
|
195
|
+
"""Wait until there are no pending packets for a connection."""
|
|
196
|
+
if not (connection_state := self._connection_state.get(connection_handle)):
|
|
197
|
+
raise ValueError('no such connection')
|
|
198
|
+
|
|
199
|
+
await connection_state.drained.wait()
|
|
200
|
+
|
|
184
201
|
|
|
185
202
|
# -----------------------------------------------------------------------------
|
|
186
203
|
class Connection:
|
|
@@ -234,16 +251,16 @@ class IsoLink:
|
|
|
234
251
|
|
|
235
252
|
# -----------------------------------------------------------------------------
|
|
236
253
|
class Host(utils.EventEmitter):
|
|
237
|
-
connections:
|
|
238
|
-
cis_links:
|
|
239
|
-
bis_links:
|
|
240
|
-
sco_links:
|
|
254
|
+
connections: dict[int, Connection]
|
|
255
|
+
cis_links: dict[int, IsoLink]
|
|
256
|
+
bis_links: dict[int, IsoLink]
|
|
257
|
+
sco_links: dict[int, ScoLink]
|
|
241
258
|
bigs: dict[int, set[int]]
|
|
242
259
|
acl_packet_queue: Optional[DataPacketQueue] = None
|
|
243
260
|
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
|
244
261
|
iso_packet_queue: Optional[DataPacketQueue] = None
|
|
245
262
|
hci_sink: Optional[TransportSink] = None
|
|
246
|
-
hci_metadata:
|
|
263
|
+
hci_metadata: dict[str, Any]
|
|
247
264
|
long_term_key_provider: Optional[
|
|
248
265
|
Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
|
|
249
266
|
]
|
|
@@ -813,7 +830,7 @@ class Host(utils.EventEmitter):
|
|
|
813
830
|
) != 0
|
|
814
831
|
|
|
815
832
|
@property
|
|
816
|
-
def supported_commands(self) ->
|
|
833
|
+
def supported_commands(self) -> set[int]:
|
|
817
834
|
return set(
|
|
818
835
|
op_code
|
|
819
836
|
for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
|
|
@@ -836,8 +853,8 @@ class Host(utils.EventEmitter):
|
|
|
836
853
|
def on_packet(self, packet: bytes) -> None:
|
|
837
854
|
try:
|
|
838
855
|
hci_packet = hci.HCI_Packet.from_bytes(packet)
|
|
839
|
-
except Exception
|
|
840
|
-
logger.
|
|
856
|
+
except Exception:
|
|
857
|
+
logger.exception('!!! error parsing packet from bytes')
|
|
841
858
|
return
|
|
842
859
|
|
|
843
860
|
if self.ready or (
|
|
@@ -1127,11 +1144,19 @@ class Host(utils.EventEmitter):
|
|
|
1127
1144
|
else:
|
|
1128
1145
|
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
|
1129
1146
|
|
|
1130
|
-
def on_hci_le_advertising_report_event(
|
|
1147
|
+
def on_hci_le_advertising_report_event(
|
|
1148
|
+
self,
|
|
1149
|
+
event: (
|
|
1150
|
+
hci.HCI_LE_Advertising_Report_Event
|
|
1151
|
+
| hci.HCI_LE_Extended_Advertising_Report_Event
|
|
1152
|
+
),
|
|
1153
|
+
):
|
|
1131
1154
|
for report in event.reports:
|
|
1132
1155
|
self.emit('advertising_report', report)
|
|
1133
1156
|
|
|
1134
|
-
def on_hci_le_extended_advertising_report_event(
|
|
1157
|
+
def on_hci_le_extended_advertising_report_event(
|
|
1158
|
+
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
|
1159
|
+
):
|
|
1135
1160
|
self.on_hci_le_advertising_report_event(event)
|
|
1136
1161
|
|
|
1137
1162
|
def on_hci_le_advertising_set_terminated_event(self, event):
|
|
@@ -1262,7 +1287,24 @@ class Host(utils.EventEmitter):
|
|
|
1262
1287
|
self.cis_links[event.connection_handle] = IsoLink(
|
|
1263
1288
|
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
|
1264
1289
|
)
|
|
1265
|
-
self.emit(
|
|
1290
|
+
self.emit(
|
|
1291
|
+
'cis_establishment',
|
|
1292
|
+
event.connection_handle,
|
|
1293
|
+
event.cig_sync_delay,
|
|
1294
|
+
event.cis_sync_delay,
|
|
1295
|
+
event.transport_latency_c_to_p,
|
|
1296
|
+
event.transport_latency_p_to_c,
|
|
1297
|
+
event.phy_c_to_p,
|
|
1298
|
+
event.phy_p_to_c,
|
|
1299
|
+
event.nse,
|
|
1300
|
+
event.bn_c_to_p,
|
|
1301
|
+
event.bn_p_to_c,
|
|
1302
|
+
event.ft_c_to_p,
|
|
1303
|
+
event.ft_p_to_c,
|
|
1304
|
+
event.max_pdu_c_to_p,
|
|
1305
|
+
event.max_pdu_p_to_c,
|
|
1306
|
+
event.iso_interval,
|
|
1307
|
+
)
|
|
1266
1308
|
else:
|
|
1267
1309
|
self.emit(
|
|
1268
1310
|
'cis_establishment_failure', event.connection_handle, event.status
|
|
@@ -1350,6 +1392,15 @@ class Host(utils.EventEmitter):
|
|
|
1350
1392
|
def on_hci_synchronous_connection_changed_event(self, event):
|
|
1351
1393
|
pass
|
|
1352
1394
|
|
|
1395
|
+
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
|
1396
|
+
self.emit(
|
|
1397
|
+
'mode_change',
|
|
1398
|
+
event.connection_handle,
|
|
1399
|
+
event.status,
|
|
1400
|
+
event.current_mode,
|
|
1401
|
+
event.interval,
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1353
1404
|
def on_hci_role_change_event(self, event):
|
|
1354
1405
|
if event.status == hci.HCI_SUCCESS:
|
|
1355
1406
|
logger.debug(
|
|
@@ -1365,6 +1416,10 @@ class Host(utils.EventEmitter):
|
|
|
1365
1416
|
self.emit('role_change_failure', event.bd_addr, event.status)
|
|
1366
1417
|
|
|
1367
1418
|
def on_hci_le_data_length_change_event(self, event):
|
|
1419
|
+
if (connection := self.connections.get(event.connection_handle)) is None:
|
|
1420
|
+
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
|
1421
|
+
return
|
|
1422
|
+
|
|
1368
1423
|
self.emit(
|
|
1369
1424
|
'connection_data_length_change',
|
|
1370
1425
|
event.connection_handle,
|
|
@@ -1385,7 +1440,7 @@ class Host(utils.EventEmitter):
|
|
|
1385
1440
|
event.status,
|
|
1386
1441
|
)
|
|
1387
1442
|
|
|
1388
|
-
def on_hci_encryption_change_event(self, event):
|
|
1443
|
+
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
|
|
1389
1444
|
# Notify the client
|
|
1390
1445
|
if event.status == hci.HCI_SUCCESS:
|
|
1391
1446
|
self.emit(
|
|
@@ -1399,7 +1454,9 @@ class Host(utils.EventEmitter):
|
|
|
1399
1454
|
'connection_encryption_failure', event.connection_handle, event.status
|
|
1400
1455
|
)
|
|
1401
1456
|
|
|
1402
|
-
def on_hci_encryption_change_v2_event(
|
|
1457
|
+
def on_hci_encryption_change_v2_event(
|
|
1458
|
+
self, event: hci.HCI_Encryption_Change_V2_Event
|
|
1459
|
+
):
|
|
1403
1460
|
# Notify the client
|
|
1404
1461
|
if event.status == hci.HCI_SUCCESS:
|
|
1405
1462
|
self.emit(
|
|
@@ -1520,13 +1577,15 @@ class Host(utils.EventEmitter):
|
|
|
1520
1577
|
self.emit('inquiry_complete')
|
|
1521
1578
|
|
|
1522
1579
|
def on_hci_inquiry_result_with_rssi_event(self, event):
|
|
1523
|
-
for
|
|
1580
|
+
for bd_addr, class_of_device, rssi in zip(
|
|
1581
|
+
event.bd_addr, event.class_of_device, event.rssi
|
|
1582
|
+
):
|
|
1524
1583
|
self.emit(
|
|
1525
1584
|
'inquiry_result',
|
|
1526
|
-
|
|
1527
|
-
|
|
1585
|
+
bd_addr,
|
|
1586
|
+
class_of_device,
|
|
1528
1587
|
b'',
|
|
1529
|
-
|
|
1588
|
+
rssi,
|
|
1530
1589
|
)
|
|
1531
1590
|
|
|
1532
1591
|
def on_hci_extended_inquiry_result_event(self, event):
|
|
@@ -1586,5 +1645,15 @@ class Host(utils.EventEmitter):
|
|
|
1586
1645
|
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
|
1587
1646
|
self.emit('cs_subevent_result_continue', event)
|
|
1588
1647
|
|
|
1648
|
+
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
|
1649
|
+
self.emit(
|
|
1650
|
+
'le_subrate_change',
|
|
1651
|
+
event.connection_handle,
|
|
1652
|
+
event.subrate_factor,
|
|
1653
|
+
event.peripheral_latency,
|
|
1654
|
+
event.continuation_number,
|
|
1655
|
+
event.supervision_timeout,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1589
1658
|
def on_hci_vendor_event(self, event):
|
|
1590
1659
|
self.emit('vendor_event', event)
|
bumble/keys.py
CHANGED
|
@@ -26,7 +26,7 @@ import dataclasses
|
|
|
26
26
|
import logging
|
|
27
27
|
import os
|
|
28
28
|
import json
|
|
29
|
-
from typing import TYPE_CHECKING,
|
|
29
|
+
from typing import TYPE_CHECKING, Optional, Any
|
|
30
30
|
from typing_extensions import Self
|
|
31
31
|
|
|
32
32
|
from bumble.colors import color
|
|
@@ -157,7 +157,7 @@ class KeyStore:
|
|
|
157
157
|
async def get(self, _name: str) -> Optional[PairingKeys]:
|
|
158
158
|
return None
|
|
159
159
|
|
|
160
|
-
async def get_all(self) ->
|
|
160
|
+
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
|
161
161
|
return []
|
|
162
162
|
|
|
163
163
|
async def delete_all(self) -> None:
|
|
@@ -272,7 +272,7 @@ class JsonKeyStore(KeyStore):
|
|
|
272
272
|
|
|
273
273
|
@classmethod
|
|
274
274
|
def from_device(
|
|
275
|
-
cls:
|
|
275
|
+
cls: type[Self], device: Device, filename: Optional[str] = None
|
|
276
276
|
) -> Self:
|
|
277
277
|
if not filename:
|
|
278
278
|
# Extract the filename from the config if there is one
|
|
@@ -356,7 +356,7 @@ class JsonKeyStore(KeyStore):
|
|
|
356
356
|
|
|
357
357
|
# -----------------------------------------------------------------------------
|
|
358
358
|
class MemoryKeyStore(KeyStore):
|
|
359
|
-
all_keys:
|
|
359
|
+
all_keys: dict[str, PairingKeys]
|
|
360
360
|
|
|
361
361
|
def __init__(self) -> None:
|
|
362
362
|
self.all_keys = {}
|
|
@@ -371,5 +371,5 @@ class MemoryKeyStore(KeyStore):
|
|
|
371
371
|
async def get(self, name: str) -> Optional[PairingKeys]:
|
|
372
372
|
return self.all_keys.get(name)
|
|
373
373
|
|
|
374
|
-
async def get_all(self) ->
|
|
374
|
+
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
|
375
375
|
return list(self.all_keys.items())
|