bumble 0.0.180__py3-none-any.whl → 0.0.182__py3-none-any.whl

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