bumble 0.0.180__py3-none-any.whl → 0.0.181__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
@@ -21,7 +21,7 @@ import functools
21
21
  import json
22
22
  import asyncio
23
23
  import logging
24
- from contextlib import asynccontextmanager, AsyncExitStack
24
+ from contextlib import asynccontextmanager, AsyncExitStack, closing
25
25
  from dataclasses import dataclass
26
26
  from collections.abc import Iterable
27
27
  from typing import (
@@ -49,6 +49,7 @@ from .hci import (
49
49
  HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
50
50
  HCI_CENTRAL_ROLE,
51
51
  HCI_COMMAND_STATUS_PENDING,
52
+ HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE,
52
53
  HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
53
54
  HCI_DISPLAY_YES_NO_IO_CAPABILITY,
54
55
  HCI_DISPLAY_ONLY_IO_CAPABILITY,
@@ -85,29 +86,35 @@ from .hci import (
85
86
  HCI_Constant,
86
87
  HCI_Create_Connection_Cancel_Command,
87
88
  HCI_Create_Connection_Command,
89
+ HCI_Create_Connection_Command,
88
90
  HCI_Disconnect_Command,
89
91
  HCI_Encryption_Change_Event,
90
92
  HCI_Error,
91
93
  HCI_IO_Capability_Request_Reply_Command,
92
94
  HCI_Inquiry_Cancel_Command,
93
95
  HCI_Inquiry_Command,
96
+ HCI_IsoDataPacket,
97
+ HCI_LE_Accept_CIS_Request_Command,
94
98
  HCI_LE_Add_Device_To_Resolving_List_Command,
95
99
  HCI_LE_Advertising_Report_Event,
96
100
  HCI_LE_Clear_Resolving_List_Command,
97
101
  HCI_LE_Connection_Update_Command,
98
102
  HCI_LE_Create_Connection_Cancel_Command,
99
103
  HCI_LE_Create_Connection_Command,
104
+ HCI_LE_Create_CIS_Command,
100
105
  HCI_LE_Enable_Encryption_Command,
101
106
  HCI_LE_Extended_Advertising_Report_Event,
102
107
  HCI_LE_Extended_Create_Connection_Command,
103
108
  HCI_LE_Rand_Command,
104
109
  HCI_LE_Read_PHY_Command,
110
+ HCI_LE_Reject_CIS_Request_Command,
105
111
  HCI_LE_Remove_Advertising_Set_Command,
106
112
  HCI_LE_Set_Address_Resolution_Enable_Command,
107
113
  HCI_LE_Set_Advertising_Data_Command,
108
114
  HCI_LE_Set_Advertising_Enable_Command,
109
115
  HCI_LE_Set_Advertising_Parameters_Command,
110
116
  HCI_LE_Set_Advertising_Set_Random_Address_Command,
117
+ HCI_LE_Set_CIG_Parameters_Command,
111
118
  HCI_LE_Set_Data_Length_Command,
112
119
  HCI_LE_Set_Default_PHY_Command,
113
120
  HCI_LE_Set_Extended_Scan_Enable_Command,
@@ -116,6 +123,7 @@ from .hci import (
116
123
  HCI_LE_Set_Extended_Advertising_Data_Command,
117
124
  HCI_LE_Set_Extended_Advertising_Enable_Command,
118
125
  HCI_LE_Set_Extended_Advertising_Parameters_Command,
126
+ HCI_LE_Set_Host_Feature_Command,
119
127
  HCI_LE_Set_PHY_Command,
120
128
  HCI_LE_Set_Random_Address_Command,
121
129
  HCI_LE_Set_Scan_Enable_Command,
@@ -130,6 +138,7 @@ from .hci import (
130
138
  HCI_Switch_Role_Command,
131
139
  HCI_Set_Connection_Encryption_Command,
132
140
  HCI_StatusError,
141
+ HCI_SynchronousDataPacket,
133
142
  HCI_User_Confirmation_Request_Negative_Reply_Command,
134
143
  HCI_User_Confirmation_Request_Reply_Command,
135
144
  HCI_User_Passkey_Request_Negative_Reply_Command,
@@ -161,6 +170,7 @@ from .core import (
161
170
  from .utils import (
162
171
  AsyncRunner,
163
172
  CompositeEventEmitter,
173
+ EventWatcher,
164
174
  setup_event_forwarding,
165
175
  composite_listener,
166
176
  deprecated,
@@ -427,6 +437,38 @@ class AdvertisingType(IntEnum):
427
437
  )
428
438
 
429
439
 
440
+ # -----------------------------------------------------------------------------
441
+ @dataclass
442
+ class LegacyAdvertiser:
443
+ device: Device
444
+ advertising_type: AdvertisingType
445
+ own_address_type: OwnAddressType
446
+ auto_restart: bool
447
+ advertising_data: Optional[bytes]
448
+ scan_response_data: Optional[bytes]
449
+
450
+ async def stop(self) -> None:
451
+ await self.device.stop_legacy_advertising()
452
+
453
+
454
+ # -----------------------------------------------------------------------------
455
+ @dataclass
456
+ class ExtendedAdvertiser(CompositeEventEmitter):
457
+ device: Device
458
+ handle: int
459
+ advertising_properties: HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties
460
+ own_address_type: OwnAddressType
461
+ auto_restart: bool
462
+ advertising_data: Optional[bytes]
463
+ scan_response_data: Optional[bytes]
464
+
465
+ def __post_init__(self) -> None:
466
+ super().__init__()
467
+
468
+ async def stop(self) -> None:
469
+ await self.device.stop_extended_advertising(self.handle)
470
+
471
+
430
472
  # -----------------------------------------------------------------------------
431
473
  class LePhyOptions:
432
474
  # Coded PHY preference
@@ -592,6 +634,46 @@ class ConnectionParametersPreferences:
592
634
  ConnectionParametersPreferences.default = ConnectionParametersPreferences()
593
635
 
594
636
 
637
+ # -----------------------------------------------------------------------------
638
+ @dataclass
639
+ class ScoLink(CompositeEventEmitter):
640
+ device: Device
641
+ acl_connection: Connection
642
+ handle: int
643
+ link_type: int
644
+
645
+ def __post_init__(self):
646
+ super().__init__()
647
+
648
+ async def disconnect(
649
+ self, reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
650
+ ) -> None:
651
+ await self.device.disconnect(self, reason)
652
+
653
+
654
+ # -----------------------------------------------------------------------------
655
+ @dataclass
656
+ class CisLink(CompositeEventEmitter):
657
+ class State(IntEnum):
658
+ PENDING = 0
659
+ ESTABLISHED = 1
660
+
661
+ device: Device
662
+ acl_connection: Connection # Based ACL connection
663
+ handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
664
+ cis_id: int # CIS ID assigned by Central device
665
+ cig_id: int # CIG ID assigned by Central device
666
+ state: State = State.PENDING
667
+
668
+ def __post_init__(self):
669
+ super().__init__()
670
+
671
+ async def disconnect(
672
+ self, reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
673
+ ) -> None:
674
+ await self.device.disconnect(self, reason)
675
+
676
+
595
677
  # -----------------------------------------------------------------------------
596
678
  class Connection(CompositeEventEmitter):
597
679
  device: Device
@@ -608,6 +690,9 @@ class Connection(CompositeEventEmitter):
608
690
  gatt_client: gatt_client.Client
609
691
  pairing_peer_io_capability: Optional[int]
610
692
  pairing_peer_authentication_requirements: Optional[int]
693
+ advertiser_after_disconnection: Union[
694
+ LegacyAdvertiser, ExtendedAdvertiser, None
695
+ ] = None
611
696
 
612
697
  @composite_listener
613
698
  class Listener:
@@ -870,6 +955,7 @@ class DeviceConfiguration:
870
955
  self.keystore = None
871
956
  self.gatt_services: List[Dict[str, Any]] = []
872
957
  self.address_resolution_offload = False
958
+ self.cis_enabled = False
873
959
 
874
960
  def load_from_dict(self, config: Dict[str, Any]) -> None:
875
961
  # Load simple properties
@@ -905,6 +991,7 @@ class DeviceConfiguration:
905
991
  self.address_resolution_offload = config.get(
906
992
  'address_resolution_offload', self.address_resolution_offload
907
993
  )
994
+ self.cis_enabled = config.get('cis_enabled', self.cis_enabled)
908
995
 
909
996
  # Load or synthesize an IRK
910
997
  irk = config.get('irk')
@@ -1011,7 +1098,11 @@ class Device(CompositeEventEmitter):
1011
1098
  ]
1012
1099
  advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
1013
1100
  config: DeviceConfiguration
1014
- extended_advertising_handles: Set[int]
1101
+ legacy_advertiser: Optional[LegacyAdvertiser]
1102
+ extended_advertisers: Dict[int, ExtendedAdvertiser]
1103
+ sco_links: Dict[int, ScoLink]
1104
+ cis_links: Dict[int, CisLink]
1105
+ _pending_cis: Dict[int, Tuple[int, int]]
1015
1106
 
1016
1107
  @composite_listener
1017
1108
  class Listener:
@@ -1086,10 +1177,7 @@ class Device(CompositeEventEmitter):
1086
1177
 
1087
1178
  self._host = None
1088
1179
  self.powered_on = False
1089
- self.advertising = False
1090
- self.advertising_type = None
1091
1180
  self.auto_restart_inquiry = True
1092
- self.auto_restart_advertising = False
1093
1181
  self.command_timeout = 10 # seconds
1094
1182
  self.gatt_server = gatt_server.Server(self)
1095
1183
  self.sdp_server = sdp.Server(self)
@@ -1104,16 +1192,19 @@ class Device(CompositeEventEmitter):
1104
1192
  self.disconnecting = False
1105
1193
  self.connections = {} # Connections, by connection handle
1106
1194
  self.pending_connections = {} # Connections, by BD address (BR/EDR only)
1195
+ self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
1196
+ self.cis_links = {} # CisLinks, by connection handle (LE only)
1197
+ self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
1107
1198
  self.classic_enabled = False
1108
1199
  self.inquiry_response = None
1109
1200
  self.address_resolver = None
1110
1201
  self.classic_pending_accepts = {
1111
1202
  Address.ANY: []
1112
1203
  } # Futures, by BD address OR [Futures] for Address.ANY
1113
- self.extended_advertising_handles = set()
1204
+ self.legacy_advertiser = None
1205
+ self.extended_advertisers = {}
1114
1206
 
1115
1207
  # Own address type cache
1116
- self.advertising_own_address_type = None
1117
1208
  self.connect_own_address_type = None
1118
1209
 
1119
1210
  # Use the initial config or a default
@@ -1133,6 +1224,7 @@ class Device(CompositeEventEmitter):
1133
1224
  self.le_enabled = config.le_enabled
1134
1225
  self.classic_enabled = config.classic_enabled
1135
1226
  self.le_simultaneous_enabled = config.le_simultaneous_enabled
1227
+ self.cis_enabled = config.cis_enabled
1136
1228
  self.classic_sc_enabled = config.classic_sc_enabled
1137
1229
  self.classic_ssp_enabled = config.classic_ssp_enabled
1138
1230
  self.classic_smp_enabled = config.classic_smp_enabled
@@ -1373,7 +1465,7 @@ class Device(CompositeEventEmitter):
1373
1465
  await self.host.reset()
1374
1466
 
1375
1467
  # Try to get the public address from the controller
1376
- response = await self.send_command(HCI_Read_BD_ADDR_Command()) # type: ignore[call-arg]
1468
+ response = await self.send_command(HCI_Read_BD_ADDR_Command())
1377
1469
  if response.return_parameters.status == HCI_SUCCESS:
1378
1470
  logger.debug(
1379
1471
  color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')
@@ -1396,7 +1488,7 @@ class Device(CompositeEventEmitter):
1396
1488
  HCI_Write_LE_Host_Support_Command(
1397
1489
  le_supported_host=int(self.le_enabled),
1398
1490
  simultaneous_le_host=int(self.le_simultaneous_enabled),
1399
- ) # type: ignore[call-arg]
1491
+ )
1400
1492
  )
1401
1493
 
1402
1494
  if self.le_enabled:
@@ -1406,7 +1498,7 @@ class Device(CompositeEventEmitter):
1406
1498
  if self.host.supports_command(HCI_LE_RAND_COMMAND):
1407
1499
  # Get 8 random bytes
1408
1500
  response = await self.send_command(
1409
- HCI_LE_Rand_Command(), check_result=True # type: ignore[call-arg]
1501
+ HCI_LE_Rand_Command(), check_result=True
1410
1502
  )
1411
1503
 
1412
1504
  # Ensure the address bytes can be a static random address
@@ -1427,7 +1519,7 @@ class Device(CompositeEventEmitter):
1427
1519
  await self.send_command(
1428
1520
  HCI_LE_Set_Random_Address_Command(
1429
1521
  random_address=self.random_address
1430
- ), # type: ignore[call-arg]
1522
+ ),
1431
1523
  check_result=True,
1432
1524
  )
1433
1525
 
@@ -1440,25 +1532,35 @@ class Device(CompositeEventEmitter):
1440
1532
  await self.send_command(
1441
1533
  HCI_LE_Set_Address_Resolution_Enable_Command(
1442
1534
  address_resolution_enable=1
1443
- ) # type: ignore[call-arg]
1535
+ )
1536
+ )
1537
+
1538
+ if self.cis_enabled:
1539
+ await self.send_command(
1540
+ HCI_LE_Set_Host_Feature_Command(
1541
+ bit_number=(
1542
+ HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE
1543
+ ),
1544
+ bit_value=1,
1545
+ )
1444
1546
  )
1445
1547
 
1446
1548
  if self.classic_enabled:
1447
1549
  await self.send_command(
1448
- HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8')) # type: ignore[call-arg]
1550
+ HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8'))
1449
1551
  )
1450
1552
  await self.send_command(
1451
- HCI_Write_Class_Of_Device_Command(class_of_device=self.class_of_device) # type: ignore[call-arg]
1553
+ HCI_Write_Class_Of_Device_Command(class_of_device=self.class_of_device)
1452
1554
  )
1453
1555
  await self.send_command(
1454
1556
  HCI_Write_Simple_Pairing_Mode_Command(
1455
1557
  simple_pairing_mode=int(self.classic_ssp_enabled)
1456
- ) # type: ignore[call-arg]
1558
+ )
1457
1559
  )
1458
1560
  await self.send_command(
1459
1561
  HCI_Write_Secure_Connections_Host_Support_Command(
1460
1562
  secure_connections_host_support=int(self.classic_sc_enabled)
1461
- ) # type: ignore[call-arg]
1563
+ )
1462
1564
  )
