bumble 0.0.204__py3-none-any.whl → 0.0.207__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/apps/auracast.py +626 -87
- bumble/apps/bench.py +225 -147
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/gatt.py +35 -6
- bumble/gatt_client.py +14 -2
- bumble/hci.py +812 -14
- bumble/host.py +359 -63
- bumble/l2cap.py +3 -16
- bumble/profiles/aics.py +19 -38
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +10 -19
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +54 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +2 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/RECORD +37 -34
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/sdp.py
CHANGED
|
@@ -16,15 +16,21 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
|
+
import asyncio
|
|
19
20
|
import logging
|
|
20
21
|
import struct
|
|
21
|
-
from typing import
|
|
22
|
+
from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
|
|
22
23
|
from typing_extensions import Self
|
|
23
24
|
|
|
24
|
-
from
|
|
25
|
-
from .colors import color
|
|
26
|
-
from .core import
|
|
27
|
-
|
|
25
|
+
from bumble import core, l2cap
|
|
26
|
+
from bumble.colors import color
|
|
27
|
+
from bumble.core import (
|
|
28
|
+
InvalidStateError,
|
|
29
|
+
InvalidArgumentError,
|
|
30
|
+
InvalidPacketError,
|
|
31
|
+
ProtocolError,
|
|
32
|
+
)
|
|
33
|
+
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
|
28
34
|
|
|
29
35
|
if TYPE_CHECKING:
|
|
30
36
|
from .device import Device, Connection
|
|
@@ -124,7 +130,7 @@ SDP_ATTRIBUTE_ID_NAMES = {
|
|
|
124
130
|
SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
|
|
125
131
|
|
|
126
132
|
# To be used in searches where an attribute ID list allows a range to be specified
|
|
127
|
-
SDP_ALL_ATTRIBUTES_RANGE = (
|
|
133
|
+
SDP_ALL_ATTRIBUTES_RANGE = (0x0000, 0xFFFF)
|
|
128
134
|
|
|
129
135
|
# fmt: on
|
|
130
136
|
# pylint: enable=line-too-long
|
|
@@ -242,11 +248,11 @@ class DataElement:
|
|
|
242
248
|
return DataElement(DataElement.BOOLEAN, value)
|
|
243
249
|
|
|
244
250
|
@staticmethod
|
|
245
|
-
def sequence(value:
|
|
251
|
+
def sequence(value: Iterable[DataElement]) -> DataElement:
|
|
246
252
|
return DataElement(DataElement.SEQUENCE, value)
|
|
247
253
|
|
|
248
254
|
@staticmethod
|
|
249
|
-
def alternative(value:
|
|
255
|
+
def alternative(value: Iterable[DataElement]) -> DataElement:
|
|
250
256
|
return DataElement(DataElement.ALTERNATIVE, value)
|
|
251
257
|
|
|
252
258
|
@staticmethod
|
|
@@ -473,7 +479,9 @@ class ServiceAttribute:
|
|
|
473
479
|
self.value = value
|
|
474
480
|
|
|
475
481
|
@staticmethod
|
|
476
|
-
def list_from_data_elements(
|
|
482
|
+
def list_from_data_elements(
|
|
483
|
+
elements: Sequence[DataElement],
|
|
484
|
+
) -> list[ServiceAttribute]:
|
|
477
485
|
attribute_list = []
|
|
478
486
|
for i in range(0, len(elements) // 2):
|
|
479
487
|
attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
|
|
@@ -486,7 +494,7 @@ class ServiceAttribute:
|
|
|
486
494
|
|
|
487
495
|
@staticmethod
|
|
488
496
|
def find_attribute_in_list(
|
|
489
|
-
attribute_list:
|
|
497
|
+
attribute_list: Iterable[ServiceAttribute], attribute_id: int
|
|
490
498
|
) -> Optional[DataElement]:
|
|
491
499
|
return next(
|
|
492
500
|
(
|
|
@@ -534,7 +542,12 @@ class SDP_PDU:
|
|
|
534
542
|
See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT
|
|
535
543
|
'''
|
|
536
544
|
|
|
537
|
-
|
|
545
|
+
RESPONSE_PDU_IDS = {
|
|
546
|
+
SDP_SERVICE_SEARCH_REQUEST: SDP_SERVICE_SEARCH_RESPONSE,
|
|
547
|
+
SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
|
|
548
|
+
SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
|
|
549
|
+
}
|
|
550
|
+
sdp_pdu_classes: dict[int, Type[SDP_PDU]] = {}
|
|
538
551
|
name = None
|
|
539
552
|
pdu_id = 0
|
|
540
553
|
|
|
@@ -558,7 +571,7 @@ class SDP_PDU:
|
|
|
558
571
|
@staticmethod
|
|
559
572
|
def parse_service_record_handle_list_preceded_by_count(
|
|
560
573
|
data: bytes, offset: int
|
|
561
|
-
) ->
|
|
574
|
+
) -> tuple[int, list[int]]:
|
|
562
575
|
count = struct.unpack_from('>H', data, offset - 2)[0]
|
|
563
576
|
handle_list = [
|
|
564
577
|
struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)
|
|
@@ -639,6 +652,8 @@ class SDP_ErrorResponse(SDP_PDU):
|
|
|
639
652
|
See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU
|
|
640
653
|
'''
|
|
641
654
|
|
|
655
|
+
error_code: int
|
|
656
|
+
|
|
642
657
|
|
|
643
658
|
# -----------------------------------------------------------------------------
|
|
644
659
|
@SDP_PDU.subclass(
|
|
@@ -675,7 +690,7 @@ class SDP_ServiceSearchResponse(SDP_PDU):
|
|
|
675
690
|
See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU
|
|
676
691
|
'''
|
|
677
692
|
|
|
678
|
-
service_record_handle_list:
|
|
693
|
+
service_record_handle_list: list[int]
|
|
679
694
|
total_service_record_count: int
|
|
680
695
|
current_service_record_count: int
|
|
681
696
|
continuation_state: bytes
|
|
@@ -752,31 +767,99 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
|
|
752
767
|
See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU
|
|
753
768
|
'''
|
|
754
769
|
|
|
755
|
-
|
|
756
|
-
|
|
770
|
+
attribute_lists_byte_count: int
|
|
771
|
+
attribute_lists: bytes
|
|
757
772
|
continuation_state: bytes
|
|
758
773
|
|
|
759
774
|
|
|
760
775
|
# -----------------------------------------------------------------------------
|
|
761
776
|
class Client:
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def __init__(self, connection: Connection) -> None:
|
|
777
|
+
def __init__(self, connection: Connection, mtu: int = 0) -> None:
|
|
765
778
|
self.connection = connection
|
|
766
|
-
self.
|
|
767
|
-
self.
|
|
779
|
+
self.channel: Optional[l2cap.ClassicChannel] = None
|
|
780
|
+
self.mtu = mtu
|
|
781
|
+
self.request_semaphore = asyncio.Semaphore(1)
|
|
782
|
+
self.pending_request: Optional[SDP_PDU] = None
|
|
783
|
+
self.pending_response: Optional[asyncio.futures.Future[SDP_PDU]] = None
|
|
784
|
+
self.next_transaction_id = 0
|
|
768
785
|
|
|
769
786
|
async def connect(self) -> None:
|
|
770
787
|
self.channel = await self.connection.create_l2cap_channel(
|
|
771
|
-
spec=
|
|
788
|
+
spec=(
|
|
789
|
+
l2cap.ClassicChannelSpec(SDP_PSM, self.mtu)
|
|
790
|
+
if self.mtu
|
|
791
|
+
else l2cap.ClassicChannelSpec(SDP_PSM)
|
|
792
|
+
)
|
|
772
793
|
)
|
|
794
|
+
self.channel.sink = self.on_pdu
|
|
773
795
|
|
|
774
796
|
async def disconnect(self) -> None:
|
|
775
797
|
if self.channel:
|
|
776
798
|
await self.channel.disconnect()
|
|
777
799
|
self.channel = None
|
|
778
800
|
|
|
779
|
-
|
|
801
|
+
def make_transaction_id(self) -> int:
|
|
802
|
+
transaction_id = self.next_transaction_id
|
|
803
|
+
self.next_transaction_id = (self.next_transaction_id + 1) & 0xFFFF
|
|
804
|
+
return transaction_id
|
|
805
|
+
|
|
806
|
+
def on_pdu(self, pdu: bytes) -> None:
|
|
807
|
+
if not self.pending_request:
|
|
808
|
+
logger.warning('received response with no pending request')
|
|
809
|
+
return
|
|
810
|
+
assert self.pending_response is not None
|
|
811
|
+
|
|
812
|
+
response = SDP_PDU.from_bytes(pdu)
|
|
813
|
+
|
|
814
|
+
# Check that the transaction ID is what we expect
|
|
815
|
+
if self.pending_request.transaction_id != response.transaction_id:
|
|
816
|
+
logger.warning(
|
|
817
|
+
f"received response with transaction ID {response.transaction_id} "
|
|
818
|
+
f"but expected {self.pending_request.transaction_id}"
|
|
819
|
+
)
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
# Check if the response is an error
|
|
823
|
+
if isinstance(response, SDP_ErrorResponse):
|
|
824
|
+
self.pending_response.set_exception(
|
|
825
|
+
ProtocolError(error_code=response.error_code)
|
|
826
|
+
)
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
# Check that the type of the response matches the request
|
|
830
|
+
if response.pdu_id != SDP_PDU.RESPONSE_PDU_IDS.get(self.pending_request.pdu_id):
|
|
831
|
+
logger.warning("response type mismatch")
|
|
832
|
+
return
|
|
833
|
+
|
|
834
|
+
self.pending_response.set_result(response)
|
|
835
|
+
|
|
836
|
+
async def send_request(self, request: SDP_PDU) -> SDP_PDU:
|
|
837
|
+
assert self.channel is not None
|
|
838
|
+
async with self.request_semaphore:
|
|
839
|
+
assert self.pending_request is None
|
|
840
|
+
assert self.pending_response is None
|
|
841
|
+
|
|
842
|
+
# Create a future value to hold the eventual response
|
|
843
|
+
self.pending_response = asyncio.get_running_loop().create_future()
|
|
844
|
+
self.pending_request = request
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
self.channel.send_pdu(bytes(request))
|
|
848
|
+
return await self.pending_response
|
|
849
|
+
finally:
|
|
850
|
+
self.pending_request = None
|
|
851
|
+
self.pending_response = None
|
|
852
|
+
|
|
853
|
+
async def search_services(self, uuids: Iterable[core.UUID]) -> list[int]:
|
|
854
|
+
"""
|
|
855
|
+
Search for services by UUID.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
uuids: service the UUIDs to search for.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
A list of matching service record handles.
|
|
862
|
+
"""
|
|
780
863
|
if self.pending_request is not None:
|
|
781
864
|
raise InvalidStateError('request already pending')
|
|
782
865
|
if self.channel is None:
|
|
@@ -791,16 +874,16 @@ class Client:
|
|
|
791
874
|
continuation_state = bytes([0])
|
|
792
875
|
watchdog = SDP_CONTINUATION_WATCHDOG
|
|
793
876
|
while watchdog > 0:
|
|
794
|
-
|
|
877
|
+
response = await self.send_request(
|
|
795
878
|
SDP_ServiceSearchRequest(
|
|
796
|
-
transaction_id=
|
|
879
|
+
transaction_id=self.make_transaction_id(),
|
|
797
880
|
service_search_pattern=service_search_pattern,
|
|
798
881
|
maximum_service_record_count=0xFFFF,
|
|
799
882
|
continuation_state=continuation_state,
|
|
800
883
|
)
|
|
801
884
|
)
|
|
802
|
-
response = SDP_PDU.from_bytes(response_pdu)
|
|
803
885
|
logger.debug(f'<<< Response: {response}')
|
|
886
|
+
assert isinstance(response, SDP_ServiceSearchResponse)
|
|
804
887
|
service_record_handle_list += response.service_record_handle_list
|
|
805
888
|
continuation_state = response.continuation_state
|
|
806
889
|
if len(continuation_state) == 1 and continuation_state[0] == 0:
|
|
@@ -811,8 +894,21 @@ class Client:
|
|
|
811
894
|
return service_record_handle_list
|
|
812
895
|
|
|
813
896
|
async def search_attributes(
|
|
814
|
-
self,
|
|
815
|
-
|
|
897
|
+
self,
|
|
898
|
+
uuids: Iterable[core.UUID],
|
|
899
|
+
attribute_ids: Iterable[Union[int, tuple[int, int]]],
|
|
900
|
+
) -> list[list[ServiceAttribute]]:
|
|
901
|
+
"""
|
|
902
|
+
Search for attributes by UUID and attribute IDs.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
uuids: the service UUIDs to search for.
|
|
906
|
+
attribute_ids: list of attribute IDs or (start, end) attribute ID ranges.
|
|
907
|
+
(use (0, 0xFFFF) to include all attributes)
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
A list of list of attributes, one list per matching service.
|
|
911
|
+
"""
|
|
816
912
|
if self.pending_request is not None:
|
|
817
913
|
raise InvalidStateError('request already pending')
|
|
818
914
|
if self.channel is None:
|
|
@@ -824,8 +920,8 @@ class Client:
|
|
|
824
920
|
attribute_id_list = DataElement.sequence(
|
|
825
921
|
[
|
|
826
922
|
(
|
|
827
|
-
DataElement.
|
|
828
|
-
attribute_id[0]
|
|
923
|
+
DataElement.unsigned_integer_32(
|
|
924
|
+
attribute_id[0] << 16 | attribute_id[1]
|
|
829
925
|
)
|
|
830
926
|
if isinstance(attribute_id, tuple)
|
|
831
927
|
else DataElement.unsigned_integer_16(attribute_id)
|
|
@@ -839,17 +935,17 @@ class Client:
|
|
|
839
935
|
continuation_state = bytes([0])
|
|
840
936
|
watchdog = SDP_CONTINUATION_WATCHDOG
|
|
841
937
|
while watchdog > 0:
|
|
842
|
-
|
|
938
|
+
response = await self.send_request(
|
|
843
939
|
SDP_ServiceSearchAttributeRequest(
|
|
844
|
-
transaction_id=
|
|
940
|
+
transaction_id=self.make_transaction_id(),
|
|
845
941
|
service_search_pattern=service_search_pattern,
|
|
846
942
|
maximum_attribute_byte_count=0xFFFF,
|
|
847
943
|
attribute_id_list=attribute_id_list,
|
|
848
944
|
continuation_state=continuation_state,
|
|
849
945
|
)
|
|
850
946
|
)
|
|
851
|
-
response = SDP_PDU.from_bytes(response_pdu)
|
|
852
947
|
logger.debug(f'<<< Response: {response}')
|
|
948
|
+
assert isinstance(response, SDP_ServiceSearchAttributeResponse)
|
|
853
949
|
accumulator += response.attribute_lists
|
|
854
950
|
continuation_state = response.continuation_state
|
|
855
951
|
if len(continuation_state) == 1 and continuation_state[0] == 0:
|
|
@@ -872,8 +968,18 @@ class Client:
|
|
|
872
968
|
async def get_attributes(
|
|
873
969
|
self,
|
|
874
970
|
service_record_handle: int,
|
|
875
|
-
attribute_ids:
|
|
876
|
-
) ->
|
|
971
|
+
attribute_ids: Iterable[Union[int, tuple[int, int]]],
|
|
972
|
+
) -> list[ServiceAttribute]:
|
|
973
|
+
"""
|
|
974
|
+
Get attributes for a service.
|
|
975
|
+
|
|
976
|
+
Args:
|
|
977
|
+
service_record_handle: the handle for a service
|
|
978
|
+
attribute_ids: list or attribute IDs or (start, end) attribute ID handles.
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
A list of attributes.
|
|
982
|
+
"""
|
|
877
983
|
if self.pending_request is not None:
|
|
878
984
|
raise InvalidStateError('request already pending')
|
|
879
985
|
if self.channel is None:
|
|
@@ -882,8 +988,8 @@ class Client:
|
|
|
882
988
|
attribute_id_list = DataElement.sequence(
|
|
883
989
|
[
|
|
884
990
|
(
|
|
885
|
-
DataElement.
|
|
886
|
-
attribute_id[0]
|
|
991
|
+
DataElement.unsigned_integer_32(
|
|
992
|
+
attribute_id[0] << 16 | attribute_id[1]
|
|
887
993
|
)
|
|
888
994
|
if isinstance(attribute_id, tuple)
|
|
889
995
|
else DataElement.unsigned_integer_16(attribute_id)
|
|
@@ -897,17 +1003,17 @@ class Client:
|
|
|
897
1003
|
continuation_state = bytes([0])
|
|
898
1004
|
watchdog = SDP_CONTINUATION_WATCHDOG
|
|
899
1005
|
while watchdog > 0:
|
|
900
|
-
|
|
1006
|
+
response = await self.send_request(
|
|
901
1007
|
SDP_ServiceAttributeRequest(
|
|
902
|
-
transaction_id=
|
|
1008
|
+
transaction_id=self.make_transaction_id(),
|
|
903
1009
|
service_record_handle=service_record_handle,
|
|
904
1010
|
maximum_attribute_byte_count=0xFFFF,
|
|
905
1011
|
attribute_id_list=attribute_id_list,
|
|
906
1012
|
continuation_state=continuation_state,
|
|
907
1013
|
)
|
|
908
1014
|
)
|
|
909
|
-
response = SDP_PDU.from_bytes(response_pdu)
|
|
910
1015
|
logger.debug(f'<<< Response: {response}')
|
|
1016
|
+
assert isinstance(response, SDP_ServiceAttributeResponse)
|
|
911
1017
|
accumulator += response.attribute_list
|
|
912
1018
|
continuation_state = response.continuation_state
|
|
913
1019
|
if len(continuation_state) == 1 and continuation_state[0] == 0:
|
|
@@ -933,17 +1039,17 @@ class Client:
|
|
|
933
1039
|
|
|
934
1040
|
# -----------------------------------------------------------------------------
|
|
935
1041
|
class Server:
|
|
936
|
-
CONTINUATION_STATE = bytes([0x01,
|
|
1042
|
+
CONTINUATION_STATE = bytes([0x01, 0x00])
|
|
937
1043
|
channel: Optional[l2cap.ClassicChannel]
|
|
938
|
-
Service = NewType('Service',
|
|
939
|
-
service_records:
|
|
940
|
-
current_response: Union[None, bytes,
|
|
1044
|
+
Service = NewType('Service', list[ServiceAttribute])
|
|
1045
|
+
service_records: dict[int, Service]
|
|
1046
|
+
current_response: Union[None, bytes, tuple[int, list[int]]]
|
|
941
1047
|
|
|
942
1048
|
def __init__(self, device: Device) -> None:
|
|
943
1049
|
self.device = device
|
|
944
1050
|
self.service_records = {} # Service records maps, by record handle
|
|
945
1051
|
self.channel = None
|
|
946
|
-
self.current_response = None
|
|
1052
|
+
self.current_response = None # Current response data, used for continuations
|
|
947
1053
|
|
|
948
1054
|
def register(self, l2cap_channel_manager: l2cap.ChannelManager) -> None:
|
|
949
1055
|
l2cap_channel_manager.create_classic_server(
|
|
@@ -954,7 +1060,7 @@ class Server:
|
|
|
954
1060
|
logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}')
|
|
955
1061
|
self.channel.send_pdu(response)
|
|
956
1062
|
|
|
957
|
-
def match_services(self, search_pattern: DataElement) ->
|
|
1063
|
+
def match_services(self, search_pattern: DataElement) -> dict[int, Service]:
|
|
958
1064
|
# Find the services for which the attributes in the pattern is a subset of the
|
|
959
1065
|
# service's attribute values (NOTE: the value search recurses into sequences)
|
|
960
1066
|
matching_services = {}
|
|
@@ -1011,6 +1117,31 @@ class Server:
|
|
|
1011
1117
|
)
|
|
1012
1118
|
)
|
|
1013
1119
|
|
|
1120
|
+
def check_continuation(
|
|
1121
|
+
self,
|
|
1122
|
+
continuation_state: bytes,
|
|
1123
|
+
transaction_id: int,
|
|
1124
|
+
) -> Optional[bool]:
|
|
1125
|
+
# Check if this is a valid continuation
|
|
1126
|
+
if len(continuation_state) > 1:
|
|
1127
|
+
if (
|
|
1128
|
+
self.current_response is None
|
|
1129
|
+
or continuation_state != self.CONTINUATION_STATE
|
|
1130
|
+
):
|
|
1131
|
+
self.send_response(
|
|
1132
|
+
SDP_ErrorResponse(
|
|
1133
|
+
transaction_id=transaction_id,
|
|
1134
|
+
error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
|
|
1135
|
+
)
|
|
1136
|
+
)
|
|
1137
|
+
return None
|
|
1138
|
+
return True
|
|
1139
|
+
|
|
1140
|
+
# Cleanup any partial response leftover
|
|
1141
|
+
self.current_response = None
|
|
1142
|
+
|
|
1143
|
+
return False
|
|
1144
|
+
|
|
1014
1145
|
def get_next_response_payload(self, maximum_size):
|
|
1015
1146
|
if len(self.current_response) > maximum_size:
|
|
1016
1147
|
payload = self.current_response[:maximum_size]
|
|
@@ -1025,7 +1156,7 @@ class Server:
|
|
|
1025
1156
|
|
|
1026
1157
|
@staticmethod
|
|
1027
1158
|
def get_service_attributes(
|
|
1028
|
-
service: Service, attribute_ids:
|
|
1159
|
+
service: Service, attribute_ids: Iterable[DataElement]
|
|
1029
1160
|
) -> DataElement:
|
|
1030
1161
|
attributes = []
|
|
1031
1162
|
for attribute_id in attribute_ids:
|
|
@@ -1053,30 +1184,24 @@ class Server:
|
|
|
1053
1184
|
|
|
1054
1185
|
def on_sdp_service_search_request(self, request: SDP_ServiceSearchRequest) -> None:
|
|
1055
1186
|
# Check if this is a continuation
|
|
1056
|
-
if
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
)
|
|
1063
|
-
)
|
|
1064
|
-
return
|
|
1065
|
-
else:
|
|
1066
|
-
# Cleanup any partial response leftover
|
|
1067
|
-
self.current_response = None
|
|
1187
|
+
if (
|
|
1188
|
+
continuation := self.check_continuation(
|
|
1189
|
+
request.continuation_state, request.transaction_id
|
|
1190
|
+
)
|
|
1191
|
+
) is None:
|
|
1192
|
+
return
|
|
1068
1193
|
|
|
1194
|
+
if not continuation:
|
|
1069
1195
|
# Find the matching services
|
|
1070
1196
|
matching_services = self.match_services(request.service_search_pattern)
|
|
1071
1197
|
service_record_handles = list(matching_services.keys())
|
|
1198
|
+
logger.debug(f'Service Record Handles: {service_record_handles}')
|
|
1072
1199
|
|
|
1073
1200
|
# Only return up to the maximum requested
|
|
1074
1201
|
service_record_handles_subset = service_record_handles[
|
|
1075
1202
|
: request.maximum_service_record_count
|
|
1076
1203
|
]
|
|
1077
1204
|
|
|
1078
|
-
# Serialize to a byte array, and remember the total count
|
|
1079
|
-
logger.debug(f'Service Record Handles: {service_record_handles}')
|
|
1080
1205
|
self.current_response = (
|
|
1081
1206
|
len(service_record_handles),
|
|
1082
1207
|
service_record_handles_subset,
|
|
@@ -1084,15 +1209,21 @@ class Server:
|
|
|
1084
1209
|
|
|
1085
1210
|
# Respond, keeping any unsent handles for later
|
|
1086
1211
|
assert isinstance(self.current_response, tuple)
|
|
1087
|
-
|
|
1088
|
-
|
|
1212
|
+
assert self.channel is not None
|
|
1213
|
+
total_service_record_count, service_record_handles = self.current_response
|
|
1214
|
+
maximum_service_record_count = (self.channel.peer_mtu - 11) // 4
|
|
1215
|
+
service_record_handles_remaining = service_record_handles[
|
|
1216
|
+
maximum_service_record_count:
|
|
1089
1217
|
]
|
|
1218
|
+
service_record_handles = service_record_handles[:maximum_service_record_count]
|
|
1090
1219
|
self.current_response = (
|
|
1091
|
-
|
|
1092
|
-
|
|
1220
|
+
total_service_record_count,
|
|
1221
|
+
service_record_handles_remaining,
|
|
1093
1222
|
)
|
|
1094
1223
|
continuation_state = (
|
|
1095
|
-
Server.CONTINUATION_STATE
|
|
1224
|
+
Server.CONTINUATION_STATE
|
|
1225
|
+
if service_record_handles_remaining
|
|
1226
|
+
else bytes([0])
|
|
1096
1227
|
)
|
|
1097
1228
|
service_record_handle_list = b''.join(
|
|
1098
1229
|
[struct.pack('>I', handle) for handle in service_record_handles]
|
|
@@ -1100,7 +1231,7 @@ class Server:
|
|
|
1100
1231
|
self.send_response(
|
|
1101
1232
|
SDP_ServiceSearchResponse(
|
|
1102
1233
|
transaction_id=request.transaction_id,
|
|
1103
|
-
total_service_record_count=
|
|
1234
|
+
total_service_record_count=total_service_record_count,
|
|
1104
1235
|
current_service_record_count=len(service_record_handles),
|
|
1105
1236
|
service_record_handle_list=service_record_handle_list,
|
|
1106
1237
|
continuation_state=continuation_state,
|
|
@@ -1111,19 +1242,14 @@ class Server:
|
|
|
1111
1242
|
self, request: SDP_ServiceAttributeRequest
|
|
1112
1243
|
) -> None:
|
|
1113
1244
|
# Check if this is a continuation
|
|
1114
|
-
if
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
)
|
|
1121
|
-
)
|
|
1122
|
-
return
|
|
1123
|
-
else:
|
|
1124
|
-
# Cleanup any partial response leftover
|
|
1125
|
-
self.current_response = None
|
|
1245
|
+
if (
|
|
1246
|
+
continuation := self.check_continuation(
|
|
1247
|
+
request.continuation_state, request.transaction_id
|
|
1248
|
+
)
|
|
1249
|
+
) is None:
|
|
1250
|
+
return
|
|
1126
1251
|
|
|
1252
|
+
if not continuation:
|
|
1127
1253
|
# Check that the service exists
|
|
1128
1254
|
service = self.service_records.get(request.service_record_handle)
|
|
1129
1255
|
if service is None:
|
|
@@ -1145,14 +1271,18 @@ class Server:
|
|
|
1145
1271
|
self.current_response = bytes(attribute_list)
|
|
1146
1272
|
|
|
1147
1273
|
# Respond, keeping any pending chunks for later
|
|
1274
|
+
assert self.channel is not None
|
|
1275
|
+
maximum_attribute_byte_count = min(
|
|
1276
|
+
request.maximum_attribute_byte_count, self.channel.peer_mtu - 9
|
|
1277
|
+
)
|
|
1148
1278
|
attribute_list_response, continuation_state = self.get_next_response_payload(
|
|
1149
|
-
|
|
1279
|
+
maximum_attribute_byte_count
|
|
1150
1280
|
)
|
|
1151
1281
|
self.send_response(
|
|
1152
1282
|
SDP_ServiceAttributeResponse(
|
|
1153
1283
|
transaction_id=request.transaction_id,
|
|
1154
1284
|
attribute_list_byte_count=len(attribute_list_response),
|
|
1155
|
-
attribute_list=
|
|
1285
|
+
attribute_list=attribute_list_response,
|
|
1156
1286
|
continuation_state=continuation_state,
|
|
1157
1287
|
)
|
|
1158
1288
|
)
|
|
@@ -1161,18 +1291,14 @@ class Server:
|
|
|
1161
1291
|
self, request: SDP_ServiceSearchAttributeRequest
|
|
1162
1292
|
) -> None:
|
|
1163
1293
|
# Check if this is a continuation
|
|
1164
|
-
if
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
)
|
|
1171
|
-
)
|
|
1172
|
-
else:
|
|
1173
|
-
# Cleanup any partial response leftover
|
|
1174
|
-
self.current_response = None
|
|
1294
|
+
if (
|
|
1295
|
+
continuation := self.check_continuation(
|
|
1296
|
+
request.continuation_state, request.transaction_id
|
|
1297
|
+
)
|
|
1298
|
+
) is None:
|
|
1299
|
+
return
|
|
1175
1300
|
|
|
1301
|
+
if not continuation:
|
|
1176
1302
|
# Find the matching services
|
|
1177
1303
|
matching_services = self.match_services(
|
|
1178
1304
|
request.service_search_pattern
|
|
@@ -1192,14 +1318,18 @@ class Server:
|
|
|
1192
1318
|
self.current_response = bytes(attribute_lists)
|
|
1193
1319
|
|
|
1194
1320
|
# Respond, keeping any pending chunks for later
|
|
1321
|
+
assert self.channel is not None
|
|
1322
|
+
maximum_attribute_byte_count = min(
|
|
1323
|
+
request.maximum_attribute_byte_count, self.channel.peer_mtu - 9
|
|
1324
|
+
)
|
|
1195
1325
|
attribute_lists_response, continuation_state = self.get_next_response_payload(
|
|
1196
|
-
|
|
1326
|
+
maximum_attribute_byte_count
|
|
1197
1327
|
)
|
|
1198
1328
|
self.send_response(
|
|
1199
1329
|
SDP_ServiceSearchAttributeResponse(
|
|
1200
1330
|
transaction_id=request.transaction_id,
|
|
1201
1331
|
attribute_lists_byte_count=len(attribute_lists_response),
|
|
1202
|
-
attribute_lists=
|
|
1332
|
+
attribute_lists=attribute_lists_response,
|
|
1203
1333
|
continuation_state=continuation_state,
|
|
1204
1334
|
)
|
|
1205
1335
|
)
|
bumble/smp.py
CHANGED
|
@@ -1326,7 +1326,7 @@ class Session:
|
|
|
1326
1326
|
self.connection.abort_on('disconnection', self.on_pairing())
|
|
1327
1327
|
|
|
1328
1328
|
def on_connection_encryption_change(self) -> None:
|
|
1329
|
-
if self.connection.is_encrypted:
|
|
1329
|
+
if self.connection.is_encrypted and not self.completed:
|
|
1330
1330
|
if self.is_responder:
|
|
1331
1331
|
# The responder distributes its keys first, the initiator later
|
|
1332
1332
|
self.distribute_keys()
|
bumble/utils.py
CHANGED
|
@@ -447,7 +447,7 @@ def deprecated(msg: str):
|
|
|
447
447
|
def wrapper(function):
|
|
448
448
|
@functools.wraps(function)
|
|
449
449
|
def inner(*args, **kwargs):
|
|
450
|
-
warnings.warn(msg, DeprecationWarning)
|
|
450
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
451
451
|
return function(*args, **kwargs)
|
|
452
452
|
|
|
453
453
|
return inner
|
|
@@ -464,7 +464,7 @@ def experimental(msg: str):
|
|
|
464
464
|
def wrapper(function):
|
|
465
465
|
@functools.wraps(function)
|
|
466
466
|
def inner(*args, **kwargs):
|
|
467
|
-
warnings.warn(msg, FutureWarning)
|
|
467
|
+
warnings.warn(msg, FutureWarning, stacklevel=2)
|
|
468
468
|
return function(*args, **kwargs)
|
|
469
469
|
|
|
470
470
|
return inner
|
bumble/vendor/android/hci.py
CHANGED
|
@@ -299,7 +299,7 @@ class HCI_Android_Vendor_Event(HCI_Extended_Event):
|
|
|
299
299
|
|
|
300
300
|
|
|
301
301
|
HCI_Android_Vendor_Event.register_subevents(globals())
|
|
302
|
-
HCI_Event.
|
|
302
|
+
HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters)
|
|
303
303
|
|
|
304
304
|
|
|
305
305
|
# -----------------------------------------------------------------------------
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: bumble
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.207
|
|
4
4
|
Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Author-email: tbd@tbd.com
|
|
5
|
+
Author-email: Google <bumble-dev@google.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/google/bumble
|
|
8
7
|
Requires-Python: >=3.8
|
|
9
8
|
Description-Content-Type: text/markdown
|
|
10
9
|
License-File: LICENSE
|
|
11
10
|
Requires-Dist: aiohttp~=3.8; platform_system != "Emscripten"
|
|
12
11
|
Requires-Dist: appdirs>=1.4; platform_system != "Emscripten"
|
|
13
12
|
Requires-Dist: click>=8.1.3; platform_system != "Emscripten"
|
|
14
|
-
Requires-Dist: cryptography
|
|
13
|
+
Requires-Dist: cryptography>=39; platform_system != "Emscripten"
|
|
15
14
|
Requires-Dist: cryptography>=39.0; platform_system == "Emscripten"
|
|
16
15
|
Requires-Dist: grpcio>=1.62.1; platform_system != "Emscripten"
|
|
17
16
|
Requires-Dist: humanize>=4.6.0; platform_system != "Emscripten"
|
|
@@ -35,6 +34,7 @@ Requires-Dist: pytest-html>=3.2.0; extra == "test"
|
|
|
35
34
|
Requires-Dist: coverage>=6.4; extra == "test"
|
|
36
35
|
Provides-Extra: development
|
|
37
36
|
Requires-Dist: black==24.3; extra == "development"
|
|
37
|
+
Requires-Dist: bt-test-interfaces>=0.0.6; extra == "development"
|
|
38
38
|
Requires-Dist: grpcio-tools>=1.62.1; extra == "development"
|
|
39
39
|
Requires-Dist: invoke>=1.7.3; extra == "development"
|
|
40
40
|
Requires-Dist: mobly>=1.12.2; extra == "development"
|
|
@@ -45,16 +45,18 @@ Requires-Dist: pyyaml>=6.0; extra == "development"
|
|
|
45
45
|
Requires-Dist: types-appdirs>=1.4.3; extra == "development"
|
|
46
46
|
Requires-Dist: types-invoke>=1.7.3; extra == "development"
|
|
47
47
|
Requires-Dist: types-protobuf>=4.21.0; extra == "development"
|
|
48
|
-
Requires-Dist: wasmtime==20.0.0; extra == "development"
|
|
49
48
|
Provides-Extra: avatar
|
|
50
49
|
Requires-Dist: pandora-avatar==0.0.10; extra == "avatar"
|
|
51
50
|
Requires-Dist: rootcanal==1.10.0; python_version >= "3.10" and extra == "avatar"
|
|
52
51
|
Provides-Extra: pandora
|
|
53
52
|
Requires-Dist: bt-test-interfaces>=0.0.6; extra == "pandora"
|
|
54
53
|
Provides-Extra: documentation
|
|
55
|
-
Requires-Dist: mkdocs>=1.
|
|
56
|
-
Requires-Dist: mkdocs-material>=
|
|
57
|
-
Requires-Dist: mkdocstrings[python]>=0.
|
|
54
|
+
Requires-Dist: mkdocs>=1.6.0; extra == "documentation"
|
|
55
|
+
Requires-Dist: mkdocs-material>=9.6; extra == "documentation"
|
|
56
|
+
Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "documentation"
|
|
57
|
+
Provides-Extra: auracast
|
|
58
|
+
Requires-Dist: lc3py; (python_version >= "3.10" and platform_system == "Linux" and platform_machine == "x86_64") and extra == "auracast"
|
|
59
|
+
Requires-Dist: sounddevice>=0.5.1; extra == "auracast"
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
_ _ _
|