bumble 0.0.194__py3-none-any.whl → 0.0.198__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 (54) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +692 -0
  3. bumble/apps/bench.py +77 -23
  4. bumble/apps/console.py +5 -20
  5. bumble/apps/controller_info.py +3 -3
  6. bumble/apps/device_info.py +230 -0
  7. bumble/apps/gatt_dump.py +4 -0
  8. bumble/apps/lea_unicast/app.py +16 -17
  9. bumble/at.py +12 -6
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +5 -1
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +726 -122
  17. bumble/device.py +817 -117
  18. bumble/drivers/rtk.py +13 -8
  19. bumble/gatt.py +6 -1
  20. bumble/gatt_client.py +10 -4
  21. bumble/hci.py +283 -20
  22. bumble/hid.py +24 -28
  23. bumble/host.py +29 -0
  24. bumble/l2cap.py +24 -17
  25. bumble/link.py +8 -3
  26. bumble/pandora/host.py +3 -2
  27. bumble/profiles/ascs.py +739 -0
  28. bumble/profiles/bap.py +85 -862
  29. bumble/profiles/bass.py +440 -0
  30. bumble/profiles/csip.py +4 -4
  31. bumble/profiles/gap.py +110 -0
  32. bumble/profiles/heart_rate_service.py +4 -3
  33. bumble/profiles/le_audio.py +83 -0
  34. bumble/profiles/mcp.py +448 -0
  35. bumble/profiles/pacs.py +210 -0
  36. bumble/profiles/pbp.py +46 -0
  37. bumble/profiles/tmap.py +89 -0
  38. bumble/rfcomm.py +14 -3
  39. bumble/sdp.py +13 -11
  40. bumble/smp.py +20 -8
  41. bumble/snoop.py +5 -4
  42. bumble/transport/__init__.py +8 -2
  43. bumble/transport/android_emulator.py +9 -3
  44. bumble/transport/android_netsim.py +9 -7
  45. bumble/transport/common.py +46 -18
  46. bumble/transport/pyusb.py +2 -2
  47. bumble/transport/unix.py +56 -0
  48. bumble/transport/usb.py +57 -46
  49. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  50. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/RECORD +54 -43
  51. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  52. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  53. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  54. {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -16,22 +16,22 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
- from enum import IntEnum
20
- import copy
21
- import functools
22
- import json
23
19
  import asyncio
24
- import logging
25
- import secrets
26
- import sys
20
+ from collections.abc import Iterable
27
21
  from contextlib import (
28
22
  asynccontextmanager,
29
23
  AsyncExitStack,
30
24
  closing,
31
- AbstractAsyncContextManager,
32
25
  )
26
+ import copy
33
27
  from dataclasses import dataclass, field
34
- from collections.abc import Iterable
28
+ from enum import Enum, IntEnum
29
+ import functools
30
+ import itertools
31
+ import json
32
+ import logging
33
+ import secrets
34
+ import sys
35
35
  from typing import (
36
36
  Any,
37
37
  Callable,
@@ -51,6 +51,7 @@ from typing_extensions import Self
51
51
 
52
52
  from pyee import EventEmitter
53
53
 
54
+ from bumble import hci
54
55
  from .colors import color
55
56
  from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
56
57
  from .gatt import Characteristic, Descriptor, Service
@@ -81,6 +82,7 @@ from .hci import (
81
82
  HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
82
83
  HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
83
84
  HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
85
+ HCI_OPERATION_CANCELLED_BY_HOST_ERROR,
84
86
  HCI_R2_PAGE_SCAN_REPETITION_MODE,
85
87
  HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
86
88
  HCI_SUCCESS,
@@ -102,11 +104,17 @@ from .hci import (
102
104
  HCI_LE_Accept_CIS_Request_Command,
103
105
  HCI_LE_Add_Device_To_Resolving_List_Command,
104
106
  HCI_LE_Advertising_Report_Event,
107
+ HCI_LE_BIGInfo_Advertising_Report_Event,
105
108
  HCI_LE_Clear_Resolving_List_Command,
106
109
  HCI_LE_Connection_Update_Command,
107
110
  HCI_LE_Create_Connection_Cancel_Command,
108
111
  HCI_LE_Create_Connection_Command,
109
112
  HCI_LE_Create_CIS_Command,
113
+ HCI_LE_Periodic_Advertising_Create_Sync_Command,
114
+ HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command,
115
+ HCI_LE_Periodic_Advertising_Report_Event,
116
+ HCI_LE_Periodic_Advertising_Sync_Transfer_Command,
117
+ HCI_LE_Periodic_Advertising_Terminate_Sync_Command,
110
118
  HCI_LE_Enable_Encryption_Command,
111
119
  HCI_LE_Extended_Advertising_Report_Event,
112
120
  HCI_LE_Extended_Create_Connection_Command,
@@ -162,21 +170,29 @@ from .hci import (
162
170
  OwnAddressType,
163
171
  LeFeature,
164
172
  LeFeatureMask,
173
+ LmpFeatureMask,
165
174
  Phy,
166
175
  phy_list_to_bits,
167
176
  )
168
177
  from .host import Host
169
- from .gap import GenericAccessService
178
+ from .profiles.gap import GenericAccessService
170
179
  from .core import (
171
180
  BT_BR_EDR_TRANSPORT,
172
181
  BT_CENTRAL_ROLE,
173
182
  BT_LE_TRANSPORT,
174
183
  BT_PERIPHERAL_ROLE,
175
184
  AdvertisingData,
185
+ BaseBumbleError,
176
186
  ConnectionParameterUpdateError,
177
187
  CommandTimeoutError,
188
+ ConnectionParameters,
178
189
  ConnectionPHY,
190
+ InvalidArgumentError,
191
+ InvalidOperationError,
179
192
  InvalidStateError,
193
+ NotSupportedError,
194
+ OutOfResourcesError,
195
+ UnreachableError,
180
196
  )
181
197
  from .utils import (
182
198
  AsyncRunner,
@@ -191,13 +207,13 @@ from .keys import (
191
207
  KeyStore,
192
208
  PairingKeys,
193
209
  )
194
- from .pairing import PairingConfig
195
- from . import gatt_client
196
- from . import gatt_server
197
- from . import smp
198
- from . import sdp
199
- from . import l2cap
200
- from . import core
210
+ from bumble import pairing
211
+ from bumble import gatt_client
212
+ from bumble import gatt_server
213
+ from bumble import smp
214
+ from bumble import sdp
215
+ from bumble import l2cap
216
+ from bumble import core
201
217
 
202
218
  if TYPE_CHECKING:
203
219
  from .transport.common import TransportSource, TransportSink
@@ -248,6 +264,9 @@ DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
248
264
  DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
249
265
  HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE
250
266
  )
267
+ DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
268
+ DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
269
+ DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
251
270
 
252
271
  # fmt: on
253
272
  # pylint: enable=line-too-long
@@ -259,6 +278,8 @@ DEVICE_MAX_HIGH_DUTY_CYCLE_CONNECTABLE_DIRECTED_ADVERTISING_DURATION = 1.28
259
278
  # -----------------------------------------------------------------------------
260
279
  # Classes
261
280
  # -----------------------------------------------------------------------------
281
+ class ObjectLookupError(BaseBumbleError):
282
+ """Error raised when failed to lookup an object."""
262
283
 
263
284
 
264
285
  # -----------------------------------------------------------------------------
@@ -552,6 +573,70 @@ class AdvertisingEventProperties:
552
573
  )
553
574
 
554
575
 
576
+ # -----------------------------------------------------------------------------
577
+ @dataclass
578
+ class PeriodicAdvertisement:
579
+ address: Address
580
+ sid: int
581
+ tx_power: int = (
582
+ HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
583
+ )
584
+ rssi: int = HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
585
+ is_truncated: bool = False
586
+ data_bytes: bytes = b''
587
+
588
+ # Constants
589
+ TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
590
+ HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
591
+ )
592
+ RSSI_NOT_AVAILABLE: ClassVar[int] = (
593
+ HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
594
+ )
595
+
596
+ def __post_init__(self) -> None:
597
+ self.data = (
598
+ None if self.is_truncated else AdvertisingData.from_bytes(self.data_bytes)
599
+ )
600
+
601
+
602
+ # -----------------------------------------------------------------------------
603
+ @dataclass
604
+ class BIGInfoAdvertisement:
605
+ address: Address
606
+ sid: int
607
+ num_bis: int
608
+ nse: int
609
+ iso_interval: int
610
+ bn: int
611
+ pto: int
612
+ irc: int
613
+ max_pdu: int
614
+ sdu_interval: int
615
+ max_sdu: int
616
+ phy: Phy
617
+ framed: bool
618
+ encrypted: bool
619
+
620
+ @classmethod
621
+ def from_report(cls, address: Address, sid: int, report) -> Self:
622
+ return cls(
623
+ address,
624
+ sid,
625
+ report.num_bis,
626
+ report.nse,
627
+ report.iso_interval,
628
+ report.bn,
629
+ report.pto,
630
+ report.irc,
631
+ report.max_pdu,
632
+ report.sdu_interval,
633
+ report.max_sdu,
634
+ Phy(report.phy),
635
+ report.framing != 0,
636
+ report.encryption != 0,
637
+ )
638
+
639
+
555
640
  # -----------------------------------------------------------------------------
556
641
  # TODO: replace with typing.TypeAlias when the code base is all Python >= 3.10
557
642
  AdvertisingChannelMap = HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap
@@ -795,6 +880,206 @@ class AdvertisingSet(EventEmitter):
795
880
  self.emit('termination', status)
796
881
 
797
882
 
883
+ # -----------------------------------------------------------------------------
884
+ class PeriodicAdvertisingSync(EventEmitter):
885
+ class State(Enum):
886
+ INIT = 0
887
+ PENDING = 1
888
+ ESTABLISHED = 2
889
+ CANCELLED = 3
890
+ ERROR = 4
891
+ LOST = 5
892
+ TERMINATED = 6
893
+
894
+ _state: State
895
+ sync_handle: Optional[int]
896
+ advertiser_address: Address
897
+ sid: int
898
+ skip: int
899
+ sync_timeout: float # Sync timeout, in seconds
900
+ filter_duplicates: bool
901
+ status: int
902
+ advertiser_phy: int
903
+ periodic_advertising_interval: int
904
+ advertiser_clock_accuracy: int
905
+
906
+ def __init__(
907
+ self,
908
+ device: Device,
909
+ advertiser_address: Address,
910
+ sid: int,
911
+ skip: int,
912
+ sync_timeout: float,
913
+ filter_duplicates: bool,
914
+ ) -> None:
915
+ super().__init__()
916
+ self._state = self.State.INIT
917
+ self.sync_handle = None
918
+ self.device = device
919
+ self.advertiser_address = advertiser_address
920
+ self.sid = sid
921
+ self.skip = skip
922
+ self.sync_timeout = sync_timeout
923
+ self.filter_duplicates = filter_duplicates
924
+ self.status = HCI_SUCCESS
925
+ self.advertiser_phy = 0
926
+ self.periodic_advertising_interval = 0
927
+ self.advertiser_clock_accuracy = 0
928
+ self.data_accumulator = b''
929
+
930
+ @property
931
+ def state(self) -> State:
932
+ return self._state
933
+
934
+ @state.setter
935
+ def state(self, state: State) -> None:
936
+ logger.debug(f'{self} -> {state.name}')
937
+ self._state = state
938
+ self.emit('state_change')
939
+
940
+ async def establish(self) -> None:
941
+ if self.state != self.State.INIT:
942
+ raise InvalidStateError('sync not in init state')
943
+
944
+ options = HCI_LE_Periodic_Advertising_Create_Sync_Command.Options(0)
945
+ if self.filter_duplicates:
946
+ options |= (
947
+ HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
948
+ )
949
+
950
+ response = await self.device.send_command(
951
+ HCI_LE_Periodic_Advertising_Create_Sync_Command(
952
+ options=options,
953
+ advertising_sid=self.sid,
954
+ advertiser_address_type=self.advertiser_address.address_type,
955
+ advertiser_address=self.advertiser_address,
956
+ skip=self.skip,
957
+ sync_timeout=int(self.sync_timeout * 100),
958
+ sync_cte_type=0,
959
+ )
960
+ )
961
+ if response.status != HCI_Command_Status_Event.PENDING:
962
+ raise HCI_StatusError(response)
963
+
964
+ self.state = self.State.PENDING
965
+
966
+ async def terminate(self) -> None:
967
+ if self.state in (self.State.INIT, self.State.CANCELLED, self.State.TERMINATED):
968
+ return
969
+
970
+ if self.state == self.State.PENDING:
971
+ self.state = self.State.CANCELLED
972
+ response = await self.device.send_command(
973
+ HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command(),
974
+ )
975
+ if response.return_parameters == HCI_SUCCESS:
976
+ if self in self.device.periodic_advertising_syncs:
977
+ self.device.periodic_advertising_syncs.remove(self)
978
+ return
979
+
980
+ if self.state in (self.State.ESTABLISHED, self.State.ERROR, self.State.LOST):
981
+ self.state = self.State.TERMINATED
982
+ if self.sync_handle is not None:
983
+ await self.device.send_command(
984
+ HCI_LE_Periodic_Advertising_Terminate_Sync_Command(
985
+ sync_handle=self.sync_handle
986
+ )
987
+ )
988
+ self.device.periodic_advertising_syncs.remove(self)
989
+
990
+ async def transfer(self, connection: Connection, service_data: int = 0) -> None:
991
+ if self.sync_handle is not None:
992
+ await connection.transfer_periodic_sync(self.sync_handle, service_data)
993
+
994
+ def on_establishment(
995
+ self,
996
+ status,
997
+ sync_handle,
998
+ advertiser_phy,
999
+ periodic_advertising_interval,
1000
+ advertiser_clock_accuracy,
1001
+ ) -> None:
1002
+ self.status = status
1003
+
1004
+ if self.state == self.State.CANCELLED:
1005
+ # Somehow, we receive an established event after trying to cancel, most
1006
+ # likely because the cancel command was sent too late, when the sync was
1007
+ # already established, but before the established event was sent.
1008
+ # We need to automatically terminate.
1009
+ logger.debug(
1010
+ "received established event for cancelled sync, will terminate"
1011
+ )
1012
+ self.state = self.State.ESTABLISHED
1013
+ AsyncRunner.spawn(self.terminate())
1014
+ return
1015
+
1016
+ if status == HCI_SUCCESS:
1017
+ self.sync_handle = sync_handle
1018
+ self.advertiser_phy = advertiser_phy
1019
+ self.periodic_advertising_interval = periodic_advertising_interval
1020
+ self.advertiser_clock_accuracy = advertiser_clock_accuracy
1021
+ self.state = self.State.ESTABLISHED
1022
+ self.emit('establishment')
1023
+ return
1024
+
1025
+ # We don't need to keep a reference anymore
1026
+ if self in self.device.periodic_advertising_syncs:
1027
+ self.device.periodic_advertising_syncs.remove(self)
1028
+
1029
+ if status == HCI_OPERATION_CANCELLED_BY_HOST_ERROR:
1030
+ self.state = self.State.CANCELLED
1031
+ self.emit('cancellation')
1032
+ return
1033
+
1034
+ self.state = self.State.ERROR
1035
+ self.emit('error')
1036
+
1037
+ def on_loss(self):
1038
+ self.state = self.State.LOST
1039
+ self.emit('loss')
1040
+
1041
+ def on_periodic_advertising_report(self, report) -> None:
1042
+ self.data_accumulator += report.data
1043
+ if (
1044
+ report.data_status
1045
+ == HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_MORE_TO_COME
1046
+ ):
1047
+ return
1048
+
1049
+ self.emit(
1050
+ 'periodic_advertisement',
1051
+ PeriodicAdvertisement(
1052
+ self.advertiser_address,
1053
+ self.sid,
1054
+ report.tx_power,
1055
+ report.rssi,
1056
+ is_truncated=(
1057
+ report.data_status
1058
+ == HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME
1059
+ ),
1060
+ data_bytes=self.data_accumulator,
1061
+ ),
1062
+ )
1063
+ self.data_accumulator = b''
1064
+
1065
+ def on_biginfo_advertising_report(self, report) -> None:
1066
+ self.emit(
1067
+ 'biginfo_advertisement',
1068
+ BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
1069
+ )
1070
+
1071
+ def __str__(self) -> str:
1072
+ return (
1073
+ 'PeriodicAdvertisingSync('
1074
+ f'state={self.state.name}, '
1075
+ f'sync_handle={self.sync_handle}, '
1076
+ f'sid={self.sid}, '
1077
+ f'skip={self.skip}, '
1078
+ f'filter_duplicates={self.filter_duplicates}'
1079
+ ')'
1080
+ )
1081
+
1082
+
798
1083
  # -----------------------------------------------------------------------------
799
1084
  class LePhyOptions:
800
1085
  # Coded PHY preference
@@ -867,6 +1152,15 @@ class Peer:
867
1152
  async def discover_attributes(self) -> List[gatt_client.AttributeProxy]:
868
1153
  return await self.gatt_client.discover_attributes()
869
1154
 
1155
+ async def discover_all(self):
1156
+ await self.discover_services()
1157
+ for service in self.services:
1158
+ await self.discover_characteristics(service=service)
1159
+
1160
+ for service in self.services:
1161
+ for characteristic in service.characteristics:
1162
+ await self.discover_descriptors(characteristic=characteristic)
1163
+
870
1164
  async def subscribe(
871
1165
  self,
872
1166
  characteristic: gatt_client.CharacteristicProxy,
@@ -906,12 +1200,29 @@ class Peer:
906
1200
  return self.gatt_client.get_services_by_uuid(uuid)
907
1201
 
908
1202
  def get_characteristics_by_uuid(
909
- self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
1203
+ self,
1204
+ uuid: core.UUID,
1205
+ service: Optional[Union[gatt_client.ServiceProxy, core.UUID]] = None,
910
1206
  ) -> List[gatt_client.CharacteristicProxy]:
1207
+ if isinstance(service, core.UUID):
1208
+ return list(
1209
+ itertools.chain(
1210
+ *[
1211
+ self.get_characteristics_by_uuid(uuid, s)
1212
+ for s in self.get_services_by_uuid(service)
1213
+ ]
1214
+ )
1215
+ )
1216
+
911
1217
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
912
1218
 
913
- def create_service_proxy(self, proxy_class: Type[_PROXY_CLASS]) -> _PROXY_CLASS:
914
- return cast(_PROXY_CLASS, proxy_class.from_client(self.gatt_client))
1219
+ def create_service_proxy(
1220
+ self, proxy_class: Type[_PROXY_CLASS]
1221
+ ) -> Optional[_PROXY_CLASS]:
1222
+ if proxy := proxy_class.from_client(self.gatt_client):
1223
+ return cast(_PROXY_CLASS, proxy)
1224
+
1225
+ return None
915
1226
 
916
1227
  async def discover_service_and_create_proxy(
917
1228
  self, proxy_class: Type[_PROXY_CLASS]
@@ -1008,6 +1319,7 @@ class Connection(CompositeEventEmitter):
1008
1319
  handle: int
1009
1320
  transport: int
1010
1321
  self_address: Address
1322
+ self_resolvable_address: Optional[Address]
1011
1323
  peer_address: Address
1012
1324
  peer_resolvable_address: Optional[Address]
1013
1325
  peer_le_features: Optional[LeFeatureMask]
@@ -1055,6 +1367,7 @@ class Connection(CompositeEventEmitter):
1055
1367
  handle,
1056
1368
  transport,
1057
1369
  self_address,
1370
+ self_resolvable_address,
1058
1371
  peer_address,
1059
1372
  peer_resolvable_address,
1060
1373
  role,
@@ -1066,6 +1379,7 @@ class Connection(CompositeEventEmitter):
1066
1379
  self.handle = handle
1067
1380
  self.transport = transport
1068
1381
  self.self_address = self_address
1382
+ self.self_resolvable_address = self_resolvable_address
1069
1383
  self.peer_address = peer_address
1070
1384
  self.peer_resolvable_address = peer_resolvable_address
1071
1385
  self.peer_name = None # Classic only
@@ -1099,6 +1413,7 @@ class Connection(CompositeEventEmitter):
1099
1413
  None,
1100
1414
  BT_BR_EDR_TRANSPORT,
1101
1415
  device.public_address,
1416
+ None,
1102
1417
  peer_address,
1103
1418
  None,
1104
1419
  role,
@@ -1192,11 +1507,9 @@ class Connection(CompositeEventEmitter):
1192
1507
 
1193
1508
  try:
1194
1509
  await asyncio.wait_for(self.device.abort_on('flush', abort), timeout)
1195
- except asyncio.TimeoutError:
1196
- pass
1197
-
1198
- self.remove_listener('disconnection', abort.set_result)
1199
- self.remove_listener('disconnection_failure', abort.set_exception)
1510
+ finally:
1511
+ self.remove_listener('disconnection', abort.set_result)
1512
+ self.remove_listener('disconnection_failure', abort.set_exception)
1200
1513
 
1201
1514
  async def set_data_length(self, tx_octets, tx_time) -> None:
1202
1515
  return await self.device.set_data_length(self, tx_octets, tx_time)
@@ -1227,6 +1540,11 @@ class Connection(CompositeEventEmitter):
1227
1540
  async def get_phy(self):
1228
1541
  return await self.device.get_connection_phy(self)
1229
1542
 
1543
+ async def transfer_periodic_sync(
1544
+ self, sync_handle: int, service_data: int = 0
1545
+ ) -> None:
1546
+ await self.device.transfer_periodic_sync(self, sync_handle, service_data)
1547
+
1230
1548
  # [Classic only]
1231
1549
  async def request_remote_name(self):
1232
1550
  return await self.device.request_remote_name(self)
@@ -1257,7 +1575,9 @@ class Connection(CompositeEventEmitter):
1257
1575
  f'Connection(handle=0x{self.handle:04X}, '
1258
1576
  f'role={self.role_name}, '
1259
1577
  f'self_address={self.self_address}, '
1260
- f'peer_address={self.peer_address})'
1578
+ f'self_resolvable_address={self.self_resolvable_address}, '
1579
+ f'peer_address={self.peer_address}, '
1580
+ f'peer_resolvable_address={self.peer_resolvable_address})'
1261
1581
  )
1262
1582
 
1263
1583
 
@@ -1272,13 +1592,15 @@ class DeviceConfiguration:
1272
1592
  advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1273
1593
  advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1274
1594
  le_enabled: bool = True
1275
- # LE host enable 2nd parameter
1276
1595
  le_simultaneous_enabled: bool = False
1596
+ le_privacy_enabled: bool = False
1597
+ le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
1277
1598
  classic_enabled: bool = False
1278
1599
  classic_sc_enabled: bool = True
1279
1600
  classic_ssp_enabled: bool = True
1280
1601
  classic_smp_enabled: bool = True
1281
1602
  classic_accept_any: bool = True
1603
+ classic_interlaced_scan_enabled: bool = True
1282
1604
  connectable: bool = True
1283
1605
  discoverable: bool = True
1284
1606
  advertising_data: bytes = bytes(
@@ -1289,7 +1611,10 @@ class DeviceConfiguration:
1289
1611
  irk: bytes = bytes(16) # This really must be changed for any level of security
1290
1612
  keystore: Optional[str] = None
1291
1613
  address_resolution_offload: bool = False
1614
+ address_generation_offload: bool = False
1292
1615
  cis_enabled: bool = False
1616
+ identity_address_type: Optional[int] = None
1617
+ io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
1293
1618
 
1294
1619
  def __post_init__(self) -> None:
1295
1620
  self.gatt_services: List[Dict[str, Any]] = []
@@ -1374,7 +1699,9 @@ def with_connection_from_handle(function):
1374
1699
  @functools.wraps(function)
1375
1700
  def wrapper(self, connection_handle, *args, **kwargs):
1376
1701
  if (connection := self.lookup_connection(connection_handle)) is None:
1377
- raise ValueError(f'no connection for handle: 0x{connection_handle:04x}')
1702
+ raise ObjectLookupError(
1703
+ f'no connection for handle: 0x{connection_handle:04x}'
1704
+ )
1378
1705
  return function(self, connection, *args, **kwargs)
1379
1706
 
1380
1707
  return wrapper
@@ -1389,7 +1716,7 @@ def with_connection_from_address(function):
1389
1716
  for connection in self.connections.values():
1390
1717
  if connection.peer_address == address:
1391
1718
  return function(self, connection, *args, **kwargs)
1392
- raise ValueError('no connection for address')
1719
+ raise ObjectLookupError('no connection for address')
1393
1720
 
1394
1721
  return wrapper
1395
1722
 
@@ -1409,6 +1736,20 @@ def try_with_connection_from_address(function):
1409
1736
  return wrapper
1410
1737
 
1411
1738
 
1739
+ # Decorator that converts the first argument from a sync handle to a periodic
1740
+ # advertising sync object
1741
+ def with_periodic_advertising_sync_from_handle(function):
1742
+ @functools.wraps(function)
1743
+ def wrapper(self, sync_handle, *args, **kwargs):
1744
+ if (sync := self.lookup_periodic_advertising_sync(sync_handle)) is None:
1745
+ raise ValueError(
1746
+ f'no periodic advertising sync for handle: 0x{sync_handle:04x}'
1747
+ )
1748
+ return function(self, sync, *args, **kwargs)
1749
+
1750
+ return wrapper
1751
+
1752
+
1412
1753
  # Decorator that adds a method to the list of event handlers for host events.
1413
1754
  # This assumes that the method name starts with `on_`
1414
1755
  def host_event_handler(function):
@@ -1425,8 +1766,9 @@ device_host_event_handlers: List[str] = []
1425
1766
  # -----------------------------------------------------------------------------
1426
1767
  class Device(CompositeEventEmitter):
1427
1768
  # Incomplete list of fields.
1428
- random_address: Address
1429
- public_address: Address
1769
+ random_address: Address # Random address that may change with RPA
1770
+ public_address: Address # Public address (obtained from the controller)
1771
+ static_address: Address # Random address that can be set but does not change
1430
1772
  classic_enabled: bool
1431
1773
  name: str
1432
1774
  class_of_device: int
@@ -1439,6 +1781,7 @@ class Device(CompositeEventEmitter):
1439
1781
  Address, List[asyncio.Future[Union[Connection, Tuple[Address, int, int]]]]
1440
1782
  ]
1441
1783
  advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
1784
+ periodic_advertising_syncs: List[PeriodicAdvertisingSync]
1442
1785
  config: DeviceConfiguration
1443
1786
  legacy_advertiser: Optional[LegacyAdvertiser]
1444
1787
  sco_links: Dict[int, ScoLink]
@@ -1524,6 +1867,7 @@ class Device(CompositeEventEmitter):
1524
1867
  [l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]
1525
1868
  )
1526
1869
  self.advertisement_accumulators = {} # Accumulators, by address
1870
+ self.periodic_advertising_syncs = []
1527
1871
  self.scanning = False
1528
1872
  self.scanning_is_passive = False
1529
1873
  self.discovering = False
@@ -1554,26 +1898,33 @@ class Device(CompositeEventEmitter):
1554
1898
  config = config or DeviceConfiguration()
1555
1899
  self.config = config
1556
1900
 
1557
- self.public_address = Address('00:00:00:00:00:00')
1558
1901
  self.name = config.name
1902
+ self.public_address = Address.ANY
1559
1903
  self.random_address = config.address
1904
+ self.static_address = config.address
1560
1905
  self.class_of_device = config.class_of_device
1561
1906
  self.keystore = None
1562
1907
  self.irk = config.irk
1563
1908
  self.le_enabled = config.le_enabled
1564
- self.classic_enabled = config.classic_enabled
1565
1909
  self.le_simultaneous_enabled = config.le_simultaneous_enabled
1910
+ self.le_privacy_enabled = config.le_privacy_enabled
1911
+ self.le_rpa_timeout = config.le_rpa_timeout
1912
+ self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
1913
+ self.classic_enabled = config.classic_enabled
1566
1914
  self.cis_enabled = config.cis_enabled
1567
1915
  self.classic_sc_enabled = config.classic_sc_enabled
1568
1916
  self.classic_ssp_enabled = config.classic_ssp_enabled
1569
1917
  self.classic_smp_enabled = config.classic_smp_enabled
1918
+ self.classic_interlaced_scan_enabled = config.classic_interlaced_scan_enabled
1570
1919
  self.discoverable = config.discoverable
1571
1920
  self.connectable = config.connectable
1572
1921
  self.classic_accept_any = config.classic_accept_any
1573
1922
  self.address_resolution_offload = config.address_resolution_offload
1923
+ self.address_generation_offload = config.address_generation_offload
1574
1924
 
1575
1925
  # Extended advertising.
1576
1926
  self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
1927
+ self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
1577
1928
 
1578
1929
  # Legacy advertising.
1579
1930
  # The advertising and scan response data, as well as the advertising interval
@@ -1625,10 +1976,23 @@ class Device(CompositeEventEmitter):
1625
1976
  if isinstance(address, str):
1626
1977
  address = Address(address)
1627
1978
  self.random_address = address
1979
+ self.static_address = address
1628
1980
 
1629
1981
  # Setup SMP
1630
1982
  self.smp_manager = smp.Manager(
1631
- self, pairing_config_factory=lambda connection: PairingConfig()
1983
+ self,
1984
+ pairing_config_factory=lambda connection: pairing.PairingConfig(
1985
+ identity_address_type=(
1986
+ pairing.PairingConfig.AddressType(self.config.identity_address_type)
1987
+ if self.config.identity_address_type
1988
+ else None
1989
+ ),
1990
+ delegate=pairing.PairingDelegate(
1991
+ io_capability=pairing.PairingDelegate.IoCapability(
1992
+ self.config.io_capability
1993
+ )
1994
+ ),
1995
+ ),
1632
1996
  )
1633
1997
 
1634
1998
  self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
@@ -1706,6 +2070,18 @@ class Device(CompositeEventEmitter):
1706
2070
 
1707
2071
  return None
1708
2072
 
2073
+ def lookup_periodic_advertising_sync(
2074
+ self, sync_handle: int
2075
+ ) -> Optional[PeriodicAdvertisingSync]:
2076
+ return next(
2077
+ (
2078
+ sync
2079
+ for sync in self.periodic_advertising_syncs
2080
+ if sync.sync_handle == sync_handle
2081
+ ),
2082
+ None,
2083
+ )
2084
+
1709
2085
  @deprecated("Please use create_l2cap_server()")
1710
2086
  def register_l2cap_server(self, psm, server) -> int:
1711
2087
  return self.l2cap_channel_manager.register_server(psm, server)
@@ -1798,7 +2174,7 @@ class Device(CompositeEventEmitter):
1798
2174
  spec=spec,
1799
2175
  )
1800
2176
  else:
1801
- raise ValueError(f'Unexpected mode {spec}')
2177
+ raise InvalidArgumentError(f'Unexpected mode {spec}')
1802
2178
 
1803
2179
  def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
1804
2180
  self.host.send_l2cap_pdu(connection_handle, cid, pdu)
@@ -1840,26 +2216,26 @@ class Device(CompositeEventEmitter):
1840
2216
  HCI_Write_LE_Host_Support_Command(
1841
2217
  le_supported_host=int(self.le_enabled),
1842
2218
  simultaneous_le_host=int(self.le_simultaneous_enabled),
1843
- )
2219
+ ),
2220
+ check_result=True,
1844
2221
  )
