bumble 0.0.195__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 (50) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/at.py +12 -6
  8. bumble/avc.py +8 -5
  9. bumble/avctp.py +3 -2
  10. bumble/avdtp.py +5 -1
  11. bumble/avrcp.py +2 -1
  12. bumble/codecs.py +17 -13
  13. bumble/colors.py +6 -2
  14. bumble/core.py +37 -7
  15. bumble/device.py +382 -111
  16. bumble/drivers/rtk.py +13 -8
  17. bumble/gatt.py +6 -1
  18. bumble/gatt_client.py +10 -4
  19. bumble/hci.py +50 -25
  20. bumble/hid.py +24 -28
  21. bumble/host.py +4 -0
  22. bumble/l2cap.py +24 -17
  23. bumble/link.py +8 -3
  24. bumble/profiles/ascs.py +739 -0
  25. bumble/profiles/bap.py +1 -874
  26. bumble/profiles/bass.py +440 -0
  27. bumble/profiles/csip.py +4 -4
  28. bumble/profiles/gap.py +110 -0
  29. bumble/profiles/heart_rate_service.py +4 -3
  30. bumble/profiles/le_audio.py +43 -9
  31. bumble/profiles/mcp.py +448 -0
  32. bumble/profiles/pacs.py +210 -0
  33. bumble/profiles/tmap.py +89 -0
  34. bumble/rfcomm.py +4 -2
  35. bumble/sdp.py +13 -11
  36. bumble/smp.py +20 -8
  37. bumble/snoop.py +5 -4
  38. bumble/transport/__init__.py +8 -2
  39. bumble/transport/android_emulator.py +9 -3
  40. bumble/transport/android_netsim.py +9 -7
  41. bumble/transport/common.py +46 -18
  42. bumble/transport/pyusb.py +2 -2
  43. bumble/transport/unix.py +56 -0
  44. bumble/transport/usb.py +57 -46
  45. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  46. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
  47. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  49. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  50. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -27,6 +27,7 @@ import copy
27
27
  from dataclasses import dataclass, field
28
28
  from enum import Enum, IntEnum
29
29
  import functools
30
+ import itertools
30
31
  import json
31
32
  import logging
32
33
  import secrets
@@ -50,6 +51,7 @@ from typing_extensions import Self
50
51
 
51
52
  from pyee import EventEmitter
52
53
 
54
+ from bumble import hci
53
55
  from .colors import color
54
56
  from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
55
57
  from .gatt import Characteristic, Descriptor, Service
@@ -111,6 +113,7 @@ from .hci import (
111
113
  HCI_LE_Periodic_Advertising_Create_Sync_Command,
112
114
  HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command,
113
115
  HCI_LE_Periodic_Advertising_Report_Event,
116
+ HCI_LE_Periodic_Advertising_Sync_Transfer_Command,
114
117
  HCI_LE_Periodic_Advertising_Terminate_Sync_Command,
115
118
  HCI_LE_Enable_Encryption_Command,
116
119
  HCI_LE_Extended_Advertising_Report_Event,
@@ -167,21 +170,29 @@ from .hci import (
167
170
  OwnAddressType,
168
171
  LeFeature,
169
172
  LeFeatureMask,
173
+ LmpFeatureMask,
170
174
  Phy,
171
175
  phy_list_to_bits,
172
176
  )
173
177
  from .host import Host
174
- from .gap import GenericAccessService
178
+ from .profiles.gap import GenericAccessService
175
179
  from .core import (
176
180
  BT_BR_EDR_TRANSPORT,
177
181
  BT_CENTRAL_ROLE,
178
182
  BT_LE_TRANSPORT,
179
183
  BT_PERIPHERAL_ROLE,
180
184
  AdvertisingData,
185
+ BaseBumbleError,
181
186
  ConnectionParameterUpdateError,
182
187
  CommandTimeoutError,
188
+ ConnectionParameters,
183
189
  ConnectionPHY,
190
+ InvalidArgumentError,
191
+ InvalidOperationError,
184
192
  InvalidStateError,
193
+ NotSupportedError,
194
+ OutOfResourcesError,
195
+ UnreachableError,
185
196
  )
186
197
  from .utils import (
187
198
  AsyncRunner,
@@ -196,13 +207,13 @@ from .keys import (
196
207
  KeyStore,
197
208
  PairingKeys,
198
209
  )
199
- from .pairing import PairingConfig
200
- from . import gatt_client
201
- from . import gatt_server
202
- from . import smp
203
- from . import sdp
204
- from . import l2cap
205
- 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
206
217
 
207
218
  if TYPE_CHECKING:
208
219
  from .transport.common import TransportSource, TransportSink
@@ -253,8 +264,9 @@ DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
253
264
  DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
254
265
  HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE
255
266
  )
256
- DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
267
+ DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
257
268
  DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
269
+ DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
258
270
 
259
271
  # fmt: on
260
272
  # pylint: enable=line-too-long
@@ -266,6 +278,8 @@ DEVICE_MAX_HIGH_DUTY_CYCLE_CONNECTABLE_DIRECTED_ADVERTISING_DURATION = 1.28
266
278
  # -----------------------------------------------------------------------------
267
279
  # Classes
268
280
  # -----------------------------------------------------------------------------
281
+ class ObjectLookupError(BaseBumbleError):
282
+ """Error raised when failed to lookup an object."""
269
283
 
270
284
 
271
285
  # -----------------------------------------------------------------------------
@@ -958,20 +972,25 @@ class PeriodicAdvertisingSync(EventEmitter):
958
972
  response = await self.device.send_command(
959
973
  HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command(),
960
974
  )
961
- if response.status == HCI_SUCCESS:
975
+ if response.return_parameters == HCI_SUCCESS:
962
976
  if self in self.device.periodic_advertising_syncs:
963
977
  self.device.periodic_advertising_syncs.remove(self)
964
978
  return
965
979
 
966
980
  if self.state in (self.State.ESTABLISHED, self.State.ERROR, self.State.LOST):
967
981
  self.state = self.State.TERMINATED
968
- await self.device.send_command(
969
- HCI_LE_Periodic_Advertising_Terminate_Sync_Command(
970
- sync_handle=self.sync_handle
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
+ )
971
987
  )
972
- )
973
988
  self.device.periodic_advertising_syncs.remove(self)
