bumble 0.0.192__py3-none-any.whl → 0.0.194__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/hfp.py CHANGED
@@ -50,7 +50,7 @@ from bumble.core import (
50
50
  ProtocolError,
51
51
  BT_GENERIC_AUDIO_SERVICE,
52
52
  BT_HANDSFREE_SERVICE,
53
- BT_HEADSET_AUDIO_GATEWAY_SERVICE,
53
+ BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
54
54
  BT_L2CAP_PROTOCOL_ID,
55
55
  BT_RFCOMM_PROTOCOL_ID,
56
56
  )
@@ -204,17 +204,22 @@ class HfIndicator(enum.IntEnum):
204
204
  BATTERY_LEVEL = 0x02 # Battery level feature
205
205
 
206
206
 
207
- class CallHoldOperation(enum.IntEnum):
207
+ class CallHoldOperation(enum.Enum):
208
208
  """
209
209
  Call Hold supported operations (normative).
210
210
 
211
211
  AT Commands Reference Guide, 3.5.2.3.12 +CHLD - Call Holding Services.
212
212
  """
213
213
 
214
- RELEASE_ALL_HELD_CALLS = 0 # Release all held calls
215
- RELEASE_ALL_ACTIVE_CALLS = 1 # Release all active calls, accept other
216
- HOLD_ALL_ACTIVE_CALLS = 2 # Place all active calls on hold, accept other
217
- ADD_HELD_CALL = 3 # Adds a held call to conversation
214
+ RELEASE_ALL_HELD_CALLS = "0" # Release all held calls
215
+ RELEASE_ALL_ACTIVE_CALLS = "1" # Release all active calls, accept other
216
+ RELEASE_SPECIFIC_CALL = "1x" # Release a specific call X
217
+ HOLD_ALL_ACTIVE_CALLS = "2" # Place all active calls on hold, accept other
218
+ HOLD_ALL_CALLS_EXCEPT = "2x" # Place all active calls except call X
219
+ ADD_HELD_CALL = "3" # Adds a held call to conversation
220
+ CONNECT_TWO_CALLS = (
221
+ "4" # Connects the two calls and disconnects the subscriber from both calls
222
+ )
218
223
 
219
224
 
220
225
  class ResponseHoldStatus(enum.IntEnum):
@@ -335,10 +340,82 @@ class CallInfo:
335
340
  status: CallInfoStatus
336
341
  mode: CallInfoMode
337
342
  multi_party: CallInfoMultiParty
338
- number: Optional[int] = None
343
+ number: Optional[str] = None
339
344
  type: Optional[int] = None
340
345
 
341
346
 
347
+ @dataclasses.dataclass
348
+ class CallLineIdentification:
349
+ """
350
+ Calling Line Identification notification.
351
+
352
+ TS 127 007 - V6.8.0, 7.6 Calling line identification presentation +CLIP, but only
353
+ number, type and alpha are meaningful in HFP.
354
+
355
+ Attributes:
356
+ number: String type phone number of format specified by `type`.
357
+ type: Type of address octet in integer format (refer TS 24.008 [8] subclause
358
+ 10.5.4.7).
359
+ subaddr: String type subaddress of format specified by `satype`.
360
+ satype: Type of subaddress octet in integer format (refer TS 24.008 [8]
361
+ subclause 10.5.4.8).
362
+ alpha: Optional string type alphanumeric representation of number corresponding
363
+ to the entry found in phonebook; used character set should be the one selected
364
+ with command Select TE Character Set +CSCS.
365
+ cli_validity: 0 CLI valid, 1 CLI has been withheld by the originator, 2 CLI is
366
+ not available due to interworking problems or limitations of originating
367
+ network.
368
+ """
369
+
370
+ number: str
371
+ type: int
372
+ subaddr: Optional[str] = None
373
+ satype: Optional[int] = None
374
+ alpha: Optional[str] = None
375
+ cli_validity: Optional[int] = None
376
+
377
+ @classmethod
378
+ def parse_from(cls: Type[Self], parameters: List[bytes]) -> Self:
379
+ return cls(
380
+ number=parameters[0].decode(),
381
+ type=int(parameters[1]),
382
+ subaddr=parameters[2].decode() if len(parameters) >= 3 else None,
383
+ satype=(
384
+ int(parameters[3]) if len(parameters) >= 4 and parameters[3] else None
385
+ ),
386
+ alpha=parameters[4].decode() if len(parameters) >= 5 else None,
387
+ cli_validity=(
388
+ int(parameters[5]) if len(parameters) >= 6 and parameters[5] else None
389
+ ),
390
+ )
391
+
392
+ def to_clip_string(self) -> str:
393
+ return ','.join(
394
+ str(arg) if arg else ''
395
+ for arg in [
396
+ self.number,
397
+ self.type,
398
+ self.subaddr,
399
+ self.satype,
400
+ self.alpha,
401
+ self.cli_validity,
402
+ ]
403
+ )
404
+
405
+
406
+ class VoiceRecognitionState(enum.IntEnum):
407
+ """
408
+ vrec values provided in AT+BVRA command.
409
+
410
+ Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07 and 3GPP 27.007.
411
+ """
412
+
413
+ DISABLE = 0
414
+ ENABLE = 1
415
+ # (Enhanced Voice Recognition Status only) HF is ready to accept audio.
416
+ ENHANCED_READY = 2
417
+
418
+
342
419
  class CmeError(enum.IntEnum):
