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/_version.py +2 -2
- bumble/apps/bench.py +69 -12
- bumble/apps/lea_unicast/app.py +577 -0
- bumble/apps/lea_unicast/index.html +68 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- bumble/apps/rfcomm_bridge.py +511 -0
- bumble/device.py +157 -118
- bumble/hci.py +14 -25
- bumble/hfp.py +279 -31
- bumble/host.py +9 -5
- bumble/keys.py +7 -4
- bumble/l2cap.py +5 -2
- bumble/profiles/bap.py +52 -11
- bumble/rfcomm.py +173 -60
- bumble/sdp.py +1 -1
- bumble/transport/common.py +4 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/METADATA +5 -4
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/RECORD +22 -18
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/LICENSE +0 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/WHEEL +0 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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.
|
|
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
|
-
|
|
217
|
-
|
|
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[
|
|
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
|
-
|
|
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__(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
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=[
|
|
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
|
|
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
|
|
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
|
-
@
|
|
257
|
-
def from_device(
|
|
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
|
|
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.
|
|
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
|
|
2231
|
+
except BaseException as e:
|
|
2229
2232
|
del connection_channels[source_cid]
|
|
2230
2233
|
raise e
|
|
2231
2234
|
|