1463
1565
  await self.set_connectable(self.connectable)
1464
1566
  await self.set_discoverable(self.discoverable)
@@ -1482,7 +1584,7 @@ class Device(CompositeEventEmitter):
1482
1584
  self.address_resolver = smp.AddressResolver(resolving_keys)
1483
1585
 
1484
1586
  if self.address_resolution_offload:
1485
- await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
1587
+ await self.send_command(HCI_LE_Clear_Resolving_List_Command())
1486
1588
 
1487
1589
  for irk, address in resolving_keys:
1488
1590
  await self.send_command(
@@ -1491,7 +1593,7 @@ class Device(CompositeEventEmitter):
1491
1593
  peer_identity_address=address,
1492
1594
  peer_irk=irk,
1493
1595
  local_irk=self.irk,
1494
- ) # type: ignore[call-arg]
1596
+ )
1495
1597
  )
1496
1598
 
1497
1599
  def supports_le_feature(self, feature):
@@ -1510,6 +1612,7 @@ class Device(CompositeEventEmitter):
1510
1612
 
1511
1613
  return self.host.supports_le_feature(feature_map[phy])
1512
1614
 
1615
+ @deprecated("Please use start_legacy_advertising.")
1513
1616
  async def start_advertising(
1514
1617
  self,
1515
1618
  advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
@@ -1517,16 +1620,50 @@ class Device(CompositeEventEmitter):
1517
1620
  own_address_type: int = OwnAddressType.RANDOM,
1518
1621
  auto_restart: bool = False,
1519
1622
  ) -> None:
1623
+ await self.start_legacy_advertising(
1624
+ advertising_type=advertising_type,
1625
+ target=target,
1626
+ own_address_type=OwnAddressType(own_address_type),
1627
+ auto_restart=auto_restart,
1628
+ )
1629
+
1630
+ async def start_legacy_advertising(
1631
+ self,
1632
+ advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
1633
+ target: Optional[Address] = None,
1634
+ own_address_type: OwnAddressType = OwnAddressType.RANDOM,
1635
+ auto_restart: bool = False,
1636
+ advertising_data: Optional[bytes] = None,
1637
+ scan_response_data: Optional[bytes] = None,
1638
+ ) -> LegacyAdvertiser:
1639
+ """Starts an legacy advertisement.
1640
+
1641
+ Args:
1642
+ advertising_type: Advertising type passed to HCI_LE_Set_Advertising_Parameters_Command.
1643
+ target: Directed advertising target. Directed type should be set in advertising_type arg.
1644
+ own_address_type: own address type to use in the advertising.
1645
+ auto_restart: whether the advertisement will be restarted after disconnection.
1646
+ scan_response_data: raw scan response.
1647
+ advertising_data: raw advertising data.
1648
+
1649
+ Returns:
1650
+ LegacyAdvertiser object containing the metadata of advertisement.
1651
+ """
1652
+ if self.extended_advertisers:
1653
+ logger.warning(
1654
+ 'Trying to start Legacy and Extended Advertising at the same time!'
1655
+ )
1656
+
1520
1657
  # If we're advertising, stop first
1521
- if self.advertising:
1658
+ if self.legacy_advertiser:
1522
1659
  await self.stop_advertising()
1523
1660
 
1524
1661
  # Set/update the advertising data if the advertising type allows it
1525
1662
  if advertising_type.has_data:
1526
1663
  await self.send_command(
1527
1664
  HCI_LE_Set_Advertising_Data_Command(
1528
- advertising_data=self.advertising_data
1529
- ), # type: ignore[call-arg]
1665
+ advertising_data=advertising_data or self.advertising_data or b''
1666
+ ),
1530
1667
  check_result=True,
1531
1668
  )
1532
1669
 
@@ -1534,8 +1671,10 @@ class Device(CompositeEventEmitter):
1534
1671
  if advertising_type.is_scannable:
1535
1672
  await self.send_command(
1536
1673
  HCI_LE_Set_Scan_Response_Data_Command(
1537
- scan_response_data=self.scan_response_data
1538
- ), # type: ignore[call-arg]
1674
+ scan_response_data=scan_response_data
1675
+ or self.scan_response_data
1676
+ or b''
1677
+ ),
1539
1678
  check_result=True,
1540
1679
  )
1541
1680
 
@@ -1561,55 +1700,67 @@ class Device(CompositeEventEmitter):
1561
1700
  peer_address=peer_address,
1562
1701
  advertising_channel_map=7,
1563
1702
  advertising_filter_policy=0,
1564
- ), # type: ignore[call-arg]
1703
+ ),
1565
1704
  check_result=True,
1566
1705
  )
1567
1706
 
1568
1707
  # Enable advertising
1569
1708
  await self.send_command(
1570
- HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1), # type: ignore[call-arg]
1709
+ HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1),
1571
1710
  check_result=True,
1572
1711
  )
1573
1712
 
1574
- self.advertising_type = advertising_type
1575
- self.advertising_own_address_type = own_address_type
1576
- self.advertising = True
1577
- self.auto_restart_advertising = auto_restart
1713
+ self.legacy_advertiser = LegacyAdvertiser(
1714
+ device=self,
1715
+ advertising_type=advertising_type,
1716
+ own_address_type=own_address_type,
1717
+ auto_restart=auto_restart,
1718
+ advertising_data=advertising_data,
1719
+ scan_response_data=scan_response_data,
1720
+ )
1721
+ return self.legacy_advertiser
1578
1722
 
1723
+ @deprecated("Please use stop_legacy_advertising.")
1579
1724
  async def stop_advertising(self) -> None:
1725
+ await self.stop_legacy_advertising()
1726
+
1727
+ async def stop_legacy_advertising(self) -> None:
1580
1728
  # Disable advertising
1581
- if self.advertising:
1729
+ if self.legacy_advertiser:
1582
1730
  await self.send_command(
1583
- HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0), # type: ignore[call-arg]
1731
+ HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0),
1584
1732
  check_result=True,
1585
1733
  )
1586
1734
 
1587
- self.advertising_type = None
1588
- self.advertising_own_address_type = None
1589
- self.advertising = False
1590
- self.auto_restart_advertising = False
1735
+ self.legacy_advertiser = None
1591
1736
 
1592
1737
  @experimental('Extended Advertising is still experimental - Might be changed soon.')