1845
2222
 
1846
2223
  if self.le_enabled:
1847
- # Set the controller address
1848
- if self.random_address == Address.ANY_RANDOM:
1849
- # Try to use an address generated at random by the controller
1850
- if self.host.supports_command(HCI_LE_RAND_COMMAND):
1851
- # Get 8 random bytes
1852
- response = await self.send_command(
1853
- HCI_LE_Rand_Command(), check_result=True
2224
+ # Generate a random address if not set.
2225
+ if self.static_address == Address.ANY_RANDOM:
2226
+ self.static_address = Address.generate_static_address()
2227
+
2228
+ # If LE Privacy is enabled, generate an RPA
2229
+ if self.le_privacy_enabled:
2230
+ self.random_address = Address.generate_private_address(self.irk)
2231
+ logger.info(f'Initial RPA: {self.random_address}')
2232
+ if self.le_rpa_timeout > 0:
2233
+ # Start a task to periodically generate a new RPA
2234
+ self.le_rpa_periodic_update_task = asyncio.create_task(
2235
+ self._run_rpa_periodic_update()
1854
2236
  )
1855
-
1856
- # Ensure the address bytes can be a static random address
1857
- address_bytes = response.return_parameters.random_number[
1858
- :5
1859
- ] + bytes([response.return_parameters.random_number[5] | 0xC0])
1860
-
1861
- # Create a static random address from the random bytes
1862
- self.random_address = Address(address_bytes)
2237
+ else:
2238
+ self.random_address = self.static_address
1863
2239
 
1864
2240
  if self.random_address != Address.ANY_RANDOM:
1865
2241
  logger.debug(
@@ -1884,7 +2260,8 @@ class Device(CompositeEventEmitter):
1884
2260
  await self.send_command(
1885
2261
  HCI_LE_Set_Address_Resolution_Enable_Command(
1886
2262
  address_resolution_enable=1
1887
- )
2263
+ ),
2264
+ check_result=True,
1888
2265
  )
1889
2266
 
1890
2267
  if self.cis_enabled:
@@ -1892,7 +2269,8 @@ class Device(CompositeEventEmitter):
1892
2269
  HCI_LE_Set_Host_Feature_Command(
1893
2270
  bit_number=LeFeature.CONNECTED_ISOCHRONOUS_STREAM,
1894
2271
  bit_value=1,
1895
- )
2272
+ ),
2273
+ check_result=True,
1896
2274
  )
