bumble 0.0.204__py3-none-any.whl → 0.0.207__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bumble/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,
@@ -54,7 +54,7 @@ from pyee import EventEmitter
54
54
  from .colors import color
55
55
  from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
56
56
  from .gatt import Characteristic, Descriptor, Service
57
- from .host import Host
57
+ from .host import DataPacketQueue, Host
58
58
  from .profiles.gap import GenericAccessService
59
59
  from .core import (
60
60
  BT_BR_EDR_TRANSPORT,
@@ -95,6 +95,7 @@ from bumble import smp
95
95
  from bumble import sdp
96
96
  from bumble import l2cap
97
97
  from bumble import core
98
+ from bumble.profiles import gatt_service
98
99
 
99
100
  if TYPE_CHECKING:
100
101
  from .transport.common import TransportSource, TransportSink
@@ -119,6 +120,10 @@ DEVICE_MIN_LE_RSSI = -127
119
120
  DEVICE_MAX_LE_RSSI = 20
120
121
  DEVICE_MIN_EXTENDED_ADVERTISING_SET_HANDLE = 0x00
121
122
  DEVICE_MAX_EXTENDED_ADVERTISING_SET_HANDLE = 0xEF
123
+ DEVICE_MIN_BIG_HANDLE = 0x00
124
+ DEVICE_MAX_BIG_HANDLE = 0xEF
125
+ DEVICE_MIN_CS_CONFIG_ID = 0x00
126
+ DEVICE_MAX_CS_CONFIG_ID = 0x03
122
127
 
123
128
  DEVICE_DEFAULT_ADDRESS = '00:00:00:00:00:00'
124
129
  DEVICE_DEFAULT_ADVERTISING_INTERVAL = 1000 # ms
@@ -254,7 +259,7 @@ class ExtendedAdvertisement(Advertisement):
254
259
  secondary_phy = report.secondary_phy,
255
260
  tx_power = report.tx_power,
256
261
  sid = report.advertising_sid,
257
- data_bytes = report.data
262
+ data_bytes = report.data,
258
263
  )
259
264
  # fmt: on
260
265
 
@@ -380,8 +385,12 @@ class LegacyAdvertiser:
380
385
  # Set the advertising parameters
381
386
  await self.device.send_command(
382
387
  hci.HCI_LE_Set_Advertising_Parameters_Command(
383
- advertising_interval_min=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:
@@ -1470,6 +1798,10 @@ class Connection(CompositeEventEmitter):
1470
1798
  self.peer_le_features = await self.device.get_remote_le_features(self)
1471
1799
  return self.peer_le_features
1472
1800
 
1801
+ @property
1802
+ def data_packet_queue(self) -> DataPacketQueue | None:
1803
+ return self.device.host.get_data_packet_queue(self.handle)
1804
+
1473
1805
  async def __aenter__(self):
1474
1806
  return self
1475
1807
 
@@ -1533,11 +1865,14 @@ class DeviceConfiguration:
1533
1865
  address_resolution_offload: bool = False
1534
1866
  address_generation_offload: bool = False
1535
1867
  cis_enabled: bool = False
1868
+ channel_sounding_enabled: bool = False
1536
1869
  identity_address_type: Optional[int] = None
1537
1870
  io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
1871
+ gap_service_enabled: bool = True
1872
+ gatt_service_enabled: bool = True
1538
1873
 
1539
1874
  def __post_init__(self) -> None:
1540
- self.gatt_services: List[Dict[str, Any]] = []
1875
+ self.gatt_services: list[Dict[str, Any]] = []
1541
1876
 
1542
1877
  def load_from_dict(self, config: Dict[str, Any]) -> None:
1543
1878
  config = copy.deepcopy(config)
@@ -1684,7 +2019,7 @@ def host_event_handler(function):
1684
2019
  # List of host event handlers for the Device class.
1685
2020
  # (we define this list outside the class, because referencing a class in method
1686
2021
  # decorators is not straightforward)
1687
- device_host_event_handlers: List[str] = []
2022
+ device_host_event_handlers: list[str] = []
1688
2023
 
1689
2024
 
1690
2025
  # -----------------------------------------------------------------------------
@@ -1701,19 +2036,24 @@ class Device(CompositeEventEmitter):
1701
2036
  gatt_server: gatt_server.Server
1702
2037
  advertising_data: bytes
1703
2038
  scan_response_data: bytes
2039
+ cs_capabilities: ChannelSoundingCapabilities | None = None
1704
2040
  connections: Dict[int, Connection]
1705
2041
  pending_connections: Dict[hci.Address, Connection]
1706
2042
  classic_pending_accepts: Dict[
1707
2043
  hci.Address,
1708
- List[asyncio.Future[Union[Connection, Tuple[hci.Address, int, int]]]],
2044
+ list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
1709
2045
  ]
1710
2046
  advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
1711
- periodic_advertising_syncs: List[PeriodicAdvertisingSync]
2047
+ periodic_advertising_syncs: list[PeriodicAdvertisingSync]
1712
2048
  config: DeviceConfiguration
1713
2049
  legacy_advertiser: Optional[LegacyAdvertiser]
1714
2050
  sco_links: Dict[int, ScoLink]
1715
2051
  cis_links: Dict[int, CisLink]
1716
- _pending_cis: Dict[int, Tuple[int, int]]
2052
+ bigs = dict[int, Big]()
2053
+ bis_links = dict[int, BisLink]()
2054
+ big_syncs = dict[int, BigSync]()
2055
+ _pending_cis: Dict[int, tuple[int, int]]
2056
+ gatt_service: gatt_service.GenericAttributeProfileService | None = None
1717
2057
 
1718
2058
  @composite_listener
1719
2059
  class Listener:
@@ -1780,7 +2120,6 @@ class Device(CompositeEventEmitter):
1780
2120
  address: Optional[hci.Address] = None,
1781
2121
  config: Optional[DeviceConfiguration] = None,
1782
2122
  host: Optional[Host] = None,
1783
- generic_access_service: bool = True,
1784
2123
  ) -> None:
1785
2124
  super().__init__()
1786
2125
 
@@ -1927,7 +2266,10 @@ class Device(CompositeEventEmitter):
1927
2266
  # Register the SDP server with the L2CAP Channel Manager
1928
2267
  self.sdp_server.register(self.l2cap_channel_manager)
1929
2268
 
1930
- self.add_default_services(generic_access_service)
2269
+ self.add_default_services(
2270
+ add_gap_service=config.gap_service_enabled,
2271
+ add_gatt_service=config.gatt_service_enabled,
2272
+ )
1931
2273
  self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
1932
2274
 
1933
2275
  # Forward some events
@@ -2009,6 +2351,17 @@ class Device(CompositeEventEmitter):
2009
2351
  None,
2010
2352
  )
