bumble 0.0.222__py3-none-any.whl → 0.0.224__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 (43) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/controller_info.py +90 -114
  3. bumble/apps/controller_loopback.py +11 -9
  4. bumble/apps/gg_bridge.py +1 -1
  5. bumble/apps/hci_bridge.py +3 -1
  6. bumble/apps/l2cap_bridge.py +1 -1
  7. bumble/apps/rfcomm_bridge.py +1 -1
  8. bumble/apps/scan.py +10 -4
  9. bumble/apps/speaker/speaker.py +1 -1
  10. bumble/apps/usb_probe.py +15 -2
  11. bumble/att.py +97 -32
  12. bumble/avctp.py +1 -1
  13. bumble/avdtp.py +3 -3
  14. bumble/avrcp.py +366 -190
  15. bumble/bridge.py +10 -2
  16. bumble/controller.py +14 -1
  17. bumble/core.py +1 -1
  18. bumble/device.py +999 -577
  19. bumble/drivers/intel.py +45 -39
  20. bumble/drivers/rtk.py +102 -43
  21. bumble/gatt.py +2 -2
  22. bumble/gatt_client.py +5 -4
  23. bumble/gatt_server.py +100 -1
  24. bumble/hci.py +1367 -844
  25. bumble/hid.py +2 -2
  26. bumble/host.py +339 -157
  27. bumble/l2cap.py +13 -6
  28. bumble/pandora/l2cap.py +1 -1
  29. bumble/profiles/battery_service.py +25 -34
  30. bumble/profiles/heart_rate_service.py +130 -121
  31. bumble/rfcomm.py +1 -1
  32. bumble/sdp.py +2 -2
  33. bumble/smp.py +8 -3
  34. bumble/snoop.py +111 -1
  35. bumble/transport/android_netsim.py +1 -1
  36. bumble/vendor/android/hci.py +108 -86
  37. bumble/vendor/zephyr/hci.py +24 -18
  38. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/METADATA +4 -3
  39. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/RECORD +43 -43
  40. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/WHEEL +1 -1
  41. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/entry_points.txt +0 -0
  42. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/licenses/LICENSE +0 -0
  43. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -79,6 +79,7 @@ if TYPE_CHECKING:
79
79
  from bumble.transport.common import TransportSink, TransportSource
80
80
 
81
81
  _T = TypeVar('_T')
82
+ _RP = TypeVar('_RP', bound=hci.HCI_ReturnParameters)
82
83
 
83
84
  # -----------------------------------------------------------------------------
84
85
  # Logging
@@ -373,24 +374,22 @@ class LegacyAdvertiser:
373
374
  async def start(self) -> None:
374
375
  # Set/update the advertising data if the advertising type allows it
375
376
  if self.advertising_type.has_data:
376
- await self.device.send_command(
377
+ await self.device.send_sync_command(
377
378
  hci.HCI_LE_Set_Advertising_Data_Command(
378
379
  advertising_data=self.device.advertising_data
379
- ),
380
- check_result=True,
380
+ )
381
381
  )
382
382
 
383
383
  # Set/update the scan response data if the advertising is scannable
384
384
  if self.advertising_type.is_scannable:
385
- await self.device.send_command(
385
+ await self.device.send_sync_command(
386
386
  hci.HCI_LE_Set_Scan_Response_Data_Command(
387
387
  scan_response_data=self.device.scan_response_data
388
- ),
389
- check_result=True,
388
+ )
390
389
  )
391
390
 
392
391
  # Set the advertising parameters
393
- await self.device.send_command(
392
+ await self.device.send_sync_command(
394
393
  hci.HCI_LE_Set_Advertising_Parameters_Command(
395
394
  advertising_interval_min=int(
396
395
  self.device.advertising_interval_min / 0.625
@@ -404,21 +403,18 @@ class LegacyAdvertiser:
404
403
  peer_address=self.peer_address,
405
404
  advertising_channel_map=7,
406
405
  advertising_filter_policy=0,
407
- ),
408
- check_result=True,
406
+ )
409
407
  )
410
408
 
411
409
  # Enable advertising
412
- await self.device.send_command(
413
- hci.HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1),
414
- check_result=True,
410
+ await self.device.send_sync_command(
411
+ hci.HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1)
415
412
  )
416
413
 
417
414
  async def stop(self) -> None:
418
415
  # Disable advertising
419
- await self.device.send_command(
420
- hci.HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0),
421
- check_result=True,
416
+ await self.device.send_sync_command(
417
+ hci.HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0)
422
418
  )
423
419
 
424
420
 
@@ -635,7 +631,7 @@ class AdvertisingSet(utils.EventEmitter):
635
631
  "connectable and scannable"
636
632
  )
637
633
 
638
- response = await self.device.send_command(
634
+ response = await self.device.send_sync_command(
639
635
  hci.HCI_LE_Set_Extended_Advertising_Parameters_Command(
640
636
  advertising_handle=self.advertising_handle,
641
637
  advertising_event_properties=int(
@@ -668,22 +664,20 @@ class AdvertisingSet(utils.EventEmitter):
668
664
  scan_request_notification_enable=(
669
665
  1 if advertising_parameters.enable_scan_request_notifications else 0
670
666
  ),
671
- ),
672
- check_result=True,
667
+ )
673
668
  )
674
- self.selected_tx_power = response.return_parameters.selected_tx_power
669
+ self.selected_tx_power = response.selected_tx_power
675
670
  self.advertising_parameters = advertising_parameters
676
671
 
677
672
  async def set_advertising_data(self, advertising_data: bytes) -> None:
678
673
  # pylint: disable=line-too-long
679
- await self.device.send_command(
674
+ await self.device.send_sync_command(
680
675
  hci.HCI_LE_Set_Extended_Advertising_Data_Command(
681
676
  advertising_handle=self.advertising_handle,
682
677
  operation=hci.HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
683
678
  fragment_preference=hci.HCI_LE_Set_Extended_Advertising_Parameters_Command.SHOULD_NOT_FRAGMENT,
684
679
  advertising_data=advertising_data,
685
- ),
686
- check_result=True,
680
+ )
687
681
  )
688
682
  self.advertising_data = advertising_data
689
683
 
@@ -699,21 +693,20 @@ class AdvertisingSet(utils.EventEmitter):
699
693
  )
700
694
  return
701
695
 
702
- await self.device.send_command(
696
+ await self.device.send_sync_command(
703
697
  hci.HCI_LE_Set_Extended_Scan_Response_Data_Command(
704
698
  advertising_handle=self.advertising_handle,
705
699
  operation=hci.HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
706
700
  fragment_preference=hci.HCI_LE_Set_Extended_Advertising_Parameters_Command.SHOULD_NOT_FRAGMENT,
707
701
  scan_response_data=scan_response_data,
708
- ),
709
- check_result=True,
702
+ )
710
703
  )
711
704
  self.scan_response_data = scan_response_data
712
705
 
713
706
  async def set_periodic_advertising_parameters(
714
707
  self, advertising_parameters: PeriodicAdvertisingParameters
715
708
  ) -> None:
716
- await self.device.send_command(
709
+ await self.device.send_sync_command(
717
710
  hci.HCI_LE_Set_Periodic_Advertising_Parameters_Command(
718
711
  advertising_handle=self.advertising_handle,
719
712
  periodic_advertising_interval_min=int(
@@ -723,29 +716,26 @@ class AdvertisingSet(utils.EventEmitter):
723
716
  advertising_parameters.periodic_advertising_interval_max / 1.25
724
717
  ),
725
718
  periodic_advertising_properties=advertising_parameters.periodic_advertising_properties,
726
- ),
727
- check_result=True,
719
+ )
728
720
  )
729
721
  self.periodic_advertising_parameters = advertising_parameters
730
722
 
731
723
  async def set_periodic_advertising_data(self, advertising_data: bytes) -> None:
732
- await self.device.send_command(
724
+ await self.device.send_sync_command(
733
725
  hci.HCI_LE_Set_Periodic_Advertising_Data_Command(
734
726
  advertising_handle=self.advertising_handle,
735
727
  operation=hci.HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
736
728
  advertising_data=advertising_data,
737
- ),
738
- check_result=True,
729
+ )
739
730
  )
740
731
  self.periodic_advertising_data = advertising_data
741
732
 
742
733
  async def set_random_address(self, random_address: hci.Address) -> None:
743
- await self.device.send_command(
734
+ await self.device.send_sync_command(
744
735
  hci.HCI_LE_Set_Advertising_Set_Random_Address_Command(
745
736
  advertising_handle=self.advertising_handle,
746
737
  random_address=(random_address or self.device.random_address),
747
- ),
748
- check_result=True,
738
+ )
749
739
  )
750
740
 