1897
2275
 
1898
2276
  if self.classic_enabled:
@@ -1915,6 +2293,21 @@ class Device(CompositeEventEmitter):
1915
2293
  await self.set_connectable(self.connectable)
1916
2294
  await self.set_discoverable(self.discoverable)
1917
2295
 
2296
+ if self.classic_interlaced_scan_enabled:
2297
+ if self.host.supports_lmp_features(LmpFeatureMask.INTERLACED_PAGE_SCAN):
2298
+ await self.send_command(
2299
+ hci.HCI_Write_Page_Scan_Type_Command(page_scan_type=1),
2300
+ check_result=True,
2301
+ )
2302
+
2303
+ if self.host.supports_lmp_features(
2304
+ LmpFeatureMask.INTERLACED_INQUIRY_SCAN
2305
+ ):
2306
+ await self.send_command(
2307
+ hci.HCI_Write_Inquiry_Scan_Type_Command(scan_type=1),
2308
+ check_result=True,
2309
+ )
2310
+
1918
2311
  # Done
1919
2312
  self.powered_on = True
1920
2313
 
@@ -1923,9 +2316,45 @@ class Device(CompositeEventEmitter):
1923
2316
 
1924
2317
  async def power_off(self) -> None:
1925
2318
  if self.powered_on:
