bumble 0.0.212__py3-none-any.whl → 0.0.213__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 +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +11 -9
- bumble/apps/bench.py +480 -31
- bumble/apps/console.py +3 -3
- bumble/apps/controller_info.py +47 -10
- bumble/apps/controller_loopback.py +7 -3
- bumble/apps/controllers.py +2 -2
- bumble/apps/device_info.py +2 -2
- bumble/apps/gatt_dump.py +2 -2
- bumble/apps/gg_bridge.py +2 -2
- bumble/apps/hci_bridge.py +2 -2
- bumble/apps/l2cap_bridge.py +2 -2
- bumble/apps/lea_unicast/app.py +6 -1
- bumble/apps/pair.py +19 -11
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/rfcomm_bridge.py +1 -1
- bumble/apps/scan.py +2 -2
- bumble/apps/show.py +4 -2
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +126 -18
- bumble/at.py +4 -4
- bumble/att.py +2 -6
- bumble/avc.py +7 -7
- bumble/avctp.py +3 -3
- bumble/avdtp.py +16 -20
- bumble/avrcp.py +41 -53
- bumble/colors.py +2 -2
- bumble/controller.py +84 -23
- bumble/device.py +348 -182
- bumble/drivers/__init__.py +2 -2
- bumble/drivers/common.py +0 -2
- bumble/drivers/intel.py +37 -40
- bumble/drivers/rtk.py +28 -35
- bumble/gatt.py +4 -4
- bumble/gatt_adapters.py +4 -5
- bumble/gatt_client.py +26 -31
- bumble/gatt_server.py +7 -11
- bumble/hci.py +2601 -2909
- bumble/helpers.py +4 -5
- bumble/hfp.py +32 -37
- bumble/host.py +94 -35
- bumble/keys.py +5 -5
- bumble/l2cap.py +310 -394
- bumble/link.py +6 -270
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +1 -1
- bumble/pandora/config.py +2 -2
- bumble/pandora/device.py +6 -6
- bumble/pandora/host.py +27 -28
- bumble/pandora/l2cap.py +2 -2
- bumble/pandora/security.py +6 -6
- bumble/pandora/utils.py +3 -3
- bumble/profiles/ascs.py +132 -131
- bumble/profiles/asha.py +2 -2
- bumble/profiles/bap.py +3 -4
- bumble/profiles/csip.py +2 -2
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +2 -2
- bumble/profiles/hap.py +34 -33
- bumble/profiles/le_audio.py +4 -4
- bumble/profiles/mcp.py +4 -4
- bumble/profiles/vcs.py +3 -5
- bumble/rfcomm.py +10 -10
- bumble/rtp.py +1 -2
- bumble/sdp.py +2 -2
- bumble/smp.py +57 -61
- bumble/tools/rtk_util.py +2 -2
- bumble/transport/__init__.py +2 -16
- bumble/transport/android_netsim.py +5 -5
- bumble/transport/common.py +4 -4
- bumble/transport/pyusb.py +2 -2
- bumble/utils.py +2 -5
- bumble/vendor/android/hci.py +118 -200
- bumble/vendor/zephyr/hci.py +32 -27
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/METADATA +2 -2
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/RECORD +83 -86
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
- bumble/apps/link_relay/__init__.py +0 -0
- bumble/apps/link_relay/link_relay.py +0 -289
- bumble/apps/link_relay/logging.yml +0 -21
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -35,12 +35,10 @@ import secrets
|
|
|
35
35
|
import sys
|
|
36
36
|
from typing import (
|
|
37
37
|
Any,
|
|
38
|
+
Awaitable,
|
|
38
39
|
Callable,
|
|
39
40
|
ClassVar,
|
|
40
|
-
Deque,
|
|
41
|
-
Dict,
|
|
42
41
|
Optional,
|
|
43
|
-
Type,
|
|
44
42
|
TypeVar,
|
|
45
43
|
Union,
|
|
46
44
|
cast,
|
|
@@ -87,6 +85,7 @@ from bumble.profiles import gatt_service
|
|
|
87
85
|
if TYPE_CHECKING:
|
|
88
86
|
from bumble.transport.common import TransportSource, TransportSink
|
|
89
87
|
|
|
88
|
+
_T = TypeVar('_T')
|
|
90
89
|
|
|
91
90
|
# -----------------------------------------------------------------------------
|
|
92
91
|
# Logging
|
|
@@ -99,9 +98,9 @@ logger = logging.getLogger(__name__)
|
|
|
99
98
|
# fmt: off
|
|
100
99
|
# pylint: disable=line-too-long
|
|
101
100
|
|
|
102
|
-
DEVICE_MIN_SCAN_INTERVAL =
|
|
101
|
+
DEVICE_MIN_SCAN_INTERVAL = 2.5
|
|
103
102
|
DEVICE_MAX_SCAN_INTERVAL = 10240
|
|
104
|
-
DEVICE_MIN_SCAN_WINDOW =
|
|
103
|
+
DEVICE_MIN_SCAN_WINDOW = 2.5
|
|
105
104
|
DEVICE_MAX_SCAN_WINDOW = 10240
|
|
106
105
|
DEVICE_MIN_LE_RSSI = -127
|
|
107
106
|
DEVICE_MAX_LE_RSSI = 20
|
|
@@ -140,6 +139,9 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
|
|
|
140
139
|
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
|
141
140
|
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
|
142
141
|
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
|
|
142
|
+
DEVICE_DEFAULT_ISO_CIS_MAX_SDU = 251
|
|
143
|
+
DEVICE_DEFAULT_ISO_CIS_RTN = 10
|
|
144
|
+
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY = 100
|
|
143
145
|
|
|
144
146
|
# fmt: on
|
|
145
147
|
# pylint: enable=line-too-long
|
|
@@ -202,25 +204,35 @@ class Advertisement:
|
|
|
202
204
|
# -----------------------------------------------------------------------------
|
|
203
205
|
class LegacyAdvertisement(Advertisement):
|
|
204
206
|
@classmethod
|
|
205
|
-
def from_advertising_report(
|
|
207
|
+
def from_advertising_report(
|
|
208
|
+
cls, report: hci.HCI_LE_Advertising_Report_Event.Report
|
|
209
|
+
) -> Self:
|
|
206
210
|
return cls(
|
|
207
211
|
address=report.address,
|
|
208
212
|
rssi=report.rssi,
|
|
209
213
|
is_legacy=True,
|
|
210
|
-
is_connectable=
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
is_connectable=(
|
|
215
|
+
report.event_type
|
|
216
|
+
in (
|
|
217
|
+
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
|
218
|
+
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
|
|
219
|
+
)
|
|
220
|
+
),
|
|
221
|
+
is_directed=(
|
|
222
|
+
report.event_type
|
|
223
|
+
== hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
|
|
224
|
+
),
|
|
225
|
+
is_scannable=(
|
|
226
|
+
report.event_type
|
|
227
|
+
in (
|
|
228
|
+
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
|
229
|
+
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
|
|
230
|
+
)
|
|
214
231
|
),
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
in (
|
|
219
|
-
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
|
|
220
|
-
hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
|
232
|
+
is_scan_response=(
|
|
233
|
+
report.event_type
|
|
234
|
+
== hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
|
|
221
235
|
),
|
|
222
|
-
is_scan_response=report.event_type
|
|
223
|
-
== hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
|
224
236
|
data_bytes=report.data,
|
|
225
237
|
)
|
|
226
238
|
|
|
@@ -228,18 +240,20 @@ class LegacyAdvertisement(Advertisement):
|
|
|
228
240
|
# -----------------------------------------------------------------------------
|
|
229
241
|
class ExtendedAdvertisement(Advertisement):
|
|
230
242
|
@classmethod
|
|
231
|
-
def from_advertising_report(
|
|
243
|
+
def from_advertising_report(
|
|
244
|
+
cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
|
|
245
|
+
) -> Self:
|
|
232
246
|
# fmt: off
|
|
233
247
|
# pylint: disable=line-too-long
|
|
234
248
|
return cls(
|
|
235
249
|
address = report.address,
|
|
236
250
|
rssi = report.rssi,
|
|
237
|
-
is_legacy = report.event_type &
|
|
251
|
+
is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0,
|
|
238
252
|
is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
|
|
239
|
-
is_connectable = report.event_type &
|
|
240
|
-
is_directed = report.event_type &
|
|
241
|
-
is_scannable = report.event_type &
|
|
242
|
-
is_scan_response = report.event_type &
|
|
253
|
+
is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0,
|
|
254
|
+
is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0,
|
|
255
|
+
is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0,
|
|
256
|
+
is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0,
|
|
243
257
|
is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
|
|
244
258
|
is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
|
|
245
259
|
primary_phy = report.primary_phy,
|
|
@@ -436,7 +450,7 @@ class AdvertisingEventProperties:
|
|
|
436
450
|
|
|
437
451
|
@classmethod
|
|
438
452
|
def from_advertising_type(
|
|
439
|
-
cls:
|
|
453
|
+
cls: type[AdvertisingEventProperties],
|
|
440
454
|
advertising_type: AdvertisingType,
|
|
441
455
|
) -> AdvertisingEventProperties:
|
|
442
456
|
return cls(
|
|
@@ -478,7 +492,18 @@ class PeriodicAdvertisement:
|
|
|
478
492
|
|
|
479
493
|
# -----------------------------------------------------------------------------
|
|
480
494
|
@dataclass
|
|
481
|
-
class
|
|
495
|
+
class BigInfoAdvertisement:
|
|
496
|
+
class Framing(utils.OpenIntEnum):
|
|
497
|
+
# fmt: off
|
|
498
|
+
UNFRAMED = 0X00
|
|
499
|
+
FRAMED_SEGMENTABLE_MODE = 0X01
|
|
500
|
+
FRAMED_UNSEGMENTED_MODE = 0X02
|
|
501
|
+
|
|
502
|
+
class Encryption(utils.OpenIntEnum):
|
|
503
|
+
# fmt: off
|
|
504
|
+
UNENCRYPTED = 0x00
|
|
505
|
+
ENCRYPTED = 0x01
|
|
506
|
+
|
|
482
507
|
address: hci.Address
|
|
483
508
|
sid: int
|
|
484
509
|
num_bis: int
|
|
@@ -491,8 +516,8 @@ class BIGInfoAdvertisement:
|
|
|
491
516
|
sdu_interval: int
|
|
492
517
|
max_sdu: int
|
|
493
518
|
phy: hci.Phy
|
|
494
|
-
|
|
495
|
-
|
|
519
|
+
framing: Framing
|
|
520
|
+
encryption: Encryption
|
|
496
521
|
|
|
497
522
|
@classmethod
|
|
498
523
|
def from_report(cls, address: hci.Address, sid: int, report) -> Self:
|
|
@@ -509,8 +534,8 @@ class BIGInfoAdvertisement:
|
|
|
509
534
|
report.sdu_interval,
|
|
510
535
|
report.max_sdu,
|
|
511
536
|
hci.Phy(report.phy),
|
|
512
|
-
report.framing
|
|
513
|
-
report.encryption
|
|
537
|
+
cls.Framing(report.framing),
|
|
538
|
+
cls.Encryption(report.encryption),
|
|
514
539
|
)
|
|
515
540
|
|
|
516
541
|
|
|
@@ -1002,7 +1027,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
1002
1027
|
def on_biginfo_advertising_report(self, report) -> None:
|
|
1003
1028
|
self.emit(
|
|
1004
1029
|
self.EVENT_BIGINFO_ADVERTISEMENT,
|
|
1005
|
-
|
|
1030
|
+
BigInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
|
|
1006
1031
|
)
|
|
1007
1032
|
|
|
1008
1033
|
def __str__(self) -> str:
|
|
@@ -1020,14 +1045,24 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|
|
1020
1045
|
# -----------------------------------------------------------------------------
|
|
1021
1046
|
@dataclass
|
|
1022
1047
|
class BigParameters:
|
|
1048
|
+
class Packing(utils.OpenIntEnum):
|
|
1049
|
+
# fmt: off
|
|
1050
|
+
SEQUENTIAL = 0x00
|
|
1051
|
+
INTERLEAVED = 0x01
|
|
1052
|
+
|
|
1053
|
+
class Framing(utils.OpenIntEnum):
|
|
1054
|
+
# fmt: off
|
|
1055
|
+
UNFRAMED = 0x00
|
|
1056
|
+
FRAMED = 0x01
|
|
1057
|
+
|
|
1023
1058
|
num_bis: int
|
|
1024
|
-
sdu_interval: int
|
|
1059
|
+
sdu_interval: int # SDU interval, in microseconds
|
|
1025
1060
|
max_sdu: int
|
|
1026
|
-
max_transport_latency: int
|
|
1061
|
+
max_transport_latency: int # Max transport latency, in milliseconds
|
|
1027
1062
|
rtn: int
|
|
1028
1063
|
phy: hci.PhyBit = hci.PhyBit.LE_2M
|
|
1029
|
-
packing:
|
|
1030
|
-
framing:
|
|
1064
|
+
packing: Packing = Packing.SEQUENTIAL
|
|
1065
|
+
framing: Framing = Framing.UNFRAMED
|
|
1031
1066
|
broadcast_code: bytes | None = None
|
|
1032
1067
|
|
|
1033
1068
|
|
|
@@ -1050,15 +1085,15 @@ class Big(utils.EventEmitter):
|
|
|
1050
1085
|
state: State = State.PENDING
|
|
1051
1086
|
|
|
1052
1087
|
# Attributes provided by BIG Create Complete event
|
|
1053
|
-
big_sync_delay: int = 0
|
|
1054
|
-
transport_latency_big: int = 0
|
|
1055
|
-
phy:
|
|
1088
|
+
big_sync_delay: int = 0 # Sync delay, in microseconds
|
|
1089
|
+
transport_latency_big: int = 0 # Transport latency, in microseconds
|
|
1090
|
+
phy: hci.Phy = hci.Phy.LE_1M
|
|
1056
1091
|
nse: int = 0
|
|
1057
1092
|
bn: int = 0
|
|
1058
1093
|
pto: int = 0
|
|
1059
1094
|
irc: int = 0
|
|
1060
1095
|
max_pdu: int = 0
|
|
1061
|
-
iso_interval: float = 0.0
|
|
1096
|
+
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
|
1062
1097
|
bis_links: Sequence[BisLink] = ()
|
|
1063
1098
|
|
|
1064
1099
|
def __post_init__(self) -> None:
|
|
@@ -1343,7 +1378,7 @@ class Peer:
|
|
|
1343
1378
|
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
|
1344
1379
|
|
|
1345
1380
|
def create_service_proxy(
|
|
1346
|
-
self, proxy_class:
|
|
1381
|
+
self, proxy_class: type[_PROXY_CLASS]
|
|
1347
1382
|
) -> Optional[_PROXY_CLASS]:
|
|
1348
1383
|
if proxy := proxy_class.from_client(self.gatt_client):
|
|
1349
1384
|
return cast(_PROXY_CLASS, proxy)
|
|
@@ -1351,7 +1386,7 @@ class Peer:
|
|
|
1351
1386
|
return None
|
|
1352
1387
|
|
|
1353
1388
|
async def discover_service_and_create_proxy(
|
|
1354
|
-
self, proxy_class:
|
|
1389
|
+
self, proxy_class: type[_PROXY_CLASS]
|
|
1355
1390
|
) -> Optional[_PROXY_CLASS]:
|
|
1356
1391
|
# Discover the first matching service and its characteristics
|
|
1357
1392
|
services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
|
|
@@ -1464,7 +1499,7 @@ class _IsoLink:
|
|
|
1464
1499
|
check_result=True,
|
|
1465
1500
|
)
|
|
1466
1501
|
|
|
1467
|
-
async def remove_data_path(self,
|
|
1502
|
+
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
|
|
1468
1503
|
"""Remove a data path with controller on given direction.
|
|
1469
1504
|
|
|
1470
1505
|
Args:
|
|
@@ -1476,7 +1511,9 @@ class _IsoLink:
|
|
|
1476
1511
|
response = await self.device.send_command(
|
|
1477
1512
|
hci.HCI_LE_Remove_ISO_Data_Path_Command(
|
|
1478
1513
|
connection_handle=self.handle,
|
|
1479
|
-
data_path_direction=
|
|
1514
|
+
data_path_direction=sum(
|
|
1515
|
+
1 << direction for direction in set(directions)
|
|
1516
|
+
),
|
|
1480
1517
|
),
|
|
1481
1518
|
check_result=False,
|
|
1482
1519
|
)
|
|
@@ -1486,10 +1523,74 @@ class _IsoLink:
|
|
|
1486
1523
|
"""Write an ISO SDU."""
|
|
1487
1524
|
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
|
|
1488
1525
|
|
|
1526
|
+
async def get_tx_time_stamp(self) -> tuple[int, int, int]:
|
|
1527
|
+
response = await self.device.host.send_command(
|
|
1528
|
+
hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle),
|
|
1529
|
+
check_result=True,
|
|
1530
|
+
)
|
|
1531
|
+
return (
|
|
1532
|
+
response.return_parameters.packet_sequence_number,
|
|
1533
|
+
response.return_parameters.tx_time_stamp,
|
|
1534
|
+
response.return_parameters.time_offset,
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1489
1537
|
@property
|
|
1490
1538
|
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1491
1539
|
return self.device.host.get_data_packet_queue(self.handle)
|
|
1492
1540
|
|
|
1541
|
+
async def drain(self) -> None:
|
|
1542
|
+
if data_packet_queue := self.data_packet_queue:
|
|
1543
|
+
await data_packet_queue.drain(self.handle)
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
# -----------------------------------------------------------------------------
|
|
1547
|
+
@dataclass
|
|
1548
|
+
class CigParameters:
|
|
1549
|
+
class WorstCaseSca(utils.OpenIntEnum):
|
|
1550
|
+
# fmt: off
|
|
1551
|
+
SCA_251_TO_500_PPM = 0x00
|
|
1552
|
+
SCA_151_TO_250_PPM = 0x01
|
|
1553
|
+
SCA_101_TO_150_PPM = 0x02
|
|
1554
|
+
SCA_76_TO_100_PPM = 0x03
|
|
1555
|
+
SCA_51_TO_75_PPM = 0x04
|
|
1556
|
+
SCA_31_TO_50_PPM = 0x05
|
|
1557
|
+
SCA_21_TO_30_PPM = 0x06
|
|
1558
|
+
SCA_0_TO_20_PPM = 0x07
|
|
1559
|
+
|
|
1560
|
+
class Packing(utils.OpenIntEnum):
|
|
1561
|
+
# fmt: off
|
|
1562
|
+
SEQUENTIAL = 0x00
|
|
1563
|
+
INTERLEAVED = 0x01
|
|
1564
|
+
|
|
1565
|
+
class Framing(utils.OpenIntEnum):
|
|
1566
|
+
# fmt: off
|
|
1567
|
+
UNFRAMED = 0x00
|
|
1568
|
+
FRAMED = 0x01
|
|
1569
|
+
|
|
1570
|
+
@dataclass
|
|
1571
|
+
class CisParameters:
|
|
1572
|
+
cis_id: int
|
|
1573
|
+
max_sdu_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
|
|
1574
|
+
max_sdu_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
|
|
1575
|
+
phy_c_to_p: hci.PhyBit = hci.PhyBit.LE_2M
|
|
1576
|
+
phy_p_to_c: hci.PhyBit = hci.PhyBit.LE_2M
|
|
1577
|
+
rtn_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of C->P retransmissions
|
|
1578
|
+
rtn_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of P->C retransmissions
|
|
1579
|
+
|
|
1580
|
+
cig_id: int
|
|
1581
|
+
cis_parameters: list[CisParameters]
|
|
1582
|
+
sdu_interval_c_to_p: int # C->P SDU interval, in microseconds
|
|
1583
|
+
sdu_interval_p_to_c: int # P->C SDU interval, in microseconds
|
|
1584
|
+
worst_case_sca: WorstCaseSca = WorstCaseSca.SCA_251_TO_500_PPM
|
|
1585
|
+
packing: Packing = Packing.SEQUENTIAL
|
|
1586
|
+
framing: Framing = Framing.UNFRAMED
|
|
1587
|
+
max_transport_latency_c_to_p: int = (
|
|
1588
|
+
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
|
|
1589
|
+
)
|
|
1590
|
+
max_transport_latency_p_to_c: int = (
|
|
1591
|
+
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1493
1594
|
|
|
1494
1595
|
# -----------------------------------------------------------------------------
|
|
1495
1596
|
@dataclass
|
|
@@ -1503,6 +1604,20 @@ class CisLink(utils.EventEmitter, _IsoLink):
|
|
|
1503
1604
|
handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
|
|
1504
1605
|
cis_id: int # CIS ID assigned by Central device
|
|
1505
1606
|
cig_id: int # CIG ID assigned by Central device
|
|
1607
|
+
cig_sync_delay: int = 0 # CIG sync delay, in microseconds
|
|
1608
|
+
cis_sync_delay: int = 0 # CIS sync delay, in microseconds
|
|
1609
|
+
transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
|
|
1610
|
+
transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
|
|
1611
|
+
phy_c_to_p: Optional[hci.Phy] = None
|
|
1612
|
+
phy_p_to_c: Optional[hci.Phy] = None
|
|
1613
|
+
nse: int = 0
|
|
1614
|
+
bn_c_to_p: int = 0
|
|
1615
|
+
bn_p_to_c: int = 0
|
|
1616
|
+
ft_c_to_p: int = 0
|
|
1617
|
+
ft_p_to_c: int = 0
|
|
1618
|
+
max_pdu_c_to_p: int = 0
|
|
1619
|
+
max_pdu_p_to_c: int = 0
|
|
1620
|
+
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
|
1506
1621
|
state: State = State.PENDING
|
|
1507
1622
|
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
|
1508
1623
|
|
|
@@ -1545,7 +1660,7 @@ class IsoPacketStream:
|
|
|
1545
1660
|
self.iso_link = iso_link
|
|
1546
1661
|
self.data_packet_queue = iso_link.data_packet_queue
|
|
1547
1662
|
self.data_packet_queue.on('flow', self._on_flow)
|
|
1548
|
-
self._thresholds:
|
|
1663
|
+
self._thresholds: collections.deque[int] = collections.deque()
|
|
1549
1664
|
self._semaphore = asyncio.Semaphore(max_queue_size)
|
|
1550
1665
|
|
|
1551
1666
|
def _on_flow(self) -> None:
|
|
@@ -1585,6 +1700,7 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1585
1700
|
peer_resolvable_address: Optional[hci.Address]
|
|
1586
1701
|
peer_le_features: Optional[hci.LeFeatureMask]
|
|
1587
1702
|
role: hci.Role
|
|
1703
|
+
parameters: Parameters
|
|
1588
1704
|
encryption: int
|
|
1589
1705
|
encryption_key_size: int
|
|
1590
1706
|
authenticated: bool
|
|
@@ -1594,6 +1710,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1594
1710
|
pairing_peer_authentication_requirements: Optional[int]
|
|
1595
1711
|
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
|
|
1596
1712
|
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
|
|
1713
|
+
classic_mode: int = hci.HCI_Mode_Change_Event.Mode.ACTIVE
|
|
1714
|
+
classic_interval: int = 0
|
|
1597
1715
|
|
|
1598
1716
|
EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
|
|
1599
1717
|
EVENT_DISCONNECTION = "disconnection"
|
|
@@ -1620,6 +1738,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1620
1738
|
EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED = "channel_sounding_config_removed"
|
|
1621
1739
|
EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE = "channel_sounding_procedure_failure"
|
|
1622
1740
|
EVENT_CHANNEL_SOUNDING_PROCEDURE = "channel_sounding_procedure"
|
|
1741
|
+
EVENT_MODE_CHANGE = "mode_change"
|
|
1742
|
+
EVENT_MODE_CHANGE_FAILURE = "mode_change_failure"
|
|
1623
1743
|
EVENT_ROLE_CHANGE = "role_change"
|
|
1624
1744
|
EVENT_ROLE_CHANGE_FAILURE = "role_change_failure"
|
|
1625
1745
|
EVENT_CLASSIC_PAIRING = "classic_pairing"
|
|
@@ -1629,6 +1749,9 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1629
1749
|
EVENT_PAIRING_FAILURE = "pairing_failure"
|
|
1630
1750
|
EVENT_SECURITY_REQUEST = "security_request"
|
|
1631
1751
|
EVENT_LINK_KEY = "link_key"
|
|
1752
|
+
EVENT_CIS_REQUEST = "cis_request"
|
|
1753
|
+
EVENT_CIS_ESTABLISHMENT = "cis_establishment"
|
|
1754
|
+
EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
|
|
1632
1755
|
|
|
1633
1756
|
@utils.composite_listener
|
|
1634
1757
|
class Listener:
|
|
@@ -1884,6 +2007,12 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1884
2007
|
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1885
2008
|
return self.device.host.get_data_packet_queue(self.handle)
|
|
1886
2009
|
|
|
2010
|
+
def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
|
2011
|
+
"""
|
|
2012
|
+
Helper method to call `utils.cancel_on_event` for the 'disconnection' event
|
|
2013
|
+
"""
|
|
2014
|
+
return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
|
|
2015
|
+
|
|
1887
2016
|
async def __aenter__(self):
|
|
1888
2017
|
return self
|
|
1889
2018
|
|
|
@@ -1954,9 +2083,9 @@ class DeviceConfiguration:
|
|
|
1954
2083
|
gatt_service_enabled: bool = True
|
|
1955
2084
|
|
|
1956
2085
|
def __post_init__(self) -> None:
|
|
1957
|
-
self.gatt_services: list[
|
|
2086
|
+
self.gatt_services: list[dict[str, Any]] = []
|
|
1958
2087
|
|
|
1959
|
-
def load_from_dict(self, config:
|
|
2088
|
+
def load_from_dict(self, config: dict[str, Any]) -> None:
|
|
1960
2089
|
config = copy.deepcopy(config)
|
|
1961
2090
|
|
|
1962
2091
|
# Load simple properties
|
|
@@ -2016,13 +2145,13 @@ class DeviceConfiguration:
|
|
|
2016
2145
|
self.load_from_dict(json.load(file))
|
|
2017
2146
|
|
|
2018
2147
|
@classmethod
|
|
2019
|
-
def from_file(cls:
|
|
2148
|
+
def from_file(cls: type[Self], filename: str) -> Self:
|
|
2020
2149
|
config = cls()
|
|
2021
2150
|
config.load_from_file(filename)
|
|
2022
2151
|
return config
|
|
2023
2152
|
|
|
2024
2153
|
@classmethod
|
|
2025
|
-
def from_dict(cls:
|
|
2154
|
+
def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
|
|
2026
2155
|
device_config = cls()
|
|
2027
2156
|
device_config.load_from_dict(config)
|
|
2028
2157
|
return device_config
|
|
@@ -2119,22 +2248,22 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2119
2248
|
advertising_data: bytes
|
|
2120
2249
|
scan_response_data: bytes
|
|
2121
2250
|
cs_capabilities: ChannelSoundingCapabilities | None = None
|
|
2122
|
-
connections:
|
|
2123
|
-
pending_connections:
|
|
2124
|
-
classic_pending_accepts:
|
|
2251
|
+
connections: dict[int, Connection]
|
|
2252
|
+
pending_connections: dict[hci.Address, Connection]
|
|
2253
|
+
classic_pending_accepts: dict[
|
|
2125
2254
|
hci.Address,
|
|
2126
2255
|
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
|
2127
2256
|
]
|
|
2128
|
-
advertisement_accumulators:
|
|
2257
|
+
advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
|
|
2129
2258
|
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
|
|
2130
2259
|
config: DeviceConfiguration
|
|
2131
2260
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
2132
|
-
sco_links:
|
|
2133
|
-
cis_links:
|
|
2261
|
+
sco_links: dict[int, ScoLink]
|
|
2262
|
+
cis_links: dict[int, CisLink]
|
|
2134
2263
|
bigs: dict[int, Big]
|
|
2135
2264
|
bis_links: dict[int, BisLink]
|
|
2136
2265
|
big_syncs: dict[int, BigSync]
|
|
2137
|
-
_pending_cis:
|
|
2266
|
+
_pending_cis: dict[int, tuple[int, int]]
|
|
2138
2267
|
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
2139
2268
|
|
|
2140
2269
|
EVENT_ADVERTISEMENT = "advertisement"
|
|
@@ -2294,8 +2423,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2294
2423
|
self.address_generation_offload = config.address_generation_offload
|
|
2295
2424
|
|
|
2296
2425
|
# Extended advertising.
|
|
2297
|
-
self.extended_advertising_sets:
|
|
2298
|
-
self.connecting_extended_advertising_sets:
|
|
2426
|
+
self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
|
|
2427
|
+
self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
|
|
2299
2428
|
|
|
2300
2429
|
# Legacy advertising.
|
|
2301
2430
|
# The advertising and scan response data, as well as the advertising interval
|
|
@@ -4271,11 +4400,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4271
4400
|
self.smp_manager.pairing_config_factory = pairing_config_factory
|
|
4272
4401
|
|
|
4273
4402
|
@property
|
|
4274
|
-
def smp_session_proxy(self) ->
|
|
4403
|
+
def smp_session_proxy(self) -> type[smp.Session]:
|
|
4275
4404
|
return self.smp_manager.session_proxy
|
|
4276
4405
|
|
|
4277
4406
|
@smp_session_proxy.setter
|
|
4278
|
-
def smp_session_proxy(self, session_proxy:
|
|
4407
|
+
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
|
|
4279
4408
|
self.smp_manager.session_proxy = session_proxy
|
|
4280
4409
|
|
|
4281
4410
|
async def pair(self, connection):
|
|
@@ -4359,9 +4488,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4359
4488
|
raise hci.HCI_StatusError(result)
|
|
4360
4489
|
|
|
4361
4490
|
# Wait for the authentication to complete
|
|
4362
|
-
await
|
|
4363
|
-
connection, Connection.EVENT_DISCONNECTION, pending_authentication
|
|
4364
|
-
)
|
|
4491
|
+
await connection.cancel_on_disconnection(pending_authentication)
|
|
4365
4492
|
finally:
|
|
4366
4493
|
connection.remove_listener(
|
|
4367
4494
|
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
|
|
@@ -4448,9 +4575,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4448
4575
|
raise hci.HCI_StatusError(result)
|
|
4449
4576
|
|
|
4450
4577
|
# Wait for the result
|
|
4451
|
-
await
|
|
4452
|
-
connection, Connection.EVENT_DISCONNECTION, pending_encryption
|
|
4453
|
-
)
|
|
4578
|
+
await connection.cancel_on_disconnection(pending_encryption)
|
|
4454
4579
|
finally:
|
|
4455
4580
|
connection.remove_listener(
|
|
4456
4581
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
|
@@ -4494,9 +4619,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4494
4619
|
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4495
4620
|
)
|
|
4496
4621
|
raise hci.HCI_StatusError(result)
|
|
4497
|
-
await
|
|
4498
|
-
connection, Connection.EVENT_DISCONNECTION, pending_role_change
|
|
4499
|
-
)
|
|
4622
|
+
await connection.cancel_on_disconnection(pending_role_change)
|
|
4500
4623
|
finally:
|
|
4501
4624
|
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
|
4502
4625
|
connection.remove_listener(
|
|
@@ -4556,48 +4679,39 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4556
4679
|
@utils.experimental('Only for testing.')
|
|
4557
4680
|
async def setup_cig(
|
|
4558
4681
|
self,
|
|
4559
|
-
|
|
4560
|
-
cis_id: Sequence[int],
|
|
4561
|
-
sdu_interval: tuple[int, int],
|
|
4562
|
-
framing: int,
|
|
4563
|
-
max_sdu: tuple[int, int],
|
|
4564
|
-
retransmission_number: int,
|
|
4565
|
-
max_transport_latency: tuple[int, int],
|
|
4682
|
+
parameters: CigParameters,
|
|
4566
4683
|
) -> list[int]:
|
|
4567
4684
|
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
|
4568
4685
|
|
|
4569
4686
|
Args:
|
|
4570
|
-
|
|
4571
|
-
cis_id: CID ID list.
|
|
4572
|
-
sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
|
|
4573
|
-
framing: Un-framing(0) or Framing(1).
|
|
4574
|
-
max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
|
|
4575
|
-
retransmission_number: retransmission_number.
|
|
4576
|
-
max_transport_latency: Max transport latencies of
|
|
4577
|
-
(Central->Peripheral, Peripheral->Cental).
|
|
4687
|
+
parameters: CIG parameters.
|
|
4578
4688
|
|
|
4579
4689
|
Returns:
|
|
4580
4690
|
List of created CIS handles corresponding to the same order of [cid_id].
|
|
4581
4691
|
"""
|
|
4582
|
-
num_cis = len(
|
|
4692
|
+
num_cis = len(parameters.cis_parameters)
|
|
4583
4693
|
|
|
4584
4694
|
response = await self.send_command(
|
|
4585
4695
|
hci.HCI_LE_Set_CIG_Parameters_Command(
|
|
4586
|
-
cig_id=cig_id,
|
|
4587
|
-
sdu_interval_c_to_p=
|
|
4588
|
-
sdu_interval_p_to_c=
|
|
4589
|
-
worst_case_sca=
|
|
4590
|
-
packing=
|
|
4591
|
-
framing=framing,
|
|
4592
|
-
max_transport_latency_c_to_p=
|
|
4593
|
-
max_transport_latency_p_to_c=
|
|
4594
|
-
cis_id=cis_id,
|
|
4595
|
-
max_sdu_c_to_p=[
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4696
|
+
cig_id=parameters.cig_id,
|
|
4697
|
+
sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
|
|
4698
|
+
sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
|
|
4699
|
+
worst_case_sca=parameters.worst_case_sca,
|
|
4700
|
+
packing=int(parameters.packing),
|
|
4701
|
+
framing=int(parameters.framing),
|
|
4702
|
+
max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
|
|
4703
|
+
max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
|
|
4704
|
+
cis_id=[cis.cis_id for cis in parameters.cis_parameters],
|
|
4705
|
+
max_sdu_c_to_p=[
|
|
4706
|
+
cis.max_sdu_c_to_p for cis in parameters.cis_parameters
|
|
4707
|
+
],
|
|
4708
|
+
max_sdu_p_to_c=[
|
|
4709
|
+
cis.max_sdu_p_to_c for cis in parameters.cis_parameters
|
|
4710
|
+
],
|
|
4711
|
+
phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
|
|
4712
|
+
phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
|
|
4713
|
+
rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
|
|
4714
|
+
rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
|
|
4601
4715
|
),
|
|
4602
4716
|
check_result=True,
|
|
4603
4717
|
)
|
|
@@ -4605,19 +4719,17 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4605
4719
|
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
|
|
4606
4720
|
# Server, so here it only provides a basic functionality for testing.
|
|
4607
4721
|
cis_handles = response.return_parameters.connection_handle[:]
|
|
4608
|
-
for
|
|
4609
|
-
self._pending_cis[cis_handle] = (
|
|
4722
|
+
for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
|
|
4723
|
+
self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
|
|
4610
4724
|
|
|
4611
4725
|
return cis_handles
|
|
4612
4726
|
|
|
4613
4727
|
# [LE only]
|
|
4614
4728
|
@utils.experimental('Only for testing.')
|
|
4615
4729
|
async def create_cis(
|
|
4616
|
-
self, cis_acl_pairs: Sequence[tuple[int,
|
|
4730
|
+
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
|
|
4617
4731
|
) -> list[CisLink]:
|
|
4618
|
-
for cis_handle,
|
|
4619
|
-
acl_connection = self.lookup_connection(acl_handle)
|
|
4620
|
-
assert acl_connection
|
|
4732
|
+
for cis_handle, acl_connection in cis_acl_pairs:
|
|
4621
4733
|
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
|
4622
4734
|
self.cis_links[cis_handle] = CisLink(
|
|
4623
4735
|
device=self,
|
|
@@ -4637,8 +4749,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4637
4749
|
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
|
4638
4750
|
pending_future.set_result(cis_link)
|
|
4639
4751
|
|
|
4640
|
-
def on_cis_establishment_failure(
|
|
4641
|
-
if pending_future := pending_cis_establishments.get(
|
|
4752
|
+
def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
|
|
4753
|
+
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
|
4642
4754
|
pending_future.set_exception(hci.HCI_Error(status))
|
|
4643
4755
|
|
|
4644
4756
|
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
|
|
@@ -4648,7 +4760,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4648
4760
|
await self.send_command(
|
|
4649
4761
|
hci.HCI_LE_Create_CIS_Command(
|
|
4650
4762
|
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
|
4651
|
-
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
|
4763
|
+
acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
|
|
4652
4764
|
),
|
|
4653
4765
|
check_result=True,
|
|
4654
4766
|
)
|
|
@@ -4657,26 +4769,21 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4657
4769
|
|
|
4658
4770
|
# [LE only]
|
|
4659
4771
|
@utils.experimental('Only for testing.')
|
|
4660
|
-
async def accept_cis_request(self,
|
|
4772
|
+
async def accept_cis_request(self, cis_link: CisLink) -> None:
|
|
4661
4773
|
"""[LE Only] Accepts an incoming CIS request.
|
|
4662
4774
|
|
|
4663
|
-
|
|
4664
|
-
|
|
4775
|
+
This method returns when the CIS is established, or raises an exception if
|
|
4776
|
+
the CIS establishment fails.
|
|
4665
4777
|
|
|
4666
4778
|
Args:
|
|
4667
4779
|
handle: CIS handle to accept.
|
|
4668
|
-
|
|
4669
|
-
Returns:
|
|
4670
|
-
CIS link object on the given handle.
|
|
4671
4780
|
"""
|
|
4672
|
-
if not (cis_link := self.cis_links.get(handle)):
|
|
4673
|
-
raise InvalidStateError(f'No pending CIS request of handle {handle}')
|
|
4674
4781
|
|
|
4675
4782
|
# There might be multiple ASE sharing a CIS channel.
|
|
4676
4783
|
# If one of them has accepted the request, the others should just leverage it.
|
|
4677
4784
|
async with self._cis_lock:
|
|
4678
4785
|
if cis_link.state == CisLink.State.ESTABLISHED:
|
|
4679
|
-
return
|
|
4786
|
+
return
|
|
4680
4787
|
|
|
4681
4788
|
with closing(utils.EventWatcher()) as watcher:
|
|
4682
4789
|
pending_establishment = asyncio.get_running_loop().create_future()
|
|
@@ -4695,26 +4802,24 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4695
4802
|
)
|
|
4696
4803
|
|
|
4697
4804
|
await self.send_command(
|
|
4698
|
-
hci.HCI_LE_Accept_CIS_Request_Command(
|
|
4805
|
+
hci.HCI_LE_Accept_CIS_Request_Command(
|
|
4806
|
+
connection_handle=cis_link.handle
|
|
4807
|
+
),
|
|
4699
4808
|
check_result=True,
|
|
4700
4809
|
)
|
|
4701
4810
|
|
|
4702
4811
|
await pending_establishment
|
|
4703
|
-
return cis_link
|
|
4704
|
-
|
|
4705
|
-
# Mypy believes this is reachable when context is an ExitStack.
|
|
4706
|
-
raise UnreachableError()
|
|
4707
4812
|
|
|
4708
4813
|
# [LE only]
|
|
4709
4814
|
@utils.experimental('Only for testing.')
|
|
4710
4815
|
async def reject_cis_request(
|
|
4711
4816
|
self,
|
|
4712
|
-
|
|
4817
|
+
cis_link: CisLink,
|
|
4713
4818
|
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
4714
4819
|
) -> None:
|
|
4715
4820
|
await self.send_command(
|
|
4716
4821
|
hci.HCI_LE_Reject_CIS_Request_Command(
|
|
4717
|
-
connection_handle=handle, reason=reason
|
|
4822
|
+
connection_handle=cis_link.handle, reason=reason
|
|
4718
4823
|
),
|
|
4719
4824
|
check_result=True,
|
|
4720
4825
|
)
|
|
@@ -5071,8 +5176,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5071
5176
|
# Store the keys in the key store
|
|
5072
5177
|
if self.keystore:
|
|
5073
5178
|
authenticated = key_type in (
|
|
5074
|
-
hci.
|
|
5075
|
-
hci.
|
|
5179
|
+
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
|
5180
|
+
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
|
5076
5181
|
)
|
|
5077
5182
|
pairing_keys = PairingKeys(
|
|
5078
5183
|
link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
|
|
@@ -5252,7 +5357,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5252
5357
|
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
|
5253
5358
|
big.big_sync_delay = big_sync_delay
|
|
5254
5359
|
big.transport_latency_big = transport_latency_big
|
|
5255
|
-
big.phy = phy
|
|
5360
|
+
big.phy = hci.Phy(phy)
|
|
5256
5361
|
big.nse = nse
|
|
5257
5362
|
big.bn = bn
|
|
5258
5363
|
big.pto = pto
|
|
@@ -5519,8 +5624,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5519
5624
|
|
|
5520
5625
|
# Handle SCO request.
|
|
5521
5626
|
if link_type in (
|
|
5522
|
-
hci.HCI_Connection_Complete_Event.
|
|
5523
|
-
hci.HCI_Connection_Complete_Event.
|
|
5627
|
+
hci.HCI_Connection_Complete_Event.LinkType.SCO,
|
|
5628
|
+
hci.HCI_Connection_Complete_Event.LinkType.ESCO,
|
|
5524
5629
|
):
|
|
5525
5630
|
if connection := self.find_connection_by_bd_addr(
|
|
5526
5631
|
bd_addr, transport=PhysicalTransport.BR_EDR
|
|
@@ -5628,7 +5733,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5628
5733
|
# [Classic only]
|
|
5629
5734
|
@host_event_handler
|
|
5630
5735
|
@with_connection_from_address
|
|
5631
|
-
def on_authentication_io_capability_request(self, connection):
|
|
5736
|
+
def on_authentication_io_capability_request(self, connection: Connection):
|
|
5632
5737
|
# Ask what the pairing config should be for this connection
|
|
5633
5738
|
pairing_config = self.pairing_config_factory(connection)
|
|
5634
5739
|
|
|
@@ -5636,13 +5741,13 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5636
5741
|
authentication_requirements = (
|
|
5637
5742
|
# No Bonding
|
|
5638
5743
|
(
|
|
5639
|
-
hci.
|
|
5640
|
-
hci.
|
|
5744
|
+
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
|
|
5745
|
+
hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
|
|
5641
5746
|
),
|
|
5642
5747
|
# General Bonding
|
|
5643
5748
|
(
|
|
5644
|
-
hci.
|
|
5645
|
-
hci.
|
|
5749
|
+
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
|
|
5750
|
+
hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
|
|
5646
5751
|
),
|
|
5647
5752
|
)[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
|
|
5648
5753
|
|
|
@@ -5697,30 +5802,30 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5697
5802
|
raise UnreachableError()
|
|
5698
5803
|
|
|
5699
5804
|
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6
|
|
5700
|
-
methods = {
|
|
5701
|
-
hci.
|
|
5702
|
-
hci.
|
|
5703
|
-
hci.
|
|
5704
|
-
hci.
|
|
5705
|
-
hci.
|
|
5805
|
+
methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
|
|
5806
|
+
hci.IoCapability.DISPLAY_ONLY: {
|
|
5807
|
+
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
|
5808
|
+
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
|
5809
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5810
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5706
5811
|
},
|
|
5707
|
-
hci.
|
|
5708
|
-
hci.
|
|
5709
|
-
hci.
|
|
5710
|
-
hci.
|
|
5711
|
-
hci.
|
|
5812
|
+
hci.IoCapability.DISPLAY_YES_NO: {
|
|
5813
|
+
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
|
5814
|
+
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
|
5815
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5816
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5712
5817
|
},
|
|
5713
|
-
hci.
|
|
5714
|
-
hci.
|
|
5715
|
-
hci.
|
|
5716
|
-
hci.
|
|
5717
|
-
hci.
|
|
5818
|
+
hci.IoCapability.KEYBOARD_ONLY: {
|
|
5819
|
+
hci.IoCapability.DISPLAY_ONLY: na,
|
|
5820
|
+
hci.IoCapability.DISPLAY_YES_NO: na,
|
|
5821
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5822
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5718
5823
|
},
|
|
5719
|
-
hci.
|
|
5720
|
-
hci.
|
|
5721
|
-
hci.
|
|
5722
|
-
hci.
|
|
5723
|
-
hci.
|
|
5824
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: {
|
|
5825
|
+
hci.IoCapability.DISPLAY_ONLY: confirm,
|
|
5826
|
+
hci.IoCapability.DISPLAY_YES_NO: confirm,
|
|
5827
|
+
hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
|
|
5828
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5724
5829
|
},
|
|
5725
5830
|
}
|
|
5726
5831
|
|
|
@@ -5728,9 +5833,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5728
5833
|
|
|
5729
5834
|
async def reply() -> None:
|
|
5730
5835
|
try:
|
|
5731
|
-
if await
|
|
5732
|
-
connection, Connection.EVENT_DISCONNECTION, method()
|
|
5733
|
-
):
|
|
5836
|
+
if await connection.cancel_on_disconnection(method()):
|
|
5734
5837
|
await self.host.send_command(
|
|
5735
5838
|
hci.HCI_User_Confirmation_Request_Reply_Command(
|
|
5736
5839
|
bd_addr=connection.peer_address
|
|
@@ -5757,10 +5860,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5757
5860
|
|
|
5758
5861
|
async def reply() -> None:
|
|
5759
5862
|
try:
|
|
5760
|
-
number = await
|
|
5761
|
-
|
|
5762
|
-
Connection.EVENT_DISCONNECTION,
|
|
5763
|
-
pairing_config.delegate.get_number(),
|
|
5863
|
+
number = await connection.cancel_on_disconnection(
|
|
5864
|
+
pairing_config.delegate.get_number()
|
|
5764
5865
|
)
|
|
5765
5866
|
if number is not None:
|
|
5766
5867
|
await self.host.send_command(
|
|
@@ -5780,6 +5881,19 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5780
5881
|
|
|
5781
5882
|
utils.AsyncRunner.spawn(reply())
|
|
5782
5883
|
|
|
5884
|
+
# [Classic only]
|
|
5885
|
+
@host_event_handler
|
|
5886
|
+
@with_connection_from_handle
|
|
5887
|
+
def on_mode_change(
|
|
5888
|
+
self, connection: Connection, status: int, current_mode: int, interval: int
|
|
5889
|
+
):
|
|
5890
|
+
if status == hci.HCI_SUCCESS:
|
|
5891
|
+
connection.classic_mode = current_mode
|
|
5892
|
+
connection.classic_interval = interval
|
|
5893
|
+
connection.emit(connection.EVENT_MODE_CHANGE)
|
|
5894
|
+
else:
|
|
5895
|
+
connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
|
|
5896
|
+
|
|
5783
5897
|
# [Classic only]
|
|
5784
5898
|
@host_event_handler
|
|
5785
5899
|
@with_connection_from_address
|
|
@@ -5790,13 +5904,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5790
5904
|
io_capability = pairing_config.delegate.classic_io_capability
|
|
5791
5905
|
|
|
5792
5906
|
# Respond
|
|
5793
|
-
if io_capability == hci.
|
|
5907
|
+
if io_capability == hci.IoCapability.KEYBOARD_ONLY:
|
|
5794
5908
|
# Ask the user to enter a string
|
|
5795
5909
|
async def get_pin_code():
|
|
5796
|
-
pin_code = await
|
|
5797
|
-
|
|
5798
|
-
Connection.EVENT_DISCONNECTION,
|
|
5799
|
-
pairing_config.delegate.get_string(16),
|
|
5910
|
+
pin_code = await connection.cancel_on_disconnection(
|
|
5911
|
+
pairing_config.delegate.get_string(16)
|
|
5800
5912
|
)
|
|
5801
5913
|
|
|
5802
5914
|
if pin_code is not None:
|
|
@@ -5834,10 +5946,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5834
5946
|
pairing_config = self.pairing_config_factory(connection)
|
|
5835
5947
|
|
|
5836
5948
|
# Show the passkey to the user
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
Connection.EVENT_DISCONNECTION,
|
|
5840
|
-
pairing_config.delegate.display_number(passkey, digits=6),
|
|
5949
|
+
connection.cancel_on_disconnection(
|
|
5950
|
+
pairing_config.delegate.display_number(passkey, digits=6)
|
|
5841
5951
|
)
|
|
5842
5952
|
|
|
5843
5953
|
# [Classic only]
|
|
@@ -5924,24 +6034,63 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5924
6034
|
f'cis_id=[0x{cis_id:02X}] ***'
|
|
5925
6035
|
)
|
|
5926
6036
|
# LE_CIS_Established event doesn't provide info, so we must store them here.
|
|
5927
|
-
|
|
6037
|
+
cis_link = CisLink(
|
|
5928
6038
|
device=self,
|
|
5929
6039
|
acl_connection=acl_connection,
|
|
5930
6040
|
handle=cis_handle,
|
|
5931
6041
|
cig_id=cig_id,
|
|
5932
6042
|
cis_id=cis_id,
|
|
5933
6043
|
)
|
|
5934
|
-
self.
|
|
6044
|
+
self.cis_links[cis_handle] = cis_link
|
|
6045
|
+
acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
|
|
6046
|
+
self.emit(self.EVENT_CIS_REQUEST, cis_link)
|
|
5935
6047
|
|
|
5936
6048
|
# [LE only]
|
|
5937
6049
|
@host_event_handler
|
|
5938
6050
|
@utils.experimental('Only for testing')
|
|
5939
|
-
def on_cis_establishment(
|
|
6051
|
+
def on_cis_establishment(
|
|
6052
|
+
self,
|
|
6053
|
+
cis_handle: int,
|
|
6054
|
+
cig_sync_delay: int,
|
|
6055
|
+
cis_sync_delay: int,
|
|
6056
|
+
transport_latency_c_to_p: int,
|
|
6057
|
+
transport_latency_p_to_c: int,
|
|
6058
|
+
phy_c_to_p: int,
|
|
6059
|
+
phy_p_to_c: int,
|
|
6060
|
+
nse: int,
|
|
6061
|
+
bn_c_to_p: int,
|
|
6062
|
+
bn_p_to_c: int,
|
|
6063
|
+
ft_c_to_p: int,
|
|
6064
|
+
ft_p_to_c: int,
|
|
6065
|
+
max_pdu_c_to_p: int,
|
|
6066
|
+
max_pdu_p_to_c: int,
|
|
6067
|
+
iso_interval: int,
|
|
6068
|
+
) -> None:
|
|
6069
|
+
if cis_handle not in self.cis_links:
|
|
6070
|
+
logger.warning("CIS link not found")
|
|
6071
|
+
return
|
|
6072
|
+
|
|
5940
6073
|
cis_link = self.cis_links[cis_handle]
|
|
5941
6074
|
cis_link.state = CisLink.State.ESTABLISHED
|
|
5942
6075
|
|
|
5943
6076
|
assert cis_link.acl_connection
|
|
5944
6077
|
|
|
6078
|
+
# Update the CIS
|
|
6079
|
+
cis_link.cig_sync_delay = cig_sync_delay
|
|
6080
|
+
cis_link.cis_sync_delay = cis_sync_delay
|
|
6081
|
+
cis_link.transport_latency_c_to_p = transport_latency_c_to_p
|
|
6082
|
+
cis_link.transport_latency_p_to_c = transport_latency_p_to_c
|
|
6083
|
+
cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
|
|
6084
|
+
cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
|
|
6085
|
+
cis_link.nse = nse
|
|
6086
|
+
cis_link.bn_c_to_p = bn_c_to_p
|
|
6087
|
+
cis_link.bn_p_to_c = bn_p_to_c
|
|
6088
|
+
cis_link.ft_c_to_p = ft_c_to_p
|
|
6089
|
+
cis_link.ft_p_to_c = ft_p_to_c
|
|
6090
|
+
cis_link.max_pdu_c_to_p = max_pdu_c_to_p
|
|
6091
|
+
cis_link.max_pdu_p_to_c = max_pdu_p_to_c
|
|
6092
|
+
cis_link.iso_interval = iso_interval * 1.25
|
|
6093
|
+
|
|
5945
6094
|
logger.debug(
|
|
5946
6095
|
f'*** CIS Establishment '
|
|
5947
6096
|
f'{cis_link.acl_connection.peer_address}, '
|
|
@@ -5951,16 +6100,27 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5951
6100
|
)
|
|
5952
6101
|
|
|
5953
6102
|
cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
|
|
6103
|
+
cis_link.acl_connection.emit(
|
|
6104
|
+
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
|
|
6105
|
+
)
|
|
5954
6106
|
self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
|
|
5955
6107
|
|
|
5956
6108
|
# [LE only]
|
|
5957
6109
|
@host_event_handler
|
|
5958
6110
|
@utils.experimental('Only for testing')
|
|
5959
6111
|
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
|
6112
|
+
if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
|
|
6113
|
+
logger.warning("CIS link not found")
|
|
6114
|
+
return
|
|
6115
|
+
|
|
5960
6116
|
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6117
|
+
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
|
6118
|
+
cis_link.acl_connection.emit(
|
|
6119
|
+
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
|
|
6120
|
+
cis_link,
|
|
6121
|
+
status,
|
|
6122
|
+
)
|
|
6123
|
+
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
|
|
5964
6124
|
|
|
5965
6125
|
# [LE only]
|
|
5966
6126
|
@host_event_handler
|
|
@@ -5974,7 +6134,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5974
6134
|
@host_event_handler
|
|
5975
6135
|
@with_connection_from_handle
|
|
5976
6136
|
def on_connection_encryption_change(
|
|
5977
|
-
self, connection, encryption, encryption_key_size
|
|
6137
|
+
self, connection: Connection, encryption: int, encryption_key_size: int
|
|
5978
6138
|
):
|
|
5979
6139
|
logger.debug(
|
|
5980
6140
|
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
|
@@ -5987,14 +6147,14 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5987
6147
|
if (
|
|
5988
6148
|
not connection.authenticated
|
|
5989
6149
|
and connection.transport == PhysicalTransport.BR_EDR
|
|
5990
|
-
and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
|
6150
|
+
and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
|
5991
6151
|
):
|
|
5992
6152
|
connection.authenticated = True
|
|
5993
6153
|
connection.sc = True
|
|
5994
6154
|
if (
|
|
5995
6155
|
not connection.authenticated
|
|
5996
6156
|
and connection.transport == PhysicalTransport.LE
|
|
5997
|
-
and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
|
|
6157
|
+
and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
|
|
5998
6158
|
):
|
|
5999
6159
|
connection.authenticated = True
|
|
6000
6160
|
connection.sc = True
|
|
@@ -6021,13 +6181,19 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6021
6181
|
|
|
6022
6182
|
@host_event_handler
|
|
6023
6183
|
@with_connection_from_handle
|
|
6024
|
-
def on_connection_parameters_update(
|
|
6184
|
+
def on_connection_parameters_update(
|
|
6185
|
+
self, connection: Connection, connection_parameters: core.ConnectionParameters
|
|
6186
|
+
):
|
|
6025
6187
|
logger.debug(
|
|
6026
6188
|
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
|
6027
6189
|
f'{connection.peer_address} as {connection.role_name}, '
|
|
6028
6190
|
f'{connection_parameters}'
|
|
6029
6191
|
)
|
|
6030
|
-
connection.parameters =
|
|
6192
|
+
connection.parameters = Connection.Parameters(
|
|
6193
|
+
connection_parameters.connection_interval * 1.25,
|
|
6194
|
+
connection_parameters.peripheral_latency,
|
|
6195
|
+
connection_parameters.supervision_timeout * 10.0,
|
|
6196
|
+
)
|
|
6031
6197
|
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
|
6032
6198
|
|
|
6033
6199
|
@host_event_handler
|