2011
2353
 
2354
+ def next_big_handle(self) -> int | None:
2355
+ return next(
2356
+ (
2357
+ handle
2358
+ for handle in range(DEVICE_MIN_BIG_HANDLE, DEVICE_MAX_BIG_HANDLE + 1)
2359
+ if handle
2360
+ not in itertools.chain(self.bigs.keys(), self.big_syncs.keys())
2361
+ ),
2362
+ None,
2363
+ )
2364
+
2012
2365
  @deprecated("Please use create_l2cap_server()")
2013
2366
  def register_l2cap_server(self, psm, server) -> int:
2014
2367
  return self.l2cap_channel_manager.register_server(psm, server)
@@ -2167,7 +2520,7 @@ class Device(CompositeEventEmitter):
2167
2520
  if self.random_address != hci.Address.ANY_RANDOM:
2168
2521
  logger.debug(
2169
2522
  color(
2170
- f'LE Random hci.Address: {self.random_address}',
2523
+ f'LE Random Address: {self.random_address}',
2171
2524
  'yellow',
2172
2525
  )
2173
2526
  )
@@ -2200,6 +2553,41 @@ class Device(CompositeEventEmitter):
2200
2553
  check_result=True,
2201
2554
  )
2202
2555
 
2556
+ if self.config.channel_sounding_enabled:
2557
+ await self.send_command(
2558
+ hci.HCI_LE_Set_Host_Feature_Command(
2559
+ bit_number=hci.LeFeature.CHANNEL_SOUNDING_HOST_SUPPORT,
2560
+ bit_value=1,
2561
+ ),
2562
+ check_result=True,
2563
+ )
2564
+ result = await self.send_command(
2565
+ hci.HCI_LE_CS_Read_Local_Supported_Capabilities_Command(),
2566
+ check_result=True,
2567
+ )
2568
+ self.cs_capabilities = ChannelSoundingCapabilities(
2569
+ num_config_supported=result.return_parameters.num_config_supported,
2570
+ max_consecutive_procedures_supported=result.return_parameters.max_consecutive_procedures_supported,
2571
+ num_antennas_supported=result.return_parameters.num_antennas_supported,
2572
+ max_antenna_paths_supported=result.return_parameters.max_antenna_paths_supported,
2573
+ roles_supported=result.return_parameters.roles_supported,
2574
+ modes_supported=result.return_parameters.modes_supported,
2575
+ rtt_capability=result.return_parameters.rtt_capability,
2576
+ rtt_aa_only_n=result.return_parameters.rtt_aa_only_n,
2577
+ rtt_sounding_n=result.return_parameters.rtt_sounding_n,
2578
+ rtt_random_payload_n=result.return_parameters.rtt_random_payload_n,
2579
+ nadm_sounding_capability=result.return_parameters.nadm_sounding_capability,
2580
+ nadm_random_capability=result.return_parameters.nadm_random_capability,
2581
+ cs_sync_phys_supported=result.return_parameters.cs_sync_phys_supported,
2582
+ subfeatures_supported=result.return_parameters.subfeatures_supported,
2583
+ t_ip1_times_supported=result.return_parameters.t_ip1_times_supported,
2584
+ t_ip2_times_supported=result.return_parameters.t_ip2_times_supported,
2585
+ t_fcs_times_supported=result.return_parameters.t_fcs_times_supported,
2586
+ t_pm_times_supported=result.return_parameters.t_pm_times_supported,
2587
+ t_sw_time_supported=result.return_parameters.t_sw_time_supported,
2588
+ tx_snr_capability=result.return_parameters.tx_snr_capability,
2589
+ )
2590
+
2203
2591
  if self.classic_enabled:
