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.
Files changed (51) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +631 -98
  3. bumble/apps/bench.py +238 -157
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/controller_info.py +23 -7
  6. bumble/apps/device_info.py +50 -4
  7. bumble/apps/gg_bridge.py +1 -1
  8. bumble/apps/lea_unicast/app.py +61 -201
  9. bumble/att.py +51 -37
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +305 -156
  14. bumble/device.py +1090 -99
  15. bumble/gatt.py +36 -226
  16. bumble/gatt_adapters.py +374 -0
  17. bumble/gatt_client.py +52 -33
  18. bumble/gatt_server.py +5 -5
  19. bumble/hci.py +812 -14
  20. bumble/host.py +367 -65
  21. bumble/l2cap.py +3 -16
  22. bumble/pairing.py +5 -5
  23. bumble/pandora/host.py +7 -12
  24. bumble/profiles/aics.py +48 -57
  25. bumble/profiles/ascs.py +8 -19
  26. bumble/profiles/asha.py +16 -14
  27. bumble/profiles/bass.py +16 -22
  28. bumble/profiles/battery_service.py +13 -3
  29. bumble/profiles/device_information_service.py +16 -14
  30. bumble/profiles/gap.py +12 -8
  31. bumble/profiles/gatt_service.py +167 -0
  32. bumble/profiles/gmap.py +198 -0
  33. bumble/profiles/hap.py +8 -6
  34. bumble/profiles/heart_rate_service.py +20 -4
  35. bumble/profiles/le_audio.py +87 -4
  36. bumble/profiles/mcp.py +11 -9
  37. bumble/profiles/pacs.py +61 -16
  38. bumble/profiles/tmap.py +8 -12
  39. bumble/profiles/{vcp.py → vcs.py} +35 -29
  40. bumble/profiles/vocs.py +62 -85
  41. bumble/sdp.py +223 -93
  42. bumble/smp.py +1 -1
  43. bumble/utils.py +12 -2
  44. bumble/vendor/android/hci.py +1 -1
  45. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
  46. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
  47. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
  49. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  50. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
  51. {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
- from collections.abc import Iterable
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=self.device.advertising_interval_min,
384
- advertising_interval_max=self.device.advertising_interval_max,
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) -> List[gatt_client.ServiceProxy]:
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
- ) -> List[gatt_client.ServiceProxy]:
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
- ) -> List[gatt_client.ServiceProxy]:
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
- ) -> List[gatt_client.ServiceProxy]:
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
- ) -> List[gatt_client.CharacteristicProxy]:
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) -> List[gatt_client.AttributeProxy]:
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
- ) -> List[bytes]:
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) -> List[gatt_client.ServiceProxy]:
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
- ) -> List[gatt_client.CharacteristicProxy]:
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: int = DEVICE_DEFAULT_CONNECTION_INTERVAL_MIN
1176
- connection_interval_max: int = DEVICE_DEFAULT_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: Optional[Callable[[hci.HCI_IsoDataPacket], Any]] = None
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: List[Dict[str, Any]] = []
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: List[str] = []
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
- List[asyncio.Future[Union[Connection, Tuple[hci.Address, int, int]]]],
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: List[PeriodicAdvertisingSync]
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
- _pending_cis: Dict[int, Tuple[int, int]]
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(generic_access_service)
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 hci.Address: {self.random_address}',
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: int = DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
2627
- scan_window: int = DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
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: List[int] = [hci.HCI_LE_1M_PHY, hci.HCI_LE_CODED_PHY],
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(scan_window / 0.625)] * scanning_phy_count,
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 (result.return_parameters.tx_phy, result.return_parameters.rx_phy)
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(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True)
3588
- if local_name is None:
3589
- local_name = ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME, raw=True)
3590
- if local_name is not None:
3591
- if local_name.decode('utf-8') == name:
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: List[int],
3962
- sdu_interval: Tuple[int, int],
4387
+ cis_id: Sequence[int],
4388
+ sdu_interval: tuple[int, int],
3963
4389
  framing: int,
3964
- max_sdu: Tuple[int, int],
4390
+ max_sdu: tuple[int, int],
3965
4391
  retransmission_number: int,
3966
- max_transport_latency: Tuple[int, int],
3967
- ) -> List[int]:
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(self, cis_acl_pairs: List[Tuple[int, int]]) -> List[CisLink]:
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(self, generic_access_service=True):
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 generic_access_service:
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
- async def notify_subscriber(self, connection, attribute, value=None, force=False):
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(self, attribute, value=None, force=False):
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(self, connection, attribute, value=None, force=False):
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(self, attribute, value=None, force=False):
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._emit_le_connection(connection)
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, connection_phy):
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'{connection_phy}'
5846
+ f'{phy}'
4955
5847
  )
4956
- connection.phy = 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
- color('no GATT client for connection 0x{connection_handle:04X}')
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
- color('no GATT server for connection 0x{connection_handle:04X}')
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)