751
741
  async def start(
@@ -761,28 +751,26 @@ class AdvertisingSet(utils.EventEmitter):
761
751
  max_advertising_events: Maximum number of events to advertise for. Use 0
762
752
  (the default) for an unlimited number of advertisements.
763
753
  """
764
- await self.device.send_command(
754
+ await self.device.send_sync_command(
765
755
  hci.HCI_LE_Set_Extended_Advertising_Enable_Command(
766
756
  enable=1,
767
757
  advertising_handles=[self.advertising_handle],
768
758
  durations=[round(duration * 100)],
769
759
  max_extended_advertising_events=[max_advertising_events],
770
- ),
771
- check_result=True,
760
+ )
772
761
  )
773
762
  self.enabled = True
774
763
 
775
764
  self.emit(self.EVENT_START)
776
765
 
777
766
  async def stop(self) -> None:
778
- await self.device.send_command(
767
+ await self.device.send_sync_command(
779
768
  hci.HCI_LE_Set_Extended_Advertising_Enable_Command(
780
769
  enable=0,
781
770
  advertising_handles=[self.advertising_handle],
782
771
  durations=[0],
783
772
  max_extended_advertising_events=[0],
784
- ),
785
- check_result=True,
773
+ )
786
774
  )
787
775
  self.enabled = False
788
776
 
@@ -791,12 +779,11 @@ class AdvertisingSet(utils.EventEmitter):
791
779
  async def start_periodic(self, include_adi: bool = False) -> None:
792
780
  if self.periodic_enabled:
793
781
  return
794
- await self.device.send_command(
782
+ await self.device.send_sync_command(
795
783
  hci.HCI_LE_Set_Periodic_Advertising_Enable_Command(
796
784
  enable=1 | (2 if include_adi else 0),
797
785
  advertising_handle=self.advertising_handle,
798
- ),
799
- check_result=True,
786
+ )
800
787
  )
801
788
  self.periodic_enabled = True
802
789
 
@@ -805,23 +792,21 @@ class AdvertisingSet(utils.EventEmitter):
805
792
  async def stop_periodic(self) -> None:
806
793
  if not self.periodic_enabled:
807
794
  return
808
- await self.device.send_command(
795
+ await self.device.send_sync_command(
809
796
  hci.HCI_LE_Set_Periodic_Advertising_Enable_Command(
810
797
  enable=0,
811
798
  advertising_handle=self.advertising_handle,
812
- ),
813
- check_result=True,
799
+ )
814
800
  )
815
801
  self.periodic_enabled = False
816
802
 
817
803
  self.emit(self.EVENT_STOP_PERIODIC)
818
804
 
819
805
  async def remove(self) -> None:
820
- await self.device.send_command(
806
+ await self.device.send_sync_command(
821
807
  hci.HCI_LE_Remove_Advertising_Set_Command(
822
808
  advertising_handle=self.advertising_handle
823
- ),
824
- check_result=True,
809
+ )
825
810
  )
826
811
  del self.device.extended_advertising_sets[self.advertising_handle]
827
812
 
@@ -916,7 +901,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
916
901
  hci.HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
917
902
  )
918
903
 
919
- await self.device.send_command(
904
+ await self.device.send_async_command(
920
905
  hci.HCI_LE_Periodic_Advertising_Create_Sync_Command(
921
906
  options=options,
922
907
  advertising_sid=self.sid,
@@ -925,8 +910,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
925
910
  skip=self.skip,
926
911
  sync_timeout=int(self.sync_timeout * 100),
927
912
  sync_cte_type=0,
928
- ),
929
- check_result=True,
913
+ )
930
914
  )
931
915
 
932
916
  self.state = self.State.PENDING
@@ -937,18 +921,20 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
937
921
 
938
922
  if self.state == self.State.PENDING:
939
923
  self.state = self.State.CANCELLED
940
- response = await self.device.send_command(
941
- hci.HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command(),
942
- )
943
- if response.return_parameters == hci.HCI_SUCCESS:
924
+ try:
925
+ await self.device.send_sync_command(
926
+ hci.HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command()
927
+ )
944
928
  if self in self.device.periodic_advertising_syncs:
945
929
  self.device.periodic_advertising_syncs.remove(self)
930
+ except hci.HCI_Error:
931
+ pass
946
932
  return
947
933
 
948
934
  if self.state in (self.State.ESTABLISHED, self.State.ERROR, self.State.LOST):
949
935
  self.state = self.State.TERMINATED
950
936
  if self.sync_handle is not None:
951
- await self.device.send_command(
937
+ await self.device.send_sync_command(
952
938
  hci.HCI_LE_Periodic_Advertising_Terminate_Sync_Command(
953
939
  sync_handle=self.sync_handle
954
940
  )
@@ -1118,11 +1104,10 @@ class Big(utils.EventEmitter):
1118
1104
  with closing(utils.EventWatcher()) as watcher:
1119
1105
  terminated = asyncio.Event()
1120
1106
  watcher.once(self, Big.Event.TERMINATION, lambda _: terminated.set())
1121
- await self.device.send_command(
1107
+ await self.device.send_async_command(
1122
1108
  hci.HCI_LE_Terminate_BIG_Command(
1123
1109
  big_handle=self.big_handle, reason=reason
1124
- ),
1125
- check_result=True,
1110
+ )
1126
1111
  )
1127
1112
  await terminated.wait()
1128
1113
 
@@ -1174,9 +1159,8 @@ class BigSync(utils.EventEmitter):
1174
1159
  logger.error('BIG Sync %d is not active.', self.big_handle)
1175
1160
  return
1176
1161
 
1177
- await self.device.send_command(
1178
- hci.HCI_LE_BIG_Terminate_Sync_Command(big_handle=self.big_handle),
1179
- check_result=True,
1162
+ await self.device.send_sync_command(
1163
+ hci.HCI_LE_BIG_Terminate_Sync_Command(big_handle=self.big_handle)
1180
1164
  )
1181
1165
  self.state = BigSync.State.TERMINATED
1182
1166
 
@@ -1193,7 +1177,7 @@ class ChannelSoundingCapabilities:
1193
1177
  rtt_capability: int
1194
1178
  rtt_aa_only_n: int
1195
1179
  rtt_sounding_n: int
1196
- rtt_random_payload_n: int
1180
+ rtt_random_sequence_n: int
1197
1181
  nadm_sounding_capability: int
1198
1182
  nadm_random_capability: int
1199
1183
  cs_sync_phys_supported: int
@@ -1383,10 +1367,7 @@ class Peer:
1383
1367
  def create_service_proxy(
1384
1368
  self, proxy_class: type[_PROXY_CLASS]
1385
1369
  ) -> _PROXY_CLASS | None:
1386
- if proxy := proxy_class.from_client(self.gatt_client):
1387
- return cast(_PROXY_CLASS, proxy)
1388
-
1389
- return None
1370
+ return proxy_class.from_client(self.gatt_client)
1390
1371
 
1391
1372
  async def discover_service_and_create_proxy(
1392
1373
  self, proxy_class: type[_PROXY_CLASS]
@@ -1406,7 +1387,7 @@ class Peer:
1406
1387
  async def request_name(self) -> str:
1407
1388
  return await self.connection.request_remote_name()
1408
1389
 
1409
- async def __aenter__(self):
1390
+ async def __aenter__(self) -> Self:
1410
1391
  await self.discover_services()
1411
1392
  for service in self.services:
1412
1393
  await service.discover_characteristics()
@@ -1428,8 +1409,8 @@ class ConnectionParametersPreferences:
1428
1409
  connection_interval_max: float = DEVICE_DEFAULT_CONNECTION_INTERVAL_MAX
1429
1410
  max_latency: int = DEVICE_DEFAULT_CONNECTION_MAX_LATENCY
1430
1411
  supervision_timeout: int = DEVICE_DEFAULT_CONNECTION_SUPERVISION_TIMEOUT
1431
- min_ce_length: int = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH
1432
- max_ce_length: int = DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH
1412
+ min_ce_length: float = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH
1413
+ max_ce_length: float = DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH
1433
1414
 
1434
1415
 
1435
1416
  ConnectionParametersPreferences.default = ConnectionParametersPreferences()
@@ -1499,7 +1480,7 @@ class _IsoLink:
1499
1480
  async with self._data_path_lock:
1500
1481
  if direction in self.data_paths:
1501
1482
  return
1502
- await self.device.send_command(
1483
+ await self.device.send_sync_command(
1503
1484
  hci.HCI_LE_Setup_ISO_Data_Path_Command(
1504
1485
  connection_handle=self.handle,
1505
1486
  data_path_direction=direction,
@@ -1507,8 +1488,7 @@ class _IsoLink:
1507
1488
  codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT),
1508
1489
  controller_delay=controller_delay,
1509
1490
  codec_configuration=codec_configuration,
1510
- ),
1511
- check_result=True,
1491
+ )
1512
1492
  )
1513
1493
  self.data_paths.add(direction)
1514
1494
 
@@ -1525,14 +1505,13 @@ class _IsoLink:
1525
1505
  directions_to_remove = set(directions).intersection(self.data_paths)
1526
1506
  if not directions_to_remove:
1527
1507
  return
1528
- await self.device.send_command(
1508
+ await self.device.send_sync_command(
1529
1509
  hci.HCI_LE_Remove_ISO_Data_Path_Command(
1530
1510
  connection_handle=self.handle,
1531
1511
  data_path_direction=sum(
1532
1512
  1 << direction for direction in directions_to_remove
1533
1513
  ),
1534
- ),
1535
- check_result=True,
1514
+ )
1536
1515
  )
1537
1516
  self.data_paths.difference_update(directions_to_remove)
1538
1517
 
@@ -1541,14 +1520,13 @@ class _IsoLink:
1541
1520
  self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
1542
1521
 
1543
1522
  async def get_tx_time_stamp(self) -> tuple[int, int, int]:
1544
- response = await self.device.host.send_command(
1545
- hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle),
1546
- check_result=True,
1523
+ response = await self.device.send_sync_command(
1524
+ hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle)
1547
1525
  )
1548
1526
  return (
1549
- response.return_parameters.packet_sequence_number,
1550
- response.return_parameters.tx_time_stamp,
1551
- response.return_parameters.time_offset,
1527
+ response.packet_sequence_number,
1528
+ response.tx_time_stamp,
1529
+ response.time_offset,
1552
1530
  )
1553
1531
 
1554
1532
  @property
@@ -1719,7 +1697,7 @@ class Connection(utils.CompositeEventEmitter):
1719
1697
  peer_address: hci.Address
1720
1698
  peer_name: str | None
1721
1699
  peer_resolvable_address: hci.Address | None
1722
- peer_le_features: hci.LeFeatureMask | None
1700
+ peer_le_features: hci.LeFeatureMask
1723
1701
  role: hci.Role
1724
1702
  parameters: Parameters
1725
1703
  encryption: int
@@ -1772,8 +1750,8 @@ class Connection(utils.CompositeEventEmitter):
1772
1750
  EVENT_CIS_REQUEST = "cis_request"
1773
1751
  EVENT_CIS_ESTABLISHMENT = "cis_establishment"
1774
1752
  EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
1775
- EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
1776
- EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
1753
+ EVENT_LE_REMOTE_FEATURES_CHANGE = "le_remote_features_change"
1754
+ EVENT_LE_REMOTE_FEATURES_CHANGE_FAILURE = "le_remote_features_change_failure"
1777
1755
 
1778
1756
  @utils.composite_listener
1779
1757
  class Listener:
@@ -1851,14 +1829,14 @@ class Connection(utils.CompositeEventEmitter):
1851
1829
  self.authenticated = False
1852
1830
  self.sc = False
1853
1831
  self.att_mtu = att.ATT_DEFAULT_MTU
1854
- self.data_length = DEVICE_DEFAULT_DATA_LENGTH
1832
+ self.data_length: tuple[int, int, int, int] = DEVICE_DEFAULT_DATA_LENGTH
1855
1833
  self.gatt_client = gatt_client.Client(self) # Per-connection client
1856
1834
  self.gatt_server = (
1857
1835
  device.gatt_server
1858
1836
  ) # By default, use the device's shared server
1859
1837
  self.pairing_peer_io_capability = None
1860
1838
  self.pairing_peer_authentication_requirements = None
1861
- self.peer_le_features = None
1839
+ self.peer_le_features = hci.LeFeatureMask(0)
1862
1840
  self.cs_configs = {}
1863
1841
  self.cs_procedures = {}
1864
1842
 
@@ -1940,16 +1918,21 @@ class Connection(utils.CompositeEventEmitter):
1940
1918
  connection_interval_max: float,
1941
1919
  max_latency: int,
1942
1920
  supervision_timeout: float,
1921
+ min_ce_length: float = 0.0,
1922
+ max_ce_length: float = 0.0,
1943
1923
  use_l2cap=False,
1944
1924
  ) -> None:
1945
1925
  """
1946
- Request an update of the connection parameters.
1926
+ Request a change of the connection parameters.
1927
+
1928
+ For short connection intervals (below 7.5ms, introduced in Bluetooth 6.2),
1929
+ use the `update_parameters_with_subrate` method instead.
1947
1930
 
1948
1931
  Args:
1949
1932
  connection_interval_min: Minimum interval, in milliseconds.
1950
1933
  connection_interval_max: Maximum interval, in milliseconds.
1951
- max_latency: Latency, in number of intervals.
1952
- supervision_timeout: Timeout, in milliseconds.
1934
+ max_latency: Max latency, in number of intervals.
1935
+ supervision_timeout: Supervision Timeout, in milliseconds.
1953
1936
  use_l2cap: Request the update via L2CAP.
1954
1937
  """
1955
1938
  return await self.device.update_connection_parameters(
@@ -1959,6 +1942,77 @@ class Connection(utils.CompositeEventEmitter):
1959
1942
  max_latency,
1960
1943
  supervision_timeout,
1961
1944
  use_l2cap=use_l2cap,
1945
+ min_ce_length=min_ce_length,
1946
+ max_ce_length=max_ce_length,
1947
+ )
1948
+
1949
+ async def update_parameters_with_subrate(
1950
+ self,
1951
+ connection_interval_min: float,
1952
+ connection_interval_max: float,
1953
+ subrate_min: int,
1954
+ subrate_max: int,
1955
+ max_latency: int,
1956
+ continuation_number: int,
1957
+ supervision_timeout: float,
1958
+ min_ce_length: float,
1959
+ max_ce_length: float,
1960
+ ) -> None:
1961
+ """
1962
+ Request a change of the connection parameters.
1963
+ This is similar to `update_parameters` but also allows specifying
1964
+ the subrate parameters and supports shorter connection intervals (below
1965
+ 7.5ms, as introduced in Bluetooth 6.2).
1966
+
1967
+ Args:
1968
+ connection_interval_min: Minimum interval, in milliseconds.
1969
+ connection_interval_max: Maximum interval, in milliseconds.
1970
+ subrate_min: Minimum subrate factor.
1971
+ subrate_max: Maximum subrate factor.
1972
+ max_latency: Max latency, in number of intervals.
1973
+ continuation_number: Continuation number.
1974
+ supervision_timeout: Supervision Timeout, in milliseconds.
1975
+ min_ce_length: Minimum connection event length, in milliseconds.
1976
+ max_ce_length: Maximumsub connection event length, in milliseconds.
1977
+ """
1978
+ return await self.device.update_connection_parameters_with_subrate(
1979
+ self,
1980
+ connection_interval_min,
1981
+ connection_interval_max,
1982
+ subrate_min,
1983
+ subrate_max,
1984
+ max_latency,
1985
+ continuation_number,
1986
+ supervision_timeout,
1987
+ min_ce_length,
1988
+ max_ce_length,
1989
+ )
1990
+
1991
+ async def update_subrate(
1992
+ self,
1993
+ subrate_min: int,
1994
+ subrate_max: int,
1995
+ max_latency: int,
1996
+ continuation_number: int,
1997
+ supervision_timeout: float,
1998
+ ) -> None:
1999
+ """
2000
+ Request request a change to the subrating factor and/or other parameters.
2001
+
2002
+ Args:
2003
+ subrate_min: Minimum subrate factor.
2004
+ subrate_max: Maximum subrate factor.
2005
+ max_latency: Max latency, in number of intervals.
2006
+ continuation_number: Continuation number.
2007
+ supervision_timeout: Supervision Timeout, in milliseconds.
2008
+ """
2009
+ return await self.device.update_connection_subrate(
2010
+ self,
2011
+ subrate_min,
2012
+ subrate_max,
2013
+ max_latency,
2014
+ continuation_number,
2015
+ supervision_timeout,
1962
2016
  )
1963
2017
 
1964
2018
  async def set_phy(
@@ -2065,6 +2119,7 @@ class DeviceConfiguration:
2065
2119
  le_privacy_enabled: bool = False
2066
2120
  le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
2067
2121
  le_subrate_enabled: bool = False
2122
+ le_shorter_connection_intervals_enabled: bool = False
2068
2123
  classic_enabled: bool = False
2069
2124
  classic_sc_enabled: bool = True
2070
2125
  classic_ssp_enabled: bool = True
@@ -2371,7 +2426,7 @@ class Device(utils.CompositeEventEmitter):
2371
2426
  self._host = None
2372
2427
  self.powered_on = False
2373
2428
  self.auto_restart_inquiry = True
2374
- self.command_timeout = 10 # seconds
2429
+ self.command_timeout = 10.0 # seconds
2375
2430
  self.gatt_server = gatt_server.Server(self)
2376
2431
  self.sdp_server = sdp.Server(self)
2377
2432
  self.l2cap_channel_manager = l2cap.ChannelManager(
@@ -2419,6 +2474,9 @@ class Device(utils.CompositeEventEmitter):
2419
2474
  self.le_rpa_timeout = config.le_rpa_timeout
2420
2475
  self.le_rpa_periodic_update_task: asyncio.Task | None = None
2421
2476
  self.le_subrate_enabled = config.le_subrate_enabled
2477
+ self.le_shorter_connection_intervals_enabled = (
2478
+ config.le_shorter_connection_intervals_enabled
2479
+ )
2422
2480
  self.classic_enabled = config.classic_enabled
2423
2481
  self.cis_enabled = config.cis_enabled
2424
2482
  self.classic_sc_enabled = config.classic_sc_enabled
@@ -2677,10 +2735,88 @@ class Device(utils.CompositeEventEmitter):
2677
2735
  def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
2678
2736
  self.host.send_l2cap_pdu(connection_handle, cid, pdu)
2679
2737
 
2680
- async def send_command(self, command: hci.HCI_Command, check_result: bool = False):
2738
+ @overload
2739
+ async def send_command(
2740
+ self, command: hci.HCI_SyncCommand, check_result: bool = False
2741
+ ) -> hci.HCI_Command_Complete_Event: ...
2742
+
2743
+ @overload
2744
+ async def send_command(
2745
+ self, command: hci.HCI_AsyncCommand, check_result: bool = False
2746
+ ) -> hci.HCI_Command_Status_Event: ...
2747
+
2748
+ async def send_command(
2749
+ self,
2750
+ command: hci.HCI_SyncCommand | hci.HCI_AsyncCommand,
2751
+ check_result: bool = False,
2752
+ ) -> hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event:
2753
+ '''
2754
+ Send a synchronous or asynchronous command via the host.
2755
+
2756
+ NOTE: use `send_sync_command` instead for synchronous commands.
2757
+ '''
2758
+ try:
2759
+ return await self.host.send_command(
2760
+ command, check_result, self.command_timeout
2761
+ )
2762
+ except asyncio.TimeoutError as error:
2763
+ logger.warning(f'!!! Command {command.name} timed out')
2764
+ raise CommandTimeoutError() from error
2765
+
2766
+ async def send_sync_command(self, command: hci.HCI_SyncCommand[_RP]) -> _RP:
2767
+ '''
2768
+ Send a synchronous command via the host.
2769
+
2770
+ If the `status` field of the response's `return_parameters` is not equal to
2771
+ `SUCCESS` an exception is raised.
2772
+
2773
+ Params:
2774
+ command: the command to send.
2775
+
2776
+ Returns:
2777
+ An instance of the return parameters class associated with the command class.
2778
+ '''
2779
+ try:
2780
+ return await self.host.send_sync_command(command, self.command_timeout)
2781
+ except asyncio.TimeoutError as error:
2782
+ logger.warning(f'!!! Command {command.name} timed out')
2783
+ raise CommandTimeoutError() from error
2784
+
2785
+ async def send_sync_command_raw(
2786
+ self, command: hci.HCI_SyncCommand[_RP]
2787
+ ) -> hci.HCI_Command_Complete_Event[_RP]:
2788
+ '''
2789
+ Send a synchronous command via the host without checking the response.
2790
+
2791
+ Params:
2792
+ command: the command to send.
2793
+
2794
+ Returns:
2795
+ An HCI_Command_Complete_Event instance.
2796
+ '''
2797
+ try:
2798
+ return await self.host.send_sync_command_raw(command, self.command_timeout)
2799
+ except asyncio.TimeoutError as error:
2800
+ logger.warning(f'!!! Command {command.name} timed out')
2801
+ raise CommandTimeoutError() from error
2802
+
2803
+ async def send_async_command(
2804
+ self, command: hci.HCI_AsyncCommand, check_status: bool = True
2805
+ ) -> hci.HCI_ErrorCode:
2806
+ '''
2807
+ Send an asynchronous command via the host.
2808
+
2809
+ Params:
2810
+ command: the command to send.
2811
+ check_status: If `True`, check the `status` field of the response and
2812
+ raise and exception if not equal to `PENDING`.
2813
+
2814
+ Returns:
2815
+ A status code.
2816
+ '''
2681
2817
  try:
2682
- return await asyncio.wait_for(
2683
- self.host.send_command(command, check_result), self.command_timeout
2818
+ return await self.host.send_async_command(
2819
+ command, check_status, self.command_timeout
2684
2820
  )
2685
2821
  except asyncio.TimeoutError as error:
2686
2822
  logger.warning(f'!!! Command {command.name} timed out')
@@ -2691,12 +2827,12 @@ class Device(utils.CompositeEventEmitter):
2691
2827
  await self.host.reset()
2692
2828
 
2693
2829
  # Try to get the public address from the controller
2694
- response = await self.send_command(hci.HCI_Read_BD_ADDR_Command())
2695
- if response.return_parameters.status == hci.HCI_SUCCESS:
2696
- logger.debug(
2697
- color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')
2698
- )
2699
- self.public_address = response.return_parameters.bd_addr
2830
+ try:
2831
+ response = await self.host.send_sync_command(hci.HCI_Read_BD_ADDR_Command())
2832
+ logger.debug(color(f'BD_ADDR: {response.bd_addr}', 'yellow'))
2833
+ self.public_address = response.bd_addr
2834
+ except hci.HCI_Error:
2835
+ logger.debug('Controller has no public address')
2700
2836
 
2701
2837
  # Instantiate the Key Store (we do this here rather than at __init__ time
2702
2838
  # because some Key Store implementations use the public address as a namespace)
@@ -2710,12 +2846,11 @@ class Device(utils.CompositeEventEmitter):
2710
2846
  )
2711
2847
 
2712
2848
  if self.host.supports_command(hci.HCI_WRITE_LE_HOST_SUPPORT_COMMAND):
2713
- await self.send_command(
2849
+ await self.send_sync_command(
2714
2850
  hci.HCI_Write_LE_Host_Support_Command(
2715
2851
  le_supported_host=int(self.le_enabled),
2716
2852
  simultaneous_le_host=int(self.le_simultaneous_enabled),
2717
- ),
2718
- check_result=True,
2853
+ )
2719
2854
  )
2720
2855
 
2721
2856
  if self.le_enabled:
@@ -2742,11 +2877,10 @@ class Device(utils.CompositeEventEmitter):
2742
2877
  'yellow',
2743
2878
  )
2744
2879
  )
2745
- await self.send_command(
2880
+ await self.send_sync_command(
2746
2881
  hci.HCI_LE_Set_Random_Address_Command(
2747
2882
  random_address=self.random_address
2748
- ),
2749
- check_result=True,
2883
+ )
2750
2884
  )
2751
2885
 
2752
2886
  # Load the address resolving list
@@ -2755,81 +2889,100 @@ class Device(utils.CompositeEventEmitter):
2755
2889
 
2756
2890
  # Enable address resolution
2757
2891
  if self.address_resolution_offload:
2758
- await self.send_command(
2892
+ await self.send_sync_command(
2759
2893
  hci.HCI_LE_Set_Address_Resolution_Enable_Command(
2760
2894
  address_resolution_enable=1
2761
- ),
2762
- check_result=True,
2895
+ )
2763
2896
  )
2764
2897
 
2765
- if self.cis_enabled:
2766
- await self.send_command(
2898
+ if self.cis_enabled and self.host.supports_command(
2899
+ hci.HCI_LE_SET_HOST_FEATURE_COMMAND
2900
+ ):
2901
+ await self.send_sync_command(
2767
2902
  hci.HCI_LE_Set_Host_Feature_Command(
2768
2903
  bit_number=hci.LeFeature.CONNECTED_ISOCHRONOUS_STREAM,
2769
2904
  bit_value=1,
2770
- ),
2771
- check_result=True,
2905
+ )
2772
2906
  )
2773
2907
 
2774
- if self.le_subrate_enabled:
2775
- await self.send_command(
2908
+ if (
2909
+ self.le_subrate_enabled
2910
+ and self.host.supports_command(hci.HCI_LE_SET_HOST_FEATURE_COMMAND)
2911
+ and self.host.supports_le_features(
2912
+ hci.LeFeatureMask.CONNECTION_SUBRATING
2913
+ )
2914
+ ):
2915
+ await self.send_sync_command(
2776
2916
  hci.HCI_LE_Set_Host_Feature_Command(
2777
2917
  bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
2778
2918
  bit_value=1,
2779
- ),
2780
- check_result=True,
2919
+ )
2781
2920
  )
2782
2921
 
2783
- if self.config.channel_sounding_enabled:
2784
- await self.send_command(
2922
+ if self.config.channel_sounding_enabled and self.host.supports_command(
2923
+ hci.HCI_LE_SET_HOST_FEATURE_COMMAND
2924
+ ):
2925
+ await self.send_sync_command(
2785
2926
  hci.HCI_LE_Set_Host_Feature_Command(
2786
2927
  bit_number=hci.LeFeature.CHANNEL_SOUNDING_HOST_SUPPORT,
2787
2928
  bit_value=1,
2788
- ),
2789
- check_result=True,
2929
+ )
2790
2930
  )
2791
- result = await self.send_command(
2792
- hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command(),
2793
- check_result=True,
2931
+ result = await self.send_sync_command(
2932
+ hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command()
2794
2933
  )
2795
2934
  self.cs_capabilities = ChannelSoundingCapabilities(
2796
- num_config_supported=result.return_parameters.num_config_supported,
2797
- max_consecutive_procedures_supported=result.return_parameters.max_consecutive_procedures_supported,
2798
- num_antennas_supported=result.return_parameters.num_antennas_supported,
2799
- max_antenna_paths_supported=result.return_parameters.max_antenna_paths_supported,
2800
- roles_supported=result.return_parameters.roles_supported,
2801
- modes_supported=result.return_parameters.modes_supported,
2802
- rtt_capability=result.return_parameters.rtt_capability,
2803
- rtt_aa_only_n=result.return_parameters.rtt_aa_only_n,
2804
- rtt_sounding_n=result.return_parameters.rtt_sounding_n,
2805
- rtt_random_payload_n=result.return_parameters.rtt_random_payload_n,
2806
- nadm_sounding_capability=result.return_parameters.nadm_sounding_capability,
2807
- nadm_random_capability=result.return_parameters.nadm_random_capability,
2808
- cs_sync_phys_supported=result.return_parameters.cs_sync_phys_supported,
2809
- subfeatures_supported=result.return_parameters.subfeatures_supported,
2810
- t_ip1_times_supported=result.return_parameters.t_ip1_times_supported,
2811
- t_ip2_times_supported=result.return_parameters.t_ip2_times_supported,
2812
- t_fcs_times_supported=result.return_parameters.t_fcs_times_supported,
2813
- t_pm_times_supported=result.return_parameters.t_pm_times_supported,
2814
- t_sw_time_supported=result.return_parameters.t_sw_time_supported,
2815
- tx_snr_capability=result.return_parameters.tx_snr_capability,
2935
+ num_config_supported=result.num_config_supported,
2936
+ max_consecutive_procedures_supported=result.max_consecutive_procedures_supported,
2937
+ num_antennas_supported=result.num_antennas_supported,
2938
+ max_antenna_paths_supported=result.max_antenna_paths_supported,
2939
+ roles_supported=result.roles_supported,
2940
+ modes_supported=result.modes_supported,
2941
+ rtt_capability=result.rtt_capability,
2942
+ rtt_aa_only_n=result.rtt_aa_only_n,
2943
+ rtt_sounding_n=result.rtt_sounding_n,
2944
+ rtt_random_sequence_n=result.rtt_random_sequence_n,
2945
+ nadm_sounding_capability=result.nadm_sounding_capability,
2946
+ nadm_random_capability=result.nadm_random_capability,
2947
+ cs_sync_phys_supported=result.cs_sync_phys_supported,
2948
+ subfeatures_supported=result.subfeatures_supported,
2949
+ t_ip1_times_supported=result.t_ip1_times_supported,
2950
+ t_ip2_times_supported=result.t_ip2_times_supported,
2951
+ t_fcs_times_supported=result.t_fcs_times_supported,
2952
+ t_pm_times_supported=result.t_pm_times_supported,
2953
+ t_sw_time_supported=result.t_sw_time_supported,
2954
+ tx_snr_capability=result.tx_snr_capability,
2955
+ )
2956
+
2957
+ if (
2958
+ self.le_shorter_connection_intervals_enabled
2959
+ and self.host.supports_command(hci.HCI_LE_SET_HOST_FEATURE_COMMAND)
2960
+ and self.host.supports_le_features(
2961
+ hci.LeFeatureMask.SHORTER_CONNECTION_INTERVALS
2962
+ )
2963
+ ):
2964
+ await self.send_sync_command(
2965
+ hci.HCI_LE_Set_Host_Feature_Command(
2966
+ bit_number=hci.LeFeature.SHORTER_CONNECTION_INTERVALS_HOST_SUPPORT,
2967
+ bit_value=1,
2968
+ )
2816
2969
  )
2817
2970
 
2818
2971
  if self.classic_enabled:
2819
- await self.send_command(
2972
+ await self.send_sync_command_raw(
2820
2973
  hci.HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
2821
2974
  )
2822
- await self.send_command(
2975
+ await self.send_sync_command_raw(
2823
2976
  hci.HCI_Write_Class_Of_Device_Command(
2824
2977
  class_of_device=self.class_of_device
2825
2978
  )
2826
2979
  )
2827
- await self.send_command(
2980
+ await self.send_sync_command_raw(
2828
2981
  hci.HCI_Write_Simple_Pairing_Mode_Command(
2829
2982
  simple_pairing_mode=int(self.classic_ssp_enabled)
2830
2983
  )
2831
2984
  )
2832
- await self.send_command(
2985
+ await self.send_sync_command_raw(
2833
2986
  hci.HCI_Write_Secure_Connections_Host_Support_Command(
2834
2987
  secure_connections_host_support=int(self.classic_sc_enabled)
2835
2988
  )
@@ -2841,17 +2994,15 @@ class Device(utils.CompositeEventEmitter):
2841
2994
  if self.host.supports_lmp_features(
2842
2995
  hci.LmpFeatureMask.INTERLACED_PAGE_SCAN
2843
2996
  ):
2844
- await self.send_command(
2845
- hci.HCI_Write_Page_Scan_Type_Command(page_scan_type=1),
2846
- check_result=True,
2997
+ await self.send_sync_command(
2998
+ hci.HCI_Write_Page_Scan_Type_Command(page_scan_type=1)
2847
2999
  )
2848
3000
 
2849
3001
  if self.host.supports_lmp_features(
2850
3002
  hci.LmpFeatureMask.INTERLACED_INQUIRY_SCAN
2851
3003
  ):
2852
- await self.send_command(
2853
- hci.HCI_Write_Inquiry_Scan_Type_Command(scan_type=1),
2854
- check_result=True,
3004
+ await self.send_sync_command(
3005
+ hci.HCI_Write_Inquiry_Scan_Type_Command(scan_type=1)
2855
3006
  )
2856
3007
 
2857
3008
  # Done
@@ -2883,15 +3034,17 @@ class Device(utils.CompositeEventEmitter):
2883
3034
  return False
2884
3035
 
2885
3036
  random_address = hci.Address.generate_private_address(self.irk)
2886
- response = await self.send_command(
2887
- hci.HCI_LE_Set_Random_Address_Command(random_address=self.random_address)
2888
- )
2889
- if response.return_parameters == hci.HCI_SUCCESS:
3037
+ try:
3038
+ await self.send_sync_command(
3039
+ hci.HCI_LE_Set_Random_Address_Command(
3040
+ random_address=self.random_address
3041
+ )
3042
+ )
2890
3043
  logger.info(f'new RPA: {random_address}')
2891
3044
  self.random_address = random_address
2892
3045
  return True
2893
- else:
2894
- logger.warning(f'failed to set RPA: {response.return_parameters}')
3046
+ except hci.HCI_Error as error:
3047
+ logger.warning(f'failed to set RPA: {error.error_code!r}')
2895
3048
  return False
2896
3049
 
2897
3050
  async def _run_rpa_periodic_update(self) -> None:
@@ -2909,30 +3062,26 @@ class Device(utils.CompositeEventEmitter):
2909
3062
  self.address_resolver = smp.AddressResolver(resolving_keys)
2910
3063
 
2911
3064
  if self.address_resolution_offload or self.address_generation_offload:
2912
- await self.send_command(
2913
- hci.HCI_LE_Clear_Resolving_List_Command(), check_result=True
2914
- )
3065
+ await self.send_sync_command(hci.HCI_LE_Clear_Resolving_List_Command())
2915
3066
 
2916
3067
  # Add an empty entry for non-directed address generation.
2917
- await self.send_command(
3068
+ await self.send_sync_command(
2918
3069
  hci.HCI_LE_Add_Device_To_Resolving_List_Command(
2919
3070
  peer_identity_address_type=hci.Address.ANY.address_type,
2920
3071
  peer_identity_address=hci.Address.ANY,
2921
3072
  peer_irk=bytes(16),
2922
3073
  local_irk=self.irk,
2923
- ),
2924
- check_result=True,
3074
+ )
2925
3075
  )
2926
3076
 
2927
3077
  for irk, address in resolving_keys:
2928
- await self.send_command(
3078
+ await self.send_sync_command(
2929
3079
  hci.HCI_LE_Add_Device_To_Resolving_List_Command(
2930
3080
  peer_identity_address_type=address.address_type,
2931
3081
  peer_identity_address=address,
2932
3082
  peer_irk=irk,
2933
3083
  local_irk=self.irk,
2934
- ),
2935
- check_result=True,
3084
+ )
2936
3085
  )
2937
3086
 
2938
3087
  def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
@@ -3200,11 +3349,10 @@ class Device(utils.CompositeEventEmitter):
3200
3349
  except hci.HCI_Error as error:
3201
3350
  # Remove the advertising set so that it doesn't stay dangling in the
3202
3351
  # controller.
3203
- await self.send_command(
3352
+ await self.send_sync_command(
3204
3353
  hci.HCI_LE_Remove_Advertising_Set_Command(
3205
3354
  advertising_handle=advertising_handle
3206
- ),
3207
- check_result=False,
3355
+ )
3208
3356
  )
3209
3357
  raise error
3210
3358
 
@@ -3287,7 +3435,7 @@ class Device(utils.CompositeEventEmitter):
3287
3435
  if scanning_phy_count == 0:
3288
3436
  raise InvalidArgumentError('at least one scanning PHY must be enabled')
3289
3437
 
3290
- await self.send_command(
3438
+ await self.send_sync_command(
3291
3439
  hci.HCI_LE_Set_Extended_Scan_Parameters_Command(
3292
3440
  own_address_type=own_address_type,
3293
3441
  scanning_filter_policy=scanning_filter_policy,
@@ -3295,19 +3443,17 @@ class Device(utils.CompositeEventEmitter):
3295
3443
  scan_types=[scan_type] * scanning_phy_count,
3296
3444
  scan_intervals=[int(scan_interval / 0.625)] * scanning_phy_count,
3297
3445
  scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
3298
- ),
3299
- check_result=True,
3446
+ )
3300
3447
  )
3301
3448
 
3302
3449
  # Enable scanning
3303
- await self.send_command(
3450
+ await self.send_sync_command(
3304
3451
  hci.HCI_LE_Set_Extended_Scan_Enable_Command(
3305
3452
  enable=1,
3306
3453
  filter_duplicates=1 if filter_duplicates else 0,
3307
3454
  duration=0, # TODO allow other values
3308
3455
  period=0, # TODO allow other values
3309
- ),
3310
- check_result=True,
3456
+ )
3311
3457
  )
3312
3458
  else:
3313
3459
  # Set the scanning parameters
@@ -3316,7 +3462,7 @@ class Device(utils.CompositeEventEmitter):
3316
3462
  if active
3317
3463
  else hci.HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING
3318
3464
  )
3319
- await self.send_command(
3465
+ await self.send_sync_command(
3320
3466
  # pylint: disable=line-too-long
3321
3467
  hci.HCI_LE_Set_Scan_Parameters_Command(
3322
3468
  le_scan_type=scan_type,
@@ -3324,16 +3470,14 @@ class Device(utils.CompositeEventEmitter):
3324
3470
  le_scan_window=int(scan_window / 0.625),
3325
3471
  own_address_type=own_address_type,
3326
3472
  scanning_filter_policy=hci.HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY,
3327
- ),
3328
- check_result=True,
3473
+ )
3329
3474
  )
3330
3475
 
3331
3476
  # Enable scanning
3332
- await self.send_command(
3477
+ await self.send_sync_command(
3333
3478
  hci.HCI_LE_Set_Scan_Enable_Command(
3334
3479
  le_scan_enable=1, filter_duplicates=1 if filter_duplicates else 0
3335
- ),
3336
- check_result=True,
3480
+ )
3337
3481
  )
3338
3482
 
3339
3483
  self.scanning_is_passive = not active
@@ -3342,18 +3486,16 @@ class Device(utils.CompositeEventEmitter):
3342
3486
  async def stop_scanning(self, legacy: bool = False) -> None:
3343
3487
  # Disable scanning
3344
3488
  if not legacy and self.supports_le_extended_advertising:
3345
- await self.send_command(
3489
+ await self.send_sync_command(
3346
3490
  hci.HCI_LE_Set_Extended_Scan_Enable_Command(
3347
3491
  enable=0, filter_duplicates=0, duration=0, period=0
3348
- ),
3349
- check_result=True,
3492
+ )
3350
3493
  )
3351
3494
  else:
3352
- await self.send_command(
3495
+ await self.send_sync_command(
3353
3496
  hci.HCI_LE_Set_Scan_Enable_Command(
3354
3497
  le_scan_enable=0, filter_duplicates=0
3355
- ),
3356
- check_result=True,
3498
+ )
3357
3499
  )
3358
3500
 
3359
3501
  self.scanning = False
@@ -3526,21 +3668,19 @@ class Device(utils.CompositeEventEmitter):
3526
3668
  periodic_advertising_sync.on_biginfo_advertising_report(report)
3527
3669
 
3528
3670
  async def start_discovery(self, auto_restart: bool = True) -> None:
3529
- await self.send_command(
3671
+ await self.send_sync_command(
3530
3672
  hci.HCI_Write_Inquiry_Mode_Command(
3531
3673
  inquiry_mode=hci.HCI_EXTENDED_INQUIRY_MODE
3532
- ),
3533
- check_result=True,
3674
+ )
3534
3675
  )
3535
3676
 
3536
3677
  self.discovering = False
3537
- await self.send_command(
3678
+ await self.send_async_command(
3538
3679
  hci.HCI_Inquiry_Command(
3539
3680
  lap=hci.HCI_GENERAL_INQUIRY_LAP,
3540
3681
  inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
3541
3682
  num_responses=0, # Unlimited number of responses.
3542
- ),
3543
- check_result=True,
3683
+ )
3544
3684
  )
3545
3685
 
3546
3686
  self.auto_restart_inquiry = auto_restart
@@ -3548,7 +3688,7 @@ class Device(utils.CompositeEventEmitter):
3548
3688
 
3549
3689
  async def stop_discovery(self) -> None:
3550
3690
  if self.discovering:
3551
- await self.send_command(hci.HCI_Inquiry_Cancel_Command(), check_result=True)
3691
+ await self.send_sync_command(hci.HCI_Inquiry_Cancel_Command())
3552
3692
  self.auto_restart_inquiry = True
3553
3693
  self.discovering = False
3554
3694
 
@@ -3576,9 +3716,8 @@ class Device(utils.CompositeEventEmitter):
3576
3716
  else:
3577
3717
  scan_enable = 0x00
3578
3718
 
3579
- return await self.send_command(
3580
- hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable),
3581
- check_result=True,
3719
+ return await self.send_sync_command(
3720
+ hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable)
3582
3721
  )
3583
3722
 
3584
3723
  async def set_discoverable(self, discoverable: bool = True) -> None:
@@ -3591,11 +3730,10 @@ class Device(utils.CompositeEventEmitter):
3591
3730
  )
3592
3731
 
3593
3732
  # Update the controller
3594
- await self.send_command(
3733
+ await self.send_sync_command(
3595
3734
  hci.HCI_Write_Extended_Inquiry_Response_Command(
3596
3735
  fec_required=0, extended_inquiry_response=self.inquiry_response
3597
- ),
3598
- check_result=True,
3736
+ )
3599
3737
  )
3600
3738
  await self.set_scan_enable(
3601
3739
  inquiry_scan_enabled=self.discoverable,
@@ -3610,287 +3748,168 @@ class Device(utils.CompositeEventEmitter):
3610
3748
  page_scan_enabled=self.connectable,
3611
3749
  )
3612
3750
 
3613
- async def connect(
3751
+ async def connect_le(
3614
3752
  self,
3615
3753
  peer_address: hci.Address | str,
3616
- transport: core.PhysicalTransport = PhysicalTransport.LE,
3617
3754
  connection_parameters_preferences: (
3618
3755
  dict[hci.Phy, ConnectionParametersPreferences] | None
3619
3756
  ) = None,
3620
3757
  own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
3621
3758
  timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3622
- always_resolve: bool = False,
3623
3759
  ) -> Connection:
3624
- '''
3625
- Request a connection to a peer.
3626
-
3627
- When the transport is BLE, this method cannot be called if there is already a
3628
- pending connection.
3629
-
3630
- Args:
3631
- peer_address:
3632
- hci.Address or name of the device to connect to.
3633
- If a string is passed:
3634
- If the string is an address followed by a `@` suffix, the `always_resolve`
3635
- argument is implicitly set to True, so the connection is made to the
3636
- address after resolution.
3637
- If the string is any other address, the connection is made to that
3638
- address (with or without address resolution, depending on the
3639
- `always_resolve` argument).
3640
- For any other string, a scan for devices using that string as their name
3641
- is initiated, and a connection to the first matching device's address
3642
- is made. In that case, `always_resolve` is ignored.
3643
-
3644
- connection_parameters_preferences:
3645
- (BLE only, ignored for BR/EDR)
3646
- * None: use the 1M PHY with default parameters
3647
- * map: each entry has a PHY as key and a ConnectionParametersPreferences
3648
- object as value
3649
-
3650
- own_address_type:
3651
- (BLE only, ignored for BR/EDR)
3652
- hci.OwnAddressType.RANDOM to use this device's random address, or
3653
- hci.OwnAddressType.PUBLIC to use this device's public address.
3654
-
3655
- timeout:
3656
- Maximum time to wait for a connection to be established, in seconds.
3657
- Pass None for an unlimited time.
3658
-
3659
- always_resolve:
3660
- (BLE only, ignored for BR/EDR)
3661
- If True, always initiate a scan, resolving addresses, and connect to the
3662
- address that resolves to `peer_address`.
3663
- '''
3664
-
3665
- # Check parameters
3666
- if transport not in (PhysicalTransport.LE, PhysicalTransport.BR_EDR):
3667
- raise InvalidArgumentError('invalid transport')
3668
- transport = core.PhysicalTransport(transport)
3669
-
3670
- # Adjust the transport automatically if we need to
3671
- if transport == PhysicalTransport.LE and not self.le_enabled:
3672
- transport = PhysicalTransport.BR_EDR
3673
- elif transport == PhysicalTransport.BR_EDR and not self.classic_enabled:
3674
- transport = PhysicalTransport.LE
3675
-
3676
3760
  # Check that there isn't already a pending connection
3677
- if transport == PhysicalTransport.LE and self.is_le_connecting:
3761
+ if self.is_le_connecting:
3678
3762
  raise InvalidStateError('connection already pending')
3679
3763
 
3764
+ try_resolve = not self.address_resolution_offload
3680
3765
  if isinstance(peer_address, str):
3681
3766
  try:
3682
- if transport == PhysicalTransport.LE and peer_address.endswith('@'):
3683
- peer_address = hci.Address.from_string_for_transport(
3684
- peer_address[:-1], transport
3685
- )
3686
- always_resolve = True
3687
- logger.debug('forcing address resolution')
3688
- else:
3689
- peer_address = hci.Address.from_string_for_transport(
3690
- peer_address, transport
3691
- )
3767
+ peer_address = hci.Address.from_string_for_transport(
3768
+ peer_address, PhysicalTransport.LE
3769
+ )
3692
3770
  except (InvalidArgumentError, ValueError):
3693
3771
  # If the address is not parsable, assume it is a name instead
3694
- always_resolve = False
3695
3772
  logger.debug('looking for peer by name')
3696
3773
  assert isinstance(peer_address, str)
3697
3774
  peer_address = await self.find_peer_by_name(
3698
- peer_address, transport
3775
+ peer_address, PhysicalTransport.LE
3699
3776
  ) # TODO: timeout
3700
- else:
3701
- # All BR/EDR addresses should be public addresses
3702
- if (
3703
- transport == PhysicalTransport.BR_EDR
3704
- and peer_address.address_type != hci.Address.PUBLIC_DEVICE_ADDRESS
3705
- ):
3706
- raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
3777
+ try_resolve = False
3707
3778
 
3708
3779
  assert isinstance(peer_address, hci.Address)
3709
3780
 
3710
- if transport == PhysicalTransport.LE and always_resolve:
3711
- logger.debug('resolving address')
3781
+ if (
3782
+ try_resolve
3783
+ and self.address_resolver is not None
3784
+ and self.address_resolver.can_resolve_to(peer_address)
3785
+ ):
3786
+ # If we have an IRK for this address, we should resolve.
3787
+ logger.debug('have IRK for address, resolving...')
3712
3788
  peer_address = await self.find_peer_by_identity_address(
3713
3789
  peer_address
3714
3790
  ) # TODO: timeout
3715
3791
 
3716
3792
  def on_connection(connection):
3717
- if transport == PhysicalTransport.LE or (
3718
- # match BR/EDR connection event against peer address
3719
- connection.transport == transport
3720
- and connection.peer_address == peer_address
3721
- ):
3722
- pending_connection.set_result(connection)
3793
+ pending_connection.set_result(connection)
3723
3794
 
3724
3795
  def on_connection_failure(error: core.ConnectionError):
3725
- if transport == PhysicalTransport.LE or (
3726
- # match BR/EDR connection failure event against peer address
3727
- error.transport == transport
3728
- and error.peer_address == peer_address
3729
- ):
3730
- pending_connection.set_exception(error)
3796
+ pending_connection.set_exception(error)
3731
3797
 
3732
- # Create a future so that we can wait for the connection's result
3798
+ # Create a future so that we can wait for the connection result
3733
3799
  pending_connection = asyncio.get_running_loop().create_future()
3734
3800
  self.on(self.EVENT_CONNECTION, on_connection)
3735
3801
  self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3736
3802
 
3737
3803
  try:
3738
3804
  # Tell the controller to connect
3739
- if transport == PhysicalTransport.LE:
3740
- if connection_parameters_preferences is None:
3741
- if connection_parameters_preferences is None:
3742
- connection_parameters_preferences = {
3743
- hci.HCI_LE_1M_PHY: ConnectionParametersPreferences.default
3744
- }
3805
+ if connection_parameters_preferences is None:
3806
+ connection_parameters_preferences = {
3807
+ hci.HCI_LE_1M_PHY: ConnectionParametersPreferences.default
3808
+ }
3745
3809
 
3746
- self.connect_own_address_type = own_address_type
3810
+ self.connect_own_address_type = own_address_type
3747
3811
 
3748
- if self.host.supports_command(
3749
- hci.HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND
3750
- ):
3751
- # Only keep supported PHYs
3752
- phys = sorted(
3753
- list(
3754
- set(
3755
- filter(
3756
- self.supports_le_phy,
3757
- connection_parameters_preferences.keys(),
3758
- )
3812
+ if self.host.supports_command(
3813
+ hci.HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND
3814
+ ):
3815
+ # Only keep supported PHYs
3816
+ phys = sorted(
3817
+ list(
3818
+ set(
3819
+ filter(
3820
+ self.supports_le_phy,
3821
+ connection_parameters_preferences.keys(),
3759
3822
  )
3760
3823
  )
3761
3824
  )
3762
- if not phys:
3763
- raise InvalidArgumentError('at least one supported PHY needed')
3764
-
3765
- phy_count = len(phys)
3766
- initiating_phys = hci.phy_list_to_bits(phys)
3767
-
3768
- connection_interval_mins = [
3769
- int(
3770
- connection_parameters_preferences[
3771
- phy
3772
- ].connection_interval_min
3773
- / 1.25
3774
- )
3775
- for phy in phys
3776
- ]
3777
- connection_interval_maxs = [
3778
- int(
3779
- connection_parameters_preferences[
3780
- phy
3781
- ].connection_interval_max
3782
- / 1.25
3783
- )
3784
- for phy in phys
3785
- ]
3786
- max_latencies = [
3787
- connection_parameters_preferences[phy].max_latency
3788
- for phy in phys
3789
- ]
3790
- supervision_timeouts = [
3791
- int(
3792
- connection_parameters_preferences[phy].supervision_timeout
3793
- / 10
3794
- )
3795
- for phy in phys
3796
- ]
3797
- min_ce_lengths = [
3798
- int(
3799
- connection_parameters_preferences[phy].min_ce_length / 0.625
3800
- )
3801
- for phy in phys
3802
- ]
3803
- max_ce_lengths = [
3804
- int(
3805
- connection_parameters_preferences[phy].max_ce_length / 0.625
3806
- )
3807
- for phy in phys
3808
- ]
3825
+ )
3826
+ if not phys:
3827
+ raise InvalidArgumentError('at least one supported PHY needed')
3809
3828
 
3810
- await self.send_command(
3811
- hci.HCI_LE_Extended_Create_Connection_Command(
3812
- initiator_filter_policy=0,
3813
- own_address_type=own_address_type,
3814
- peer_address_type=peer_address.address_type,
3815
- peer_address=peer_address,
3816
- initiating_phys=initiating_phys,
3817
- scan_intervals=(
3818
- int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),
3819
- )
3820
- * phy_count,
3821
- scan_windows=(
3822
- int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),
3823
- )
3824
- * phy_count,
3825
- connection_interval_mins=connection_interval_mins,
3826
- connection_interval_maxs=connection_interval_maxs,
3827
- max_latencies=max_latencies,
3828
- supervision_timeouts=supervision_timeouts,
3829
- min_ce_lengths=min_ce_lengths,
3830
- max_ce_lengths=max_ce_lengths,
3831
- ),
3832
- check_result=True,
3829
+ phy_count = len(phys)
3830
+ initiating_phys = hci.phy_list_to_bits(phys)
3831
+
3832
+ connection_interval_mins = [
3833
+ int(
3834
+ connection_parameters_preferences[phy].connection_interval_min
3835
+ / 1.25
3833
3836
  )
3834
- else:
3835
- if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
3836
- raise InvalidArgumentError('1M PHY preferences required')
3837
-
3838
- prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
3839
- await self.send_command(
3840
- hci.HCI_LE_Create_Connection_Command(
3841
- le_scan_interval=int(
3842
- DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
3843
- ),
3844
- le_scan_window=int(
3845
- DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625
3846
- ),
3847
- initiator_filter_policy=0,
3848
- peer_address_type=peer_address.address_type,
3849
- peer_address=peer_address,
3850
- own_address_type=own_address_type,
3851
- connection_interval_min=int(
3852
- prefs.connection_interval_min / 1.25
3853
- ),
3854
- connection_interval_max=int(
3855
- prefs.connection_interval_max / 1.25
3856
- ),
3857
- max_latency=prefs.max_latency,
3858
- supervision_timeout=int(prefs.supervision_timeout / 10),
3859
- min_ce_length=int(prefs.min_ce_length / 0.625),
3860
- max_ce_length=int(prefs.max_ce_length / 0.625),
3861
- ),
3862
- check_result=True,
3837
+ for phy in phys
3838
+ ]
3839
+ connection_interval_maxs = [
3840
+ int(
3841
+ connection_parameters_preferences[phy].connection_interval_max
3842
+ / 1.25
3843
+ )
3844
+ for phy in phys
3845
+ ]
3846
+ max_latencies = [
3847
+ connection_parameters_preferences[phy].max_latency for phy in phys
3848
+ ]
3849
+ supervision_timeouts = [
3850
+ int(connection_parameters_preferences[phy].supervision_timeout / 10)
3851
+ for phy in phys
3852
+ ]
3853
+ min_ce_lengths = [
3854
+ int(connection_parameters_preferences[phy].min_ce_length / 0.625)
3855
+ for phy in phys
3856
+ ]
3857
+ max_ce_lengths = [
3858
+ int(connection_parameters_preferences[phy].max_ce_length / 0.625)
3859
+ for phy in phys
3860
+ ]
3861
+
3862
+ await self.send_async_command(
3863
+ hci.HCI_LE_Extended_Create_Connection_Command(
3864
+ initiator_filter_policy=0,
3865
+ own_address_type=own_address_type,
3866
+ peer_address_type=peer_address.address_type,
3867
+ peer_address=peer_address,
3868
+ initiating_phys=initiating_phys,
3869
+ scan_intervals=(
3870
+ int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),
3871
+ )
3872
+ * phy_count,
3873
+ scan_windows=(int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),)
3874
+ * phy_count,
3875
+ connection_interval_mins=connection_interval_mins,
3876
+ connection_interval_maxs=connection_interval_maxs,
3877
+ max_latencies=max_latencies,
3878
+ supervision_timeouts=supervision_timeouts,
3879
+ min_ce_lengths=min_ce_lengths,
3880
+ max_ce_lengths=max_ce_lengths,
3863
3881
  )
3864
- else:
3865
- # Save pending connection
3866
- self.pending_connections[peer_address] = Connection(
3867
- device=self,
3868
- handle=0,
3869
- transport=core.PhysicalTransport.BR_EDR,
3870
- self_address=self.public_address,
3871
- self_resolvable_address=None,
3872
- peer_address=peer_address,
3873
- peer_resolvable_address=None,
3874
- role=hci.Role.CENTRAL,
3875
- parameters=Connection.Parameters(0, 0, 0),
3876
3882
  )
3877
-
3878
- # TODO: allow passing other settings
3879
- await self.send_command(
3880
- hci.HCI_Create_Connection_Command(
3881
- bd_addr=peer_address,
3882
- packet_type=0xCC18, # FIXME: change
3883
- page_scan_repetition_mode=hci.HCI_R2_PAGE_SCAN_REPETITION_MODE,
3884
- clock_offset=0x0000,
3885
- allow_role_switch=0x01,
3886
- reserved=0,
3887
- ),
3888
- check_result=True,
3883
+ else:
3884
+ if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
3885
+ raise InvalidArgumentError('1M PHY preferences required')
3886
+
3887
+ prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
3888
+ await self.send_async_command(
3889
+ hci.HCI_LE_Create_Connection_Command(
3890
+ le_scan_interval=int(
3891
+ DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
3892
+ ),
3893
+ le_scan_window=int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),
3894
+ initiator_filter_policy=0,
3895
+ peer_address_type=peer_address.address_type,
3896
+ peer_address=peer_address,
3897
+ own_address_type=own_address_type,
3898
+ connection_interval_min=int(
3899
+ prefs.connection_interval_min / 1.25
3900
+ ),
3901
+ connection_interval_max=int(
3902
+ prefs.connection_interval_max / 1.25
3903
+ ),
3904
+ max_latency=prefs.max_latency,
3905
+ supervision_timeout=int(prefs.supervision_timeout / 10),
3906
+ min_ce_length=int(prefs.min_ce_length / 0.625),
3907
+ max_ce_length=int(prefs.max_ce_length / 0.625),
3908
+ )
3889
3909
  )
3890
3910
 
3891
3911
  # Wait for the connection process to complete
3892
- if transport == PhysicalTransport.LE:
3893
- self.le_connecting = True
3912
+ self.le_connecting = True
3894
3913
 
3895
3914
  if timeout is None:
3896
3915
  return await utils.cancel_on_event(
@@ -3902,14 +3921,107 @@ class Device(utils.CompositeEventEmitter):
3902
3921
  asyncio.shield(pending_connection), timeout
3903
3922
  )
3904
3923
  except asyncio.TimeoutError:
3905
- if transport == PhysicalTransport.LE:
3906
- await self.send_command(
3907
- hci.HCI_LE_Create_Connection_Cancel_Command()
3908
- )
3909
- else:
3910
- await self.send_command(
3911
- hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
3924
+ await self.send_sync_command(
3925
+ hci.HCI_LE_Create_Connection_Cancel_Command()
3926
+ )
3927
+
3928
+ try:
3929
+ return await utils.cancel_on_event(
3930
+ self, Device.EVENT_FLUSH, pending_connection
3912
3931
  )
3932
+ except core.ConnectionError as error:
3933
+ raise core.TimeoutError() from error
3934
+ finally:
3935
+ self.remove_listener(self.EVENT_CONNECTION, on_connection)
3936
+ self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3937
+ self.le_connecting = False
3938
+ self.connect_own_address_type = None
3939
+
3940
+ async def connect_classic(
3941
+ self,
3942
+ peer_address: hci.Address | str,
3943
+ timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3944
+ ) -> Connection:
3945
+ if isinstance(peer_address, str):
3946
+ try:
3947
+ peer_address = hci.Address.from_string_for_transport(
3948
+ peer_address, PhysicalTransport.BR_EDR
3949
+ )
3950
+ except (InvalidArgumentError, ValueError):
3951
+ # If the address is not parsable, assume it is a name instead
3952
+ logger.debug('looking for peer by name')
3953
+ assert isinstance(peer_address, str)
3954
+ peer_address = await self.find_peer_by_name(
3955
+ peer_address, PhysicalTransport.BR_EDR
3956
+ ) # TODO: timeout
3957
+ else:
3958
+ # All BR/EDR addresses should be public addresses
3959
+ if peer_address.address_type != hci.Address.PUBLIC_DEVICE_ADDRESS:
3960
+ raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
3961
+
3962
+ assert isinstance(peer_address, hci.Address)
3963
+
3964
+ def on_connection(connection):
3965
+ if (
3966
+ # match BR/EDR connection event against peer address
3967
+ connection.transport == PhysicalTransport.BR_EDR
3968
+ and connection.peer_address == peer_address
3969
+ ):
3970
+ pending_connection.set_result(connection)
3971
+
3972
+ def on_connection_failure(error: core.ConnectionError):
3973
+ if (
3974
+ # match BR/EDR connection failure event against peer address
3975
+ error.transport == PhysicalTransport.BR_EDR
3976
+ and error.peer_address == peer_address
3977
+ ):
3978
+ pending_connection.set_exception(error)
3979
+
3980
+ # Create a future so that we can wait for the connection result
3981
+ pending_connection = asyncio.get_running_loop().create_future()
3982
+ self.on(self.EVENT_CONNECTION, on_connection)
3983
+ self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3984
+
3985
+ try:
3986
+ # Save pending connection
3987
+ self.pending_connections[peer_address] = Connection(
3988
+ device=self,
3989
+ handle=0,
3990
+ transport=core.PhysicalTransport.BR_EDR,
3991
+ self_address=self.public_address,
3992
+ self_resolvable_address=None,
3993
+ peer_address=peer_address,
3994
+ peer_resolvable_address=None,
3995
+ role=hci.Role.CENTRAL,
3996
+ parameters=Connection.Parameters(0, 0, 0),
3997
+ )
3998
+
3999
+ # TODO: allow passing other settings
4000
+ await self.send_async_command(
4001
+ hci.HCI_Create_Connection_Command(
4002
+ bd_addr=peer_address,
4003
+ packet_type=0xCC18, # FIXME: change
4004
+ page_scan_repetition_mode=hci.HCI_R2_PAGE_SCAN_REPETITION_MODE,
4005
+ clock_offset=0x0000,
4006
+ allow_role_switch=0x01,
4007
+ reserved=0,
4008
+ )
4009
+ )
4010
+
4011
+ # Wait for the connection process to complete
4012
+ if timeout is None:
4013
+ return await utils.cancel_on_event(
4014
+ self, Device.EVENT_FLUSH, pending_connection
4015
+ )
4016
+
4017
+ try:
4018
+ return await asyncio.wait_for(
4019
+ asyncio.shield(pending_connection), timeout
4020
+ )
4021
+ except asyncio.TimeoutError:
4022
+ await self.send_sync_command(
4023
+ hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
4024
+ )
3913
4025
 
3914
4026
  try:
3915
4027
  return await utils.cancel_on_event(
@@ -3920,11 +4032,78 @@ class Device(utils.CompositeEventEmitter):
3920
4032
  finally:
3921
4033
  self.remove_listener(self.EVENT_CONNECTION, on_connection)
3922
4034
  self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3923
- if transport == PhysicalTransport.LE:
3924
- self.le_connecting = False
3925
- self.connect_own_address_type = None
3926
- else:
3927
- self.pending_connections.pop(peer_address, None)
4035
+ self.pending_connections.pop(peer_address, None)
4036
+
4037
+ async def connect(
4038
+ self,
4039
+ peer_address: hci.Address | str,
4040
+ transport: core.PhysicalTransport = PhysicalTransport.LE,
4041
+ connection_parameters_preferences: (
4042
+ dict[hci.Phy, ConnectionParametersPreferences] | None
4043
+ ) = None,
4044
+ own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
4045
+ timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
4046
+ always_resolve: bool = False,
4047
+ ) -> Connection:
4048
+ '''
4049
+ Request a connection to a peer.
4050
+
4051
+ When the transport is BLE, this method cannot be called if there is already a
4052
+ pending connection.
4053
+
4054
+ Args:
4055
+ peer_address:
4056
+ hci.Address or name of the device to connect to.
4057
+ If a string is passed:
4058
+ [deprecated] If the string is an address followed by a `@` suffix, the
4059
+ `always_resolve`argument is implicitly set to True, so the connection is
4060
+ made to the address after resolution.
4061
+ If the string is any other address, the connection is made to that
4062
+ address (with or without address resolution, depending on the
4063
+ `always_resolve` argument).
4064
+ For any other string, a scan for devices using that string as their name
4065
+ is initiated, and a connection to the first matching device's address
4066
+ is made. In that case, `always_resolve` is ignored.
4067
+
4068
+ connection_parameters_preferences:
4069
+ (BLE only, ignored for BR/EDR)
4070
+ * None: use the 1M PHY with default parameters
4071
+ * map: each entry has a PHY as key and a ConnectionParametersPreferences
4072
+ object as value
4073
+
4074
+ own_address_type:
4075
+ (BLE only, ignored for BR/EDR)
4076
+ hci.OwnAddressType.RANDOM to use this device's random address, or
4077
+ hci.OwnAddressType.PUBLIC to use this device's public address.
4078
+
4079
+ timeout:
4080
+ Maximum time to wait for a connection to be established, in seconds.
4081
+ Pass None for an unlimited time.
4082
+
4083
+ always_resolve:
4084
+ [deprecated] (ignore)
4085
+ '''
4086
+
4087
+ # Connect using the appropriate transport
4088
+ # (auto-correct the transport based on declared capabilities)
4089
+ if transport == PhysicalTransport.LE or (
4090
+ self.le_enabled and not self.classic_enabled
4091
+ ):
4092
+ return await self.connect_le(
4093
+ peer_address=peer_address,
4094
+ connection_parameters_preferences=connection_parameters_preferences,
4095
+ own_address_type=own_address_type,
4096
+ timeout=timeout,
4097
+ )
4098
+
4099
+ if transport == PhysicalTransport.BR_EDR or (
4100
+ self.classic_enabled and not self.le_enabled
4101
+ ):
4102
+ return await self.connect_classic(
4103
+ peer_address=peer_address, timeout=timeout
4104
+ )
4105
+
4106
+ raise ValueError('invalid transport')
3928
4107
 
3929
4108
  async def accept(
3930
4109
  self,
@@ -4036,11 +4215,10 @@ class Device(utils.CompositeEventEmitter):
4036
4215
 
4037
4216
  try:
4038
4217
  # Accept connection request
4039
- await self.send_command(
4218
+ await self.send_async_command(
4040
4219
  hci.HCI_Accept_Connection_Request_Command(
4041
4220
  bd_addr=peer_address, role=role
4042
- ),
4043
- check_result=True,
4221
+ )
4044
4222
  )
4045
4223
 
4046
4224
  # Wait for connection complete
@@ -4076,9 +4254,7 @@ class Device(utils.CompositeEventEmitter):
4076
4254
  if peer_address is None:
4077
4255
  if not self.is_le_connecting:
4078
4256
  return
4079
- await self.send_command(
4080
- hci.HCI_LE_Create_Connection_Cancel_Command(), check_result=True
4081
- )
4257
+ await self.send_sync_command(hci.HCI_LE_Create_Connection_Cancel_Command())
4082
4258
 
4083
4259
  # BR/EDR: try to cancel to ongoing connection
4084
4260
  # NOTE: This API does not prevent from trying to cancel a connection which is
@@ -4095,9 +4271,8 @@ class Device(utils.CompositeEventEmitter):
4095
4271
  peer_address, PhysicalTransport.BR_EDR
4096
4272
  ) # TODO: timeout
4097
4273
 
4098
- await self.send_command(
4099
- hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address),
4100
- check_result=True,
4274
+ await self.send_sync_command(
4275
+ hci.HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
4101
4276
  )
4102
4277
 
4103
4278
  async def disconnect(
@@ -4115,11 +4290,10 @@ class Device(utils.CompositeEventEmitter):
4115
4290
  self.disconnecting = True
4116
4291
 
4117
4292
  # Request a disconnection
4118
- await self.send_command(
4293
+ await self.send_async_command(
4119
4294
  hci.HCI_Disconnect_Command(
4120
4295
  connection_handle=connection.handle, reason=reason
4121
- ),
4122
- check_result=True,
4296
+ )
4123
4297
  )
4124
4298
  return await utils.cancel_on_event(
4125
4299
  self, Device.EVENT_FLUSH, pending_disconnection
@@ -4143,13 +4317,12 @@ class Device(utils.CompositeEventEmitter):
4143
4317
  if tx_time < 0x0148 or tx_time > 0x4290:
4144
4318
  raise InvalidArgumentError('tx_time must be between 0x0148 and 0x4290')
4145
4319
 
4146
- return await self.send_command(
4320
+ await self.send_sync_command(
4147
4321
  hci.HCI_LE_Set_Data_Length_Command(
4148
4322
  connection_handle=connection.handle,
4149
4323
  tx_octets=tx_octets,
4150
4324
  tx_time=tx_time,
4151
- ),
4152
- check_result=True,
4325
+ )
4153
4326
  )
4154
4327
 
4155
4328
  async def update_connection_parameters(
@@ -4166,6 +4339,9 @@ class Device(utils.CompositeEventEmitter):
4166
4339
  '''
4167
4340
  Request an update of the connection parameters.
4168
4341
 
4342
+ For short connection intervals (below 7.5 ms, introduced in Bluetooth 6.2),
4343
+ use `update_connection_parameters_with_subrate` instead.
4344
+
4169
4345
  Args:
4170
4346
  connection: The connection to update
4171
4347
  connection_interval_min: Minimum interval, in milliseconds.
@@ -4206,36 +4382,163 @@ class Device(utils.CompositeEventEmitter):
4206
4382
 
4207
4383
  return
4208
4384
 
4209
- await self.send_command(
4210
- hci.HCI_LE_Connection_Update_Command(
4211
- connection_handle=connection.handle,
4212
- connection_interval_min=connection_interval_min,
4213
- connection_interval_max=connection_interval_max,
4214
- max_latency=max_latency,
4215
- supervision_timeout=supervision_timeout,
4216
- min_ce_length=min_ce_length,
4217
- max_ce_length=max_ce_length,
4218
- ),
4219
- check_result=True,
4220
- )
4385
+ pending_result = asyncio.get_running_loop().create_future()
4386
+ with closing(utils.EventWatcher()) as watcher:
4387
+
4388
+ @watcher.on(connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
4389
+ def _():
4390
+ pending_result.set_result(None)
4391
+
4392
+ @watcher.on(
4393
+ connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE
4394
+ )
4395
+ def _(error_code: int):
4396
+ pending_result.set_exception(hci.HCI_Error(error_code))
4397
+
4398
+ await self.send_async_command(
4399
+ hci.HCI_LE_Connection_Update_Command(
4400
+ connection_handle=connection.handle,
4401
+ connection_interval_min=connection_interval_min,
4402
+ connection_interval_max=connection_interval_max,
4403
+ max_latency=max_latency,
4404
+ supervision_timeout=supervision_timeout,
4405
+ min_ce_length=min_ce_length,
4406
+ max_ce_length=max_ce_length,
4407
+ )
4408
+ )
4409
+
4410
+ await connection.cancel_on_disconnection(pending_result)
4411
+
4412
+ async def update_connection_parameters_with_subrate(
4413
+ self,
4414
+ connection: Connection,
4415
+ connection_interval_min: float,
4416
+ connection_interval_max: float,
4417
+ subrate_min: int,
4418
+ subrate_max: int,
4419
+ max_latency: int,
4420
+ continuation_number: int,
4421
+ supervision_timeout: float,
4422
+ min_ce_length: float = 0.0,
4423
+ max_ce_length: float = 0.0,
4424
+ ) -> None:
4425
+ '''
4426
+ Request a change of the connection parameters.
4427
+ This is similar to `update_connection_parameters` but also allows specifying
4428
+ the subrate parameters and supports shorter connection intervals (below
4429
+ 7.5ms, as introduced in Bluetooth 6.2).
4430
+
4431
+ Args:
4432
+ connection: The connection to update
4433
+ connection_interval_min: Minimum interval, in milliseconds.
4434
+ connection_interval_max: Maximum interval, in milliseconds.
4435
+ subrate_min: Minimum subrate factor.
4436
+ subrate_max: Maximum subrate factor.
4437
+ max_latency: Max latency, in number of intervals.
4438
+ continuation_number: Continuation number.
4439
+ supervision_timeout: Supervision Timeout, in milliseconds.
4440
+ min_ce_length: Minimum connection event length, in milliseconds.
4441
+ max_ce_length: Maximum connection event length, in milliseconds.
4442
+ '''
4443
+
4444
+ # Convert the input parameters
4445
+ connection_interval_min = int(connection_interval_min / 0.125)
4446
+ connection_interval_max = int(connection_interval_max / 0.125)
4447
+ supervision_timeout = int(supervision_timeout / 10)
4448
+ min_ce_length = int(min_ce_length / 0.125)
4449
+ max_ce_length = int(max_ce_length / 0.125)
4450
+
4451
+ pending_result = asyncio.get_running_loop().create_future()
4452
+ with closing(utils.EventWatcher()) as watcher:
4453
+
4454
+ @watcher.on(connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
4455
+ def _():
4456
+ pending_result.set_result(None)
4457
+
4458
+ @watcher.on(
4459
+ connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE
4460
+ )
4461
+ def _(error_code: int):
4462
+ pending_result.set_exception(hci.HCI_Error(error_code))
4463
+
4464
+ await self.send_async_command(
4465
+ hci.HCI_LE_Connection_Rate_Request_Command(
4466
+ connection_handle=connection.handle,
4467
+ connection_interval_min=connection_interval_min,
4468
+ connection_interval_max=connection_interval_max,
4469
+ subrate_min=subrate_min,
4470
+ subrate_max=subrate_max,
4471
+ max_latency=max_latency,
4472
+ continuation_number=continuation_number,
4473
+ supervision_timeout=supervision_timeout,
4474
+ min_ce_length=min_ce_length,
4475
+ max_ce_length=max_ce_length,
4476
+ )
4477
+ )
4478
+
4479
+ await connection.cancel_on_disconnection(pending_result)
4480
+
4481
+ async def update_connection_subrate(
4482
+ self,
4483
+ connection: Connection,
4484
+ subrate_min: int,
4485
+ subrate_max: int,
4486
+ max_latency: int,
4487
+ continuation_number: int,
4488
+ supervision_timeout: float,
4489
+ ) -> None:
4490
+ '''
4491
+ Request a change to the subrating factor and/or other parameters.
4492
+
4493
+ Args:
4494
+ connection: The connection to update
4495
+ subrate_min: Minimum subrate factor.
4496
+ subrate_max: Maximum subrate factor.
4497
+ max_latency: Max latency, in number of intervals.
4498
+ continuation_number: Continuation number.
4499
+ supervision_timeout: Supervision Timeout, in milliseconds.
4500
+ '''
4501
+
4502
+ pending_result = asyncio.get_running_loop().create_future()
4503
+ with closing(utils.EventWatcher()) as watcher:
4504
+
4505
+ @watcher.on(connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
4506
+ def _():
4507
+ pending_result.set_result(None)
4508
+
4509
+ @watcher.on(
4510
+ connection, connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE
4511
+ )
4512
+ def _(error_code: int):
4513
+ pending_result.set_exception(hci.HCI_Error(error_code))
4514
+
4515
+ await self.send_async_command(
4516
+ hci.HCI_LE_Subrate_Request_Command(
4517
+ connection_handle=connection.handle,
4518
+ subrate_min=subrate_min,
4519
+ subrate_max=subrate_max,
4520
+ max_latency=max_latency,
4521
+ continuation_number=continuation_number,
4522
+ supervision_timeout=int(supervision_timeout / 10),
4523
+ )
4524
+ )
4525
+
4526
+ await connection.cancel_on_disconnection(pending_result)
4221
4527
 
4222
4528
  async def get_connection_rssi(self, connection):
4223
- result = await self.send_command(
4224
- hci.HCI_Read_RSSI_Command(handle=connection.handle), check_result=True
4529
+ result = await self.send_sync_command(
4530
+ hci.HCI_Read_RSSI_Command(handle=connection.handle)
4225
4531
  )
4226
- return result.return_parameters.rssi
4532
+ return result.rssi
4227
4533
 
4228
4534
  async def get_connection_phy(self, connection: Connection) -> ConnectionPHY:
4229
4535
  if not self.host.supports_command(hci.HCI_LE_READ_PHY_COMMAND):
4230
4536
  return ConnectionPHY(hci.Phy.LE_1M, hci.Phy.LE_1M)
4231
4537
 
4232
- result = await self.send_command(
4233
- hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
4234
- check_result=True,
4235
- )
4236
- return ConnectionPHY(
4237
- result.return_parameters.tx_phy, result.return_parameters.rx_phy
4538
+ result = await self.send_sync_command(
4539
+ hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle)
4238
4540
  )
4541
+ return ConnectionPHY(result.tx_phy, result.rx_phy)
4239
4542
 
4240
4543
  async def set_connection_phy(
4241
4544
  self,
@@ -4252,60 +4555,139 @@ class Device(utils.CompositeEventEmitter):
4252
4555
  (1 if rx_phys is None else 0) << 1
4253
4556
  )
4254
4557
 
4255
- await self.send_command(
4558
+ await self.send_async_command(
4256
4559
  hci.HCI_LE_Set_PHY_Command(
4257
4560
  connection_handle=connection.handle,
4258
4561
  all_phys=all_phys_bits,
4259
4562
  tx_phys=hci.phy_list_to_bits(tx_phys),
4260
4563
  rx_phys=hci.phy_list_to_bits(rx_phys),
4261
4564
  phy_options=phy_options,
4262
- ),
4263
- check_result=True,
4565
+ )
4264
4566
  )
4265
4567
 
4266
4568
  async def set_default_phy(
4267
4569
  self,
4268
4570
  tx_phys: Iterable[hci.Phy] | None = None,
4269
4571
  rx_phys: Iterable[hci.Phy] | None = None,
4270
- ):
4572
+ ) -> None:
4271
4573
  all_phys_bits = (1 if tx_phys is None else 0) | (
4272
4574
  (1 if rx_phys is None else 0) << 1
4273
4575
  )
4274
4576
 
4275
- return await self.send_command(
4577
+ await self.send_sync_command(
4276
4578
  hci.HCI_LE_Set_Default_PHY_Command(
4277
4579
  all_phys=all_phys_bits,
4278
4580
  tx_phys=hci.phy_list_to_bits(tx_phys),
4279
4581
  rx_phys=hci.phy_list_to_bits(rx_phys),
4582
+ )
4583
+ )
4584
+
4585
+ async def set_default_connection_subrate(
4586
+ self,
4587
+ subrate_min: int,
4588
+ subrate_max: int,
4589
+ max_latency: int,
4590
+ continuation_number: int,
4591
+ supervision_timeout: float,
4592
+ ) -> None:
4593
+ '''
4594
+ Set the default subrate parameters for new connections.
4595
+
4596
+ Args:
4597
+ subrate_min: Minimum subrate factor.
4598
+ subrate_max: Maximum subrate factor.
4599
+ max_latency: Max latency, in number of intervals.
4600
+ continuation_number: Continuation number.
4601
+ supervision_timeout: Supervision Timeout, in milliseconds.
4602
+ '''
4603
+
4604
+ # Convert the input parameters
4605
+ supervision_timeout = int(supervision_timeout / 10)
4606
+
4607
+ await self.send_command(
4608
+ hci.HCI_LE_Set_Default_Subrate_Command(
4609
+ subrate_min=subrate_min,
4610
+ subrate_max=subrate_max,
4611
+ max_latency=max_latency,
4612
+ continuation_number=continuation_number,
4613
+ supervision_timeout=supervision_timeout,
4280
4614
  ),
4281
4615
  check_result=True,
4282
4616
  )
4283
4617
 
4618
+ async def set_default_connection_parameters(
4619
+ self,
4620
+ connection_interval_min: float,
4621
+ connection_interval_max: float,
4622
+ subrate_min: int,
4623
+ subrate_max: int,
4624
+ max_latency: int,
4625
+ continuation_number: int,
4626
+ supervision_timeout: float,
4627
+ min_ce_length: float = 0.0,
4628
+ max_ce_length: float = 0.0,
4629
+ ) -> None:
4630
+ '''
4631
+ Set the default connection parameters for new connections.
4632
+
4633
+ Args:
4634
+ connection_interval_min: Minimum interval, in milliseconds.
4635
+ connection_interval_max: Maximum interval, in milliseconds.
4636
+ subrate_min: Minimum subrate factor.
4637
+ subrate_max: Maximum subrate factor.
4638
+ max_latency: Max latency, in number of intervals.
4639
+ continuation_number: Continuation number.
4640
+ supervision_timeout: Supervision Timeout, in milliseconds.
4641
+ min_ce_length: Minimum connection event length, in milliseconds.
4642
+ max_ce_length: Maximum connection event length, in milliseconds.
4643
+ '''
4644
+
4645
+ # Convert the input parameters
4646
+ connection_interval_min = int(connection_interval_min / 0.125)
4647
+ connection_interval_max = int(connection_interval_max / 0.125)
4648
+ supervision_timeout = int(supervision_timeout / 10)
4649
+ min_ce_length = int(min_ce_length / 0.125)
4650
+ max_ce_length = int(max_ce_length / 0.125)
4651
+
4652
+ await self.send_sync_command(
4653
+ hci.HCI_LE_Set_Default_Rate_Parameters_Command(
4654
+ connection_interval_min=connection_interval_min,
4655
+ connection_interval_max=connection_interval_max,
4656
+ subrate_min=subrate_min,
4657
+ subrate_max=subrate_max,
4658
+ max_latency=max_latency,
4659
+ continuation_number=continuation_number,
4660
+ supervision_timeout=supervision_timeout,
4661
+ min_ce_length=min_ce_length,
4662
+ max_ce_length=max_ce_length,
4663
+ )
4664
+ )
4665
+
4284
4666
  async def transfer_periodic_sync(
4285
4667
  self, connection: Connection, sync_handle: int, service_data: int = 0
4286
4668
  ) -> None:
4287
- return await self.send_command(
4669
+ await self.send_sync_command(
4288
4670
  hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Command(
4289
4671
  connection_handle=connection.handle,
4290
4672
  service_data=service_data,
4291
4673
  sync_handle=sync_handle,
4292
- ),
4293
- check_result=True,
4674
+ )
4294
4675
  )
4295
4676
 
4296
4677
  async def transfer_periodic_set_info(
4297
4678
  self, connection: Connection, advertising_handle: int, service_data: int = 0
4298
4679
  ) -> None:
4299
- return await self.send_command(
4680
+ await self.send_sync_command(
4300
4681
  hci.HCI_LE_Periodic_Advertising_Set_Info_Transfer_Command(
4301
4682
  connection_handle=connection.handle,
4302
4683
  service_data=service_data,
4303
4684
  advertising_handle=advertising_handle,
4304
- ),
4305
- check_result=True,
4685
+ )
4306
4686
  )
4307
4687
 
4308
- async def find_peer_by_name(self, name: str, transport=PhysicalTransport.LE):
4688
+ async def find_peer_by_name(
4689
+ self, name: str, transport=PhysicalTransport.LE
4690
+ ) -> hci.Address:
4309
4691
  """
4310
4692
  Scan for a peer with a given name and return its address.
4311
4693
  """
@@ -4323,6 +4705,7 @@ class Device(utils.CompositeEventEmitter):
4323
4705
  listener: Callable[..., None] | None = None
4324
4706
  was_scanning = self.scanning
4325
4707
  was_discovering = self.discovering
4708
+ event_name: str | None = None
4326
4709
  try:
4327
4710
  if transport == PhysicalTransport.LE:
4328
4711
  event_name = 'advertisement'
@@ -4348,11 +4731,11 @@ class Device(utils.CompositeEventEmitter):
4348
4731
  if not self.discovering:
4349
4732
  await self.start_discovery()
4350
4733
  else:
4351
- return None
4734
+ raise ValueError('invalid transport')
4352
4735
 
4353
4736
  return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address)
4354
4737
  finally:
4355
- if listener is not None:
4738
+ if listener is not None and event_name is not None:
4356
4739
  self.remove_listener(event_name, listener)
4357
4740
 
4358
4741
  if transport == PhysicalTransport.LE and not was_scanning:
@@ -4367,6 +4750,8 @@ class Device(utils.CompositeEventEmitter):
4367
4750
  Scan for a peer with a resolvable address that can be resolved to a given
4368
4751
  identity address.
4369
4752
  """
4753
+ if self.address_resolver is None:
4754
+ raise InvalidStateError('no resolver')
4370
4755
 
4371
4756
  # Create a future to wait for an address to be found
4372
4757
  peer_address = asyncio.get_running_loop().create_future()
@@ -4378,7 +4763,7 @@ class Device(utils.CompositeEventEmitter):
4378
4763
  peer_address.set_result(address)
4379
4764
  return
4380
4765
 
4381
- if address.is_resolvable:
4766
+ if address.is_resolvable and self.address_resolver is not None:
4382
4767
  resolved_address = self.address_resolver.resolve(address)
4383
4768
  if resolved_address == identity_address:
4384
4769
  if not peer_address.done():
@@ -4490,11 +4875,10 @@ class Device(utils.CompositeEventEmitter):
4490
4875
  pending_authentication.set_exception(hci.HCI_Error(error_code))
4491
4876
 
4492
4877
  # Request the authentication
4493
- await self.send_command(
4878
+ await self.send_async_command(
4494
4879
  hci.HCI_Authentication_Requested_Command(
4495
4880
  connection_handle=connection.handle
4496
- ),
4497
- check_result=True,
4881
+ )
4498
4882
  )
4499
4883
 
4500
4884
  # Wait for the authentication to complete
@@ -4542,22 +4926,20 @@ class Device(utils.CompositeEventEmitter):
4542
4926
  if connection.role != hci.Role.CENTRAL:
4543
4927
  raise InvalidStateError('only centrals can start encryption')
4544
4928
 
4545
- await self.send_command(
4929
+ await self.send_async_command(
4546
4930
  hci.HCI_LE_Enable_Encryption_Command(
4547
4931
  connection_handle=connection.handle,
4548
4932
  random_number=rand,
4549
4933
  encrypted_diversifier=ediv,
4550
4934
  long_term_key=ltk,
4551
- ),
4552
- check_result=True,
4935
+ )
4553
4936
  )
4554
4937
  else:
4555
- await self.send_command(
4938
+ await self.send_async_command(
4556
4939
  hci.HCI_Set_Connection_Encryption_Command(
4557
4940
  connection_handle=connection.handle,
4558
4941
  encryption_enable=0x01 if enable else 0x00,
4559
- ),
4560
- check_result=True,
4942
+ )
4561
4943
  )
4562
4944
 
4563
4945
  # Wait for the result
@@ -4589,15 +4971,13 @@ class Device(utils.CompositeEventEmitter):
4589
4971
  def _(error_code: int):
4590
4972
  pending_role_change.set_exception(hci.HCI_Error(error_code))
4591
4973
 
4592
- await self.send_command(
4593
- hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role),
4594
- check_result=True,
4974
+ await self.send_async_command(
4975
+ hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role)
4595
4976
  )
