bumble 0.0.212__py3-none-any.whl → 0.0.214__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.
Files changed (92) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +14 -11
  5. bumble/apps/bench.py +482 -37
  6. bumble/apps/console.py +3 -3
  7. bumble/apps/controller_info.py +44 -12
  8. bumble/apps/controller_loopback.py +7 -7
  9. bumble/apps/controllers.py +4 -5
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +5 -5
  12. bumble/apps/gg_bridge.py +5 -5
  13. bumble/apps/hci_bridge.py +5 -4
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +8 -3
  16. bumble/apps/pair.py +19 -11
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/player/player.py +2 -3
  19. bumble/apps/rfcomm_bridge.py +3 -4
  20. bumble/apps/scan.py +4 -5
  21. bumble/apps/show.py +6 -4
  22. bumble/apps/speaker/speaker.html +1 -0
  23. bumble/apps/speaker/speaker.js +113 -62
  24. bumble/apps/speaker/speaker.py +123 -19
  25. bumble/apps/unbond.py +2 -3
  26. bumble/apps/usb_probe.py +2 -3
  27. bumble/at.py +4 -4
  28. bumble/att.py +2 -6
  29. bumble/avc.py +7 -7
  30. bumble/avctp.py +3 -3
  31. bumble/avdtp.py +16 -20
  32. bumble/avrcp.py +42 -54
  33. bumble/colors.py +2 -2
  34. bumble/controller.py +174 -45
  35. bumble/device.py +398 -182
  36. bumble/drivers/__init__.py +2 -2
  37. bumble/drivers/common.py +0 -2
  38. bumble/drivers/intel.py +37 -40
  39. bumble/drivers/rtk.py +28 -35
  40. bumble/gatt.py +4 -4
  41. bumble/gatt_adapters.py +4 -5
  42. bumble/gatt_client.py +26 -31
  43. bumble/gatt_server.py +7 -11
  44. bumble/hci.py +2648 -2909
  45. bumble/helpers.py +4 -5
  46. bumble/hfp.py +32 -37
  47. bumble/host.py +104 -35
  48. bumble/keys.py +5 -5
  49. bumble/l2cap.py +312 -409
  50. bumble/link.py +16 -280
  51. bumble/logging.py +65 -0
  52. bumble/pairing.py +23 -20
  53. bumble/pandora/__init__.py +2 -2
  54. bumble/pandora/config.py +2 -2
  55. bumble/pandora/device.py +6 -6
  56. bumble/pandora/host.py +27 -28
  57. bumble/pandora/l2cap.py +2 -2
  58. bumble/pandora/security.py +6 -6
  59. bumble/pandora/utils.py +3 -3
  60. bumble/profiles/ams.py +404 -0
  61. bumble/profiles/ascs.py +142 -131
  62. bumble/profiles/asha.py +2 -2
  63. bumble/profiles/bap.py +3 -4
  64. bumble/profiles/csip.py +2 -2
  65. bumble/profiles/device_information_service.py +2 -2
  66. bumble/profiles/gap.py +2 -2
  67. bumble/profiles/hap.py +34 -33
  68. bumble/profiles/le_audio.py +4 -4
  69. bumble/profiles/mcp.py +4 -4
  70. bumble/profiles/vcs.py +3 -5
  71. bumble/rfcomm.py +10 -10
  72. bumble/rtp.py +1 -2
  73. bumble/sdp.py +2 -2
  74. bumble/smp.py +62 -63
  75. bumble/tools/intel_util.py +3 -2
  76. bumble/tools/rtk_util.py +6 -5
  77. bumble/transport/__init__.py +2 -16
  78. bumble/transport/android_netsim.py +5 -5
  79. bumble/transport/common.py +4 -4
  80. bumble/transport/pyusb.py +2 -2
  81. bumble/utils.py +2 -5
  82. bumble/vendor/android/hci.py +118 -200
  83. bumble/vendor/zephyr/hci.py +32 -27
  84. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/METADATA +4 -3
  85. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
  86. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
  87. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -1
  88. bumble/apps/link_relay/__init__.py +0 -0
  89. bumble/apps/link_relay/link_relay.py +0 -289
  90. bumble/apps/link_relay/logging.yml +0 -21
  91. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
  92. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/top_level.txt +0 -0
