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/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
- supported_audio_channel_counts: Sequence[int]
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
- supported_audio_channel_counts = [1]
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
- supported_audio_channel_counts = bits_to_channel_counts(value)
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
- supported_audio_channel_counts=supported_audio_channel_counts,
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.supported_audio_channel_counts),
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 cis_id == self.cis_id and self.state == self.State.ENABLING:
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 cis_link.cis_id == self.cis_id and self.state == self.State.ENABLING:
880
- self.state = self.State.STREAMING
881
- self.cis_link = cis_link
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.state = self.State.DISABLING
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 self.state != AseStateMachine.State.DISABLING:
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 = 2048
108
- RFCOMM_DEFAULT_WINDOW_SIZE = 7
109
- RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
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
- window_size: int
370
+ initial_credits: int
367
371
 
368
372
  def __post_init__(self) -> None:
369
- if self.window_size < 1 or self.window_size > 7:
373
+ if self.initial_credits < 1 or self.initial_credits > 7:
370
374
  logger.warning(
371
- f'Error Recovery Window size {self.window_size} is out of range [1, 7].'
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
- window_size=data[7] & 0x07,
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.window_size & 0x07,
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
- max_frame_size: int,
455
- window_size: int,
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.max_frame_size = max_frame_size
461
- self.window_size = window_size
462
- self.rx_credits = window_size
463
- self.rx_threshold = window_size // 2
464
- self.tx_credits = window_size
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.sink = None
470
- self.connection_result = None
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
- max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead
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 != DLC.State.CONNECTING:
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('!!! received SABM when not in CONNECTING state', 'red')
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.sink:
553
- self.sink(data) # pylint: disable=not-callable
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.max_frame_size,
654
+ max_frame_size=self.rx_max_frame_size,
597
655
  max_retransmissions=0,
598
- window_size=self.window_size,
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.rx_threshold:
607
- return self.window_size - self.rx_credits
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], bool]]
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(self, pn.dlci, pn.max_frame_size, pn.window_size)
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
- dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
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
- window_size: int = RFCOMM_DEFAULT_WINDOW_SIZE,
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
- pn = RFCOMM_MCC_PN(
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
- window_size=window_size,
970
+ initial_credits=initial_credits,
881
971
  )
882
- mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=1, data=bytes(pn))
883
- logger.debug(f'>>> Sending MCC: {pn}')
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
- result = await self.open_result
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.multiplexer = None
968
- self.acceptors = {}
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(self, acceptor: Callable[[DLC], None], channel: int = 0) -> int:
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) -> bool:
1014
- return channel_number in self.acceptors
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 = self.acceptors.get(dlc.dlci >> 1)
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.warning(f'{color("!!! Exception in handler:", "red")} {error}')
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,
@@ -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.192
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.8.0 ; extra == 'development'
38
+ Requires-Dist: mypy ==1.10.0 ; extra == 'development'
39
39
  Requires-Dist: nox >=2022 ; extra == 'development'
40
- Requires-Dist: pylint ==2.15.8 ; extra == 'development'
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.0 ; extra == 'test'
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'