343
420
  """
344
421
  CME ERROR codes (partial listed).
@@ -359,7 +436,7 @@ class CmeError(enum.IntEnum):
359
436
  # -----------------------------------------------------------------------------
360
437
 
361
438
  # Response codes.
362
- RESPONSE_CODES = [
439
+ RESPONSE_CODES = {
363
440
  "+APLSIRI",
364
441
  "+BAC",
365
442
  "+BCC",
@@ -390,10 +467,10 @@ RESPONSE_CODES = [
390
467
  "+XAPL",
391
468
  "A",
392
469
  "D",
393
- ]
470
+ }
394
471
 
395
472
  # Unsolicited responses and statuses.
396
- UNSOLICITED_CODES = [
473
+ UNSOLICITED_CODES = {
397
474
  "+APLSIRI",
398
475
  "+BCS",
399
476
  "+BIND",
@@ -411,10 +488,10 @@ UNSOLICITED_CODES = [
411
488
  "NO ANSWER",
412
489
  "NO CARRIER",
413
490
  "RING",
414
- ]
491
+ }
415
492
 
416
493
  # Status codes
417
- STATUS_CODES = [
494
+ STATUS_CODES = {
418
495
  "+CME ERROR",
419
496
  "BLACKLISTED",
420
497
  "BUSY",
@@ -423,7 +500,7 @@ STATUS_CODES = [
423
500
  "NO ANSWER",
424
501
  "NO CARRIER",
425
502
  "OK",
426
- ]
503
+ }
427
504
 
428
505
 
429
506
  @dataclasses.dataclass
@@ -626,10 +703,25 @@ class HfProtocol(pyee.EventEmitter):
626
703
  ag_indicator: When AG update their indicators, notify the new state.
627
704
  Args:
628
705
  ag_indicator: AgIndicator
706
+ speaker_volume: Emitted when AG update speaker volume autonomously.
707
+ Args:
708
+ volume: Int
709
+ microphone_volume: Emitted when AG update microphone volume autonomously.
710
+ Args:
711
+ volume: Int
712
+ microphone_volume: Emitted when AG sends a ringtone request.
713
+ Args:
714
+ None
715
+ cli_notification: Emitted when notify the call metadata on line.
716
+ Args:
717
+ cli_notification: CallLineIdentification
718
+ voice_recognition: Emitted when AG starts voice recognition autonomously.
719
+ Args:
720
+ vrec: VoiceRecognitionState
629
721
  """
630
722
 
631
- class HfLoopTermination(HfpProtocolError): ...
632
- """Termination signal for run() loop."""
723
+ class HfLoopTermination(HfpProtocolError):
724
+ """Termination signal for run() loop."""
633
725
 
634
726
  supported_hf_features: int
635
727
  supported_audio_codecs: List[AudioCodec]
@@ -651,7 +743,11 @@ class HfProtocol(pyee.EventEmitter):
651
743
  read_buffer: bytearray