4596
4977
  await connection.cancel_on_disconnection(pending_role_change)
4597
4978
 
4598
4979
  # [Classic only]
4599
4980
  async def request_remote_name(self, remote: hci.Address | Connection) -> str:
4600
- # Set up event handlers
4601
4981
  pending_name: asyncio.Future[str] = asyncio.get_running_loop().create_future()
4602
4982
 
4603
4983
  peer_address = (
@@ -4616,14 +4996,13 @@ class Device(utils.CompositeEventEmitter):
4616
4996
  if address == peer_address:
4617
4997
  pending_name.set_exception(hci.HCI_Error(error_code))
4618
4998
 
4619
- await self.send_command(
4999
+ await self.send_async_command(
4620
5000
  hci.HCI_Remote_Name_Request_Command(
4621
5001
  bd_addr=peer_address,
4622
5002
  page_scan_repetition_mode=hci.HCI_Remote_Name_Request_Command.R2,
4623
5003
  reserved=0,
4624
5004
  clock_offset=0, # TODO investigate non-0 values
4625
- ),
4626
- check_result=True,
5005
+ )
4627
5006
  )
4628
5007
 
4629
5008
  # Wait for the result
@@ -4634,7 +5013,7 @@ class Device(utils.CompositeEventEmitter):
4634
5013
  async def setup_cig(
4635
5014
  self,
4636
5015
  parameters: CigParameters,
4637
- ) -> list[int]:
5016
+ ) -> Sequence[int]:
4638
5017
  """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
4639
5018
 
4640
5019
  Args:
@@ -4643,7 +5022,7 @@ class Device(utils.CompositeEventEmitter):
4643
5022
  Returns:
4644
5023
  List of created CIS handles corresponding to the same order of [cid_id].
4645
5024
  """