2204
2592
  await self.send_command(
2205
2593
  hci.HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
@@ -2283,7 +2671,7 @@ class Device(CompositeEventEmitter):
2283
2671
  """Update the RPA periodically"""
2284
2672
  while self.le_rpa_timeout != 0:
2285
2673
  await asyncio.sleep(self.le_rpa_timeout)
2286
- if not self.update_rpa():
2674
+ if not await self.update_rpa():
2287
2675
  logger.debug("periodic RPA update failed")
2288
2676
 
2289
2677
  async def refresh_resolving_list(self) -> None:
@@ -2623,11 +3011,11 @@ class Device(CompositeEventEmitter):
2623
3011
  self,
2624
3012
  legacy: bool = False,
2625
3013
  active: bool = True,
2626
- scan_interval: int = DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
2627
- scan_window: int = DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
3014
+ scan_interval: float = DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
3015
+ scan_window: float = DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
2628
3016
  own_address_type: int = hci.OwnAddressType.RANDOM,
2629
3017
  filter_duplicates: bool = False,
2630
- scanning_phys: List[int] = [hci.HCI_LE_1M_PHY, hci.HCI_LE_CODED_PHY],
3018
+ scanning_phys: Sequence[int] = (hci.HCI_LE_1M_PHY, hci.HCI_LE_CODED_PHY),
2631
3019
  ) -> None:
2632
3020
  # Check that the arguments are legal
2633
3021
  if scan_interval < scan_window:
@@ -2674,7 +3062,7 @@ class Device(CompositeEventEmitter):
2674
3062
  scanning_filter_policy=scanning_filter_policy,
2675
3063
  scanning_phys=scanning_phys_bits,
2676
3064
  scan_types=[scan_type] * scanning_phy_count,
2677
- scan_intervals=[int(scan_window / 0.625)] * scanning_phy_count,
3065
+ scan_intervals=[int(scan_interval / 0.625)] * scanning_phy_count,
2678
3066
  scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
2679
3067
  ),
2680
3068
  check_result=True,
@@ -2840,6 +3228,41 @@ class Device(CompositeEventEmitter):
2840
3228
  "periodic advertising sync establishment for unknown address/sid"
2841
3229
  )
2842
3230
 
3231
+ @host_event_handler
3232
+ def on_periodic_advertising_sync_transfer(
3233
+ self,
3234
+ status: int,
3235
+ connection_handle: int,
3236
+ sync_handle: int,
3237
+ advertising_sid: int,
3238
+ advertiser_address: hci.Address,
3239
+ advertiser_phy: int,
3240
+ periodic_advertising_interval: int,
3241
+ advertiser_clock_accuracy: int,
3242
+ ) -> None:
3243
+ if not (connection := self.lookup_connection(connection_handle)):
3244
+ logger.error(
3245
+ "Receive PAST from unknown connection 0x%04X", connection_handle
3246
+ )
3247
+
3248
+ pa_sync = PeriodicAdvertisingSync(
3249
+ device=self,
3250
+ advertiser_address=advertiser_address,
3251
+ sid=advertising_sid,
3252
+ skip=0,
3253
+ sync_timeout=0.0,
3254
+ filter_duplicates=False,
3255
+ )
3256
+ self.periodic_advertising_syncs.append(pa_sync)
3257
+ pa_sync.on_establishment(
3258
+ status=status,
3259
+ sync_handle=sync_handle,
3260
+ advertiser_phy=advertiser_phy,
3261
+ periodic_advertising_interval=periodic_advertising_interval,
3262
+ advertiser_clock_accuracy=advertiser_clock_accuracy,
3263
+ )
3264
+ self.emit('periodic_advertising_sync_transfer', pa_sync, connection)
3265
+
2843
3266
  @host_event_handler
2844
3267
  @with_periodic_advertising_sync_from_handle