974
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
+
975
994
  def on_establishment(
976
995
  self,
977
996
  status,
@@ -1133,6 +1152,15 @@ class Peer:
1133
1152
  async def discover_attributes(self) -> List[gatt_client.AttributeProxy]:
1134
1153
  return await self.gatt_client.discover_attributes()
1135
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
+
1136
1164
  async def subscribe(
1137
1165
  self,
1138
1166
  characteristic: gatt_client.CharacteristicProxy,
@@ -1172,12 +1200,29 @@ class Peer:
1172
1200
  return self.gatt_client.get_services_by_uuid(uuid)
1173
1201
 
1174
1202
  def get_characteristics_by_uuid(
1175
- 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,
1176
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
+
1177
1217
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
1178
1218
 
1179
- def create_service_proxy(self, proxy_class: Type[_PROXY_CLASS]) -> _PROXY_CLASS:
1180
- 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
1181
1226
 
1182
1227
  async def discover_service_and_create_proxy(
1183
1228
  self, proxy_class: Type[_PROXY_CLASS]
@@ -1274,6 +1319,7 @@ class Connection(CompositeEventEmitter):
1274
1319
  handle: int
1275
1320
  transport: int
1276
1321
  self_address: Address
1322
+ self_resolvable_address: Optional[Address]
1277
1323
  peer_address: Address
1278
1324
  peer_resolvable_address: Optional[Address]
1279
1325
  peer_le_features: Optional[LeFeatureMask]
@@ -1321,6 +1367,7 @@ class Connection(CompositeEventEmitter):
1321
1367
  handle,
1322
1368
  transport,
1323
1369
  self_address,
1370
+ self_resolvable_address,
1324
1371
  peer_address,
1325
1372
  peer_resolvable_address,
1326
1373
  role,
@@ -1332,6 +1379,7 @@ class Connection(CompositeEventEmitter):
1332
1379
  self.handle = handle
1333
1380
  self.transport = transport
1334
1381
  self.self_address = self_address
1382
+ self.self_resolvable_address = self_resolvable_address
1335
1383
  self.peer_address = peer_address
1336
1384
  self.peer_resolvable_address = peer_resolvable_address
1337
1385
  self.peer_name = None # Classic only
@@ -1365,6 +1413,7 @@ class Connection(CompositeEventEmitter):
1365
1413
  None,
1366
1414
  BT_BR_EDR_TRANSPORT,
1367
1415
  device.public_address,
1416
+ None,
1368
1417
  peer_address,
1369
1418
  None,
1370
1419
  role,
@@ -1458,11 +1507,9 @@ class Connection(CompositeEventEmitter):
1458
1507
 
1459
1508
  try:
1460
1509
  await asyncio.wait_for(self.device.abort_on('flush', abort), timeout)
1461
- except asyncio.TimeoutError:
1462
- pass
1463
-
1464
- self.remove_listener('disconnection', abort.set_result)
1465
- 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)
1466
1513
 
1467
1514
  async def set_data_length(self, tx_octets, tx_time) -> None:
1468
1515
  return await self.device.set_data_length(self, tx_octets, tx_time)
@@ -1493,6 +1540,11 @@ class Connection(CompositeEventEmitter):
1493
1540
  async def get_phy(self):
1494
1541
  return await self.device.get_connection_phy(self)
1495
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
+
1496
1548
  # [Classic only]
1497
1549
  async def request_remote_name(self):
1498
1550
  return await self.device.request_remote_name(self)
@@ -1523,7 +1575,9 @@ class Connection(CompositeEventEmitter):
1523
1575
  f'Connection(handle=0x{self.handle:04X}, '
1524
1576
  f'role={self.role_name}, '
1525
1577
  f'self_address={self.self_address}, '
1526
- 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})'
1527
1581
  )
