bumble 0.0.219__py3-none-any.whl → 0.0.221__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/a2dp.py +5 -5
- bumble/apps/auracast.py +746 -479
- bumble/apps/bench.py +4 -5
- bumble/apps/console.py +5 -10
- bumble/apps/controller_info.py +12 -7
- bumble/apps/controller_loopback.py +1 -2
- bumble/apps/device_info.py +2 -3
- bumble/apps/gatt_dump.py +0 -1
- bumble/apps/lea_unicast/app.py +1 -1
- bumble/apps/pair.py +49 -46
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +10 -12
- bumble/apps/rfcomm_bridge.py +10 -11
- bumble/apps/scan.py +1 -3
- bumble/apps/speaker/speaker.py +3 -4
- bumble/at.py +4 -5
- bumble/att.py +91 -25
- bumble/audio/io.py +8 -6
- bumble/avc.py +1 -2
- bumble/avctp.py +2 -3
- bumble/avdtp.py +53 -57
- bumble/avrcp.py +25 -27
- bumble/codecs.py +15 -15
- bumble/colors.py +7 -8
- bumble/controller.py +1201 -643
- bumble/core.py +41 -49
- bumble/crypto/__init__.py +2 -1
- bumble/crypto/builtin.py +2 -8
- bumble/data_types.py +2 -1
- bumble/decoder.py +2 -3
- bumble/device.py +278 -325
- bumble/drivers/__init__.py +3 -2
- bumble/drivers/intel.py +6 -8
- bumble/drivers/rtk.py +1 -1
- bumble/gatt.py +9 -9
- bumble/gatt_adapters.py +6 -6
- bumble/gatt_client.py +110 -60
- bumble/gatt_server.py +209 -139
- bumble/hci.py +87 -74
- bumble/helpers.py +5 -5
- bumble/hfp.py +27 -26
- bumble/hid.py +9 -9
- bumble/host.py +44 -50
- bumble/keys.py +17 -17
- bumble/l2cap.py +1015 -218
- bumble/link.py +54 -284
- bumble/ll.py +200 -0
- bumble/lmp.py +324 -0
- bumble/pairing.py +14 -15
- bumble/pandora/__init__.py +2 -2
- bumble/pandora/device.py +6 -4
- bumble/pandora/host.py +19 -10
- bumble/pandora/l2cap.py +8 -9
- bumble/pandora/security.py +18 -16
- bumble/pandora/utils.py +4 -4
- bumble/profiles/aics.py +6 -8
- bumble/profiles/ams.py +3 -5
- bumble/profiles/ancs.py +11 -11
- bumble/profiles/ascs.py +5 -5
- bumble/profiles/asha.py +10 -9
- bumble/profiles/bass.py +9 -3
- bumble/profiles/battery_service.py +1 -2
- bumble/profiles/csip.py +9 -10
- bumble/profiles/device_information_service.py +16 -17
- bumble/profiles/gap.py +3 -4
- bumble/profiles/gatt_service.py +0 -1
- bumble/profiles/gmap.py +12 -13
- bumble/profiles/hap.py +3 -3
- bumble/profiles/heart_rate_service.py +7 -8
- bumble/profiles/le_audio.py +1 -1
- bumble/profiles/mcp.py +28 -28
- bumble/profiles/pacs.py +13 -17
- bumble/profiles/pbp.py +16 -0
- bumble/profiles/vcs.py +2 -2
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +19 -18
- bumble/sdp.py +12 -11
- bumble/smp.py +20 -30
- bumble/snoop.py +12 -5
- bumble/tools/generate_company_id_list.py +1 -1
- bumble/tools/intel_util.py +2 -2
- bumble/tools/rtk_fw_download.py +1 -1
- bumble/tools/rtk_util.py +1 -1
- bumble/transport/__init__.py +1 -2
- bumble/transport/android_emulator.py +2 -3
- bumble/transport/android_netsim.py +49 -40
- bumble/transport/common.py +9 -9
- bumble/transport/file.py +1 -2
- bumble/transport/hci_socket.py +2 -3
- bumble/transport/pty.py +3 -5
- bumble/transport/pyusb.py +8 -5
- bumble/transport/serial.py +1 -2
- bumble/transport/vhci.py +1 -2
- bumble/transport/ws_server.py +2 -3
- bumble/utils.py +23 -14
- bumble/vendor/android/hci.py +4 -2
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/METADATA +4 -3
- bumble-0.0.221.dist-info/RECORD +185 -0
- bumble-0.0.219.dist-info/RECORD +0 -183
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -25,20 +25,15 @@ import itertools
|
|
|
25
25
|
import json
|
|
26
26
|
import logging
|
|
27
27
|
import secrets
|
|
28
|
-
import
|
|
29
|
-
from collections.abc import Iterable, Sequence
|
|
28
|
+
from collections.abc import Awaitable, Callable, Iterable, Sequence
|
|
30
29
|
from contextlib import AsyncExitStack, asynccontextmanager, closing
|
|
31
30
|
from dataclasses import dataclass, field
|
|
32
31
|
from enum import Enum, IntEnum
|
|
33
32
|
from typing import (
|
|
34
33
|
TYPE_CHECKING,
|
|
35
34
|
Any,
|
|
36
|
-
Awaitable,
|
|
37
|
-
Callable,
|
|
38
35
|
ClassVar,
|
|
39
|
-
Optional,
|
|
40
36
|
TypeVar,
|
|
41
|
-
Union,
|
|
42
37
|
cast,
|
|
43
38
|
overload,
|
|
44
39
|
)
|
|
@@ -46,6 +41,7 @@ from typing import (
|
|
|
46
41
|
from typing_extensions import Self
|
|
47
42
|
|
|
48
43
|
from bumble import (
|
|
44
|
+
att,
|
|
49
45
|
core,
|
|
50
46
|
data_types,
|
|
51
47
|
gatt,
|
|
@@ -58,7 +54,6 @@ from bumble import (
|
|
|
58
54
|
smp,
|
|
59
55
|
utils,
|
|
60
56
|
)
|
|
61
|
-
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
|
62
57
|
from bumble.colors import color
|
|
63
58
|
from bumble.core import (
|
|
64
59
|
AdvertisingData,
|
|
@@ -176,6 +171,7 @@ class Advertisement:
|
|
|
176
171
|
)
|
|
177
172
|
sid: int = 0
|
|
178
173
|
data_bytes: bytes = b''
|
|
174
|
+
data: AdvertisingData = field(init=False)
|
|
179
175
|
|
|
180
176
|
# Constants
|
|
181
177
|
TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
|
|
@@ -189,7 +185,7 @@ class Advertisement:
|
|
|
189
185
|
self.data = AdvertisingData.from_bytes(self.data_bytes)
|
|
190
186
|
|
|
191
187
|
@classmethod
|
|
192
|
-
def from_advertising_report(cls, report) ->
|
|
188
|
+
def from_advertising_report(cls, report) -> Advertisement | None:
|
|
193
189
|
if isinstance(report, hci.HCI_LE_Advertising_Report_Event.Report):
|
|
194
190
|
return LegacyAdvertisement.from_advertising_report(report)
|
|
195
191
|
|
|
@@ -265,12 +261,22 @@ class ExtendedAdvertisement(Advertisement):
|
|
|
265
261
|
|
|
266
262
|
# -----------------------------------------------------------------------------
|
|
267
263
|
class AdvertisementDataAccumulator:
|
|
264
|
+
last_advertisement: Advertisement | None
|
|
265
|
+
last_data: bytes
|
|
266
|
+
passive: bool
|
|
267
|
+
|
|
268
268
|
def __init__(self, passive: bool = False):
|
|
269
269
|
self.passive = passive
|
|
270
270
|
self.last_advertisement = None
|
|
271
271
|
self.last_data = b''
|
|
272
272
|
|
|
273
|
-
def update(
|
|
273
|
+
def update(
|
|
274
|
+
self,
|
|
275
|
+
report: (
|
|
276
|
+
hci.HCI_LE_Advertising_Report_Event.Report
|
|
277
|
+
| hci.HCI_LE_Extended_Advertising_Report_Event.Report
|
|
278
|
+
),
|
|
279
|
+
) -> Advertisement | None:
|
|
274
280
|
advertisement = Advertisement.from_advertising_report(report)
|
|
275
281
|
if advertisement is None:
|
|
276
282
|
return None
|
|
@@ -283,10 +289,12 @@ class AdvertisementDataAccumulator:
|
|
|
283
289
|
and not self.last_advertisement.is_scan_response
|
|
284
290
|
):
|
|
285
291
|
# This is the response to a scannable advertisement
|
|
286
|
-
result
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
if result := Advertisement.from_advertising_report(report):
|
|
293
|
+
result.is_connectable = self.last_advertisement.is_connectable
|
|
294
|
+
result.is_scannable = True
|
|
295
|
+
result.data = AdvertisingData.from_bytes(
|
|
296
|
+
self.last_data + report.data
|
|
297
|
+
)
|
|
290
298
|
self.last_data = b''
|
|
291
299
|
else:
|
|
292
300
|
if (
|
|
@@ -473,6 +481,7 @@ class PeriodicAdvertisement:
|
|
|
473
481
|
rssi: int = hci.HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
|
474
482
|
is_truncated: bool = False
|
|
475
483
|
data_bytes: bytes = b''
|
|
484
|
+
data: AdvertisingData | None = field(init=False)
|
|
476
485
|
|
|
477
486
|
# Constants
|
|
478
487
|
TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
|
|
@@ -593,11 +602,11 @@ class AdvertisingSet(utils.EventEmitter):
|
|
|
593
602
|
device: Device
|
|
594
603
|
advertising_handle: int
|
|
595
604
|
auto_restart: bool
|
|
596
|
-
random_address:
|
|
605
|
+
random_address: hci.Address | None
|
|
597
606
|
advertising_parameters: AdvertisingParameters
|
|
598
607
|
advertising_data: bytes
|
|
599
608
|
scan_response_data: bytes
|
|
600
|
-
periodic_advertising_parameters:
|
|
609
|
+
periodic_advertising_parameters: PeriodicAdvertisingParameters | None
|
|
601
610
|
periodic_advertising_data: bytes
|
|
602
611
|
selected_tx_power: int = 0
|
|
603
612
|
enabled: bool = False
|
|
@@ -844,7 +853,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
844
853
|
TERMINATED = 6
|
|
845
854
|
|
|
846
855
|
_state: State
|
|
847
|
-
sync_handle:
|
|
856
|
+
sync_handle: int | None
|
|
848
857
|
advertiser_address: hci.Address
|
|
849
858
|
sid: int
|
|
850
859
|
skip: int
|
|
@@ -857,8 +866,8 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
857
866
|
|
|
858
867
|
EVENT_STATE_CHANGE = "state_change"
|
|
859
868
|
EVENT_ESTABLISHMENT = "establishment"
|
|
869
|
+
EVENT_ESTABLISHMENT_ERROR = "establishment_error"
|
|
860
870
|
EVENT_CANCELLATION = "cancellation"
|
|
861
|
-
EVENT_ERROR = "error"
|
|
862
871
|
EVENT_LOSS = "loss"
|
|
863
872
|
EVENT_PERIODIC_ADVERTISEMENT = "periodic_advertisement"
|
|
864
873
|
EVENT_BIGINFO_ADVERTISEMENT = "biginfo_advertisement"
|
|
@@ -907,7 +916,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
907
916
|
hci.HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
|
|
908
917
|
)
|
|
909
918
|
|
|
910
|
-
|
|
919
|
+
await self.device.send_command(
|
|
911
920
|
hci.HCI_LE_Periodic_Advertising_Create_Sync_Command(
|
|
912
921
|
options=options,
|
|
913
922
|
advertising_sid=self.sid,
|
|
@@ -916,10 +925,9 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
916
925
|
skip=self.skip,
|
|
917
926
|
sync_timeout=int(self.sync_timeout * 100),
|
|
918
927
|
sync_cte_type=0,
|
|
919
|
-
)
|
|
928
|
+
),
|
|
929
|
+
check_result=True,
|
|
920
930
|
)
|
|
921
|
-
if response.status != hci.HCI_Command_Status_Event.PENDING:
|
|
922
|
-
raise hci.HCI_StatusError(response)
|
|
923
931
|
|
|
924
932
|
self.state = self.State.PENDING
|
|
925
933
|
|
|
@@ -992,7 +1000,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
992
1000
|
return
|
|
993
1001
|
|
|
994
1002
|
self.state = self.State.ERROR
|
|
995
|
-
self.emit(self.
|
|
1003
|
+
self.emit(self.EVENT_ESTABLISHMENT_ERROR)
|
|
996
1004
|
|
|
997
1005
|
def on_loss(self):
|
|
998
1006
|
self.state = self.State.LOST
|
|
@@ -1093,6 +1101,7 @@ class Big(utils.EventEmitter):
|
|
|
1093
1101
|
max_pdu: int = 0
|
|
1094
1102
|
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
|
1095
1103
|
bis_links: Sequence[BisLink] = ()
|
|
1104
|
+
device: Device = field(init=False)
|
|
1096
1105
|
|
|
1097
1106
|
def __post_init__(self) -> None:
|
|
1098
1107
|
super().__init__()
|
|
@@ -1154,6 +1163,7 @@ class BigSync(utils.EventEmitter):
|
|
|
1154
1163
|
max_pdu: int = 0
|
|
1155
1164
|
iso_interval: float = 0.0
|
|
1156
1165
|
bis_links: Sequence[BisLink] = ()
|
|
1166
|
+
device: Device = field(init=False)
|
|
1157
1167
|
|
|
1158
1168
|
def __post_init__(self) -> None:
|
|
1159
1169
|
super().__init__()
|
|
@@ -1272,7 +1282,7 @@ class Peer:
|
|
|
1272
1282
|
return mtu
|
|
1273
1283
|
|
|
1274
1284
|
async def discover_service(
|
|
1275
|
-
self, uuid:
|
|
1285
|
+
self, uuid: core.UUID | str
|
|
1276
1286
|
) -> list[gatt_client.ServiceProxy]:
|
|
1277
1287
|
return await self.gatt_client.discover_service(uuid)
|
|
1278
1288
|
|
|
@@ -1288,8 +1298,8 @@ class Peer:
|
|
|
1288
1298
|
|
|
1289
1299
|
async def discover_characteristics(
|
|
1290
1300
|
self,
|
|
1291
|
-
uuids: Iterable[
|
|
1292
|
-
service:
|
|
1301
|
+
uuids: Iterable[core.UUID | str] = (),
|
|
1302
|
+
service: gatt_client.ServiceProxy | None = None,
|
|
1293
1303
|
) -> list[gatt_client.CharacteristicProxy[bytes]]:
|
|
1294
1304
|
return await self.gatt_client.discover_characteristics(
|
|
1295
1305
|
uuids=uuids, service=service
|
|
@@ -1297,9 +1307,9 @@ class Peer:
|
|
|
1297
1307
|
|
|
1298
1308
|
async def discover_descriptors(
|
|
1299
1309
|
self,
|
|
1300
|
-
characteristic:
|
|
1301
|
-
start_handle:
|
|
1302
|
-
end_handle:
|
|
1310
|
+
characteristic: gatt_client.CharacteristicProxy | None = None,
|
|
1311
|
+
start_handle: int | None = None,
|
|
1312
|
+
end_handle: int | None = None,
|
|
1303
1313
|
):
|
|
1304
1314
|
return await self.gatt_client.discover_descriptors(
|
|
1305
1315
|
characteristic, start_handle, end_handle
|
|
@@ -1320,7 +1330,7 @@ class Peer:
|
|
|
1320
1330
|
async def subscribe(
|
|
1321
1331
|
self,
|
|
1322
1332
|
characteristic: gatt_client.CharacteristicProxy,
|
|
1323
|
-
subscriber:
|
|
1333
|
+
subscriber: Callable[[bytes], Any] | None = None,
|
|
1324
1334
|
prefer_notify: bool = True,
|
|
1325
1335
|
) -> None:
|
|
1326
1336
|
return await self.gatt_client.subscribe(
|
|
@@ -1330,25 +1340,23 @@ class Peer:
|
|
|
1330
1340
|
async def unsubscribe(
|
|
1331
1341
|
self,
|
|
1332
1342
|
characteristic: gatt_client.CharacteristicProxy,
|
|
1333
|
-
subscriber:
|
|
1343
|
+
subscriber: Callable[[bytes], Any] | None = None,
|
|
1334
1344
|
) -> None:
|
|
1335
1345
|
return await self.gatt_client.unsubscribe(characteristic, subscriber)
|
|
1336
1346
|
|
|
1337
|
-
async def read_value(
|
|
1338
|
-
self, attribute: Union[int, gatt_client.AttributeProxy]
|
|
1339
|
-
) -> bytes:
|
|
1347
|
+
async def read_value(self, attribute: int | gatt_client.AttributeProxy) -> bytes:
|
|
1340
1348
|
return await self.gatt_client.read_value(attribute)
|
|
1341
1349
|
|
|
1342
1350
|
async def write_value(
|
|
1343
1351
|
self,
|
|
1344
|
-
attribute:
|
|
1352
|
+
attribute: int | gatt_client.AttributeProxy,
|
|
1345
1353
|
value: bytes,
|
|
1346
1354
|
with_response: bool = False,
|
|
1347
1355
|
) -> None:
|
|
1348
1356
|
return await self.gatt_client.write_value(attribute, value, with_response)
|
|
1349
1357
|
|
|
1350
1358
|
async def read_characteristics_by_uuid(
|
|
1351
|
-
self, uuid: core.UUID, service:
|
|
1359
|
+
self, uuid: core.UUID, service: gatt_client.ServiceProxy | None = None
|
|
1352
1360
|
) -> list[bytes]:
|
|
1353
1361
|
return await self.gatt_client.read_characteristics_by_uuid(uuid, service)
|
|
1354
1362
|
|
|
@@ -1358,7 +1366,7 @@ class Peer:
|
|
|
1358
1366
|
def get_characteristics_by_uuid(
|
|
1359
1367
|
self,
|
|
1360
1368
|
uuid: core.UUID,
|
|
1361
|
-
service:
|
|
1369
|
+
service: gatt_client.ServiceProxy | core.UUID | None = None,
|
|
1362
1370
|
) -> list[gatt_client.CharacteristicProxy[bytes]]:
|
|
1363
1371
|
if isinstance(service, core.UUID):
|
|
1364
1372
|
return list(
|
|
@@ -1374,7 +1382,7 @@ class Peer:
|
|
|
1374
1382
|
|
|
1375
1383
|
def create_service_proxy(
|
|
1376
1384
|
self, proxy_class: type[_PROXY_CLASS]
|
|
1377
|
-
) ->
|
|
1385
|
+
) -> _PROXY_CLASS | None:
|
|
1378
1386
|
if proxy := proxy_class.from_client(self.gatt_client):
|
|
1379
1387
|
return cast(_PROXY_CLASS, proxy)
|
|
1380
1388
|
|
|
@@ -1382,7 +1390,7 @@ class Peer:
|
|
|
1382
1390
|
|
|
1383
1391
|
async def discover_service_and_create_proxy(
|
|
1384
1392
|
self, proxy_class: type[_PROXY_CLASS]
|
|
1385
|
-
) ->
|
|
1393
|
+
) -> _PROXY_CLASS | None:
|
|
1386
1394
|
# Discover the first matching service and its characteristics
|
|
1387
1395
|
services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
|
|
1388
1396
|
if services:
|
|
@@ -1391,7 +1399,7 @@ class Peer:
|
|
|
1391
1399
|
return self.create_service_proxy(proxy_class)
|
|
1392
1400
|
return None
|
|
1393
1401
|
|
|
1394
|
-
async def sustain(self, timeout:
|
|
1402
|
+
async def sustain(self, timeout: float | None = None) -> None:
|
|
1395
1403
|
await self.connection.sustain(timeout)
|
|
1396
1404
|
|
|
1397
1405
|
# [Classic only]
|
|
@@ -1434,7 +1442,7 @@ class ScoLink(utils.CompositeEventEmitter):
|
|
|
1434
1442
|
acl_connection: Connection
|
|
1435
1443
|
handle: int
|
|
1436
1444
|
link_type: int
|
|
1437
|
-
sink:
|
|
1445
|
+
sink: Callable[[hci.HCI_SynchronousDataPacket], Any] | None = None
|
|
1438
1446
|
|
|
1439
1447
|
EVENT_DISCONNECTION: ClassVar[str] = "disconnection"
|
|
1440
1448
|
EVENT_DISCONNECTION_FAILURE: ClassVar[str] = "disconnection_failure"
|
|
@@ -1617,8 +1625,8 @@ class CisLink(utils.EventEmitter, _IsoLink):
|
|
|
1617
1625
|
cis_sync_delay: int = 0 # CIS sync delay, in microseconds
|
|
1618
1626
|
transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
|
|
1619
1627
|
transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
|
|
1620
|
-
phy_c_to_p:
|
|
1621
|
-
phy_p_to_c:
|
|
1628
|
+
phy_c_to_p: hci.Phy | None = None
|
|
1629
|
+
phy_p_to_c: hci.Phy | None = None
|
|
1622
1630
|
nse: int = 0
|
|
1623
1631
|
bn_c_to_p: int = 0
|
|
1624
1632
|
bn_p_to_c: int = 0
|
|
@@ -1651,6 +1659,7 @@ class BisLink(_IsoLink):
|
|
|
1651
1659
|
handle: int
|
|
1652
1660
|
big: Big | BigSync
|
|
1653
1661
|
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
|
1662
|
+
device: Device = field(init=False)
|
|
1654
1663
|
|
|
1655
1664
|
def __post_init__(self) -> None:
|
|
1656
1665
|
super().__init__()
|
|
@@ -1706,11 +1715,11 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1706
1715
|
handle: int
|
|
1707
1716
|
transport: core.PhysicalTransport
|
|
1708
1717
|
self_address: hci.Address
|
|
1709
|
-
self_resolvable_address:
|
|
1718
|
+
self_resolvable_address: hci.Address | None
|
|
1710
1719
|
peer_address: hci.Address
|
|
1711
|
-
peer_name:
|
|
1712
|
-
peer_resolvable_address:
|
|
1713
|
-
peer_le_features:
|
|
1720
|
+
peer_name: str | None
|
|
1721
|
+
peer_resolvable_address: hci.Address | None
|
|
1722
|
+
peer_le_features: hci.LeFeatureMask | None
|
|
1714
1723
|
role: hci.Role
|
|
1715
1724
|
parameters: Parameters
|
|
1716
1725
|
encryption: int
|
|
@@ -1718,8 +1727,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1718
1727
|
authenticated: bool
|
|
1719
1728
|
sc: bool
|
|
1720
1729
|
gatt_client: gatt_client.Client
|
|
1721
|
-
pairing_peer_io_capability:
|
|
1722
|
-
pairing_peer_authentication_requirements:
|
|
1730
|
+
pairing_peer_io_capability: int | None
|
|
1731
|
+
pairing_peer_authentication_requirements: int | None
|
|
1723
1732
|
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
|
|
1724
1733
|
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
|
|
1725
1734
|
classic_mode: int = hci.HCI_Mode_Change_Event.Mode.ACTIVE
|
|
@@ -1739,7 +1748,6 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1739
1748
|
EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE = "connection_parameters_update_failure"
|
|
1740
1749
|
EVENT_CONNECTION_PHY_UPDATE = "connection_phy_update"
|
|
1741
1750
|
EVENT_CONNECTION_PHY_UPDATE_FAILURE = "connection_phy_update_failure"
|
|
1742
|
-
EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
|
|
1743
1751
|
EVENT_CONNECTION_DATA_LENGTH_CHANGE = "connection_data_length_change"
|
|
1744
1752
|
EVENT_CHANNEL_SOUNDING_CAPABILITIES_FAILURE = (
|
|
1745
1753
|
"channel_sounding_capabilities_failure"
|
|
@@ -1821,9 +1829,9 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1821
1829
|
handle: int,
|
|
1822
1830
|
transport: core.PhysicalTransport,
|
|
1823
1831
|
self_address: hci.Address,
|
|
1824
|
-
self_resolvable_address:
|
|
1832
|
+
self_resolvable_address: hci.Address | None,
|
|
1825
1833
|
peer_address: hci.Address,
|
|
1826
|
-
peer_resolvable_address:
|
|
1834
|
+
peer_resolvable_address: hci.Address | None,
|
|
1827
1835
|
role: hci.Role,
|
|
1828
1836
|
parameters: Parameters,
|
|
1829
1837
|
):
|
|
@@ -1842,7 +1850,7 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1842
1850
|
self.encryption_key_size = 0
|
|
1843
1851
|
self.authenticated = False
|
|
1844
1852
|
self.sc = False
|
|
1845
|
-
self.att_mtu = ATT_DEFAULT_MTU
|
|
1853
|
+
self.att_mtu = att.ATT_DEFAULT_MTU
|
|
1846
1854
|
self.data_length = DEVICE_DEFAULT_DATA_LENGTH
|
|
1847
1855
|
self.gatt_client = gatt_client.Client(self) # Per-connection client
|
|
1848
1856
|
self.gatt_server = (
|
|
@@ -1886,8 +1894,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1886
1894
|
) -> l2cap.LeCreditBasedChannel: ...
|
|
1887
1895
|
|
|
1888
1896
|
async def create_l2cap_channel(
|
|
1889
|
-
self, spec:
|
|
1890
|
-
) ->
|
|
1897
|
+
self, spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec
|
|
1898
|
+
) -> l2cap.ClassicChannel | l2cap.LeCreditBasedChannel:
|
|
1891
1899
|
return await self.device.create_l2cap_channel(connection=self, spec=spec)
|
|
1892
1900
|
|
|
1893
1901
|
async def disconnect(
|
|
@@ -1911,20 +1919,17 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1911
1919
|
async def switch_role(self, role: hci.Role) -> None:
|
|
1912
1920
|
return await self.device.switch_role(self, role)
|
|
1913
1921
|
|
|
1914
|
-
async def sustain(self, timeout:
|
|
1922
|
+
async def sustain(self, timeout: float | None = None) -> None:
|
|
1915
1923
|
"""Idles the current task waiting for a disconnect or timeout"""
|
|
1916
1924
|
|
|
1917
1925
|
abort = asyncio.get_running_loop().create_future()
|
|
1918
|
-
|
|
1919
|
-
|
|
1926
|
+
with closing(utils.EventWatcher()) as watcher:
|
|
1927
|
+
watcher.on(self, self.EVENT_DISCONNECTION, abort.set_result)
|
|
1928
|
+
watcher.on(self, self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
|
1920
1929
|
|
|
1921
|
-
try:
|
|
1922
1930
|
await asyncio.wait_for(
|
|
1923
1931
|
utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout
|
|
1924
1932
|
)
|
|
1925
|
-
finally:
|
|
1926
|
-
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
|
1927
|
-
self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
|
1928
1933
|
|
|
1929
1934
|
async def set_data_length(self, tx_octets: int, tx_time: int) -> None:
|
|
1930
1935
|
return await self.device.set_data_length(self, tx_octets, tx_time)
|
|
@@ -1958,8 +1963,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1958
1963
|
|
|
1959
1964
|
async def set_phy(
|
|
1960
1965
|
self,
|
|
1961
|
-
tx_phys:
|
|
1962
|
-
rx_phys:
|
|
1966
|
+
tx_phys: Iterable[hci.Phy] | None = None,
|
|
1967
|
+
rx_phys: Iterable[hci.Phy] | None = None,
|
|
1963
1968
|
phy_options: int = 0,
|
|
1964
1969
|
):
|
|
1965
1970
|
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
|
|
@@ -1995,6 +2000,15 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1995
2000
|
self.peer_le_features = await self.device.get_remote_le_features(self)
|
|
1996
2001
|
return self.peer_le_features
|
|
1997
2002
|
|
|
2003
|
+
def on_att_mtu_update(self, mtu: int):
|
|
2004
|
+
logger.debug(
|
|
2005
|
+
f'*** Connection ATT MTU Update: [0x{self.handle:04X}] '
|
|
2006
|
+
f'{self.peer_address} as {self.role_name}, '
|
|
2007
|
+
f'{mtu}'
|
|
2008
|
+
)
|
|
2009
|
+
self.att_mtu = mtu
|
|
2010
|
+
self.emit(self.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
|
2011
|
+
|
|
1998
2012
|
@property
|
|
1999
2013
|
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
2000
2014
|
return self.device.host.get_data_packet_queue(self.handle)
|
|
@@ -2063,18 +2077,26 @@ class DeviceConfiguration:
|
|
|
2063
2077
|
AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
|
|
2064
2078
|
)
|
|
2065
2079
|
irk: bytes = bytes(16) # This really must be changed for any level of security
|
|
2066
|
-
keystore:
|
|
2080
|
+
keystore: str | None = None
|
|
2067
2081
|
address_resolution_offload: bool = False
|
|
2068
2082
|
address_generation_offload: bool = False
|
|
2069
2083
|
cis_enabled: bool = False
|
|
2070
2084
|
channel_sounding_enabled: bool = False
|
|
2071
|
-
identity_address_type:
|
|
2085
|
+
identity_address_type: int | None = None
|
|
2072
2086
|
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
|
2073
2087
|
gap_service_enabled: bool = True
|
|
2074
2088
|
gatt_service_enabled: bool = True
|
|
2089
|
+
enhanced_retransmission_supported: bool = False
|
|
2090
|
+
l2cap_extended_features: Sequence[int] = (
|
|
2091
|
+
l2cap.L2CAP_Information_Request.ExtendedFeatures.FIXED_CHANNELS,
|
|
2092
|
+
l2cap.L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION,
|
|
2093
|
+
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE,
|
|
2094
|
+
)
|
|
2095
|
+
eatt_enabled: bool = False
|
|
2096
|
+
gatt_services: list[dict[str, Any]] = field(init=False)
|
|
2075
2097
|
|
|
2076
2098
|
def __post_init__(self) -> None:
|
|
2077
|
-
self.gatt_services
|
|
2099
|
+
self.gatt_services = []
|
|
2078
2100
|
|
|
2079
2101
|
def load_from_dict(self, config: dict[str, Any]) -> None:
|
|
2080
2102
|
config = copy.deepcopy(config)
|
|
@@ -2130,7 +2152,7 @@ class DeviceConfiguration:
|
|
|
2130
2152
|
setattr(self, key, value)
|
|
2131
2153
|
|
|
2132
2154
|
def load_from_file(self, filename: str) -> None:
|
|
2133
|
-
with open(filename,
|
|
2155
|
+
with open(filename, encoding='utf-8') as file:
|
|
2134
2156
|
self.load_from_dict(json.load(file))
|
|
2135
2157
|
|
|
2136
2158
|
@classmethod
|
|
@@ -2241,12 +2263,12 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2241
2263
|
pending_connections: dict[hci.Address, Connection]
|
|
2242
2264
|
classic_pending_accepts: dict[
|
|
2243
2265
|
hci.Address,
|
|
2244
|
-
list[asyncio.Future[
|
|
2266
|
+
list[asyncio.Future[Connection | tuple[hci.Address, int, int]]],
|
|
2245
2267
|
]
|
|
2246
2268
|
advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
|
|
2247
2269
|
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
|
|
2248
2270
|
config: DeviceConfiguration
|
|
2249
|
-
legacy_advertiser:
|
|
2271
|
+
legacy_advertiser: LegacyAdvertiser | None
|
|
2250
2272
|
sco_links: dict[int, ScoLink]
|
|
2251
2273
|
cis_links: dict[int, CisLink]
|
|
2252
2274
|
bigs: dict[int, Big]
|
|
@@ -2254,6 +2276,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2254
2276
|
big_syncs: dict[int, BigSync]
|
|
2255
2277
|
_pending_cis: dict[int, tuple[int, int]]
|
|
2256
2278
|
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
2279
|
+
keystore: KeyStore | None = None
|
|
2257
2280
|
|
|
2258
2281
|
EVENT_ADVERTISEMENT = "advertisement"
|
|
2259
2282
|
EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER = "periodic_advertising_sync_transfer"
|
|
@@ -2334,13 +2357,17 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2334
2357
|
|
|
2335
2358
|
def __init__(
|
|
2336
2359
|
self,
|
|
2337
|
-
name:
|
|
2338
|
-
address:
|
|
2339
|
-
config:
|
|
2340
|
-
host:
|
|
2360
|
+
name: str | None = None,
|
|
2361
|
+
address: hci.Address | None = None,
|
|
2362
|
+
config: DeviceConfiguration | None = None,
|
|
2363
|
+
host: Host | None = None,
|
|
2341
2364
|
) -> None:
|
|
2342
2365
|
super().__init__()
|
|
2343
2366
|
|
|
2367
|
+
# Use the initial config or a default
|
|
2368
|
+
config = config or DeviceConfiguration()
|
|
2369
|
+
self.config = config
|
|
2370
|
+
|
|
2344
2371
|
self._host = None
|
|
2345
2372
|
self.powered_on = False
|
|
2346
2373
|
self.auto_restart_inquiry = True
|
|
@@ -2348,7 +2375,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2348
2375
|
self.gatt_server = gatt_server.Server(self)
|
|
2349
2376
|
self.sdp_server = sdp.Server(self)
|
|
2350
2377
|
self.l2cap_channel_manager = l2cap.ChannelManager(
|
|
2351
|
-
|
|
2378
|
+
config.l2cap_extended_features
|
|
2352
2379
|
)
|
|
2353
2380
|
self.advertisement_accumulators = {} # Accumulators, by address
|
|
2354
2381
|
self.periodic_advertising_syncs = []
|
|
@@ -2374,19 +2401,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2374
2401
|
hci.Address.ANY: []
|
|
2375
2402
|
} # Futures, by BD address OR [Futures] for hci.Address.ANY
|
|
2376
2403
|
|
|
2377
|
-
|
|
2378
|
-
if sys.version_info >= (3, 10):
|
|
2379
|
-
self._cis_lock = asyncio.Lock()
|
|
2380
|
-
else:
|
|
2381
|
-
self._cis_lock = AsyncExitStack()
|
|
2404
|
+
self._cis_lock = asyncio.Lock()
|
|
2382
2405
|
|
|
2383
2406
|
# Own address type cache
|
|
2384
2407
|
self.connect_own_address_type = None
|
|
2385
2408
|
|
|
2386
|
-
# Use the initial config or a default
|
|
2387
|
-
config = config or DeviceConfiguration()
|
|
2388
|
-
self.config = config
|
|
2389
|
-
|
|
2390
2409
|
self.name = config.name
|
|
2391
2410
|
self.public_address = hci.Address.ANY
|
|
2392
2411
|
self.random_address = config.address
|
|
@@ -2398,7 +2417,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2398
2417
|
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
|
2399
2418
|
self.le_privacy_enabled = config.le_privacy_enabled
|
|
2400
2419
|
self.le_rpa_timeout = config.le_rpa_timeout
|
|
2401
|
-
self.le_rpa_periodic_update_task:
|
|
2420
|
+
self.le_rpa_periodic_update_task: asyncio.Task | None = None
|
|
2402
2421
|
self.le_subrate_enabled = config.le_subrate_enabled
|
|
2403
2422
|
self.classic_enabled = config.classic_enabled
|
|
2404
2423
|
self.cis_enabled = config.cis_enabled
|
|
@@ -2422,8 +2441,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2422
2441
|
# can be initialized from a config object, and for backward compatibility for
|
|
2423
2442
|
# client code that may set those values directly before calling
|
|
2424
2443
|
# start_advertising().
|
|
2425
|
-
self.legacy_advertising_set:
|
|
2426
|
-
self.legacy_advertiser:
|
|
2444
|
+
self.legacy_advertising_set: AdvertisingSet | None = None
|
|
2445
|
+
self.legacy_advertiser: LegacyAdvertiser | None = None
|
|
2427
2446
|
self.advertising_data = config.advertising_data
|
|
2428
2447
|
self.scan_response_data = config.scan_response_data
|
|
2429
2448
|
self.advertising_interval_min = config.advertising_interval_min
|
|
@@ -2494,7 +2513,10 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2494
2513
|
add_gap_service=config.gap_service_enabled,
|
|
2495
2514
|
add_gatt_service=config.gatt_service_enabled,
|
|
2496
2515
|
)
|
|
2497
|
-
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
|
2516
|
+
self.l2cap_channel_manager.register_fixed_channel(att.ATT_CID, self.on_gatt_pdu)
|
|
2517
|
+
|
|
2518
|
+
if self.config.eatt_enabled:
|
|
2519
|
+
self.gatt_server.register_eatt()
|
|
2498
2520
|
|
|
2499
2521
|
# Forward some events
|
|
2500
2522
|
utils.setup_event_forwarding(
|
|
@@ -2541,7 +2563,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2541
2563
|
def sdp_service_records(self, service_records):
|
|
2542
2564
|
self.sdp_server.service_records = service_records
|
|
2543
2565
|
|
|
2544
|
-
def lookup_connection(self, connection_handle: int) ->
|
|
2566
|
+
def lookup_connection(self, connection_handle: int) -> Connection | None:
|
|
2545
2567
|
if connection := self.connections.get(connection_handle):
|
|
2546
2568
|
return connection
|
|
2547
2569
|
|
|
@@ -2550,9 +2572,9 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2550
2572
|
def find_connection_by_bd_addr(
|
|
2551
2573
|
self,
|
|
2552
2574
|
bd_addr: hci.Address,
|
|
2553
|
-
transport:
|
|
2575
|
+
transport: int | None = None,
|
|
2554
2576
|
check_address_type: bool = False,
|
|
2555
|
-
) ->
|
|
2577
|
+
) -> Connection | None:
|
|
2556
2578
|
for connection in self.connections.values():
|
|
2557
2579
|
if bytes(connection.peer_address) == bytes(bd_addr):
|
|
2558
2580
|
if (
|
|
@@ -2567,7 +2589,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2567
2589
|
|
|
2568
2590
|
def lookup_periodic_advertising_sync(
|
|
2569
2591
|
self, sync_handle: int
|
|
2570
|
-
) ->
|
|
2592
|
+
) -> PeriodicAdvertisingSync | None:
|
|
2571
2593
|
return next(
|
|
2572
2594
|
(
|
|
2573
2595
|
sync
|
|
@@ -2605,8 +2627,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2605
2627
|
async def create_l2cap_channel(
|
|
2606
2628
|
self,
|
|
2607
2629
|
connection: Connection,
|
|
2608
|
-
spec:
|
|
2609
|
-
) ->
|
|
2630
|
+
spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec,
|
|
2631
|
+
) -> l2cap.ClassicChannel | l2cap.LeCreditBasedChannel:
|
|
2610
2632
|
if isinstance(spec, l2cap.ClassicChannelSpec):
|
|
2611
2633
|
return await self.l2cap_channel_manager.create_classic_channel(
|
|
2612
2634
|
connection=connection, spec=spec
|
|
@@ -2620,25 +2642,25 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2620
2642
|
def create_l2cap_server(
|
|
2621
2643
|
self,
|
|
2622
2644
|
spec: l2cap.ClassicChannelSpec,
|
|
2623
|
-
handler:
|
|
2645
|
+
handler: Callable[[l2cap.ClassicChannel], Any] | None = None,
|
|
2624
2646
|
) -> l2cap.ClassicChannelServer: ...
|
|
2625
2647
|
|
|
2626
2648
|
@overload
|
|
2627
2649
|
def create_l2cap_server(
|
|
2628
2650
|
self,
|
|
2629
2651
|
spec: l2cap.LeCreditBasedChannelSpec,
|
|
2630
|
-
handler:
|
|
2652
|
+
handler: Callable[[l2cap.LeCreditBasedChannel], Any] | None = None,
|
|
2631
2653
|
) -> l2cap.LeCreditBasedChannelServer: ...
|
|
2632
2654
|
|
|
2633
2655
|
def create_l2cap_server(
|
|
2634
2656
|
self,
|
|
2635
|
-
spec:
|
|
2636
|
-
handler:
|
|
2637
|
-
Callable[[l2cap.ClassicChannel], Any]
|
|
2638
|
-
Callable[[l2cap.LeCreditBasedChannel], Any]
|
|
2639
|
-
None
|
|
2640
|
-
|
|
2641
|
-
) ->
|
|
2657
|
+
spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec,
|
|
2658
|
+
handler: (
|
|
2659
|
+
Callable[[l2cap.ClassicChannel], Any]
|
|
2660
|
+
| Callable[[l2cap.LeCreditBasedChannel], Any]
|
|
2661
|
+
| None
|
|
2662
|
+
) = None,
|
|
2663
|
+
) -> l2cap.ClassicChannelServer | l2cap.LeCreditBasedChannelServer:
|
|
2642
2664
|
if isinstance(spec, l2cap.ClassicChannelSpec):
|
|
2643
2665
|
return self.l2cap_channel_manager.create_classic_server(
|
|
2644
2666
|
spec=spec,
|
|
@@ -2887,7 +2909,9 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2887
2909
|
self.address_resolver = smp.AddressResolver(resolving_keys)
|
|
2888
2910
|
|
|
2889
2911
|
if self.address_resolution_offload or self.address_generation_offload:
|
|
2890
|
-
await self.send_command(
|
|
2912
|
+
await self.send_command(
|
|
2913
|
+
hci.HCI_LE_Clear_Resolving_List_Command(), check_result=True
|
|
2914
|
+
)
|
|
2891
2915
|
|
|
2892
2916
|
# Add an empty entry for non-directed address generation.
|
|
2893
2917
|
await self.send_command(
|
|
@@ -2896,7 +2920,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2896
2920
|
peer_identity_address=hci.Address.ANY,
|
|
2897
2921
|
peer_irk=bytes(16),
|
|
2898
2922
|
local_irk=self.irk,
|
|
2899
|
-
)
|
|
2923
|
+
),
|
|
2924
|
+
check_result=True,
|
|
2900
2925
|
)
|
|
2901
2926
|
|
|
2902
2927
|
for irk, address in resolving_keys:
|
|
@@ -2906,7 +2931,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2906
2931
|
peer_identity_address=address,
|
|
2907
2932
|
peer_irk=irk,
|
|
2908
2933
|
local_irk=self.irk,
|
|
2909
|
-
)
|
|
2934
|
+
),
|
|
2935
|
+
check_result=True,
|
|
2910
2936
|
)
|
|
2911
2937
|
|
|
2912
2938
|
def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
|
|
@@ -2936,13 +2962,13 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2936
2962
|
async def start_advertising(
|
|
2937
2963
|
self,
|
|
2938
2964
|
advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
|
2939
|
-
target:
|
|
2965
|
+
target: hci.Address | None = None,
|
|
2940
2966
|
own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
|
|
2941
2967
|
auto_restart: bool = False,
|
|
2942
|
-
advertising_data:
|
|
2943
|
-
scan_response_data:
|
|
2944
|
-
advertising_interval_min:
|
|
2945
|
-
advertising_interval_max:
|
|
2968
|
+
advertising_data: bytes | None = None,
|
|
2969
|
+
scan_response_data: bytes | None = None,
|
|
2970
|
+
advertising_interval_min: float | None = None,
|
|
2971
|
+
advertising_interval_max: float | None = None,
|
|
2946
2972
|
) -> None:
|
|
2947
2973
|
"""Start legacy advertising.
|
|
2948
2974
|
|
|
@@ -3046,11 +3072,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3046
3072
|
|
|
3047
3073
|
async def create_advertising_set(
|
|
3048
3074
|
self,
|
|
3049
|
-
advertising_parameters:
|
|
3050
|
-
random_address:
|
|
3075
|
+
advertising_parameters: AdvertisingParameters | None = None,
|
|
3076
|
+
random_address: hci.Address | None = None,
|
|
3051
3077
|
advertising_data: bytes = b'',
|
|
3052
3078
|
scan_response_data: bytes = b'',
|
|
3053
|
-
periodic_advertising_parameters:
|
|
3079
|
+
periodic_advertising_parameters: PeriodicAdvertisingParameters | None = None,
|
|
3054
3080
|
periodic_advertising_data: bytes = b'',
|
|
3055
3081
|
auto_start: bool = True,
|
|
3056
3082
|
auto_restart: bool = False,
|
|
@@ -3337,7 +3363,13 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3337
3363
|
return self.scanning
|
|
3338
3364
|
|
|
3339
3365
|
@host_event_handler
|
|
3340
|
-
def on_advertising_report(
|
|
3366
|
+
def on_advertising_report(
|
|
3367
|
+
self,
|
|
3368
|
+
report: (
|
|
3369
|
+
hci.HCI_LE_Advertising_Report_Event.Report
|
|
3370
|
+
| hci.HCI_LE_Extended_Advertising_Report_Event.Report
|
|
3371
|
+
),
|
|
3372
|
+
) -> None:
|
|
3341
3373
|
if not (accumulator := self.advertisement_accumulators.get(report.address)):
|
|
3342
3374
|
accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive)
|
|
3343
3375
|
self.advertisement_accumulators[report.address] = accumulator
|
|
@@ -3501,16 +3533,15 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3501
3533
|
check_result=True,
|
|
3502
3534
|
)
|
|
3503
3535
|
|
|
3504
|
-
|
|
3536
|
+
self.discovering = False
|
|
3537
|
+
await self.send_command(
|
|
3505
3538
|
hci.HCI_Inquiry_Command(
|
|
3506
3539
|
lap=hci.HCI_GENERAL_INQUIRY_LAP,
|
|
3507
3540
|
inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
|
|
3508
3541
|
num_responses=0, # Unlimited number of responses.
|
|
3509
|
-
)
|
|
3542
|
+
),
|
|
3543
|
+
check_result=True,
|
|
3510
3544
|
)
|
|
3511
|
-
if response.status != hci.HCI_Command_Status_Event.PENDING:
|
|
3512
|
-
self.discovering = False
|
|
3513
|
-
raise hci.HCI_StatusError(response)
|
|
3514
3545
|
|
|
3515
3546
|
self.auto_restart_inquiry = auto_restart
|
|
3516
3547
|
self.discovering = True
|
|
@@ -3546,7 +3577,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3546
3577
|
scan_enable = 0x00
|
|
3547
3578
|
|
|
3548
3579
|
return await self.send_command(
|
|
3549
|
-
hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable)
|
|
3580
|
+
hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable),
|
|
3581
|
+
check_result=True,
|
|
3550
3582
|
)
|
|
3551
3583
|
|
|
3552
3584
|
async def set_discoverable(self, discoverable: bool = True) -> None:
|
|
@@ -3580,13 +3612,13 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3580
3612
|
|
|
3581
3613
|
async def connect(
|
|
3582
3614
|
self,
|
|
3583
|
-
peer_address:
|
|
3615
|
+
peer_address: hci.Address | str,
|
|
3584
3616
|
transport: core.PhysicalTransport = PhysicalTransport.LE,
|
|
3585
|
-
connection_parameters_preferences:
|
|
3586
|
-
dict[hci.Phy, ConnectionParametersPreferences]
|
|
3587
|
-
|
|
3617
|
+
connection_parameters_preferences: (
|
|
3618
|
+
dict[hci.Phy, ConnectionParametersPreferences] | None
|
|
3619
|
+
) = None,
|
|
3588
3620
|
own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
|
|
3589
|
-
timeout:
|
|
3621
|
+
timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
|
3590
3622
|
always_resolve: bool = False,
|
|
3591
3623
|
) -> Connection:
|
|
3592
3624
|
'''
|
|
@@ -3775,7 +3807,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3775
3807
|
for phy in phys
|
|
3776
3808
|
]
|
|
3777
3809
|
|
|
3778
|
-
|
|
3810
|
+
await self.send_command(
|
|
3779
3811
|
hci.HCI_LE_Extended_Create_Connection_Command(
|
|
3780
3812
|
initiator_filter_policy=0,
|
|
3781
3813
|
own_address_type=own_address_type,
|
|
@@ -3796,14 +3828,15 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3796
3828
|
supervision_timeouts=supervision_timeouts,
|
|
3797
3829
|
min_ce_lengths=min_ce_lengths,
|
|
3798
3830
|
max_ce_lengths=max_ce_lengths,
|
|
3799
|
-
)
|
|
3831
|
+
),
|
|
3832
|
+
check_result=True,
|
|
3800
3833
|
)
|
|
3801
3834
|
else:
|
|
3802
3835
|
if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
|
|
3803
3836
|
raise InvalidArgumentError('1M PHY preferences required')
|
|
3804
3837
|
|
|
3805
3838
|
prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
|
|
3806
|
-
|
|
3839
|
+
await self.send_command(
|
|
3807
3840
|
hci.HCI_LE_Create_Connection_Command(
|
|
3808
3841
|
le_scan_interval=int(
|
|
3809
3842
|
DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
|
|
@@ -3825,7 +3858,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3825
3858
|
supervision_timeout=int(prefs.supervision_timeout / 10),
|
|
3826
3859
|
min_ce_length=int(prefs.min_ce_length / 0.625),
|
|
3827
3860
|
max_ce_length=int(prefs.max_ce_length / 0.625),
|
|
3828
|
-
)
|
|
3861
|
+
),
|
|
3862
|
+
check_result=True,
|
|
3829
3863
|
)
|
|
3830
3864
|
else:
|
|
3831
3865
|
# Save pending connection
|
|
@@ -3842,7 +3876,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3842
3876
|
)
|
|
3843
3877
|
|
|
3844
3878
|
# TODO: allow passing other settings
|
|
3845
|
-
|
|
3879
|
+
await self.send_command(
|
|
3846
3880
|
hci.HCI_Create_Connection_Command(
|
|
3847
3881
|
bd_addr=peer_address,
|
|
3848
3882
|
packet_type=0xCC18, # FIXME: change
|
|
@@ -3850,12 +3884,10 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3850
3884
|
clock_offset=0x0000,
|
|
3851
3885
|
allow_role_switch=0x01,
|
|
3852
3886
|
reserved=0,
|
|
3853
|
-
)
|
|
3887
|
+
),
|
|
3888
|
+
check_result=True,
|
|
3854
3889
|
)
|
|
3855
3890
|
|
|
3856
|
-
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
|
3857
|
-
raise hci.HCI_StatusError(result)
|
|
3858
|
-
|
|
3859
3891
|
# Wait for the connection process to complete
|
|
3860
3892
|
if transport == PhysicalTransport.LE:
|
|
3861
3893
|
self.le_connecting = True
|
|
@@ -3896,9 +3928,9 @@ class Device(utils.CompositeEventEmitter):
|
|
|
3896
3928
|
|
|
3897
3929
|
async def accept(
|
|
3898
3930
|
self,
|
|
3899
|
-
peer_address:
|
|
3931
|
+
peer_address: hci.Address | str = hci.Address.ANY,
|
|
3900
3932
|
role: hci.Role = hci.Role.PERIPHERAL,
|
|
3901
|
-
timeout:
|
|
3933
|
+
timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
|
3902
3934
|
) -> Connection:
|
|
3903
3935
|
'''
|
|
3904
3936
|
Wait and accept any incoming connection or a connection from `peer_address` when
|
|
@@ -4007,7 +4039,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4007
4039
|
await self.send_command(
|
|
4008
4040
|
hci.HCI_Accept_Connection_Request_Command(
|
|
4009
4041
|
bd_addr=peer_address, role=role
|
|
4010
|
-
)
|
|
4042
|
+
),
|
|
4043
|
+
check_result=True,
|
|
4011
4044
|
)
|
|
4012
4045
|
|
|
4013
4046
|
# Wait for connection complete
|
|
@@ -4021,7 +4054,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4021
4054
|
self.pending_connections.pop(peer_address, None)
|
|
4022
4055
|
|
|
4023
4056
|
@asynccontextmanager
|
|
4024
|
-
async def connect_as_gatt(self, peer_address:
|
|
4057
|
+
async def connect_as_gatt(self, peer_address: hci.Address | str):
|
|
4025
4058
|
async with AsyncExitStack() as stack:
|
|
4026
4059
|
connection = await stack.enter_async_context(
|
|
4027
4060
|
await self.connect(peer_address)
|
|
@@ -4068,7 +4101,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4068
4101
|
)
|
|
4069
4102
|
|
|
4070
4103
|
async def disconnect(
|
|
4071
|
-
self, connection:
|
|
4104
|
+
self, connection: Connection | ScoLink | CisLink, reason: int
|
|
4072
4105
|
) -> None:
|
|
4073
4106
|
# Create a future so that we can wait for the disconnection's result
|
|
4074
4107
|
pending_disconnection = asyncio.get_running_loop().create_future()
|
|
@@ -4077,19 +4110,17 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4077
4110
|
connection.EVENT_DISCONNECTION_FAILURE, pending_disconnection.set_exception
|
|
4078
4111
|
)
|
|
4079
4112
|
|
|
4080
|
-
# Request a disconnection
|
|
4081
|
-
result = await self.send_command(
|
|
4082
|
-
hci.HCI_Disconnect_Command(
|
|
4083
|
-
connection_handle=connection.handle, reason=reason
|
|
4084
|
-
)
|
|
4085
|
-
)
|
|
4086
|
-
|
|
4087
4113
|
try:
|
|
4088
|
-
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
|
4089
|
-
raise hci.HCI_StatusError(result)
|
|
4090
|
-
|
|
4091
4114
|
# Wait for the disconnection process to complete
|
|
4092
4115
|
self.disconnecting = True
|
|
4116
|
+
|
|
4117
|
+
# Request a disconnection
|
|
4118
|
+
await self.send_command(
|
|
4119
|
+
hci.HCI_Disconnect_Command(
|
|
4120
|
+
connection_handle=connection.handle, reason=reason
|
|
4121
|
+
),
|
|
4122
|
+
check_result=True,
|
|
4123
|
+
)
|
|
4093
4124
|
return await utils.cancel_on_event(
|
|
4094
4125
|
self, Device.EVENT_FLUSH, pending_disconnection
|
|
4095
4126
|
)
|
|
@@ -4175,7 +4206,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4175
4206
|
|
|
4176
4207
|
return
|
|
4177
4208
|
|
|
4178
|
-
|
|
4209
|
+
await self.send_command(
|
|
4179
4210
|
hci.HCI_LE_Connection_Update_Command(
|
|
4180
4211
|
connection_handle=connection.handle,
|
|
4181
4212
|
connection_interval_min=connection_interval_min,
|
|
@@ -4184,10 +4215,9 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4184
4215
|
supervision_timeout=supervision_timeout,
|
|
4185
4216
|
min_ce_length=min_ce_length,
|
|
4186
4217
|
max_ce_length=max_ce_length,
|
|
4187
|
-
)
|
|
4218
|
+
),
|
|
4219
|
+
check_result=True,
|
|
4188
4220
|
)
|
|
4189
|
-
if result.status != hci.HCI_Command_Status_Event.PENDING:
|
|
4190
|
-
raise hci.HCI_StatusError(result)
|
|
4191
4221
|
|
|
4192
4222
|
async def get_connection_rssi(self, connection):
|
|
4193
4223
|
result = await self.send_command(
|
|
@@ -4210,8 +4240,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4210
4240
|
async def set_connection_phy(
|
|
4211
4241
|
self,
|
|
4212
4242
|
connection: Connection,
|
|
4213
|
-
tx_phys:
|
|
4214
|
-
rx_phys:
|
|
4243
|
+
tx_phys: Iterable[hci.Phy] | None = None,
|
|
4244
|
+
rx_phys: Iterable[hci.Phy] | None = None,
|
|
4215
4245
|
phy_options: int = 0,
|
|
4216
4246
|
):
|
|
4217
4247
|
if not self.host.supports_command(hci.HCI_LE_SET_PHY_COMMAND):
|
|
@@ -4222,27 +4252,21 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4222
4252
|
(1 if rx_phys is None else 0) << 1
|
|
4223
4253
|
)
|
|
4224
4254
|
|
|
4225
|
-
|
|
4255
|
+
await self.send_command(
|
|
4226
4256
|
hci.HCI_LE_Set_PHY_Command(
|
|
4227
4257
|
connection_handle=connection.handle,
|
|
4228
4258
|
all_phys=all_phys_bits,
|
|
4229
4259
|
tx_phys=hci.phy_list_to_bits(tx_phys),
|
|
4230
4260
|
rx_phys=hci.phy_list_to_bits(rx_phys),
|
|
4231
4261
|
phy_options=phy_options,
|
|
4232
|
-
)
|
|
4262
|
+
),
|
|
4263
|
+
check_result=True,
|
|
4233
4264
|
)
|
|
4234
4265
|
|
|
4235
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4236
|
-
logger.warning(
|
|
4237
|
-
'HCI_LE_Set_PHY_Command failed: '
|
|
4238
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4239
|
-
)
|
|
4240
|
-
raise hci.HCI_StatusError(result)
|
|
4241
|
-
|
|
4242
4266
|
async def set_default_phy(
|
|
4243
4267
|
self,
|
|
4244
|
-
tx_phys:
|
|
4245
|
-
rx_phys:
|
|
4268
|
+
tx_phys: Iterable[hci.Phy] | None = None,
|
|
4269
|
+
rx_phys: Iterable[hci.Phy] | None = None,
|
|
4246
4270
|
):
|
|
4247
4271
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
|
4248
4272
|
(1 if rx_phys is None else 0) << 1
|
|
@@ -4296,7 +4320,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4296
4320
|
if local_name == name:
|
|
4297
4321
|
peer_address.set_result(address)
|
|
4298
4322
|
|
|
4299
|
-
listener:
|
|
4323
|
+
listener: Callable[..., None] | None = None
|
|
4300
4324
|
was_scanning = self.scanning
|
|
4301
4325
|
was_discovering = self.discovering
|
|
4302
4326
|
try:
|
|
@@ -4410,7 +4434,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4410
4434
|
|
|
4411
4435
|
async def get_long_term_key(
|
|
4412
4436
|
self, connection_handle: int, rand: bytes, ediv: int
|
|
4413
|
-
) ->
|
|
4437
|
+
) -> bytes | None:
|
|
4414
4438
|
if (connection := self.lookup_connection(connection_handle)) is None:
|
|
4415
4439
|
return None
|
|
4416
4440
|
|
|
@@ -4434,7 +4458,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4434
4458
|
return keys.ltk_peripheral.value
|
|
4435
4459
|
return None
|
|
4436
4460
|
|
|
4437
|
-
async def get_link_key(self, address: hci.Address) ->
|
|
4461
|
+
async def get_link_key(self, address: hci.Address) -> bytes | None:
|
|
4438
4462
|
if self.keystore is None:
|
|
4439
4463
|
return None
|
|
4440
4464
|
|
|
@@ -4455,43 +4479,26 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4455
4479
|
async def authenticate(self, connection: Connection) -> None:
|
|
4456
4480
|
# Set up event handlers
|
|
4457
4481
|
pending_authentication = asyncio.get_running_loop().create_future()
|
|
4482
|
+
with closing(utils.EventWatcher()) as watcher:
|
|
4458
4483
|
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
def on_authentication_failure(error_code):
|
|
4463
|
-
pending_authentication.set_exception(hci.HCI_Error(error_code))
|
|
4484
|
+
@watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION)
|
|
4485
|
+
def on_authentication() -> None:
|
|
4486
|
+
pending_authentication.set_result(None)
|
|
4464
4487
|
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
on_authentication_failure,
|
|
4469
|
-
)
|
|
4488
|
+
@watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE)
|
|
4489
|
+
def on_authentication_failure(error_code: int) -> None:
|
|
4490
|
+
pending_authentication.set_exception(hci.HCI_Error(error_code))
|
|
4470
4491
|
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
result = await self.send_command(
|
|
4492
|
+
# Request the authentication
|
|
4493
|
+
await self.send_command(
|
|
4474
4494
|
hci.HCI_Authentication_Requested_Command(
|
|
4475
4495
|
connection_handle=connection.handle
|
|
4476
|
-
)
|
|
4496
|
+
),
|
|
4497
|
+
check_result=True,
|
|
4477
4498
|
)
|
|
4478
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4479
|
-
logger.warning(
|
|
4480
|
-
'HCI_Authentication_Requested_Command failed: '
|
|
4481
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4482
|
-
)
|
|
4483
|
-
raise hci.HCI_StatusError(result)
|
|
4484
4499
|
|
|
4485
4500
|
# Wait for the authentication to complete
|
|
4486
4501
|
await connection.cancel_on_disconnection(pending_authentication)
|
|
4487
|
-
finally:
|
|
4488
|
-
connection.remove_listener(
|
|
4489
|
-
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
|
|
4490
|
-
)
|
|
4491
|
-
connection.remove_listener(
|
|
4492
|
-
connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
|
|
4493
|
-
on_authentication_failure,
|
|
4494
|
-
)
|
|
4495
4502
|
|
|
4496
4503
|
async def encrypt(self, connection: Connection, enable: bool = True):
|
|
4497
4504
|
if not enable and connection.transport == PhysicalTransport.LE:
|
|
@@ -4500,21 +4507,17 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4500
4507
|
# Set up event handlers
|
|
4501
4508
|
pending_encryption = asyncio.get_running_loop().create_future()
|
|
4502
4509
|
|
|
4503
|
-
|
|
4504
|
-
|
|
4510
|
+
# Request the encryption
|
|
4511
|
+
with closing(utils.EventWatcher()) as watcher:
|
|
4505
4512
|
|
|
4506
|
-
|
|
4507
|
-
|
|
4513
|
+
@watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
|
4514
|
+
def _() -> None:
|
|
4515
|
+
pending_encryption.set_result(None)
|
|
4508
4516
|
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
connection.on(
|
|
4513
|
-
connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
|
|
4514
|
-
)
|
|
4517
|
+
@watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_FAILURE)
|
|
4518
|
+
def _(error_code: int):
|
|
4519
|
+
pending_encryption.set_exception(hci.HCI_Error(error_code))
|
|
4515
4520
|
|
|
4516
|
-
# Request the encryption
|
|
4517
|
-
try:
|
|
4518
4521
|
if connection.transport == PhysicalTransport.LE:
|
|
4519
4522
|
# Look for a key in the key store
|
|
4520
4523
|
if self.keystore is None:
|
|
@@ -4531,53 +4534,34 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4531
4534
|
ediv = 0
|
|
4532
4535
|
elif keys.ltk_central is not None:
|
|
4533
4536
|
ltk = keys.ltk_central.value
|
|
4534
|
-
rand = keys.ltk_central.rand
|
|
4535
|
-
ediv = keys.ltk_central.ediv
|
|
4537
|
+
rand = keys.ltk_central.rand or b''
|
|
4538
|
+
ediv = keys.ltk_central.ediv or 0
|
|
4536
4539
|
else:
|
|
4537
4540
|
raise InvalidOperationError('no LTK found for peer')
|
|
4538
4541
|
|
|
4539
4542
|
if connection.role != hci.Role.CENTRAL:
|
|
4540
4543
|
raise InvalidStateError('only centrals can start encryption')
|
|
4541
4544
|
|
|
4542
|
-
|
|
4545
|
+
await self.send_command(
|
|
4543
4546
|
hci.HCI_LE_Enable_Encryption_Command(
|
|
4544
4547
|
connection_handle=connection.handle,
|
|
4545
4548
|
random_number=rand,
|
|
4546
4549
|
encrypted_diversifier=ediv,
|
|
4547
4550
|
long_term_key=ltk,
|
|
4548
|
-
)
|
|
4551
|
+
),
|
|
4552
|
+
check_result=True,
|
|
4549
4553
|
)
|
|
4550
|
-
|
|
4551
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4552
|
-
logger.warning(
|
|
4553
|
-
'HCI_LE_Enable_Encryption_Command failed: '
|
|
4554
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4555
|
-
)
|
|
4556
|
-
raise hci.HCI_StatusError(result)
|
|
4557
4554
|
else:
|
|
4558
|
-
|
|
4555
|
+
await self.send_command(
|
|
4559
4556
|
hci.HCI_Set_Connection_Encryption_Command(
|
|
4560
4557
|
connection_handle=connection.handle,
|
|
4561
4558
|
encryption_enable=0x01 if enable else 0x00,
|
|
4562
|
-
)
|
|
4559
|
+
),
|
|
4560
|
+
check_result=True,
|
|
4563
4561
|
)
|
|
4564
4562
|
|
|
4565
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4566
|
-
logger.warning(
|
|
4567
|
-
'HCI_Set_Connection_Encryption_Command failed: '
|
|
4568
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4569
|
-
)
|
|
4570
|
-
raise hci.HCI_StatusError(result)
|
|
4571
|
-
|
|
4572
4563
|
# Wait for the result
|
|
4573
4564
|
await connection.cancel_on_disconnection(pending_encryption)
|
|
4574
|
-
finally:
|
|
4575
|
-
connection.remove_listener(
|
|
4576
|
-
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
|
4577
|
-
)
|
|
4578
|
-
connection.remove_listener(
|
|
4579
|
-
connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
|
|
4580
|
-
)
|
|
4581
4565
|
|
|
4582
4566
|
async def update_keys(self, address: str, keys: PairingKeys) -> None:
|
|
4583
4567
|
if self.keystore is None:
|
|
@@ -4595,80 +4579,55 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4595
4579
|
async def switch_role(self, connection: Connection, role: hci.Role):
|
|
4596
4580
|
pending_role_change = asyncio.get_running_loop().create_future()
|
|
4597
4581
|
|
|
4598
|
-
|
|
4599
|
-
pending_role_change.set_result(new_role)
|
|
4582
|
+
with closing(utils.EventWatcher()) as watcher:
|
|
4600
4583
|
|
|
4601
|
-
|
|
4602
|
-
|
|
4584
|
+
@watcher.on(connection, connection.EVENT_ROLE_CHANGE)
|
|
4585
|
+
def _(new_role: hci.Role):
|
|
4586
|
+
pending_role_change.set_result(new_role)
|
|
4603
4587
|
|
|
4604
|
-
|
|
4605
|
-
|
|
4588
|
+
@watcher.on(connection, connection.EVENT_ROLE_CHANGE_FAILURE)
|
|
4589
|
+
def _(error_code: int):
|
|
4590
|
+
pending_role_change.set_exception(hci.HCI_Error(error_code))
|
|
4606
4591
|
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4592
|
+
await self.send_command(
|
|
4593
|
+
hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role),
|
|
4594
|
+
check_result=True,
|
|
4610
4595
|
)
|
|
4611
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4612
|
-
logger.warning(
|
|
4613
|
-
'HCI_Switch_Role_Command failed: '
|
|
4614
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4615
|
-
)
|
|
4616
|
-
raise hci.HCI_StatusError(result)
|
|
4617
4596
|
await connection.cancel_on_disconnection(pending_role_change)
|
|
4618
|
-
finally:
|
|
4619
|
-
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
|
4620
|
-
connection.remove_listener(
|
|
4621
|
-
connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure
|
|
4622
|
-
)
|
|
4623
4597
|
|
|
4624
4598
|
# [Classic only]
|
|
4625
|
-
async def request_remote_name(self, remote:
|
|
4599
|
+
async def request_remote_name(self, remote: hci.Address | Connection) -> str:
|
|
4626
4600
|
# Set up event handlers
|
|
4627
|
-
pending_name = asyncio.get_running_loop().create_future()
|
|
4601
|
+
pending_name: asyncio.Future[str] = asyncio.get_running_loop().create_future()
|
|
4628
4602
|
|
|
4629
4603
|
peer_address = (
|
|
4630
4604
|
remote if isinstance(remote, hci.Address) else remote.peer_address
|
|
4631
4605
|
)
|
|
4632
4606
|
|
|
4633
|
-
|
|
4634
|
-
self.EVENT_REMOTE_NAME,
|
|
4635
|
-
lambda address, remote_name: (
|
|
4636
|
-
pending_name.set_result(remote_name)
|
|
4637
|
-
if address == peer_address
|
|
4638
|
-
else None
|
|
4639
|
-
),
|
|
4640
|
-
)
|
|
4641
|
-
failure_handler = self.on(
|
|
4642
|
-
self.EVENT_REMOTE_NAME_FAILURE,
|
|
4643
|
-
lambda address, error_code: (
|
|
4644
|
-
pending_name.set_exception(hci.HCI_Error(error_code))
|
|
4645
|
-
if address == peer_address
|
|
4646
|
-
else None
|
|
4647
|
-
),
|
|
4648
|
-
)
|
|
4607
|
+
with closing(utils.EventWatcher()) as watcher:
|
|
4649
4608
|
|
|
4650
|
-
|
|
4651
|
-
|
|
4609
|
+
@watcher.on(self, self.EVENT_REMOTE_NAME)
|
|
4610
|
+
def _(address: hci.Address, remote_name: str) -> None:
|
|
4611
|
+
if address == peer_address:
|
|
4612
|
+
pending_name.set_result(remote_name)
|
|
4613
|
+
|
|
4614
|
+
@watcher.on(self, self.EVENT_REMOTE_NAME_FAILURE)
|
|
4615
|
+
def _(address: hci.Address, error_code: int) -> None:
|
|
4616
|
+
if address == peer_address:
|
|
4617
|
+
pending_name.set_exception(hci.HCI_Error(error_code))
|
|
4618
|
+
|
|
4619
|
+
await self.send_command(
|
|
4652
4620
|
hci.HCI_Remote_Name_Request_Command(
|
|
4653
4621
|
bd_addr=peer_address,
|
|
4654
4622
|
page_scan_repetition_mode=hci.HCI_Remote_Name_Request_Command.R2,
|
|
4655
4623
|
reserved=0,
|
|
4656
4624
|
clock_offset=0, # TODO investigate non-0 values
|
|
4657
|
-
)
|
|
4625
|
+
),
|
|
4626
|
+
check_result=True,
|
|
4658
4627
|
)
|
|
4659
4628
|
|
|
4660
|
-
if result.status != hci.HCI_COMMAND_STATUS_PENDING:
|
|
4661
|
-
logger.warning(
|
|
4662
|
-
'HCI_Remote_Name_Request_Command failed: '
|
|
4663
|
-
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4664
|
-
)
|
|
4665
|
-
raise hci.HCI_StatusError(result)
|
|
4666
|
-
|
|
4667
4629
|
# Wait for the result
|
|
4668
4630
|
return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name)
|
|
4669
|
-
finally:
|
|
4670
|
-
self.remove_listener(self.EVENT_REMOTE_NAME, handler)
|
|
4671
|
-
self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
|
|
4672
4631
|
|
|
4673
4632
|
# [LE only]
|
|
4674
4633
|
@utils.experimental('Only for testing.')
|
|
@@ -4684,8 +4643,6 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4684
4643
|
Returns:
|
|
4685
4644
|
List of created CIS handles corresponding to the same order of [cid_id].
|
|
4686
4645
|
"""
|
|
4687
|
-
num_cis = len(parameters.cis_parameters)
|
|
4688
|
-
|
|
4689
4646
|
response = await self.send_command(
|
|
4690
4647
|
hci.HCI_LE_Set_CIG_Parameters_Command(
|
|
4691
4648
|
cig_id=parameters.cig_id,
|
|
@@ -5202,14 +5159,18 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5202
5159
|
if add_gap_service:
|
|
5203
5160
|
self.gatt_server.add_service(GenericAccessService(self.name))
|
|
5204
5161
|
if add_gatt_service:
|
|
5205
|
-
self.gatt_service = gatt_service.GenericAttributeProfileService(
|
|
5162
|
+
self.gatt_service = gatt_service.GenericAttributeProfileService(
|
|
5163
|
+
gatt.ServerSupportedFeatures.EATT_SUPPORTED
|
|
5164
|
+
if self.config.eatt_enabled
|
|
5165
|
+
else None
|
|
5166
|
+
)
|
|
5206
5167
|
self.gatt_server.add_service(self.gatt_service)
|
|
5207
5168
|
|
|
5208
5169
|
async def notify_subscriber(
|
|
5209
5170
|
self,
|
|
5210
5171
|
connection: Connection,
|
|
5211
5172
|
attribute: Attribute,
|
|
5212
|
-
value:
|
|
5173
|
+
value: Any | None = None,
|
|
5213
5174
|
force: bool = False,
|
|
5214
5175
|
) -> None:
|
|
5215
5176
|
"""
|
|
@@ -5228,7 +5189,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5228
5189
|
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
|
5229
5190
|
|
|
5230
5191
|
async def notify_subscribers(
|
|
5231
|
-
self, attribute: Attribute, value=None, force=False
|
|
5192
|
+
self, attribute: Attribute, value: Any | None = None, force: bool = False
|
|
5232
5193
|
) -> None:
|
|
5233
5194
|
"""
|
|
5234
5195
|
Send a notification to all the subscribers of an attribute.
|
|
@@ -5248,7 +5209,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5248
5209
|
self,
|
|
5249
5210
|
connection: Connection,
|
|
5250
5211
|
attribute: Attribute,
|
|
5251
|
-
value:
|
|
5212
|
+
value: Any | None = None,
|
|
5252
5213
|
force: bool = False,
|
|
5253
5214
|
):
|
|
5254
5215
|
"""
|
|
@@ -5269,7 +5230,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5269
5230
|
await self.gatt_server.indicate_subscriber(connection, attribute, value, force)
|
|
5270
5231
|
|
|
5271
5232
|
async def indicate_subscribers(
|
|
5272
|
-
self, attribute: Attribute, value:
|
|
5233
|
+
self, attribute: Attribute, value: Any | None = None, force: bool = False
|
|
5273
5234
|
):
|
|
5274
5235
|
"""
|
|
5275
5236
|
Send an indication to all the subscribers of an attribute.
|
|
@@ -5304,8 +5265,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5304
5265
|
|
|
5305
5266
|
if status != hci.HCI_SUCCESS:
|
|
5306
5267
|
logger.debug(
|
|
5307
|
-
f'advertising set {advertising_handle} '
|
|
5308
|
-
f'terminated with status {status}'
|
|
5268
|
+
f'advertising set {advertising_handle} terminated with status {status}'
|
|
5309
5269
|
)
|
|
5310
5270
|
return
|
|
5311
5271
|
|
|
@@ -5494,8 +5454,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5494
5454
|
self,
|
|
5495
5455
|
connection_handle: int,
|
|
5496
5456
|
peer_address: hci.Address,
|
|
5497
|
-
self_resolvable_address:
|
|
5498
|
-
peer_resolvable_address:
|
|
5457
|
+
self_resolvable_address: hci.Address | None,
|
|
5458
|
+
peer_resolvable_address: hci.Address | None,
|
|
5499
5459
|
role: hci.Role,
|
|
5500
5460
|
connection_interval: int,
|
|
5501
5461
|
peripheral_latency: int,
|
|
@@ -5530,7 +5490,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5530
5490
|
peer_address = resolved_address
|
|
5531
5491
|
|
|
5532
5492
|
self_address = None
|
|
5533
|
-
own_address_type:
|
|
5493
|
+
own_address_type: hci.OwnAddressType | None = None
|
|
5534
5494
|
if role == hci.Role.CENTRAL:
|
|
5535
5495
|
own_address_type = self.connect_own_address_type
|
|
5536
5496
|
assert own_address_type is not None
|
|
@@ -5684,7 +5644,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5684
5644
|
|
|
5685
5645
|
self.host.send_command_sync(
|
|
5686
5646
|
hci.HCI_Accept_Connection_Request_Command(
|
|
5687
|
-
bd_addr=bd_addr,
|
|
5647
|
+
bd_addr=bd_addr,
|
|
5648
|
+
role=0x01, # Remain the peripheral
|
|
5688
5649
|
)
|
|
5689
5650
|
)
|
|
5690
5651
|
|
|
@@ -5753,9 +5714,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5753
5714
|
|
|
5754
5715
|
@host_event_handler
|
|
5755
5716
|
@with_connection_from_handle
|
|
5756
|
-
def on_connection_authentication_failure(
|
|
5757
|
-
self, connection: Connection, error: core.ConnectionError
|
|
5758
|
-
):
|
|
5717
|
+
def on_connection_authentication_failure(self, connection: Connection, error: int):
|
|
5759
5718
|
logger.debug(
|
|
5760
5719
|
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
|
5761
5720
|
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
|
@@ -5810,11 +5769,15 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5810
5769
|
# [Classic only]
|
|
5811
5770
|
@host_event_handler
|
|
5812
5771
|
@with_connection_from_address
|
|
5813
|
-
def on_authentication_user_confirmation_request(
|
|
5772
|
+
def on_authentication_user_confirmation_request(
|
|
5773
|
+
self, connection: Connection, code: int
|
|
5774
|
+
) -> None:
|
|
5814
5775
|
# Ask what the pairing config should be for this connection
|
|
5815
5776
|
pairing_config = self.pairing_config_factory(connection)
|
|
5816
5777
|
io_capability = pairing_config.delegate.classic_io_capability
|
|
5817
5778
|
peer_io_capability = connection.pairing_peer_io_capability
|
|
5779
|
+
if peer_io_capability is None:
|
|
5780
|
+
raise core.InvalidStateError("Unknown pairing_peer_io_capability")
|
|
5818
5781
|
|
|
5819
5782
|
async def confirm() -> bool:
|
|
5820
5783
|
# Ask the user to confirm the pairing, without display
|
|
@@ -5941,15 +5904,16 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5941
5904
|
# Respond
|
|
5942
5905
|
if io_capability == hci.IoCapability.KEYBOARD_ONLY:
|
|
5943
5906
|
# Ask the user to enter a string
|
|
5944
|
-
async def get_pin_code():
|
|
5945
|
-
|
|
5907
|
+
async def get_pin_code() -> None:
|
|
5908
|
+
pin_code_str = await connection.cancel_on_disconnection(
|
|
5946
5909
|
pairing_config.delegate.get_string(16)
|
|
5947
5910
|
)
|
|
5948
5911
|
|
|
5949
|
-
if
|
|
5950
|
-
pin_code = bytes(
|
|
5912
|
+
if pin_code_str is not None:
|
|
5913
|
+
pin_code = bytes(pin_code_str, encoding='utf-8')
|
|
5951
5914
|
pin_code_len = len(pin_code)
|
|
5952
|
-
|
|
5915
|
+
if not 1 <= pin_code_len <= 16:
|
|
5916
|
+
raise core.InvalidArgumentError("pin_code should be 1-16 bytes")
|
|
5953
5917
|
await self.host.send_command(
|
|
5954
5918
|
hci.HCI_PIN_Code_Request_Reply_Command(
|
|
5955
5919
|
bd_addr=connection.peer_address,
|
|
@@ -5991,7 +5955,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5991
5955
|
@host_event_handler
|
|
5992
5956
|
@try_with_connection_from_address
|
|
5993
5957
|
def on_remote_name(
|
|
5994
|
-
self, connection:
|
|
5958
|
+
self, connection: Connection | None, address: hci.Address, remote_name: bytes
|
|
5995
5959
|
):
|
|
5996
5960
|
# Try to decode the name
|
|
5997
5961
|
try:
|
|
@@ -6010,7 +5974,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6010
5974
|
@host_event_handler
|
|
6011
5975
|
@try_with_connection_from_address
|
|
6012
5976
|
def on_remote_name_failure(
|
|
6013
|
-
self, connection:
|
|
5977
|
+
self, connection: Connection | None, address: hci.Address, error: int
|
|
6014
5978
|
):
|
|
6015
5979
|
if connection:
|
|
6016
5980
|
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
|
|
@@ -6299,17 +6263,6 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6299
6263
|
)
|
|
6300
6264
|
connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
|
|
6301
6265
|
|
|
6302
|
-
@host_event_handler
|
|
6303
|
-
@with_connection_from_handle
|
|
6304
|
-
def on_connection_att_mtu_update(self, connection: Connection, att_mtu: int):
|
|
6305
|
-
logger.debug(
|
|
6306
|
-
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
|
|
6307
|
-
f'{connection.peer_address} as {connection.role_name}, '
|
|
6308
|
-
f'{att_mtu}'
|
|
6309
|
-
)
|
|
6310
|
-
connection.att_mtu = att_mtu
|
|
6311
|
-
connection.emit(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
|
6312
|
-
|
|
6313
6266
|
@host_event_handler
|
|
6314
6267
|
@with_connection_from_handle
|
|
6315
6268
|
def on_connection_data_length_change(
|
|
@@ -6455,7 +6408,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6455
6408
|
@host_event_handler
|
|
6456
6409
|
@try_with_connection_from_address
|
|
6457
6410
|
def on_role_change_failure(
|
|
6458
|
-
self, connection:
|
|
6411
|
+
self, connection: Connection | None, address: hci.Address, error: int
|
|
6459
6412
|
):
|
|
6460
6413
|
if connection:
|
|
6461
6414
|
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
|
|
@@ -6479,7 +6432,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6479
6432
|
def on_pairing(
|
|
6480
6433
|
self,
|
|
6481
6434
|
connection: Connection,
|
|
6482
|
-
identity_address:
|
|
6435
|
+
identity_address: hci.Address | None,
|
|
6483
6436
|
keys: PairingKeys,
|
|
6484
6437
|
sc: bool,
|
|
6485
6438
|
) -> None:
|
|
@@ -6496,7 +6449,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6496
6449
|
@with_connection_from_handle
|
|
6497
6450
|
def on_gatt_pdu(self, connection: Connection, pdu: bytes):
|
|
6498
6451
|
# Parse the L2CAP payload into an ATT PDU object
|
|
6499
|
-
att_pdu = ATT_PDU.from_bytes(pdu)
|
|
6452
|
+
att_pdu = att.ATT_PDU.from_bytes(pdu)
|
|
6500
6453
|
|
|
6501
6454
|
# Conveniently, even-numbered op codes are client->server and
|
|
6502
6455
|
# odd-numbered ones are server->client
|