4646
- response = await self.send_command(
5025
+ response = await self.send_sync_command(
4647
5026
  hci.HCI_LE_Set_CIG_Parameters_Command(
4648
5027
  cig_id=parameters.cig_id,
4649
5028
  sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
@@ -4664,13 +5043,12 @@ class Device(utils.CompositeEventEmitter):
4664
5043
  phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
4665
5044
  rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
4666
5045
  rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
4667
- ),
4668
- check_result=True,
5046
+ )
4669
5047
  )
4670
5048
 
4671
5049
  # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
4672
5050
  # Server, so here it only provides a basic functionality for testing.
4673
- cis_handles = response.return_parameters.connection_handle[:]
5051
+ cis_handles = response.connection_handle[:]
4674
5052
  for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
4675
5053
  self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
4676
5054
 
@@ -4710,12 +5088,11 @@ class Device(utils.CompositeEventEmitter):
4710
5088
  watcher.on(
4711
5089
  self, self.EVENT_CIS_ESTABLISHMENT_FAILURE, on_cis_establishment_failure
4712
5090
  )
4713
- await self.send_command(
5091
+ await self.send_async_command(
4714
5092
  hci.HCI_LE_Create_CIS_Command(
4715
5093
  cis_connection_handle=[p[0] for p in cis_acl_pairs],
4716
5094
  acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
4717
- ),
4718
- check_result=True,
5095
+ )
4719
5096
  )