1593
1738
  async def start_extended_advertising(
1594
1739
  self,
1595
1740
  advertising_properties: HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties = HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties.CONNECTABLE_ADVERTISING,
1596
1741
  target: Address = Address.ANY,
1597
- own_address_type: int = OwnAddressType.RANDOM,
1598
- scan_response: Optional[bytes] = None,
1742
+ own_address_type: OwnAddressType = OwnAddressType.RANDOM,
1743
+ auto_restart: bool = True,
1599
1744
  advertising_data: Optional[bytes] = None,
1600
- ) -> int:
1745
+ scan_response_data: Optional[bytes] = None,
1746
+ ) -> ExtendedAdvertiser:
1601
1747
  """Starts an extended advertising set.
1602
1748
 
1603
1749
  Args:
1604
1750
  advertising_properties: Properties to pass in HCI_LE_Set_Extended_Advertising_Parameters_Command
1605
1751
  target: Directed advertising target. Directed property should be set in advertising_properties arg.
1606
1752
  own_address_type: own address type to use in the advertising.
1607
- scan_response: raw scan response. When a non-none value is set, HCI_LE_Set_Extended_Scan_Response_Data_Command will be sent.
1753
+ auto_restart: whether the advertisement will be restarted after disconnection.
1608
1754
  advertising_data: raw advertising data. When a non-none value is set, HCI_LE_Set_Advertising_Set_Random_Address_Command will be sent.
1755
+ scan_response_data: raw scan response. When a non-none value is set, HCI_LE_Set_Extended_Scan_Response_Data_Command will be sent.
1609
1756
 
1610
1757
  Returns:
1611
- Handle of the new advertising set.
1758
+ ExtendedAdvertiser object containing the metadata of advertisement.
1612
1759
  """
1760
+ if self.legacy_advertiser:
1761
+ logger.warning(
1762
+ 'Trying to start Legacy and Extended Advertising at the same time!'
1763
+ )
1613
1764
 
1614
1765
  adv_handle = -1
1615
1766
  # Find a free handle
@@ -1617,7 +1768,7 @@ class Device(CompositeEventEmitter):
1617
1768
  DEVICE_MIN_EXTENDED_ADVERTISING_SET_HANDLE,
1618
1769
  DEVICE_MAX_EXTENDED_ADVERTISING_SET_HANDLE + 1,
1619
1770
  ):
1620
- if i not in self.extended_advertising_handles:
1771
+ if i not in self.extended_advertisers:
1621
1772
  adv_handle = i
1622
1773
  break
1623
1774
 
@@ -1647,7 +1798,7 @@ class Device(CompositeEventEmitter):
1647
1798
  secondary_advertising_phy=1, # LE 1M
1648
1799
  advertising_sid=0,
1649
1800
  scan_request_notification_enable=0,
1650
- ), # type: ignore[call-arg]
1801
+ ),
1651
1802
  check_result=True,
1652
1803
  )
1653
1804
 
@@ -1659,19 +1810,19 @@ class Device(CompositeEventEmitter):
1659
1810
  operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
1660
1811
  fragment_preference=0x01, # Should not fragment
1661
1812
  advertising_data=advertising_data,
1662
- ), # type: ignore[call-arg]
1813
+ ),
1663
1814
  check_result=True,
1664
1815
  )
1665
1816
 
1666
1817
  # Set the scan response if present
1667
- if scan_response is not None:
1818
+ if scan_response_data is not None:
1668
1819
  await self.send_command(
1669
1820
  HCI_LE_Set_Extended_Scan_Response_Data_Command(
1670
1821
  advertising_handle=adv_handle,
1671
1822
  operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
1672
1823
  fragment_preference=0x01, # Should not fragment
1673
- scan_response_data=scan_response,
1674
- ), # type: ignore[call-arg]
1824
+ scan_response_data=scan_response_data,
1825
+ ),
1675
1826
  check_result=True,
1676
1827
  )
1677
1828
 
@@ -1683,7 +1834,7 @@ class Device(CompositeEventEmitter):
1683
1834
  HCI_LE_Set_Advertising_Set_Random_Address_Command(
1684
1835
  advertising_handle=adv_handle,
1685
1836
  random_address=self.random_address,
1686
- ), # type: ignore[call-arg]
1837
+ ),
1687
1838
  check_result=True,
1688
1839
  )
1689
1840
 
@@ -1694,19 +1845,27 @@ class Device(CompositeEventEmitter):
1694
1845
  advertising_handles=[adv_handle],
1695
1846
  durations=[0], # Forever
1696
1847
  max_extended_advertising_events=[0], # Infinite
1697
- ), # type: ignore[call-arg]
1848
+ ),
1698
1849
  check_result=True,
1699
1850
  )
1700
1851
  except HCI_Error as error:
1701
1852
  # When any step fails, cleanup the advertising handle.
1702
1853
  await self.send_command(
1703
- HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle), # type: ignore[call-arg]
1854
+ HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle),
1704
1855
  check_result=False,
1705
1856
  )
1706
1857
  raise error
1707
1858
 
1708
- self.extended_advertising_handles.add(adv_handle)
1709
- return adv_handle
1859
+ advertiser = self.extended_advertisers[adv_handle] = ExtendedAdvertiser(
1860
+ device=self,
1861
+ handle=adv_handle,
1862
+ advertising_properties=advertising_properties,
1863
+ own_address_type=own_address_type,
1864
+ auto_restart=auto_restart,
1865
+ advertising_data=advertising_data,
1866
+ scan_response_data=scan_response_data,
1867
+ )
1868
+ return advertiser
1710
1869
 
1711
1870
  @experimental('Extended Advertising is still experimental - Might be changed soon.')
1712
1871
  async def stop_extended_advertising(self, adv_handle: int) -> None:
@@ -1722,19 +1881,19 @@ class Device(CompositeEventEmitter):
1722
1881
  advertising_handles=[adv_handle],
1723
1882
  durations=[0],
1724
1883
  max_extended_advertising_events=[0],
1725
- ), # type: ignore[call-arg]
1884
+ ),
1726
1885
  check_result=True,
1727
1886
  )
1728
1887
  # Remove advertising set
1729
1888
  await self.send_command(
1730
- HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle), # type: ignore[call-arg]
1889
+ HCI_LE_Remove_Advertising_Set_Command(advertising_handle=adv_handle),
1731
1890
  check_result=True,
1732
1891
  )
1733
- self.extended_advertising_handles.remove(adv_handle)
1892
+ del self.extended_advertisers[adv_handle]
1734
1893
 
1735
1894
  @property
1736
1895
  def is_advertising(self):
1737
- return self.advertising
1896
+ return self.legacy_advertiser or self.extended_advertisers
1738
1897
 
1739
1898
  async def start_scanning(
1740
1899
  self,
@@ -1795,7 +1954,7 @@ class Device(CompositeEventEmitter):
1795
1954
  scan_types=[scan_type] * scanning_phy_count,
1796
1955
  scan_intervals=[int(scan_window / 0.625)] * scanning_phy_count,
1797
1956
  scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
1798
- ), # type: ignore[call-arg]
1957
+ ),
1799
1958
  check_result=True,
1800
1959
  )
1801
1960
 
@@ -1806,7 +1965,7 @@ class Device(CompositeEventEmitter):
1806
1965
  filter_duplicates=1 if filter_duplicates else 0,