652
744
  active_codec: AudioCodec
653
745
 
654
- def __init__(self, dlc: rfcomm.DLC, configuration: HfConfiguration) -> None:
746
+ def __init__(
747
+ self,
748
+ dlc: rfcomm.DLC,
749
+ configuration: HfConfiguration,
750
+ ) -> None:
655
751
  super().__init__()
656
752
 
657
753
  # Configure internal state.
@@ -841,7 +937,7 @@ class HfProtocol(pyee.EventEmitter):
841
937
 
842
938
  if self.supports_hf_feature(
843
939
  HfFeature.THREE_WAY_CALLING
844
- ) and self.supports_ag_feature(HfFeature.THREE_WAY_CALLING):
940
+ ) and self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
845
941
  # After the HF has enabled the “Indicators status update” function in
846
942
  # the AG, and if the “Call waiting and 3-way calling” bit was set in the
847
943
  # supported features bitmap by both the HF and the AG, the HF shall
@@ -854,9 +950,8 @@ class HfProtocol(pyee.EventEmitter):
854
950
  )
855
951
 
856
952
  self.supported_ag_call_hold_operations = [
857
- CallHoldOperation(int(operation))
953
+ CallHoldOperation(operation.decode())
858
954
  for operation in response.parameters[0]
859
- if not b'x' in operation
860
955
  ]
861
956
 
862
957
  # 4.2.1.4 HF Indicators
@@ -986,8 +1081,9 @@ class HfProtocol(pyee.EventEmitter):
986
1081
  mode=CallInfoMode(int(response.parameters[3])),
987
1082
  multi_party=CallInfoMultiParty(int(response.parameters[4])),
988
1083
  )
1084
+ if len(response.parameters) >= 6:
1085
+ call_info.number = response.parameters[5].decode()
989
1086
  if len(response.parameters) >= 7:
990
- call_info.number = int(response.parameters[5])
991
1087
  call_info.type = int(response.parameters[6])
992
1088
  calls.append(call_info)
993
1089
  return calls
@@ -1010,6 +1106,21 @@ class HfProtocol(pyee.EventEmitter):
1010
1106
  await self.update_ag_indicator(
1011
1107
  int(result.parameters[0]), int(result.parameters[1])
1012
1108
  )
1109
+ elif result.code == "+VGS":
1110
+ self.emit('speaker_volume', int(result.parameters[0]))
1111
+ elif result.code == "+VGM":
1112
+ self.emit('microphone_volume', int(result.parameters[0]))
1113
+ elif result.code == "RING":
1114
+ self.emit('ring')
1115
+ elif result.code == "+CLIP":
1116
+ self.emit(
1117
+ 'cli_notification', CallLineIdentification.parse_from(result.parameters)
1118
+ )
1119
+ elif result.code == "+BVRA":
1120
+ # TODO: Support Enhanced Voice Recognition.
1121
+ self.emit(
1122
+ 'voice_recognition', VoiceRecognitionState(int(result.parameters[0]))
1123
+ )
1013
1124
  else:
1014
1125
  logging.info(f"unhandled unsolicited response {result.code}")
1015
1126
 
@@ -1045,11 +1156,24 @@ class AgProtocol(pyee.EventEmitter):
1045
1156
  active_codec: AudioCodec
1046
1157
  hf_indicator: When HF update their indicators, notify the new state.
1047
1158
  Args:
1048
- hf_indicator: HfIndicator
1159
+ hf_indicator: HfIndicatorState
1049
1160
  codec_connection_request: Emit when HF sends AT+BCC to request codec connection.
1050
1161
  answer: Emit when HF sends ATA to answer phone call.
1051
1162
  hang_up: Emit when HF sends AT+CHUP to hang up phone call.
1052
1163
  dial: Emit when HF sends ATD to dial phone call.