4720
5097
 
4721
5098
  return await asyncio.gather(*pending_cis_establishments.values())
@@ -4754,11 +5131,10 @@ class Device(utils.CompositeEventEmitter):
4754
5131
  on_establishment_failure,
4755
5132
  )
4756
5133
 
4757
- await self.send_command(
5134
+ await self.send_async_command(
4758
5135
  hci.HCI_LE_Accept_CIS_Request_Command(
4759
5136
  connection_handle=cis_link.handle
4760
- ),
4761
- check_result=True,
5137
+ )
4762
5138
  )
4763
5139
 
4764
5140
  await pending_establishment
@@ -4770,11 +5146,10 @@ class Device(utils.CompositeEventEmitter):
4770
5146
  cis_link: CisLink,
4771
5147
  reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
4772
5148
  ) -> None:
4773
- await self.send_command(
5149
+ await self.send_sync_command(
4774
5150
  hci.HCI_LE_Reject_CIS_Request_Command(
4775
5151
  connection_handle=cis_link.handle, reason=reason
4776
- ),
4777
- check_result=True,
5152
+ )
4778
5153
  )
4779
5154
 
4780
5155
  # [LE only]
@@ -4803,7 +5178,7 @@ class Device(utils.CompositeEventEmitter):
4803
5178
  )