1528
1582
 
1529
1583
 
@@ -1538,13 +1592,15 @@ class DeviceConfiguration:
1538
1592
  advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1539
1593
  advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1540
1594
  le_enabled: bool = True
1541
- # LE host enable 2nd parameter
1542
1595
  le_simultaneous_enabled: bool = False
1596
+ le_privacy_enabled: bool = False
1597
+ le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
1543
1598
  classic_enabled: bool = False
1544
1599
  classic_sc_enabled: bool = True
1545
1600
  classic_ssp_enabled: bool = True
1546
1601
  classic_smp_enabled: bool = True
1547
1602
  classic_accept_any: bool = True
1603
+ classic_interlaced_scan_enabled: bool = True
1548
1604
  connectable: bool = True
1549
1605
  discoverable: bool = True
1550
1606
  advertising_data: bytes = bytes(
@@ -1555,7 +1611,10 @@ class DeviceConfiguration:
1555
1611
  irk: bytes = bytes(16) # This really must be changed for any level of security
1556
1612
  keystore: Optional[str] = None
1557
1613
  address_resolution_offload: bool = False
1614
+ address_generation_offload: bool = False
1558
1615
  cis_enabled: bool = False
1616
+ identity_address_type: Optional[int] = None
1617
+ io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
1559
1618
 
1560
1619
  def __post_init__(self) -> None:
1561
1620
  self.gatt_services: List[Dict[str, Any]] = []
@@ -1640,7 +1699,9 @@ def with_connection_from_handle(function):
1640
1699
  @functools.wraps(function)
1641
1700
  def wrapper(self, connection_handle, *args, **kwargs):
1642
1701
  if (connection := self.lookup_connection(connection_handle)) is None:
1643
- 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
+ )
1644
1705
  return function(self, connection, *args, **kwargs)
1645
1706
 
1646
1707
  return wrapper
@@ -1655,7 +1716,7 @@ def with_connection_from_address(function):
1655
1716
  for connection in self.connections.values():
1656
1717
  if connection.peer_address == address:
1657
1718
  return function(self, connection, *args, **kwargs)
1658
- raise ValueError('no connection for address')
1719
+ raise ObjectLookupError('no connection for address')
1659
1720
 
1660
1721
  return wrapper
1661
1722
 
@@ -1705,8 +1766,9 @@ device_host_event_handlers: List[str] = []
1705
1766
  # -----------------------------------------------------------------------------
1706
1767
  class Device(CompositeEventEmitter):
1707
1768
  # Incomplete list of fields.
1708
- random_address: Address
1709
- 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
1710
1772
  classic_enabled: bool
1711
1773
  name: str
1712
1774
  class_of_device: int
@@ -1836,23 +1898,29 @@ class Device(CompositeEventEmitter):
1836
1898
  config = config or DeviceConfiguration()
1837
1899
  self.config = config
1838
1900
 
1839
- self.public_address = Address('00:00:00:00:00:00')
1840
1901
  self.name = config.name
1902
+ self.public_address = Address.ANY
1841
1903
  self.random_address = config.address
1904
+ self.static_address = config.address
1842
1905
  self.class_of_device = config.class_of_device
1843
1906
  self.keystore = None
1844
1907
  self.irk = config.irk
1845
1908
  self.le_enabled = config.le_enabled
1846
- self.classic_enabled = config.classic_enabled
1847
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
1848
1914
  self.cis_enabled = config.cis_enabled
1849
1915
  self.classic_sc_enabled = config.classic_sc_enabled
1850
1916
  self.classic_ssp_enabled = config.classic_ssp_enabled
1851
1917
  self.classic_smp_enabled = config.classic_smp_enabled
1918
+ self.classic_interlaced_scan_enabled = config.classic_interlaced_scan_enabled
1852
1919
  self.discoverable = config.discoverable
1853
1920
  self.connectable = config.connectable
1854
1921
  self.classic_accept_any = config.classic_accept_any
1855
1922
  self.address_resolution_offload = config.address_resolution_offload
1923
+ self.address_generation_offload = config.address_generation_offload
1856
1924
 
1857
1925
  # Extended advertising.
