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/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 Dict, List, Type, Optional, Tuple, Union, NewType, TYPE_CHECKING
22
+ from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
22
23
  from typing_extensions import Self
23
24
 
24
- from . import core, l2cap
25
- from .colors import color
26
- from .core import InvalidStateError, InvalidArgumentError, InvalidPacketError
27
- from .hci import HCI_Object, name_or_number, key_with_value
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 = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
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: List[DataElement]) -> DataElement:
251
+ def sequence(value: Iterable[DataElement]) -> DataElement:
246
252
  return DataElement(DataElement.SEQUENCE, value)
247
253
 
248
254
  @staticmethod
249
- def alternative(value: List[DataElement]) -> DataElement:
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(elements: List[DataElement]) -> List[ServiceAttribute]:
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: List[ServiceAttribute], attribute_id: int
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
- sdp_pdu_classes: Dict[int, Type[SDP_PDU]] = {}
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
- ) -> Tuple[int, List[int]]:
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: List[int]
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
- attribute_list_byte_count: int
756
- attribute_list: bytes
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
- channel: Optional[l2cap.ClassicChannel]
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.pending_request = None
767
- self.channel = None
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=l2cap.ClassicChannelSpec(SDP_PSM)
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
- async def search_services(self, uuids: List[core.UUID]) -> List[int]:
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
- response_pdu = await self.channel.send_request(
877
+ response = await self.send_request(
795
878
  SDP_ServiceSearchRequest(
796
- transaction_id=0, # Transaction ID TODO: pick a real value
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, uuids: List[core.UUID], attribute_ids: List[Union[int, Tuple[int, int]]]
815
- ) -> List[List[ServiceAttribute]]:
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.unsigned_integer(
828
- attribute_id[0], value_size=attribute_id[1]
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
- response_pdu = await self.channel.send_request(
938
+ response = await self.send_request(
843
939
  SDP_ServiceSearchAttributeRequest(
844
- transaction_id=0, # Transaction ID TODO: pick a real value
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: List[Union[int, Tuple[int, int]]],
876
- ) -> List[ServiceAttribute]:
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.unsigned_integer(
886
- attribute_id[0], value_size=attribute_id[1]
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
- response_pdu = await self.channel.send_request(
1006
+ response = await self.send_request(
901
1007
  SDP_ServiceAttributeRequest(
902
- transaction_id=0, # Transaction ID TODO: pick a real value
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, 0x43])
1042
+ CONTINUATION_STATE = bytes([0x01, 0x00])
937
1043
  channel: Optional[l2cap.ClassicChannel]
938
- Service = NewType('Service', List[ServiceAttribute])
939
- service_records: Dict[int, Service]
940
- current_response: Union[None, bytes, Tuple[int, List[int]]]
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) -> Dict[int, Service]:
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: List[DataElement]
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 len(request.continuation_state) > 1:
1057
- if self.current_response is None:
1058
- self.send_response(
1059
- SDP_ErrorResponse(
1060
- transaction_id=request.transaction_id,
1061
- error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
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
- service_record_handles = self.current_response[1][
1088
- : request.maximum_service_record_count
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
- self.current_response[0],
1092
- self.current_response[1][request.maximum_service_record_count :],
1220
+ total_service_record_count,
1221
+ service_record_handles_remaining,
1093
1222
  )
1094
1223
  continuation_state = (
1095
- Server.CONTINUATION_STATE if self.current_response[1] else bytes([0])
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=self.current_response[0],
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 len(request.continuation_state) > 1:
1115
- if self.current_response is None:
1116
- self.send_response(
1117
- SDP_ErrorResponse(
1118
- transaction_id=request.transaction_id,
1119
- error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
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
- request.maximum_attribute_byte_count
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=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 len(request.continuation_state) > 1:
1165
- if self.current_response is None:
1166
- self.send_response(
1167
- SDP_ErrorResponse(
1168
- transaction_id=request.transaction_id,
1169
- error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
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
- request.maximum_attribute_byte_count
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=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
@@ -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.vendor_factory = HCI_Android_Vendor_Event.subclass_from_parameters
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
1
+ Metadata-Version: 2.2
2
2
  Name: bumble
3
- Version: 0.0.204
3
+ Version: 0.0.207
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
- Home-page: https://github.com/google/bumble
6
- Author: Google
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==39; platform_system != "Emscripten"
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.4.0; extra == "documentation"
56
- Requires-Dist: mkdocs-material>=8.5.6; extra == "documentation"
57
- Requires-Dist: mkdocstrings[python]>=0.19.0; extra == "documentation"
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
  _ _ _