2319
+ if self.le_rpa_periodic_update_task:
2320
+ self.le_rpa_periodic_update_task.cancel()
2321
+
1926
2322
  await self.host.flush()
2323
+
1927
2324
  self.powered_on = False
1928
2325
 
2326
+ async def update_rpa(self) -> bool:
2327
+ """
2328
+ Try to update the RPA.
2329
+
2330
+ Returns:
2331
+ True if the RPA was updated, False if it could not be updated.
2332
+ """
2333
+
2334
+ # Check if this is a good time to rotate the address
2335
+ if self.is_advertising or self.is_scanning or self.is_le_connecting:
2336
+ logger.debug('skipping RPA update')
2337
+ return False
2338
+
2339
+ random_address = Address.generate_private_address(self.irk)
2340
+ response = await self.send_command(
2341
+ HCI_LE_Set_Random_Address_Command(random_address=self.random_address)
2342
+ )
2343
+ if response.return_parameters == HCI_SUCCESS:
2344
+ logger.info(f'new RPA: {random_address}')
2345
+ self.random_address = random_address
2346
+ return True
2347
+ else:
2348
+ logger.warning(f'failed to set RPA: {response.return_parameters}')
2349
+ return False
2350
+
2351
+ async def _run_rpa_periodic_update(self) -> None:
2352
+ """Update the RPA periodically"""
2353
+ while self.le_rpa_timeout != 0:
2354
+ await asyncio.sleep(self.le_rpa_timeout)
2355
+ if not self.update_rpa():
2356
+ logger.debug("periodic RPA update failed")
2357
+
1929
2358
  async def refresh_resolving_list(self) -> None:
1930
2359
  assert self.keystore is not None
1931
2360
 
@@ -1933,7 +2362,7 @@ class Device(CompositeEventEmitter):
1933
2362
  # Create a host-side address resolver
1934
2363
  self.address_resolver = smp.AddressResolver(resolving_keys)
1935
2364
 
1936
- if self.address_resolution_offload:
2365
+ if self.address_resolution_offload or self.address_generation_offload:
1937
2366
  await self.send_command(HCI_LE_Clear_Resolving_List_Command())
1938
2367
 
1939
2368
  # Add an empty entry for non-directed address generation.
@@ -1959,7 +2388,7 @@ class Device(CompositeEventEmitter):
1959
2388
  def supports_le_features(self, feature: LeFeatureMask) -> bool:
1960
2389
  return self.host.supports_le_features(feature)
1961
2390
 
1962
- def supports_le_phy(self, phy):
2391
+ def supports_le_phy(self, phy: int) -> bool:
1963
2392
  if phy == HCI_LE_1M_PHY:
1964
2393
  return True
1965
2394
 
@@ -1968,7 +2397,7 @@ class Device(CompositeEventEmitter):
1968
2397
  HCI_LE_CODED_PHY: LeFeatureMask.LE_CODED_PHY,
1969
2398
  }
1970
2399
  if phy not in feature_map:
1971
- raise ValueError('invalid PHY')
2400
+ raise InvalidArgumentError('invalid PHY')
1972
2401
 
1973
2402
  return self.supports_le_features(feature_map[phy])
1974
2403
 
@@ -1976,6 +2405,10 @@ class Device(CompositeEventEmitter):
1976
2405
  def supports_le_extended_advertising(self):
1977
2406
  return self.supports_le_features(LeFeatureMask.LE_EXTENDED_ADVERTISING)
1978
2407
 
2408
+ @property
2409
+ def supports_le_periodic_advertising(self):
2410
+ return self.supports_le_features(LeFeatureMask.LE_PERIODIC_ADVERTISING)
2411
+
1979
2412
  async def start_advertising(
1980
2413
  self,
1981
2414
  advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
@@ -2028,7 +2461,7 @@ class Device(CompositeEventEmitter):
2028
2461
  # Decide what peer address to use
2029
2462
  if advertising_type.is_directed:
2030
2463
  if target is None:
2031
- raise ValueError('directed advertising requires a target')
2464
+ raise InvalidArgumentError('directed advertising requires a target')
2032
2465
  peer_address = target
2033
2466
  else:
2034
2467
  peer_address = Address.ANY
@@ -2135,7 +2568,7 @@ class Device(CompositeEventEmitter):
2135
2568
  and advertising_data
2136
2569
  and scan_response_data
2137
2570
  ):
2138
- raise ValueError(
2571
+ raise InvalidArgumentError(
2139
2572
  "Extended advertisements can't have both data and scan \
2140
2573
  response data"
2141
2574
  )
@@ -2151,7 +2584,9 @@ class Device(CompositeEventEmitter):
2151
2584
  if handle not in self.extended_advertising_sets
2152
2585
  )
2153
2586
  except StopIteration as exc:
2154
- raise RuntimeError("all valid advertising handles already in use") from exc
2587
+ raise OutOfResourcesError(
2588
+ "all valid advertising handles already in use"
2589
+ ) from exc
2155
2590
 
2156
2591
  # Use the device's random address if a random address is needed but none was
2157
2592
  # provided.
@@ -2250,14 +2685,14 @@ class Device(CompositeEventEmitter):
2250
2685
  ) -> None:
2251
2686
  # Check that the arguments are legal
2252
2687
  if scan_interval < scan_window:
2253
- raise ValueError('scan_interval must be >= scan_window')
2688
+ raise InvalidArgumentError('scan_interval must be >= scan_window')
2254
2689
  if (
2255
2690
  scan_interval < DEVICE_MIN_SCAN_INTERVAL
2256
2691
  or scan_interval > DEVICE_MAX_SCAN_INTERVAL
2257
2692
  ):
2258
- raise ValueError('scan_interval out of range')
2693
+ raise InvalidArgumentError('scan_interval out of range')
2259
2694
  if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW:
2260
- raise ValueError('scan_interval out of range')
2695
+ raise InvalidArgumentError('scan_interval out of range')
2261
2696
 
2262
2697
  # Reset the accumulators
2263
2698
  self.advertisement_accumulators = {}
@@ -2285,7 +2720,7 @@ class Device(CompositeEventEmitter):
2285
2720
  scanning_phy_count += 1
2286
2721
 
2287
2722
  if scanning_phy_count == 0:
2288
- raise ValueError('at least one scanning PHY must be enabled')
2723
+ raise InvalidArgumentError('at least one scanning PHY must be enabled')
2289
2724
 