1807
1966
  duration=0, # TODO allow other values
1808
1967
  period=0, # TODO allow other values
1809
- ), # type: ignore[call-arg]
1968
+ ),
1810
1969
  check_result=True,
1811
1970
  )
1812
1971
  else:
@@ -1824,7 +1983,7 @@ class Device(CompositeEventEmitter):
1824
1983
  le_scan_window=int(scan_window / 0.625),
1825
1984
  own_address_type=own_address_type,
1826
1985
  scanning_filter_policy=HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY,
1827
- ), # type: ignore[call-arg]
1986
+ ),
1828
1987
  check_result=True,
1829
1988
  )
1830
1989
 
@@ -1832,7 +1991,7 @@ class Device(CompositeEventEmitter):
1832
1991
  await self.send_command(
1833
1992
  HCI_LE_Set_Scan_Enable_Command(
1834
1993
  le_scan_enable=1, filter_duplicates=1 if filter_duplicates else 0
1835
- ), # type: ignore[call-arg]
1994
+ ),
1836
1995
  check_result=True,
1837
1996
  )
1838
1997
 
@@ -1845,12 +2004,12 @@ class Device(CompositeEventEmitter):
1845
2004
  await self.send_command(
1846
2005
  HCI_LE_Set_Extended_Scan_Enable_Command(
1847
2006
  enable=0, filter_duplicates=0, duration=0, period=0
1848
- ), # type: ignore[call-arg]
2007
+ ),
1849
2008
  check_result=True,
1850
2009
  )
1851
2010
  else:
1852
2011
  await self.send_command(
1853
- HCI_LE_Set_Scan_Enable_Command(le_scan_enable=0, filter_duplicates=0), # type: ignore[call-arg]
2012
+ HCI_LE_Set_Scan_Enable_Command(le_scan_enable=0, filter_duplicates=0),
1854
2013
  check_result=True,
1855
2014
  )
1856
2015
 
@@ -1870,7 +2029,7 @@ class Device(CompositeEventEmitter):
1870
2029
 
1871
2030
  async def start_discovery(self, auto_restart: bool = True) -> None:
1872
2031
  await self.send_command(
1873
- HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE), # type: ignore[call-arg]
2032
+ HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE),
1874
2033
  check_result=True,
1875
2034
  )
1876
2035
 
@@ -1879,7 +2038,7 @@ class Device(CompositeEventEmitter):
1879
2038
  lap=HCI_GENERAL_INQUIRY_LAP,
1880
2039
  inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
1881
2040
  num_responses=0, # Unlimited number of responses.
1882
- ) # type: ignore[call-arg]
2041
+ )
1883
2042
  )
1884
2043
  if response.status != HCI_Command_Status_Event.PENDING:
1885
2044
  self.discovering = False
@@ -1890,7 +2049,7 @@ class Device(CompositeEventEmitter):
1890
2049
 
1891
2050
  async def stop_discovery(self) -> None:
1892
2051
  if self.discovering:
1893
- await self.send_command(HCI_Inquiry_Cancel_Command(), check_result=True) # type: ignore[call-arg]
2052
+ await self.send_command(HCI_Inquiry_Cancel_Command(), check_result=True)
1894
2053
  self.auto_restart_inquiry = True
1895
2054
  self.discovering = False
1896
2055
 
@@ -1938,7 +2097,7 @@ class Device(CompositeEventEmitter):
1938
2097
  await self.send_command(
1939
2098
  HCI_Write_Extended_Inquiry_Response_Command(
1940
2099
  fec_required=0, extended_inquiry_response=self.inquiry_response
1941
- ), # type: ignore[call-arg]
2100
+ ),
1942
2101
  check_result=True,
1943
2102
  )
1944
2103
  await self.set_scan_enable(
@@ -2127,7 +2286,7 @@ class Device(CompositeEventEmitter):
2127
2286
  supervision_timeouts=supervision_timeouts,
2128
2287
  min_ce_lengths=min_ce_lengths,
2129
2288
  max_ce_lengths=max_ce_lengths,
2130
- ) # type: ignore[call-arg]
2289
+ )
2131
2290
  )
2132
2291
  else:
2133
2292
  if HCI_LE_1M_PHY not in connection_parameters_preferences:
@@ -2156,7 +2315,7 @@ class Device(CompositeEventEmitter):
2156
2315
  supervision_timeout=int(prefs.supervision_timeout / 10),
2157
2316
  min_ce_length=int(prefs.min_ce_length / 0.625),
2158
2317
  max_ce_length=int(prefs.max_ce_length / 0.625),
2159
- ) # type: ignore[call-arg]
2318
+ )
2160
2319
  )
2161
2320
  else:
2162
2321
  # Save pending connection
@@ -2173,7 +2332,7 @@ class Device(CompositeEventEmitter):
2173
2332
  clock_offset=0x0000,
2174
2333
  allow_role_switch=0x01,
2175
2334
  reserved=0,
2176
- ) # type: ignore[call-arg]
2335
+ )
2177
2336
  )
2178
2337
 
2179
2338
  if result.status != HCI_Command_Status_Event.PENDING:
@@ -2192,10 +2351,10 @@ class Device(CompositeEventEmitter):
2192
2351
  )
2193
2352
  except asyncio.TimeoutError:
2194
2353
  if transport == BT_LE_TRANSPORT:
2195
- await self.send_command(HCI_LE_Create_Connection_Cancel_Command()) # type: ignore[call-arg]
2354
+ await self.send_command(HCI_LE_Create_Connection_Cancel_Command())
2196
2355
  else:
2197
2356
  await self.send_command(
2198
- HCI_Create_Connection_Cancel_Command(bd_addr=peer_address) # type: ignore[call-arg]
2357
+ HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
2199
2358
  )
2200
2359
 
2201
2360
  try:
@@ -2309,7 +2468,7 @@ class Device(CompositeEventEmitter):
2309
2468
  try:
2310
2469
  # Accept connection request
2311
2470
  await self.send_command(
2312
- HCI_Accept_Connection_Request_Command(bd_addr=peer_address, role=role) # type: ignore[call-arg]
2471
+ HCI_Accept_Connection_Request_Command(bd_addr=peer_address, role=role)
2313
2472
  )
2314
2473
 
2315
2474
  # Wait for connection complete
@@ -2366,7 +2525,9 @@ class Device(CompositeEventEmitter):
2366
2525
  check_result=True,
2367
2526
  )
2368
2527
 
2369
- async def disconnect(self, connection, reason):
2528
+ async def disconnect(
2529
+ self, connection: Union[Connection, ScoLink, CisLink], reason: int
2530
+ ) -> None:
2370
2531
  # Create a future so that we can wait for the disconnection's result
2371
2532
  pending_disconnection = asyncio.get_running_loop().create_future()
2372
2533
  connection.on('disconnection', pending_disconnection.set_result)
@@ -2405,7 +2566,7 @@ class Device(CompositeEventEmitter):
2405
2566
  connection_handle=connection.handle,
2406
2567
  tx_octets=tx_octets,
