bumble 0.0.192__py3-none-any.whl → 0.0.194__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/bench.py +69 -12
- bumble/apps/lea_unicast/app.py +577 -0
- bumble/apps/lea_unicast/index.html +68 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- bumble/apps/rfcomm_bridge.py +511 -0
- bumble/device.py +157 -118
- bumble/hci.py +14 -25
- bumble/hfp.py +279 -31
- bumble/host.py +9 -5
- bumble/keys.py +7 -4
- bumble/l2cap.py +5 -2
- bumble/profiles/bap.py +52 -11
- bumble/rfcomm.py +173 -60
- bumble/sdp.py +1 -1
- bumble/transport/common.py +4 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/METADATA +5 -4
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/RECORD +22 -18
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/LICENSE +0 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/WHEEL +0 -0
- {bumble-0.0.192.dist-info → bumble-0.0.194.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
from enum import IntEnum
|
|
20
|
+
import copy
|
|
20
21
|
import functools
|
|
21
22
|
import json
|
|
22
23
|
import asyncio
|
|
23
24
|
import logging
|
|
24
25
|
import secrets
|
|
25
|
-
|
|
26
|
+
import sys
|
|
27
|
+
from contextlib import (
|
|
28
|
+
asynccontextmanager,
|
|
29
|
+
AsyncExitStack,
|
|
30
|
+
closing,
|
|
31
|
+
AbstractAsyncContextManager,
|
|
32
|
+
)
|
|
26
33
|
from dataclasses import dataclass, field
|
|
27
34
|
from collections.abc import Iterable
|
|
28
35
|
from typing import (
|
|
@@ -40,6 +47,7 @@ from typing import (
|
|
|
40
47
|
overload,
|
|
41
48
|
TYPE_CHECKING,
|
|
42
49
|
)
|
|
50
|
+
from typing_extensions import Self
|
|
43
51
|
|
|
44
52
|
from pyee import EventEmitter
|
|
45
53
|
|
|
@@ -959,8 +967,9 @@ class ScoLink(CompositeEventEmitter):
|
|
|
959
967
|
acl_connection: Connection
|
|
960
968
|
handle: int
|
|
961
969
|
link_type: int
|
|
970
|
+
sink: Optional[Callable[[HCI_SynchronousDataPacket], Any]] = None
|
|
962
971
|
|
|
963
|
-
def __post_init__(self):
|
|
972
|
+
def __post_init__(self) -> None:
|
|
964
973
|
super().__init__()
|
|
965
974
|
|
|
966
975
|
async def disconnect(
|
|
@@ -982,8 +991,9 @@ class CisLink(CompositeEventEmitter):
|
|
|
982
991
|
cis_id: int # CIS ID assigned by Central device
|
|
983
992
|
cig_id: int # CIG ID assigned by Central device
|
|
984
993
|
state: State = State.PENDING
|
|
994
|
+
sink: Optional[Callable[[HCI_IsoDataPacket], Any]] = None
|
|
985
995
|
|
|
986
|
-
def __post_init__(self):
|
|
996
|
+
def __post_init__(self) -> None:
|
|
987
997
|
super().__init__()
|
|
988
998
|
|
|
989
999
|
async def disconnect(
|
|
@@ -1252,75 +1262,47 @@ class Connection(CompositeEventEmitter):
|
|
|
1252
1262
|
|
|
1253
1263
|
|
|
1254
1264
|
# -----------------------------------------------------------------------------
|
|
1265
|
+
@dataclass
|
|
1255
1266
|
class DeviceConfiguration:
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
AdvertisingData(
|
|
1276
|
-
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
|
|
1277
|
-
)
|
|
1267
|
+
# Setup defaults
|
|
1268
|
+
name: str = DEVICE_DEFAULT_NAME
|
|
1269
|
+
address: Address = Address(DEVICE_DEFAULT_ADDRESS)
|
|
1270
|
+
class_of_device: int = DEVICE_DEFAULT_CLASS_OF_DEVICE
|
|
1271
|
+
scan_response_data: bytes = DEVICE_DEFAULT_SCAN_RESPONSE_DATA
|
|
1272
|
+
advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
|
1273
|
+
advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
|
1274
|
+
le_enabled: bool = True
|
|
1275
|
+
# LE host enable 2nd parameter
|
|
1276
|
+
le_simultaneous_enabled: bool = False
|
|
1277
|
+
classic_enabled: bool = False
|
|
1278
|
+
classic_sc_enabled: bool = True
|
|
1279
|
+
classic_ssp_enabled: bool = True
|
|
1280
|
+
classic_smp_enabled: bool = True
|
|
1281
|
+
classic_accept_any: bool = True
|
|
1282
|
+
connectable: bool = True
|
|
1283
|
+
discoverable: bool = True
|
|
1284
|
+
advertising_data: bytes = bytes(
|
|
1285
|
+
AdvertisingData(
|
|
1286
|
+
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(DEVICE_DEFAULT_NAME, 'utf-8'))]
|
|
1278
1287
|
)
|
|
1279
|
-
|
|
1280
|
-
|
|
1288
|
+
)
|
|
1289
|
+
irk: bytes = bytes(16) # This really must be changed for any level of security
|
|
1290
|
+
keystore: Optional[str] = None
|
|
1291
|
+
address_resolution_offload: bool = False
|
|
1292
|
+
cis_enabled: bool = False
|
|
1293
|
+
|
|
1294
|
+
def __post_init__(self) -> None:
|
|
1281
1295
|
self.gatt_services: List[Dict[str, Any]] = []
|
|
1282
|
-
self.address_resolution_offload = False
|
|
1283
|
-
self.cis_enabled = False
|
|
1284
1296
|
|
|
1285
1297
|
def load_from_dict(self, config: Dict[str, Any]) -> None:
|
|
1298
|
+
config = copy.deepcopy(config)
|
|
1299
|
+
|
|
1286
1300
|
# Load simple properties
|
|
1287
|
-
|
|
1288
|
-
if address := config.get('address', None):
|
|
1301
|
+
if address := config.pop('address', None):
|
|
1289
1302
|
self.address = Address(address)
|
|
1290
|
-
self.class_of_device = config.get('class_of_device', self.class_of_device)
|
|
1291
|
-
self.advertising_interval_min = config.get(
|
|
1292
|
-
'advertising_interval', self.advertising_interval_min
|
|
1293
|
-
)
|
|
1294
|
-
self.advertising_interval_max = self.advertising_interval_min
|
|
1295
|
-
self.keystore = config.get('keystore')
|
|
1296
|
-
self.le_enabled = config.get('le_enabled', self.le_enabled)
|
|
1297
|
-
self.le_simultaneous_enabled = config.get(
|
|
1298
|
-
'le_simultaneous_enabled', self.le_simultaneous_enabled
|
|
1299
|
-
)
|
|
1300
|
-
self.classic_enabled = config.get('classic_enabled', self.classic_enabled)
|
|
1301
|
-
self.classic_sc_enabled = config.get(
|
|
1302
|
-
'classic_sc_enabled', self.classic_sc_enabled
|
|
1303
|
-
)
|
|
1304
|
-
self.classic_ssp_enabled = config.get(
|
|
1305
|
-
'classic_ssp_enabled', self.classic_ssp_enabled
|
|
1306
|
-
)
|
|
1307
|
-
self.classic_smp_enabled = config.get(
|
|
1308
|
-
'classic_smp_enabled', self.classic_smp_enabled
|
|
1309
|
-
)
|
|
1310
|
-
self.classic_accept_any = config.get(
|
|
1311
|
-
'classic_accept_any', self.classic_accept_any
|
|
1312
|
-
)
|
|
1313
|
-
self.connectable = config.get('connectable', self.connectable)
|
|
1314
|
-
self.discoverable = config.get('discoverable', self.discoverable)
|
|
1315
|
-
self.gatt_services = config.get('gatt_services', self.gatt_services)
|
|
1316
|
-
self.address_resolution_offload = config.get(
|
|
1317
|
-
'address_resolution_offload', self.address_resolution_offload
|
|
1318
|
-
)
|
|
1319
|
-
self.cis_enabled = config.get('cis_enabled', self.cis_enabled)
|
|
1320
1303
|
|
|
1321
1304
|
# Load or synthesize an IRK
|
|
1322
|
-
irk
|
|
1323
|
-
if irk:
|
|
1305
|
+
if irk := config.pop('irk', None):
|
|
1324
1306
|
self.irk = bytes.fromhex(irk)
|
|
1325
1307
|
elif self.address != Address(DEVICE_DEFAULT_ADDRESS):
|
|
1326
1308
|
# Construct an IRK from the address bytes
|
|
@@ -1332,21 +1314,53 @@ class DeviceConfiguration:
|
|
|
1332
1314
|
# Fallback - when both IRK and address are not set, randomly generate an IRK.
|
|
1333
1315
|
self.irk = secrets.token_bytes(16)
|
|
1334
1316
|
|
|
1317
|
+
if (name := config.pop('name', None)) is not None:
|
|
1318
|
+
self.name = name
|
|
1319
|
+
|
|
1335
1320
|
# Load advertising data
|
|
1336
|
-
advertising_data
|
|
1337
|
-
if advertising_data:
|
|
1321
|
+
if advertising_data := config.pop('advertising_data', None):
|
|
1338
1322
|
self.advertising_data = bytes.fromhex(advertising_data)
|
|
1339
|
-
elif
|
|
1323
|
+
elif name is not None:
|
|
1340
1324
|
self.advertising_data = bytes(
|
|
1341
1325
|
AdvertisingData(
|
|
1342
1326
|
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
|
|
1343
1327
|
)
|
|
1344
1328
|
)
|
|
1345
1329
|
|
|
1346
|
-
|
|
1330
|
+
# Load advertising interval (for backward compatibility)
|
|
1331
|
+
if advertising_interval := config.pop('advertising_interval', None):
|
|
1332
|
+
self.advertising_interval_min = advertising_interval
|
|
1333
|
+
self.advertising_interval_max = advertising_interval
|
|
1334
|
+
if (
|
|
1335
|
+
'advertising_interval_max' in config
|
|
1336
|
+
or 'advertising_interval_min' in config
|
|
1337
|
+
):
|
|
1338
|
+
logger.warning(
|
|
1339
|
+
'Trying to set both advertising_interval and '
|
|
1340
|
+
'advertising_interval_min/max, advertising_interval will be'
|
|
1341
|
+
'ignored.'
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
# Load data in primitive types.
|
|
1345
|
+
for key, value in config.items():
|
|
1346
|
+
setattr(self, key, value)
|
|
1347
|
+
|
|
1348
|
+
def load_from_file(self, filename: str) -> None:
|
|
1347
1349
|
with open(filename, 'r', encoding='utf-8') as file:
|
|
1348
1350
|
self.load_from_dict(json.load(file))
|
|
1349
1351
|
|
|
1352
|
+
@classmethod
|
|
1353
|
+
def from_file(cls: Type[Self], filename: str) -> Self:
|
|
1354
|
+
config = cls()
|
|
1355
|
+
config.load_from_file(filename)
|
|
1356
|
+
return config
|
|
1357
|
+
|
|
1358
|
+
@classmethod
|
|
1359
|
+
def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
|
|
1360
|
+
device_config = cls()
|
|
1361
|
+
device_config.load_from_dict(config)
|
|
1362
|
+
return device_config
|
|
1363
|
+
|
|
1350
1364
|
|
|
1351
1365
|
# -----------------------------------------------------------------------------
|
|
1352
1366
|
# Decorators used with the following Device class
|
|
@@ -1470,8 +1484,7 @@ class Device(CompositeEventEmitter):
|
|
|
1470
1484
|
|
|
1471
1485
|
@classmethod
|
|
1472
1486
|
def from_config_file(cls, filename: str) -> Device:
|
|
1473
|
-
config = DeviceConfiguration()
|
|
1474
|
-
config.load_from_file(filename)
|
|
1487
|
+
config = DeviceConfiguration.from_file(filename)
|
|
1475
1488
|
return cls(config=config)
|
|
1476
1489
|
|
|
1477
1490
|
@classmethod
|
|
@@ -1488,8 +1501,7 @@ class Device(CompositeEventEmitter):
|
|
|
1488
1501
|
def from_config_file_with_hci(
|
|
1489
1502
|
cls, filename: str, hci_source: TransportSource, hci_sink: TransportSink
|
|
1490
1503
|
) -> Device:
|
|
1491
|
-
config = DeviceConfiguration()
|
|
1492
|
-
config.load_from_file(filename)
|
|
1504
|
+
config = DeviceConfiguration.from_file(filename)
|
|
1493
1505
|
return cls.from_config_with_hci(config, hci_source, hci_sink)
|
|
1494
1506
|
|
|
1495
1507
|
def __init__(
|
|
@@ -1529,6 +1541,12 @@ class Device(CompositeEventEmitter):
|
|
|
1529
1541
|
Address.ANY: []
|
|
1530
1542
|
} # Futures, by BD address OR [Futures] for Address.ANY
|
|
1531
1543
|
|
|
1544
|
+
# In Python <= 3.9 + Rust Runtime, asyncio.Lock cannot be properly initiated.
|
|
1545
|
+
if sys.version_info >= (3, 10):
|
|
1546
|
+
self._cis_lock = asyncio.Lock()
|
|
1547
|
+
else:
|
|
1548
|
+
self._cis_lock = AsyncExitStack()
|
|
1549
|
+
|
|
1532
1550
|
# Own address type cache
|
|
1533
1551
|
self.connect_own_address_type = None
|
|
1534
1552
|
|
|
@@ -2184,7 +2202,7 @@ class Device(CompositeEventEmitter):
|
|
|
2184
2202
|
# controller.
|
|
2185
2203
|
await self.send_command(
|
|
2186
2204
|
HCI_LE_Remove_Advertising_Set_Command(
|
|
2187
|
-
advertising_handle=
|
|
2205
|
+
advertising_handle=advertising_handle
|
|
2188
2206
|
),
|
|
2189
2207
|
check_result=False,
|
|
2190
2208
|
)
|
|
@@ -3402,49 +3420,71 @@ class Device(CompositeEventEmitter):
|
|
|
3402
3420
|
for cis_handle, _ in cis_acl_pairs
|
|
3403
3421
|
}
|
|
3404
3422
|
|
|
3405
|
-
@watcher.on(self, 'cis_establishment')
|
|
3406
3423
|
def on_cis_establishment(cis_link: CisLink) -> None:
|
|
3407
3424
|
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
|
3408
3425
|
pending_future.set_result(cis_link)
|
|
3409
3426
|
|
|
3410
|
-
|
|
3427
|
+
def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
|
|
3428
|
+
if pending_future := pending_cis_establishments.get(cis_handle):
|
|
3429
|
+
pending_future.set_exception(HCI_Error(status))
|
|
3430
|
+
|
|
3431
|
+
watcher.on(self, 'cis_establishment', on_cis_establishment)
|
|
3432
|
+
watcher.on(self, 'cis_establishment_failure', on_cis_establishment_failure)
|
|
3433
|
+
await self.send_command(
|
|
3411
3434
|
HCI_LE_Create_CIS_Command(
|
|
3412
3435
|
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
|
3413
3436
|
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
|
3414
3437
|
),
|
|
3438
|
+
check_result=True,
|
|
3415
3439
|
)
|
|
3416
|
-
if result.status != HCI_COMMAND_STATUS_PENDING:
|
|
3417
|
-
logger.warning(
|
|
3418
|
-
'HCI_LE_Create_CIS_Command failed: '
|
|
3419
|
-
f'{HCI_Constant.error_name(result.status)}'
|
|
3420
|
-
)
|
|
3421
|
-
raise HCI_StatusError(result)
|
|
3422
3440
|
|
|
3423
3441
|
return await asyncio.gather(*pending_cis_establishments.values())
|
|
3424
3442
|
|
|
3425
3443
|
# [LE only]
|
|
3426
3444
|
@experimental('Only for testing.')
|
|
3427
3445
|
async def accept_cis_request(self, handle: int) -> CisLink:
|
|
3428
|
-
|
|
3429
|
-
HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
|
|
3430
|
-
)
|
|
3431
|
-
if result.status != HCI_COMMAND_STATUS_PENDING:
|
|
3432
|
-
logger.warning(
|
|
3433
|
-
'HCI_LE_Accept_CIS_Request_Command failed: '
|
|
3434
|
-
f'{HCI_Constant.error_name(result.status)}'
|
|
3435
|
-
)
|
|
3436
|
-
raise HCI_StatusError(result)
|
|
3446
|
+
"""[LE Only] Accepts an incoming CIS request.
|
|
3437
3447
|
|
|
3438
|
-
|
|
3448
|
+
When the specified CIS handle is already created, this method returns the
|
|
3449
|
+
existed CIS link object immediately.
|
|
3439
3450
|
|
|
3440
|
-
|
|
3451
|
+
Args:
|
|
3452
|
+
handle: CIS handle to accept.
|
|
3441
3453
|
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3454
|
+
Returns:
|
|
3455
|
+
CIS link object on the given handle.
|
|
3456
|
+
"""
|
|
3457
|
+
if not (cis_link := self.cis_links.get(handle)):
|
|
3458
|
+
raise InvalidStateError(f'No pending CIS request of handle {handle}')
|
|
3459
|
+
|
|
3460
|
+
# There might be multiple ASE sharing a CIS channel.
|
|
3461
|
+
# If one of them has accepted the request, the others should just leverage it.
|
|
3462
|
+
async with self._cis_lock:
|
|
3463
|
+
if cis_link.state == CisLink.State.ESTABLISHED:
|
|
3464
|
+
return cis_link
|
|
3465
|
+
|
|
3466
|
+
with closing(EventWatcher()) as watcher:
|
|
3467
|
+
pending_establishment = asyncio.get_running_loop().create_future()
|
|
3468
|
+
|
|
3469
|
+
def on_establishment() -> None:
|
|
3470
|
+
pending_establishment.set_result(None)
|
|
3471
|
+
|
|
3472
|
+
def on_establishment_failure(status: int) -> None:
|
|
3473
|
+
pending_establishment.set_exception(HCI_Error(status))
|
|
3474
|
+
|
|
3475
|
+
watcher.on(cis_link, 'establishment', on_establishment)
|
|
3476
|
+
watcher.on(cis_link, 'establishment_failure', on_establishment_failure)
|
|
3477
|
+
|
|
3478
|
+
await self.send_command(
|
|
3479
|
+
HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
|
|
3480
|
+
check_result=True,
|
|
3481
|
+
)
|
|
3482
|
+
|
|
3483
|
+
await pending_establishment
|
|
3484
|
+
return cis_link
|
|
3446
3485
|
|
|
3447
|
-
|
|
3486
|
+
# Mypy believes this is reachable when context is an ExitStack.
|
|
3487
|
+
raise InvalidStateError('Unreachable')
|
|
3448
3488
|
|
|
3449
3489
|
# [LE only]
|
|
3450
3490
|
@experimental('Only for testing.')
|
|
@@ -3453,15 +3493,10 @@ class Device(CompositeEventEmitter):
|
|
|
3453
3493
|
handle: int,
|
|
3454
3494
|
reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
3455
3495
|
) -> None:
|
|
3456
|
-
|
|
3496
|
+
await self.send_command(
|
|
3457
3497
|
HCI_LE_Reject_CIS_Request_Command(connection_handle=handle, reason=reason),
|
|
3498
|
+
check_result=True,
|
|
3458
3499
|
)
|
|
3459
|
-
if result.status != HCI_COMMAND_STATUS_PENDING:
|
|
3460
|
-
logger.warning(
|
|
3461
|
-
'HCI_LE_Reject_CIS_Request_Command failed: '
|
|
3462
|
-
f'{HCI_Constant.error_name(result.status)}'
|
|
3463
|
-
)
|
|
3464
|
-
raise HCI_StatusError(result)
|
|
3465
3500
|
|
|
3466
3501
|
async def get_remote_le_features(self, connection: Connection) -> LeFeatureMask:
|
|
3467
3502
|
"""[LE Only] Reads remote LE supported features.
|
|
@@ -3481,11 +3516,17 @@ class Device(CompositeEventEmitter):
|
|
|
3481
3516
|
if handle == connection.handle:
|
|
3482
3517
|
read_feature_future.set_result(LeFeatureMask(features))
|
|
3483
3518
|
|
|
3519
|
+
def on_failure(handle: int, status: int):
|
|
3520
|
+
if handle == connection.handle:
|
|
3521
|
+
read_feature_future.set_exception(HCI_Error(status))
|
|
3522
|
+
|
|
3484
3523
|
watcher.on(self.host, 'le_remote_features', on_le_remote_features)
|
|
3524
|
+
watcher.on(self.host, 'le_remote_features_failure', on_failure)
|
|
3485
3525
|
await self.send_command(
|
|
3486
3526
|
HCI_LE_Read_Remote_Features_Command(
|
|
3487
3527
|
connection_handle=connection.handle
|
|
3488
3528
|
),
|
|
3529
|
+
check_result=True,
|
|
3489
3530
|
)
|
|
3490
3531
|
return await read_feature_future
|
|
3491
3532
|
|
|
@@ -3660,7 +3701,6 @@ class Device(CompositeEventEmitter):
|
|
|
3660
3701
|
# We were connected via a legacy advertisement.
|
|
3661
3702
|
if self.legacy_advertiser:
|
|
3662
3703
|
own_address_type = self.legacy_advertiser.own_address_type
|
|
3663
|
-
self.legacy_advertiser = None
|
|
3664
3704
|
else:
|
|
3665
3705
|
# This should not happen, but just in case, pick a default.
|
|
3666
3706
|
logger.warning("connection without an advertiser")
|
|
@@ -3691,15 +3731,14 @@ class Device(CompositeEventEmitter):
|
|
|
3691
3731
|
)
|
|
3692
3732
|
self.connections[connection_handle] = connection
|
|
3693
3733
|
|
|
3694
|
-
if
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
)
|
|
3734
|
+
if role == HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
|
|
3735
|
+
if self.legacy_advertiser.auto_restart:
|
|
3736
|
+
connection.once(
|
|
3737
|
+
'disconnection',
|
|
3738
|
+
lambda _: self.abort_on('flush', self.legacy_advertiser.start()),
|
|
3739
|
+
)
|
|
3740
|
+
else:
|
|
3741
|
+
self.legacy_advertiser = None
|
|
3703
3742
|
|
|
3704
3743
|
if role == HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
|
3705
3744
|
# We can emit now, we have all the info we need
|
|
@@ -4107,8 +4146,8 @@ class Device(CompositeEventEmitter):
|
|
|
4107
4146
|
@host_event_handler
|
|
4108
4147
|
@experimental('Only for testing')
|
|
4109
4148
|
def on_sco_packet(self, sco_handle: int, packet: HCI_SynchronousDataPacket) -> None:
|
|
4110
|
-
if sco_link := self.sco_links.get(sco_handle):
|
|
4111
|
-
sco_link.
|
|
4149
|
+
if (sco_link := self.sco_links.get(sco_handle)) and sco_link.sink:
|
|
4150
|
+
sco_link.sink(packet)
|
|
4112
4151
|
|
|
4113
4152
|
# [LE only]
|
|
4114
4153
|
@host_event_handler
|
|
@@ -4164,15 +4203,15 @@ class Device(CompositeEventEmitter):
|
|
|
4164
4203
|
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
|
4165
4204
|
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
|
4166
4205
|
if cis_link := self.cis_links.pop(cis_handle):
|
|
4167
|
-
cis_link.emit('establishment_failure')
|
|
4206
|
+
cis_link.emit('establishment_failure', status)
|
|
4168
4207
|
self.emit('cis_establishment_failure', cis_handle, status)
|
|
4169
4208
|
|
|
4170
4209
|
# [LE only]
|
|
4171
4210
|
@host_event_handler
|
|
4172
4211
|
@experimental('Only for testing')
|
|
4173
4212
|
def on_iso_packet(self, handle: int, packet: HCI_IsoDataPacket) -> None:
|
|
4174
|
-
if cis_link := self.cis_links.get(handle):
|
|
4175
|
-
cis_link.
|
|
4213
|
+
if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
|
|
4214
|
+
cis_link.sink(packet)
|
|
4176
4215
|
|
|
4177
4216
|
@host_event_handler
|
|
4178
4217
|
@with_connection_from_handle
|
bumble/hci.py
CHANGED
|
@@ -23,7 +23,7 @@ import functools
|
|
|
23
23
|
import logging
|
|
24
24
|
import secrets
|
|
25
25
|
import struct
|
|
26
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union
|
|
26
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union, ClassVar
|
|
27
27
|
|
|
28
28
|
from bumble import crypto
|
|
29
29
|
from .colors import color
|
|
@@ -2003,7 +2003,7 @@ class HCI_Packet:
|
|
|
2003
2003
|
Abstract Base class for HCI packets
|
|
2004
2004
|
'''
|
|
2005
2005
|
|
|
2006
|
-
hci_packet_type: int
|
|
2006
|
+
hci_packet_type: ClassVar[int]
|
|
2007
2007
|
|
|
2008
2008
|
@staticmethod
|
|
2009
2009
|
def from_bytes(packet: bytes) -> HCI_Packet:
|
|
@@ -6192,12 +6192,23 @@ class HCI_SynchronousDataPacket(HCI_Packet):
|
|
|
6192
6192
|
|
|
6193
6193
|
|
|
6194
6194
|
# -----------------------------------------------------------------------------
|
|
6195
|
+
@dataclasses.dataclass
|
|
6195
6196
|
class HCI_IsoDataPacket(HCI_Packet):
|
|
6196
6197
|
'''
|
|
6197
6198
|
See Bluetooth spec @ 5.4.5 HCI ISO Data Packets
|
|
6198
6199
|
'''
|
|
6199
6200
|
|
|
6200
|
-
hci_packet_type = HCI_ISO_DATA_PACKET
|
|
6201
|
+
hci_packet_type: ClassVar[int] = HCI_ISO_DATA_PACKET
|
|
6202
|
+
|
|
6203
|
+
connection_handle: int
|
|
6204
|
+
data_total_length: int
|
|
6205
|
+
iso_sdu_fragment: bytes
|
|
6206
|
+
pb_flag: int
|
|
6207
|
+
ts_flag: int = 0
|
|
6208
|
+
time_stamp: Optional[int] = None
|
|
6209
|
+
packet_sequence_number: Optional[int] = None
|
|
6210
|
+
iso_sdu_length: Optional[int] = None
|
|
6211
|
+
packet_status_flag: Optional[int] = None
|
|
6201
6212
|
|
|
6202
6213
|
@staticmethod
|
|
6203
6214
|
def from_bytes(packet: bytes) -> HCI_IsoDataPacket:
|
|
@@ -6241,28 +6252,6 @@ class HCI_IsoDataPacket(HCI_Packet):
|
|
|
6241
6252
|
iso_sdu_fragment=iso_sdu_fragment,
|
|
6242
6253
|
)
|
|
6243
6254
|
|
|
6244
|
-
def __init__(
|
|
6245
|
-
self,
|
|
6246
|
-
connection_handle: int,
|
|
6247
|
-
pb_flag: int,
|
|
6248
|
-
ts_flag: int,
|
|
6249
|
-
data_total_length: int,
|
|
6250
|
-
time_stamp: Optional[int],
|
|
6251
|
-
packet_sequence_number: Optional[int],
|
|
6252
|
-
iso_sdu_length: Optional[int],
|
|
6253
|
-
packet_status_flag: Optional[int],
|
|
6254
|
-
iso_sdu_fragment: bytes,
|
|
6255
|
-
) -> None:
|
|
6256
|
-
self.connection_handle = connection_handle
|
|
6257
|
-
self.pb_flag = pb_flag
|
|
6258
|
-
self.ts_flag = ts_flag
|
|
6259
|
-
self.data_total_length = data_total_length
|
|
6260
|
-
self.time_stamp = time_stamp
|
|
6261
|
-
self.packet_sequence_number = packet_sequence_number
|
|
6262
|
-
self.iso_sdu_length = iso_sdu_length
|
|
6263
|
-
self.packet_status_flag = packet_status_flag
|
|
6264
|
-
self.iso_sdu_fragment = iso_sdu_fragment
|
|
6265
|
-
|
|
6266
6255
|
def __bytes__(self) -> bytes:
|
|
6267
6256
|
return self.to_bytes()
|
|
6268
6257
|
|