4804
5179
 
4805
5180
  try:
4806
- await self.send_command(
5181
+ await self.send_async_command(
4807
5182
  hci.HCI_LE_Create_BIG_Command(
4808
5183
  big_handle=big_handle,
4809
5184
  advertising_handle=advertising_set.advertising_handle,
@@ -4817,8 +5192,7 @@ class Device(utils.CompositeEventEmitter):
4817
5192
  framing=parameters.framing,
4818
5193
  encryption=1 if parameters.broadcast_code else 0,
4819
5194
  broadcast_code=parameters.broadcast_code or bytes(16),
4820
- ),
4821
- check_result=True,
5195
+ )
4822
5196
  )
4823
5197
  await established
4824
5198
  except hci.HCI_Error:
@@ -4858,7 +5232,7 @@ class Device(utils.CompositeEventEmitter):
4858
5232
  )
4859
5233
 
4860
5234
  try:
4861
- await self.send_command(
5235
+ await self.send_async_command(
4862
5236
  hci.HCI_LE_BIG_Create_Sync_Command(
4863
5237
  big_handle=big_handle,
4864
5238
  sync_handle=pa_sync_handle,
@@ -4867,8 +5241,7 @@ class Device(utils.CompositeEventEmitter):
4867
5241
  mse=parameters.mse,
4868
5242
  big_sync_timeout=parameters.big_sync_timeout,
4869
5243
  bis=parameters.bis,
4870
- ),
4871
- check_result=True,
5244
+ )
4872
5245
  )