2407
2568
  tx_time=tx_time,
2408
- ), # type: ignore[call-arg]
2569
+ ),
2409
2570
  check_result=True,
2410
2571
  )
2411
2572
 
@@ -2451,7 +2612,7 @@ class Device(CompositeEventEmitter):
2451
2612
  supervision_timeout=supervision_timeout,
2452
2613
  min_ce_length=min_ce_length,
2453
2614
  max_ce_length=max_ce_length,
2454
- ) # type: ignore[call-arg]
2615
+ )
2455
2616
  )
2456
2617
  if result.status != HCI_Command_Status_Event.PENDING:
2457
2618
  raise HCI_StatusError(result)
@@ -2779,7 +2940,7 @@ class Device(CompositeEventEmitter):
2779
2940
 
2780
2941
  try:
2781
2942
  result = await self.send_command(
2782
- HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role) # type: ignore[call-arg]
2943
+ HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role)
2783
2944
  )
2784
2945
  if result.status != HCI_COMMAND_STATUS_PENDING:
2785
2946
  logger.warning(
@@ -2821,7 +2982,7 @@ class Device(CompositeEventEmitter):
2821
2982
  page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R2,
2822
2983
  reserved=0,
2823
2984
  clock_offset=0, # TODO investigate non-0 values
2824
- ) # type: ignore[call-arg]
2985
+ )
2825
2986
  )
2826
2987
 
2827
2988
  if result.status != HCI_COMMAND_STATUS_PENDING:
@@ -2837,6 +2998,150 @@ class Device(CompositeEventEmitter):
2837
2998
  self.remove_listener('remote_name', handler)
2838
2999
  self.remove_listener('remote_name_failure', failure_handler)
2839
3000
 
3001
+ # [LE only]
3002
+ @experimental('Only for testing.')
3003
+ async def setup_cig(
3004
+ self,
3005
+ cig_id: int,
3006
+ cis_id: List[int],
3007
+ sdu_interval: Tuple[int, int],
3008
+ framing: int,
3009
+ max_sdu: Tuple[int, int],
3010
+ retransmission_number: int,
3011
+ max_transport_latency: Tuple[int, int],
3012
+ ) -> List[int]:
3013
+ """Sends HCI_LE_Set_CIG_Parameters_Command.
3014
+
3015
+ Args:
3016
+ cig_id: CIG_ID.
3017
+ cis_id: CID ID list.
3018
+ sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
3019
+ framing: Un-framing(0) or Framing(1).
3020
+ max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
3021
+ retransmission_number: retransmission_number.
3022
+ max_transport_latency: Max transport latencies of
3023
+ (Central->Peripheral, Peripheral->Cental).
3024
+
3025
+ Returns:
3026
+ List of created CIS handles corresponding to the same order of [cid_id].
3027
+ """
3028
+ num_cis = len(cis_id)
3029
+
3030
+ response = await self.send_command(
3031
+ HCI_LE_Set_CIG_Parameters_Command(
3032
+ cig_id=cig_id,
3033
+ sdu_interval_c_to_p=sdu_interval[0],
3034
+ sdu_interval_p_to_c=sdu_interval[1],
3035
+ worst_case_sca=0x00, # 251-500 ppm
3036
+ packing=0x00, # Sequential
3037
+ framing=framing,
3038
+ max_transport_latency_c_to_p=max_transport_latency[0],
3039
+ max_transport_latency_p_to_c=max_transport_latency[1],
3040
+ cis_id=cis_id,
3041
+ max_sdu_c_to_p=[max_sdu[0]] * num_cis,
3042
+ max_sdu_p_to_c=[max_sdu[1]] * num_cis,
3043
+ phy_c_to_p=[HCI_LE_2M_PHY] * num_cis,
3044
+ phy_p_to_c=[HCI_LE_2M_PHY] * num_cis,
3045
+ rtn_c_to_p=[retransmission_number] * num_cis,
3046
+ rtn_p_to_c=[retransmission_number] * num_cis,
3047
+ ),
3048
+ check_result=True,
3049
+ )
3050
+
3051
+ # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
3052
+ # Server, so here it only provides a basic functionality for testing.
3053
+ cis_handles = response.return_parameters.connection_handle[:]
3054
+ for id, cis_handle in zip(cis_id, cis_handles):
3055
+ self._pending_cis[cis_handle] = (id, cig_id)
3056
+
3057
+ return cis_handles
3058
+
3059
+ # [LE only]
3060
+ @experimental('Only for testing.')
3061
+ async def create_cis(self, cis_acl_pairs: List[Tuple[int, int]]) -> List[CisLink]:
3062
+ for cis_handle, acl_handle in cis_acl_pairs:
3063
+ acl_connection = self.lookup_connection(acl_handle)
3064
+ assert acl_connection
3065
+ cis_id, cig_id = self._pending_cis.pop(cis_handle)
3066
+ self.cis_links[cis_handle] = CisLink(
3067
+ device=self,
3068
+ acl_connection=acl_connection,
3069
+ handle=cis_handle,
3070
+ cis_id=cis_id,
3071
+ cig_id=cig_id,
3072
+ )
3073
+
3074
+ result = await self.send_command(
3075
+ HCI_LE_Create_CIS_Command(
3076
+ cis_connection_handle=[p[0] for p in cis_acl_pairs],
3077
+ acl_connection_handle=[p[1] for p in cis_acl_pairs],
3078
+ ),
3079
+ )
3080
+ if result.status != HCI_COMMAND_STATUS_PENDING:
3081
+ logger.warning(
3082
+ 'HCI_LE_Create_CIS_Command failed: '
3083
+ f'{HCI_Constant.error_name(result.status)}'
3084
+ )
3085
+ raise HCI_StatusError(result)
3086
+
3087
+ pending_cis_establishments: Dict[int, asyncio.Future[CisLink]] = {}
3088
+ for cis_handle, _ in cis_acl_pairs:
3089
+ pending_cis_establishments[
3090
+ cis_handle
3091
+ ] = asyncio.get_running_loop().create_future()
3092
+
3093
+ with closing(EventWatcher()) as watcher:
3094
+
3095
+ @watcher.on(self, 'cis_establishment')
3096
+ def on_cis_establishment(cis_link: CisLink) -> None:
3097
+ if pending_future := pending_cis_establishments.get(
3098
+ cis_link.handle, None
3099
+ ):
3100
+ pending_future.set_result(cis_link)
3101
+
3102
+ return await asyncio.gather(*pending_cis_establishments.values())
3103
+
3104
+ # [LE only]
3105
+ @experimental('Only for testing.')
3106
+ async def accept_cis_request(self, handle: int) -> CisLink:
3107
+ result = await self.send_command(
3108
+ HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
3109
+ )
3110
+ if result.status != HCI_COMMAND_STATUS_PENDING:
3111
+ logger.warning(
3112
+ 'HCI_LE_Accept_CIS_Request_Command failed: '
3113
+ f'{HCI_Constant.error_name(result.status)}'
3114
+ )
3115
+ raise HCI_StatusError(result)
3116
+
3117
+ pending_cis_establishment = asyncio.get_running_loop().create_future()
3118
+
3119
+ with closing(EventWatcher()) as watcher:
3120
+
3121
+ @watcher.on(self, 'cis_establishment')
3122
+ def on_cis_establishment(cis_link: CisLink) -> None:
3123
+ if cis_link.handle == handle:
3124
+ pending_cis_establishment.set_result(cis_link)
3125
+
3126
+ return await pending_cis_establishment
3127
+
3128
+ # [LE only]
3129
+ @experimental('Only for testing.')
3130
+ async def reject_cis_request(
3131
+ self,
3132
+ handle: int,
3133
+ reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
3134
+ ) -> None:
3135
+ result = await self.send_command(
3136
+ HCI_LE_Reject_CIS_Request_Command(connection_handle=handle, reason=reason),
3137
+ )
3138
+ if result.status != HCI_COMMAND_STATUS_PENDING:
3139
+ logger.warning(
3140
+ 'HCI_LE_Reject_CIS_Request_Command failed: '
3141
+ f'{HCI_Constant.error_name(result.status)}'
3142
+ )
3143
+ raise HCI_StatusError(result)
3144
+
2840
3145
  @host_event_handler
