bumble 0.0.195__py3-none-any.whl → 0.0.199__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +351 -66
- bumble/apps/console.py +5 -20
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/apps/pair.py +32 -5
- bumble/at.py +12 -6
- bumble/att.py +56 -40
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +7 -3
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/decoder.py +14 -10
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +32 -13
- bumble/gatt.py +30 -20
- bumble/gatt_client.py +15 -29
- bumble/gatt_server.py +14 -6
- bumble/hci.py +322 -32
- bumble/hid.py +24 -28
- bumble/host.py +20 -6
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/__init__.py +3 -0
- bumble/pandora/l2cap.py +310 -0
- bumble/profiles/aics.py +520 -0
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/asha.py +295 -0
- bumble/profiles/bap.py +1 -874
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/hap.py +665 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +43 -9
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/tmap.py +89 -0
- bumble/profiles/vcp.py +5 -3
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +43 -12
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +21 -4
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
- bumble/profiles/asha_service.py +0 -193
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.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
|
|
200
|
-
from
|
|
201
|
-
from
|
|
202
|
-
from
|
|
203
|
-
from
|
|
204
|
-
from
|
|
205
|
-
from
|
|
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
|
|
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.
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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,
|
|
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(
|
|
1180
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
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'
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
#
|
|
2143
|
-
if self.
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3019
|
+
|
|
3020
|
+
When the transport is BLE, this method cannot be called if there is already a
|
|
2875
3021
|
pending connection.
|
|
2876
3022
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2902
|
-
peer_address
|
|
2903
|
-
|
|
2904
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3471
|
-
self.remove_listener(event_name,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4078
|
-
|
|
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
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
)
|