1164
+ voice_recognition: Emit when HF requests voice recognition state.
1165
+ Args:
1166
+ vrec: VoiceRecognitionState
1167
+ call_hold: Emit when HF requests call hold operation.
1168
+ Args:
1169
+ operation: CallHoldOperation
1170
+ call_index: Optional[int]
1171
+ speaker_volume: Emitted when AG update speaker volume autonomously.
1172
+ Args:
1173
+ volume: Int
1174
+ microphone_volume: Emitted when AG update microphone volume autonomously.
1175
+ Args:
1176
+ volume: Int
1053
1177
  """
1054
1178
 
1055
1179
  supported_hf_features: int
@@ -1066,10 +1190,13 @@ class AgProtocol(pyee.EventEmitter):
1066
1190
 
1067
1191
  read_buffer: bytearray
1068
1192
  active_codec: AudioCodec
1193
+ calls: List[CallInfo]
1069
1194
 
1070
1195
  indicator_report_enabled: bool
1071
1196
  inband_ringtone_enabled: bool
1072
1197
  cme_error_enabled: bool
1198
+ cli_notification_enabled: bool
1199
+ call_waiting_enabled: bool
1073
1200
  _remained_slc_setup_features: Set[HfFeature]
1074
1201
 
1075
1202
  def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
@@ -1079,6 +1206,7 @@ class AgProtocol(pyee.EventEmitter):
1079
1206
  self.dlc = dlc
1080
1207
  self.read_buffer = bytearray()
1081
1208
  self.active_codec = AudioCodec.CVSD
1209
+ self.calls = []
1082
1210
 
1083
1211
  # Build local features.
1084
1212
  self.supported_ag_features = sum(configuration.supported_ag_features)
@@ -1095,6 +1223,8 @@ class AgProtocol(pyee.EventEmitter):
1095
1223
  self.supported_audio_codecs = []
1096
1224
  self.indicator_report_enabled = False
1097
1225
  self.cme_error_enabled = False
1226
+ self.cli_notification_enabled = False
1227
+ self.call_waiting_enabled = False
1098
1228
 
1099
1229
  self.hf_indicators = collections.OrderedDict()
1100
1230
 
@@ -1168,6 +1298,21 @@ class AgProtocol(pyee.EventEmitter):
1168
1298
  self.inband_ringtone_enabled = enabled
1169
1299
  self.send_response(f'+BSIR: {1 if enabled else 0}')
1170
1300
 
1301
+ def set_speaker_volume(self, level: int) -> None:
1302
+ """Reports speaker volume."""
1303
+
1304
+ self.send_response(f'+VGS: {level}')
1305
+
1306
+ def set_microphone_volume(self, level: int) -> None:
1307
+ """Reports microphone volume."""
1308
+
1309
+ self.send_response(f'+VGM: {level}')
1310
+
1311
+ def send_ring(self) -> None:
1312
+ """Sends RING command to trigger ringtone on HF."""
1313
+
1314
+ self.send_response('RING')
1315
+
1171
1316
  def update_ag_indicator(self, indicator: AgIndicator, value: int) -> None:
1172
1317
  """Updates AG indicator.
1173
1318
 
@@ -1212,6 +1357,14 @@ class AgProtocol(pyee.EventEmitter):
1212
1357
  if (new_codec := await at_bcs_future) != codec:
1213
1358
  raise HfpProtocolError(f'Expect codec: {codec}, but get {new_codec}')
1214
1359
 
1360
+ def send_cli_notification(self, cli: CallLineIdentification) -> None:
1361
+ """Sends +CLIP CLI notification."""
1362
+
1363
+ if not self.cli_notification_enabled:
1364
+ logger.warning('Try to send CLIP while CLI notification is not enabled')
1365
+
1366
+ self.send_response(f'+CLIP: {cli.to_clip_string()}')
1367
+
1215
1368
  def _check_remained_slc_commands(self) -> None:
1216
1369
  if not self._remained_slc_setup_features:
1217
1370
  self.emit('slc_complete')
@@ -1240,6 +1393,54 @@ class AgProtocol(pyee.EventEmitter):
1240
1393
  self.send_ok()
1241
1394
  self.emit('codec_negotiation', self.active_codec)
1242
1395
 