2845
3268
  def on_periodic_advertising_sync_loss(
@@ -3958,13 +4381,13 @@ class Device(CompositeEventEmitter):
3958
4381
  async def setup_cig(
3959
4382
  self,
3960
4383
  cig_id: int,
3961
- cis_id: List[int],
3962
- sdu_interval: Tuple[int, int],
4384
+ cis_id: Sequence[int],
4385
+ sdu_interval: tuple[int, int],
3963
4386
  framing: int,
3964
- max_sdu: Tuple[int, int],
4387
+ max_sdu: tuple[int, int],
3965
4388
  retransmission_number: int,
3966
- max_transport_latency: Tuple[int, int],
3967
- ) -> List[int]:
4389
+ max_transport_latency: tuple[int, int],
4390
+ ) -> list[int]:
3968
4391
  """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
3969
4392
 
3970
4393
  Args:
@@ -4013,7 +4436,9 @@ class Device(CompositeEventEmitter):
4013
4436
 
4014
4437
  # [LE only]
4015
4438
  @experimental('Only for testing.')
4016
- async def create_cis(self, cis_acl_pairs: List[Tuple[int, int]]) -> List[CisLink]:
4439
+ async def create_cis(
4440
+ self, cis_acl_pairs: Sequence[tuple[int, int]]
4441
+ ) -> list[CisLink]:
4017
4442
  for cis_handle, acl_handle in cis_acl_pairs:
4018
4443
  acl_connection = self.lookup_connection(acl_handle)
4019
4444
  assert acl_connection
@@ -4112,6 +4537,106 @@ class Device(CompositeEventEmitter):
4112
4537
  check_result=True,
4113
4538
  )
4114
4539
 
4540
+ # [LE only]
4541
+ @experimental('Only for testing.')
4542
+ async def create_big(
4543
+ self, advertising_set: AdvertisingSet, parameters: BigParameters
4544
+ ) -> Big:
4545
+ if (big_handle := self.next_big_handle()) is None:
4546
+ raise core.OutOfResourcesError("All valid BIG handles already in use")
4547
+
4548
+ with closing(EventWatcher()) as watcher:
4549
+ big = Big(
4550
+ big_handle=big_handle,
4551
+ parameters=parameters,
4552
+ advertising_set=advertising_set,
4553
+ )
4554
+ self.bigs[big_handle] = big
4555
+ established = asyncio.get_running_loop().create_future()
4556
+ watcher.once(
4557
+ big, big.Event.ESTABLISHMENT, lambda: established.set_result(None)
4558
+ )
4559
+ watcher.once(
4560
+ big,
4561
+ big.Event.ESTABLISHMENT_FAILURE,
4562
+ lambda status: established.set_exception(hci.HCI_Error(status)),
4563
+ )
4564
+
4565
+ try:
4566
+ await self.send_command(
4567
+ hci.HCI_LE_Create_BIG_Command(
4568
+ big_handle=big_handle,
4569
+ advertising_handle=advertising_set.advertising_handle,
4570
+ num_bis=parameters.num_bis,
4571
+ sdu_interval=parameters.sdu_interval,
4572
+ max_sdu=parameters.max_sdu,
4573
+ max_transport_latency=parameters.max_transport_latency,
4574
+ rtn=parameters.rtn,
4575
+ phy=parameters.phy,
4576
+ packing=parameters.packing,
4577
+ framing=parameters.framing,
4578
+ encryption=1 if parameters.broadcast_code else 0,
4579
+ broadcast_code=parameters.broadcast_code or bytes(16),
4580
+ ),
4581
+ check_result=True,
4582
+ )
4583
+ await established
4584
+ except hci.HCI_Error:
4585
+ del self.bigs[big_handle]
4586
+ raise
4587
+
4588
+ return big
4589
+
4590
+ # [LE only]
4591
+ @experimental('Only for testing.')
4592
+ async def create_big_sync(
4593
+ self, pa_sync: PeriodicAdvertisingSync, parameters: BigSyncParameters
4594
+ ) -> BigSync:
4595
+ if (big_handle := self.next_big_handle()) is None:
4596
+ raise core.OutOfResourcesError("All valid BIG handles already in use")
4597
+
4598
+ if (pa_sync_handle := pa_sync.sync_handle) is None:
4599
+ raise core.InvalidStateError("PA Sync is not established")
4600
+
4601
+ with closing(EventWatcher()) as watcher:
4602
+ big_sync = BigSync(
4603
+ big_handle=big_handle,
4604
+ parameters=parameters,
4605
+ pa_sync=pa_sync,
4606
+ )
4607
+ self.big_syncs[big_handle] = big_sync
4608
+ established = asyncio.get_running_loop().create_future()
4609
+ watcher.once(
4610
+ big_sync,
4611
+ big_sync.Event.ESTABLISHMENT,
4612
+ lambda: established.set_result(None),
4613
+ )
4614
+ watcher.once(
4615
+ big_sync,
4616
+ big_sync.Event.ESTABLISHMENT_FAILURE,
4617
+ lambda status: established.set_exception(hci.HCI_Error(status)),
4618
+ )
4619
+
4620
+ try:
4621
+ await self.send_command(
4622
+ hci.HCI_LE_BIG_Create_Sync_Command(
4623
+ big_handle=big_handle,
4624
+ sync_handle=pa_sync_handle,
4625
+ encryption=1 if parameters.broadcast_code else 0,
4626
+ broadcast_code=parameters.broadcast_code or bytes(16),
4627
+ mse=parameters.mse,
4628
+ big_sync_timeout=parameters.big_sync_timeout,
4629
+ bis=parameters.bis,
4630
+ ),
4631
+ check_result=True,
4632
+ )
4633
+ await established
4634
+ except hci.HCI_Error:
4635
+ del self.big_syncs[big_handle]
4636
+ raise
4637
+
4638
+ return big_sync
4639
+
4115
4640
  async def get_remote_le_features(self, connection: Connection) -> hci.LeFeatureMask:
4116
4641
  """[LE Only] Reads remote LE supported features.
4117
4642
 
@@ -4144,6 +4669,213 @@ class Device(CompositeEventEmitter):
4144
4669
  )
4145
4670
  return await read_feature_future
4146
4671
 