2290
2725
  await self.send_command(
2291
2726
  HCI_LE_Set_Extended_Scan_Parameters_Command(
@@ -2368,6 +2803,120 @@ class Device(CompositeEventEmitter):
2368
2803
  if advertisement := accumulator.update(report):
2369
2804
  self.emit('advertisement', advertisement)
2370
2805
 
2806
+ async def create_periodic_advertising_sync(
2807
+ self,
2808
+ advertiser_address: Address,
2809
+ sid: int,
2810
+ skip: int = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP,
2811
+ sync_timeout: float = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT,
2812
+ filter_duplicates: bool = False,
2813
+ ) -> PeriodicAdvertisingSync:
2814
+ # Check that the controller supports the feature.
2815
+ if not self.supports_le_periodic_advertising:
2816
+ raise NotSupportedError()
2817
+
2818
+ # Check that there isn't already an equivalent entry
2819
+ if any(
2820
+ sync.advertiser_address == advertiser_address and sync.sid == sid
2821
+ for sync in self.periodic_advertising_syncs
2822
+ ):
2823
+ raise ValueError("equivalent entry already created")
2824
+
2825
+ # Create a new entry
2826
+ sync = PeriodicAdvertisingSync(
2827
+ device=self,
2828
+ advertiser_address=advertiser_address,
2829
+ sid=sid,
2830
+ skip=skip,
2831
+ sync_timeout=sync_timeout,
2832
+ filter_duplicates=filter_duplicates,
2833
+ )
2834
+
2835
+ self.periodic_advertising_syncs.append(sync)
2836
+
2837
+ # Check if any sync should be started
2838
+ await self._update_periodic_advertising_syncs()
2839
+
2840
+ return sync
2841
+
2842
+ async def _update_periodic_advertising_syncs(self) -> None:
2843
+ # Check if there's already a pending sync
2844
+ if any(
2845
+ sync.state == PeriodicAdvertisingSync.State.PENDING
2846
+ for sync in self.periodic_advertising_syncs
2847
+ ):
2848
+ logger.debug("at least one sync pending, nothing to update yet")
2849
+ return
2850
+
2851
+ # Start the next sync that's waiting to be started
2852
+ if ready := next(
2853
+ (
2854
+ sync
2855
+ for sync in self.periodic_advertising_syncs
2856
+ if sync.state == PeriodicAdvertisingSync.State.INIT
2857
+ ),
2858
+ None,
2859
+ ):
2860
+ await ready.establish()
2861
+ return
2862
+
2863
+ @host_event_handler
2864
+ def on_periodic_advertising_sync_establishment(
2865
+ self,
2866
+ status: int,
2867
+ sync_handle: int,
2868
+ advertising_sid: int,
2869
+ advertiser_address: Address,
2870
+ advertiser_phy: int,
2871
+ periodic_advertising_interval: int,
2872
+ advertiser_clock_accuracy: int,
2873
+ ) -> None:
2874
+ for periodic_advertising_sync in self.periodic_advertising_syncs:
2875
+ if (
2876
+ periodic_advertising_sync.advertiser_address == advertiser_address
2877
+ and periodic_advertising_sync.sid == advertising_sid
2878
+ ):
2879
+ periodic_advertising_sync.on_establishment(
2880
+ status,
2881
+ sync_handle,
2882
+ advertiser_phy,
2883
+ periodic_advertising_interval,
2884
+ advertiser_clock_accuracy,
2885
+ )
2886
+
2887
+ AsyncRunner.spawn(self._update_periodic_advertising_syncs())
2888
+
2889
+ return
2890
+
2891
+ logger.warning(
2892
+ "periodic advertising sync establishment for unknown address/sid"
2893
+ )
2894
+
2895
+ @host_event_handler
2896
+ @with_periodic_advertising_sync_from_handle
2897
+ def on_periodic_advertising_sync_loss(
2898
+ self, periodic_advertising_sync: PeriodicAdvertisingSync
2899
+ ):
2900
+ periodic_advertising_sync.on_loss()
2901
+
2902
+ @host_event_handler
2903
+ @with_periodic_advertising_sync_from_handle
2904
+ def on_periodic_advertising_report(
2905
+ self,
2906
+ periodic_advertising_sync: PeriodicAdvertisingSync,
2907
+ report: HCI_LE_Periodic_Advertising_Report_Event,
2908
+ ):
2909
+ periodic_advertising_sync.on_periodic_advertising_report(report)
2910
+
2911
+ @host_event_handler
2912
+ @with_periodic_advertising_sync_from_handle
2913
+ def on_biginfo_advertising_report(
2914
+ self,
2915
+ periodic_advertising_sync: PeriodicAdvertisingSync,
2916
+ report: HCI_LE_BIGInfo_Advertising_Report_Event,
2917
+ ):
2918
+ periodic_advertising_sync.on_biginfo_advertising_report(report)
2919
+
2371
2920
  async def start_discovery(self, auto_restart: bool = True) -> None:
2372
2921
  await self.send_command(
2373
2922
  HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE),
@@ -2463,23 +3012,52 @@ class Device(CompositeEventEmitter):
2463
3012
  ] = None,
2464
3013
  own_address_type: int = OwnAddressType.RANDOM,
2465
3014
  timeout: Optional[float] = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3015
+ always_resolve: bool = False,
2466
3016
  ) -> Connection:
2467
3017
  '''
2468
3018
  Request a connection to a peer.
2469
- When transport is BLE, this method cannot be called if there is already a
3019
+
3020
+ When the transport is BLE, this method cannot be called if there is already a
2470
3021
  pending connection.
2471
3022
 
2472
- connection_parameters_preferences: (BLE only, ignored for BR/EDR)
2473
- * None: use the 1M PHY with default parameters
2474
- * map: each entry has a PHY as key and a ConnectionParametersPreferences
2475
- object as value
3023
+ Args:
3024
+ peer_address:
3025
+ Address or name of the device to connect to.
3026
+ If a string is passed:
3027
+ If the string is an address followed by a `@` suffix, the `always_resolve`
3028
+ argument is implicitly set to True, so the connection is made to the
3029
+ address after resolution.
3030
+ If the string is any other address, the connection is made to that
3031
+ address (with or without address resolution, depending on the
3032
+ `always_resolve` argument).
3033
+ For any other string, a scan for devices using that string as their name
3034
+ is initiated, and a connection to the first matching device's address
3035
+ is made. In that case, `always_resolve` is ignored.
3036
+
3037
+ connection_parameters_preferences:
3038
+ (BLE only, ignored for BR/EDR)
3039
+ * None: use the 1M PHY with default parameters
3040
+ * map: each entry has a PHY as key and a ConnectionParametersPreferences
3041
+ object as value
2476
3042
 
2477
- own_address_type: (BLE only)
3043
+ own_address_type:
3044
+ (BLE only, ignored for BR/EDR)
3045
+ OwnAddressType.RANDOM to use this device's random address, or
3046
+ OwnAddressType.PUBLIC to use this device's public address.
3047
+
3048
+ timeout:
3049
+ Maximum time to wait for a connection to be established, in seconds.
3050
+ Pass None for an unlimited time.
3051
+
3052
+ always_resolve:
3053
+ (BLE only, ignored for BR/EDR)
3054
+ If True, always initiate a scan, resolving addresses, and connect to the
3055
+ address that resolves to `peer_address`.
2478
3056
  '''
2479
3057
 
2480
3058
  # Check parameters
2481
3059
  if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
2482
- raise ValueError('invalid transport')
3060
+ raise InvalidArgumentError('invalid transport')
2483
3061
 
2484
3062
  # Adjust the transport automatically if we need to
2485
3063
  if transport == BT_LE_TRANSPORT and not self.le_enabled:
@@ -2493,11 +3071,19 @@ class Device(CompositeEventEmitter):
2493
3071
 
2494
3072
  if isinstance(peer_address, str):
2495
3073
  try:
2496
- peer_address = Address.from_string_for_transport(
2497
- peer_address, transport
2498
- )
2499
- except ValueError:
3074
+ if transport == BT_LE_TRANSPORT and peer_address.endswith('@'):
3075
+ peer_address = Address.from_string_for_transport(
3076
+ peer_address[:-1], transport
3077
+ )
3078
+ always_resolve = True
3079
+ logger.debug('forcing address resolution')
3080
+ else:
3081
+ peer_address = Address.from_string_for_transport(
3082
+ peer_address, transport
3083
+ )
3084
+ except (InvalidArgumentError, ValueError):
2500
3085
  # If the address is not parsable, assume it is a name instead
3086
+ always_resolve = False
2501
3087
  logger.debug('looking for peer by name')
2502
3088
  peer_address = await self.find_peer_by_name(
2503
3089
  peer_address, transport
@@ -2508,10 +3094,16 @@ class Device(CompositeEventEmitter):
2508
3094
  transport == BT_BR_EDR_TRANSPORT
2509
3095
  and peer_address.address_type != Address.PUBLIC_DEVICE_ADDRESS
2510
3096
  ):
2511
- raise ValueError('BR/EDR addresses must be PUBLIC')
3097
+ raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
2512
3098
 
2513
3099
  assert isinstance(peer_address, Address)
2514
3100
 
3101
+ if transport == BT_LE_TRANSPORT and always_resolve:
3102
+ logger.debug('resolving address')
3103
+ peer_address = await self.find_peer_by_identity_address(
3104
+ peer_address
3105
+ ) # TODO: timeout
3106
+
2515
3107
  def on_connection(connection):
2516
3108
  if transport == BT_LE_TRANSPORT or (
2517
3109
  # match BR/EDR connection event against peer address
@@ -2559,7 +3151,7 @@ class Device(CompositeEventEmitter):
2559
3151
  )
2560
3152
  )
2561
3153
  if not phys:
2562
- raise ValueError('at least one supported PHY needed')
3154
+ raise InvalidArgumentError('at least one supported PHY needed')
2563
3155
 
2564
3156
  phy_count = len(phys)
2565
3157
  initiating_phys = phy_list_to_bits(phys)
@@ -2631,7 +3223,7 @@ class Device(CompositeEventEmitter):
2631
3223
  )
2632
3224
  else:
2633
3225
  if HCI_LE_1M_PHY not in connection_parameters_preferences:
2634
- raise ValueError('1M PHY preferences required')
3226
+ raise InvalidArgumentError('1M PHY preferences required')
2635
3227
 
2636
3228
  prefs = connection_parameters_preferences[HCI_LE_1M_PHY]
2637
3229
  result = await self.send_command(
@@ -2731,7 +3323,7 @@ class Device(CompositeEventEmitter):
2731
3323
  if isinstance(peer_address, str):
2732
3324
  try:
2733
3325
  peer_address = Address(peer_address)
2734
- except ValueError:
3326
+ except InvalidArgumentError:
2735
3327
  # If the address is not parsable, assume it is a name instead
2736
3328
  logger.debug('looking for peer by name')
2737
3329
  peer_address = await self.find_peer_by_name(
@@ -2741,7 +3333,7 @@ class Device(CompositeEventEmitter):
2741
3333
  assert isinstance(peer_address, Address)
2742
3334
 
2743
3335
  if peer_address == Address.NIL:
2744
- raise ValueError('accept on nil address')
3336
+ raise InvalidArgumentError('accept on nil address')
2745
3337
 
2746
3338
  # Create a future so that we can wait for the request
2747
3339
  pending_request_fut = asyncio.get_running_loop().create_future()
@@ -2854,7 +3446,7 @@ class Device(CompositeEventEmitter):
2854
3446
  if isinstance(peer_address, str):
2855
3447
  try:
2856
3448
  peer_address = Address(peer_address)
2857
- except ValueError:
3449
+ except InvalidArgumentError:
2858
3450
  # If the address is not parsable, assume it is a name instead
2859
3451
  logger.debug('looking for peer by name')
2860
3452
  peer_address = await self.find_peer_by_name(
@@ -2897,10 +3489,10 @@ class Device(CompositeEventEmitter):
2897
3489
 
2898
3490
  async def set_data_length(self, connection, tx_octets, tx_time) -> None:
2899
3491
  if tx_octets < 0x001B or tx_octets > 0x00FB:
2900
- raise ValueError('tx_octets must be between 0x001B and 0x00FB')
3492
+ raise InvalidArgumentError('tx_octets must be between 0x001B and 0x00FB')
2901
3493
 
2902
3494
  if tx_time < 0x0148 or tx_time > 0x4290:
2903
- raise ValueError('tx_time must be between 0x0148 and 0x4290')
3495
+ raise InvalidArgumentError('tx_time must be between 0x0148 and 0x4290')
2904
3496
 
2905
3497
  return await self.send_command(
2906
3498
  HCI_LE_Set_Data_Length_Command(
@@ -3013,15 +3605,26 @@ class Device(CompositeEventEmitter):
3013
3605
  check_result=True,
3014
3606
  )
3015
3607
 
3608
+ async def transfer_periodic_sync(
3609
+ self, connection: Connection, sync_handle: int, service_data: int = 0
3610
+ ) -> None:
3611
+ return await self.send_command(
3612
+ HCI_LE_Periodic_Advertising_Sync_Transfer_Command(
3613
+ connection_handle=connection.handle,
3614
+ service_data=service_data,
3615
+ sync_handle=sync_handle,
3616
+ ),
3617
+ check_result=True,
3618
+ )
3619
+
3016
3620
  async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT):
3017
3621
  """
3018
- Scan for a peer with a give name and return its address and transport
3622
+ Scan for a peer with a given name and return its address.
3019
3623
  """
3020
3624
 
3021
3625
  # Create a future to wait for an address to be found
3022
3626
  peer_address = asyncio.get_running_loop().create_future()
3023
3627
 
3024
- # Scan/inquire with event handlers to handle scan/inquiry results
3025
3628
  def on_peer_found(address, ad_data):
3026
3629
  local_name = ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True)
3027
3630
  if local_name is None:
@@ -3030,13 +3633,13 @@ class Device(CompositeEventEmitter):
3030
3633
  if local_name.decode('utf-8') == name:
3031
3634
  peer_address.set_result(address)
3032
3635
 
3033
- handler = None
3636
+ listener = None
3034
3637
  was_scanning = self.scanning
3035
3638
  was_discovering = self.discovering
3036
3639
  try:
3037
3640
  if transport == BT_LE_TRANSPORT:
3038
3641
  event_name = 'advertisement'
3039
- handler = self.on(
3642
+ listener = self.on(
3040
3643
  event_name,
3041
3644
  lambda advertisement: on_peer_found(
3042
3645
  advertisement.address, advertisement.data
@@ -3048,7 +3651,7 @@ class Device(CompositeEventEmitter):
3048
3651
 
3049
3652
  elif transport == BT_BR_EDR_TRANSPORT:
3050
3653
  event_name = 'inquiry_result'
3051
- handler = self.on(
3654
+ listener = self.on(
3052
3655
  event_name,
3053
3656
  lambda address, class_of_device, eir_data, rssi: on_peer_found(
3054
3657
  address, eir_data
@@ -3062,21 +3665,67 @@ class Device(CompositeEventEmitter):
3062
3665
 
3063
3666
  return await self.abort_on('flush', peer_address)
3064
3667
  finally:
3065
- if handler is not None:
3066
- self.remove_listener(event_name, handler)
3668
+ if listener is not None:
3669
+ self.remove_listener(event_name, listener)
3067
3670
 
3068
3671
  if transport == BT_LE_TRANSPORT and not was_scanning:
3069
3672
  await self.stop_scanning()
3070
3673
  elif transport == BT_BR_EDR_TRANSPORT and not was_discovering:
3071
3674
  await self.stop_discovery()
3072
3675
 
3676
+ async def find_peer_by_identity_address(self, identity_address: Address) -> Address:
3677
+ """
3678
+ Scan for a peer with a resolvable address that can be resolved to a given
3679
+ identity address.
3680
+ """
3681
+
3682
+ # Create a future to wait for an address to be found
3683
+ peer_address = asyncio.get_running_loop().create_future()
3684
+
3685
+ def on_peer_found(address, _):
3686
+ if address == identity_address:
3687
+ if not peer_address.done():
3688
+ logger.debug(f'*** Matching public address found for {address}')
3689
+ peer_address.set_result(address)
3690
+ return
3691
+
3692
+ if address.is_resolvable:
3693
+ resolved_address = self.address_resolver.resolve(address)
3694
+ if resolved_address == identity_address:
3695
+ if not peer_address.done():
3696
+ logger.debug(f'*** Matching identity found for {address}')
3697
+ peer_address.set_result(address)
3698
+ return
3699
+
3700
+ was_scanning = self.scanning
3701
+ event_name = 'advertisement'
3702
+ listener = None
3703
+ try:
3704
+ listener = self.on(
3705
+ event_name,
3706
+ lambda advertisement: on_peer_found(
3707
+ advertisement.address, advertisement.data
3708
+ ),
3709
+ )
3710
+
3711
+ if not self.scanning:
3712
+ await self.start_scanning(filter_duplicates=True)
3713
+
3714
+ return await self.abort_on('flush', peer_address)
3715
+ finally:
3716
+ if listener is not None:
3717
+ self.remove_listener(event_name, listener)
3718
+
3719
+ if not was_scanning:
3720
+ await self.stop_scanning()
3721
+
3073
3722
  @property
3074
- def pairing_config_factory(self) -> Callable[[Connection], PairingConfig]:
3723
+ def pairing_config_factory(self) -> Callable[[Connection], pairing.PairingConfig]:
3075
3724
  return self.smp_manager.pairing_config_factory
3076
3725
 
3077
3726
  @pairing_config_factory.setter
3078
3727
  def pairing_config_factory(
3079
- self, pairing_config_factory: Callable[[Connection], PairingConfig]
3728
+ self, pairing_config_factory: Callable[[Connection], pairing.PairingConfig]
3080
3729
  ) -> None:
3081
3730
  self.smp_manager.pairing_config_factory = pairing_config_factory
3082
3731
 
@@ -3175,7 +3824,7 @@ class Device(CompositeEventEmitter):
3175
3824
 
3176
3825
  async def encrypt(self, connection, enable=True):
3177
3826
  if not enable and connection.transport == BT_LE_TRANSPORT:
3178
- raise ValueError('`enable` parameter is classic only.')
3827
+ raise InvalidArgumentError('`enable` parameter is classic only.')
3179
3828
 
3180
3829
  # Set up event handlers
3181
3830
  pending_encryption = asyncio.get_running_loop().create_future()
@@ -3194,11 +3843,12 @@ class Device(CompositeEventEmitter):
3194
3843
  if connection.transport == BT_LE_TRANSPORT:
3195
3844
  # Look for a key in the key store
3196
3845
  if self.keystore is None:
3197
- raise RuntimeError('no key store')
3846
+ raise InvalidOperationError('no key store')
3198
3847
 
3848
+ logger.debug(f'Looking up key for {connection.peer_address}')
3199
3849
  keys = await self.keystore.get(str(connection.peer_address))
3200
3850
  if keys is None:
3201
- raise RuntimeError('keys not found in key store')
3851
+ raise InvalidOperationError('keys not found in key store')
3202
3852
 
3203
3853
  if keys.ltk is not None:
3204
3854
  ltk = keys.ltk.value
@@ -3209,7 +3859,7 @@ class Device(CompositeEventEmitter):
3209
3859
  rand = keys.ltk_central.rand
3210
3860
  ediv = keys.ltk_central.ediv
3211
3861
  else:
3212
- raise RuntimeError('no LTK found for peer')
3862
+ raise InvalidOperationError('no LTK found for peer')
3213
3863
 
3214
3864
  if connection.role != HCI_CENTRAL_ROLE:
3215
3865
  raise InvalidStateError('only centrals can start encryption')
@@ -3484,7 +4134,7 @@ class Device(CompositeEventEmitter):
3484
4134
  return cis_link
3485
4135
 
3486
4136
  # Mypy believes this is reachable when context is an ExitStack.
3487
- raise InvalidStateError('Unreachable')
4137
+ raise UnreachableError()
3488
4138
 
3489
4139
  # [LE only]
3490
4140
  @experimental('Only for testing.')
@@ -3605,18 +4255,38 @@ class Device(CompositeEventEmitter):
3605
4255
  )
3606
4256
  return
3607
4257
 
3608
- if not (connection := self.lookup_connection(connection_handle)):
3609
- logger.warning(f'no connection for handle 0x{connection_handle:04x}')
4258
+ if connection := self.lookup_connection(connection_handle):
4259
+ # We have already received the connection complete event.
4260
+ self._complete_le_extended_advertising_connection(
4261
+ connection, advertising_set
4262
+ )
3610
4263
  return
3611
4264
 
4265
+ # Associate the connection handle with the advertising set, the connection
4266
+ # will complete later.
4267
+ logger.debug(
4268
+ f'the connection with handle {connection_handle:04X} will complete later'
4269
+ )
4270
+ self.connecting_extended_advertising_sets[connection_handle] = advertising_set
4271
+
4272
+ def _complete_le_extended_advertising_connection(
4273
+ self, connection: Connection, advertising_set: AdvertisingSet
4274
+ ) -> None:
3612
4275
  # Update the connection address.
3613
4276
  connection.self_address = (
3614
4277
  advertising_set.random_address
3615
- if advertising_set.advertising_parameters.own_address_type
4278
+ if advertising_set.random_address is not None
4279
+ and advertising_set.advertising_parameters.own_address_type
3616
4280
  in (OwnAddressType.RANDOM, OwnAddressType.RESOLVABLE_OR_RANDOM)
3617
4281
  else self.public_address
3618
4282
  )
3619
4283
 
4284
+ if advertising_set.advertising_parameters.own_address_type in (
4285
+ OwnAddressType.RANDOM,
4286
+ OwnAddressType.PUBLIC,
4287
+ ):
4288
+ connection.self_resolvable_address = None
4289
+
3620
4290
  # Setup auto-restart of the advertising set if needed.
3621
4291
  if advertising_set.auto_restart:
3622
4292
  connection.once(
@@ -3652,12 +4322,23 @@ class Device(CompositeEventEmitter):
3652
4322
  @host_event_handler
3653
4323
  def on_connection(
3654
4324
  self,
3655
- connection_handle,
3656
- transport,
3657
- peer_address,
3658
- role,
3659
- connection_parameters,
3660
- ):
4325
+ connection_handle: int,
4326
+ transport: int,
4327
+ peer_address: Address,
4328
+ self_resolvable_address: Optional[Address],
4329
+ peer_resolvable_address: Optional[Address],
4330
+ role: int,
4331
+ connection_parameters: ConnectionParameters,
4332
+ ) -> None:
4333
+ # Convert all-zeros addresses into None.
4334
+ if self_resolvable_address == Address.ANY_RANDOM:
4335
+ self_resolvable_address = None
4336
+ if (
4337
+ peer_resolvable_address == Address.ANY_RANDOM
4338
+ or not peer_address.is_resolved
4339
+ ):
4340
+ peer_resolvable_address = None
4341
+
3661
4342
  logger.debug(
3662
4343
  f'*** Connection: [0x{connection_handle:04X}] '
3663
4344
  f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}'
@@ -3678,17 +4359,18 @@ class Device(CompositeEventEmitter):
3678
4359
 
3679
4360
  return
3680
4361
 
3681
- # Resolve the peer address if we can
3682
- peer_resolvable_address = None
3683
- if self.address_resolver:
3684
- if peer_address.is_resolvable:
3685
- resolved_address = self.address_resolver.resolve(peer_address)
3686
- if resolved_address is not None:
3687
- logger.debug(f'*** Address resolved as {resolved_address}')
3688
- peer_resolvable_address = peer_address
3689
- peer_address = resolved_address
4362
+ if peer_resolvable_address is None:
4363
+ # Resolve the peer address if we can
4364
+ if self.address_resolver:
4365
+ if peer_address.is_resolvable:
4366
+ resolved_address = self.address_resolver.resolve(peer_address)
4367
+ if resolved_address is not None:
4368
+ logger.debug(f'*** Address resolved as {resolved_address}')
4369
+ peer_resolvable_address = peer_address
4370
+ peer_address = resolved_address
3690
4371
 
3691
4372
  self_address = None
4373
+ own_address_type: Optional[int] = None
3692
4374
  if role == HCI_CENTRAL_ROLE:
3693
4375
  own_address_type = self.connect_own_address_type
3694
4376
  assert own_address_type is not None
@@ -3717,12 +4399,18 @@ class Device(CompositeEventEmitter):
3717
4399
  else self.random_address
3718
4400
  )
3719
4401
 
4402
+ # Some controllers may return local resolvable address even not using address
4403
+ # generation offloading. Ignore the value to prevent SMP failure.
4404
+ if own_address_type in (OwnAddressType.RANDOM, OwnAddressType.PUBLIC):
4405
+ self_resolvable_address = None
4406
+
3720
4407
  # Create a connection.
3721
4408
  connection = Connection(
3722
4409
  self,
3723
4410
  connection_handle,
3724
4411
  transport,
3725
4412
  self_address,
4413
+ self_resolvable_address,
3726
4414
  peer_address,
3727
4415
  peer_resolvable_address,
3728
4416
  role,
@@ -3733,9 +4421,10 @@ class Device(CompositeEventEmitter):
3733
4421
 
3734
4422
  if role == HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
3735
4423
  if self.legacy_advertiser.auto_restart:
4424
+ advertiser = self.legacy_advertiser
3736
4425
  connection.once(
3737
4426
  'disconnection',
3738
- lambda _: self.abort_on('flush', self.legacy_advertiser.start()),
4427
+ lambda _: self.abort_on('flush', advertiser.start()),
3739
4428
  )
3740
4429
  else:
3741
4430
  self.legacy_advertiser = None
@@ -3743,6 +4432,16 @@ class Device(CompositeEventEmitter):
3743
4432
  if role == HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
3744
4433
  # We can emit now, we have all the info we need
3745
4434
  self._emit_le_connection(connection)
4435
+ return
4436
+
4437
+ if role == HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
4438
+ if advertising_set := self.connecting_extended_advertising_sets.pop(
4439
+ connection_handle, None
4440
+ ):
4441
+ # We have already received the advertising set termination event.
4442
+ self._complete_le_extended_advertising_connection(
4443
+ connection, advertising_set
4444
+ )
3746
4445
 
3747
4446
  @host_event_handler
3748
4447
  def on_connection_failure(self, transport, peer_address, error_code):
@@ -3948,7 +4647,7 @@ class Device(CompositeEventEmitter):
3948
4647
  return await pairing_config.delegate.confirm(auto=True)
3949
4648
 
3950
4649
  async def na() -> bool:
3951
- assert False, "N/A: unreachable"
4650
+ raise UnreachableError()
3952
4651
 
3953
4652
  # See Bluetooth spec @ Vol 3, Part C 5.2.2.6
3954
4653
  methods = {
@@ -4409,5 +5108,6 @@ class Device(CompositeEventEmitter):
4409
5108
  return (
4410
5109
  f'Device(name="{self.name}", '
4411
5110
  f'random_address="{self.random_address}", '
4412
- f'public_address="{self.public_address}")'
5111
+ f'public_address="{self.public_address}", '
5112
+ f'static_address="{self.static_address}")'
4413
5113
  )