2841
3146
  def on_flush(self):
2842
3147
  self.emit('flush')
@@ -2929,13 +3234,18 @@ class Device(CompositeEventEmitter):
2929
3234
  # Guess which own address type is used for this connection.
2930
3235
  # This logic is somewhat correct but may need to be improved
2931
3236
  # when multiple advertising are run simultaneously.
3237
+ advertiser = None
2932
3238
  if self.connect_own_address_type is not None:
2933
3239
  own_address_type = self.connect_own_address_type
3240
+ elif self.legacy_advertiser:
3241
+ own_address_type = self.legacy_advertiser.own_address_type
3242
+ # Store advertiser for restarting - it's only required for legacy, since
3243
+ # extended advertisement produces HCI_Advertising_Set_Terminated.
3244
+ if self.legacy_advertiser.auto_restart:
3245
+ advertiser = self.legacy_advertiser
2934
3246
  else:
2935
- own_address_type = self.advertising_own_address_type
2936
-
2937
- # We are no longer advertising
2938
- self.advertising = False
3247
+ # For extended advertisement, determining own address type later.
3248
+ own_address_type = OwnAddressType.RANDOM
2939
3249
 
2940
3250
  if own_address_type in (
2941
3251
  OwnAddressType.PUBLIC,
@@ -2957,6 +3267,7 @@ class Device(CompositeEventEmitter):
2957
3267
  connection_parameters,
2958
3268
  ConnectionPHY(HCI_LE_1M_PHY, HCI_LE_1M_PHY),
2959
3269
  )
3270
+ connection.advertiser_after_disconnection = advertiser
2960
3271
  self.connections[connection_handle] = connection
2961
3272
 
2962
3273
  # If supported, read which PHY we're connected with before
@@ -2988,10 +3299,10 @@ class Device(CompositeEventEmitter):
2988
3299
  # For directed advertising, this means a timeout
2989
3300
  if (
2990
3301
  transport == BT_LE_TRANSPORT
2991
- and self.advertising
2992
- and self.advertising_type.is_directed
3302
+ and self.legacy_advertiser
3303
+ and self.legacy_advertiser.advertising_type.is_directed
2993
3304
  ):
2994
- self.advertising = False
3305
+ self.legacy_advertiser = None
2995
3306
 
2996
3307
  # Notify listeners
2997
3308
  error = core.ConnectionError(
@@ -3041,30 +3352,49 @@ class Device(CompositeEventEmitter):
3041
3352
  )
3042
3353
 
3043
3354
  @host_event_handler
3044
- @with_connection_from_handle
3045
- def on_disconnection(self, connection, reason):
3046
- logger.debug(
3047
- f'*** Disconnection: [0x{connection.handle:04X}] '
3048
- f'{connection.peer_address} as {connection.role_name}, reason={reason}'
3049
- )
3050
- connection.emit('disconnection', reason)
3051
-
3052
- # Remove the connection from the map
3053
- del self.connections[connection.handle]
3054
-
3055
- # Cleanup subsystems that maintain per-connection state
3056
- self.gatt_server.on_disconnection(connection)
3057
-
3058
- # Restart advertising if auto-restart is enabled
3059
- if self.auto_restart_advertising:
3060
- logger.debug('restarting advertising')
3061
- self.abort_on(
3062
- 'flush',
3063
- self.start_advertising(
3064
- advertising_type=self.advertising_type,
3065
- own_address_type=self.advertising_own_address_type,
3066
- auto_restart=True,
3067
- ),
3355
+ def on_disconnection(self, connection_handle: int, reason: int) -> None:
3356
+ if connection := self.connections.pop(connection_handle, None):
3357
+ logger.debug(
3358
+ f'*** Disconnection: [0x{connection.handle:04X}] '
3359
+ f'{connection.peer_address} as {connection.role_name}, reason={reason}'
3360
+ )
3361
+ connection.emit('disconnection', reason)
3362
+
3363
+ # Cleanup subsystems that maintain per-connection state
3364
+ self.gatt_server.on_disconnection(connection)
3365
+
3366
+ # Restart advertising if auto-restart is enabled
3367
+ if advertiser := connection.advertiser_after_disconnection:
3368
+ logger.debug('restarting advertising')
3369
+ if isinstance(advertiser, LegacyAdvertiser):
3370
+ self.abort_on(
3371
+ 'flush',
3372
+ self.start_legacy_advertising(
3373
+ advertising_type=advertiser.advertising_type,
3374
+ own_address_type=advertiser.own_address_type,
3375
+ advertising_data=advertiser.advertising_data,
3376
+ scan_response_data=advertiser.scan_response_data,
3377
+ auto_restart=True,
3378
+ ),
3379
+ )
3380
+ elif isinstance(advertiser, ExtendedAdvertiser):
3381
+ self.abort_on(
3382
+ 'flush',
3383
+ self.start_extended_advertising(
3384
+ advertising_properties=advertiser.advertising_properties,
3385
+ own_address_type=advertiser.own_address_type,
3386
+ advertising_data=advertiser.advertising_data,
3387
+ scan_response_data=advertiser.scan_response_data,
3388
+ auto_restart=True,
3389
+ ),
3390
+ )
3391
+ elif sco_link := self.sco_links.pop(connection_handle, None):
3392
+ sco_link.emit('disconnection', reason)
3393
+ elif cis_link := self.cis_links.pop(connection_handle, None):
3394
+ cis_link.emit('disconnection', reason)
3395
+ else:
3396
+ logger.error(
3397
+ f'*** Unknown disconnection handle=0x{connection_handle}, reason={reason} ***'
3068
3398
  )
3069
3399
 
3070
3400
  @host_event_handler
@@ -3215,7 +3545,7 @@ class Device(CompositeEventEmitter):
3215
3545
  try:
3216
3546
  if await connection.abort_on('disconnection', method()):
3217
3547
  await self.host.send_command(
3218
- HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
3548
+ HCI_User_Confirmation_Request_Reply_Command(
3219
3549
  bd_addr=connection.peer_address
3220
3550
  )
3221
3551
  )
@@ -3224,7 +3554,7 @@ class Device(CompositeEventEmitter):
3224
3554
  logger.warning(f'exception while confirming: {error}')
3225
3555
 
3226
3556
  await self.host.send_command(
3227
- HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
3557
+ HCI_User_Confirmation_Request_Negative_Reply_Command(
3228
3558
  bd_addr=connection.peer_address
3229
3559
  )
3230
3560
  )
@@ -3245,7 +3575,7 @@ class Device(CompositeEventEmitter):
3245
3575
  )
