bumble 0.0.212__py3-none-any.whl → 0.0.214__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 +14 -11
- bumble/apps/bench.py +482 -37
- bumble/apps/console.py +3 -3
- bumble/apps/controller_info.py +44 -12
- bumble/apps/controller_loopback.py +7 -7
- bumble/apps/controllers.py +4 -5
- bumble/apps/device_info.py +4 -5
- bumble/apps/gatt_dump.py +5 -5
- bumble/apps/gg_bridge.py +5 -5
- bumble/apps/hci_bridge.py +5 -4
- bumble/apps/l2cap_bridge.py +5 -5
- bumble/apps/lea_unicast/app.py +8 -3
- bumble/apps/pair.py +19 -11
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +2 -3
- bumble/apps/rfcomm_bridge.py +3 -4
- bumble/apps/scan.py +4 -5
- bumble/apps/show.py +6 -4
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +123 -19
- bumble/apps/unbond.py +2 -3
- bumble/apps/usb_probe.py +2 -3
- 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 +42 -54
- bumble/colors.py +2 -2
- bumble/controller.py +174 -45
- bumble/device.py +398 -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 +2648 -2909
- bumble/helpers.py +4 -5
- bumble/hfp.py +32 -37
- bumble/host.py +104 -35
- bumble/keys.py +5 -5
- bumble/l2cap.py +312 -409
- bumble/link.py +16 -280
- bumble/logging.py +65 -0
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +2 -2
- 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/ams.py +404 -0
- bumble/profiles/ascs.py +142 -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 +62 -63
- bumble/tools/intel_util.py +3 -2
- bumble/tools/rtk_util.py +6 -5
- 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.214.dist-info}/METADATA +4 -3
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
- {bumble-0.0.212.dist-info → bumble-0.0.214.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.214.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.212.dist-info → bumble-0.0.214.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
|
+
)
|
|
214
220
|
),
|
|
215
|
-
is_directed=
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
)
|
|
231
|
+
),
|
|
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,11 @@ 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"
|
|
1755
|
+
EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
|
|
1756
|
+
EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
|
|
1632
1757
|
|
|
1633
1758
|
@utils.composite_listener
|
|
1634
1759
|
class Listener:
|
|
@@ -1664,6 +1789,12 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1664
1789
|
connection_interval: float # Connection interval, in milliseconds. [LE only]
|
|
1665
1790
|
peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
|
|
1666
1791
|
supervision_timeout: float # Supervision timeout, in milliseconds.
|
|
1792
|
+
subrate_factor: int = (
|
|
1793
|
+
1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
|
1794
|
+
)
|
|
1795
|
+
continuation_number: int = (
|
|
1796
|
+
0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
|
1797
|
+
)
|
|
1667
1798
|
|
|
1668
1799
|
def __init__(
|
|
1669
1800
|
self,
|
|
@@ -1884,6 +2015,12 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1884
2015
|
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1885
2016
|
return self.device.host.get_data_packet_queue(self.handle)
|
|
1886
2017
|
|
|
2018
|
+
def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
|
2019
|
+
"""
|
|
2020
|
+
Helper method to call `utils.cancel_on_event` for the 'disconnection' event
|
|
2021
|
+
"""
|
|
2022
|
+
return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
|
|
2023
|
+
|
|
1887
2024
|
async def __aenter__(self):
|
|
1888
2025
|
return self
|
|
1889
2026
|
|
|
@@ -1929,6 +2066,7 @@ class DeviceConfiguration:
|
|
|
1929
2066
|
le_simultaneous_enabled: bool = False
|
|
1930
2067
|
le_privacy_enabled: bool = False
|
|
1931
2068
|
le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
|
|
2069
|
+
le_subrate_enabled: bool = False
|
|
1932
2070
|
classic_enabled: bool = False
|
|
1933
2071
|
classic_sc_enabled: bool = True
|
|
1934
2072
|
classic_ssp_enabled: bool = True
|
|
@@ -1954,9 +2092,9 @@ class DeviceConfiguration:
|
|
|
1954
2092
|
gatt_service_enabled: bool = True
|
|
1955
2093
|
|
|
1956
2094
|
def __post_init__(self) -> None:
|
|
1957
|
-
self.gatt_services: list[
|
|
2095
|
+
self.gatt_services: list[dict[str, Any]] = []
|
|
1958
2096
|
|
|
1959
|
-
def load_from_dict(self, config:
|
|
2097
|
+
def load_from_dict(self, config: dict[str, Any]) -> None:
|
|
1960
2098
|
config = copy.deepcopy(config)
|
|
1961
2099
|
|
|
1962
2100
|
# Load simple properties
|
|
@@ -2016,13 +2154,13 @@ class DeviceConfiguration:
|
|
|
2016
2154
|
self.load_from_dict(json.load(file))
|
|
2017
2155
|
|
|
2018
2156
|
@classmethod
|
|
2019
|
-
def from_file(cls:
|
|
2157
|
+
def from_file(cls: type[Self], filename: str) -> Self:
|
|
2020
2158
|
config = cls()
|
|
2021
2159
|
config.load_from_file(filename)
|
|
2022
2160
|
return config
|
|
2023
2161
|
|
|
2024
2162
|
@classmethod
|
|
2025
|
-
def from_dict(cls:
|
|
2163
|
+
def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
|
|
2026
2164
|
device_config = cls()
|
|
2027
2165
|
device_config.load_from_dict(config)
|
|
2028
2166
|
return device_config
|
|
@@ -2119,22 +2257,22 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2119
2257
|
advertising_data: bytes
|
|
2120
2258
|
scan_response_data: bytes
|
|
2121
2259
|
cs_capabilities: ChannelSoundingCapabilities | None = None
|
|
2122
|
-
connections:
|
|
2123
|
-
pending_connections:
|
|
2124
|
-
classic_pending_accepts:
|
|
2260
|
+
connections: dict[int, Connection]
|
|
2261
|
+
pending_connections: dict[hci.Address, Connection]
|
|
2262
|
+
classic_pending_accepts: dict[
|
|
2125
2263
|
hci.Address,
|
|
2126
2264
|
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
|
2127
2265
|
]
|
|
2128
|
-
advertisement_accumulators:
|
|
2266
|
+
advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
|
|
2129
2267
|
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
|
|
2130
2268
|
config: DeviceConfiguration
|
|
2131
2269
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
2132
|
-
sco_links:
|
|
2133
|
-
cis_links:
|
|
2270
|
+
sco_links: dict[int, ScoLink]
|
|
2271
|
+
cis_links: dict[int, CisLink]
|
|
2134
2272
|
bigs: dict[int, Big]
|
|
2135
2273
|
bis_links: dict[int, BisLink]
|
|
2136
2274
|
big_syncs: dict[int, BigSync]
|
|
2137
|
-
_pending_cis:
|
|
2275
|
+
_pending_cis: dict[int, tuple[int, int]]
|
|
2138
2276
|
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
2139
2277
|
|
|
2140
2278
|
EVENT_ADVERTISEMENT = "advertisement"
|
|
@@ -2281,6 +2419,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2281
2419
|
self.le_privacy_enabled = config.le_privacy_enabled
|
|
2282
2420
|
self.le_rpa_timeout = config.le_rpa_timeout
|
|
2283
2421
|
self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
|
|
2422
|
+
self.le_subrate_enabled = config.le_subrate_enabled
|
|
2284
2423
|
self.classic_enabled = config.classic_enabled
|
|
2285
2424
|
self.cis_enabled = config.cis_enabled
|
|
2286
2425
|
self.classic_sc_enabled = config.classic_sc_enabled
|
|
@@ -2294,8 +2433,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2294
2433
|
self.address_generation_offload = config.address_generation_offload
|
|
2295
2434
|
|
|
2296
2435
|
# Extended advertising.
|
|
2297
|
-
self.extended_advertising_sets:
|
|
2298
|
-
self.connecting_extended_advertising_sets:
|
|
2436
|
+
self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
|
|
2437
|
+
self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
|
|
2299
2438
|
|
|
2300
2439
|
# Legacy advertising.
|
|
2301
2440
|
# The advertising and scan response data, as well as the advertising interval
|
|
@@ -2660,6 +2799,15 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2660
2799
|
check_result=True,
|
|
2661
2800
|
)
|
|
2662
2801
|
|
|
2802
|
+
if self.le_subrate_enabled:
|
|
2803
|
+
await self.send_command(
|
|
2804
|
+
hci.HCI_LE_Set_Host_Feature_Command(
|
|
2805
|
+
bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
|
|
2806
|
+
bit_value=1,
|
|
2807
|
+
),
|
|
2808
|
+
check_result=True,
|
|
2809
|
+
)
|
|
2810
|
+
|
|
2663
2811
|
if self.config.channel_sounding_enabled:
|
|
2664
2812
|
await self.send_command(
|
|
2665
2813
|
hci.HCI_LE_Set_Host_Feature_Command(
|
|
@@ -4271,11 +4419,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4271
4419
|
self.smp_manager.pairing_config_factory = pairing_config_factory
|
|
4272
4420
|
|
|
4273
4421
|
@property
|
|
4274
|
-
def smp_session_proxy(self) ->
|
|
4422
|
+
def smp_session_proxy(self) -> type[smp.Session]:
|
|
4275
4423
|
return self.smp_manager.session_proxy
|
|
4276
4424
|
|
|
4277
4425
|
@smp_session_proxy.setter
|
|
4278
|
-
def smp_session_proxy(self, session_proxy:
|
|
4426
|
+
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
|
|
4279
4427
|
self.smp_manager.session_proxy = session_proxy
|
|
4280
4428
|
|
|
4281
4429
|
async def pair(self, connection):
|
|
@@ -4359,9 +4507,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4359
4507
|
raise hci.HCI_StatusError(result)
|
|
4360
4508
|
|
|
4361
4509
|
# Wait for the authentication to complete
|
|
4362
|
-
await
|
|
4363
|
-
connection, Connection.EVENT_DISCONNECTION, pending_authentication
|
|
4364
|
-
)
|
|
4510
|
+
await connection.cancel_on_disconnection(pending_authentication)
|
|
4365
4511
|
finally:
|
|
4366
4512
|
connection.remove_listener(
|
|
4367
4513
|
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
|
|
@@ -4448,9 +4594,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4448
4594
|
raise hci.HCI_StatusError(result)
|
|
4449
4595
|
|
|
4450
4596
|
# Wait for the result
|
|
4451
|
-
await
|
|
4452
|
-
connection, Connection.EVENT_DISCONNECTION, pending_encryption
|
|
4453
|
-
)
|
|
4597
|
+
await connection.cancel_on_disconnection(pending_encryption)
|
|
4454
4598
|
finally:
|
|
4455
4599
|
connection.remove_listener(
|
|
4456
4600
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
|
|
@@ -4494,9 +4638,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4494
4638
|
f'{hci.HCI_Constant.error_name(result.status)}'
|
|
4495
4639
|
)
|
|
4496
4640
|
raise hci.HCI_StatusError(result)
|
|
4497
|
-
await
|
|
4498
|
-
connection, Connection.EVENT_DISCONNECTION, pending_role_change
|
|
4499
|
-
)
|
|
4641
|
+
await connection.cancel_on_disconnection(pending_role_change)
|
|
4500
4642
|
finally:
|
|
4501
4643
|
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
|
|
4502
4644
|
connection.remove_listener(
|
|
@@ -4556,48 +4698,39 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4556
4698
|
@utils.experimental('Only for testing.')
|
|
4557
4699
|
async def setup_cig(
|
|
4558
4700
|
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],
|
|
4701
|
+
parameters: CigParameters,
|
|
4566
4702
|
) -> list[int]:
|
|
4567
4703
|
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
|
4568
4704
|
|
|
4569
4705
|
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).
|
|
4706
|
+
parameters: CIG parameters.
|
|
4578
4707
|
|
|
4579
4708
|
Returns:
|
|
4580
4709
|
List of created CIS handles corresponding to the same order of [cid_id].
|
|
4581
4710
|
"""
|
|
4582
|
-
num_cis = len(
|
|
4711
|
+
num_cis = len(parameters.cis_parameters)
|
|
4583
4712
|
|
|
4584
4713
|
response = await self.send_command(
|
|
4585
4714
|
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
|
-
|
|
4715
|
+
cig_id=parameters.cig_id,
|
|
4716
|
+
sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
|
|
4717
|
+
sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
|
|
4718
|
+
worst_case_sca=parameters.worst_case_sca,
|
|
4719
|
+
packing=int(parameters.packing),
|
|
4720
|
+
framing=int(parameters.framing),
|
|
4721
|
+
max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
|
|
4722
|
+
max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
|
|
4723
|
+
cis_id=[cis.cis_id for cis in parameters.cis_parameters],
|
|
4724
|
+
max_sdu_c_to_p=[
|
|
4725
|
+
cis.max_sdu_c_to_p for cis in parameters.cis_parameters
|
|
4726
|
+
],
|
|
4727
|
+
max_sdu_p_to_c=[
|
|
4728
|
+
cis.max_sdu_p_to_c for cis in parameters.cis_parameters
|
|
4729
|
+
],
|
|
4730
|
+
phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
|
|
4731
|
+
phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
|
|
4732
|
+
rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
|
|
4733
|
+
rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
|
|
4601
4734
|
),
|
|
4602
4735
|
check_result=True,
|
|
4603
4736
|
)
|
|
@@ -4605,19 +4738,17 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4605
4738
|
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
|
|
4606
4739
|
# Server, so here it only provides a basic functionality for testing.
|
|
4607
4740
|
cis_handles = response.return_parameters.connection_handle[:]
|
|
4608
|
-
for
|
|
4609
|
-
self._pending_cis[cis_handle] = (
|
|
4741
|
+
for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
|
|
4742
|
+
self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
|
|
4610
4743
|
|
|
4611
4744
|
return cis_handles
|
|
4612
4745
|
|
|
4613
4746
|
# [LE only]
|
|
4614
4747
|
@utils.experimental('Only for testing.')
|
|
4615
4748
|
async def create_cis(
|
|
4616
|
-
self, cis_acl_pairs: Sequence[tuple[int,
|
|
4749
|
+
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
|
|
4617
4750
|
) -> list[CisLink]:
|
|
4618
|
-
for cis_handle,
|
|
4619
|
-
acl_connection = self.lookup_connection(acl_handle)
|
|
4620
|
-
assert acl_connection
|
|
4751
|
+
for cis_handle, acl_connection in cis_acl_pairs:
|
|
4621
4752
|
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
|
4622
4753
|
self.cis_links[cis_handle] = CisLink(
|
|
4623
4754
|
device=self,
|
|
@@ -4637,8 +4768,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4637
4768
|
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
|
4638
4769
|
pending_future.set_result(cis_link)
|
|
4639
4770
|
|
|
4640
|
-
def on_cis_establishment_failure(
|
|
4641
|
-
if pending_future := pending_cis_establishments.get(
|
|
4771
|
+
def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
|
|
4772
|
+
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
|
4642
4773
|
pending_future.set_exception(hci.HCI_Error(status))
|
|
4643
4774
|
|
|
4644
4775
|
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
|
|
@@ -4648,7 +4779,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4648
4779
|
await self.send_command(
|
|
4649
4780
|
hci.HCI_LE_Create_CIS_Command(
|
|
4650
4781
|
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
|
4651
|
-
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
|
4782
|
+
acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
|
|
4652
4783
|
),
|
|
4653
4784
|
check_result=True,
|
|
4654
4785
|
)
|
|
@@ -4657,26 +4788,21 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4657
4788
|
|
|
4658
4789
|
# [LE only]
|
|
4659
4790
|
@utils.experimental('Only for testing.')
|
|
4660
|
-
async def accept_cis_request(self,
|
|
4791
|
+
async def accept_cis_request(self, cis_link: CisLink) -> None:
|
|
4661
4792
|
"""[LE Only] Accepts an incoming CIS request.
|
|
4662
4793
|
|
|
4663
|
-
|
|
4664
|
-
|
|
4794
|
+
This method returns when the CIS is established, or raises an exception if
|
|
4795
|
+
the CIS establishment fails.
|
|
4665
4796
|
|
|
4666
4797
|
Args:
|
|
4667
4798
|
handle: CIS handle to accept.
|
|
4668
|
-
|
|
4669
|
-
Returns:
|
|
4670
|
-
CIS link object on the given handle.
|
|
4671
4799
|
"""
|
|
4672
|
-
if not (cis_link := self.cis_links.get(handle)):
|
|
4673
|
-
raise InvalidStateError(f'No pending CIS request of handle {handle}')
|
|
4674
4800
|
|
|
4675
4801
|
# There might be multiple ASE sharing a CIS channel.
|
|
4676
4802
|
# If one of them has accepted the request, the others should just leverage it.
|
|
4677
4803
|
async with self._cis_lock:
|
|
4678
4804
|
if cis_link.state == CisLink.State.ESTABLISHED:
|
|
4679
|
-
return
|
|
4805
|
+
return
|
|
4680
4806
|
|
|
4681
4807
|
with closing(utils.EventWatcher()) as watcher:
|
|
4682
4808
|
pending_establishment = asyncio.get_running_loop().create_future()
|
|
@@ -4695,26 +4821,24 @@ class Device(utils.CompositeEventEmitter):
|
|
|
4695
4821
|
)
|
|
4696
4822
|
|
|
4697
4823
|
await self.send_command(
|
|
4698
|
-
hci.HCI_LE_Accept_CIS_Request_Command(
|
|
4824
|
+
hci.HCI_LE_Accept_CIS_Request_Command(
|
|
4825
|
+
connection_handle=cis_link.handle
|
|
4826
|
+
),
|
|
4699
4827
|
check_result=True,
|
|
4700
4828
|
)
|
|
4701
4829
|
|
|
4702
4830
|
await pending_establishment
|
|
4703
|
-
return cis_link
|
|
4704
|
-
|
|
4705
|
-
# Mypy believes this is reachable when context is an ExitStack.
|
|
4706
|
-
raise UnreachableError()
|
|
4707
4831
|
|
|
4708
4832
|
# [LE only]
|
|
4709
4833
|
@utils.experimental('Only for testing.')
|
|
4710
4834
|
async def reject_cis_request(
|
|
4711
4835
|
self,
|
|
4712
|
-
|
|
4836
|
+
cis_link: CisLink,
|
|
4713
4837
|
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
4714
4838
|
) -> None:
|
|
4715
4839
|
await self.send_command(
|
|
4716
4840
|
hci.HCI_LE_Reject_CIS_Request_Command(
|
|
4717
|
-
connection_handle=handle, reason=reason
|
|
4841
|
+
connection_handle=cis_link.handle, reason=reason
|
|
4718
4842
|
),
|
|
4719
4843
|
check_result=True,
|
|
4720
4844
|
)
|
|
@@ -5071,8 +5195,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5071
5195
|
# Store the keys in the key store
|
|
5072
5196
|
if self.keystore:
|
|
5073
5197
|
authenticated = key_type in (
|
|
5074
|
-
hci.
|
|
5075
|
-
hci.
|
|
5198
|
+
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
|
5199
|
+
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
|
5076
5200
|
)
|
|
5077
5201
|
pairing_keys = PairingKeys(
|
|
5078
5202
|
link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
|
|
@@ -5252,7 +5376,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5252
5376
|
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
|
5253
5377
|
big.big_sync_delay = big_sync_delay
|
|
5254
5378
|
big.transport_latency_big = transport_latency_big
|
|
5255
|
-
big.phy = phy
|
|
5379
|
+
big.phy = hci.Phy(phy)
|
|
5256
5380
|
big.nse = nse
|
|
5257
5381
|
big.bn = bn
|
|
5258
5382
|
big.pto = pto
|
|
@@ -5519,8 +5643,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5519
5643
|
|
|
5520
5644
|
# Handle SCO request.
|
|
5521
5645
|
if link_type in (
|
|
5522
|
-
hci.HCI_Connection_Complete_Event.
|
|
5523
|
-
hci.HCI_Connection_Complete_Event.
|
|
5646
|
+
hci.HCI_Connection_Complete_Event.LinkType.SCO,
|
|
5647
|
+
hci.HCI_Connection_Complete_Event.LinkType.ESCO,
|
|
5524
5648
|
):
|
|
5525
5649
|
if connection := self.find_connection_by_bd_addr(
|
|
5526
5650
|
bd_addr, transport=PhysicalTransport.BR_EDR
|
|
@@ -5628,7 +5752,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5628
5752
|
# [Classic only]
|
|
5629
5753
|
@host_event_handler
|
|
5630
5754
|
@with_connection_from_address
|
|
5631
|
-
def on_authentication_io_capability_request(self, connection):
|
|
5755
|
+
def on_authentication_io_capability_request(self, connection: Connection):
|
|
5632
5756
|
# Ask what the pairing config should be for this connection
|
|
5633
5757
|
pairing_config = self.pairing_config_factory(connection)
|
|
5634
5758
|
|
|
@@ -5636,13 +5760,13 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5636
5760
|
authentication_requirements = (
|
|
5637
5761
|
# No Bonding
|
|
5638
5762
|
(
|
|
5639
|
-
hci.
|
|
5640
|
-
hci.
|
|
5763
|
+
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
|
|
5764
|
+
hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
|
|
5641
5765
|
),
|
|
5642
5766
|
# General Bonding
|
|
5643
5767
|
(
|
|
5644
|
-
hci.
|
|
5645
|
-
hci.
|
|
5768
|
+
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
|
|
5769
|
+
hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
|
|
5646
5770
|
),
|
|
5647
5771
|
)[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
|
|
5648
5772
|
|
|
@@ -5697,30 +5821,30 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5697
5821
|
raise UnreachableError()
|
|
5698
5822
|
|
|
5699
5823
|
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6
|
|
5700
|
-
methods = {
|
|
5701
|
-
hci.
|
|
5702
|
-
hci.
|
|
5703
|
-
hci.
|
|
5704
|
-
hci.
|
|
5705
|
-
hci.
|
|
5824
|
+
methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
|
|
5825
|
+
hci.IoCapability.DISPLAY_ONLY: {
|
|
5826
|
+
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
|
5827
|
+
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
|
5828
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5829
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5706
5830
|
},
|
|
5707
|
-
hci.
|
|
5708
|
-
hci.
|
|
5709
|
-
hci.
|
|
5710
|
-
hci.
|
|
5711
|
-
hci.
|
|
5831
|
+
hci.IoCapability.DISPLAY_YES_NO: {
|
|
5832
|
+
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
|
5833
|
+
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
|
5834
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5835
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5712
5836
|
},
|
|
5713
|
-
hci.
|
|
5714
|
-
hci.
|
|
5715
|
-
hci.
|
|
5716
|
-
hci.
|
|
5717
|
-
hci.
|
|
5837
|
+
hci.IoCapability.KEYBOARD_ONLY: {
|
|
5838
|
+
hci.IoCapability.DISPLAY_ONLY: na,
|
|
5839
|
+
hci.IoCapability.DISPLAY_YES_NO: na,
|
|
5840
|
+
hci.IoCapability.KEYBOARD_ONLY: na,
|
|
5841
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5718
5842
|
},
|
|
5719
|
-
hci.
|
|
5720
|
-
hci.
|
|
5721
|
-
hci.
|
|
5722
|
-
hci.
|
|
5723
|
-
hci.
|
|
5843
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: {
|
|
5844
|
+
hci.IoCapability.DISPLAY_ONLY: confirm,
|
|
5845
|
+
hci.IoCapability.DISPLAY_YES_NO: confirm,
|
|
5846
|
+
hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
|
|
5847
|
+
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
|
5724
5848
|
},
|
|
5725
5849
|
}
|
|
5726
5850
|
|
|
@@ -5728,9 +5852,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5728
5852
|
|
|
5729
5853
|
async def reply() -> None:
|
|
5730
5854
|
try:
|
|
5731
|
-
if await
|
|
5732
|
-
connection, Connection.EVENT_DISCONNECTION, method()
|
|
5733
|
-
):
|
|
5855
|
+
if await connection.cancel_on_disconnection(method()):
|
|
5734
5856
|
await self.host.send_command(
|
|
5735
5857
|
hci.HCI_User_Confirmation_Request_Reply_Command(
|
|
5736
5858
|
bd_addr=connection.peer_address
|
|
@@ -5757,10 +5879,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5757
5879
|
|
|
5758
5880
|
async def reply() -> None:
|
|
5759
5881
|
try:
|
|
5760
|
-
number = await
|
|
5761
|
-
|
|
5762
|
-
Connection.EVENT_DISCONNECTION,
|
|
5763
|
-
pairing_config.delegate.get_number(),
|
|
5882
|
+
number = await connection.cancel_on_disconnection(
|
|
5883
|
+
pairing_config.delegate.get_number()
|
|
5764
5884
|
)
|
|
5765
5885
|
if number is not None:
|
|
5766
5886
|
await self.host.send_command(
|
|
@@ -5780,6 +5900,19 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5780
5900
|
|
|
5781
5901
|
utils.AsyncRunner.spawn(reply())
|
|
5782
5902
|
|
|
5903
|
+
# [Classic only]
|
|
5904
|
+
@host_event_handler
|
|
5905
|
+
@with_connection_from_handle
|
|
5906
|
+
def on_mode_change(
|
|
5907
|
+
self, connection: Connection, status: int, current_mode: int, interval: int
|
|
5908
|
+
):
|
|
5909
|
+
if status == hci.HCI_SUCCESS:
|
|
5910
|
+
connection.classic_mode = current_mode
|
|
5911
|
+
connection.classic_interval = interval
|
|
5912
|
+
connection.emit(connection.EVENT_MODE_CHANGE)
|
|
5913
|
+
else:
|
|
5914
|
+
connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
|
|
5915
|
+
|
|
5783
5916
|
# [Classic only]
|
|
5784
5917
|
@host_event_handler
|
|
5785
5918
|
@with_connection_from_address
|
|
@@ -5790,13 +5923,11 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5790
5923
|
io_capability = pairing_config.delegate.classic_io_capability
|
|
5791
5924
|
|
|
5792
5925
|
# Respond
|
|
5793
|
-
if io_capability == hci.
|
|
5926
|
+
if io_capability == hci.IoCapability.KEYBOARD_ONLY:
|
|
5794
5927
|
# Ask the user to enter a string
|
|
5795
5928
|
async def get_pin_code():
|
|
5796
|
-
pin_code = await
|
|
5797
|
-
|
|
5798
|
-
Connection.EVENT_DISCONNECTION,
|
|
5799
|
-
pairing_config.delegate.get_string(16),
|
|
5929
|
+
pin_code = await connection.cancel_on_disconnection(
|
|
5930
|
+
pairing_config.delegate.get_string(16)
|
|
5800
5931
|
)
|
|
5801
5932
|
|
|
5802
5933
|
if pin_code is not None:
|
|
@@ -5834,10 +5965,8 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5834
5965
|
pairing_config = self.pairing_config_factory(connection)
|
|
5835
5966
|
|
|
5836
5967
|
# Show the passkey to the user
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
Connection.EVENT_DISCONNECTION,
|
|
5840
|
-
pairing_config.delegate.display_number(passkey, digits=6),
|
|
5968
|
+
connection.cancel_on_disconnection(
|
|
5969
|
+
pairing_config.delegate.display_number(passkey, digits=6)
|
|
5841
5970
|
)
|
|
5842
5971
|
|
|
5843
5972
|
# [Classic only]
|
|
@@ -5924,24 +6053,63 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5924
6053
|
f'cis_id=[0x{cis_id:02X}] ***'
|
|
5925
6054
|
)
|
|
5926
6055
|
# LE_CIS_Established event doesn't provide info, so we must store them here.
|
|
5927
|
-
|
|
6056
|
+
cis_link = CisLink(
|
|
5928
6057
|
device=self,
|
|
5929
6058
|
acl_connection=acl_connection,
|
|
5930
6059
|
handle=cis_handle,
|
|
5931
6060
|
cig_id=cig_id,
|
|
5932
6061
|
cis_id=cis_id,
|
|
5933
6062
|
)
|
|
5934
|
-
self.
|
|
6063
|
+
self.cis_links[cis_handle] = cis_link
|
|
6064
|
+
acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
|
|
6065
|
+
self.emit(self.EVENT_CIS_REQUEST, cis_link)
|
|
5935
6066
|
|
|
5936
6067
|
# [LE only]
|
|
5937
6068
|
@host_event_handler
|
|
5938
6069
|
@utils.experimental('Only for testing')
|
|
5939
|
-
def on_cis_establishment(
|
|
6070
|
+
def on_cis_establishment(
|
|
6071
|
+
self,
|
|
6072
|
+
cis_handle: int,
|
|
6073
|
+
cig_sync_delay: int,
|
|
6074
|
+
cis_sync_delay: int,
|
|
6075
|
+
transport_latency_c_to_p: int,
|
|
6076
|
+
transport_latency_p_to_c: int,
|
|
6077
|
+
phy_c_to_p: int,
|
|
6078
|
+
phy_p_to_c: int,
|
|
6079
|
+
nse: int,
|
|
6080
|
+
bn_c_to_p: int,
|
|
6081
|
+
bn_p_to_c: int,
|
|
6082
|
+
ft_c_to_p: int,
|
|
6083
|
+
ft_p_to_c: int,
|
|
6084
|
+
max_pdu_c_to_p: int,
|
|
6085
|
+
max_pdu_p_to_c: int,
|
|
6086
|
+
iso_interval: int,
|
|
6087
|
+
) -> None:
|
|
6088
|
+
if cis_handle not in self.cis_links:
|
|
6089
|
+
logger.warning("CIS link not found")
|
|
6090
|
+
return
|
|
6091
|
+
|
|
5940
6092
|
cis_link = self.cis_links[cis_handle]
|
|
5941
6093
|
cis_link.state = CisLink.State.ESTABLISHED
|
|
5942
6094
|
|
|
5943
6095
|
assert cis_link.acl_connection
|
|
5944
6096
|
|
|
6097
|
+
# Update the CIS
|
|
6098
|
+
cis_link.cig_sync_delay = cig_sync_delay
|
|
6099
|
+
cis_link.cis_sync_delay = cis_sync_delay
|
|
6100
|
+
cis_link.transport_latency_c_to_p = transport_latency_c_to_p
|
|
6101
|
+
cis_link.transport_latency_p_to_c = transport_latency_p_to_c
|
|
6102
|
+
cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
|
|
6103
|
+
cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
|
|
6104
|
+
cis_link.nse = nse
|
|
6105
|
+
cis_link.bn_c_to_p = bn_c_to_p
|
|
6106
|
+
cis_link.bn_p_to_c = bn_p_to_c
|
|
6107
|
+
cis_link.ft_c_to_p = ft_c_to_p
|
|
6108
|
+
cis_link.ft_p_to_c = ft_p_to_c
|
|
6109
|
+
cis_link.max_pdu_c_to_p = max_pdu_c_to_p
|
|
6110
|
+
cis_link.max_pdu_p_to_c = max_pdu_p_to_c
|
|
6111
|
+
cis_link.iso_interval = iso_interval * 1.25
|
|
6112
|
+
|
|
5945
6113
|
logger.debug(
|
|
5946
6114
|
f'*** CIS Establishment '
|
|
5947
6115
|
f'{cis_link.acl_connection.peer_address}, '
|
|
@@ -5951,16 +6119,27 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5951
6119
|
)
|
|
5952
6120
|
|
|
5953
6121
|
cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
|
|
6122
|
+
cis_link.acl_connection.emit(
|
|
6123
|
+
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
|
|
6124
|
+
)
|
|
5954
6125
|
self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
|
|
5955
6126
|
|
|
5956
6127
|
# [LE only]
|
|
5957
6128
|
@host_event_handler
|
|
5958
6129
|
@utils.experimental('Only for testing')
|
|
5959
6130
|
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
|
6131
|
+
if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
|
|
6132
|
+
logger.warning("CIS link not found")
|
|
6133
|
+
return
|
|
6134
|
+
|
|
5960
6135
|
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6136
|
+
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
|
6137
|
+
cis_link.acl_connection.emit(
|
|
6138
|
+
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
|
|
6139
|
+
cis_link,
|
|
6140
|
+
status,
|
|
6141
|
+
)
|
|
6142
|
+
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
|
|
5964
6143
|
|
|
5965
6144
|
# [LE only]
|
|
5966
6145
|
@host_event_handler
|
|
@@ -5974,7 +6153,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5974
6153
|
@host_event_handler
|
|
5975
6154
|
@with_connection_from_handle
|
|
5976
6155
|
def on_connection_encryption_change(
|
|
5977
|
-
self, connection, encryption, encryption_key_size
|
|
6156
|
+
self, connection: Connection, encryption: int, encryption_key_size: int
|
|
5978
6157
|
):
|
|
5979
6158
|
logger.debug(
|
|
5980
6159
|
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
|
@@ -5987,14 +6166,14 @@ class Device(utils.CompositeEventEmitter):
|
|
|
5987
6166
|
if (
|
|
5988
6167
|
not connection.authenticated
|
|
5989
6168
|
and connection.transport == PhysicalTransport.BR_EDR
|
|
5990
|
-
and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
|
6169
|
+
and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
|
5991
6170
|
):
|
|
5992
6171
|
connection.authenticated = True
|
|
5993
6172
|
connection.sc = True
|
|
5994
6173
|
if (
|
|
5995
6174
|
not connection.authenticated
|
|
5996
6175
|
and connection.transport == PhysicalTransport.LE
|
|
5997
|
-
and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
|
|
6176
|
+
and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
|
|
5998
6177
|
):
|
|
5999
6178
|
connection.authenticated = True
|
|
6000
6179
|
connection.sc = True
|
|
@@ -6021,13 +6200,31 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6021
6200
|
|
|
6022
6201
|
@host_event_handler
|
|
6023
6202
|
@with_connection_from_handle
|
|
6024
|
-
def on_connection_parameters_update(
|
|
6203
|
+
def on_connection_parameters_update(
|
|
6204
|
+
self, connection: Connection, connection_parameters: core.ConnectionParameters
|
|
6205
|
+
):
|
|
6025
6206
|
logger.debug(
|
|
6026
6207
|
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
|
6027
6208
|
f'{connection.peer_address} as {connection.role_name}, '
|
|
6028
6209
|
f'{connection_parameters}'
|
|
6029
6210
|
)
|
|
6030
|
-
|
|
6211
|
+
if (
|
|
6212
|
+
connection.parameters.connection_interval
|
|
6213
|
+
!= connection_parameters.connection_interval * 1.25
|
|
6214
|
+
):
|
|
6215
|
+
connection.parameters = Connection.Parameters(
|
|
6216
|
+
connection_parameters.connection_interval * 1.25,
|
|
6217
|
+
connection_parameters.peripheral_latency,
|
|
6218
|
+
connection_parameters.supervision_timeout * 10.0,
|
|
6219
|
+
)
|
|
6220
|
+
else:
|
|
6221
|
+
connection.parameters = Connection.Parameters(
|
|
6222
|
+
connection_parameters.connection_interval * 1.25,
|
|
6223
|
+
connection_parameters.peripheral_latency,
|
|
6224
|
+
connection_parameters.supervision_timeout * 10.0,
|
|
6225
|
+
connection.parameters.subrate_factor,
|
|
6226
|
+
connection.parameters.continuation_number,
|
|
6227
|
+
)
|
|
6031
6228
|
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
|
6032
6229
|
|
|
6033
6230
|
@host_event_handler
|
|
@@ -6060,6 +6257,25 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6060
6257
|
)
|
|
6061
6258
|
connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error)
|
|
6062
6259
|
|
|
6260
|
+
@host_event_handler
|
|
6261
|
+
@with_connection_from_handle
|
|
6262
|
+
def on_le_subrate_change(
|
|
6263
|
+
self,
|
|
6264
|
+
connection: Connection,
|
|
6265
|
+
subrate_factor: int,
|
|
6266
|
+
peripheral_latency: int,
|
|
6267
|
+
continuation_number: int,
|
|
6268
|
+
supervision_timeout: int,
|
|
6269
|
+
):
|
|
6270
|
+
connection.parameters = Connection.Parameters(
|
|
6271
|
+
connection.parameters.connection_interval,
|
|
6272
|
+
peripheral_latency,
|
|
6273
|
+
supervision_timeout * 10.0,
|
|
6274
|
+
subrate_factor,
|
|
6275
|
+
continuation_number,
|
|
6276
|
+
)
|
|
6277
|
+
connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
|
|
6278
|
+
|
|
6063
6279
|
@host_event_handler
|
|
6064
6280
|
@with_connection_from_handle
|
|
6065
6281
|
def on_connection_att_mtu_update(self, connection, att_mtu):
|