4672
+ @experimental('Only for testing.')
4673
+ async def get_remote_cs_capabilities(
4674
+ self, connection: Connection
4675
+ ) -> ChannelSoundingCapabilities:
4676
+ complete_future: asyncio.Future[ChannelSoundingCapabilities] = (
4677
+ asyncio.get_running_loop().create_future()
4678
+ )
4679
+
4680
+ with closing(EventWatcher()) as watcher:
4681
+ watcher.once(
4682
+ connection, 'channel_sounding_capabilities', complete_future.set_result
4683
+ )
4684
+ watcher.once(
4685
+ connection,
4686
+ 'channel_sounding_capabilities_failure',
4687
+ lambda status: complete_future.set_exception(hci.HCI_Error(status)),
4688
+ )
4689
+ await self.send_command(
4690
+ hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Command(
4691
+ connection_handle=connection.handle
4692
+ ),
4693
+ check_result=True,
4694
+ )
4695
+ return await complete_future
4696
+
4697
+ @experimental('Only for testing.')
4698
+ async def set_default_cs_settings(
4699
+ self,
4700
+ connection: Connection,
4701
+ role_enable: int = (
4702
+ hci.CsRoleMask.INITIATOR | hci.CsRoleMask.REFLECTOR
4703
+ ), # Both role
4704
+ cs_sync_antenna_selection: int = 0xFF, # No Preference
4705
+ max_tx_power: int = 0x04, # 4 dB
4706
+ ) -> None:
4707
+ await self.send_command(
4708
+ hci.HCI_LE_CS_Set_Default_Settings_Command(
4709
+ connection_handle=connection.handle,
4710
+ role_enable=role_enable,
4711
+ cs_sync_antenna_selection=cs_sync_antenna_selection,
4712
+ max_tx_power=max_tx_power,
4713
+ ),
4714
+ check_result=True,
4715
+ )
4716
+
4717
+ @experimental('Only for testing.')
4718
+ async def create_cs_config(
4719
+ self,
4720
+ connection: Connection,
4721
+ config_id: int | None = None,
4722
+ create_context: int = 0x01,
4723
+ main_mode_type: int = 0x02,
4724
+ sub_mode_type: int = 0xFF,
4725
+ min_main_mode_steps: int = 0x02,
4726
+ max_main_mode_steps: int = 0x05,
4727
+ main_mode_repetition: int = 0x00,
4728
+ mode_0_steps: int = 0x03,
4729
+ role: int = hci.CsRole.INITIATOR,
4730
+ rtt_type: int = hci.RttType.AA_ONLY,
4731
+ cs_sync_phy: int = hci.CsSyncPhy.LE_1M,
4732
+ channel_map: bytes = b'\x54\x55\x55\x54\x55\x55\x55\x55\x55\x15',
4733
+ channel_map_repetition: int = 0x01,
4734
+ channel_selection_type: int = hci.HCI_LE_CS_Create_Config_Command.ChannelSelectionType.ALGO_3B,
4735
+ ch3c_shape: int = hci.HCI_LE_CS_Create_Config_Command.Ch3cShape.HAT,
4736
+ ch3c_jump: int = 0x03,
4737
+ ) -> ChannelSoundingConfig:
4738
+ complete_future: asyncio.Future[ChannelSoundingConfig] = (
4739
+ asyncio.get_running_loop().create_future()
4740
+ )
4741
+ if config_id is None:
4742
+ # Allocate an ID.
4743
+ config_id = next(
4744
+ (
4745
+ i
4746
+ for i in range(DEVICE_MIN_CS_CONFIG_ID, DEVICE_MAX_CS_CONFIG_ID + 1)
4747
+ if i not in connection.cs_configs
4748
+ ),
4749
+ None,
4750
+ )
4751
+ if config_id is None:
4752
+ raise OutOfResourcesError("No available config ID on this connection!")
4753
+
4754
+ with closing(EventWatcher()) as watcher:
4755
+ watcher.once(
4756
+ connection, 'channel_sounding_config', complete_future.set_result
4757
+ )
4758
+ watcher.once(
4759
+ connection,
4760
+ 'channel_sounding_config_failure',
4761
+ lambda status: complete_future.set_exception(hci.HCI_Error(status)),
4762
+ )
4763
+ await self.send_command(
4764
+ hci.HCI_LE_CS_Create_Config_Command(
4765
+ connection_handle=connection.handle,
4766
+ config_id=config_id,
4767
+ create_context=create_context,
4768
+ main_mode_type=main_mode_type,
4769
+ sub_mode_type=sub_mode_type,
4770
+ min_main_mode_steps=min_main_mode_steps,
4771
+ max_main_mode_steps=max_main_mode_steps,
4772
+ main_mode_repetition=main_mode_repetition,
4773
+ mode_0_steps=mode_0_steps,
4774
+ role=role,
4775
+ rtt_type=rtt_type,
4776
+ cs_sync_phy=cs_sync_phy,
4777
+ channel_map=channel_map,
4778
+ channel_map_repetition=channel_map_repetition,
4779
+ channel_selection_type=channel_selection_type,
4780
+ ch3c_shape=ch3c_shape,
4781
+ ch3c_jump=ch3c_jump,
4782
+ reserved=0x00,
4783
+ ),
4784
+ check_result=True,
4785
+ )
4786
+ return await complete_future
4787
+
4788
+ @experimental('Only for testing.')
4789
+ async def enable_cs_security(self, connection: Connection) -> None:
4790
+ complete_future: asyncio.Future[None] = (
4791
+ asyncio.get_running_loop().create_future()
4792
+ )
4793
+ with closing(EventWatcher()) as watcher:
4794
+
4795
+ def on_event(event: hci.HCI_LE_CS_Security_Enable_Complete_Event) -> None:
4796
+ if event.connection_handle != connection.handle:
4797
+ return
4798
+ if event.status == hci.HCI_SUCCESS:
4799
+ complete_future.set_result(None)
4800
+ else:
4801
+ complete_future.set_exception(hci.HCI_Error(event.status))
4802
+
4803
+ watcher.once(self.host, 'cs_security', on_event)
4804
+ await self.send_command(
4805
+ hci.HCI_LE_CS_Security_Enable_Command(
4806
+ connection_handle=connection.handle
4807
+ ),
4808
+ check_result=True,
4809
+ )
4810
+ return await complete_future
4811
+
4812
+ @experimental('Only for testing.')
4813
+ async def set_cs_procedure_parameters(
4814
+ self,
4815
+ connection: Connection,
4816
+ config: ChannelSoundingConfig,
4817
+ tone_antenna_config_selection=0x00,
4818
+ preferred_peer_antenna=0x00,
4819
+ max_procedure_len=0x2710, # 6.25s
4820
+ min_procedure_interval=0x01,
4821
+ max_procedure_interval=0xFF,
4822
+ max_procedure_count=0x01,
4823
+ min_subevent_len=0x0004E2, # 1250us
4824
+ max_subevent_len=0x1E8480, # 2s
4825
+ phy=hci.CsSyncPhy.LE_1M,
4826
+ tx_power_delta=0x00,
4827
+ snr_control_initiator=hci.CsSnr.NOT_APPLIED,
4828
+ snr_control_reflector=hci.CsSnr.NOT_APPLIED,
4829
+ ) -> None:
4830
+ await self.send_command(
4831
+ hci.HCI_LE_CS_Set_Procedure_Parameters_Command(
4832
+ connection_handle=connection.handle,
4833
+ config_id=config.config_id,
4834
+ max_procedure_len=max_procedure_len,
4835
+ min_procedure_interval=min_procedure_interval,
4836
+ max_procedure_interval=max_procedure_interval,
4837
+ max_procedure_count=max_procedure_count,
4838
+ min_subevent_len=min_subevent_len,
4839
+ max_subevent_len=max_subevent_len,
4840
+ tone_antenna_config_selection=tone_antenna_config_selection,
4841
+ phy=phy,
4842
+ tx_power_delta=tx_power_delta,
4843
+ preferred_peer_antenna=preferred_peer_antenna,
4844
+ snr_control_initiator=snr_control_initiator,
4845
+ snr_control_reflector=snr_control_reflector,
4846
+ ),
4847
+ check_result=True,
4848
+ )
4849
+
4850
+ @experimental('Only for testing.')
4851
+ async def enable_cs_procedure(
4852
+ self,
4853
+ connection: Connection,
4854
+ config: ChannelSoundingConfig,
4855
+ enabled: bool = True,
4856
+ ) -> ChannelSoundingProcedure:
4857
+ complete_future: asyncio.Future[ChannelSoundingProcedure] = (
4858
+ asyncio.get_running_loop().create_future()
4859
+ )
4860
+ with closing(EventWatcher()) as watcher:
4861
+ watcher.once(
4862
+ connection, 'channel_sounding_procedure', complete_future.set_result
4863
+ )
4864
+ watcher.once(
4865
+ connection,
4866
+ 'channel_sounding_procedure_failure',
4867
+ lambda x: complete_future.set_exception(hci.HCI_Error(x)),
4868
+ )
4869
+ await self.send_command(
4870
+ hci.HCI_LE_CS_Procedure_Enable_Command(
4871
+ connection_handle=connection.handle,
4872
+ config_id=config.config_id,
4873
+ enable=enabled,
4874
+ ),
4875
+ check_result=True,
4876
+ )
4877
+ return await complete_future
4878
+
4147
4879
  @host_event_handler