4873
5246
  await established
4874
5247
  except hci.HCI_Error:
@@ -4901,11 +5274,10 @@ class Device(utils.CompositeEventEmitter):
4901
5274
 
4902
5275
  watcher.on(self.host, 'le_remote_features', on_le_remote_features)
4903
5276
  watcher.on(self.host, 'le_remote_features_failure', on_failure)
4904
- await self.send_command(
5277
+ await self.send_async_command(
4905
5278
  hci.HCI_LE_Read_Remote_Features_Command(
4906
5279
  connection_handle=connection.handle
4907
- ),
4908
- check_result=True,
5280
+ )
4909
5281
  )
4910
5282
  return await read_feature_future
4911
5283
 
@@ -4926,11 +5298,10 @@ class Device(utils.CompositeEventEmitter):
4926
5298
  'channel_sounding_capabilities_failure',
4927
5299
  lambda status: complete_future.set_exception(hci.HCI_Error(status)),
4928
5300
  )
4929
- await self.send_command(
5301
+ await self.send_async_command(
4930
5302
  hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Command(
4931
5303
  connection_handle=connection.handle
4932
- ),
4933
- check_result=True,
5304
+ )
4934
5305
  )
4935
5306
  return await complete_future
4936
5307
 
@@ -4944,14 +5315,13 @@ class Device(utils.CompositeEventEmitter):
4944
5315
  cs_sync_antenna_selection: int = 0xFF, # No Preference