1858
1926
  self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
@@ -1908,10 +1976,23 @@ class Device(CompositeEventEmitter):
1908
1976
  if isinstance(address, str):
1909
1977
  address = Address(address)
1910
1978
  self.random_address = address
1979
+ self.static_address = address
1911
1980
 
1912
1981
  # Setup SMP
1913
1982
  self.smp_manager = smp.Manager(
1914
- 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
+ ),
1915
1996
  )
1916
1997
 
1917
1998
  self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
@@ -2093,7 +2174,7 @@ class Device(CompositeEventEmitter):
2093
2174
  spec=spec,
2094
2175
  )
2095
2176
  else:
2096
- raise ValueError(f'Unexpected mode {spec}')
2177
+ raise InvalidArgumentError(f'Unexpected mode {spec}')
2097
2178
 
2098
2179
  def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
2099
2180
  self.host.send_l2cap_pdu(connection_handle, cid, pdu)
@@ -2135,26 +2216,26 @@ class Device(CompositeEventEmitter):
2135
2216
  HCI_Write_LE_Host_Support_Command(
2136
2217
  le_supported_host=int(self.le_enabled),
2137
2218
  simultaneous_le_host=int(self.le_simultaneous_enabled),
2138
- )
2219
+ ),
2220
+ check_result=True,
2139
2221
  )
2140
2222
 
2141
2223
  if self.le_enabled:
2142
- # Set the controller address
2143
- if self.random_address == Address.ANY_RANDOM:
2144
- # Try to use an address generated at random by the controller
2145
- if self.host.supports_command(HCI_LE_RAND_COMMAND):
2146
- # Get 8 random bytes
2147
- response = await self.send_command(
2148
- 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()
2149
2236
  )
2150
-
2151
- # Ensure the address bytes can be a static random address
2152
- address_bytes = response.return_parameters.random_number[
2153
- :5
2154
- ] + bytes([response.return_parameters.random_number[5] | 0xC0])
2155
-
2156
- # Create a static random address from the random bytes
2157
- self.random_address = Address(address_bytes)
2237
+ else:
2238
+ self.random_address = self.static_address
2158
2239
 
2159
2240
  if self.random_address != Address.ANY_RANDOM:
2160
2241
  logger.debug(
@@ -2179,7 +2260,8 @@ class Device(CompositeEventEmitter):
2179
2260
  await self.send_command(
2180
2261
  HCI_LE_Set_Address_Resolution_Enable_Command(
2181
2262
  address_resolution_enable=1
2182
- )
2263
+ ),
2264
+ check_result=True,
2183
2265
  )
2184
2266
 
2185
2267
  if self.cis_enabled:
@@ -2187,7 +2269,8 @@ class Device(CompositeEventEmitter):
2187
2269
  HCI_LE_Set_Host_Feature_Command(
2188
2270
  bit_number=LeFeature.CONNECTED_ISOCHRONOUS_STREAM,
2189
2271
  bit_value=1,
2190
- )
2272
+ ),
2273
+ check_result=True,
2191
2274
  )
2192
2275
 
2193
2276
  if self.classic_enabled:
@@ -2210,6 +2293,21 @@ class Device(CompositeEventEmitter):
2210
2293
  await self.set_connectable(self.connectable)
2211
2294
  await self.set_discoverable(self.discoverable)
2212
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
+
2213
2311
  # Done
2214
2312
  self.powered_on = True
2215
2313
 
@@ -2218,9 +2316,45 @@ class Device(CompositeEventEmitter):
2218
2316
 
2219
2317
  async def power_off(self) -> None:
2220
2318
  if self.powered_on:
2319
+ if self.le_rpa_periodic_update_task:
2320
+ self.le_rpa_periodic_update_task.cancel()
2321
+
2221
2322
  await self.host.flush()
2323
+
2222
2324
  self.powered_on = False
2223
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
+
2224
2358
  async def refresh_resolving_list(self) -> None:
2225
2359
  assert self.keystore is not None
2226
2360
 
@@ -2228,7 +2362,7 @@ class Device(CompositeEventEmitter):
2228
2362
  # Create a host-side address resolver
2229
2363
  self.address_resolver = smp.AddressResolver(resolving_keys)
2230
2364
 
2231
- if self.address_resolution_offload:
2365
+ if self.address_resolution_offload or self.address_generation_offload:
2232
2366
  await self.send_command(HCI_LE_Clear_Resolving_List_Command())
2233
2367
 
2234
2368
  # Add an empty entry for non-directed address generation.
@@ -2254,7 +2388,7 @@ class Device(CompositeEventEmitter):
2254
2388
  def supports_le_features(self, feature: LeFeatureMask) -> bool:
2255
2389
  return self.host.supports_le_features(feature)
2256
2390
 
