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/profiles/bap.py
CHANGED
|
@@ -78,6 +78,10 @@ class AudioLocation(enum.IntFlag):
|
|
|
78
78
|
LEFT_SURROUND = 0x04000000
|
|
79
79
|
RIGHT_SURROUND = 0x08000000
|
|
80
80
|
|
|
81
|
+
@property
|
|
82
|
+
def channel_count(self) -> int:
|
|
83
|
+
return bin(self.value).count('1')
|
|
84
|
+
|
|
81
85
|
|
|
82
86
|
class AudioInputType(enum.IntEnum):
|
|
83
87
|
'''Bluetooth Assigned Numbers, Section 6.12.2 - Audio Input Type'''
|
|
@@ -218,6 +222,13 @@ class FrameDuration(enum.IntEnum):
|
|
|
218
222
|
DURATION_7500_US = 0x00
|
|
219
223
|
DURATION_10000_US = 0x01
|
|
220
224
|
|
|
225
|
+
@property
|
|
226
|
+
def us(self) -> int:
|
|
227
|
+
return {
|
|
228
|
+
FrameDuration.DURATION_7500_US: 7500,
|
|
229
|
+
FrameDuration.DURATION_10000_US: 10000,
|
|
230
|
+
}[self]
|
|
231
|
+
|
|
221
232
|
|
|
222
233
|
class SupportedFrameDuration(enum.IntFlag):
|
|
223
234
|
'''Bluetooth Assigned Numbers, Section 6.12.4.2 - Frame Duration'''
|
|
@@ -534,7 +545,7 @@ class CodecSpecificCapabilities:
|
|
|
534
545
|
|
|
535
546
|
supported_sampling_frequencies: SupportedSamplingFrequency
|
|
536
547
|
supported_frame_durations: SupportedFrameDuration
|
|
537
|
-
|
|
548
|
+
supported_audio_channel_count: Sequence[int]
|
|
538
549
|
min_octets_per_codec_frame: int
|
|
539
550
|
max_octets_per_codec_frame: int
|
|
540
551
|
supported_max_codec_frames_per_sdu: int
|
|
@@ -543,7 +554,7 @@ class CodecSpecificCapabilities:
|
|
|
543
554
|
def from_bytes(cls, data: bytes) -> CodecSpecificCapabilities:
|
|
544
555
|
offset = 0
|
|
545
556
|
# Allowed default values.
|
|
546
|
-
|
|
557
|
+
supported_audio_channel_count = [1]
|
|
547
558
|
supported_max_codec_frames_per_sdu = 1
|
|
548
559
|
while offset < len(data):
|
|
549
560
|
length, type = struct.unpack_from('BB', data, offset)
|
|
@@ -556,7 +567,7 @@ class CodecSpecificCapabilities:
|
|
|
556
567
|
elif type == CodecSpecificCapabilities.Type.FRAME_DURATION:
|
|
557
568
|
supported_frame_durations = SupportedFrameDuration(value)
|
|
558
569
|
elif type == CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT:
|
|
559
|
-
|
|
570
|
+
supported_audio_channel_count = bits_to_channel_counts(value)
|
|
560
571
|
elif type == CodecSpecificCapabilities.Type.OCTETS_PER_FRAME:
|
|
561
572
|
min_octets_per_sample = value & 0xFFFF
|
|
562
573
|
max_octets_per_sample = value >> 16
|
|
@@ -567,7 +578,7 @@ class CodecSpecificCapabilities:
|
|
|
567
578
|
return CodecSpecificCapabilities(
|
|
568
579
|
supported_sampling_frequencies=supported_sampling_frequencies,
|
|
569
580
|
supported_frame_durations=supported_frame_durations,
|
|
570
|
-
|
|
581
|
+
supported_audio_channel_count=supported_audio_channel_count,
|
|
571
582
|
min_octets_per_codec_frame=min_octets_per_sample,
|
|
572
583
|
max_octets_per_codec_frame=max_octets_per_sample,
|
|
573
584
|
supported_max_codec_frames_per_sdu=supported_max_codec_frames_per_sdu,
|
|
@@ -584,7 +595,7 @@ class CodecSpecificCapabilities:
|
|
|
584
595
|
self.supported_frame_durations,
|
|
585
596
|
2,
|
|
586
597
|
CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT,
|
|
587
|
-
channel_counts_to_bits(self.
|
|
598
|
+
channel_counts_to_bits(self.supported_audio_channel_count),
|
|
588
599
|
5,
|
|
589
600
|
CodecSpecificCapabilities.Type.OCTETS_PER_FRAME,
|
|
590
601
|
self.min_octets_per_codec_frame,
|
|
@@ -870,15 +881,22 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
870
881
|
cig_id: int,
|
|
871
882
|
cis_id: int,
|
|
872
883
|
) -> None:
|
|
873
|
-
if
|
|
884
|
+
if (
|
|
885
|
+
cig_id == self.cig_id
|
|
886
|
+
and cis_id == self.cis_id
|
|
887
|
+
and self.state == self.State.ENABLING
|
|
888
|
+
):
|
|
874
889
|
acl_connection.abort_on(
|
|
875
890
|
'flush', self.service.device.accept_cis_request(cis_handle)
|
|
876
891
|
)
|
|
877
892
|
|
|
878
893
|
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
|
879
|
-
if
|
|
880
|
-
|
|
881
|
-
|
|
894
|
+
if (
|
|
895
|
+
cis_link.cig_id == self.cig_id
|
|
896
|
+
and cis_link.cis_id == self.cis_id
|
|
897
|
+
and self.state == self.State.ENABLING
|
|
898
|
+
):
|
|
899
|
+
cis_link.on('disconnection', self.on_cis_disconnection)
|
|
882
900
|
|
|
883
901
|
async def post_cis_established():
|
|
884
902
|
await self.service.device.send_command(
|
|
@@ -891,9 +909,15 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
891
909
|
codec_configuration=b'',
|
|
892
910
|
)
|
|
893
911
|
)
|
|
912
|
+
if self.role == AudioRole.SINK:
|
|
913
|
+
self.state = self.State.STREAMING
|
|
894
914
|
await self.service.device.notify_subscribers(self, self.value)
|
|
895
915
|
|
|
896
916
|
cis_link.acl_connection.abort_on('flush', post_cis_established())
|
|
917
|
+
self.cis_link = cis_link
|
|
918
|
+
|
|
919
|
+
def on_cis_disconnection(self, _reason) -> None:
|
|
920
|
+
self.cis_link = None
|
|
897
921
|
|
|
898
922
|
def on_config_codec(
|
|
899
923
|
self,
|
|
@@ -991,11 +1015,17 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
991
1015
|
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
992
1016
|
AseReasonCode.NONE,
|
|
993
1017
|
)
|
|
994
|
-
self.
|
|
1018
|
+
if self.role == AudioRole.SINK:
|
|
1019
|
+
self.state = self.State.QOS_CONFIGURED
|
|
1020
|
+
else:
|
|
1021
|
+
self.state = self.State.DISABLING
|
|
995
1022
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
996
1023
|
|
|
997
1024
|
def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
998
|
-
if
|
|
1025
|
+
if (
|
|
1026
|
+
self.role != AudioRole.SOURCE
|
|
1027
|
+
or self.state != AseStateMachine.State.DISABLING
|
|
1028
|
+
):
|
|
999
1029
|
return (
|
|
1000
1030
|
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1001
1031
|
AseReasonCode.NONE,
|
|
@@ -1046,6 +1076,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
1046
1076
|
def state(self, new_state: State) -> None:
|
|
1047
1077
|
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
|
1048
1078
|
self._state = new_state
|
|
1079
|
+
self.emit('state_change')
|
|
1049
1080
|
|
|
1050
1081
|
@property
|
|
1051
1082
|
def value(self):
|
|
@@ -1118,6 +1149,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
1118
1149
|
|
|
1119
1150
|
ase_state_machines: Dict[int, AseStateMachine]
|
|
1120
1151
|
ase_control_point: gatt.Characteristic
|
|
1152
|
+
_active_client: Optional[device.Connection] = None
|
|
1121
1153
|
|
|
1122
1154
|
def __init__(
|
|
1123
1155
|
self,
|
|
@@ -1155,7 +1187,16 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
1155
1187
|
else:
|
|
1156
1188
|
return (ase_id, AseResponseCode.INVALID_ASE_ID, AseReasonCode.NONE)
|
|
1157
1189
|
|
|
1190
|
+
def _on_client_disconnected(self, _reason: int) -> None:
|
|
1191
|
+
for ase in self.ase_state_machines.values():
|
|
1192
|
+
ase.state = AseStateMachine.State.IDLE
|
|
1193
|
+
self._active_client = None
|
|
1194
|
+
|
|
1158
1195
|
def on_write_ase_control_point(self, connection, data):
|
|
1196
|
+
if not self._active_client and connection:
|
|
1197
|
+
self._active_client = connection
|
|
1198
|
+
connection.once('disconnection', self._on_client_disconnected)
|
|
1199
|
+
|
|
1159
1200
|
operation = ASE_Operation.from_bytes(data)
|
|
1160
1201
|
responses = []
|
|
1161
1202
|
logger.debug(f'*** ASCS Write {operation} ***')
|
bumble/rfcomm.py
CHANGED
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import asyncio
|
|
22
|
+
import collections
|
|
22
23
|
import dataclasses
|
|
23
24
|
import enum
|
|
24
25
|
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
|
@@ -54,6 +55,7 @@ logger = logging.getLogger(__name__)
|
|
|
54
55
|
# fmt: off
|
|
55
56
|
|
|
56
57
|
RFCOMM_PSM = 0x0003
|
|
58
|
+
DEFAULT_RX_QUEUE_SIZE = 32
|
|
57
59
|
|
|
58
60
|
class FrameType(enum.IntEnum):
|
|
59
61
|
SABM = 0x2F # Control field [1,1,1,1,_,1,0,0] LSB-first
|
|
@@ -104,9 +106,11 @@ CRC_TABLE = bytes([
|
|
|
104
106
|
0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
|
|
105
107
|
])
|
|
106
108
|
|
|
107
|
-
RFCOMM_DEFAULT_L2CAP_MTU
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
RFCOMM_DEFAULT_L2CAP_MTU = 2048
|
|
110
|
+
RFCOMM_DEFAULT_INITIAL_CREDITS = 7
|
|
111
|
+
RFCOMM_DEFAULT_MAX_CREDITS = 32
|
|
112
|
+
RFCOMM_DEFAULT_CREDIT_THRESHOLD = RFCOMM_DEFAULT_MAX_CREDITS // 2
|
|
113
|
+
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
|
|
110
114
|
|
|
111
115
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
|
|
112
116
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
|
@@ -363,12 +367,12 @@ class RFCOMM_MCC_PN:
|
|
|
363
367
|
ack_timer: int
|
|
364
368
|
max_frame_size: int
|
|
365
369
|
max_retransmissions: int
|
|
366
|
-
|
|
370
|
+
initial_credits: int
|
|
367
371
|
|
|
368
372
|
def __post_init__(self) -> None:
|
|
369
|
-
if self.
|
|
373
|
+
if self.initial_credits < 1 or self.initial_credits > 7:
|
|
370
374
|
logger.warning(
|
|
371
|
-
f'
|
|
375
|
+
f'Initial credits {self.initial_credits} is out of range [1, 7].'
|
|
372
376
|
)
|
|
373
377
|
|
|
374
378
|
@staticmethod
|
|
@@ -380,7 +384,7 @@ class RFCOMM_MCC_PN:
|
|
|
380
384
|
ack_timer=data[3],
|
|
381
385
|
max_frame_size=data[4] | data[5] << 8,
|
|
382
386
|
max_retransmissions=data[6],
|
|
383
|
-
|
|
387
|
+
initial_credits=data[7] & 0x07,
|
|
384
388
|
)
|
|
385
389
|
|
|
386
390
|
def __bytes__(self) -> bytes:
|
|
@@ -394,7 +398,7 @@ class RFCOMM_MCC_PN:
|
|
|
394
398
|
(self.max_frame_size >> 8) & 0xFF,
|
|
395
399
|
self.max_retransmissions & 0xFF,
|
|
396
400
|
# Only 3 bits are meaningful.
|
|
397
|
-
self.
|
|
401
|
+
self.initial_credits & 0x07,
|
|
398
402
|
]
|
|
399
403
|
)
|
|
400
404
|
|
|
@@ -444,39 +448,58 @@ class DLC(EventEmitter):
|
|
|
444
448
|
DISCONNECTED = 0x04
|
|
445
449
|
RESET = 0x05
|
|
446
450
|
|
|
447
|
-
connection_result: Optional[asyncio.Future]
|
|
448
|
-
sink: Optional[Callable[[bytes], None]]
|
|
449
|
-
|
|
450
451
|
def __init__(
|
|
451
452
|
self,
|
|
452
453
|
multiplexer: Multiplexer,
|
|
453
454
|
dlci: int,
|
|
454
|
-
|
|
455
|
-
|
|
455
|
+
tx_max_frame_size: int,
|
|
456
|
+
tx_initial_credits: int,
|
|
457
|
+
rx_max_frame_size: int,
|
|
458
|
+
rx_initial_credits: int,
|
|
456
459
|
) -> None:
|
|
457
460
|
super().__init__()
|
|
458
461
|
self.multiplexer = multiplexer
|
|
459
462
|
self.dlci = dlci
|
|
460
|
-
self.
|
|
461
|
-
self.
|
|
462
|
-
self.
|
|
463
|
-
self.
|
|
464
|
-
self.
|
|
463
|
+
self.rx_max_frame_size = rx_max_frame_size
|
|
464
|
+
self.rx_initial_credits = rx_initial_credits
|
|
465
|
+
self.rx_max_credits = RFCOMM_DEFAULT_MAX_CREDITS
|
|
466
|
+
self.rx_credits = rx_initial_credits
|
|
467
|
+
self.rx_credits_threshold = RFCOMM_DEFAULT_CREDIT_THRESHOLD
|
|
468
|
+
self.tx_max_frame_size = tx_max_frame_size
|
|
469
|
+
self.tx_credits = tx_initial_credits
|
|
465
470
|
self.tx_buffer = b''
|
|
466
471
|
self.state = DLC.State.INIT
|
|
467
472
|
self.role = multiplexer.role
|
|
468
473
|
self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0
|
|
469
|
-
self.
|
|
470
|
-
self.
|
|
474
|
+
self.connection_result: Optional[asyncio.Future] = None
|
|
475
|
+
self.disconnection_result: Optional[asyncio.Future] = None
|
|
471
476
|
self.drained = asyncio.Event()
|
|
472
477
|
self.drained.set()
|
|
478
|
+
# Queued packets when sink is not set.
|
|
479
|
+
self._enqueued_rx_packets: collections.deque[bytes] = collections.deque(
|
|
480
|
+
maxlen=DEFAULT_RX_QUEUE_SIZE
|
|
481
|
+
)
|
|
482
|
+
self._sink: Optional[Callable[[bytes], None]] = None
|
|
473
483
|
|
|
474
484
|
# Compute the MTU
|
|
475
485
|
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
|
476
486
|
self.mtu = min(
|
|
477
|
-
|
|
487
|
+
tx_max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead
|
|
478
488
|
)
|
|
479
489
|
|
|
490
|
+
@property
|
|
491
|
+
def sink(self) -> Optional[Callable[[bytes], None]]:
|
|
492
|
+
return self._sink
|
|
493
|
+
|
|
494
|
+
@sink.setter
|
|
495
|
+
def sink(self, sink: Optional[Callable[[bytes], None]]) -> None:
|
|
496
|
+
self._sink = sink
|
|
497
|
+
# Dump queued packets to sink
|
|
498
|
+
if sink:
|
|
499
|
+
for packet in self._enqueued_rx_packets:
|
|
500
|
+
sink(packet) # pylint: disable=not-callable
|
|
501
|
+
self._enqueued_rx_packets.clear()
|
|
502
|
+
|
|
480
503
|
def change_state(self, new_state: State) -> None:
|
|
481
504
|
logger.debug(f'{self} state change -> {color(new_state.name, "magenta")}')
|
|
482
505
|
self.state = new_state
|
|
@@ -507,20 +530,35 @@ class DLC(EventEmitter):
|
|
|
507
530
|
self.emit('open')
|
|
508
531
|
|
|
509
532
|
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
|
510
|
-
if self.state
|
|
533
|
+
if self.state == DLC.State.CONNECTING:
|
|
534
|
+
# Exchange the modem status with the peer
|
|
535
|
+
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
|
536
|
+
mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.MSC, c_r=1, data=bytes(msc))
|
|
537
|
+
logger.debug(f'>>> MCC MSC Command: {msc}')
|
|
538
|
+
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
|
539
|
+
|
|
540
|
+
self.change_state(DLC.State.CONNECTED)
|
|
541
|
+
if self.connection_result:
|
|
542
|
+
self.connection_result.set_result(None)
|
|
543
|
+
self.connection_result = None
|
|
544
|
+
self.multiplexer.on_dlc_open_complete(self)
|
|
545
|
+
elif self.state == DLC.State.DISCONNECTING:
|
|
546
|
+
self.change_state(DLC.State.DISCONNECTED)
|
|
547
|
+
if self.disconnection_result:
|
|
548
|
+
self.disconnection_result.set_result(None)
|
|
549
|
+
self.disconnection_result = None
|
|
550
|
+
self.multiplexer.on_dlc_disconnection(self)
|
|
551
|
+
self.emit('close')
|
|
552
|
+
else:
|
|
511
553
|
logger.warning(
|
|
512
|
-
color(
|
|
554
|
+
color(
|
|
555
|
+
(
|
|
556
|
+
'!!! received UA frame when not in '
|
|
557
|
+
'CONNECTING or DISCONNECTING state'
|
|
558
|
+
),
|
|
559
|
+
'red',
|
|
560
|
+
)
|
|
513
561
|
)
|
|
514
|
-
return
|
|
515
|
-
|
|
516
|
-
# Exchange the modem status with the peer
|
|
517
|
-
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
|
518
|
-
mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.MSC, c_r=1, data=bytes(msc))
|
|
519
|
-
logger.debug(f'>>> MCC MSC Command: {msc}')
|
|
520
|
-
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
|
521
|
-
|
|
522
|
-
self.change_state(DLC.State.CONNECTED)
|
|
523
|
-
self.multiplexer.on_dlc_open_complete(self)
|
|
524
562
|
|
|
525
563
|
def on_dm_frame(self, frame: RFCOMM_Frame) -> None:
|
|
526
564
|
# TODO: handle all states
|
|
@@ -549,8 +587,15 @@ class DLC(EventEmitter):
|
|
|
549
587
|
f'rx_credits={self.rx_credits}: {data.hex()}'
|
|
550
588
|
)
|
|
551
589
|
if data:
|
|
552
|
-
if self.
|
|
553
|
-
self.
|
|
590
|
+
if self._sink:
|
|
591
|
+
self._sink(data) # pylint: disable=not-callable
|
|
592
|
+
else:
|
|
593
|
+
self._enqueued_rx_packets.append(data)
|
|
594
|
+
if (
|
|
595
|
+
self._enqueued_rx_packets.maxlen
|
|
596
|
+
and len(self._enqueued_rx_packets) >= self._enqueued_rx_packets.maxlen
|
|
597
|
+
):
|
|
598
|
+
logger.warning(f'DLC [{self.dlci}] received packet queue is full')
|
|
554
599
|
|
|
555
600
|
# Update the credits
|
|
556
601
|
if self.rx_credits > 0:
|
|
@@ -584,6 +629,19 @@ class DLC(EventEmitter):
|
|
|
584
629
|
self.connection_result = asyncio.get_running_loop().create_future()
|
|
585
630
|
self.send_frame(RFCOMM_Frame.sabm(c_r=self.c_r, dlci=self.dlci))
|
|
586
631
|
|
|
632
|
+
async def disconnect(self) -> None:
|
|
633
|
+
if self.state != DLC.State.CONNECTED:
|
|
634
|
+
raise InvalidStateError('invalid state')
|
|
635
|
+
|
|
636
|
+
self.disconnection_result = asyncio.get_running_loop().create_future()
|
|
637
|
+
self.change_state(DLC.State.DISCONNECTING)
|
|
638
|
+
self.send_frame(
|
|
639
|
+
RFCOMM_Frame.disc(
|
|
640
|
+
c_r=1 if self.role == Multiplexer.Role.INITIATOR else 0, dlci=self.dlci
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
await self.disconnection_result
|
|
644
|
+
|
|
587
645
|
def accept(self) -> None:
|
|
588
646
|
if self.state != DLC.State.INIT:
|
|
589
647
|
raise InvalidStateError('invalid state')
|
|
@@ -593,9 +651,9 @@ class DLC(EventEmitter):
|
|
|
593
651
|
cl=0xE0,
|
|
594
652
|
priority=7,
|
|
595
653
|
ack_timer=0,
|
|
596
|
-
max_frame_size=self.
|
|
654
|
+
max_frame_size=self.rx_max_frame_size,
|
|
597
655
|
max_retransmissions=0,
|
|
598
|
-
|
|
656
|
+
initial_credits=self.rx_initial_credits,
|
|
599
657
|
)
|
|
600
658
|
mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=0, data=bytes(pn))
|
|
601
659
|
logger.debug(f'>>> PN Response: {pn}')
|
|
@@ -603,8 +661,8 @@ class DLC(EventEmitter):
|
|
|
603
661
|
self.change_state(DLC.State.CONNECTING)
|
|
604
662
|
|
|
605
663
|
def rx_credits_needed(self) -> int:
|
|
606
|
-
if self.rx_credits <= self.
|
|
607
|
-
return self.
|
|
664
|
+
if self.rx_credits <= self.rx_credits_threshold:
|
|
665
|
+
return self.rx_max_credits - self.rx_credits
|
|
608
666
|
|
|
609
667
|
return 0
|
|
610
668
|
|
|
@@ -664,6 +722,17 @@ class DLC(EventEmitter):
|
|
|
664
722
|
async def drain(self) -> None:
|
|
665
723
|
await self.drained.wait()
|
|
666
724
|
|
|
725
|
+
def abort(self) -> None:
|
|
726
|
+
logger.debug(f'aborting DLC: {self}')
|
|
727
|
+
if self.connection_result:
|
|
728
|
+
self.connection_result.cancel()
|
|
729
|
+
self.connection_result = None
|
|
730
|
+
if self.disconnection_result:
|
|
731
|
+
self.disconnection_result.cancel()
|
|
732
|
+
self.disconnection_result = None
|
|
733
|
+
self.change_state(DLC.State.RESET)
|
|
734
|
+
self.emit('close')
|
|
735
|
+
|
|
667
736
|
def __str__(self) -> str:
|
|
668
737
|
return f'DLC(dlci={self.dlci},state={self.state.name})'
|
|
669
738
|
|
|
@@ -686,7 +755,7 @@ class Multiplexer(EventEmitter):
|
|
|
686
755
|
connection_result: Optional[asyncio.Future]
|
|
687
756
|
disconnection_result: Optional[asyncio.Future]
|
|
688
757
|
open_result: Optional[asyncio.Future]
|
|
689
|
-
acceptor: Optional[Callable[[int],
|
|
758
|
+
acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
|
|
690
759
|
dlcs: Dict[int, DLC]
|
|
691
760
|
|
|
692
761
|
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
|
|
@@ -698,11 +767,15 @@ class Multiplexer(EventEmitter):
|
|
|
698
767
|
self.connection_result = None
|
|
699
768
|
self.disconnection_result = None
|
|
700
769
|
self.open_result = None
|
|
770
|
+
self.open_pn: Optional[RFCOMM_MCC_PN] = None
|
|
771
|
+
self.open_rx_max_credits = 0
|
|
701
772
|
self.acceptor = None
|
|
702
773
|
|
|
703
774
|
# Become a sink for the L2CAP channel
|
|
704
775
|
l2cap_channel.sink = self.on_pdu
|
|
705
776
|
|
|
777
|
+
l2cap_channel.on('close', self.on_l2cap_channel_close)
|
|
778
|
+
|
|
706
779
|
def change_state(self, new_state: State) -> None:
|
|
707
780
|
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
|
708
781
|
self.state = new_state
|
|
@@ -766,6 +839,7 @@ class Multiplexer(EventEmitter):
|
|
|
766
839
|
'rfcomm',
|
|
767
840
|
)
|
|
768
841
|
)
|
|
842
|
+
self.open_result = None
|
|
769
843
|
else:
|
|
770
844
|
logger.warning(f'unexpected state for DM: {self}')
|
|
771
845
|
|
|
@@ -803,9 +877,16 @@ class Multiplexer(EventEmitter):
|
|
|
803
877
|
else:
|
|
804
878
|
if self.acceptor:
|
|
805
879
|
channel_number = pn.dlci >> 1
|
|
806
|
-
if self.acceptor(channel_number):
|
|
880
|
+
if dlc_params := self.acceptor(channel_number):
|
|
807
881
|
# Create a new DLC
|
|
808
|
-
dlc = DLC(
|
|
882
|
+
dlc = DLC(
|
|
883
|
+
self,
|
|
884
|
+
dlci=pn.dlci,
|
|
885
|
+
tx_max_frame_size=pn.max_frame_size,
|
|
886
|
+
tx_initial_credits=pn.initial_credits,
|
|
887
|
+
rx_max_frame_size=dlc_params[0],
|
|
888
|
+
rx_initial_credits=dlc_params[1],
|
|
889
|
+
)
|
|
809
890
|
self.dlcs[pn.dlci] = dlc
|
|
810
891
|
|
|
811
892
|
# Re-emit the handshake completion event
|
|
@@ -823,8 +904,17 @@ class Multiplexer(EventEmitter):
|
|
|
823
904
|
# Response
|
|
824
905
|
logger.debug(f'>>> PN Response: {pn}')
|
|
825
906
|
if self.state == Multiplexer.State.OPENING:
|
|
826
|
-
|
|
907
|
+
assert self.open_pn
|
|
908
|
+
dlc = DLC(
|
|
909
|
+
self,
|
|
910
|
+
dlci=pn.dlci,
|
|
911
|
+
tx_max_frame_size=pn.max_frame_size,
|
|
912
|
+
tx_initial_credits=pn.initial_credits,
|
|
913
|
+
rx_max_frame_size=self.open_pn.max_frame_size,
|
|
914
|
+
rx_initial_credits=self.open_pn.initial_credits,
|
|
915
|
+
)
|
|
827
916
|
self.dlcs[pn.dlci] = dlc
|
|
917
|
+
self.open_pn = None
|
|
828
918
|
dlc.connect()
|
|
829
919
|
else:
|
|
830
920
|
logger.warning('ignoring PN response')
|
|
@@ -862,7 +952,7 @@ class Multiplexer(EventEmitter):
|
|
|
862
952
|
self,
|
|
863
953
|
channel: int,
|
|
864
954
|
max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
|
|
865
|
-
|
|
955
|
+
initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
|
|
866
956
|
) -> DLC:
|
|
867
957
|
if self.state != Multiplexer.State.CONNECTED:
|
|
868
958
|
if self.state == Multiplexer.State.OPENING:
|
|
@@ -870,17 +960,19 @@ class Multiplexer(EventEmitter):
|
|
|
870
960
|
|
|
871
961
|
raise InvalidStateError('not connected')
|
|
872
962
|
|
|
873
|
-
|
|
963
|
+
self.open_pn = RFCOMM_MCC_PN(
|
|
874
964
|
dlci=channel << 1,
|
|
875
965
|
cl=0xF0,
|
|
876
966
|
priority=7,
|
|
877
967
|
ack_timer=0,
|
|
878
968
|
max_frame_size=max_frame_size,
|
|
879
969
|
max_retransmissions=0,
|
|
880
|
-
|
|
970
|
+
initial_credits=initial_credits,
|
|
881
971
|
)
|
|
882
|
-
mcc = RFCOMM_Frame.make_mcc(
|
|
883
|
-
|
|
972
|
+
mcc = RFCOMM_Frame.make_mcc(
|
|
973
|
+
mcc_type=MccType.PN, c_r=1, data=bytes(self.open_pn)
|
|
974
|
+
)
|
|
975
|
+
logger.debug(f'>>> Sending MCC: {self.open_pn}')
|
|
884
976
|
self.open_result = asyncio.get_running_loop().create_future()
|
|
885
977
|
self.change_state(Multiplexer.State.OPENING)
|
|
886
978
|
self.send_frame(
|
|
@@ -890,15 +982,31 @@ class Multiplexer(EventEmitter):
|
|
|
890
982
|
information=mcc,
|
|
891
983
|
)
|
|
892
984
|
)
|
|
893
|
-
|
|
894
|
-
self.open_result = None
|
|
895
|
-
return result
|
|
985
|
+
return await self.open_result
|
|
896
986
|
|
|
897
987
|
def on_dlc_open_complete(self, dlc: DLC) -> None:
|
|
898
988
|
logger.debug(f'DLC [{dlc.dlci}] open complete')
|
|
989
|
+
|
|
899
990
|
self.change_state(Multiplexer.State.CONNECTED)
|
|
991
|
+
|
|
900
992
|
if self.open_result:
|
|
901
993
|
self.open_result.set_result(dlc)
|
|
994
|
+
self.open_result = None
|
|
995
|
+
|
|
996
|
+
def on_dlc_disconnection(self, dlc: DLC) -> None:
|
|
997
|
+
logger.debug(f'DLC [{dlc.dlci}] disconnection')
|
|
998
|
+
self.dlcs.pop(dlc.dlci, None)
|
|
999
|
+
|
|
1000
|
+
def on_l2cap_channel_close(self) -> None:
|
|
1001
|
+
logger.debug('L2CAP channel closed, cleaning up')
|
|
1002
|
+
if self.open_result:
|
|
1003
|
+
self.open_result.cancel()
|
|
1004
|
+
self.open_result = None
|
|
1005
|
+
if self.disconnection_result:
|
|
1006
|
+
self.disconnection_result.cancel()
|
|
1007
|
+
self.disconnection_result = None
|
|
1008
|
+
for dlc in self.dlcs.values():
|
|
1009
|
+
dlc.abort()
|
|
902
1010
|
|
|
903
1011
|
def __str__(self) -> str:
|
|
904
1012
|
return f'Multiplexer(state={self.state.name})'
|
|
@@ -957,15 +1065,13 @@ class Client:
|
|
|
957
1065
|
|
|
958
1066
|
# -----------------------------------------------------------------------------
|
|
959
1067
|
class Server(EventEmitter):
|
|
960
|
-
acceptors: Dict[int, Callable[[DLC], None]]
|
|
961
|
-
|
|
962
1068
|
def __init__(
|
|
963
1069
|
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
|
964
1070
|
) -> None:
|
|
965
1071
|
super().__init__()
|
|
966
1072
|
self.device = device
|
|
967
|
-
self.
|
|
968
|
-
self.
|
|
1073
|
+
self.acceptors: Dict[int, Callable[[DLC], None]] = {}
|
|
1074
|
+
self.dlc_configs: Dict[int, Tuple[int, int]] = {}
|
|
969
1075
|
|
|
970
1076
|
# Register ourselves with the L2CAP channel manager
|
|
971
1077
|
self.l2cap_server = device.create_l2cap_server(
|
|
@@ -973,7 +1079,13 @@ class Server(EventEmitter):
|
|
|
973
1079
|
handler=self.on_connection,
|
|
974
1080
|
)
|
|
975
1081
|
|
|
976
|
-
def listen(
|
|
1082
|
+
def listen(
|
|
1083
|
+
self,
|
|
1084
|
+
acceptor: Callable[[DLC], None],
|
|
1085
|
+
channel: int = 0,
|
|
1086
|
+
max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
|
|
1087
|
+
initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
|
|
1088
|
+
) -> int:
|
|
977
1089
|
if channel:
|
|
978
1090
|
if channel in self.acceptors:
|
|
979
1091
|
# Busy
|
|
@@ -993,6 +1105,8 @@ class Server(EventEmitter):
|
|
|
993
1105
|
return 0
|
|
994
1106
|
|
|
995
1107
|
self.acceptors[channel] = acceptor
|
|
1108
|
+
self.dlc_configs[channel] = (max_frame_size, initial_credits)
|
|
1109
|
+
|
|
996
1110
|
return channel
|
|
997
1111
|
|
|
998
1112
|
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
|
@@ -1010,15 +1124,14 @@ class Server(EventEmitter):
|
|
|
1010
1124
|
# Notify
|
|
1011
1125
|
self.emit('start', multiplexer)
|
|
1012
1126
|
|
|
1013
|
-
def accept_dlc(self, channel_number: int) ->
|
|
1014
|
-
return
|
|
1127
|
+
def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
|
|
1128
|
+
return self.dlc_configs.get(channel_number)
|
|
1015
1129
|
|
|
1016
1130
|
def on_dlc(self, dlc: DLC) -> None:
|
|
1017
1131
|
logger.debug(f'@@@ new DLC connected: {dlc}')
|
|
1018
1132
|
|
|
1019
1133
|
# Let the acceptor know
|
|
1020
|
-
acceptor
|
|
1021
|
-
if acceptor:
|
|
1134
|
+
if acceptor := self.acceptors.get(dlc.dlci >> 1):
|
|
1022
1135
|
acceptor(dlc)
|
|
1023
1136
|
|
|
1024
1137
|
def __enter__(self) -> Self:
|
bumble/sdp.py
CHANGED
|
@@ -997,7 +997,7 @@ class Server:
|
|
|
997
997
|
try:
|
|
998
998
|
handler(sdp_pdu)
|
|
999
999
|
except Exception as error:
|
|
1000
|
-
logger.
|
|
1000
|
+
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
|
|
1001
1001
|
self.send_response(
|
|
1002
1002
|
SDP_ErrorResponse(
|
|
1003
1003
|
transaction_id=sdp_pdu.transaction_id,
|
bumble/transport/common.py
CHANGED
|
@@ -425,6 +425,10 @@ class SnoopingTransport(Transport):
|
|
|
425
425
|
class Source:
|
|
426
426
|
sink: TransportSink
|
|
427
427
|
|
|
428
|
+
@property
|
|
429
|
+
def metadata(self) -> dict[str, Any]:
|
|
430
|
+
return getattr(self.source, 'metadata', {})
|
|
431
|
+
|
|
428
432
|
def __init__(self, source: TransportSource, snooper: Snooper):
|
|
429
433
|
self.source = source
|
|
430
434
|
self.snooper = snooper
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bumble
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.194
|
|
4
4
|
Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
|
5
5
|
Home-page: https://github.com/google/bumble
|
|
6
6
|
Author: Google
|
|
@@ -35,13 +35,14 @@ Provides-Extra: development
|
|
|
35
35
|
Requires-Dist: black ==24.3 ; extra == 'development'
|
|
36
36
|
Requires-Dist: grpcio-tools >=1.62.1 ; extra == 'development'
|
|
37
37
|
Requires-Dist: invoke >=1.7.3 ; extra == 'development'
|
|
38
|
-
Requires-Dist: mypy ==1.
|
|
38
|
+
Requires-Dist: mypy ==1.10.0 ; extra == 'development'
|
|
39
39
|
Requires-Dist: nox >=2022 ; extra == 'development'
|
|
40
|
-
Requires-Dist: pylint ==
|
|
40
|
+
Requires-Dist: pylint ==3.1.0 ; extra == 'development'
|
|
41
41
|
Requires-Dist: pyyaml >=6.0 ; extra == 'development'
|
|
42
42
|
Requires-Dist: types-appdirs >=1.4.3 ; extra == 'development'
|
|
43
43
|
Requires-Dist: types-invoke >=1.7.3 ; extra == 'development'
|
|
44
44
|
Requires-Dist: types-protobuf >=4.21.0 ; extra == 'development'
|
|
45
|
+
Requires-Dist: wasmtime ==20.0.0 ; extra == 'development'
|
|
45
46
|
Provides-Extra: documentation
|
|
46
47
|
Requires-Dist: mkdocs >=1.4.0 ; extra == 'documentation'
|
|
47
48
|
Requires-Dist: mkdocs-material >=8.5.6 ; extra == 'documentation'
|
|
@@ -49,7 +50,7 @@ Requires-Dist: mkdocstrings[python] >=0.19.0 ; extra == 'documentation'
|
|
|
49
50
|
Provides-Extra: pandora
|
|
50
51
|
Requires-Dist: bt-test-interfaces >=0.0.6 ; extra == 'pandora'
|
|
51
52
|
Provides-Extra: test
|
|
52
|
-
Requires-Dist: pytest >=8.
|
|
53
|
+
Requires-Dist: pytest >=8.2 ; extra == 'test'
|
|
53
54
|
Requires-Dist: pytest-asyncio >=0.23.5 ; extra == 'test'
|
|
54
55
|
Requires-Dist: pytest-html >=3.2.0 ; extra == 'test'
|
|
55
56
|
Requires-Dist: coverage >=6.4 ; extra == 'test'
|