4148
4880
  def on_flush(self):
4149
4881
  self.emit('flush')
@@ -4178,10 +4910,15 @@ class Device(CompositeEventEmitter):
4178
4910
  def add_services(self, services):
4179
4911
  self.gatt_server.add_services(services)
4180
4912
 
4181
- def add_default_services(self, generic_access_service=True):
4913
+ def add_default_services(
4914
+ self, add_gap_service: bool = True, add_gatt_service: bool = True
4915
+ ) -> None:
4182
4916
  # Add a GAP Service if requested
4183
- if generic_access_service:
4917
+ if add_gap_service:
4184
4918
  self.gatt_server.add_service(GenericAccessService(self.name))
4919
+ if add_gatt_service:
4920
+ self.gatt_service = gatt_service.GenericAttributeProfileService()
4921
+ self.gatt_server.add_service(self.gatt_service)
4185
4922
 
4186
4923
  async def notify_subscriber(self, connection, attribute, value=None, force=False):
4187
4924
  await self.gatt_server.notify_subscriber(connection, attribute, value, force)
@@ -4233,6 +4970,112 @@ class Device(CompositeEventEmitter):
4233
4970
  )
4234
4971
  self.connecting_extended_advertising_sets[connection_handle] = advertising_set
4235
4972
 
4973
+ @host_event_handler
4974
+ def on_big_establishment(
4975
+ self,
4976
+ status: int,
4977
+ big_handle: int,
4978
+ bis_handles: list[int],
4979
+ big_sync_delay: int,
4980
+ transport_latency_big: int,
4981
+ phy: int,
4982
+ nse: int,
4983
+ bn: int,
4984
+ pto: int,
4985
+ irc: int,
4986
+ max_pdu: int,
4987
+ iso_interval: int,
4988
+ ) -> None:
4989
+ if not (big := self.bigs.get(big_handle)):
4990
+ logger.warning('BIG %d not found', big_handle)
4991
+ return
4992
+
4993
+ if status != hci.HCI_SUCCESS:
4994
+ del self.bigs[big_handle]
4995
+ logger.debug('Unable to create BIG %d', big_handle)
4996
+ big.state = Big.State.TERMINATED
4997
+ big.emit(Big.Event.ESTABLISHMENT_FAILURE, status)
4998
+ return
4999
+
5000
+ big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
5001
+ big.big_sync_delay = big_sync_delay
5002
+ big.transport_latency_big = transport_latency_big
5003
+ big.phy = phy
5004
+ big.nse = nse
5005
+ big.bn = bn
5006
+ big.pto = pto
5007
+ big.irc = irc
5008
+ big.max_pdu = max_pdu
5009
+ big.iso_interval = iso_interval
5010
+ big.state = Big.State.ACTIVE
5011
+
5012
+ for bis_link in big.bis_links:
5013
+ self.bis_links[bis_link.handle] = bis_link
5014
+ big.emit(Big.Event.ESTABLISHMENT)
5015
+
5016
+ @host_event_handler
5017
+ def on_big_termination(self, reason: int, big_handle: int) -> None:
5018
+ if not (big := self.bigs.pop(big_handle, None)):
5019
+ logger.warning('BIG %d not found', big_handle)
5020
+ return
5021
+
5022
+ big.state = Big.State.TERMINATED
5023
+ for bis_link in big.bis_links:
5024
+ self.bis_links.pop(bis_link.handle, None)
5025
+ big.emit(Big.Event.TERMINATION, reason)
5026
+
5027
+ @host_event_handler
5028
+ def on_big_sync_establishment(
5029
+ self,
5030
+ status: int,
5031
+ big_handle: int,
5032
+ transport_latency_big: int,
5033
+ nse: int,
5034
+ bn: int,
5035
+ pto: int,
5036
+ irc: int,
5037
+ max_pdu: int,
5038
+ iso_interval: int,
5039
+ bis_handles: list[int],
5040
+ ) -> None:
5041
+ if not (big_sync := self.big_syncs.get(big_handle)):
5042
+ logger.warning('BIG Sync %d not found', big_handle)
5043
+ return
5044
+
5045
+ if status != hci.HCI_SUCCESS:
5046
+ del self.big_syncs[big_handle]
5047
+ logger.debug('Unable to create BIG Sync %d', big_handle)
5048
+ big_sync.state = BigSync.State.TERMINATED
5049
+ big_sync.emit(BigSync.Event.ESTABLISHMENT_FAILURE, status)
5050
+ return
5051
+
5052
+ big_sync.transport_latency_big = transport_latency_big
5053
+ big_sync.nse = nse
5054
+ big_sync.bn = bn
5055
+ big_sync.pto = pto
5056
+ big_sync.irc = irc
5057
+ big_sync.max_pdu = max_pdu
5058
+ big_sync.iso_interval = iso_interval
5059
+ big_sync.bis_links = [
5060
+ BisLink(handle=handle, big=big_sync) for handle in bis_handles
5061
+ ]
5062
+ big_sync.state = BigSync.State.ACTIVE
5063
+
5064
+ for bis_link in big_sync.bis_links:
5065
+ self.bis_links[bis_link.handle] = bis_link
5066
+ big_sync.emit(BigSync.Event.ESTABLISHMENT)
5067
+
5068
+ @host_event_handler
5069
+ def on_big_sync_lost(self, big_handle: int, reason: int) -> None:
5070
+ if not (big_sync := self.big_syncs.pop(big_handle, None)):
5071
+ logger.warning('BIG %d not found', big_handle)
5072
+ return
5073
+
5074
+ for bis_link in big_sync.bis_links:
5075
+ self.bis_links.pop(bis_link.handle, None)
5076
+ big_sync.state = BigSync.State.TERMINATED
5077
+ big_sync.emit(BigSync.Event.TERMINATION, reason)
5078
+
4236
5079
  def _complete_le_extended_advertising_connection(
4237
5080
  self, connection: Connection, advertising_set: AdvertisingSet
4238
5081
  ) -> None:
@@ -4879,6 +5722,8 @@ class Device(CompositeEventEmitter):
4879
5722
  def on_iso_packet(self, handle: int, packet: hci.HCI_IsoDataPacket) -> None:
4880
5723
  if (cis_link := self.cis_links.get(handle)) and cis_link.sink:
4881
5724
  cis_link.sink(packet)
5725
+ elif (bis_link := self.bis_links.get(handle)) and bis_link.sink:
5726
+ bis_link.sink(packet)
4882
5727
 
4883
5728
  @host_event_handler
4884
5729
  @with_connection_from_handle
@@ -4994,6 +5839,106 @@ class Device(CompositeEventEmitter):
4994
5839
  )
4995
5840
  connection.emit('connection_data_length_change')
4996
5841
 
5842
+ @host_event_handler
5843
+ def on_cs_remote_supported_capabilities(
5844
+ self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
5845
+ ):
5846
+ if not (connection := self.lookup_connection(event.connection_handle)):
5847
+ return
5848
+
5849
+ if event.status != hci.HCI_SUCCESS:
5850
+ connection.emit('channel_sounding_capabilities_failure', event.status)
5851
+ return
5852
+
5853
+ capabilities = ChannelSoundingCapabilities(
5854
+ num_config_supported=event.num_config_supported,
5855
+ max_consecutive_procedures_supported=event.max_consecutive_procedures_supported,
5856
+ num_antennas_supported=event.num_antennas_supported,
5857
+ max_antenna_paths_supported=event.max_antenna_paths_supported,
5858
+ roles_supported=event.roles_supported,
5859
+ modes_supported=event.modes_supported,
5860
+ rtt_capability=event.rtt_capability,
5861
+ rtt_aa_only_n=event.rtt_aa_only_n,
5862
+ rtt_sounding_n=event.rtt_sounding_n,
5863
+ rtt_random_payload_n=event.rtt_random_payload_n,
5864
+ nadm_sounding_capability=event.nadm_sounding_capability,
5865
+ nadm_random_capability=event.nadm_random_capability,
5866
+ cs_sync_phys_supported=event.cs_sync_phys_supported,
5867
+ subfeatures_supported=event.subfeatures_supported,
5868
+ t_ip1_times_supported=event.t_ip1_times_supported,
5869
+ t_ip2_times_supported=event.t_ip2_times_supported,
5870
+ t_fcs_times_supported=event.t_fcs_times_supported,
5871
+ t_pm_times_supported=event.t_pm_times_supported,
5872
+ t_sw_time_supported=event.t_sw_time_supported,
5873
+ tx_snr_capability=event.tx_snr_capability,
5874
+ )
5875
+ connection.emit('channel_sounding_capabilities', capabilities)
5876
+
5877
+ @host_event_handler
5878
+ def on_cs_config(self, event: hci.HCI_LE_CS_Config_Complete_Event):
5879
+ if not (connection := self.lookup_connection(event.connection_handle)):
5880
+ return
5881
+
5882
+ if event.status != hci.HCI_SUCCESS:
5883
+ connection.emit('channel_sounding_config_failure', event.status)
5884
+ return
5885
+ if event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.CREATED:
5886
+ config = ChannelSoundingConfig(
5887
+ config_id=event.config_id,
5888
+ main_mode_type=event.main_mode_type,
5889
+ sub_mode_type=event.sub_mode_type,
5890
+ min_main_mode_steps=event.min_main_mode_steps,
5891
+ max_main_mode_steps=event.max_main_mode_steps,
5892
+ main_mode_repetition=event.main_mode_repetition,
5893
+ mode_0_steps=event.mode_0_steps,
5894
+ role=event.role,
5895
+ rtt_type=event.rtt_type,
5896
+ cs_sync_phy=event.cs_sync_phy,
5897
+ channel_map=event.channel_map,
5898
+ channel_map_repetition=event.channel_map_repetition,
5899
+ channel_selection_type=event.channel_selection_type,
5900
+ ch3c_shape=event.ch3c_shape,
5901
+ ch3c_jump=event.ch3c_jump,
5902
+ reserved=event.reserved,
5903
+ t_ip1_time=event.t_ip1_time,
5904
+ t_ip2_time=event.t_ip2_time,
5905
+ t_fcs_time=event.t_fcs_time,
5906
+ t_pm_time=event.t_pm_time,
5907
+ )
5908
+ connection.cs_configs[event.config_id] = config
5909
+ connection.emit('channel_sounding_config', config)
5910
+ elif event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.REMOVED:
5911
+ try:
5912
+ config = connection.cs_configs.pop(event.config_id)
5913
+ connection.emit('channel_sounding_config_removed', config.config_id)
5914
+ except KeyError:
5915
+ logger.error('Removing unknown config %d', event.config_id)
5916
+
5917
+ @host_event_handler
5918
+ def on_cs_procedure(self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event):
5919
+ if not (connection := self.lookup_connection(event.connection_handle)):
5920
+ return
5921
+
5922
+ if event.status != hci.HCI_SUCCESS:
5923
+ connection.emit('channel_sounding_procedure_failure', event.status)
5924
+ return
5925
+
5926
+ procedure = ChannelSoundingProcedure(
5927
+ config_id=event.config_id,
5928
+ state=event.state,
5929
+ tone_antenna_config_selection=event.tone_antenna_config_selection,
5930
+ selected_tx_power=event.selected_tx_power,
5931
+ subevent_len=event.subevent_len,
5932
+ subevents_per_event=event.subevents_per_event,
5933
+ subevent_interval=event.subevent_interval,
5934
+ event_interval=event.event_interval,
5935
+ procedure_interval=event.procedure_interval,
5936
+ procedure_count=event.procedure_count,
5937
+ max_procedure_len=event.max_procedure_len,
5938
+ )
5939
+ connection.cs_procedures[procedure.config_id] = procedure
5940
+ connection.emit('channel_sounding_procedure', procedure)
5941
+
4997
5942
  # [Classic only]
4998
5943
  @host_event_handler
4999
5944
  @with_connection_from_address
@@ -5051,14 +5996,14 @@ class Device(CompositeEventEmitter):
5051
5996
  if att_pdu.op_code & 1:
5052
5997
  if connection.gatt_client is None:
5053
5998
  logger.warning(
5054
- color('no GATT client for connection 0x{connection_handle:04X}')
5999
+ 'No GATT client for connection 0x%04X', connection.handle
5055
6000
  )
5056
6001
  return
5057
6002
  connection.gatt_client.on_gatt_pdu(att_pdu)
5058
6003
  else:
5059
6004
  if connection.gatt_server is None:
5060
6005
  logger.warning(
5061
- color('no GATT server for connection 0x{connection_handle:04X}')
6006
+ 'No GATT server for connection 0x%04X', connection.handle
5062
6007
  )
5063
6008
  return
5064
6009
  connection.gatt_server.on_gatt_pdu(connection, att_pdu)