4945
5316
  max_tx_power: int = 0x04, # 4 dB
4946
5317
  ) -> None:
4947
- await self.send_command(
5318
+ await self.send_sync_command(
4948
5319
  hci.HCI_LE_CS_Set_Default_Settings_Command(
4949
5320
  connection_handle=connection.handle,
4950
5321
  role_enable=role_enable,
4951
5322
  cs_sync_antenna_selection=cs_sync_antenna_selection,
4952
5323
  max_tx_power=max_tx_power,
4953
- ),
4954
- check_result=True,
5324
+ )
4955
5325
  )
4956
5326
 
4957
5327
  @utils.experimental('Only for testing.')
@@ -5000,7 +5370,7 @@ class Device(utils.CompositeEventEmitter):
5000
5370
  'channel_sounding_config_failure',
5001
5371
  lambda status: complete_future.set_exception(hci.HCI_Error(status)),
5002
5372
  )
5003
- await self.send_command(
5373
+ await self.send_async_command(
5004
5374
  hci.HCI_LE_CS_Create_Config_Command(
5005
5375
  connection_handle=connection.handle,
5006
5376
  config_id=config_id,
@@ -5020,8 +5390,7 @@ class Device(utils.CompositeEventEmitter):
5020
5390
  ch3c_shape=ch3c_shape,
5021
5391
  ch3c_jump=ch3c_jump,
5022
5392
  reserved=0x00,
5023
- ),
5024
- check_result=True,
5393
+ )
5025
5394
  )
5026
5395
  return await complete_future
5027
5396
 
@@ -5041,11 +5410,10 @@ class Device(utils.CompositeEventEmitter):
5041
5410
  complete_future.set_exception(hci.HCI_Error(event.status))
5042
5411
 
5043
5412
  watcher.once(self.host, 'cs_security', on_event)
5044
- await self.send_command(
5413
+ await self.send_async_command(
5045
5414
  hci.HCI_LE_CS_Security_Enable_Command(
5046
5415
  connection_handle=connection.handle
5047
- ),
5048
- check_result=True,
5416
+ )
5049
5417
  )
5050
5418
  return await complete_future
5051
5419
 
@@ -5067,7 +5435,7 @@ class Device(utils.CompositeEventEmitter):
5067
5435
  snr_control_initiator=hci.CsSnr.NOT_APPLIED,
5068
5436
  snr_control_reflector=hci.CsSnr.NOT_APPLIED,
5069
5437
  ) -> None:
5070
- await self.send_command(
5438
+ await self.send_sync_command(
5071
5439
  hci.HCI_LE_CS_Set_Procedure_Parameters_Command(
5072
5440
  connection_handle=connection.handle,
5073
5441
  config_id=config.config_id,
@@ -5083,8 +5451,7 @@ class Device(utils.CompositeEventEmitter):
5083
5451
  preferred_peer_antenna=preferred_peer_antenna,
5084
5452
  snr_control_initiator=snr_control_initiator,
5085
5453
  snr_control_reflector=snr_control_reflector,
5086
- ),
5087
- check_result=True,
5454
+ )
5088
5455
  )
5089
5456
 
5090
5457
  @utils.experimental('Only for testing.')
@@ -5106,13 +5473,12 @@ class Device(utils.CompositeEventEmitter):
5106
5473
  'channel_sounding_procedure_failure',
5107
5474
  lambda x: complete_future.set_exception(hci.HCI_Error(x)),
5108
5475
  )
5109
- await self.send_command(
5476
+ await self.send_async_command(
5110
5477
  hci.HCI_LE_CS_Procedure_Enable_Command(
5111
5478
  connection_handle=connection.handle,
5112
5479
  config_id=config.config_id,
5113
5480
  enable=enabled,
5114
- ),
5115
- check_result=True,
5481
+ )
5116
5482
  )
5117
5483
  return await complete_future
5118
5484
 
@@ -5743,12 +6109,14 @@ class Device(utils.CompositeEventEmitter):
5743
6109
  )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
5744
6110
 
5745
6111
  # Respond
5746
- self.host.send_command_sync(
5747
- hci.HCI_IO_Capability_Request_Reply_Command(
5748
- bd_addr=connection.peer_address,
5749
- io_capability=pairing_config.delegate.classic_io_capability,
5750
- oob_data_present=0x00, # Not present
5751
- authentication_requirements=authentication_requirements,
6112
+ utils.AsyncRunner.spawn(
6113
+ self.host.send_sync_command(
6114
+ hci.HCI_IO_Capability_Request_Reply_Command(
6115
+ bd_addr=connection.peer_address,
6116
+ io_capability=pairing_config.delegate.classic_io_capability,
6117
+ oob_data_present=0x00, # Not present
6118
+ authentication_requirements=authentication_requirements,
6119
+ )
5752
6120
  )
5753
6121
  )
5754
6122
 
@@ -5832,7 +6200,7 @@ class Device(utils.CompositeEventEmitter):
5832
6200
  async def reply() -> None:
5833
6201
  try:
5834
6202
  if await connection.cancel_on_disconnection(method()):
5835
- await self.host.send_command(
6203
+ await self.host.send_sync_command(
5836
6204
  hci.HCI_User_Confirmation_Request_Reply_Command(
5837
6205
  bd_addr=connection.peer_address
5838
6206
  )
@@ -5841,7 +6209,7 @@ class Device(utils.CompositeEventEmitter):
5841
6209
  except Exception:
5842
6210
  logger.exception('exception while confirming')
5843
6211
 
5844
- await self.host.send_command(
6212
+ await self.host.send_sync_command(
5845
6213
  hci.HCI_User_Confirmation_Request_Negative_Reply_Command(
5846
6214
  bd_addr=connection.peer_address
5847
6215
  )
@@ -5862,7 +6230,7 @@ class Device(utils.CompositeEventEmitter):
5862
6230
  pairing_config.delegate.get_number()
5863
6231
  )
5864
6232
  if number is not None:
5865
- await self.host.send_command(
6233
+ await self.host.send_sync_command(
5866
6234
  hci.HCI_User_Passkey_Request_Reply_Command(
5867
6235
  bd_addr=connection.peer_address, numeric_value=number
5868
6236
  )
@@ -5871,7 +6239,7 @@ class Device(utils.CompositeEventEmitter):
5871
6239
  except Exception:
5872
6240
  logger.exception('exception while asking for pass-key')
5873
6241
 
5874
- await self.host.send_command(
6242
+ await self.host.send_sync_command(
5875
6243
  hci.HCI_User_Passkey_Request_Negative_Reply_Command(
5876
6244
  bd_addr=connection.peer_address
5877
6245
  )
@@ -5914,7 +6282,7 @@ class Device(utils.CompositeEventEmitter):
5914
6282
  pin_code_len = len(pin_code)
5915
6283
  if not 1 <= pin_code_len <= 16:
5916
6284
  raise core.InvalidArgumentError("pin_code should be 1-16 bytes")
5917
- await self.host.send_command(
6285
+ await self.host.send_sync_command(
5918
6286
  hci.HCI_PIN_Code_Request_Reply_Command(
5919
6287
  bd_addr=connection.peer_address,
5920
6288
  pin_code_length=pin_code_len,
@@ -5923,17 +6291,19 @@ class Device(utils.CompositeEventEmitter):
5923
6291
  )
5924
6292
  else:
5925
6293
  logger.debug("delegate.get_string() returned None")
5926
- await self.host.send_command(
6294
+ await self.host.send_sync_command(
5927
6295
  hci.HCI_PIN_Code_Request_Negative_Reply_Command(
5928
6296
  bd_addr=connection.peer_address
5929
6297
  )
5930
6298
  )
5931
6299
 
5932
- asyncio.create_task(get_pin_code())
6300
+ utils.AsyncRunner.spawn(get_pin_code())
5933
6301
  else:
5934
- self.host.send_command_sync(
5935
- hci.HCI_PIN_Code_Request_Negative_Reply_Command(
5936
- bd_addr=connection.peer_address
6302
+ utils.AsyncRunner.spawn(
6303
+ self.host.send_sync_command(
6304
+ hci.HCI_PIN_Code_Request_Negative_Reply_Command(
6305
+ bd_addr=connection.peer_address
6306
+ )
5937
6307
  )
5938
6308
  )
5939
6309
 
@@ -6194,7 +6564,7 @@ class Device(utils.CompositeEventEmitter):
6194
6564
  ):
6195
6565
  logger.debug(
6196
6566
  f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
6197
- f'{connection.peer_address} as {connection.role_name}, '
6567
+ f'{connection.peer_address} as {connection.role_name}'
6198
6568
  )
6199
6569
  if connection.parameters.connection_interval != connection_interval * 1.25:
6200
6570
  connection.parameters = Connection.Parameters(
@@ -6218,7 +6588,41 @@ class Device(utils.CompositeEventEmitter):
6218
6588
  self, connection: Connection, error: int
6219
6589
  ):
6220
6590
  logger.debug(
6221
- f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] '
6591
+ f'*** Connection Parameters Update failed: [0x{connection.handle:04X}] '
6592
+ f'{connection.peer_address} as {connection.role_name}, '
6593
+ f'error={error}'
6594
+ )
6595
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE, error)
6596
+
6597
+ @host_event_handler
6598
+ @with_connection_from_handle
6599
+ def on_le_connection_rate_change(
6600
+ self,
6601
+ connection: Connection,
6602
+ connection_interval: int,
6603
+ subrate_factor: int,
6604
+ peripheral_latency: int,
6605
+ continuation_number: int,
6606
+ supervision_timeout: int,
6607
+ ):
6608
+ logger.debug(
6609
+ f'*** Connection Rate Change: [0x{connection.handle:04X}] '
6610
+ f'{connection.peer_address} as {connection.role_name}'
6611
+ )
6612
+ connection.parameters = Connection.Parameters(
6613
+ connection_interval=connection_interval * 0.125,
6614
+ subrate_factor=subrate_factor,
6615
+ peripheral_latency=peripheral_latency,
6616
+ continuation_number=continuation_number,
6617
+ supervision_timeout=supervision_timeout * 10.0,
6618
+ )
6619
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
6620
+
6621
+ @host_event_handler
6622
+ @with_connection_from_handle
6623
+ def on_le_connection_rate_change_failure(self, connection: Connection, error: int):
6624
+ logger.debug(
6625
+ f'*** Connection Rate Change failed: [0x{connection.handle:04X}] '
6222
6626
  f'{connection.peer_address} as {connection.role_name}, '
6223
6627
  f'error={error}'
6224
6628
  )
@@ -6261,7 +6665,25 @@ class Device(utils.CompositeEventEmitter):
6261
6665
  subrate_factor,
6262
6666
  continuation_number,
6263
6667
  )
6264
- connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
6668
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
6669
+
6670
+ @host_event_handler
6671
+ @with_connection_from_handle
6672
+ def on_le_subrate_change_failure(self, connection: Connection, status: int):
6673
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE, status)
6674
+
6675
+ @host_event_handler
6676
+ @with_connection_from_handle
6677
+ def on_le_remote_features(
6678
+ self, connection: Connection, le_features: hci.LeFeatureMask
6679
+ ):
6680
+ connection.peer_le_features = le_features
6681
+ connection.emit(connection.EVENT_LE_REMOTE_FEATURES_CHANGE)
6682
+
6683
+ @host_event_handler
6684
+ @with_connection_from_handle
6685
+ def on_le_remote_features_failure(self, connection: Connection, status: int):
6686
+ connection.emit(connection.EVENT_LE_REMOTE_FEATURES_CHANGE_FAILURE, status)
6265
6687
 
6266
6688
  @host_event_handler
6267
6689
  @with_connection_from_handle
@@ -6308,7 +6730,7 @@ class Device(utils.CompositeEventEmitter):
6308
6730
  rtt_capability=event.rtt_capability,
6309
6731
  rtt_aa_only_n=event.rtt_aa_only_n,
6310
6732
  rtt_sounding_n=event.rtt_sounding_n,
6311
- rtt_random_payload_n=event.rtt_random_payload_n,
6733
+ rtt_random_sequence_n=event.rtt_random_sequence_n,
6312
6734
  nadm_sounding_capability=event.nadm_sounding_capability,
6313
6735
  nadm_random_capability=event.nadm_random_capability,
6314
6736
  cs_sync_phys_supported=event.cs_sync_phys_supported,