bumble 0.0.204__py3-none-any.whl → 0.0.207__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +626 -87
- bumble/apps/bench.py +225 -147
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/gatt.py +35 -6
- bumble/gatt_client.py +14 -2
- bumble/hci.py +812 -14
- bumble/host.py +359 -63
- bumble/l2cap.py +3 -16
- bumble/profiles/aics.py +19 -38
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +10 -19
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +54 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +2 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/RECORD +37 -34
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import asyncio
|
|
20
|
-
|
|
20
|
+
import collections
|
|
21
|
+
from collections.abc import Iterable, Sequence
|
|
21
22
|
from contextlib import (
|
|
22
23
|
asynccontextmanager,
|
|
23
24
|
AsyncExitStack,
|
|
@@ -36,10 +37,9 @@ from typing import (
|
|
|
36
37
|
Any,
|
|
37
38
|
Callable,
|
|
38
39
|
ClassVar,
|
|
40
|
+
Deque,
|
|
39
41
|
Dict,
|
|
40
|
-
List,
|
|
41
42
|
Optional,
|
|
42
|
-
Tuple,
|
|
43
43
|
Type,
|
|
44
44
|
TypeVar,
|
|
45
45
|
Union,
|
|
@@ -54,7 +54,7 @@ from pyee import EventEmitter
|
|
|
54
54
|
from .colors import color
|
|
55
55
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
|
56
56
|
from .gatt import Characteristic, Descriptor, Service
|
|
57
|
-
from .host import Host
|
|
57
|
+
from .host import DataPacketQueue, Host
|
|
58
58
|
from .profiles.gap import GenericAccessService
|
|
59
59
|
from .core import (
|
|
60
60
|
BT_BR_EDR_TRANSPORT,
|
|
@@ -95,6 +95,7 @@ from bumble import smp
|
|
|
95
95
|
from bumble import sdp
|
|
96
96
|
from bumble import l2cap
|
|
97
97
|
from bumble import core
|
|
98
|
+
from bumble.profiles import gatt_service
|
|
98
99
|
|
|
99
100
|
if TYPE_CHECKING:
|
|
100
101
|
from .transport.common import TransportSource, TransportSink
|
|
@@ -119,6 +120,10 @@ DEVICE_MIN_LE_RSSI = -127
|
|
|
119
120
|
DEVICE_MAX_LE_RSSI = 20
|
|
120
121
|
DEVICE_MIN_EXTENDED_ADVERTISING_SET_HANDLE = 0x00
|
|
121
122
|
DEVICE_MAX_EXTENDED_ADVERTISING_SET_HANDLE = 0xEF
|
|
123
|
+
DEVICE_MIN_BIG_HANDLE = 0x00
|
|
124
|
+
DEVICE_MAX_BIG_HANDLE = 0xEF
|
|
125
|
+
DEVICE_MIN_CS_CONFIG_ID = 0x00
|
|
126
|
+
DEVICE_MAX_CS_CONFIG_ID = 0x03
|
|
122
127
|
|
|
123
128
|
DEVICE_DEFAULT_ADDRESS = '00:00:00:00:00:00'
|
|
124
129
|
DEVICE_DEFAULT_ADVERTISING_INTERVAL = 1000 # ms
|
|
@@ -254,7 +259,7 @@ class ExtendedAdvertisement(Advertisement):
|
|
|
254
259
|
secondary_phy = report.secondary_phy,
|
|
255
260
|
tx_power = report.tx_power,
|
|
256
261
|
sid = report.advertising_sid,
|
|
257
|
-
data_bytes = report.data
|
|
262
|
+
data_bytes = report.data,
|
|
258
263
|
)
|
|
259
264
|
# fmt: on
|
|
260
265
|
|
|
@@ -380,8 +385,12 @@ class LegacyAdvertiser:
|
|
|
380
385
|
# Set the advertising parameters
|
|
381
386
|
await self.device.send_command(
|
|
382
387
|
hci.HCI_LE_Set_Advertising_Parameters_Command(
|
|
383
|
-
advertising_interval_min=
|
|
384
|
-
|
|
388
|
+
advertising_interval_min=int(
|
|
389
|
+
self.device.advertising_interval_min / 0.625
|
|
390
|
+
),
|
|
391
|
+
advertising_interval_max=int(
|
|
392
|
+
self.device.advertising_interval_max / 0.625
|
|
393
|
+
),
|
|
385
394
|
advertising_type=int(self.advertising_type),
|
|
386
395
|
own_address_type=self.own_address_type,
|
|
387
396
|
peer_address_type=self.peer_address.address_type,
|
|
@@ -905,11 +914,11 @@ class PeriodicAdvertisingSync(EventEmitter):
|
|
|
905
914
|
|
|
906
915
|
def on_establishment(
|
|
907
916
|
self,
|
|
908
|
-
status,
|
|
909
|
-
sync_handle,
|
|
910
|
-
advertiser_phy,
|
|
911
|
-
periodic_advertising_interval,
|
|
912
|
-
advertiser_clock_accuracy,
|
|
917
|
+
status: int,
|
|
918
|
+
sync_handle: int,
|
|
919
|
+
advertiser_phy: int,
|
|
920
|
+
periodic_advertising_interval: int,
|
|
921
|
+
advertiser_clock_accuracy: int,
|
|
913
922
|
) -> None:
|
|
914
923
|
self.status = status
|
|
915
924
|
|
|
@@ -992,6 +1001,196 @@ class PeriodicAdvertisingSync(EventEmitter):
|
|
|
992
1001
|
)
|
|
993
1002
|
|
|
994
1003
|
|
|
1004
|
+
# -----------------------------------------------------------------------------
|
|
1005
|
+
@dataclass
|
|
1006
|
+
class BigParameters:
|
|
1007
|
+
num_bis: int
|
|
1008
|
+
sdu_interval: int
|
|
1009
|
+
max_sdu: int
|
|
1010
|
+
max_transport_latency: int
|
|
1011
|
+
rtn: int
|
|
1012
|
+
phy: hci.PhyBit = hci.PhyBit.LE_2M
|
|
1013
|
+
packing: int = 0
|
|
1014
|
+
framing: int = 0
|
|
1015
|
+
broadcast_code: bytes | None = None
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
# -----------------------------------------------------------------------------
|
|
1019
|
+
@dataclass
|
|
1020
|
+
class Big(EventEmitter):
|
|
1021
|
+
class State(IntEnum):
|
|
1022
|
+
PENDING = 0
|
|
1023
|
+
ACTIVE = 1
|
|
1024
|
+
TERMINATED = 2
|
|
1025
|
+
|
|
1026
|
+
class Event(str, Enum):
|
|
1027
|
+
ESTABLISHMENT = 'establishment'
|
|
1028
|
+
ESTABLISHMENT_FAILURE = 'establishment_failure'
|
|
1029
|
+
TERMINATION = 'termination'
|
|
1030
|
+
|
|
1031
|
+
big_handle: int
|
|
1032
|
+
advertising_set: AdvertisingSet
|
|
1033
|
+
parameters: BigParameters
|
|
1034
|
+
state: State = State.PENDING
|
|
1035
|
+
|
|
1036
|
+
# Attributes provided by BIG Create Complete event
|
|
1037
|
+
big_sync_delay: int = 0
|
|
1038
|
+
transport_latency_big: int = 0
|
|
1039
|
+
phy: int = 0
|
|
1040
|
+
nse: int = 0
|
|
1041
|
+
bn: int = 0
|
|
1042
|
+
pto: int = 0
|
|
1043
|
+
irc: int = 0
|
|
1044
|
+
max_pdu: int = 0
|
|
1045
|
+
iso_interval: int = 0
|
|
1046
|
+
bis_links: Sequence[BisLink] = ()
|
|
1047
|
+
|
|
1048
|
+
def __post_init__(self) -> None:
|
|
1049
|
+
super().__init__()
|
|
1050
|
+
self.device = self.advertising_set.device
|
|
1051
|
+
|
|
1052
|
+
async def terminate(
|
|
1053
|
+
self,
|
|
1054
|
+
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
1055
|
+
) -> None:
|
|
1056
|
+
if self.state != Big.State.ACTIVE:
|
|
1057
|
+
logger.error('BIG %d is not active.', self.big_handle)
|
|
1058
|
+
return
|
|
1059
|
+
|
|
1060
|
+
with closing(EventWatcher()) as watcher:
|
|
1061
|
+
terminated = asyncio.Event()
|
|
1062
|
+
watcher.once(self, Big.Event.TERMINATION, lambda _: terminated.set())
|
|
1063
|
+
await self.device.send_command(
|
|
1064
|
+
hci.HCI_LE_Terminate_BIG_Command(
|
|
1065
|
+
big_handle=self.big_handle, reason=reason
|
|
1066
|
+
),
|
|
1067
|
+
check_result=True,
|
|
1068
|
+
)
|
|
1069
|
+
await terminated.wait()
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
# -----------------------------------------------------------------------------
|
|
1073
|
+
@dataclass
|
|
1074
|
+
class BigSyncParameters:
|
|
1075
|
+
big_sync_timeout: int
|
|
1076
|
+
bis: Sequence[int]
|
|
1077
|
+
mse: int = 0
|
|
1078
|
+
broadcast_code: bytes | None = None
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
# -----------------------------------------------------------------------------
|
|
1082
|
+
@dataclass
|
|
1083
|
+
class BigSync(EventEmitter):
|
|
1084
|
+
class State(IntEnum):
|
|
1085
|
+
PENDING = 0
|
|
1086
|
+
ACTIVE = 1
|
|
1087
|
+
TERMINATED = 2
|
|
1088
|
+
|
|
1089
|
+
class Event(str, Enum):
|
|
1090
|
+
ESTABLISHMENT = 'establishment'
|
|
1091
|
+
ESTABLISHMENT_FAILURE = 'establishment_failure'
|
|
1092
|
+
TERMINATION = 'termination'
|
|
1093
|
+
|
|
1094
|
+
big_handle: int
|
|
1095
|
+
pa_sync: PeriodicAdvertisingSync
|
|
1096
|
+
parameters: BigSyncParameters
|
|
1097
|
+
state: State = State.PENDING
|
|
1098
|
+
|
|
1099
|
+
# Attributes provided by BIG Create Sync Complete event
|
|
1100
|
+
transport_latency_big: int = 0
|
|
1101
|
+
nse: int = 0
|
|
1102
|
+
bn: int = 0
|
|
1103
|
+
pto: int = 0
|
|
1104
|
+
irc: int = 0
|
|
1105
|
+
max_pdu: int = 0
|
|
1106
|
+
iso_interval: int = 0
|
|
1107
|
+
bis_links: Sequence[BisLink] = ()
|
|
1108
|
+
|
|
1109
|
+
def __post_init__(self) -> None:
|
|
1110
|
+
super().__init__()
|
|
1111
|
+
self.device = self.pa_sync.device
|
|
1112
|
+
|
|
1113
|
+
async def terminate(self) -> None:
|
|
1114
|
+
if self.state != BigSync.State.ACTIVE:
|
|
1115
|
+
logger.error('BIG Sync %d is not active.', self.big_handle)
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
with closing(EventWatcher()) as watcher:
|
|
1119
|
+
terminated = asyncio.Event()
|
|
1120
|
+
watcher.once(self, BigSync.Event.TERMINATION, lambda _: terminated.set())
|
|
1121
|
+
await self.device.send_command(
|
|
1122
|
+
hci.HCI_LE_BIG_Terminate_Sync_Command(big_handle=self.big_handle),
|
|
1123
|
+
check_result=True,
|
|
1124
|
+
)
|
|
1125
|
+
await terminated.wait()
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
# -----------------------------------------------------------------------------
|
|
1129
|
+
@dataclass
|
|
1130
|
+
class ChannelSoundingCapabilities:
|
|
1131
|
+
num_config_supported: int
|
|
1132
|
+
max_consecutive_procedures_supported: int
|
|
1133
|
+
num_antennas_supported: int
|
|
1134
|
+
max_antenna_paths_supported: int
|
|
1135
|
+
roles_supported: int
|
|
1136
|
+
modes_supported: int
|
|
1137
|
+
rtt_capability: int
|
|
1138
|
+
rtt_aa_only_n: int
|
|
1139
|
+
rtt_sounding_n: int
|
|
1140
|
+
rtt_random_payload_n: int
|
|
1141
|
+
nadm_sounding_capability: int
|
|
1142
|
+
nadm_random_capability: int
|
|
1143
|
+
cs_sync_phys_supported: int
|
|
1144
|
+
subfeatures_supported: int
|
|
1145
|
+
t_ip1_times_supported: int
|
|
1146
|
+
t_ip2_times_supported: int
|
|
1147
|
+
t_fcs_times_supported: int
|
|
1148
|
+
t_pm_times_supported: int
|
|
1149
|
+
t_sw_time_supported: int
|
|
1150
|
+
tx_snr_capability: int
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# -----------------------------------------------------------------------------
|
|
1154
|
+
@dataclass
|
|
1155
|
+
class ChannelSoundingConfig:
|
|
1156
|
+
config_id: int
|
|
1157
|
+
main_mode_type: int
|
|
1158
|
+
sub_mode_type: int
|
|
1159
|
+
min_main_mode_steps: int
|
|
1160
|
+
max_main_mode_steps: int
|
|
1161
|
+
main_mode_repetition: int
|
|
1162
|
+
mode_0_steps: int
|
|
1163
|
+
role: int
|
|
1164
|
+
rtt_type: int
|
|
1165
|
+
cs_sync_phy: int
|
|
1166
|
+
channel_map: bytes
|
|
1167
|
+
channel_map_repetition: int
|
|
1168
|
+
channel_selection_type: int
|
|
1169
|
+
ch3c_shape: int
|
|
1170
|
+
ch3c_jump: int
|
|
1171
|
+
reserved: int
|
|
1172
|
+
t_ip1_time: int
|
|
1173
|
+
t_ip2_time: int
|
|
1174
|
+
t_fcs_time: int
|
|
1175
|
+
t_pm_time: int
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
# -----------------------------------------------------------------------------
|
|
1179
|
+
@dataclass
|
|
1180
|
+
class ChannelSoundingProcedure:
|
|
1181
|
+
config_id: int
|
|
1182
|
+
state: int
|
|
1183
|
+
tone_antenna_config_selection: int
|
|
1184
|
+
selected_tx_power: int
|
|
1185
|
+
subevent_len: int
|
|
1186
|
+
subevents_per_event: int
|
|
1187
|
+
subevent_interval: int
|
|
1188
|
+
event_interval: int
|
|
1189
|
+
procedure_interval: int
|
|
1190
|
+
procedure_count: int
|
|
1191
|
+
max_procedure_len: int
|
|
1192
|
+
|
|
1193
|
+
|
|
995
1194
|
# -----------------------------------------------------------------------------
|
|
996
1195
|
class LePhyOptions:
|
|
997
1196
|
# Coded PHY preference
|
|
@@ -1019,7 +1218,7 @@ class Peer:
|
|
|
1019
1218
|
connection.gatt_client = self.gatt_client
|
|
1020
1219
|
|
|
1021
1220
|
@property
|
|
1022
|
-
def services(self) ->
|
|
1221
|
+
def services(self) -> list[gatt_client.ServiceProxy]:
|
|
1023
1222
|
return self.gatt_client.services
|
|
1024
1223
|
|
|
1025
1224
|
async def request_mtu(self, mtu: int) -> int:
|
|
@@ -1029,24 +1228,24 @@ class Peer:
|
|
|
1029
1228
|
|
|
1030
1229
|
async def discover_service(
|
|
1031
1230
|
self, uuid: Union[core.UUID, str]
|
|
1032
|
-
) ->
|
|
1231
|
+
) -> list[gatt_client.ServiceProxy]:
|
|
1033
1232
|
return await self.gatt_client.discover_service(uuid)
|
|
1034
1233
|
|
|
1035
1234
|
async def discover_services(
|
|
1036
1235
|
self, uuids: Iterable[core.UUID] = ()
|
|
1037
|
-
) ->
|
|
1236
|
+
) -> list[gatt_client.ServiceProxy]:
|
|
1038
1237
|
return await self.gatt_client.discover_services(uuids)
|
|
1039
1238
|
|
|
1040
1239
|
async def discover_included_services(
|
|
1041
1240
|
self, service: gatt_client.ServiceProxy
|
|
1042
|
-
) ->
|
|
1241
|
+
) -> list[gatt_client.ServiceProxy]:
|
|
1043
1242
|
return await self.gatt_client.discover_included_services(service)
|
|
1044
1243
|
|
|
1045
1244
|
async def discover_characteristics(
|
|
1046
1245
|
self,
|
|
1047
1246
|
uuids: Iterable[Union[core.UUID, str]] = (),
|
|
1048
1247
|
service: Optional[gatt_client.ServiceProxy] = None,
|
|
1049
|
-
) ->
|
|
1248
|
+
) -> list[gatt_client.CharacteristicProxy]:
|
|
1050
1249
|
return await self.gatt_client.discover_characteristics(
|
|
1051
1250
|
uuids=uuids, service=service
|
|
1052
1251
|
)
|
|
@@ -1061,7 +1260,7 @@ class Peer:
|
|
|
1061
1260
|
characteristic, start_handle, end_handle
|
|
1062
1261
|
)
|
|
1063
1262
|
|
|
1064
|
-
async def discover_attributes(self) ->
|
|
1263
|
+
async def discover_attributes(self) -> list[gatt_client.AttributeProxy]:
|
|
1065
1264
|
return await self.gatt_client.discover_attributes()
|
|
1066
1265
|
|
|
1067
1266
|
async def discover_all(self):
|
|
@@ -1105,17 +1304,17 @@ class Peer:
|
|
|
1105
1304
|
|
|
1106
1305
|
async def read_characteristics_by_uuid(
|
|
1107
1306
|
self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
|
|
1108
|
-
) ->
|
|
1307
|
+
) -> list[bytes]:
|
|
1109
1308
|
return await self.gatt_client.read_characteristics_by_uuid(uuid, service)
|
|
1110
1309
|
|
|
1111
|
-
def get_services_by_uuid(self, uuid: core.UUID) ->
|
|
1310
|
+
def get_services_by_uuid(self, uuid: core.UUID) -> list[gatt_client.ServiceProxy]:
|
|
1112
1311
|
return self.gatt_client.get_services_by_uuid(uuid)
|
|
1113
1312
|
|
|
1114
1313
|
def get_characteristics_by_uuid(
|
|
1115
1314
|
self,
|
|
1116
1315
|
uuid: core.UUID,
|
|
1117
1316
|
service: Optional[Union[gatt_client.ServiceProxy, core.UUID]] = None,
|
|
1118
|
-
) ->
|
|
1317
|
+
) -> list[gatt_client.CharacteristicProxy]:
|
|
1119
1318
|
if isinstance(service, core.UUID):
|
|
1120
1319
|
return list(
|
|
1121
1320
|
itertools.chain(
|
|
@@ -1172,8 +1371,8 @@ class Peer:
|
|
|
1172
1371
|
@dataclass
|
|
1173
1372
|
class ConnectionParametersPreferences:
|
|
1174
1373
|
default: ClassVar[ConnectionParametersPreferences]
|
|
1175
|
-
connection_interval_min:
|
|
1176
|
-
connection_interval_max:
|
|
1374
|
+
connection_interval_min: float = DEVICE_DEFAULT_CONNECTION_INTERVAL_MIN
|
|
1375
|
+
connection_interval_max: float = DEVICE_DEFAULT_CONNECTION_INTERVAL_MAX
|
|
1177
1376
|
max_latency: int = DEVICE_DEFAULT_CONNECTION_MAX_LATENCY
|
|
1178
1377
|
supervision_timeout: int = DEVICE_DEFAULT_CONNECTION_SUPERVISION_TIMEOUT
|
|
1179
1378
|
min_ce_length: int = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH
|
|
@@ -1201,9 +1400,82 @@ class ScoLink(CompositeEventEmitter):
|
|
|
1201
1400
|
await self.device.disconnect(self, reason)
|
|
1202
1401
|
|
|
1203
1402
|
|
|
1403
|
+
# -----------------------------------------------------------------------------
|
|
1404
|
+
class _IsoLink:
|
|
1405
|
+
handle: int
|
|
1406
|
+
device: Device
|
|
1407
|
+
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
|
1408
|
+
|
|
1409
|
+
class Direction(IntEnum):
|
|
1410
|
+
HOST_TO_CONTROLLER = (
|
|
1411
|
+
hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
|
|
1412
|
+
)
|
|
1413
|
+
CONTROLLER_TO_HOST = (
|
|
1414
|
+
hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
async def setup_data_path(
|
|
1418
|
+
self,
|
|
1419
|
+
direction: _IsoLink.Direction,
|
|
1420
|
+
data_path_id: int = 0,
|
|
1421
|
+
codec_id: hci.CodingFormat | None = None,
|
|
1422
|
+
controller_delay: int = 0,
|
|
1423
|
+
codec_configuration: bytes = b'',
|
|
1424
|
+
) -> None:
|
|
1425
|
+
"""Create a data path between controller and given entry.
|
|
1426
|
+
|
|
1427
|
+
Args:
|
|
1428
|
+
direction: Direction of data path.
|
|
1429
|
+
data_path_id: ID of data path. Default is 0 (HCI).
|
|
1430
|
+
codec_id: Codec ID. Default is Transparent.
|
|
1431
|
+
controller_delay: Controller delay in microseconds. Default is 0.
|
|
1432
|
+
codec_configuration: Codec-specific configuration.
|
|
1433
|
+
|
|
1434
|
+
Raises:
|
|
1435
|
+
HCI_Error: When command complete status is not HCI_SUCCESS.
|
|
1436
|
+
"""
|
|
1437
|
+
await self.device.send_command(
|
|
1438
|
+
hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
|
1439
|
+
connection_handle=self.handle,
|
|
1440
|
+
data_path_direction=direction,
|
|
1441
|
+
data_path_id=data_path_id,
|
|
1442
|
+
codec_id=codec_id or hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
|
1443
|
+
controller_delay=controller_delay,
|
|
1444
|
+
codec_configuration=codec_configuration,
|
|
1445
|
+
),
|
|
1446
|
+
check_result=True,
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
async def remove_data_path(self, direction: _IsoLink.Direction) -> int:
|
|
1450
|
+
"""Remove a data path with controller on given direction.
|
|
1451
|
+
|
|
1452
|
+
Args:
|
|
1453
|
+
direction: Direction of data path.
|
|
1454
|
+
|
|
1455
|
+
Returns:
|
|
1456
|
+
Command status.
|
|
1457
|
+
"""
|
|
1458
|
+
response = await self.device.send_command(
|
|
1459
|
+
hci.HCI_LE_Remove_ISO_Data_Path_Command(
|
|
1460
|
+
connection_handle=self.handle,
|
|
1461
|
+
data_path_direction=direction,
|
|
1462
|
+
),
|
|
1463
|
+
check_result=False,
|
|
1464
|
+
)
|
|
1465
|
+
return response.return_parameters.status
|
|
1466
|
+
|
|
1467
|
+
def write(self, sdu: bytes) -> None:
|
|
1468
|
+
"""Write an ISO SDU."""
|
|
1469
|
+
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
|
|
1470
|
+
|
|
1471
|
+
@property
|
|
1472
|
+
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1473
|
+
return self.device.host.get_data_packet_queue(self.handle)
|
|
1474
|
+
|
|
1475
|
+
|
|
1204
1476
|
# -----------------------------------------------------------------------------
|
|
1205
1477
|
@dataclass
|
|
1206
|
-
class CisLink(CompositeEventEmitter):
|
|
1478
|
+
class CisLink(CompositeEventEmitter, _IsoLink):
|
|
1207
1479
|
class State(IntEnum):
|
|
1208
1480
|
PENDING = 0
|
|
1209
1481
|
ESTABLISHED = 1
|
|
@@ -1214,7 +1486,7 @@ class CisLink(CompositeEventEmitter):
|
|
|
1214
1486
|
cis_id: int # CIS ID assigned by Central device
|
|
1215
1487
|
cig_id: int # CIG ID assigned by Central device
|
|
1216
1488
|
state: State = State.PENDING
|
|
1217
|
-
sink:
|
|
1489
|
+
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
|
1218
1490
|
|
|
1219
1491
|
def __post_init__(self) -> None:
|
|
1220
1492
|
super().__init__()
|
|
@@ -1225,6 +1497,60 @@ class CisLink(CompositeEventEmitter):
|
|
|
1225
1497
|
await self.device.disconnect(self, reason)
|
|
1226
1498
|
|
|
1227
1499
|
|
|
1500
|
+
# -----------------------------------------------------------------------------
|
|
1501
|
+
@dataclass
|
|
1502
|
+
class BisLink(_IsoLink):
|
|
1503
|
+
handle: int
|
|
1504
|
+
big: Big | BigSync
|
|
1505
|
+
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
|
1506
|
+
|
|
1507
|
+
def __post_init__(self) -> None:
|
|
1508
|
+
self.device = self.big.device
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
# -----------------------------------------------------------------------------
|
|
1512
|
+
class IsoPacketStream:
|
|
1513
|
+
"""Async stream that can write SDUs to a CIS or BIS, with a maximum queue size."""
|
|
1514
|
+
|
|
1515
|
+
iso_link: _IsoLink
|
|
1516
|
+
data_packet_queue: DataPacketQueue
|
|
1517
|
+
|
|
1518
|
+
def __init__(self, iso_link: _IsoLink, max_queue_size: int) -> None:
|
|
1519
|
+
if iso_link.data_packet_queue is None:
|
|
1520
|
+
raise ValueError('link has no data packet queue')
|
|
1521
|
+
|
|
1522
|
+
self.iso_link = iso_link
|
|
1523
|
+
self.data_packet_queue = iso_link.data_packet_queue
|
|
1524
|
+
self.data_packet_queue.on('flow', self._on_flow)
|
|
1525
|
+
self._thresholds: Deque[int] = collections.deque()
|
|
1526
|
+
self._semaphore = asyncio.Semaphore(max_queue_size)
|
|
1527
|
+
|
|
1528
|
+
def _on_flow(self) -> None:
|
|
1529
|
+
# Release the semaphore once for each completed packet.
|
|
1530
|
+
while (
|
|
1531
|
+
self._thresholds and self.data_packet_queue.completed >= self._thresholds[0]
|
|
1532
|
+
):
|
|
1533
|
+
self._thresholds.popleft()
|
|
1534
|
+
self._semaphore.release()
|
|
1535
|
+
|
|
1536
|
+
async def write(self, sdu: bytes) -> None:
|
|
1537
|
+
"""
|
|
1538
|
+
Write an SDU to the queue.
|
|
1539
|
+
|
|
1540
|
+
This method blocks until there are fewer than max_queue_size packets queued
|
|
1541
|
+
but not yet completed.
|
|
1542
|
+
"""
|
|
1543
|
+
|
|
1544
|
+
# Wait until there's space in the queue.
|
|
1545
|
+
await self._semaphore.acquire()
|
|
1546
|
+
|
|
1547
|
+
# Queue the packet.
|
|
1548
|
+
self.iso_link.write(sdu)
|
|
1549
|
+
|
|
1550
|
+
# Remember the position of the packet so we can know when it is completed.
|
|
1551
|
+
self._thresholds.append(self.data_packet_queue.queued)
|
|
1552
|
+
|
|
1553
|
+
|
|
1228
1554
|
# -----------------------------------------------------------------------------
|
|
1229
1555
|
class Connection(CompositeEventEmitter):
|
|
1230
1556
|
device: Device
|
|
@@ -1243,6 +1569,8 @@ class Connection(CompositeEventEmitter):
|
|
|
1243
1569
|
gatt_client: gatt_client.Client
|
|
1244
1570
|
pairing_peer_io_capability: Optional[int]
|
|
1245
1571
|
pairing_peer_authentication_requirements: Optional[int]
|
|
1572
|
+
cs_configs: dict[int, ChannelSoundingConfig] = {} # Config ID to Configuration
|
|
1573
|
+
cs_procedures: dict[int, ChannelSoundingProcedure] = {} # Config ID to Procedures
|
|
1246
1574
|
|
|
1247
1575
|
@composite_listener
|
|
1248
1576
|
class Listener:
|
|
@@ -1470,6 +1798,10 @@ class Connection(CompositeEventEmitter):
|
|
|
1470
1798
|
self.peer_le_features = await self.device.get_remote_le_features(self)
|
|
1471
1799
|
return self.peer_le_features
|
|
1472
1800
|
|
|
1801
|
+
@property
|
|
1802
|
+
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1803
|
+
return self.device.host.get_data_packet_queue(self.handle)
|
|
1804
|
+
|
|
1473
1805
|
async def __aenter__(self):
|
|
1474
1806
|
return self
|
|
1475
1807
|
|
|
@@ -1533,11 +1865,14 @@ class DeviceConfiguration:
|
|
|
1533
1865
|
address_resolution_offload: bool = False
|
|
1534
1866
|
address_generation_offload: bool = False
|
|
1535
1867
|
cis_enabled: bool = False
|
|
1868
|
+
channel_sounding_enabled: bool = False
|
|
1536
1869
|
identity_address_type: Optional[int] = None
|
|
1537
1870
|
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
|
1871
|
+
gap_service_enabled: bool = True
|
|
1872
|
+
gatt_service_enabled: bool = True
|
|
1538
1873
|
|
|
1539
1874
|
def __post_init__(self) -> None:
|
|
1540
|
-
self.gatt_services:
|
|
1875
|
+
self.gatt_services: list[Dict[str, Any]] = []
|
|
1541
1876
|
|
|
1542
1877
|
def load_from_dict(self, config: Dict[str, Any]) -> None:
|
|
1543
1878
|
config = copy.deepcopy(config)
|
|
@@ -1684,7 +2019,7 @@ def host_event_handler(function):
|
|
|
1684
2019
|
# List of host event handlers for the Device class.
|
|
1685
2020
|
# (we define this list outside the class, because referencing a class in method
|
|
1686
2021
|
# decorators is not straightforward)
|
|
1687
|
-
device_host_event_handlers:
|
|
2022
|
+
device_host_event_handlers: list[str] = []
|
|
1688
2023
|
|
|
1689
2024
|
|
|
1690
2025
|
# -----------------------------------------------------------------------------
|
|
@@ -1701,19 +2036,24 @@ class Device(CompositeEventEmitter):
|
|
|
1701
2036
|
gatt_server: gatt_server.Server
|
|
1702
2037
|
advertising_data: bytes
|
|
1703
2038
|
scan_response_data: bytes
|
|
2039
|
+
cs_capabilities: ChannelSoundingCapabilities | None = None
|
|
1704
2040
|
connections: Dict[int, Connection]
|
|
1705
2041
|
pending_connections: Dict[hci.Address, Connection]
|
|
1706
2042
|
classic_pending_accepts: Dict[
|
|
1707
2043
|
hci.Address,
|
|
1708
|
-
|
|
2044
|
+
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
|
1709
2045
|
]
|
|
1710
2046
|
advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
|
|
1711
|
-
periodic_advertising_syncs:
|
|
2047
|
+
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
|
|
1712
2048
|
config: DeviceConfiguration
|
|
1713
2049
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
1714
2050
|
sco_links: Dict[int, ScoLink]
|
|
1715
2051
|
cis_links: Dict[int, CisLink]
|
|
1716
|
-
|
|
2052
|
+
bigs = dict[int, Big]()
|
|
2053
|
+
bis_links = dict[int, BisLink]()
|
|
2054
|
+
big_syncs = dict[int, BigSync]()
|
|
2055
|
+
_pending_cis: Dict[int, tuple[int, int]]
|
|
2056
|
+
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
1717
2057
|
|
|
1718
2058
|
@composite_listener
|
|
1719
2059
|
class Listener:
|
|
@@ -1780,7 +2120,6 @@ class Device(CompositeEventEmitter):
|
|
|
1780
2120
|
address: Optional[hci.Address] = None,
|
|
1781
2121
|
config: Optional[DeviceConfiguration] = None,
|
|
1782
2122
|
host: Optional[Host] = None,
|
|
1783
|
-
generic_access_service: bool = True,
|
|
1784
2123
|
) -> None:
|
|
1785
2124
|
super().__init__()
|
|
1786
2125
|
|
|
@@ -1927,7 +2266,10 @@ class Device(CompositeEventEmitter):
|
|
|
1927
2266
|
# Register the SDP server with the L2CAP Channel Manager
|
|
1928
2267
|
self.sdp_server.register(self.l2cap_channel_manager)
|
|
1929
2268
|
|
|
1930
|
-
self.add_default_services(
|
|
2269
|
+
self.add_default_services(
|
|
2270
|
+
add_gap_service=config.gap_service_enabled,
|
|
2271
|
+
add_gatt_service=config.gatt_service_enabled,
|
|
2272
|
+
)
|
|
1931
2273
|
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
|
1932
2274
|
|
|
1933
2275
|
# Forward some events
|
|
@@ -2009,6 +2351,17 @@ class Device(CompositeEventEmitter):
|
|
|
2009
2351
|
None,
|
|
2010
2352
|
)
|
|
2011
2353
|
|
|
2354
|
+
def next_big_handle(self) -> int | None:
|
|
2355
|
+
return next(
|
|
2356
|
+
(
|
|
2357
|
+
handle
|
|
2358
|
+
for handle in range(DEVICE_MIN_BIG_HANDLE, DEVICE_MAX_BIG_HANDLE + 1)
|
|
2359
|
+
if handle
|
|
2360
|
+
not in itertools.chain(self.bigs.keys(), self.big_syncs.keys())
|
|
2361
|
+
),
|
|
2362
|
+
None,
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2012
2365
|
@deprecated("Please use create_l2cap_server()")
|
|
2013
2366
|
def register_l2cap_server(self, psm, server) -> int:
|
|
2014
2367
|
return self.l2cap_channel_manager.register_server(psm, server)
|
|
@@ -2167,7 +2520,7 @@ class Device(CompositeEventEmitter):
|
|
|
2167
2520
|
if self.random_address != hci.Address.ANY_RANDOM:
|
|
2168
2521
|
logger.debug(
|
|
2169
2522
|
color(
|
|
2170
|
-
f'LE Random
|
|
2523
|
+
f'LE Random Address: {self.random_address}',
|
|
2171
2524
|
'yellow',
|
|
2172
2525
|
)
|
|
2173
2526
|
)
|
|
@@ -2200,6 +2553,41 @@ class Device(CompositeEventEmitter):
|
|
|
2200
2553
|
check_result=True,
|
|
2201
2554
|
)
|
|
2202
2555
|
|
|
2556
|
+
if self.config.channel_sounding_enabled:
|
|
2557
|
+
await self.send_command(
|
|
2558
|
+
hci.HCI_LE_Set_Host_Feature_Command(
|
|
2559
|
+
bit_number=hci.LeFeature.CHANNEL_SOUNDING_HOST_SUPPORT,
|
|
2560
|
+
bit_value=1,
|
|
2561
|
+
),
|
|
2562
|
+
check_result=True,
|
|
2563
|
+
)
|
|
2564
|
+
result = await self.send_command(
|
|
2565
|
+
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command(),
|
|
2566
|
+
check_result=True,
|
|
2567
|
+
)
|
|
2568
|
+
self.cs_capabilities = ChannelSoundingCapabilities(
|
|
2569
|
+
num_config_supported=result.return_parameters.num_config_supported,
|
|
2570
|
+
max_consecutive_procedures_supported=result.return_parameters.max_consecutive_procedures_supported,
|
|
2571
|
+
num_antennas_supported=result.return_parameters.num_antennas_supported,
|
|
2572
|
+
max_antenna_paths_supported=result.return_parameters.max_antenna_paths_supported,
|
|
2573
|
+
roles_supported=result.return_parameters.roles_supported,
|
|
2574
|
+
modes_supported=result.return_parameters.modes_supported,
|
|
2575
|
+
rtt_capability=result.return_parameters.rtt_capability,
|
|
2576
|
+
rtt_aa_only_n=result.return_parameters.rtt_aa_only_n,
|
|
2577
|
+
rtt_sounding_n=result.return_parameters.rtt_sounding_n,
|
|
2578
|
+
rtt_random_payload_n=result.return_parameters.rtt_random_payload_n,
|
|
2579
|
+
nadm_sounding_capability=result.return_parameters.nadm_sounding_capability,
|
|
2580
|
+
nadm_random_capability=result.return_parameters.nadm_random_capability,
|
|
2581
|
+
cs_sync_phys_supported=result.return_parameters.cs_sync_phys_supported,
|
|
2582
|
+
subfeatures_supported=result.return_parameters.subfeatures_supported,
|
|
2583
|
+
t_ip1_times_supported=result.return_parameters.t_ip1_times_supported,
|
|
2584
|
+
t_ip2_times_supported=result.return_parameters.t_ip2_times_supported,
|
|
2585
|
+
t_fcs_times_supported=result.return_parameters.t_fcs_times_supported,
|
|
2586
|
+
t_pm_times_supported=result.return_parameters.t_pm_times_supported,
|
|
2587
|
+
t_sw_time_supported=result.return_parameters.t_sw_time_supported,
|
|
2588
|
+
tx_snr_capability=result.return_parameters.tx_snr_capability,
|
|
2589
|
+
)
|
|
2590
|
+
|
|
2203
2591
|
if self.classic_enabled:
|
|
2204
2592
|
await self.send_command(
|
|
2205
2593
|
hci.HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
|
|
@@ -2283,7 +2671,7 @@ class Device(CompositeEventEmitter):
|
|
|
2283
2671
|
"""Update the RPA periodically"""
|
|
2284
2672
|
while self.le_rpa_timeout != 0:
|
|
2285
2673
|
await asyncio.sleep(self.le_rpa_timeout)
|
|
2286
|
-
if not self.update_rpa():
|
|
2674
|
+
if not await self.update_rpa():
|
|
2287
2675
|
logger.debug("periodic RPA update failed")
|
|
2288
2676
|
|
|
2289
2677
|
async def refresh_resolving_list(self) -> None:
|
|
@@ -2623,11 +3011,11 @@ class Device(CompositeEventEmitter):
|
|
|
2623
3011
|
self,
|
|
2624
3012
|
legacy: bool = False,
|
|
2625
3013
|
active: bool = True,
|
|
2626
|
-
scan_interval:
|
|
2627
|
-
scan_window:
|
|
3014
|
+
scan_interval: float = DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
|
|
3015
|
+
scan_window: float = DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
|
|
2628
3016
|
own_address_type: int = hci.OwnAddressType.RANDOM,
|
|
2629
3017
|
filter_duplicates: bool = False,
|
|
2630
|
-
scanning_phys:
|
|
3018
|
+
scanning_phys: Sequence[int] = (hci.HCI_LE_1M_PHY, hci.HCI_LE_CODED_PHY),
|
|
2631
3019
|
) -> None:
|
|
2632
3020
|
# Check that the arguments are legal
|
|
2633
3021
|
if scan_interval < scan_window:
|
|
@@ -2674,7 +3062,7 @@ class Device(CompositeEventEmitter):
|
|
|
2674
3062
|
scanning_filter_policy=scanning_filter_policy,
|
|
2675
3063
|
scanning_phys=scanning_phys_bits,
|
|
2676
3064
|
scan_types=[scan_type] * scanning_phy_count,
|
|
2677
|
-
scan_intervals=[int(
|
|
3065
|
+
scan_intervals=[int(scan_interval / 0.625)] * scanning_phy_count,
|
|
2678
3066
|
scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
|
|
2679
3067
|
),
|
|
2680
3068
|
check_result=True,
|
|
@@ -2840,6 +3228,41 @@ class Device(CompositeEventEmitter):
|
|
|
2840
3228
|
"periodic advertising sync establishment for unknown address/sid"
|
|
2841
3229
|
)
|
|
2842
3230
|
|
|
3231
|
+
@host_event_handler
|
|
3232
|
+
def on_periodic_advertising_sync_transfer(
|
|
3233
|
+
self,
|
|
3234
|
+
status: int,
|
|
3235
|
+
connection_handle: int,
|
|
3236
|
+
sync_handle: int,
|
|
3237
|
+
advertising_sid: int,
|
|
3238
|
+
advertiser_address: hci.Address,
|
|
3239
|
+
advertiser_phy: int,
|
|
3240
|
+
periodic_advertising_interval: int,
|
|
3241
|
+
advertiser_clock_accuracy: int,
|
|
3242
|
+
) -> None:
|
|
3243
|
+
if not (connection := self.lookup_connection(connection_handle)):
|
|
3244
|
+
logger.error(
|
|
3245
|
+
"Receive PAST from unknown connection 0x%04X", connection_handle
|
|
3246
|
+
)
|
|
3247
|
+
|
|
3248
|
+
pa_sync = PeriodicAdvertisingSync(
|
|
3249
|
+
device=self,
|
|
3250
|
+
advertiser_address=advertiser_address,
|
|
3251
|
+
sid=advertising_sid,
|
|
3252
|
+
skip=0,
|
|
3253
|
+
sync_timeout=0.0,
|
|
3254
|
+
filter_duplicates=False,
|
|
3255
|
+
)
|
|
3256
|
+
self.periodic_advertising_syncs.append(pa_sync)
|
|
3257
|
+
pa_sync.on_establishment(
|
|
3258
|
+
status=status,
|
|
3259
|
+
sync_handle=sync_handle,
|
|
3260
|
+
advertiser_phy=advertiser_phy,
|
|
3261
|
+
periodic_advertising_interval=periodic_advertising_interval,
|
|
3262
|
+
advertiser_clock_accuracy=advertiser_clock_accuracy,
|
|
3263
|
+
)
|
|
3264
|
+
self.emit('periodic_advertising_sync_transfer', pa_sync, connection)
|
|
3265
|
+
|
|
2843
3266
|
@host_event_handler
|
|
2844
3267
|
@with_periodic_advertising_sync_from_handle
|
|
2845
3268
|
def on_periodic_advertising_sync_loss(
|
|
@@ -3958,13 +4381,13 @@ class Device(CompositeEventEmitter):
|
|
|
3958
4381
|
async def setup_cig(
|
|
3959
4382
|
self,
|
|
3960
4383
|
cig_id: int,
|
|
3961
|
-
cis_id:
|
|
3962
|
-
sdu_interval:
|
|
4384
|
+
cis_id: Sequence[int],
|
|
4385
|
+
sdu_interval: tuple[int, int],
|
|
3963
4386
|
framing: int,
|
|
3964
|
-
max_sdu:
|
|
4387
|
+
max_sdu: tuple[int, int],
|
|
3965
4388
|
retransmission_number: int,
|
|
3966
|
-
max_transport_latency:
|
|
3967
|
-
) ->
|
|
4389
|
+
max_transport_latency: tuple[int, int],
|
|
4390
|
+
) -> list[int]:
|
|
3968
4391
|
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
|
3969
4392
|
|
|
3970
4393
|
Args:
|
|
@@ -4013,7 +4436,9 @@ class Device(CompositeEventEmitter):
|
|
|
4013
4436
|
|
|
4014
4437
|
# [LE only]
|
|
4015
4438
|
@experimental('Only for testing.')
|
|
4016
|
-
async def create_cis(
|
|
4439
|
+
async def create_cis(
|
|
4440
|
+
self, cis_acl_pairs: Sequence[tuple[int, int]]
|
|
4441
|
+
) -> list[CisLink]:
|
|
4017
4442
|
for cis_handle, acl_handle in cis_acl_pairs:
|
|
4018
4443
|
acl_connection = self.lookup_connection(acl_handle)
|
|
4019
4444
|
assert acl_connection
|
|
@@ -4112,6 +4537,106 @@ class Device(CompositeEventEmitter):
|
|
|
4112
4537
|
check_result=True,
|
|
4113
4538
|
)
|
|
4114
4539
|
|
|
4540
|
+
# [LE only]
|
|
4541
|
+
@experimental('Only for testing.')
|
|
4542
|
+
async def create_big(
|
|
4543
|
+
self, advertising_set: AdvertisingSet, parameters: BigParameters
|
|
4544
|
+
) -> Big:
|
|
4545
|
+
if (big_handle := self.next_big_handle()) is None:
|
|
4546
|
+
raise core.OutOfResourcesError("All valid BIG handles already in use")
|
|
4547
|
+
|
|
4548
|
+
with closing(EventWatcher()) as watcher:
|
|
4549
|
+
big = Big(
|
|
4550
|
+
big_handle=big_handle,
|
|
4551
|
+
parameters=parameters,
|
|
4552
|
+
advertising_set=advertising_set,
|
|
4553
|
+
)
|
|
4554
|
+
self.bigs[big_handle] = big
|
|
4555
|
+
established = asyncio.get_running_loop().create_future()
|
|
4556
|
+
watcher.once(
|
|
4557
|
+
big, big.Event.ESTABLISHMENT, lambda: established.set_result(None)
|
|
4558
|
+
)
|
|
4559
|
+
watcher.once(
|
|
4560
|
+
big,
|
|
4561
|
+
big.Event.ESTABLISHMENT_FAILURE,
|
|
4562
|
+
lambda status: established.set_exception(hci.HCI_Error(status)),
|
|
4563
|
+
)
|
|
4564
|
+
|
|
4565
|
+
try:
|
|
4566
|
+
await self.send_command(
|
|
4567
|
+
hci.HCI_LE_Create_BIG_Command(
|
|
4568
|
+
big_handle=big_handle,
|
|
4569
|
+
advertising_handle=advertising_set.advertising_handle,
|
|
4570
|
+
num_bis=parameters.num_bis,
|
|
4571
|
+
sdu_interval=parameters.sdu_interval,
|
|
4572
|
+
max_sdu=parameters.max_sdu,
|
|
4573
|
+
max_transport_latency=parameters.max_transport_latency,
|
|
4574
|
+
rtn=parameters.rtn,
|
|
4575
|
+
phy=parameters.phy,
|
|
4576
|
+
packing=parameters.packing,
|
|
4577
|
+
framing=parameters.framing,
|
|
4578
|
+
encryption=1 if parameters.broadcast_code else 0,
|
|
4579
|
+
broadcast_code=parameters.broadcast_code or bytes(16),
|
|
4580
|
+
),
|
|
4581
|
+
check_result=True,
|
|
4582
|
+
)
|
|
4583
|
+
await established
|
|
4584
|
+
except hci.HCI_Error:
|
|
4585
|
+
del self.bigs[big_handle]
|
|
4586
|
+
raise
|
|
4587
|
+
|
|
4588
|
+
return big
|
|
4589
|
+
|
|
4590
|
+
# [LE only]
|
|
4591
|
+
@experimental('Only for testing.')
|
|
4592
|
+
async def create_big_sync(
|
|
4593
|
+
self, pa_sync: PeriodicAdvertisingSync, parameters: BigSyncParameters
|
|
4594
|
+
) -> BigSync:
|
|
4595
|
+
if (big_handle := self.next_big_handle()) is None:
|
|
4596
|
+
raise core.OutOfResourcesError("All valid BIG handles already in use")
|
|
4597
|
+
|
|
4598
|
+
if (pa_sync_handle := pa_sync.sync_handle) is None:
|
|
4599
|
+
raise core.InvalidStateError("PA Sync is not established")
|
|
4600
|
+
|
|
4601
|
+
with closing(EventWatcher()) as watcher:
|
|
4602
|
+
big_sync = BigSync(
|
|
4603
|
+
big_handle=big_handle,
|
|
4604
|
+
parameters=parameters,
|
|
4605
|
+
pa_sync=pa_sync,
|
|
4606
|
+
)
|
|
4607
|
+
self.big_syncs[big_handle] = big_sync
|
|
4608
|
+
established = asyncio.get_running_loop().create_future()
|
|
4609
|
+
watcher.once(
|
|
4610
|
+
big_sync,
|
|
4611
|
+
big_sync.Event.ESTABLISHMENT,
|
|
4612
|
+
lambda: established.set_result(None),
|
|
4613
|
+
)
|
|
4614
|
+
watcher.once(
|
|
4615
|
+
big_sync,
|
|
4616
|
+
big_sync.Event.ESTABLISHMENT_FAILURE,
|
|
4617
|
+
lambda status: established.set_exception(hci.HCI_Error(status)),
|
|
4618
|
+
)
|
|
4619
|
+
|
|
4620
|
+
try:
|
|
4621
|
+
await self.send_command(
|
|
4622
|
+
hci.HCI_LE_BIG_Create_Sync_Command(
|
|
4623
|
+
big_handle=big_handle,
|
|
4624
|
+
sync_handle=pa_sync_handle,
|
|
4625
|
+
encryption=1 if parameters.broadcast_code else 0,
|
|
4626
|
+
broadcast_code=parameters.broadcast_code or bytes(16),
|
|
4627
|
+
mse=parameters.mse,
|
|
4628
|
+
big_sync_timeout=parameters.big_sync_timeout,
|
|
4629
|
+
bis=parameters.bis,
|
|
4630
|
+
),
|
|
4631
|
+
check_result=True,
|
|
4632
|
+
)
|
|
4633
|
+
await established
|
|
4634
|
+
except hci.HCI_Error:
|
|
4635
|
+
del self.big_syncs[big_handle]
|
|
4636
|
+
raise
|
|
4637
|
+
|
|
4638
|
+
return big_sync
|
|
4639
|
+
|
|
4115
4640
|
async def get_remote_le_features(self, connection: Connection) -> hci.LeFeatureMask:
|
|
4116
4641
|
"""[LE Only] Reads remote LE supported features.
|
|
4117
4642
|
|
|
@@ -4144,6 +4669,213 @@ class Device(CompositeEventEmitter):
|
|
|
4144
4669
|
)
|
|
4145
4670
|
return await read_feature_future
|
|
4146
4671
|
|
|
4672
|
+
@experimental('Only for testing.')
|
|
4673
|
+
async def get_remote_cs_capabilities(
|
|
4674
|
+
self, connection: Connection
|
|
4675
|
+
) -> ChannelSoundingCapabilities:
|
|
4676
|
+
complete_future: asyncio.Future[ChannelSoundingCapabilities] = (
|
|
4677
|
+
asyncio.get_running_loop().create_future()
|
|
4678
|
+
)
|
|
4679
|
+
|
|
4680
|
+
with closing(EventWatcher()) as watcher:
|
|
4681
|
+
watcher.once(
|
|
4682
|
+
connection, 'channel_sounding_capabilities', complete_future.set_result
|
|
4683
|
+
)
|
|
4684
|
+
watcher.once(
|
|
4685
|
+
connection,
|
|
4686
|
+
'channel_sounding_capabilities_failure',
|
|
4687
|
+
lambda status: complete_future.set_exception(hci.HCI_Error(status)),
|
|
4688
|
+
)
|
|
4689
|
+
await self.send_command(
|
|
4690
|
+
hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Command(
|
|
4691
|
+
connection_handle=connection.handle
|
|
4692
|
+
),
|
|
4693
|
+
check_result=True,
|
|
4694
|
+
)
|
|
4695
|
+
return await complete_future
|
|
4696
|
+
|
|
4697
|
+
@experimental('Only for testing.')
|
|
4698
|
+
async def set_default_cs_settings(
|
|
4699
|
+
self,
|
|
4700
|
+
connection: Connection,
|
|
4701
|
+
role_enable: int = (
|
|
4702
|
+
hci.CsRoleMask.INITIATOR | hci.CsRoleMask.REFLECTOR
|
|
4703
|
+
), # Both role
|
|
4704
|
+
cs_sync_antenna_selection: int = 0xFF, # No Preference
|
|
4705
|
+
max_tx_power: int = 0x04, # 4 dB
|
|
4706
|
+
) -> None:
|
|
4707
|
+
await self.send_command(
|
|
4708
|
+
hci.HCI_LE_CS_Set_Default_Settings_Command(
|
|
4709
|
+
connection_handle=connection.handle,
|
|
4710
|
+
role_enable=role_enable,
|
|
4711
|
+
cs_sync_antenna_selection=cs_sync_antenna_selection,
|
|
4712
|
+
max_tx_power=max_tx_power,
|
|
4713
|
+
),
|
|
4714
|
+
check_result=True,
|
|
4715
|
+
)
|
|
4716
|
+
|
|
4717
|
+
@experimental('Only for testing.')
|
|
4718
|
+
async def create_cs_config(
|
|
4719
|
+
self,
|
|
4720
|
+
connection: Connection,
|
|
4721
|
+
config_id: int | None = None,
|
|
4722
|
+
create_context: int = 0x01,
|
|
4723
|
+
main_mode_type: int = 0x02,
|
|
4724
|
+
sub_mode_type: int = 0xFF,
|
|
4725
|
+
min_main_mode_steps: int = 0x02,
|
|
4726
|
+
max_main_mode_steps: int = 0x05,
|
|
4727
|
+
main_mode_repetition: int = 0x00,
|
|
4728
|
+
mode_0_steps: int = 0x03,
|
|
4729
|
+
role: int = hci.CsRole.INITIATOR,
|
|
4730
|
+
rtt_type: int = hci.RttType.AA_ONLY,
|
|
4731
|
+
cs_sync_phy: int = hci.CsSyncPhy.LE_1M,
|
|
4732
|
+
channel_map: bytes = b'\x54\x55\x55\x54\x55\x55\x55\x55\x55\x15',
|
|
4733
|
+
channel_map_repetition: int = 0x01,
|
|
4734
|
+
channel_selection_type: int = hci.HCI_LE_CS_Create_Config_Command.ChannelSelectionType.ALGO_3B,
|
|
4735
|
+
ch3c_shape: int = hci.HCI_LE_CS_Create_Config_Command.Ch3cShape.HAT,
|
|
4736
|
+
ch3c_jump: int = 0x03,
|
|
4737
|
+
) -> ChannelSoundingConfig:
|
|
4738
|
+
complete_future: asyncio.Future[ChannelSoundingConfig] = (
|
|
4739
|
+
asyncio.get_running_loop().create_future()
|
|
4740
|
+
)
|
|
4741
|
+
if config_id is None:
|
|
4742
|
+
# Allocate an ID.
|
|
4743
|
+
config_id = next(
|
|
4744
|
+
(
|
|
4745
|
+
i
|
|
4746
|
+
for i in range(DEVICE_MIN_CS_CONFIG_ID, DEVICE_MAX_CS_CONFIG_ID + 1)
|
|
4747
|
+
if i not in connection.cs_configs
|
|
4748
|
+
),
|
|
4749
|
+
None,
|
|
4750
|
+
)
|
|
4751
|
+
if config_id is None:
|
|
4752
|
+
raise OutOfResourcesError("No available config ID on this connection!")
|
|
4753
|
+
|
|
4754
|
+
with closing(EventWatcher()) as watcher:
|
|
4755
|
+
watcher.once(
|
|
4756
|
+
connection, 'channel_sounding_config', complete_future.set_result
|
|
4757
|
+
)
|
|
4758
|
+
watcher.once(
|
|
4759
|
+
connection,
|
|
4760
|
+
'channel_sounding_config_failure',
|
|
4761
|
+
lambda status: complete_future.set_exception(hci.HCI_Error(status)),
|
|
4762
|
+
)
|
|
4763
|
+
await self.send_command(
|
|
4764
|
+
hci.HCI_LE_CS_Create_Config_Command(
|
|
4765
|
+
connection_handle=connection.handle,
|
|
4766
|
+
config_id=config_id,
|
|
4767
|
+
create_context=create_context,
|
|
4768
|
+
main_mode_type=main_mode_type,
|
|
4769
|
+
sub_mode_type=sub_mode_type,
|
|
4770
|
+
min_main_mode_steps=min_main_mode_steps,
|
|
4771
|
+
max_main_mode_steps=max_main_mode_steps,
|
|
4772
|
+
main_mode_repetition=main_mode_repetition,
|
|
4773
|
+
mode_0_steps=mode_0_steps,
|
|
4774
|
+
role=role,
|
|
4775
|
+
rtt_type=rtt_type,
|
|
4776
|
+
cs_sync_phy=cs_sync_phy,
|
|
4777
|
+
channel_map=channel_map,
|
|
4778
|
+
channel_map_repetition=channel_map_repetition,
|
|
4779
|
+
channel_selection_type=channel_selection_type,
|
|
4780
|
+
ch3c_shape=ch3c_shape,
|
|
4781
|
+
ch3c_jump=ch3c_jump,
|
|
4782
|
+
reserved=0x00,
|
|
4783
|
+
),
|
|
4784
|
+
check_result=True,
|
|
4785
|
+
)
|
|
4786
|
+
return await complete_future
|
|
4787
|
+
|
|
4788
|
+
@experimental('Only for testing.')
|
|
4789
|
+
async def enable_cs_security(self, connection: Connection) -> None:
|
|
4790
|
+
complete_future: asyncio.Future[None] = (
|
|
4791
|
+
asyncio.get_running_loop().create_future()
|
|
4792
|
+
)
|
|
4793
|
+
with closing(EventWatcher()) as watcher:
|
|
4794
|
+
|
|
4795
|
+
def on_event(event: hci.HCI_LE_CS_Security_Enable_Complete_Event) -> None:
|
|
4796
|
+
if event.connection_handle != connection.handle:
|
|
4797
|
+
return
|
|
4798
|
+
if event.status == hci.HCI_SUCCESS:
|
|
4799
|
+
complete_future.set_result(None)
|
|
4800
|
+
else:
|
|
4801
|
+
complete_future.set_exception(hci.HCI_Error(event.status))
|
|
4802
|
+
|
|
4803
|
+
watcher.once(self.host, 'cs_security', on_event)
|
|
4804
|
+
await self.send_command(
|
|
4805
|
+
hci.HCI_LE_CS_Security_Enable_Command(
|
|
4806
|
+
connection_handle=connection.handle
|
|
4807
|
+
),
|
|
4808
|
+
check_result=True,
|
|
4809
|
+
)
|
|
4810
|
+
return await complete_future
|
|
4811
|
+
|
|
4812
|
+
@experimental('Only for testing.')
|
|
4813
|
+
async def set_cs_procedure_parameters(
|
|
4814
|
+
self,
|
|
4815
|
+
connection: Connection,
|
|
4816
|
+
config: ChannelSoundingConfig,
|
|
4817
|
+
tone_antenna_config_selection=0x00,
|
|
4818
|
+
preferred_peer_antenna=0x00,
|
|
4819
|
+
max_procedure_len=0x2710, # 6.25s
|
|
4820
|
+
min_procedure_interval=0x01,
|
|
4821
|
+
max_procedure_interval=0xFF,
|
|
4822
|
+
max_procedure_count=0x01,
|
|
4823
|
+
min_subevent_len=0x0004E2, # 1250us
|
|
4824
|
+
max_subevent_len=0x1E8480, # 2s
|
|
4825
|
+
phy=hci.CsSyncPhy.LE_1M,
|
|
4826
|
+
tx_power_delta=0x00,
|
|
4827
|
+
snr_control_initiator=hci.CsSnr.NOT_APPLIED,
|
|
4828
|
+
snr_control_reflector=hci.CsSnr.NOT_APPLIED,
|
|
4829
|
+
) -> None:
|
|
4830
|
+
await self.send_command(
|
|
4831
|
+
hci.HCI_LE_CS_Set_Procedure_Parameters_Command(
|
|
4832
|
+
connection_handle=connection.handle,
|
|
4833
|
+
config_id=config.config_id,
|
|
4834
|
+
max_procedure_len=max_procedure_len,
|
|
4835
|
+
min_procedure_interval=min_procedure_interval,
|
|
4836
|
+
max_procedure_interval=max_procedure_interval,
|
|
4837
|
+
max_procedure_count=max_procedure_count,
|
|
4838
|
+
min_subevent_len=min_subevent_len,
|
|
4839
|
+
max_subevent_len=max_subevent_len,
|
|
4840
|
+
tone_antenna_config_selection=tone_antenna_config_selection,
|
|
4841
|
+
phy=phy,
|
|
4842
|
+
tx_power_delta=tx_power_delta,
|
|
4843
|
+
preferred_peer_antenna=preferred_peer_antenna,
|
|
4844
|
+
snr_control_initiator=snr_control_initiator,
|
|
4845
|
+
snr_control_reflector=snr_control_reflector,
|
|
4846
|
+
),
|
|
4847
|
+
check_result=True,
|
|
4848
|
+
)
|
|
4849
|
+
|
|
4850
|
+
@experimental('Only for testing.')
|
|
4851
|
+
async def enable_cs_procedure(
|
|
4852
|
+
self,
|
|
4853
|
+
connection: Connection,
|
|
4854
|
+
config: ChannelSoundingConfig,
|
|
4855
|
+
enabled: bool = True,
|
|
4856
|
+
) -> ChannelSoundingProcedure:
|
|
4857
|
+
complete_future: asyncio.Future[ChannelSoundingProcedure] = (
|
|
4858
|
+
asyncio.get_running_loop().create_future()
|
|
4859
|
+
)
|
|
4860
|
+
with closing(EventWatcher()) as watcher:
|
|
4861
|
+
watcher.once(
|
|
4862
|
+
connection, 'channel_sounding_procedure', complete_future.set_result
|
|
4863
|
+
)
|
|
4864
|
+
watcher.once(
|
|
4865
|
+
connection,
|
|
4866
|
+
'channel_sounding_procedure_failure',
|
|
4867
|
+
lambda x: complete_future.set_exception(hci.HCI_Error(x)),
|
|
4868
|
+
)
|
|
4869
|
+
await self.send_command(
|
|
4870
|
+
hci.HCI_LE_CS_Procedure_Enable_Command(
|
|
4871
|
+
connection_handle=connection.handle,
|
|
4872
|
+
config_id=config.config_id,
|
|
4873
|
+
enable=enabled,
|
|
4874
|
+
),
|
|
4875
|
+
check_result=True,
|
|
4876
|
+
)
|
|
4877
|
+
return await complete_future
|
|
4878
|
+
|
|
4147
4879
|
@host_event_handler
|
|
4148
4880
|
def on_flush(self):
|
|
4149
4881
|
self.emit('flush')
|
|
@@ -4178,10 +4910,15 @@ class Device(CompositeEventEmitter):
|
|
|
4178
4910
|
def add_services(self, services):
|
|
4179
4911
|
self.gatt_server.add_services(services)
|
|
4180
4912
|
|
|
4181
|
-
def add_default_services(
|
|
4913
|
+
def add_default_services(
|
|
4914
|
+
self, add_gap_service: bool = True, add_gatt_service: bool = True
|
|
4915
|
+
) -> None:
|
|
4182
4916
|
# Add a GAP Service if requested
|
|
4183
|
-
if
|
|
4917
|
+
if add_gap_service:
|
|
4184
4918
|
self.gatt_server.add_service(GenericAccessService(self.name))
|
|
4919
|
+
if add_gatt_service:
|
|
4920
|
+
self.gatt_service = gatt_service.GenericAttributeProfileService()
|
|
4921
|
+
self.gatt_server.add_service(self.gatt_service)
|
|
4185
4922
|
|
|
4186
4923
|
async def notify_subscriber(self, connection, attribute, value=None, force=False):
|
|
4187
4924
|
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
|
@@ -4233,6 +4970,112 @@ class Device(CompositeEventEmitter):
|
|
|
4233
4970
|
)
|
|
4234
4971
|
self.connecting_extended_advertising_sets[connection_handle] = advertising_set
|
|
4235
4972
|
|
|
4973
|
+
@host_event_handler
|
|
4974
|
+
def on_big_establishment(
|
|
4975
|
+
self,
|
|
4976
|
+
status: int,
|
|
4977
|
+
big_handle: int,
|
|
4978
|
+
bis_handles: list[int],
|
|
4979
|
+
big_sync_delay: int,
|
|
4980
|
+
transport_latency_big: int,
|
|
4981
|
+
phy: int,
|
|
4982
|
+
nse: int,
|
|
4983
|
+
bn: int,
|
|
4984
|
+
pto: int,
|
|
4985
|
+
irc: int,
|
|
4986
|
+
max_pdu: int,
|
|
4987
|
+
iso_interval: int,
|
|
4988
|
+
) -> None:
|
|
4989
|
+
if not (big := self.bigs.get(big_handle)):
|
|
4990
|
+
logger.warning('BIG %d not found', big_handle)
|
|
4991
|
+
return
|
|
4992
|
+
|
|
4993
|
+
if status != hci.HCI_SUCCESS:
|
|
4994
|
+
del self.bigs[big_handle]
|
|
4995
|
+
logger.debug('Unable to create BIG %d', big_handle)
|
|
4996
|
+
big.state = Big.State.TERMINATED
|
|
4997
|
+
big.emit(Big.Event.ESTABLISHMENT_FAILURE, status)
|
|
4998
|
+
return
|
|
4999
|
+
|
|
5000
|
+
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
|
5001
|
+
big.big_sync_delay = big_sync_delay
|
|
5002
|
+
big.transport_latency_big = transport_latency_big
|
|
5003
|
+
big.phy = phy
|
|
5004
|
+
big.nse = nse
|
|
5005
|
+
big.bn = bn
|
|
5006
|
+
big.pto = pto
|
|
5007
|
+
big.irc = irc
|
|
5008
|
+
big.max_pdu = max_pdu
|
|
5009
|
+
big.iso_interval = iso_interval
|
|
5010
|
+
big.state = Big.State.ACTIVE
|
|
5011
|
+
|
|
5012
|
+
for bis_link in big.bis_links:
|
|
5013
|
+
self.bis_links[bis_link.handle] = bis_link
|
|
5014
|
+
big.emit(Big.Event.ESTABLISHMENT)
|
|
5015
|
+
|
|
5016
|
+
@host_event_handler
|
|
5017
|
+
def on_big_termination(self, reason: int, big_handle: int) -> None:
|
|
5018
|
+
if not (big := self.bigs.pop(big_handle, None)):
|
|
5019
|
+
logger.warning('BIG %d not found', big_handle)
|
|
5020
|
+
return
|
|
5021
|
+
|
|
5022
|
+
big.state = Big.State.TERMINATED
|
|
5023
|
+
for bis_link in big.bis_links:
|
|
5024
|
+
self.bis_links.pop(bis_link.handle, None)
|
|
5025
|
+
big.emit(Big.Event.TERMINATION, reason)
|
|
5026
|
+
|
|
5027
|
+
@host_event_handler
|
|
5028
|
+
def on_big_sync_establishment(
|
|
5029
|
+
self,
|
|
5030
|
+
status: int,
|
|
5031
|
+
big_handle: int,
|
|
5032
|
+
transport_latency_big: int,
|
|
5033
|
+
nse: int,
|
|
5034
|
+
bn: int,
|
|
5035
|
+
pto: int,
|
|
5036
|
+
irc: int,
|
|
5037
|
+
max_pdu: int,
|
|
5038
|
+
iso_interval: int,
|
|
5039
|
+
bis_handles: list[int],
|
|
5040
|
+
) -> None:
|
|
5041
|
+
if not (big_sync := self.big_syncs.get(big_handle)):
|
|
5042
|
+
logger.warning('BIG Sync %d not found', big_handle)
|
|
5043
|
+
return
|
|
5044
|
+
|
|
5045
|
+
if status != hci.HCI_SUCCESS:
|
|
5046
|
+
del self.big_syncs[big_handle]
|
|
5047
|
+
logger.debug('Unable to create BIG Sync %d', big_handle)
|
|
5048
|
+
big_sync.state = BigSync.State.TERMINATED
|
|
5049
|
+
big_sync.emit(BigSync.Event.ESTABLISHMENT_FAILURE, status)
|
|
5050
|
+
return
|
|
5051
|
+
|
|
5052
|
+
big_sync.transport_latency_big = transport_latency_big
|
|
5053
|
+
big_sync.nse = nse
|
|
5054
|
+
big_sync.bn = bn
|
|
5055
|
+
big_sync.pto = pto
|
|
5056
|
+
big_sync.irc = irc
|
|
5057
|
+
big_sync.max_pdu = max_pdu
|
|
5058
|
+
big_sync.iso_interval = iso_interval
|
|
5059
|
+
big_sync.bis_links = [
|
|
5060
|
+
BisLink(handle=handle, big=big_sync) for handle in bis_handles
|
|
5061
|
+
]
|
|
5062
|
+
big_sync.state = BigSync.State.ACTIVE
|
|
5063
|
+
|
|
5064
|
+
for bis_link in big_sync.bis_links:
|
|
5065
|
+
self.bis_links[bis_link.handle] = bis_link
|
|
5066
|
+
big_sync.emit(BigSync.Event.ESTABLISHMENT)
|
|
5067
|
+
|
|
5068
|
+
@host_event_handler
|
|
5069
|
+
def on_big_sync_lost(self, big_handle: int, reason: int) -> None:
|
|
5070
|
+
if not (big_sync := self.big_syncs.pop(big_handle, None)):
|
|
5071
|
+
logger.warning('BIG %d not found', big_handle)
|
|
5072
|
+
return
|
|
5073
|
+
|
|
5074
|
+
for bis_link in big_sync.bis_links:
|
|
5075
|
+
self.bis_links.pop(bis_link.handle, None)
|
|
5076
|
+
big_sync.state = BigSync.State.TERMINATED
|
|
5077
|
+
big_sync.emit(BigSync.Event.TERMINATION, reason)
|
|
5078
|
+
|
|
4236
5079
|
def _complete_le_extended_advertising_connection(
|
|
4237
5080
|
self, connection: Connection, advertising_set: AdvertisingSet
|
|
4238
5081
|
) -> None:
|
|
@@ -4879,6 +5722,8 @@ class Device(CompositeEventEmitter):
|
|
|
4879
5722
|
def on_iso_packet(self, handle: int, packet: hci.HCI_IsoDataPacket) -> None:
|
|
4880
5723
|
if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
|
|
4881
5724
|
cis_link.sink(packet)
|
|
5725
|
+
elif (bis_link := self.bis_links.get(handle)) and bis_link.sink:
|
|
5726
|
+
bis_link.sink(packet)
|
|
4882
5727
|
|
|
4883
5728
|
@host_event_handler
|
|
4884
5729
|
@with_connection_from_handle
|
|
@@ -4994,6 +5839,106 @@ class Device(CompositeEventEmitter):
|
|
|
4994
5839
|
)
|
|
4995
5840
|
connection.emit('connection_data_length_change')
|
|
4996
5841
|
|
|
5842
|
+
@host_event_handler
|
|
5843
|
+
def on_cs_remote_supported_capabilities(
|
|
5844
|
+
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
|
5845
|
+
):
|
|
5846
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5847
|
+
return
|
|
5848
|
+
|
|
5849
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5850
|
+
connection.emit('channel_sounding_capabilities_failure', event.status)
|
|
5851
|
+
return
|
|
5852
|
+
|
|
5853
|
+
capabilities = ChannelSoundingCapabilities(
|
|
5854
|
+
num_config_supported=event.num_config_supported,
|
|
5855
|
+
max_consecutive_procedures_supported=event.max_consecutive_procedures_supported,
|
|
5856
|
+
num_antennas_supported=event.num_antennas_supported,
|
|
5857
|
+
max_antenna_paths_supported=event.max_antenna_paths_supported,
|
|
5858
|
+
roles_supported=event.roles_supported,
|
|
5859
|
+
modes_supported=event.modes_supported,
|
|
5860
|
+
rtt_capability=event.rtt_capability,
|
|
5861
|
+
rtt_aa_only_n=event.rtt_aa_only_n,
|
|
5862
|
+
rtt_sounding_n=event.rtt_sounding_n,
|
|
5863
|
+
rtt_random_payload_n=event.rtt_random_payload_n,
|
|
5864
|
+
nadm_sounding_capability=event.nadm_sounding_capability,
|
|
5865
|
+
nadm_random_capability=event.nadm_random_capability,
|
|
5866
|
+
cs_sync_phys_supported=event.cs_sync_phys_supported,
|
|
5867
|
+
subfeatures_supported=event.subfeatures_supported,
|
|
5868
|
+
t_ip1_times_supported=event.t_ip1_times_supported,
|
|
5869
|
+
t_ip2_times_supported=event.t_ip2_times_supported,
|
|
5870
|
+
t_fcs_times_supported=event.t_fcs_times_supported,
|
|
5871
|
+
t_pm_times_supported=event.t_pm_times_supported,
|
|
5872
|
+
t_sw_time_supported=event.t_sw_time_supported,
|
|
5873
|
+
tx_snr_capability=event.tx_snr_capability,
|
|
5874
|
+
)
|
|
5875
|
+
connection.emit('channel_sounding_capabilities', capabilities)
|
|
5876
|
+
|
|
5877
|
+
@host_event_handler
|
|
5878
|
+
def on_cs_config(self, event: hci.HCI_LE_CS_Config_Complete_Event):
|
|
5879
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5880
|
+
return
|
|
5881
|
+
|
|
5882
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5883
|
+
connection.emit('channel_sounding_config_failure', event.status)
|
|
5884
|
+
return
|
|
5885
|
+
if event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.CREATED:
|
|
5886
|
+
config = ChannelSoundingConfig(
|
|
5887
|
+
config_id=event.config_id,
|
|
5888
|
+
main_mode_type=event.main_mode_type,
|
|
5889
|
+
sub_mode_type=event.sub_mode_type,
|
|
5890
|
+
min_main_mode_steps=event.min_main_mode_steps,
|
|
5891
|
+
max_main_mode_steps=event.max_main_mode_steps,
|
|
5892
|
+
main_mode_repetition=event.main_mode_repetition,
|
|
5893
|
+
mode_0_steps=event.mode_0_steps,
|
|
5894
|
+
role=event.role,
|
|
5895
|
+
rtt_type=event.rtt_type,
|
|
5896
|
+
cs_sync_phy=event.cs_sync_phy,
|
|
5897
|
+
channel_map=event.channel_map,
|
|
5898
|
+
channel_map_repetition=event.channel_map_repetition,
|
|
5899
|
+
channel_selection_type=event.channel_selection_type,
|
|
5900
|
+
ch3c_shape=event.ch3c_shape,
|
|
5901
|
+
ch3c_jump=event.ch3c_jump,
|
|
5902
|
+
reserved=event.reserved,
|
|
5903
|
+
t_ip1_time=event.t_ip1_time,
|
|
5904
|
+
t_ip2_time=event.t_ip2_time,
|
|
5905
|
+
t_fcs_time=event.t_fcs_time,
|
|
5906
|
+
t_pm_time=event.t_pm_time,
|
|
5907
|
+
)
|
|
5908
|
+
connection.cs_configs[event.config_id] = config
|
|
5909
|
+
connection.emit('channel_sounding_config', config)
|
|
5910
|
+
elif event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.REMOVED:
|
|
5911
|
+
try:
|
|
5912
|
+
config = connection.cs_configs.pop(event.config_id)
|
|
5913
|
+
connection.emit('channel_sounding_config_removed', config.config_id)
|
|
5914
|
+
except KeyError:
|
|
5915
|
+
logger.error('Removing unknown config %d', event.config_id)
|
|
5916
|
+
|
|
5917
|
+
@host_event_handler
|
|
5918
|
+
def on_cs_procedure(self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event):
|
|
5919
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5920
|
+
return
|
|
5921
|
+
|
|
5922
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5923
|
+
connection.emit('channel_sounding_procedure_failure', event.status)
|
|
5924
|
+
return
|
|
5925
|
+
|
|
5926
|
+
procedure = ChannelSoundingProcedure(
|
|
5927
|
+
config_id=event.config_id,
|
|
5928
|
+
state=event.state,
|
|
5929
|
+
tone_antenna_config_selection=event.tone_antenna_config_selection,
|
|
5930
|
+
selected_tx_power=event.selected_tx_power,
|
|
5931
|
+
subevent_len=event.subevent_len,
|
|
5932
|
+
subevents_per_event=event.subevents_per_event,
|
|
5933
|
+
subevent_interval=event.subevent_interval,
|
|
5934
|
+
event_interval=event.event_interval,
|
|
5935
|
+
procedure_interval=event.procedure_interval,
|
|
5936
|
+
procedure_count=event.procedure_count,
|
|
5937
|
+
max_procedure_len=event.max_procedure_len,
|
|
5938
|
+
)
|
|
5939
|
+
connection.cs_procedures[procedure.config_id] = procedure
|
|
5940
|
+
connection.emit('channel_sounding_procedure', procedure)
|
|
5941
|
+
|
|
4997
5942
|
# [Classic only]
|
|
4998
5943
|
@host_event_handler
|
|
4999
5944
|
@with_connection_from_address
|
|
@@ -5051,14 +5996,14 @@ class Device(CompositeEventEmitter):
|
|
|
5051
5996
|
if att_pdu.op_code & 1:
|
|
5052
5997
|
if connection.gatt_client is None:
|
|
5053
5998
|
logger.warning(
|
|
5054
|
-
|
|
5999
|
+
'No GATT client for connection 0x%04X', connection.handle
|
|
5055
6000
|
)
|
|
5056
6001
|
return
|
|
5057
6002
|
connection.gatt_client.on_gatt_pdu(att_pdu)
|
|
5058
6003
|
else:
|
|
5059
6004
|
if connection.gatt_server is None:
|
|
5060
6005
|
logger.warning(
|
|
5061
|
-
|
|
6006
|
+
'No GATT server for connection 0x%04X', connection.handle
|
|
5062
6007
|
)
|
|
5063
6008
|
return
|
|
5064
6009
|
connection.gatt_server.on_gatt_pdu(connection, att_pdu)
|