1396
+ def _on_bvra(self, vrec: bytes) -> None:
1397
+ self.send_ok()
1398
+ self.emit('voice_recognition', VoiceRecognitionState(int(vrec)))
1399
+
1400
+ def _on_chld(self, operation_code: bytes) -> None:
1401
+ call_index: Optional[int] = None
1402
+ if len(operation_code) > 1:
1403
+ call_index = int(operation_code[1:])
1404
+ operation_code = operation_code[:1] + b'x'
1405
+ try:
1406
+ operation = CallHoldOperation(operation_code.decode())
1407
+ except:
1408
+ logger.error(f'Invalid operation: {operation_code.decode()}')
1409
+ self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED)
1410
+ return
1411
+
1412
+ if operation not in self.supported_ag_call_hold_operations:
1413
+ logger.error(f'Unsupported operation: {operation_code.decode()}')
1414
+ self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED)
1415
+
1416
+ if call_index is not None and not any(
1417
+ call.index == call_index for call in self.calls
1418
+ ):
1419
+ logger.error(f'No matching call {call_index}')
1420
+ self.send_cme_error(CmeError.INVALID_INDEX)
1421
+
1422
+ # Real three-way calls have more complicated situations, but this is not a popular issue - let users to handle the remaining :)
1423
+
1424
+ self.send_ok()
1425
+ self.emit('call_hold', operation, call_index)
1426
+
1427
+ def _on_chld_test(self) -> None:
1428
+ if not self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
1429
+ self.send_error()
1430
+ return
1431
+
1432
+ self.send_response(
1433
+ '+CHLD: ({})'.format(
1434
+ ','.join(
1435
+ operation.value
1436
+ for operation in self.supported_ag_call_hold_operations
1437
+ )
1438
+ )
1439
+ )
1440
+ self.send_ok()
1441
+ self._remained_slc_setup_features.remove(HfFeature.THREE_WAY_CALLING)
1442
+ self._check_remained_slc_commands()
1443
+
1243
1444
  def _on_cind_test(self) -> None:
1244
1445
  if not self.ag_indicators:
1245
1446
  self.send_cme_error(CmeError.NOT_FOUND)
@@ -1271,7 +1472,12 @@ class AgProtocol(pyee.EventEmitter):
1271
1472
  display: Optional[bytes] = None,
1272
1473
  indicator: bytes = b'',
1273
1474
  ) -> None:
1274
- if int(mode) != 3 or keypad or display or int(indicator) not in (0, 1):
1475
+ if (
1476
+ int(mode) != 3
1477
+ or (keypad and int(keypad))
1478
+ or (display and int(display))
1479
+ or int(indicator) not in (0, 1)
1480
+ ):
1275
1481
  logger.error(
1276
1482
  f'Unexpected values: mode={mode!r}, keypad={keypad!r}, '
1277
1483
  f'display={display!r}, indicator={indicator!r}'
@@ -1285,6 +1491,10 @@ class AgProtocol(pyee.EventEmitter):
1285
1491
  self.cme_error_enabled = bool(int(enabled))
1286
1492
  self.send_ok()
1287
1493
 
1494
+ def _on_ccwa(self, enabled: bytes) -> None:
1495
+ self.call_waiting_enabled = bool(int(enabled))
1496
+ self.send_ok()
1497
+
1288
1498
  def _on_bind(self, *args) -> None:
1289
1499
  if not self.supports_ag_feature(AgFeature.HF_INDICATORS):
1290
1500
  self.send_error()
@@ -1364,6 +1574,36 @@ class AgProtocol(pyee.EventEmitter):
1364
1574
  self.emit('hang_up')
1365
1575
  self.send_ok()
1366
1576
 
1577
+ def _on_clcc(self) -> None:
1578
+ for call in self.calls:
1579
+ number_text = f',\"{call.number}\"' if call.number is not None else ''
1580
+ type_text = f',{call.type}' if call.type is not None else ''
1581
+ response = (
1582
+ f'+CLCC: {call.index}'
1583
+ f',{call.direction.value}'
1584
+ f',{call.status.value}'
1585
+ f',{call.mode.value}'
1586
+ f',{call.multi_party.value}'
1587
+ f'{number_text}'
1588
+ f'{type_text}'
1589
+ )
1590
+ self.send_response(response)
1591
+ self.send_ok()
1592
+
1593
+ def _on_clip(self, enabled: bytes) -> None:
1594
+ if not self.supports_hf_feature(HfFeature.CLI_PRESENTATION_CAPABILITY):
1595
+ logger.error('Remote doesn not support CLI but sends AT+CLIP')
1596
+ self.cli_notification_enabled = True if enabled == b'1' else False
1597
+ self.send_ok()
1598
+
1599
+ def _on_vgs(self, level: bytes) -> None:
1600
+ self.emit('speaker_volume', int(level))
1601
+ self.send_ok()
1602
+
1603
+ def _on_vgm(self, level: bytes) -> None:
1604
+ self.emit('microphone_volume', int(level))
1605
+ self.send_ok()
1606
+
1367
1607
 
1368
1608
  # -----------------------------------------------------------------------------
1369
1609
  # Normative SDP definitions
@@ -1546,7 +1786,7 @@ def make_ag_sdp_records(
1546
1786
  sdp.SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
1547
1787
  sdp.DataElement.sequence(
1548
1788
  [
1549
- sdp.DataElement.uuid(BT_HEADSET_AUDIO_GATEWAY_SERVICE),
1789
+ sdp.DataElement.uuid(BT_HANDSFREE_AUDIO_GATEWAY_SERVICE),
1550
1790
  sdp.DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
1551
1791
  ]
1552
1792
  ),
@@ -1573,7 +1813,7 @@ def make_ag_sdp_records(
1573
1813
  [
1574
1814
  sdp.DataElement.sequence(
1575
1815
  [
1576
- sdp.DataElement.uuid(BT_HEADSET_AUDIO_GATEWAY_SERVICE),
1816
+ sdp.DataElement.uuid(BT_HANDSFREE_AUDIO_GATEWAY_SERVICE),
1577
1817
  sdp.DataElement.unsigned_integer_16(version),
1578
1818
  ]
1579
1819
  )
@@ -1596,7 +1836,7 @@ async def find_hf_sdp_record(
1596
1836
  connection: ACL connection to make SDP search.
1597
1837
 
1598
1838
  Returns:
1599
- Dictionary mapping from channel number to service class UUID list.
1839
+ Tuple of (<RFCOMM channel>, <Profile Version>, <HF SDP features>)
1600
1840
  """
1601
1841
  async with sdp.Client(connection) as sdp_client:
1602
1842
  search_result = await sdp_client.search_attributes(
@@ -1605,6 +1845,7 @@ async def find_hf_sdp_record(
1605
1845
  sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
1606
1846
  sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
1607
1847
  sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
1848
+ sdp.SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
1608
1849
  ],
1609
1850
  )
1610
1851
  for attribute_lists in search_result:
@@ -1624,10 +1865,17 @@ async def find_hf_sdp_record(
1624
1865
  version = ProfileVersion(profile_descriptor_list[0].value[1].value)
1625
1866
  elif attribute.id == sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
1626
1867
  features = HfSdpFeature(attribute.value.value)
1627
- if not channel or not version or features is None:
1628
- logger.warning(f"Bad result {attribute_lists}.")
1629
- return None
1630
- return (channel, version, features)
1868
+ elif attribute.id == sdp.SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID:
1869
+ class_id_list = attribute.value.value
1870
+ uuid = class_id_list[0].value
1871
+ # AG record may also contain HF UUID in its profile descriptor list.
1872
+ # If found, skip this record.
1873
+ if uuid == BT_HANDSFREE_AUDIO_GATEWAY_SERVICE:
1874
+ channel, version, features = (None, None, None)
1875
+ break
1876
+
1877
+ if channel is not None and version is not None and features is not None:
1878
+ return (channel, version, features)
1631
1879
  return None
1632
1880
 
1633
1881
 
@@ -1640,11 +1888,11 @@ async def find_ag_sdp_record(
1640
1888
  connection: ACL connection to make SDP search.
1641
1889
 
1642
1890
  Returns:
1643
- Dictionary mapping from channel number to service class UUID list.
1891
+ Tuple of (<RFCOMM channel>, <Profile Version>, <AG SDP features>)
1644
1892
  """
1645
1893
  async with sdp.Client(connection) as sdp_client:
1646
1894
  search_result = await sdp_client.search_attributes(
1647
- uuids=[BT_HEADSET_AUDIO_GATEWAY_SERVICE],
1895
+ uuids=[BT_HANDSFREE_AUDIO_GATEWAY_SERVICE],
1648
1896
  attribute_ids=[
1649
1897
  sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
1650
1898
  sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
bumble/host.py CHANGED
@@ -530,7 +530,9 @@ class Host(AbortableEventEmitter):
530
530
 
531
531
  # Check the return parameters if required
532
532
  if check_result:
533
- if isinstance(response.return_parameters, int):
533
+ if isinstance(response, hci.HCI_Command_Status_Event):
534
+ status = response.status
535
+ elif isinstance(response.return_parameters, int):
534
536
  status = response.return_parameters
535
537
  elif isinstance(response.return_parameters, bytes):
536
538
  # return parameters first field is a one byte status code
@@ -719,14 +721,16 @@ class Host(AbortableEventEmitter):
719
721
  for connection_handle, num_completed_packets in zip(
720
722
  event.connection_handles, event.num_completed_packets
721
723
  ):
722
- if not (connection := self.connections.get(connection_handle)):
724
+ if connection := self.connections.get(connection_handle):
725
+ connection.acl_packet_queue.on_packets_completed(num_completed_packets)
726
+ elif not (
727
+ self.cis_links.get(connection_handle)
728
+ or self.sco_links.get(connection_handle)
729
+ ):
723
730
  logger.warning(
724
731
  'received packet completion event for unknown handle '
725
732
  f'0x{connection_handle:04X}'
726
733
  )
727
- continue
728
-
729
- connection.acl_packet_queue.on_packets_completed(num_completed_packets)
730
734
 
731
735
  # Classic only
732
736
  def on_hci_connection_request_event(self, event):
bumble/keys.py CHANGED
@@ -25,7 +25,8 @@ import asyncio
25
25
  import logging
26
26
  import os
27
27
  import json
28
- from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
28
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
29
+ from typing_extensions import Self
29
30
 
30
31
  from .colors import color
31
32
  from .hci import Address
@@ -253,8 +254,10 @@ class JsonKeyStore(KeyStore):
253
254
 
254
255
  logger.debug(f'JSON keystore: {self.filename}')
255
256
 
256
- @staticmethod
257
- def from_device(device: Device, filename=None) -> Optional[JsonKeyStore]:
257
+ @classmethod
258
+ def from_device(
259
+ cls: Type[Self], device: Device, filename: Optional[str] = None
260
+ ) -> Self:
258
261
  if not filename:
259
262
  # Extract the filename from the config if there is one
260
263
  if device.config.keystore is not None:
@@ -270,7 +273,7 @@ class JsonKeyStore(KeyStore):
270
273
  else:
271
274
  namespace = JsonKeyStore.DEFAULT_NAMESPACE
272
275
 
273
- return JsonKeyStore(namespace, filename)
276
+ return cls(namespace, filename)
274
277
 
275
278
  async def load(self):
276
279
  # Try to open the file, without failing. If the file does not exist, it
bumble/l2cap.py CHANGED
@@ -70,6 +70,7 @@ L2CAP_LE_SIGNALING_CID = 0x05
70
70
 
71
71
  L2CAP_MIN_LE_MTU = 23
72
72
  L2CAP_MIN_BR_EDR_MTU = 48
73
+ L2CAP_MAX_BR_EDR_MTU = 65535
73
74
 
74
75
  L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
75
76
 
@@ -832,7 +833,9 @@ class ClassicChannel(EventEmitter):
832
833
 
833
834
  # Wait for the connection to succeed or fail
834
835
  try:
835
- return await self.connection_result
836
+ return await self.connection.abort_on(
837
+ 'disconnection', self.connection_result
838
+ )
836
839
  finally:
837
840
  self.connection_result = None
838
841
 
@@ -2225,7 +2228,7 @@ class ChannelManager:
2225
2228
  # Connect
2226
2229
  try:
2227
2230
  await channel.connect()
2228
- except Exception as e:
2231
+ except BaseException as e:
2229
2232
  del connection_channels[source_cid]
2230
2233
  raise e
2231
2234