bumble/helpers.py CHANGED
@@ -34,9 +34,8 @@ from bumble.att import ATT_CID, ATT_PDU
34
34
  from bumble.smp import SMP_CID, SMP_Command
35
35
  from bumble.core import name_or_number
36
36
  from bumble.l2cap import (
37
+ CommandCode,
37
38
  L2CAP_PDU,
38
- L2CAP_CONNECTION_REQUEST,
39
- L2CAP_CONNECTION_RESPONSE,
40
39
  L2CAP_SIGNALING_CID,
41
40
  L2CAP_LE_SIGNALING_CID,
42
41
  L2CAP_Control_Frame,
@@ -106,14 +105,14 @@ class PacketTracer:
106
105
  self.analyzer.emit(control_frame)
107
106
 
108
107
  # Check if this signals a new channel
109
- if control_frame.code == L2CAP_CONNECTION_REQUEST:
108
+ if control_frame.code == CommandCode.L2CAP_CONNECTION_REQUEST:
110
109
  connection_request = cast(L2CAP_Connection_Request, control_frame)
111
110
  self.psms[connection_request.source_cid] = connection_request.psm
112
- elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
111
+ elif control_frame.code == CommandCode.L2CAP_CONNECTION_RESPONSE:
113
112
  connection_response = cast(L2CAP_Connection_Response, control_frame)
114
113
  if (
115
114
  connection_response.result
116
- == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
115
+ == L2CAP_Connection_Response.Result.CONNECTION_SUCCESSFUL
117
116
  ):
118
117
  if self.peer and (
119
118
  psm := self.peer.psms.get(connection_response.source_cid)
bumble/hfp.py CHANGED
@@ -26,14 +26,9 @@ import enum
26
26
  import traceback
27
27
  import re
28
28
  from typing import (
29
- Dict,
30
- List,
31
29
  Union,
32
- Set,
33
30
  Any,
34
31
  Optional,
35
- Type,
36
- Tuple,
37
32
  ClassVar,
38
33
  Iterable,
39
34
  TYPE_CHECKING,
@@ -375,7 +370,7 @@ class CallLineIdentification:
375
370
  cli_validity: Optional[int] = None
376
371
 
377
372
  @classmethod
378
- def parse_from(cls: Type[Self], parameters: List[bytes]) -> Self:
373
+ def parse_from(cls, parameters: list[bytes]) -> Self:
379
374
  return cls(
380
375
  number=parameters[0].decode(),
381
376
  type=int(parameters[1]),
@@ -505,9 +500,9 @@ STATUS_CODES = {
505
500
 
506
501
  @dataclasses.dataclass
507
502
  class HfConfiguration:
508
- supported_hf_features: List[HfFeature]
509
- supported_hf_indicators: List[HfIndicator]
510
- supported_audio_codecs: List[AudioCodec]
503
+ supported_hf_features: list[HfFeature]
504
+ supported_hf_indicators: list[HfIndicator]
505
+ supported_audio_codecs: list[AudioCodec]
511
506
 
512
507
 
513
508
  @dataclasses.dataclass
@@ -535,7 +530,7 @@ class AtResponse:
535
530
  parameters: list
536
531
 
537
532
  @classmethod
538
- def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
533
+ def parse_from(cls: type[Self], buffer: bytearray) -> Self:
539
534
  code_and_parameters = buffer.split(b':')
540
535
  parameters = (
541
536
  code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
@@ -563,7 +558,7 @@ class AtCommand:
563
558
  )
564
559
 
565
560
  @classmethod
566
- def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
561
+ def parse_from(cls: type[Self], buffer: bytearray) -> Self:
567
562
  if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
568
563
  if buffer.startswith(b'ATA'):
569
564
  return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
@@ -598,7 +593,7 @@ class AgIndicatorState:
598
593
  """
599
594
 
600
595
  indicator: AgIndicator
601
- supported_values: Set[int]
596
+ supported_values: set[int]
602
597
  current_status: int
603
598
  index: Optional[int] = None
604
599
  enabled: bool = True
@@ -616,14 +611,14 @@ class AgIndicatorState:
616
611
  return f'(\"{self.indicator.value}\",{supported_values_text})'
617
612
 
618
613
  @classmethod
619
- def call(cls: Type[Self]) -> Self:
614
+ def call(cls: type[Self]) -> Self:
620
615
  """Default call indicator state."""
621
616
  return cls(
622
617
  indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
623
618
  )
624
619
 
625
620
  @classmethod
626
- def callsetup(cls: Type[Self]) -> Self:
621
+ def callsetup(cls: type[Self]) -> Self:
627
622
  """Default callsetup indicator state."""
628
623
  return cls(
629
624
  indicator=AgIndicator.CALL_SETUP,
@@ -632,7 +627,7 @@ class AgIndicatorState:
632
627
  )
633
628
 
634
629
  @classmethod
635
- def callheld(cls: Type[Self]) -> Self:
630
+ def callheld(cls: type[Self]) -> Self:
636
631
  """Default call indicator state."""
637
632
  return cls(
638
633
  indicator=AgIndicator.CALL_HELD,
@@ -641,14 +636,14 @@ class AgIndicatorState:
641
636
  )
642
637
 
643
638
  @classmethod
644
- def service(cls: Type[Self]) -> Self:
639
+ def service(cls: type[Self]) -> Self:
645
640
  """Default service indicator state."""
646
641
  return cls(
647
642
  indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
648
643
  )
649
644
 
650
645
  @classmethod
651
- def signal(cls: Type[Self]) -> Self:
646
+ def signal(cls: type[Self]) -> Self:
652
647
  """Default signal indicator state."""
653
648
  return cls(
654
649
  indicator=AgIndicator.SIGNAL,
@@ -657,14 +652,14 @@ class AgIndicatorState:
657
652
  )
658
653
 
659
654
  @classmethod
660
- def roam(cls: Type[Self]) -> Self:
655
+ def roam(cls: type[Self]) -> Self:
661
656
  """Default roam indicator state."""
662
657
  return cls(
663
658
  indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
664
659
  )
665
660
 
666
661
  @classmethod
667
- def battchg(cls: Type[Self]) -> Self:
662
+ def battchg(cls: type[Self]) -> Self:
668
663
  """Default battery charge indicator state."""
669
664
  return cls(
670
665
  indicator=AgIndicator.BATTERY_CHARGE,
@@ -732,13 +727,13 @@ class HfProtocol(utils.EventEmitter):
732
727
  """Termination signal for run() loop."""
733
728
 
734
729
  supported_hf_features: int
735
- supported_audio_codecs: List[AudioCodec]
730
+ supported_audio_codecs: list[AudioCodec]
736
731
 
737
732
  supported_ag_features: int
738
- supported_ag_call_hold_operations: List[CallHoldOperation]
733
+ supported_ag_call_hold_operations: list[CallHoldOperation]
739
734
 
740
- ag_indicators: List[AgIndicatorState]
741
- hf_indicators: Dict[HfIndicator, HfIndicatorState]
735
+ ag_indicators: list[AgIndicatorState]
736
+ hf_indicators: dict[HfIndicator, HfIndicatorState]
742
737
 
743
738
  dlc: rfcomm.DLC
744
739
  command_lock: asyncio.Lock
@@ -836,7 +831,7 @@ class HfProtocol(utils.EventEmitter):
836
831
  cmd: str,
837
832
  timeout: float = 1.0,
838
833
  response_type: AtResponseType = AtResponseType.NONE,
839
- ) -> Union[None, AtResponse, List[AtResponse]]:
834
+ ) -> Union[None, AtResponse, list[AtResponse]]:
840
835
  """
841
836
  Sends an AT command and wait for the peer response.
842
837
  Wait for the AT responses sent by the peer, to the status code.
@@ -853,7 +848,7 @@ class HfProtocol(utils.EventEmitter):
853
848
  async with self.command_lock:
854
849
  logger.debug(f">>> {cmd}")
855
850
  self.dlc.write(cmd + '\r')
856
- responses: List[AtResponse] = []
851
+ responses: list[AtResponse] = []
857
852
 
858
853
  while True:
859
854
  result = await asyncio.wait_for(
@@ -1073,7 +1068,7 @@ class HfProtocol(utils.EventEmitter):
1073
1068
  # code, with the value indicating (call=0).
1074
1069
  await self.execute_command("AT+CHUP")
1075
1070
 
1076
- async def query_current_calls(self) -> List[CallInfo]:
1071
+ async def query_current_calls(self) -> list[CallInfo]:
1077
1072
  """4.32.1 Query List of Current Calls in AG.
1078
1073
 
1079
1074
  Return:
@@ -1204,27 +1199,27 @@ class AgProtocol(utils.EventEmitter):
1204
1199
  EVENT_MICROPHONE_VOLUME = "microphone_volume"
1205
1200
 
1206
1201
  supported_hf_features: int
1207
- supported_hf_indicators: Set[HfIndicator]
1208
- supported_audio_codecs: List[AudioCodec]
1202
+ supported_hf_indicators: set[HfIndicator]
1203
+ supported_audio_codecs: list[AudioCodec]
1209
1204
 
1210
1205
  supported_ag_features: int
1211
- supported_ag_call_hold_operations: List[CallHoldOperation]
1206
+ supported_ag_call_hold_operations: list[CallHoldOperation]
1212
1207
 
1213
- ag_indicators: List[AgIndicatorState]
1208
+ ag_indicators: list[AgIndicatorState]
1214
1209
  hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
1215
1210
 
1216
1211
  dlc: rfcomm.DLC
1217
1212
 
1218
1213
  read_buffer: bytearray
1219
1214
  active_codec: AudioCodec
1220
- calls: List[CallInfo]
1215
+ calls: list[CallInfo]
1221
1216
 
1222
1217
  indicator_report_enabled: bool
1223
1218
  inband_ringtone_enabled: bool
1224
1219
  cme_error_enabled: bool
1225
1220
  cli_notification_enabled: bool
1226
1221
  call_waiting_enabled: bool
1227
- _remained_slc_setup_features: Set[HfFeature]
1222
+ _remained_slc_setup_features: set[HfFeature]
1228
1223
 
1229
1224
  def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
1230
1225
  super().__init__()
@@ -1694,7 +1689,7 @@ def make_hf_sdp_records(
1694
1689
  rfcomm_channel: int,
1695
1690
  configuration: HfConfiguration,
1696
1691
  version: ProfileVersion = ProfileVersion.V1_8,
1697
- ) -> List[sdp.ServiceAttribute]:
1692
+ ) -> list[sdp.ServiceAttribute]:
1698
1693
  """
1699
1694
  Generates the SDP record for HFP Hands-Free support.
1700
1695
 
@@ -1780,7 +1775,7 @@ def make_ag_sdp_records(
1780
1775
  rfcomm_channel: int,
1781
1776
  configuration: AgConfiguration,
1782
1777
  version: ProfileVersion = ProfileVersion.V1_8,
1783
- ) -> List[sdp.ServiceAttribute]:
1778
+ ) -> list[sdp.ServiceAttribute]:
1784
1779
  """
1785
1780
  Generates the SDP record for HFP Audio-Gateway support.
1786
1781
 
@@ -1860,7 +1855,7 @@ def make_ag_sdp_records(
1860
1855
 
1861
1856
  async def find_hf_sdp_record(
1862
1857
  connection: device.Connection,
1863
- ) -> Optional[Tuple[int, ProfileVersion, HfSdpFeature]]:
1858
+ ) -> Optional[tuple[int, ProfileVersion, HfSdpFeature]]:
1864
1859
  """Searches a Hands-Free SDP record from remote device.
1865
1860
 
1866
1861
  Args:
@@ -1912,7 +1907,7 @@ async def find_hf_sdp_record(
1912
1907
 
1913
1908
  async def find_ag_sdp_record(
1914
1909
  connection: device.Connection,
1915
- ) -> Optional[Tuple[int, ProfileVersion, AgSdpFeature]]:
1910
+ ) -> Optional[tuple[int, ProfileVersion, AgSdpFeature]]:
1916
1911
  """Searches an Audio-Gateway SDP record from remote device.
1917
1912
 
1918
1913
  Args:
@@ -2010,7 +2005,7 @@ class EscoParameters:
2010
2005
  transmit_codec_frame_size: int = 60
2011
2006
  receive_codec_frame_size: int = 60
2012
2007
 
2013
- def asdict(self) -> Dict[str, Any]:
2008
+ def asdict(self) -> dict[str, Any]:
2014
2009
  # dataclasses.asdict() will recursively deep-copy the entire object,
2015
2010
  # which is expensive and breaks CodingFormat object, so let it simply copy here.
2016
2011
  return self.__dict__
bumble/host.py CHANGED
@@ -26,10 +26,7 @@ from typing import (
26
26
  Any,
27
27
  Awaitable,
28
28
  Callable,
29
- Deque,
30
- Dict,
31
29
  Optional,
32
- Set,
33
30
  cast,
34
31
  TYPE_CHECKING,
35
32
  )
@@ -41,7 +38,6 @@ from bumble.snoop import Snooper
41
38
  from bumble import drivers
42
39
  from bumble import hci
43
40
  from bumble.core import (
44
- PhysicalTransport,
45
41
  PhysicalTransport,
46
42
  ConnectionPHY,
47
43
  ConnectionParameters,
@@ -75,6 +71,11 @@ class DataPacketQueue(utils.EventEmitter):
75
71
 
76
72
  max_packet_size: int
77
73
 
74
+ class PerConnectionState:
75
+ def __init__(self) -> None:
76
+ self.in_flight = 0
77
+ self.drained = asyncio.Event()
78
+
78
79
  def __init__(
79
80
  self,
80
81
  max_packet_size: int,
@@ -85,11 +86,16 @@ class DataPacketQueue(utils.EventEmitter):
85
86
  self.max_packet_size = max_packet_size
86
87
  self.max_in_flight = max_in_flight
87
88
  self._in_flight = 0 # Total number of packets in flight across all connections
88
- self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
89
- int
90
- ) # Number of packets in flight per connection
89
+ self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
90
+ collections.defaultdict(DataPacketQueue.PerConnectionState)
91
+ )
92
+ self._drained_per_connection: dict[int, asyncio.Event] = (
93
+ collections.defaultdict(asyncio.Event)
94
+ )
91
95
  self._send = send
92
- self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
96
+ self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
97
+ collections.deque()
98
+ )
93
99
  self._queued = 0
94
100
  self._completed = 0
95
101
 
@@ -137,36 +143,40 @@ class DataPacketQueue(utils.EventEmitter):
137
143
  self._completed += flushed_count
138
144
  self._packets = collections.deque(packets_to_keep)
139
145
 
140
- if connection_handle in self._in_flight_per_connection:
141
- in_flight = self._in_flight_per_connection[connection_handle]
146
+ if connection_state := self._connection_state.pop(connection_handle, None):
147
+ in_flight = connection_state.in_flight
142
148
  self._completed += in_flight
143
149
  self._in_flight -= in_flight
144
- del self._in_flight_per_connection[connection_handle]
150
+ connection_state.drained.set()
145
151
 
146
152
  def _check_queue(self) -> None:
147
153
  while self._packets and self._in_flight < self.max_in_flight:
148
154
  packet, connection_handle = self._packets.pop()
149
155
  self._send(packet)
150
156
  self._in_flight += 1
151
- self._in_flight_per_connection[connection_handle] += 1
157
+ connection_state = self._connection_state[connection_handle]
158
+ connection_state.in_flight += 1
159
+ connection_state.drained.clear()
152
160
 
153
161
  def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
154
162
  """Mark one or more packets associated with a connection as completed."""
155
- if connection_handle not in self._in_flight_per_connection:
163
+ if connection_handle not in self._connection_state:
156
164
  logger.warning(
157
165
  f'received completion for unknown connection {connection_handle}'
158
166
  )
159
167
  return
160
168
 
161
- in_flight_for_connection = self._in_flight_per_connection[connection_handle]
162
- if packet_count <= in_flight_for_connection:
163
- self._in_flight_per_connection[connection_handle] -= packet_count
169
+ connection_state = self._connection_state[connection_handle]
170
+ if packet_count <= connection_state.in_flight:
171
+ connection_state.in_flight -= packet_count
164
172
  else:
165
173
  logger.warning(
166
174
  f'{packet_count} completed for {connection_handle} '
167
- f'but only {in_flight_for_connection} in flight'
175
+ f'but only {connection_state.in_flight} in flight'
168
176
  )
169
- self._in_flight_per_connection[connection_handle] = 0
177
+ connection_state.in_flight = 0
178
+ if connection_state.in_flight == 0:
179
+ connection_state.drained.set()
170
180
 
171
181
  if packet_count <= self._in_flight:
172
182
  self._in_flight -= packet_count
@@ -181,6 +191,13 @@ class DataPacketQueue(utils.EventEmitter):
181
191
  self._check_queue()
182
192
  self.emit('flow')
183
193
 
194
+ async def drain(self, connection_handle: int) -> None:
195
+ """Wait until there are no pending packets for a connection."""
196
+ if not (connection_state := self._connection_state.get(connection_handle)):
197
+ raise ValueError('no such connection')
198
+
199
+ await connection_state.drained.wait()
200
+
184
201
 
185
202
  # -----------------------------------------------------------------------------
186
203
  class Connection:
@@ -234,16 +251,16 @@ class IsoLink:
234
251
 
235
252
  # -----------------------------------------------------------------------------
236
253
  class Host(utils.EventEmitter):
237
- connections: Dict[int, Connection]
238
- cis_links: Dict[int, IsoLink]
239
- bis_links: Dict[int, IsoLink]
240
- sco_links: Dict[int, ScoLink]
254
+ connections: dict[int, Connection]
255
+ cis_links: dict[int, IsoLink]
256
+ bis_links: dict[int, IsoLink]
257
+ sco_links: dict[int, ScoLink]
241
258
  bigs: dict[int, set[int]]
242
259
  acl_packet_queue: Optional[DataPacketQueue] = None
243
260
  le_acl_packet_queue: Optional[DataPacketQueue] = None
244
261
  iso_packet_queue: Optional[DataPacketQueue] = None
245
262
  hci_sink: Optional[TransportSink] = None
246
- hci_metadata: Dict[str, Any]
263
+ hci_metadata: dict[str, Any]
247
264
  long_term_key_provider: Optional[
248
265
  Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
249
266
  ]
@@ -813,7 +830,7 @@ class Host(utils.EventEmitter):
813
830
  ) != 0
814
831
 
815
832
  @property
816
- def supported_commands(self) -> Set[int]:
833
+ def supported_commands(self) -> set[int]:
817
834
  return set(
818
835
  op_code
819
836
  for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
@@ -836,8 +853,8 @@ class Host(utils.EventEmitter):
836
853
  def on_packet(self, packet: bytes) -> None:
837
854
  try:
838
855
  hci_packet = hci.HCI_Packet.from_bytes(packet)
839
- except Exception as error:
840
- logger.warning(f'!!! error parsing packet from bytes: {error}')
856
+ except Exception:
857
+ logger.exception('!!! error parsing packet from bytes')
841
858
  return
842
859
 
843
860
  if self.ready or (
@@ -1127,11 +1144,19 @@ class Host(utils.EventEmitter):
1127
1144
  else:
1128
1145
  self.emit('connection_phy_update_failure', connection.handle, event.status)
1129
1146
 
1130
- def on_hci_le_advertising_report_event(self, event):
1147
+ def on_hci_le_advertising_report_event(
1148
+ self,
1149
+ event: (
1150
+ hci.HCI_LE_Advertising_Report_Event
1151
+ | hci.HCI_LE_Extended_Advertising_Report_Event
1152
+ ),
1153
+ ):
1131
1154
  for report in event.reports:
1132
1155
  self.emit('advertising_report', report)
1133
1156
 
1134
- def on_hci_le_extended_advertising_report_event(self, event):
1157
+ def on_hci_le_extended_advertising_report_event(
1158
+ self, event: hci.HCI_LE_Extended_Advertising_Report_Event
1159
+ ):
1135
1160
  self.on_hci_le_advertising_report_event(event)
1136
1161
 
1137
1162
  def on_hci_le_advertising_set_terminated_event(self, event):
@@ -1262,7 +1287,24 @@ class Host(utils.EventEmitter):
1262
1287
  self.cis_links[event.connection_handle] = IsoLink(
1263
1288
  handle=event.connection_handle, packet_queue=self.iso_packet_queue
1264
1289
  )
1265
- self.emit('cis_establishment', event.connection_handle)
1290
+ self.emit(
1291
+ 'cis_establishment',
1292
+ event.connection_handle,
1293
+ event.cig_sync_delay,
1294
+ event.cis_sync_delay,
1295
+ event.transport_latency_c_to_p,
1296
+ event.transport_latency_p_to_c,
1297
+ event.phy_c_to_p,
1298
+ event.phy_p_to_c,
1299
+ event.nse,
1300
+ event.bn_c_to_p,
1301
+ event.bn_p_to_c,
1302
+ event.ft_c_to_p,
1303
+ event.ft_p_to_c,
1304
+ event.max_pdu_c_to_p,
1305
+ event.max_pdu_p_to_c,
1306
+ event.iso_interval,
1307
+ )
1266
1308
  else:
1267
1309
  self.emit(
1268
1310
  'cis_establishment_failure', event.connection_handle, event.status
@@ -1350,6 +1392,15 @@ class Host(utils.EventEmitter):
1350
1392
  def on_hci_synchronous_connection_changed_event(self, event):
1351
1393
  pass
1352
1394
 
1395
+ def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
1396
+ self.emit(
1397
+ 'mode_change',
1398
+ event.connection_handle,
1399
+ event.status,
1400
+ event.current_mode,
1401
+ event.interval,
1402
+ )
1403
+
1353
1404
  def on_hci_role_change_event(self, event):
1354
1405
  if event.status == hci.HCI_SUCCESS:
1355
1406
  logger.debug(
@@ -1365,6 +1416,10 @@ class Host(utils.EventEmitter):
1365
1416
  self.emit('role_change_failure', event.bd_addr, event.status)
1366
1417
 
1367
1418
  def on_hci_le_data_length_change_event(self, event):
1419
+ if (connection := self.connections.get(event.connection_handle)) is None:
1420
+ logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
1421
+ return
1422
+
1368
1423
  self.emit(
1369
1424
  'connection_data_length_change',
1370
1425
  event.connection_handle,
@@ -1385,7 +1440,7 @@ class Host(utils.EventEmitter):
1385
1440
  event.status,
1386
1441
  )
1387
1442
 
1388
- def on_hci_encryption_change_event(self, event):
1443
+ def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
1389
1444
  # Notify the client
1390
1445
  if event.status == hci.HCI_SUCCESS:
1391
1446
  self.emit(
@@ -1399,7 +1454,9 @@ class Host(utils.EventEmitter):
1399
1454
  'connection_encryption_failure', event.connection_handle, event.status
1400
1455
  )
1401
1456
 
1402
- def on_hci_encryption_change_v2_event(self, event):
1457
+ def on_hci_encryption_change_v2_event(
1458
+ self, event: hci.HCI_Encryption_Change_V2_Event
1459
+ ):
1403
1460
  # Notify the client
1404
1461
  if event.status == hci.HCI_SUCCESS:
1405
1462
  self.emit(
@@ -1520,13 +1577,15 @@ class Host(utils.EventEmitter):
1520
1577
  self.emit('inquiry_complete')
1521
1578
 
1522
1579
  def on_hci_inquiry_result_with_rssi_event(self, event):
1523
- for response in event.responses:
1580
+ for bd_addr, class_of_device, rssi in zip(
1581
+ event.bd_addr, event.class_of_device, event.rssi
1582
+ ):
1524
1583
  self.emit(
1525
1584
  'inquiry_result',
1526
- response.bd_addr,
1527
- response.class_of_device,
1585
+ bd_addr,
1586
+ class_of_device,
1528
1587
  b'',
1529
- response.rssi,
1588
+ rssi,
1530
1589
  )
1531
1590
 
1532
1591
  def on_hci_extended_inquiry_result_event(self, event):
@@ -1586,5 +1645,15 @@ class Host(utils.EventEmitter):
1586
1645
  def on_hci_le_cs_subevent_result_continue_event(self, event):
1587
1646
  self.emit('cs_subevent_result_continue', event)
1588
1647
 
1648
+ def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
1649
+ self.emit(
1650
+ 'le_subrate_change',
1651
+ event.connection_handle,
1652
+ event.subrate_factor,
1653
+ event.peripheral_latency,
1654
+ event.continuation_number,
1655
+ event.supervision_timeout,
1656
+ )
1657
+
1589
1658
  def on_hci_vendor_event(self, event):
1590
1659
  self.emit('vendor_event', event)
bumble/keys.py CHANGED
@@ -26,7 +26,7 @@ import dataclasses
26
26
  import logging
27
27
  import os
28
28
  import json
29
- from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
29
+ from typing import TYPE_CHECKING, Optional, Any
30
30
  from typing_extensions import Self
31
31
 
32
32
  from bumble.colors import color
@@ -157,7 +157,7 @@ class KeyStore:
157
157
  async def get(self, _name: str) -> Optional[PairingKeys]:
158
158
  return None
159
159
 
160
- async def get_all(self) -> List[Tuple[str, PairingKeys]]:
160
+ async def get_all(self) -> list[tuple[str, PairingKeys]]:
161
161
  return []
162
162
 
163
163
  async def delete_all(self) -> None:
@@ -272,7 +272,7 @@ class JsonKeyStore(KeyStore):
272
272
 
273
273
  @classmethod
274
274
  def from_device(
275
- cls: Type[Self], device: Device, filename: Optional[str] = None
275
+ cls: type[Self], device: Device, filename: Optional[str] = None
276
276
  ) -> Self:
277
277
  if not filename:
278
278
  # Extract the filename from the config if there is one
@@ -356,7 +356,7 @@ class JsonKeyStore(KeyStore):
356
356
 
357
357
  # -----------------------------------------------------------------------------
358
358
  class MemoryKeyStore(KeyStore):
359
- all_keys: Dict[str, PairingKeys]
359
+ all_keys: dict[str, PairingKeys]
360
360
 
361
361
  def __init__(self) -> None:
362
362
  self.all_keys = {}
@@ -371,5 +371,5 @@ class MemoryKeyStore(KeyStore):
371
371
  async def get(self, name: str) -> Optional[PairingKeys]:
372
372
  return self.all_keys.get(name)
373
373
 
374
- async def get_all(self) -> List[Tuple[str, PairingKeys]]:
374
+ async def get_all(self) -> list[tuple[str, PairingKeys]]:
375
375
  return list(self.all_keys.items())