3246
3576
  if number is not None:
3247
3577
  await self.host.send_command(
3248
- HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
3578
+ HCI_User_Passkey_Request_Reply_Command(
3249
3579
  bd_addr=connection.peer_address, numeric_value=number
3250
3580
  )
3251
3581
  )
@@ -3254,7 +3584,7 @@ class Device(CompositeEventEmitter):
3254
3584
  logger.warning(f'exception while asking for pass-key: {error}')
3255
3585
 
3256
3586
  await self.host.send_command(
3257
- HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
3587
+ HCI_User_Passkey_Request_Negative_Reply_Command(
3258
3588
  bd_addr=connection.peer_address
3259
3589
  )
3260
3590
  )
@@ -3343,6 +3673,131 @@ class Device(CompositeEventEmitter):
3343
3673
  connection.emit('remote_name_failure', error)
3344
3674
  self.emit('remote_name_failure', address, error)
3345
3675
 
3676
+ # [Classic only]
3677
+ @host_event_handler
3678
+ @with_connection_from_address
3679
+ @experimental('Only for testing.')
3680
+ def on_sco_connection(
3681
+ self, acl_connection: Connection, sco_handle: int, link_type: int
3682
+ ) -> None:
3683
+ logger.debug(
3684
+ f'*** SCO connected: {acl_connection.peer_address}, '
3685
+ f'sco_handle=[0x{sco_handle:04X}], '
3686
+ f'link_type=[0x{link_type:02X}] ***'
3687
+ )
3688
+ sco_link = self.sco_links[sco_handle] = ScoLink(
3689
+ device=self,
3690
+ acl_connection=acl_connection,
3691
+ handle=sco_handle,
3692
+ link_type=link_type,
3693
+ )
3694
+ self.emit('sco_connection', sco_link)
3695
+
3696
+ # [Classic only]
3697
+ @host_event_handler
3698
+ @with_connection_from_address
3699
+ @experimental('Only for testing.')
3700
+ def on_sco_connection_failure(
3701
+ self, acl_connection: Connection, status: int
3702
+ ) -> None:
3703
+ logger.debug(f'*** SCO connection failure: {acl_connection.peer_address}***')
3704
+ self.emit('sco_connection_failure')
3705
+
3706
+ # [Classic only]
3707
+ @host_event_handler
3708
+ @experimental('Only for testing')
3709
+ def on_sco_packet(self, sco_handle: int, packet: HCI_SynchronousDataPacket) -> None:
3710
+ if sco_link := self.sco_links.get(sco_handle, None):
3711
+ sco_link.emit('pdu', packet)
3712
+
3713
+ # [LE only]
3714
+ @host_event_handler
3715
+ @experimental('Only for testing')
3716
+ def on_advertising_set_termination(
3717
+ self,
3718
+ status: int,
3719
+ advertising_handle: int,
3720
+ connection_handle: int,
3721
+ ) -> None:
3722
+ if status == HCI_SUCCESS:
3723
+ connection = self.lookup_connection(connection_handle)
3724
+ if advertiser := self.extended_advertisers.pop(advertising_handle, None):
3725
+ if connection:
3726
+ if advertiser.auto_restart:
3727
+ connection.advertiser_after_disconnection = advertiser
3728
+ if advertiser.own_address_type in (
3729
+ OwnAddressType.PUBLIC,
3730
+ OwnAddressType.RESOLVABLE_OR_PUBLIC,
3731
+ ):
3732
+ connection.self_address = self.public_address
3733
+ else:
3734
+ connection.self_address = self.random_address
3735
+ advertiser.emit('termination', status)
3736
+
3737
+ # [LE only]
3738
+ @host_event_handler
3739
+ @with_connection_from_handle
3740
+ @experimental('Only for testing')
3741
+ def on_cis_request(
3742
+ self,
3743
+ acl_connection: Connection,
3744
+ cis_handle: int,
3745
+ cig_id: int,
3746
+ cis_id: int,
3747
+ ) -> None:
3748
+ logger.debug(
3749
+ f'*** CIS Request '
3750
+ f'acl_handle=[0x{acl_connection.handle:04X}]{acl_connection.peer_address}, '
3751
+ f'cis_handle=[0x{cis_handle:04X}], '
3752
+ f'cig_id=[0x{cig_id:02X}], '
3753
+ f'cis_id=[0x{cis_id:02X}] ***'
3754
+ )
3755
+ # LE_CIS_Established event doesn't provide info, so we must store them here.
3756
+ self.cis_links[cis_handle] = CisLink(
3757
+ device=self,
3758
+ acl_connection=acl_connection,
3759
+ handle=cis_handle,
3760
+ cig_id=cig_id,
3761
+ cis_id=cis_id,
3762
+ )
3763
+ self.emit('cis_request', acl_connection, cis_handle, cig_id, cis_id)
3764
+
3765
+ # [LE only]
3766
+ @host_event_handler
3767
+ @experimental('Only for testing')
3768
+ def on_cis_establishment(self, cis_handle: int) -> None:
3769
+ cis_link = self.cis_links[cis_handle]
3770
+ cis_link.state = CisLink.State.ESTABLISHED
3771
+
3772
+ assert cis_link.acl_connection
3773
+
3774
+ logger.debug(
3775
+ f'*** CIS Establishment '
3776
+ f'{cis_link.acl_connection.peer_address}, '
3777
+ f'cis_handle=[0x{cis_handle:04X}], '
3778
+ f'cig_id=[0x{cis_link.cig_id:02X}], '
3779
+ f'cis_id=[0x{cis_link.cis_id:02X}] ***'
3780
+ )
3781
+
3782
+ cis_link.emit('establishment')
3783
+ self.emit('cis_establishment', cis_link)
3784
+
3785
+ # [LE only]
3786
+ @host_event_handler
3787
+ @experimental('Only for testing')
3788
+ def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
3789
+ logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
3790
+ if cis_link := self.cis_links.pop(cis_handle, None):
3791
+ cis_link.emit('establishment_failure')
3792
+ self.emit('cis_establishment_failure', cis_handle, status)
3793
+
3794
+ # [LE only]
3795
+ @host_event_handler
3796
+ @experimental('Only for testing')
3797
+ def on_iso_packet(self, handle: int, packet: HCI_IsoDataPacket) -> None:
3798
+ if cis_link := self.cis_links.get(handle, None):
3799
+ cis_link.emit('pdu', packet)
3800
+
3346
3801
  @host_event_handler
3347
3802
  @with_connection_from_handle
3348
3803
  def on_connection_encryption_change(self, connection, encryption):