2257
- def supports_le_phy(self, phy):
2391
+ def supports_le_phy(self, phy: int) -> bool:
2258
2392
  if phy == HCI_LE_1M_PHY:
2259
2393
  return True
2260
2394
 
@@ -2263,7 +2397,7 @@ class Device(CompositeEventEmitter):
2263
2397
  HCI_LE_CODED_PHY: LeFeatureMask.LE_CODED_PHY,
2264
2398
  }
2265
2399
  if phy not in feature_map:
2266
- raise ValueError('invalid PHY')
2400
+ raise InvalidArgumentError('invalid PHY')
2267
2401
 
2268
2402
  return self.supports_le_features(feature_map[phy])
2269
2403
 
@@ -2271,6 +2405,10 @@ class Device(CompositeEventEmitter):
2271
2405
  def supports_le_extended_advertising(self):
2272
2406
  return self.supports_le_features(LeFeatureMask.LE_EXTENDED_ADVERTISING)
2273
2407
 
2408
+ @property
2409
+ def supports_le_periodic_advertising(self):
2410
+ return self.supports_le_features(LeFeatureMask.LE_PERIODIC_ADVERTISING)
2411
+
2274
2412
  async def start_advertising(
2275
2413
  self,
2276
2414
  advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
@@ -2323,7 +2461,7 @@ class Device(CompositeEventEmitter):
2323
2461
  # Decide what peer address to use
2324
2462
  if advertising_type.is_directed:
2325
2463
  if target is None:
2326
- raise ValueError('directed advertising requires a target')
2464
+ raise InvalidArgumentError('directed advertising requires a target')
2327
2465
  peer_address = target
2328
2466
  else:
2329
2467
  peer_address = Address.ANY
@@ -2430,7 +2568,7 @@ class Device(CompositeEventEmitter):
2430
2568
  and advertising_data
2431
2569
  and scan_response_data
2432
2570
  ):
2433
- raise ValueError(
2571
+ raise InvalidArgumentError(
2434
2572
  "Extended advertisements can't have both data and scan \
2435
2573
  response data"
2436
2574
  )
@@ -2446,7 +2584,9 @@ class Device(CompositeEventEmitter):
2446
2584
  if handle not in self.extended_advertising_sets
2447
2585
  )
2448
2586
  except StopIteration as exc:
2449
- 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
2450
2590
 
2451
2591
  # Use the device's random address if a random address is needed but none was
2452
2592
  # provided.
@@ -2545,14 +2685,14 @@ class Device(CompositeEventEmitter):
2545
2685
  ) -> None:
2546
2686
  # Check that the arguments are legal
2547
2687
  if scan_interval < scan_window:
2548
- raise ValueError('scan_interval must be >= scan_window')
2688
+ raise InvalidArgumentError('scan_interval must be >= scan_window')
2549
2689
  if (
2550
2690
  scan_interval < DEVICE_MIN_SCAN_INTERVAL
2551
2691
  or scan_interval > DEVICE_MAX_SCAN_INTERVAL
2552
2692
  ):
2553
- raise ValueError('scan_interval out of range')
2693
+ raise InvalidArgumentError('scan_interval out of range')
2554
2694
  if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW:
2555
- raise ValueError('scan_interval out of range')
2695
+ raise InvalidArgumentError('scan_interval out of range')
2556
2696
 
2557
2697
  # Reset the accumulators
2558
2698
  self.advertisement_accumulators = {}
@@ -2580,7 +2720,7 @@ class Device(CompositeEventEmitter):
2580
2720
  scanning_phy_count += 1
2581
2721
 
2582
2722
  if scanning_phy_count == 0:
2583
- raise ValueError('at least one scanning PHY must be enabled')
2723
+ raise InvalidArgumentError('at least one scanning PHY must be enabled')
2584
2724
 
2585
2725
  await self.send_command(
2586
2726
  HCI_LE_Set_Extended_Scan_Parameters_Command(
@@ -2671,6 +2811,10 @@ class Device(CompositeEventEmitter):
2671
2811
  sync_timeout: float = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT,
2672
2812
  filter_duplicates: bool = False,
2673
2813
  ) -> PeriodicAdvertisingSync:
2814
+ # Check that the controller supports the feature.
2815
+ if not self.supports_le_periodic_advertising:
2816
+ raise NotSupportedError()
2817
+
2674
2818
  # Check that there isn't already an equivalent entry
2675
2819
  if any(
2676
2820
  sync.advertiser_address == advertiser_address and sync.sid == sid
@@ -2868,23 +3012,52 @@ class Device(CompositeEventEmitter):
2868
3012
  ] = None,
2869
3013
  own_address_type: int = OwnAddressType.RANDOM,
2870
3014
  timeout: Optional[float] = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3015
+ always_resolve: bool = False,
2871
3016
  ) -> Connection:
2872
3017
  '''
2873
3018
  Request a connection to a peer.
2874
- 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
2875
3021
  pending connection.
2876
3022
 
2877
- connection_parameters_preferences: (BLE only, ignored for BR/EDR)
2878
- * None: use the 1M PHY with default parameters
2879
- * map: each entry has a PHY as key and a ConnectionParametersPreferences
2880
- 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
2881
3042
 
2882
- 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`.
2883
3056
  '''
2884
3057
 
2885
3058
  # Check parameters
2886
3059
  if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
2887
- raise ValueError('invalid transport')
3060
+ raise InvalidArgumentError('invalid transport')
2888
3061
 
2889
3062
  # Adjust the transport automatically if we need to
2890
3063
  if transport == BT_LE_TRANSPORT and not self.le_enabled:
@@ -2898,11 +3071,19 @@ class Device(CompositeEventEmitter):
2898
3071
 
2899
3072
  if isinstance(peer_address, str):
2900
3073
  try:
2901
- peer_address = Address.from_string_for_transport(
2902
- peer_address, transport
2903
- )
2904
- 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):
2905
3085
  # If the address is not parsable, assume it is a name instead
3086
+ always_resolve = False
2906
3087
  logger.debug('looking for peer by name')
2907
3088
  peer_address = await self.find_peer_by_name(
2908
3089
  peer_address, transport
@@ -2913,10 +3094,16 @@ class Device(CompositeEventEmitter):
2913
3094
  transport == BT_BR_EDR_TRANSPORT
2914
3095
  and peer_address.address_type != Address.PUBLIC_DEVICE_ADDRESS
2915
3096
  ):
2916
- raise ValueError('BR/EDR addresses must be PUBLIC')
3097
+ raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
2917
3098
 
2918
3099
  assert isinstance(peer_address, Address)
2919
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
+
2920
3107
  def on_connection(connection):
2921
3108
  if transport == BT_LE_TRANSPORT or (
2922
3109
  # match BR/EDR connection event against peer address
@@ -2964,7 +3151,7 @@ class Device(CompositeEventEmitter):
2964
3151
  )
2965
3152
  )
2966
3153
  if not phys:
2967
- raise ValueError('at least one supported PHY needed')
3154
+ raise InvalidArgumentError('at least one supported PHY needed')
2968
3155
 
2969
3156
  phy_count = len(phys)
2970
3157
  initiating_phys = phy_list_to_bits(phys)
@@ -3036,7 +3223,7 @@ class Device(CompositeEventEmitter):
3036
3223
  )
3037
3224
  else:
3038
3225
  if HCI_LE_1M_PHY not in connection_parameters_preferences:
3039
- raise ValueError('1M PHY preferences required')
3226
+ raise InvalidArgumentError('1M PHY preferences required')
3040
3227
 
3041
3228
  prefs = connection_parameters_preferences[HCI_LE_1M_PHY]
