bumble 0.0.204__py3-none-any.whl → 0.0.208__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 +9 -4
- bumble/apps/auracast.py +631 -98
- bumble/apps/bench.py +238 -157
- bumble/apps/console.py +19 -12
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/gg_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/att.py +51 -37
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +305 -156
- bumble/device.py +1090 -99
- bumble/gatt.py +36 -226
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +52 -33
- bumble/gatt_server.py +5 -5
- bumble/hci.py +812 -14
- bumble/host.py +367 -65
- bumble/l2cap.py +3 -16
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +48 -57
- bumble/profiles/ascs.py +8 -19
- bumble/profiles/asha.py +16 -14
- bumble/profiles/bass.py +16 -22
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +167 -0
- bumble/profiles/gmap.py +198 -0
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +61 -16
- bumble/profiles/tmap.py +8 -12
- bumble/profiles/{vcp.py → vcs.py} +35 -29
- bumble/profiles/vocs.py +62 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +12 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.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,
|
|
@@ -53,8 +53,8 @@ from pyee import EventEmitter
|
|
|
53
53
|
|
|
54
54
|
from .colors import color
|
|
55
55
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
|
56
|
-
from .gatt import Characteristic, Descriptor, Service
|
|
57
|
-
from .host import Host
|
|
56
|
+
from .gatt import Attribute, Characteristic, Descriptor, Service
|
|
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:
|
|
@@ -1258,7 +1586,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1258
1586
|
def on_connection_data_length_change(self):
|
|
1259
1587
|
pass
|
|
1260
1588
|
|
|
1261
|
-
def on_connection_phy_update(self):
|
|
1589
|
+
def on_connection_phy_update(self, phy):
|
|
1262
1590
|
pass
|
|
1263
1591
|
|
|
1264
1592
|
def on_connection_phy_update_failure(self, error):
|
|
@@ -1284,7 +1612,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1284
1612
|
peer_resolvable_address,
|
|
1285
1613
|
role,
|
|
1286
1614
|
parameters,
|
|
1287
|
-
phy,
|
|
1288
1615
|
):
|
|
1289
1616
|
super().__init__()
|
|
1290
1617
|
self.device = device
|
|
@@ -1301,7 +1628,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1301
1628
|
self.authenticated = False
|
|
1302
1629
|
self.sc = False
|
|
1303
1630
|
self.link_key_type = None
|
|
1304
|
-
self.phy = phy
|
|
1305
1631
|
self.att_mtu = ATT_DEFAULT_MTU
|
|
1306
1632
|
self.data_length = DEVICE_DEFAULT_DATA_LENGTH
|
|
1307
1633
|
self.gatt_client = None # Per-connection client
|
|
@@ -1311,6 +1637,8 @@ class Connection(CompositeEventEmitter):
|
|
|
1311
1637
|
self.pairing_peer_io_capability = None
|
|
1312
1638
|
self.pairing_peer_authentication_requirements = None
|
|
1313
1639
|
self.peer_le_features = None
|
|
1640
|
+
self.cs_configs = {}
|
|
1641
|
+
self.cs_procedures = {}
|
|
1314
1642
|
|
|
1315
1643
|
# [Classic only]
|
|
1316
1644
|
@classmethod
|
|
@@ -1330,7 +1658,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1330
1658
|
None,
|
|
1331
1659
|
role,
|
|
1332
1660
|
None,
|
|
1333
|
-
None,
|
|
1334
1661
|
)
|
|
1335
1662
|
|
|
1336
1663
|
# [Classic only]
|
|
@@ -1446,12 +1773,12 @@ class Connection(CompositeEventEmitter):
|
|
|
1446
1773
|
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
|
|
1447
1774
|
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
|
|
1448
1775
|
|
|
1776
|
+
async def get_phy(self) -> ConnectionPHY:
|
|
1777
|
+
return await self.device.get_connection_phy(self)
|
|
1778
|
+
|
|
1449
1779
|
async def get_rssi(self):
|
|
1450
1780
|
return await self.device.get_connection_rssi(self)
|
|
1451
1781
|
|
|
1452
|
-
async def get_phy(self):
|
|
1453
|
-
return await self.device.get_connection_phy(self)
|
|
1454
|
-
|
|
1455
1782
|
async def transfer_periodic_sync(
|
|
1456
1783
|
self, sync_handle: int, service_data: int = 0
|
|
1457
1784
|
) -> None:
|
|
@@ -1470,6 +1797,10 @@ class Connection(CompositeEventEmitter):
|
|
|
1470
1797
|
self.peer_le_features = await self.device.get_remote_le_features(self)
|
|
1471
1798
|
return self.peer_le_features
|
|
1472
1799
|
|
|
1800
|
+
@property
|
|
1801
|
+
def data_packet_queue(self) -> DataPacketQueue | None:
|
|
1802
|
+
return self.device.host.get_data_packet_queue(self.handle)
|
|
1803
|
+
|
|
1473
1804
|
async def __aenter__(self):
|
|
1474
1805
|
return self
|
|
1475
1806
|
|
|
@@ -1533,11 +1864,14 @@ class DeviceConfiguration:
|
|
|
1533
1864
|
address_resolution_offload: bool = False
|
|
1534
1865
|
address_generation_offload: bool = False
|
|
1535
1866
|
cis_enabled: bool = False
|
|
1867
|
+
channel_sounding_enabled: bool = False
|
|
1536
1868
|
identity_address_type: Optional[int] = None
|
|
1537
1869
|
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
|
1870
|
+
gap_service_enabled: bool = True
|
|
1871
|
+
gatt_service_enabled: bool = True
|
|
1538
1872
|
|
|
1539
1873
|
def __post_init__(self) -> None:
|
|
1540
|
-
self.gatt_services:
|
|
1874
|
+
self.gatt_services: list[Dict[str, Any]] = []
|
|
1541
1875
|
|
|
1542
1876
|
def load_from_dict(self, config: Dict[str, Any]) -> None:
|
|
1543
1877
|
config = copy.deepcopy(config)
|
|
@@ -1684,7 +2018,7 @@ def host_event_handler(function):
|
|
|
1684
2018
|
# List of host event handlers for the Device class.
|
|
1685
2019
|
# (we define this list outside the class, because referencing a class in method
|
|
1686
2020
|
# decorators is not straightforward)
|
|
1687
|
-
device_host_event_handlers:
|
|
2021
|
+
device_host_event_handlers: list[str] = []
|
|
1688
2022
|
|
|
1689
2023
|
|
|
1690
2024
|
# -----------------------------------------------------------------------------
|
|
@@ -1701,19 +2035,24 @@ class Device(CompositeEventEmitter):
|
|
|
1701
2035
|
gatt_server: gatt_server.Server
|
|
1702
2036
|
advertising_data: bytes
|
|
1703
2037
|
scan_response_data: bytes
|
|
2038
|
+
cs_capabilities: ChannelSoundingCapabilities | None = None
|
|
1704
2039
|
connections: Dict[int, Connection]
|
|
1705
2040
|
pending_connections: Dict[hci.Address, Connection]
|
|
1706
2041
|
classic_pending_accepts: Dict[
|
|
1707
2042
|
hci.Address,
|
|
1708
|
-
|
|
2043
|
+
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
|
|
1709
2044
|
]
|
|
1710
2045
|
advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
|
|
1711
|
-
periodic_advertising_syncs:
|
|
2046
|
+
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
|
|
1712
2047
|
config: DeviceConfiguration
|
|
1713
2048
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
1714
2049
|
sco_links: Dict[int, ScoLink]
|
|
1715
2050
|
cis_links: Dict[int, CisLink]
|
|
1716
|
-
|
|
2051
|
+
bigs: dict[int, Big]
|
|
2052
|
+
bis_links: dict[int, BisLink]
|
|
2053
|
+
big_syncs: dict[int, BigSync]
|
|
2054
|
+
_pending_cis: Dict[int, tuple[int, int]]
|
|
2055
|
+
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
1717
2056
|
|
|
1718
2057
|
@composite_listener
|
|
1719
2058
|
class Listener:
|
|
@@ -1780,7 +2119,6 @@ class Device(CompositeEventEmitter):
|
|
|
1780
2119
|
address: Optional[hci.Address] = None,
|
|
1781
2120
|
config: Optional[DeviceConfiguration] = None,
|
|
1782
2121
|
host: Optional[Host] = None,
|
|
1783
|
-
generic_access_service: bool = True,
|
|
1784
2122
|
) -> None:
|
|
1785
2123
|
super().__init__()
|
|
1786
2124
|
|
|
@@ -1805,6 +2143,9 @@ class Device(CompositeEventEmitter):
|
|
|
1805
2143
|
self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
|
|
1806
2144
|
self.cis_links = {} # CisLinks, by connection handle (LE only)
|
|
1807
2145
|
self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
|
|
2146
|
+
self.bigs = {}
|
|
2147
|
+
self.bis_links = {}
|
|
2148
|
+
self.big_syncs = {}
|
|
1808
2149
|
self.classic_enabled = False
|
|
1809
2150
|
self.inquiry_response = None
|
|
1810
2151
|
self.address_resolver = None
|
|
@@ -1882,7 +2223,7 @@ class Device(CompositeEventEmitter):
|
|
|
1882
2223
|
permissions=descriptor["permissions"],
|
|
1883
2224
|
)
|
|
1884
2225
|
descriptors.append(new_descriptor)
|
|
1885
|
-
new_characteristic = Characteristic(
|
|
2226
|
+
new_characteristic: Characteristic[bytes] = Characteristic(
|
|
1886
2227
|
uuid=characteristic["uuid"],
|
|
1887
2228
|
properties=Characteristic.Properties.from_string(
|
|
1888
2229
|
characteristic["properties"]
|
|
@@ -1927,7 +2268,10 @@ class Device(CompositeEventEmitter):
|
|
|
1927
2268
|
# Register the SDP server with the L2CAP Channel Manager
|
|
1928
2269
|
self.sdp_server.register(self.l2cap_channel_manager)
|
|
1929
2270
|
|
|
1930
|
-
self.add_default_services(
|
|
2271
|
+
self.add_default_services(
|
|
2272
|
+
add_gap_service=config.gap_service_enabled,
|
|
2273
|
+
add_gatt_service=config.gatt_service_enabled,
|
|
2274
|
+
)
|
|
1931
2275
|
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
|
1932
2276
|
|
|
1933
2277
|
# Forward some events
|
|
@@ -2009,6 +2353,17 @@ class Device(CompositeEventEmitter):
|
|
|
2009
2353
|
None,
|
|
2010
2354
|
)
|
|
2011
2355
|
|
|
2356
|
+
def next_big_handle(self) -> int | None:
|
|
2357
|
+
return next(
|
|
2358
|
+
(
|
|
2359
|
+
handle
|
|
2360
|
+
for handle in range(DEVICE_MIN_BIG_HANDLE, DEVICE_MAX_BIG_HANDLE + 1)
|
|
2361
|
+
if handle
|
|
2362
|
+
not in itertools.chain(self.bigs.keys(), self.big_syncs.keys())
|
|
2363
|
+
),
|
|
2364
|
+
None,
|
|
2365
|
+
)
|
|
2366
|
+
|
|
2012
2367
|
@deprecated("Please use create_l2cap_server()")
|
|
2013
2368
|
def register_l2cap_server(self, psm, server) -> int:
|
|
2014
2369
|
return self.l2cap_channel_manager.register_server(psm, server)
|
|
@@ -2167,7 +2522,7 @@ class Device(CompositeEventEmitter):
|
|
|
2167
2522
|
if self.random_address != hci.Address.ANY_RANDOM:
|
|
2168
2523
|
logger.debug(
|
|
2169
2524
|
color(
|
|
2170
|
-
f'LE Random
|
|
2525
|
+
f'LE Random Address: {self.random_address}',
|
|
2171
2526
|
'yellow',
|
|
2172
2527
|
)
|
|
2173
2528
|
)
|
|
@@ -2200,6 +2555,41 @@ class Device(CompositeEventEmitter):
|
|
|
2200
2555
|
check_result=True,
|
|
2201
2556
|
)
|
|
2202
2557
|
|
|
2558
|
+
if self.config.channel_sounding_enabled:
|
|
2559
|
+
await self.send_command(
|
|
2560
|
+
hci.HCI_LE_Set_Host_Feature_Command(
|
|
2561
|
+
bit_number=hci.LeFeature.CHANNEL_SOUNDING_HOST_SUPPORT,
|
|
2562
|
+
bit_value=1,
|
|
2563
|
+
),
|
|
2564
|
+
check_result=True,
|
|
2565
|
+
)
|
|
2566
|
+
result = await self.send_command(
|
|
2567
|
+
hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command(),
|
|
2568
|
+
check_result=True,
|
|
2569
|
+
)
|
|
2570
|
+
self.cs_capabilities = ChannelSoundingCapabilities(
|
|
2571
|
+
num_config_supported=result.return_parameters.num_config_supported,
|
|
2572
|
+
max_consecutive_procedures_supported=result.return_parameters.max_consecutive_procedures_supported,
|
|
2573
|
+
num_antennas_supported=result.return_parameters.num_antennas_supported,
|
|
2574
|
+
max_antenna_paths_supported=result.return_parameters.max_antenna_paths_supported,
|
|
2575
|
+
roles_supported=result.return_parameters.roles_supported,
|
|
2576
|
+
modes_supported=result.return_parameters.modes_supported,
|
|
2577
|
+
rtt_capability=result.return_parameters.rtt_capability,
|
|
2578
|
+
rtt_aa_only_n=result.return_parameters.rtt_aa_only_n,
|
|
2579
|
+
rtt_sounding_n=result.return_parameters.rtt_sounding_n,
|
|
2580
|
+
rtt_random_payload_n=result.return_parameters.rtt_random_payload_n,
|
|
2581
|
+
nadm_sounding_capability=result.return_parameters.nadm_sounding_capability,
|
|
2582
|
+
nadm_random_capability=result.return_parameters.nadm_random_capability,
|
|
2583
|
+
cs_sync_phys_supported=result.return_parameters.cs_sync_phys_supported,
|
|
2584
|
+
subfeatures_supported=result.return_parameters.subfeatures_supported,
|
|
2585
|
+
t_ip1_times_supported=result.return_parameters.t_ip1_times_supported,
|
|
2586
|
+
t_ip2_times_supported=result.return_parameters.t_ip2_times_supported,
|
|
2587
|
+
t_fcs_times_supported=result.return_parameters.t_fcs_times_supported,
|
|
2588
|
+
t_pm_times_supported=result.return_parameters.t_pm_times_supported,
|
|
2589
|
+
t_sw_time_supported=result.return_parameters.t_sw_time_supported,
|
|
2590
|
+
tx_snr_capability=result.return_parameters.tx_snr_capability,
|
|
2591
|
+
)
|
|
2592
|
+
|
|
2203
2593
|
if self.classic_enabled:
|
|
2204
2594
|
await self.send_command(
|
|
2205
2595
|
hci.HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
|
|
@@ -2283,7 +2673,7 @@ class Device(CompositeEventEmitter):
|
|
|
2283
2673
|
"""Update the RPA periodically"""
|
|
2284
2674
|
while self.le_rpa_timeout != 0:
|
|
2285
2675
|
await asyncio.sleep(self.le_rpa_timeout)
|
|
2286
|
-
if not self.update_rpa():
|
|
2676
|
+
if not await self.update_rpa():
|
|
2287
2677
|
logger.debug("periodic RPA update failed")
|
|
2288
2678
|
|
|
2289
2679
|
async def refresh_resolving_list(self) -> None:
|
|
@@ -2623,11 +3013,11 @@ class Device(CompositeEventEmitter):
|
|
|
2623
3013
|
self,
|
|
2624
3014
|
legacy: bool = False,
|
|
2625
3015
|
active: bool = True,
|
|
2626
|
-
scan_interval:
|
|
2627
|
-
scan_window:
|
|
3016
|
+
scan_interval: float = DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
|
|
3017
|
+
scan_window: float = DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
|
|
2628
3018
|
own_address_type: int = hci.OwnAddressType.RANDOM,
|
|
2629
3019
|
filter_duplicates: bool = False,
|
|
2630
|
-
scanning_phys:
|
|
3020
|
+
scanning_phys: Sequence[int] = (hci.HCI_LE_1M_PHY, hci.HCI_LE_CODED_PHY),
|
|
2631
3021
|
) -> None:
|
|
2632
3022
|
# Check that the arguments are legal
|
|
2633
3023
|
if scan_interval < scan_window:
|
|
@@ -2674,7 +3064,7 @@ class Device(CompositeEventEmitter):
|
|
|
2674
3064
|
scanning_filter_policy=scanning_filter_policy,
|
|
2675
3065
|
scanning_phys=scanning_phys_bits,
|
|
2676
3066
|
scan_types=[scan_type] * scanning_phy_count,
|
|
2677
|
-
scan_intervals=[int(
|
|
3067
|
+
scan_intervals=[int(scan_interval / 0.625)] * scanning_phy_count,
|
|
2678
3068
|
scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
|
|
2679
3069
|
),
|
|
2680
3070
|
check_result=True,
|
|
@@ -2840,6 +3230,41 @@ class Device(CompositeEventEmitter):
|
|
|
2840
3230
|
"periodic advertising sync establishment for unknown address/sid"
|
|
2841
3231
|
)
|
|
2842
3232
|
|
|
3233
|
+
@host_event_handler
|
|
3234
|
+
def on_periodic_advertising_sync_transfer(
|
|
3235
|
+
self,
|
|
3236
|
+
status: int,
|
|
3237
|
+
connection_handle: int,
|
|
3238
|
+
sync_handle: int,
|
|
3239
|
+
advertising_sid: int,
|
|
3240
|
+
advertiser_address: hci.Address,
|
|
3241
|
+
advertiser_phy: int,
|
|
3242
|
+
periodic_advertising_interval: int,
|
|
3243
|
+
advertiser_clock_accuracy: int,
|
|
3244
|
+
) -> None:
|
|
3245
|
+
if not (connection := self.lookup_connection(connection_handle)):
|
|
3246
|
+
logger.error(
|
|
3247
|
+
"Receive PAST from unknown connection 0x%04X", connection_handle
|
|
3248
|
+
)
|
|
3249
|
+
|
|
3250
|
+
pa_sync = PeriodicAdvertisingSync(
|
|
3251
|
+
device=self,
|
|
3252
|
+
advertiser_address=advertiser_address,
|
|
3253
|
+
sid=advertising_sid,
|
|
3254
|
+
skip=0,
|
|
3255
|
+
sync_timeout=0.0,
|
|
3256
|
+
filter_duplicates=False,
|
|
3257
|
+
)
|
|
3258
|
+
self.periodic_advertising_syncs.append(pa_sync)
|
|
3259
|
+
pa_sync.on_establishment(
|
|
3260
|
+
status=status,
|
|
3261
|
+
sync_handle=sync_handle,
|
|
3262
|
+
advertiser_phy=advertiser_phy,
|
|
3263
|
+
periodic_advertising_interval=periodic_advertising_interval,
|
|
3264
|
+
advertiser_clock_accuracy=advertiser_clock_accuracy,
|
|
3265
|
+
)
|
|
3266
|
+
self.emit('periodic_advertising_sync_transfer', pa_sync, connection)
|
|
3267
|
+
|
|
2843
3268
|
@host_event_handler
|
|
2844
3269
|
@with_periodic_advertising_sync_from_handle
|
|
2845
3270
|
def on_periodic_advertising_sync_loss(
|
|
@@ -3514,12 +3939,14 @@ class Device(CompositeEventEmitter):
|
|
|
3514
3939
|
)
|
|
3515
3940
|
return result.return_parameters.rssi
|
|
3516
3941
|
|
|
3517
|
-
async def get_connection_phy(self, connection):
|
|
3942
|
+
async def get_connection_phy(self, connection: Connection) -> ConnectionPHY:
|
|
3518
3943
|
result = await self.send_command(
|
|
3519
3944
|
hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
|
|
3520
3945
|
check_result=True,
|
|
3521
3946
|
)
|
|
3522
|
-
return (
|
|
3947
|
+
return ConnectionPHY(
|
|
3948
|
+
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
|
3949
|
+
)
|
|
3523
3950
|
|
|
3524
3951
|
async def set_connection_phy(
|
|
3525
3952
|
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
|
@@ -3583,13 +4010,12 @@ class Device(CompositeEventEmitter):
|
|
|
3583
4010
|
# Create a future to wait for an address to be found
|
|
3584
4011
|
peer_address = asyncio.get_running_loop().create_future()
|
|
3585
4012
|
|
|
3586
|
-
def on_peer_found(address, ad_data):
|
|
3587
|
-
local_name = ad_data.get(
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
if local_name
|
|
3591
|
-
|
|
3592
|
-
peer_address.set_result(address)
|
|
4013
|
+
def on_peer_found(address: hci.Address, ad_data: AdvertisingData) -> None:
|
|
4014
|
+
local_name = ad_data.get(
|
|
4015
|
+
AdvertisingData.Type.COMPLETE_LOCAL_NAME
|
|
4016
|
+
) or ad_data.get(AdvertisingData.Type.SHORTENED_LOCAL_NAME)
|
|
4017
|
+
if local_name == name:
|
|
4018
|
+
peer_address.set_result(address)
|
|
3593
4019
|
|
|
3594
4020
|
listener = None
|
|
3595
4021
|
was_scanning = self.scanning
|
|
@@ -3958,13 +4384,13 @@ class Device(CompositeEventEmitter):
|
|
|
3958
4384
|
async def setup_cig(
|
|
3959
4385
|
self,
|
|
3960
4386
|
cig_id: int,
|
|
3961
|
-
cis_id:
|
|
3962
|
-
sdu_interval:
|
|
4387
|
+
cis_id: Sequence[int],
|
|
4388
|
+
sdu_interval: tuple[int, int],
|
|
3963
4389
|
framing: int,
|
|
3964
|
-
max_sdu:
|
|
4390
|
+
max_sdu: tuple[int, int],
|
|
3965
4391
|
retransmission_number: int,
|
|
3966
|
-
max_transport_latency:
|
|
3967
|
-
) ->
|
|
4392
|
+
max_transport_latency: tuple[int, int],
|
|
4393
|
+
) -> list[int]:
|
|
3968
4394
|
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
|
3969
4395
|
|
|
3970
4396
|
Args:
|
|
@@ -4013,7 +4439,9 @@ class Device(CompositeEventEmitter):
|
|
|
4013
4439
|
|
|
4014
4440
|
# [LE only]
|
|
4015
4441
|
@experimental('Only for testing.')
|
|
4016
|
-
async def create_cis(
|
|
4442
|
+
async def create_cis(
|
|
4443
|
+
self, cis_acl_pairs: Sequence[tuple[int, int]]
|
|
4444
|
+
) -> list[CisLink]:
|
|
4017
4445
|
for cis_handle, acl_handle in cis_acl_pairs:
|
|
4018
4446
|
acl_connection = self.lookup_connection(acl_handle)
|
|
4019
4447
|
assert acl_connection
|
|
@@ -4112,6 +4540,106 @@ class Device(CompositeEventEmitter):
|
|
|
4112
4540
|
check_result=True,
|
|
4113
4541
|
)
|
|
4114
4542
|
|
|
4543
|
+
# [LE only]
|
|
4544
|
+
@experimental('Only for testing.')
|
|
4545
|
+
async def create_big(
|
|
4546
|
+
self, advertising_set: AdvertisingSet, parameters: BigParameters
|
|
4547
|
+
) -> Big:
|
|
4548
|
+
if (big_handle := self.next_big_handle()) is None:
|
|
4549
|
+
raise core.OutOfResourcesError("All valid BIG handles already in use")
|
|
4550
|
+
|
|
4551
|
+
with closing(EventWatcher()) as watcher:
|
|
4552
|
+
big = Big(
|
|
4553
|
+
big_handle=big_handle,
|
|
4554
|
+
parameters=parameters,
|
|
4555
|
+
advertising_set=advertising_set,
|
|
4556
|
+
)
|
|
4557
|
+
self.bigs[big_handle] = big
|
|
4558
|
+
established = asyncio.get_running_loop().create_future()
|
|
4559
|
+
watcher.once(
|
|
4560
|
+
big, big.Event.ESTABLISHMENT, lambda: established.set_result(None)
|
|
4561
|
+
)
|
|
4562
|
+
watcher.once(
|
|
4563
|
+
big,
|
|
4564
|
+
big.Event.ESTABLISHMENT_FAILURE,
|
|
4565
|
+
lambda status: established.set_exception(hci.HCI_Error(status)),
|
|
4566
|
+
)
|
|
4567
|
+
|
|
4568
|
+
try:
|
|
4569
|
+
await self.send_command(
|
|
4570
|
+
hci.HCI_LE_Create_BIG_Command(
|
|
4571
|
+
big_handle=big_handle,
|
|
4572
|
+
advertising_handle=advertising_set.advertising_handle,
|
|
4573
|
+
num_bis=parameters.num_bis,
|
|
4574
|
+
sdu_interval=parameters.sdu_interval,
|
|
4575
|
+
max_sdu=parameters.max_sdu,
|
|
4576
|
+
max_transport_latency=parameters.max_transport_latency,
|
|
4577
|
+
rtn=parameters.rtn,
|
|
4578
|
+
phy=parameters.phy,
|
|
4579
|
+
packing=parameters.packing,
|
|
4580
|
+
framing=parameters.framing,
|
|
4581
|
+
encryption=1 if parameters.broadcast_code else 0,
|
|
4582
|
+
broadcast_code=parameters.broadcast_code or bytes(16),
|
|
4583
|
+
),
|
|
4584
|
+
check_result=True,
|
|
4585
|
+
)
|
|
4586
|
+
await established
|
|
4587
|
+
except hci.HCI_Error:
|
|
4588
|
+
del self.bigs[big_handle]
|
|
4589
|
+
raise
|
|
4590
|
+
|
|
4591
|
+
return big
|
|
4592
|
+
|
|
4593
|
+
# [LE only]
|
|
4594
|
+
@experimental('Only for testing.')
|
|
4595
|
+
async def create_big_sync(
|
|
4596
|
+
self, pa_sync: PeriodicAdvertisingSync, parameters: BigSyncParameters
|
|
4597
|
+
) -> BigSync:
|
|
4598
|
+
if (big_handle := self.next_big_handle()) is None:
|
|
4599
|
+
raise core.OutOfResourcesError("All valid BIG handles already in use")
|
|
4600
|
+
|
|
4601
|
+
if (pa_sync_handle := pa_sync.sync_handle) is None:
|
|
4602
|
+
raise core.InvalidStateError("PA Sync is not established")
|
|
4603
|
+
|
|
4604
|
+
with closing(EventWatcher()) as watcher:
|
|
4605
|
+
big_sync = BigSync(
|
|
4606
|
+
big_handle=big_handle,
|
|
4607
|
+
parameters=parameters,
|
|
4608
|
+
pa_sync=pa_sync,
|
|
4609
|
+
)
|
|
4610
|
+
self.big_syncs[big_handle] = big_sync
|
|
4611
|
+
established = asyncio.get_running_loop().create_future()
|
|
4612
|
+
watcher.once(
|
|
4613
|
+
big_sync,
|
|
4614
|
+
big_sync.Event.ESTABLISHMENT,
|
|
4615
|
+
lambda: established.set_result(None),
|
|
4616
|
+
)
|
|
4617
|
+
watcher.once(
|
|
4618
|
+
big_sync,
|
|
4619
|
+
big_sync.Event.ESTABLISHMENT_FAILURE,
|
|
4620
|
+
lambda status: established.set_exception(hci.HCI_Error(status)),
|
|
4621
|
+
)
|
|
4622
|
+
|
|
4623
|
+
try:
|
|
4624
|
+
await self.send_command(
|
|
4625
|
+
hci.HCI_LE_BIG_Create_Sync_Command(
|
|
4626
|
+
big_handle=big_handle,
|
|
4627
|
+
sync_handle=pa_sync_handle,
|
|
4628
|
+
encryption=1 if parameters.broadcast_code else 0,
|
|
4629
|
+
broadcast_code=parameters.broadcast_code or bytes(16),
|
|
4630
|
+
mse=parameters.mse,
|
|
4631
|
+
big_sync_timeout=parameters.big_sync_timeout,
|
|
4632
|
+
bis=parameters.bis,
|
|
4633
|
+
),
|
|
4634
|
+
check_result=True,
|
|
4635
|
+
)
|
|
4636
|
+
await established
|
|
4637
|
+
except hci.HCI_Error:
|
|
4638
|
+
del self.big_syncs[big_handle]
|
|
4639
|
+
raise
|
|
4640
|
+
|
|
4641
|
+
return big_sync
|
|
4642
|
+
|
|
4115
4643
|
async def get_remote_le_features(self, connection: Connection) -> hci.LeFeatureMask:
|
|
4116
4644
|
"""[LE Only] Reads remote LE supported features.
|
|
4117
4645
|
|
|
@@ -4144,6 +4672,213 @@ class Device(CompositeEventEmitter):
|
|
|
4144
4672
|
)
|
|
4145
4673
|
return await read_feature_future
|
|
4146
4674
|
|
|
4675
|
+
@experimental('Only for testing.')
|
|
4676
|
+
async def get_remote_cs_capabilities(
|
|
4677
|
+
self, connection: Connection
|
|
4678
|
+
) -> ChannelSoundingCapabilities:
|
|
4679
|
+
complete_future: asyncio.Future[ChannelSoundingCapabilities] = (
|
|
4680
|
+
asyncio.get_running_loop().create_future()
|
|
4681
|
+
)
|
|
4682
|
+
|
|
4683
|
+
with closing(EventWatcher()) as watcher:
|
|
4684
|
+
watcher.once(
|
|
4685
|
+
connection, 'channel_sounding_capabilities', complete_future.set_result
|
|
4686
|
+
)
|
|
4687
|
+
watcher.once(
|
|
4688
|
+
connection,
|
|
4689
|
+
'channel_sounding_capabilities_failure',
|
|
4690
|
+
lambda status: complete_future.set_exception(hci.HCI_Error(status)),
|
|
4691
|
+
)
|
|
4692
|
+
await self.send_command(
|
|
4693
|
+
hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Command(
|
|
4694
|
+
connection_handle=connection.handle
|
|
4695
|
+
),
|
|
4696
|
+
check_result=True,
|
|
4697
|
+
)
|
|
4698
|
+
return await complete_future
|
|
4699
|
+
|
|
4700
|
+
@experimental('Only for testing.')
|
|
4701
|
+
async def set_default_cs_settings(
|
|
4702
|
+
self,
|
|
4703
|
+
connection: Connection,
|
|
4704
|
+
role_enable: int = (
|
|
4705
|
+
hci.CsRoleMask.INITIATOR | hci.CsRoleMask.REFLECTOR
|
|
4706
|
+
), # Both role
|
|
4707
|
+
cs_sync_antenna_selection: int = 0xFF, # No Preference
|
|
4708
|
+
max_tx_power: int = 0x04, # 4 dB
|
|
4709
|
+
) -> None:
|
|
4710
|
+
await self.send_command(
|
|
4711
|
+
hci.HCI_LE_CS_Set_Default_Settings_Command(
|
|
4712
|
+
connection_handle=connection.handle,
|
|
4713
|
+
role_enable=role_enable,
|
|
4714
|
+
cs_sync_antenna_selection=cs_sync_antenna_selection,
|
|
4715
|
+
max_tx_power=max_tx_power,
|
|
4716
|
+
),
|
|
4717
|
+
check_result=True,
|
|
4718
|
+
)
|
|
4719
|
+
|
|
4720
|
+
@experimental('Only for testing.')
|
|
4721
|
+
async def create_cs_config(
|
|
4722
|
+
self,
|
|
4723
|
+
connection: Connection,
|
|
4724
|
+
config_id: int | None = None,
|
|
4725
|
+
create_context: int = 0x01,
|
|
4726
|
+
main_mode_type: int = 0x02,
|
|
4727
|
+
sub_mode_type: int = 0xFF,
|
|
4728
|
+
min_main_mode_steps: int = 0x02,
|
|
4729
|
+
max_main_mode_steps: int = 0x05,
|
|
4730
|
+
main_mode_repetition: int = 0x00,
|
|
4731
|
+
mode_0_steps: int = 0x03,
|
|
4732
|
+
role: int = hci.CsRole.INITIATOR,
|
|
4733
|
+
rtt_type: int = hci.RttType.AA_ONLY,
|
|
4734
|
+
cs_sync_phy: int = hci.CsSyncPhy.LE_1M,
|
|
4735
|
+
channel_map: bytes = b'\x54\x55\x55\x54\x55\x55\x55\x55\x55\x15',
|
|
4736
|
+
channel_map_repetition: int = 0x01,
|
|
4737
|
+
channel_selection_type: int = hci.HCI_LE_CS_Create_Config_Command.ChannelSelectionType.ALGO_3B,
|
|
4738
|
+
ch3c_shape: int = hci.HCI_LE_CS_Create_Config_Command.Ch3cShape.HAT,
|
|
4739
|
+
ch3c_jump: int = 0x03,
|
|
4740
|
+
) -> ChannelSoundingConfig:
|
|
4741
|
+
complete_future: asyncio.Future[ChannelSoundingConfig] = (
|
|
4742
|
+
asyncio.get_running_loop().create_future()
|
|
4743
|
+
)
|
|
4744
|
+
if config_id is None:
|
|
4745
|
+
# Allocate an ID.
|
|
4746
|
+
config_id = next(
|
|
4747
|
+
(
|
|
4748
|
+
i
|
|
4749
|
+
for i in range(DEVICE_MIN_CS_CONFIG_ID, DEVICE_MAX_CS_CONFIG_ID + 1)
|
|
4750
|
+
if i not in connection.cs_configs
|
|
4751
|
+
),
|
|
4752
|
+
None,
|
|
4753
|
+
)
|
|
4754
|
+
if config_id is None:
|
|
4755
|
+
raise OutOfResourcesError("No available config ID on this connection!")
|
|
4756
|
+
|
|
4757
|
+
with closing(EventWatcher()) as watcher:
|
|
4758
|
+
watcher.once(
|
|
4759
|
+
connection, 'channel_sounding_config', complete_future.set_result
|
|
4760
|
+
)
|
|
4761
|
+
watcher.once(
|
|
4762
|
+
connection,
|
|
4763
|
+
'channel_sounding_config_failure',
|
|
4764
|
+
lambda status: complete_future.set_exception(hci.HCI_Error(status)),
|
|
4765
|
+
)
|
|
4766
|
+
await self.send_command(
|
|
4767
|
+
hci.HCI_LE_CS_Create_Config_Command(
|
|
4768
|
+
connection_handle=connection.handle,
|
|
4769
|
+
config_id=config_id,
|
|
4770
|
+
create_context=create_context,
|
|
4771
|
+
main_mode_type=main_mode_type,
|
|
4772
|
+
sub_mode_type=sub_mode_type,
|
|
4773
|
+
min_main_mode_steps=min_main_mode_steps,
|
|
4774
|
+
max_main_mode_steps=max_main_mode_steps,
|
|
4775
|
+
main_mode_repetition=main_mode_repetition,
|
|
4776
|
+
mode_0_steps=mode_0_steps,
|
|
4777
|
+
role=role,
|
|
4778
|
+
rtt_type=rtt_type,
|
|
4779
|
+
cs_sync_phy=cs_sync_phy,
|
|
4780
|
+
channel_map=channel_map,
|
|
4781
|
+
channel_map_repetition=channel_map_repetition,
|
|
4782
|
+
channel_selection_type=channel_selection_type,
|
|
4783
|
+
ch3c_shape=ch3c_shape,
|
|
4784
|
+
ch3c_jump=ch3c_jump,
|
|
4785
|
+
reserved=0x00,
|
|
4786
|
+
),
|
|
4787
|
+
check_result=True,
|
|
4788
|
+
)
|
|
4789
|
+
return await complete_future
|
|
4790
|
+
|
|
4791
|
+
@experimental('Only for testing.')
|
|
4792
|
+
async def enable_cs_security(self, connection: Connection) -> None:
|
|
4793
|
+
complete_future: asyncio.Future[None] = (
|
|
4794
|
+
asyncio.get_running_loop().create_future()
|
|
4795
|
+
)
|
|
4796
|
+
with closing(EventWatcher()) as watcher:
|
|
4797
|
+
|
|
4798
|
+
def on_event(event: hci.HCI_LE_CS_Security_Enable_Complete_Event) -> None:
|
|
4799
|
+
if event.connection_handle != connection.handle:
|
|
4800
|
+
return
|
|
4801
|
+
if event.status == hci.HCI_SUCCESS:
|
|
4802
|
+
complete_future.set_result(None)
|
|
4803
|
+
else:
|
|
4804
|
+
complete_future.set_exception(hci.HCI_Error(event.status))
|
|
4805
|
+
|
|
4806
|
+
watcher.once(self.host, 'cs_security', on_event)
|
|
4807
|
+
await self.send_command(
|
|
4808
|
+
hci.HCI_LE_CS_Security_Enable_Command(
|
|
4809
|
+
connection_handle=connection.handle
|
|
4810
|
+
),
|
|
4811
|
+
check_result=True,
|
|
4812
|
+
)
|
|
4813
|
+
return await complete_future
|
|
4814
|
+
|
|
4815
|
+
@experimental('Only for testing.')
|
|
4816
|
+
async def set_cs_procedure_parameters(
|
|
4817
|
+
self,
|
|
4818
|
+
connection: Connection,
|
|
4819
|
+
config: ChannelSoundingConfig,
|
|
4820
|
+
tone_antenna_config_selection=0x00,
|
|
4821
|
+
preferred_peer_antenna=0x00,
|
|
4822
|
+
max_procedure_len=0x2710, # 6.25s
|
|
4823
|
+
min_procedure_interval=0x01,
|
|
4824
|
+
max_procedure_interval=0xFF,
|
|
4825
|
+
max_procedure_count=0x01,
|
|
4826
|
+
min_subevent_len=0x0004E2, # 1250us
|
|
4827
|
+
max_subevent_len=0x1E8480, # 2s
|
|
4828
|
+
phy=hci.CsSyncPhy.LE_1M,
|
|
4829
|
+
tx_power_delta=0x00,
|
|
4830
|
+
snr_control_initiator=hci.CsSnr.NOT_APPLIED,
|
|
4831
|
+
snr_control_reflector=hci.CsSnr.NOT_APPLIED,
|
|
4832
|
+
) -> None:
|
|
4833
|
+
await self.send_command(
|
|
4834
|
+
hci.HCI_LE_CS_Set_Procedure_Parameters_Command(
|
|
4835
|
+
connection_handle=connection.handle,
|
|
4836
|
+
config_id=config.config_id,
|
|
4837
|
+
max_procedure_len=max_procedure_len,
|
|
4838
|
+
min_procedure_interval=min_procedure_interval,
|
|
4839
|
+
max_procedure_interval=max_procedure_interval,
|
|
4840
|
+
max_procedure_count=max_procedure_count,
|
|
4841
|
+
min_subevent_len=min_subevent_len,
|
|
4842
|
+
max_subevent_len=max_subevent_len,
|
|
4843
|
+
tone_antenna_config_selection=tone_antenna_config_selection,
|
|
4844
|
+
phy=phy,
|
|
4845
|
+
tx_power_delta=tx_power_delta,
|
|
4846
|
+
preferred_peer_antenna=preferred_peer_antenna,
|
|
4847
|
+
snr_control_initiator=snr_control_initiator,
|
|
4848
|
+
snr_control_reflector=snr_control_reflector,
|
|
4849
|
+
),
|
|
4850
|
+
check_result=True,
|
|
4851
|
+
)
|
|
4852
|
+
|
|
4853
|
+
@experimental('Only for testing.')
|
|
4854
|
+
async def enable_cs_procedure(
|
|
4855
|
+
self,
|
|
4856
|
+
connection: Connection,
|
|
4857
|
+
config: ChannelSoundingConfig,
|
|
4858
|
+
enabled: bool = True,
|
|
4859
|
+
) -> ChannelSoundingProcedure:
|
|
4860
|
+
complete_future: asyncio.Future[ChannelSoundingProcedure] = (
|
|
4861
|
+
asyncio.get_running_loop().create_future()
|
|
4862
|
+
)
|
|
4863
|
+
with closing(EventWatcher()) as watcher:
|
|
4864
|
+
watcher.once(
|
|
4865
|
+
connection, 'channel_sounding_procedure', complete_future.set_result
|
|
4866
|
+
)
|
|
4867
|
+
watcher.once(
|
|
4868
|
+
connection,
|
|
4869
|
+
'channel_sounding_procedure_failure',
|
|
4870
|
+
lambda x: complete_future.set_exception(hci.HCI_Error(x)),
|
|
4871
|
+
)
|
|
4872
|
+
await self.send_command(
|
|
4873
|
+
hci.HCI_LE_CS_Procedure_Enable_Command(
|
|
4874
|
+
connection_handle=connection.handle,
|
|
4875
|
+
config_id=config.config_id,
|
|
4876
|
+
enable=enabled,
|
|
4877
|
+
),
|
|
4878
|
+
check_result=True,
|
|
4879
|
+
)
|
|
4880
|
+
return await complete_future
|
|
4881
|
+
|
|
4147
4882
|
@host_event_handler
|
|
4148
4883
|
def on_flush(self):
|
|
4149
4884
|
self.emit('flush')
|
|
@@ -4178,21 +4913,94 @@ class Device(CompositeEventEmitter):
|
|
|
4178
4913
|
def add_services(self, services):
|
|
4179
4914
|
self.gatt_server.add_services(services)
|
|
4180
4915
|
|
|
4181
|
-
def add_default_services(
|
|
4916
|
+
def add_default_services(
|
|
4917
|
+
self, add_gap_service: bool = True, add_gatt_service: bool = True
|
|
4918
|
+
) -> None:
|
|
4182
4919
|
# Add a GAP Service if requested
|
|
4183
|
-
if
|
|
4920
|
+
if add_gap_service:
|
|
4184
4921
|
self.gatt_server.add_service(GenericAccessService(self.name))
|
|
4922
|
+
if add_gatt_service:
|
|
4923
|
+
self.gatt_service = gatt_service.GenericAttributeProfileService()
|
|
4924
|
+
self.gatt_server.add_service(self.gatt_service)
|
|
4925
|
+
|
|
4926
|
+
async def notify_subscriber(
|
|
4927
|
+
self,
|
|
4928
|
+
connection: Connection,
|
|
4929
|
+
attribute: Attribute,
|
|
4930
|
+
value: Optional[Any] = None,
|
|
4931
|
+
force: bool = False,
|
|
4932
|
+
) -> None:
|
|
4933
|
+
"""
|
|
4934
|
+
Send a notification to an attribute subscriber.
|
|
4185
4935
|
|
|
4186
|
-
|
|
4936
|
+
Args:
|
|
4937
|
+
connection:
|
|
4938
|
+
The connection of the subscriber.
|
|
4939
|
+
attribute:
|
|
4940
|
+
The attribute whose value is notified.
|
|
4941
|
+
value:
|
|
4942
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4943
|
+
force:
|
|
4944
|
+
If True, send a notification even if there is no subscriber.
|
|
4945
|
+
"""
|
|
4187
4946
|
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
|
4188
4947
|
|
|
4189
|
-
async def notify_subscribers(
|
|
4948
|
+
async def notify_subscribers(
|
|
4949
|
+
self, attribute: Attribute, value=None, force=False
|
|
4950
|
+
) -> None:
|
|
4951
|
+
"""
|
|
4952
|
+
Send a notification to all the subscribers of an attribute.
|
|
4953
|
+
|
|
4954
|
+
Args:
|
|
4955
|
+
attribute:
|
|
4956
|
+
The attribute whose value is notified.
|
|
4957
|
+
value:
|
|
4958
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4959
|
+
force:
|
|
4960
|
+
If True, send a notification for every connection even if there is no
|
|
4961
|
+
subscriber.
|
|
4962
|
+
"""
|
|
4190
4963
|
await self.gatt_server.notify_subscribers(attribute, value, force)
|
|
4191
4964
|
|
|
4192
|
-
async def indicate_subscriber(
|
|
4965
|
+
async def indicate_subscriber(
|
|
4966
|
+
self,
|
|
4967
|
+
connection: Connection,
|
|
4968
|
+
attribute: Attribute,
|
|
4969
|
+
value: Optional[Any] = None,
|
|
4970
|
+
force: bool = False,
|
|
4971
|
+
):
|
|
4972
|
+
"""
|
|
4973
|
+
Send an indication to an attribute subscriber.
|
|
4974
|
+
|
|
4975
|
+
This method returns when the response to the indication has been received.
|
|
4976
|
+
|
|
4977
|
+
Args:
|
|
4978
|
+
connection:
|
|
4979
|
+
The connection of the subscriber.
|
|
4980
|
+
attribute:
|
|
4981
|
+
The attribute whose value is indicated.
|
|
4982
|
+
value:
|
|
4983
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4984
|
+
force:
|
|
4985
|
+
If True, send an indication even if there is no subscriber.
|
|
4986
|
+
"""
|
|
4193
4987
|
await self.gatt_server.indicate_subscriber(connection, attribute, value, force)
|
|
4194
4988
|
|
|
4195
|
-
async def indicate_subscribers(
|
|
4989
|
+
async def indicate_subscribers(
|
|
4990
|
+
self, attribute: Attribute, value: Optional[Any] = None, force: bool = False
|
|
4991
|
+
):
|
|
4992
|
+
"""
|
|
4993
|
+
Send an indication to all the subscribers of an attribute.
|
|
4994
|
+
|
|
4995
|
+
Args:
|
|
4996
|
+
attribute:
|
|
4997
|
+
The attribute whose value is notified.
|
|
4998
|
+
value:
|
|
4999
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
5000
|
+
force:
|
|
5001
|
+
If True, send an indication for every connection even if there is no
|
|
5002
|
+
subscriber.
|
|
5003
|
+
"""
|
|
4196
5004
|
await self.gatt_server.indicate_subscribers(attribute, value, force)
|
|
4197
5005
|
|
|
4198
5006
|
@host_event_handler
|
|
@@ -4233,6 +5041,112 @@ class Device(CompositeEventEmitter):
|
|
|
4233
5041
|
)
|
|
4234
5042
|
self.connecting_extended_advertising_sets[connection_handle] = advertising_set
|
|
4235
5043
|
|
|
5044
|
+
@host_event_handler
|
|
5045
|
+
def on_big_establishment(
|
|
5046
|
+
self,
|
|
5047
|
+
status: int,
|
|
5048
|
+
big_handle: int,
|
|
5049
|
+
bis_handles: list[int],
|
|
5050
|
+
big_sync_delay: int,
|
|
5051
|
+
transport_latency_big: int,
|
|
5052
|
+
phy: int,
|
|
5053
|
+
nse: int,
|
|
5054
|
+
bn: int,
|
|
5055
|
+
pto: int,
|
|
5056
|
+
irc: int,
|
|
5057
|
+
max_pdu: int,
|
|
5058
|
+
iso_interval: int,
|
|
5059
|
+
) -> None:
|
|
5060
|
+
if not (big := self.bigs.get(big_handle)):
|
|
5061
|
+
logger.warning('BIG %d not found', big_handle)
|
|
5062
|
+
return
|
|
5063
|
+
|
|
5064
|
+
if status != hci.HCI_SUCCESS:
|
|
5065
|
+
del self.bigs[big_handle]
|
|
5066
|
+
logger.debug('Unable to create BIG %d', big_handle)
|
|
5067
|
+
big.state = Big.State.TERMINATED
|
|
5068
|
+
big.emit(Big.Event.ESTABLISHMENT_FAILURE, status)
|
|
5069
|
+
return
|
|
5070
|
+
|
|
5071
|
+
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
|
5072
|
+
big.big_sync_delay = big_sync_delay
|
|
5073
|
+
big.transport_latency_big = transport_latency_big
|
|
5074
|
+
big.phy = phy
|
|
5075
|
+
big.nse = nse
|
|
5076
|
+
big.bn = bn
|
|
5077
|
+
big.pto = pto
|
|
5078
|
+
big.irc = irc
|
|
5079
|
+
big.max_pdu = max_pdu
|
|
5080
|
+
big.iso_interval = iso_interval
|
|
5081
|
+
big.state = Big.State.ACTIVE
|
|
5082
|
+
|
|
5083
|
+
for bis_link in big.bis_links:
|
|
5084
|
+
self.bis_links[bis_link.handle] = bis_link
|
|
5085
|
+
big.emit(Big.Event.ESTABLISHMENT)
|
|
5086
|
+
|
|
5087
|
+
@host_event_handler
|
|
5088
|
+
def on_big_termination(self, reason: int, big_handle: int) -> None:
|
|
5089
|
+
if not (big := self.bigs.pop(big_handle, None)):
|
|
5090
|
+
logger.warning('BIG %d not found', big_handle)
|
|
5091
|
+
return
|
|
5092
|
+
|
|
5093
|
+
big.state = Big.State.TERMINATED
|
|
5094
|
+
for bis_link in big.bis_links:
|
|
5095
|
+
self.bis_links.pop(bis_link.handle, None)
|
|
5096
|
+
big.emit(Big.Event.TERMINATION, reason)
|
|
5097
|
+
|
|
5098
|
+
@host_event_handler
|
|
5099
|
+
def on_big_sync_establishment(
|
|
5100
|
+
self,
|
|
5101
|
+
status: int,
|
|
5102
|
+
big_handle: int,
|
|
5103
|
+
transport_latency_big: int,
|
|
5104
|
+
nse: int,
|
|
5105
|
+
bn: int,
|
|
5106
|
+
pto: int,
|
|
5107
|
+
irc: int,
|
|
5108
|
+
max_pdu: int,
|
|
5109
|
+
iso_interval: int,
|
|
5110
|
+
bis_handles: list[int],
|
|
5111
|
+
) -> None:
|
|
5112
|
+
if not (big_sync := self.big_syncs.get(big_handle)):
|
|
5113
|
+
logger.warning('BIG Sync %d not found', big_handle)
|
|
5114
|
+
return
|
|
5115
|
+
|
|
5116
|
+
if status != hci.HCI_SUCCESS:
|
|
5117
|
+
del self.big_syncs[big_handle]
|
|
5118
|
+
logger.debug('Unable to create BIG Sync %d', big_handle)
|
|
5119
|
+
big_sync.state = BigSync.State.TERMINATED
|
|
5120
|
+
big_sync.emit(BigSync.Event.ESTABLISHMENT_FAILURE, status)
|
|
5121
|
+
return
|
|
5122
|
+
|
|
5123
|
+
big_sync.transport_latency_big = transport_latency_big
|
|
5124
|
+
big_sync.nse = nse
|
|
5125
|
+
big_sync.bn = bn
|
|
5126
|
+
big_sync.pto = pto
|
|
5127
|
+
big_sync.irc = irc
|
|
5128
|
+
big_sync.max_pdu = max_pdu
|
|
5129
|
+
big_sync.iso_interval = iso_interval
|
|
5130
|
+
big_sync.bis_links = [
|
|
5131
|
+
BisLink(handle=handle, big=big_sync) for handle in bis_handles
|
|
5132
|
+
]
|
|
5133
|
+
big_sync.state = BigSync.State.ACTIVE
|
|
5134
|
+
|
|
5135
|
+
for bis_link in big_sync.bis_links:
|
|
5136
|
+
self.bis_links[bis_link.handle] = bis_link
|
|
5137
|
+
big_sync.emit(BigSync.Event.ESTABLISHMENT)
|
|
5138
|
+
|
|
5139
|
+
@host_event_handler
|
|
5140
|
+
def on_big_sync_lost(self, big_handle: int, reason: int) -> None:
|
|
5141
|
+
if not (big_sync := self.big_syncs.pop(big_handle, None)):
|
|
5142
|
+
logger.warning('BIG %d not found', big_handle)
|
|
5143
|
+
return
|
|
5144
|
+
|
|
5145
|
+
for bis_link in big_sync.bis_links:
|
|
5146
|
+
self.bis_links.pop(bis_link.handle, None)
|
|
5147
|
+
big_sync.state = BigSync.State.TERMINATED
|
|
5148
|
+
big_sync.emit(BigSync.Event.TERMINATION, reason)
|
|
5149
|
+
|
|
4236
5150
|
def _complete_le_extended_advertising_connection(
|
|
4237
5151
|
self, connection: Connection, advertising_set: AdvertisingSet
|
|
4238
5152
|
) -> None:
|
|
@@ -4258,29 +5172,6 @@ class Device(CompositeEventEmitter):
|
|
|
4258
5172
|
lambda _: self.abort_on('flush', advertising_set.start()),
|
|
4259
5173
|
)
|
|
4260
5174
|
|
|
4261
|
-
self._emit_le_connection(connection)
|
|
4262
|
-
|
|
4263
|
-
def _emit_le_connection(self, connection: Connection) -> None:
|
|
4264
|
-
# If supported, read which PHY we're connected with before
|
|
4265
|
-
# notifying listeners of the new connection.
|
|
4266
|
-
if self.host.supports_command(hci.HCI_LE_READ_PHY_COMMAND):
|
|
4267
|
-
|
|
4268
|
-
async def read_phy():
|
|
4269
|
-
result = await self.send_command(
|
|
4270
|
-
hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
|
|
4271
|
-
check_result=True,
|
|
4272
|
-
)
|
|
4273
|
-
connection.phy = ConnectionPHY(
|
|
4274
|
-
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
|
4275
|
-
)
|
|
4276
|
-
# Emit an event to notify listeners of the new connection
|
|
4277
|
-
self.emit('connection', connection)
|
|
4278
|
-
|
|
4279
|
-
# Do so asynchronously to not block the current event handler
|
|
4280
|
-
connection.abort_on('disconnection', read_phy())
|
|
4281
|
-
|
|
4282
|
-
return
|
|
4283
|
-
|
|
4284
5175
|
self.emit('connection', connection)
|
|
4285
5176
|
|
|
4286
5177
|
@host_event_handler
|
|
@@ -4379,7 +5270,6 @@ class Device(CompositeEventEmitter):
|
|
|
4379
5270
|
peer_resolvable_address,
|
|
4380
5271
|
role,
|
|
4381
5272
|
connection_parameters,
|
|
4382
|
-
ConnectionPHY(hci.HCI_LE_1M_PHY, hci.HCI_LE_1M_PHY),
|
|
4383
5273
|
)
|
|
4384
5274
|
self.connections[connection_handle] = connection
|
|
4385
5275
|
|
|
@@ -4395,7 +5285,7 @@ class Device(CompositeEventEmitter):
|
|
|
4395
5285
|
|
|
4396
5286
|
if role == hci.HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
|
4397
5287
|
# We can emit now, we have all the info we need
|
|
4398
|
-
self.
|
|
5288
|
+
self.emit('connection', connection)
|
|
4399
5289
|
return
|
|
4400
5290
|
|
|
4401
5291
|
if role == hci.HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
|
|
@@ -4879,6 +5769,8 @@ class Device(CompositeEventEmitter):
|
|
|
4879
5769
|
def on_iso_packet(self, handle: int, packet: hci.HCI_IsoDataPacket) -> None:
|
|
4880
5770
|
if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
|
|
4881
5771
|
cis_link.sink(packet)
|
|
5772
|
+
elif (bis_link := self.bis_links.get(handle)) and bis_link.sink:
|
|
5773
|
+
bis_link.sink(packet)
|
|
4882
5774
|
|
|
4883
5775
|
@host_event_handler
|
|
4884
5776
|
@with_connection_from_handle
|
|
@@ -4947,14 +5839,13 @@ class Device(CompositeEventEmitter):
|
|
|
4947
5839
|
|
|
4948
5840
|
@host_event_handler
|
|
4949
5841
|
@with_connection_from_handle
|
|
4950
|
-
def on_connection_phy_update(self, connection,
|
|
5842
|
+
def on_connection_phy_update(self, connection, phy):
|
|
4951
5843
|
logger.debug(
|
|
4952
5844
|
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
|
4953
5845
|
f'{connection.peer_address} as {connection.role_name}, '
|
|
4954
|
-
f'{
|
|
5846
|
+
f'{phy}'
|
|
4955
5847
|
)
|
|
4956
|
-
connection.phy
|
|
4957
|
-
connection.emit('connection_phy_update')
|
|
5848
|
+
connection.emit('connection_phy_update', phy)
|
|
4958
5849
|
|
|
4959
5850
|
@host_event_handler
|
|
4960
5851
|
@with_connection_from_handle
|
|
@@ -4994,6 +5885,106 @@ class Device(CompositeEventEmitter):
|
|
|
4994
5885
|
)
|
|
4995
5886
|
connection.emit('connection_data_length_change')
|
|
4996
5887
|
|
|
5888
|
+
@host_event_handler
|
|
5889
|
+
def on_cs_remote_supported_capabilities(
|
|
5890
|
+
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
|
5891
|
+
):
|
|
5892
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5893
|
+
return
|
|
5894
|
+
|
|
5895
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5896
|
+
connection.emit('channel_sounding_capabilities_failure', event.status)
|
|
5897
|
+
return
|
|
5898
|
+
|
|
5899
|
+
capabilities = ChannelSoundingCapabilities(
|
|
5900
|
+
num_config_supported=event.num_config_supported,
|
|
5901
|
+
max_consecutive_procedures_supported=event.max_consecutive_procedures_supported,
|
|
5902
|
+
num_antennas_supported=event.num_antennas_supported,
|
|
5903
|
+
max_antenna_paths_supported=event.max_antenna_paths_supported,
|
|
5904
|
+
roles_supported=event.roles_supported,
|
|
5905
|
+
modes_supported=event.modes_supported,
|
|
5906
|
+
rtt_capability=event.rtt_capability,
|
|
5907
|
+
rtt_aa_only_n=event.rtt_aa_only_n,
|
|
5908
|
+
rtt_sounding_n=event.rtt_sounding_n,
|
|
5909
|
+
rtt_random_payload_n=event.rtt_random_payload_n,
|
|
5910
|
+
nadm_sounding_capability=event.nadm_sounding_capability,
|
|
5911
|
+
nadm_random_capability=event.nadm_random_capability,
|
|
5912
|
+
cs_sync_phys_supported=event.cs_sync_phys_supported,
|
|
5913
|
+
subfeatures_supported=event.subfeatures_supported,
|
|
5914
|
+
t_ip1_times_supported=event.t_ip1_times_supported,
|
|
5915
|
+
t_ip2_times_supported=event.t_ip2_times_supported,
|
|
5916
|
+
t_fcs_times_supported=event.t_fcs_times_supported,
|
|
5917
|
+
t_pm_times_supported=event.t_pm_times_supported,
|
|
5918
|
+
t_sw_time_supported=event.t_sw_time_supported,
|
|
5919
|
+
tx_snr_capability=event.tx_snr_capability,
|
|
5920
|
+
)
|
|
5921
|
+
connection.emit('channel_sounding_capabilities', capabilities)
|
|
5922
|
+
|
|
5923
|
+
@host_event_handler
|
|
5924
|
+
def on_cs_config(self, event: hci.HCI_LE_CS_Config_Complete_Event):
|
|
5925
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5926
|
+
return
|
|
5927
|
+
|
|
5928
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5929
|
+
connection.emit('channel_sounding_config_failure', event.status)
|
|
5930
|
+
return
|
|
5931
|
+
if event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.CREATED:
|
|
5932
|
+
config = ChannelSoundingConfig(
|
|
5933
|
+
config_id=event.config_id,
|
|
5934
|
+
main_mode_type=event.main_mode_type,
|
|
5935
|
+
sub_mode_type=event.sub_mode_type,
|
|
5936
|
+
min_main_mode_steps=event.min_main_mode_steps,
|
|
5937
|
+
max_main_mode_steps=event.max_main_mode_steps,
|
|
5938
|
+
main_mode_repetition=event.main_mode_repetition,
|
|
5939
|
+
mode_0_steps=event.mode_0_steps,
|
|
5940
|
+
role=event.role,
|
|
5941
|
+
rtt_type=event.rtt_type,
|
|
5942
|
+
cs_sync_phy=event.cs_sync_phy,
|
|
5943
|
+
channel_map=event.channel_map,
|
|
5944
|
+
channel_map_repetition=event.channel_map_repetition,
|
|
5945
|
+
channel_selection_type=event.channel_selection_type,
|
|
5946
|
+
ch3c_shape=event.ch3c_shape,
|
|
5947
|
+
ch3c_jump=event.ch3c_jump,
|
|
5948
|
+
reserved=event.reserved,
|
|
5949
|
+
t_ip1_time=event.t_ip1_time,
|
|
5950
|
+
t_ip2_time=event.t_ip2_time,
|
|
5951
|
+
t_fcs_time=event.t_fcs_time,
|
|
5952
|
+
t_pm_time=event.t_pm_time,
|
|
5953
|
+
)
|
|
5954
|
+
connection.cs_configs[event.config_id] = config
|
|
5955
|
+
connection.emit('channel_sounding_config', config)
|
|
5956
|
+
elif event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.REMOVED:
|
|
5957
|
+
try:
|
|
5958
|
+
config = connection.cs_configs.pop(event.config_id)
|
|
5959
|
+
connection.emit('channel_sounding_config_removed', config.config_id)
|
|
5960
|
+
except KeyError:
|
|
5961
|
+
logger.error('Removing unknown config %d', event.config_id)
|
|
5962
|
+
|
|
5963
|
+
@host_event_handler
|
|
5964
|
+
def on_cs_procedure(self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event):
|
|
5965
|
+
if not (connection := self.lookup_connection(event.connection_handle)):
|
|
5966
|
+
return
|
|
5967
|
+
|
|
5968
|
+
if event.status != hci.HCI_SUCCESS:
|
|
5969
|
+
connection.emit('channel_sounding_procedure_failure', event.status)
|
|
5970
|
+
return
|
|
5971
|
+
|
|
5972
|
+
procedure = ChannelSoundingProcedure(
|
|
5973
|
+
config_id=event.config_id,
|
|
5974
|
+
state=event.state,
|
|
5975
|
+
tone_antenna_config_selection=event.tone_antenna_config_selection,
|
|
5976
|
+
selected_tx_power=event.selected_tx_power,
|
|
5977
|
+
subevent_len=event.subevent_len,
|
|
5978
|
+
subevents_per_event=event.subevents_per_event,
|
|
5979
|
+
subevent_interval=event.subevent_interval,
|
|
5980
|
+
event_interval=event.event_interval,
|
|
5981
|
+
procedure_interval=event.procedure_interval,
|
|
5982
|
+
procedure_count=event.procedure_count,
|
|
5983
|
+
max_procedure_len=event.max_procedure_len,
|
|
5984
|
+
)
|
|
5985
|
+
connection.cs_procedures[procedure.config_id] = procedure
|
|
5986
|
+
connection.emit('channel_sounding_procedure', procedure)
|
|
5987
|
+
|
|
4997
5988
|
# [Classic only]
|
|
4998
5989
|
@host_event_handler
|
|
4999
5990
|
@with_connection_from_address
|
|
@@ -5051,14 +6042,14 @@ class Device(CompositeEventEmitter):
|
|
|
5051
6042
|
if att_pdu.op_code & 1:
|
|
5052
6043
|
if connection.gatt_client is None:
|
|
5053
6044
|
logger.warning(
|
|
5054
|
-
|
|
6045
|
+
'No GATT client for connection 0x%04X', connection.handle
|
|
5055
6046
|
)
|
|
5056
6047
|
return
|
|
5057
6048
|
connection.gatt_client.on_gatt_pdu(att_pdu)
|
|
5058
6049
|
else:
|
|
5059
6050
|
if connection.gatt_server is None:
|
|
5060
6051
|
logger.warning(
|
|
5061
|
-
|
|
6052
|
+
'No GATT server for connection 0x%04X', connection.handle
|
|
5062
6053
|
)
|
|
5063
6054
|
return
|
|
5064
6055
|
connection.gatt_server.on_gatt_pdu(connection, att_pdu)
|