3042
3229
  result = await self.send_command(
@@ -3136,7 +3323,7 @@ class Device(CompositeEventEmitter):
3136
3323
  if isinstance(peer_address, str):
3137
3324
  try:
3138
3325
  peer_address = Address(peer_address)
3139
- except ValueError:
3326
+ except InvalidArgumentError:
3140
3327
  # If the address is not parsable, assume it is a name instead
3141
3328
  logger.debug('looking for peer by name')
3142
3329
  peer_address = await self.find_peer_by_name(
@@ -3146,7 +3333,7 @@ class Device(CompositeEventEmitter):
3146
3333
  assert isinstance(peer_address, Address)
3147
3334
 
3148
3335
  if peer_address == Address.NIL:
3149
- raise ValueError('accept on nil address')
3336
+ raise InvalidArgumentError('accept on nil address')
3150
3337
 
3151
3338
  # Create a future so that we can wait for the request
3152
3339
  pending_request_fut = asyncio.get_running_loop().create_future()
@@ -3259,7 +3446,7 @@ class Device(CompositeEventEmitter):
3259
3446
  if isinstance(peer_address, str):
3260
3447
  try:
3261
3448
  peer_address = Address(peer_address)
3262
- except ValueError:
3449
+ except InvalidArgumentError:
3263
3450
  # If the address is not parsable, assume it is a name instead
3264
3451
  logger.debug('looking for peer by name')
3265
3452
  peer_address = await self.find_peer_by_name(
@@ -3302,10 +3489,10 @@ class Device(CompositeEventEmitter):
3302
3489
 
3303
3490
  async def set_data_length(self, connection, tx_octets, tx_time) -> None:
3304
3491
  if tx_octets < 0x001B or tx_octets > 0x00FB:
3305
- raise ValueError('tx_octets must be between 0x001B and 0x00FB')
3492
+ raise InvalidArgumentError('tx_octets must be between 0x001B and 0x00FB')
3306
3493
 
3307
3494
  if tx_time < 0x0148 or tx_time > 0x4290:
3308
- raise ValueError('tx_time must be between 0x0148 and 0x4290')
3495
+ raise InvalidArgumentError('tx_time must be between 0x0148 and 0x4290')
3309
3496
 
3310
3497
  return await self.send_command(
3311
3498
  HCI_LE_Set_Data_Length_Command(
@@ -3418,15 +3605,26 @@ class Device(CompositeEventEmitter):
3418
3605
  check_result=True,
3419
3606
  )
3420
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
+
3421
3620
  async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT):
3422
3621
  """
3423
- 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.
3424
3623
  """
3425
3624
 
3426
3625
  # Create a future to wait for an address to be found
3427
3626
  peer_address = asyncio.get_running_loop().create_future()
3428
3627
 
3429
- # Scan/inquire with event handlers to handle scan/inquiry results
3430
3628
  def on_peer_found(address, ad_data):
3431
3629
  local_name = ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True)
3432
3630
  if local_name is None:
@@ -3435,13 +3633,13 @@ class Device(CompositeEventEmitter):
3435
3633
  if local_name.decode('utf-8') == name:
3436
3634
  peer_address.set_result(address)
3437
3635
 
3438
- handler = None
3636
+ listener = None
3439
3637
  was_scanning = self.scanning
3440
3638
  was_discovering = self.discovering
3441
3639
  try:
3442
3640
  if transport == BT_LE_TRANSPORT:
3443
3641
  event_name = 'advertisement'
3444
- handler = self.on(
3642
+ listener = self.on(
3445
3643
  event_name,
3446
3644
  lambda advertisement: on_peer_found(
3447
3645
  advertisement.address, advertisement.data
@@ -3453,7 +3651,7 @@ class Device(CompositeEventEmitter):
3453
3651
 
3454
3652
  elif transport == BT_BR_EDR_TRANSPORT:
3455
3653
  event_name = 'inquiry_result'
3456
- handler = self.on(
3654
+ listener = self.on(
3457
3655
  event_name,
3458
3656
  lambda address, class_of_device, eir_data, rssi: on_peer_found(
3459
3657
  address, eir_data
@@ -3467,21 +3665,67 @@ class Device(CompositeEventEmitter):
3467
3665
 
3468
3666
  return await self.abort_on('flush', peer_address)
3469
3667
  finally:
3470
- if handler is not None:
3471
- self.remove_listener(event_name, handler)
3668
+ if listener is not None:
3669
+ self.remove_listener(event_name, listener)
3472
3670
 
3473
3671
  if transport == BT_LE_TRANSPORT and not was_scanning:
3474
3672
  await self.stop_scanning()
3475
3673
  elif transport == BT_BR_EDR_TRANSPORT and not was_discovering:
3476
3674
  await self.stop_discovery()
3477
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
+
3478
3722
  @property
3479
- def pairing_config_factory(self) -> Callable[[Connection], PairingConfig]:
3723
+ def pairing_config_factory(self) -> Callable[[Connection], pairing.PairingConfig]:
3480
3724
  return self.smp_manager.pairing_config_factory
3481
3725
 
3482
3726
  @pairing_config_factory.setter
3483
3727
  def pairing_config_factory(
3484
- self, pairing_config_factory: Callable[[Connection], PairingConfig]
3728
+ self, pairing_config_factory: Callable[[Connection], pairing.PairingConfig]
3485
3729
  ) -> None:
3486
3730
  self.smp_manager.pairing_config_factory = pairing_config_factory
3487
3731
 
@@ -3580,7 +3824,7 @@ class Device(CompositeEventEmitter):
3580
3824
 
3581
3825
  async def encrypt(self, connection, enable=True):
3582
3826
  if not enable and connection.transport == BT_LE_TRANSPORT:
3583
- raise ValueError('`enable` parameter is classic only.')
3827
+ raise InvalidArgumentError('`enable` parameter is classic only.')
3584
3828
 
3585
3829
  # Set up event handlers
3586
3830
  pending_encryption = asyncio.get_running_loop().create_future()
@@ -3599,11 +3843,12 @@ class Device(CompositeEventEmitter):
3599
3843
  if connection.transport == BT_LE_TRANSPORT:
3600
3844
  # Look for a key in the key store
3601
3845
  if self.keystore is None:
3602
- raise RuntimeError('no key store')
3846
+ raise InvalidOperationError('no key store')
3603
3847
 
3848
+ logger.debug(f'Looking up key for {connection.peer_address}')
3604
3849
  keys = await self.keystore.get(str(connection.peer_address))
3605
3850
  if keys is None:
3606
- raise RuntimeError('keys not found in key store')
3851
+ raise InvalidOperationError('keys not found in key store')
3607
3852
 
3608
3853
  if keys.ltk is not None:
3609
3854
  ltk = keys.ltk.value
@@ -3614,7 +3859,7 @@ class Device(CompositeEventEmitter):
3614
3859
  rand = keys.ltk_central.rand
3615
3860
  ediv = keys.ltk_central.ediv
3616
3861
  else:
3617
- raise RuntimeError('no LTK found for peer')
3862
+ raise InvalidOperationError('no LTK found for peer')
3618
3863
 
3619
3864
  if connection.role != HCI_CENTRAL_ROLE:
3620
3865
  raise InvalidStateError('only centrals can start encryption')
@@ -3889,7 +4134,7 @@ class Device(CompositeEventEmitter):
3889
4134
  return cis_link
3890
4135
 
3891
4136
  # Mypy believes this is reachable when context is an ExitStack.
3892
- raise InvalidStateError('Unreachable')
4137
+ raise UnreachableError()
3893
4138
 
3894
4139
  # [LE only]
3895
4140
  @experimental('Only for testing.')
@@ -4036,6 +4281,12 @@ class Device(CompositeEventEmitter):
4036
4281
  else self.public_address
4037
4282
  )
4038
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
+
4039
4290
  # Setup auto-restart of the advertising set if needed.
4040
4291
  if advertising_set.auto_restart:
4041
4292
  connection.once(
@@ -4071,12 +4322,23 @@ class Device(CompositeEventEmitter):
4071
4322
  @host_event_handler
4072
4323
  def on_connection(
4073
4324
  self,
4074
- connection_handle,
4075
- transport,
4076
- peer_address,
4077
- role,
4078
- connection_parameters,
4079
- ):
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
+
4080
4342
  logger.debug(
4081
4343
  f'*** Connection: [0x{connection_handle:04X}] '
4082
4344
  f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}'
@@ -4097,17 +4359,18 @@ class Device(CompositeEventEmitter):
4097
4359
 
4098
4360
  return
4099
4361
 
4100
- # Resolve the peer address if we can
4101
- peer_resolvable_address = None
4102
- if self.address_resolver:
4103
- if peer_address.is_resolvable:
4104
- resolved_address = self.address_resolver.resolve(peer_address)
4105
- if resolved_address is not None:
4106
- logger.debug(f'*** Address resolved as {resolved_address}')
4107
- peer_resolvable_address = peer_address
4108
- 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
4109
4371
 
4110
4372
  self_address = None
4373
+ own_address_type: Optional[int] = None
4111
4374
  if role == HCI_CENTRAL_ROLE:
4112
4375
  own_address_type = self.connect_own_address_type
4113
4376
  assert own_address_type is not None
@@ -4136,12 +4399,18 @@ class Device(CompositeEventEmitter):
4136
4399
  else self.random_address
4137
4400
  )
4138
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
+
4139
4407
  # Create a connection.
4140
4408
  connection = Connection(
4141
4409
  self,
4142
4410
  connection_handle,
4143
4411
  transport,
4144
4412
  self_address,
4413
+ self_resolvable_address,
4145
4414
  peer_address,
4146
4415
  peer_resolvable_address,
4147
4416
  role,
@@ -4152,9 +4421,10 @@ class Device(CompositeEventEmitter):
4152
4421
 
4153
4422
  if role == HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
4154
4423
  if self.legacy_advertiser.auto_restart:
4424
+ advertiser = self.legacy_advertiser
4155
4425
  connection.once(
4156
4426
  'disconnection',
4157
- lambda _: self.abort_on('flush', self.legacy_advertiser.start()),
4427
+ lambda _: self.abort_on('flush', advertiser.start()),
4158
4428
  )
4159
4429
  else:
4160
4430
  self.legacy_advertiser = None
@@ -4377,7 +4647,7 @@ class Device(CompositeEventEmitter):
4377
4647
  return await pairing_config.delegate.confirm(auto=True)
4378
4648
 
4379
4649
  async def na() -> bool:
4380
- assert False, "N/A: unreachable"
4650
+ raise UnreachableError()
4381
4651
 
4382
4652
  # See Bluetooth spec @ Vol 3, Part C 5.2.2.6
4383
4653
  methods = {
@@ -4838,5 +5108,6 @@ class Device(CompositeEventEmitter):
4838
5108
  return (
4839
5109
  f'Device(name="{self.name}", '
4840
5110
  f'random_address="{self.random_address}", '
4841
- f'public_address="{self.public_address}")'
5111
+ f'public_address="{self.public_address}", '
5112
+ f'static_address="{self.static_address}")'
4842
5113
  )