bumble 0.0.179__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,8 +21,9 @@ 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
+ from collections.abc import Iterable
26
27
  from typing import (
27
28
  Any,
28
29
  Callable,
@@ -32,6 +33,7 @@ from typing import (
32
33
  Optional,
33
34
  Tuple,
34
35
  Type,
36
+ TypeVar,
35
37
  Set,
36
38
  Union,
37
39
  cast,
@@ -47,6 +49,7 @@ from .hci import (
47
49
  HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
48
50
  HCI_CENTRAL_ROLE,
49
51
  HCI_COMMAND_STATUS_PENDING,
52
+ HCI_CONNECTED_ISOCHRONOUS_STREAM_LE_SUPPORTED_FEATURE,
50
53
  HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
51
54
  HCI_DISPLAY_YES_NO_IO_CAPABILITY,
52
55
  HCI_DISPLAY_ONLY_IO_CAPABILITY,
@@ -83,29 +86,35 @@ from .hci import (
83
86
  HCI_Constant,
84
87
  HCI_Create_Connection_Cancel_Command,
85
88
  HCI_Create_Connection_Command,
89
+ HCI_Create_Connection_Command,
86
90
  HCI_Disconnect_Command,
87
91
  HCI_Encryption_Change_Event,
88
92
  HCI_Error,
89
93
  HCI_IO_Capability_Request_Reply_Command,
90
94
  HCI_Inquiry_Cancel_Command,
91
95
  HCI_Inquiry_Command,
96
+ HCI_IsoDataPacket,
97
+ HCI_LE_Accept_CIS_Request_Command,
92
98
  HCI_LE_Add_Device_To_Resolving_List_Command,
93
99
  HCI_LE_Advertising_Report_Event,
94
100
  HCI_LE_Clear_Resolving_List_Command,
95
101
  HCI_LE_Connection_Update_Command,
96
102
  HCI_LE_Create_Connection_Cancel_Command,
97
103
  HCI_LE_Create_Connection_Command,
104
+ HCI_LE_Create_CIS_Command,
98
105
  HCI_LE_Enable_Encryption_Command,
99
106
  HCI_LE_Extended_Advertising_Report_Event,
100
107
  HCI_LE_Extended_Create_Connection_Command,
101
108
  HCI_LE_Rand_Command,
102
109
  HCI_LE_Read_PHY_Command,
110
+ HCI_LE_Reject_CIS_Request_Command,
103
111
  HCI_LE_Remove_Advertising_Set_Command,
104
112
  HCI_LE_Set_Address_Resolution_Enable_Command,
105
113
  HCI_LE_Set_Advertising_Data_Command,
106
114
  HCI_LE_Set_Advertising_Enable_Command,
107
115
  HCI_LE_Set_Advertising_Parameters_Command,
108
116
  HCI_LE_Set_Advertising_Set_Random_Address_Command,
117
+ HCI_LE_Set_CIG_Parameters_Command,
109
118
  HCI_LE_Set_Data_Length_Command,
110
119
  HCI_LE_Set_Default_PHY_Command,
111
120
  HCI_LE_Set_Extended_Scan_Enable_Command,
@@ -114,6 +123,7 @@ from .hci import (
114
123
  HCI_LE_Set_Extended_Advertising_Data_Command,
115
124
  HCI_LE_Set_Extended_Advertising_Enable_Command,
116
125
  HCI_LE_Set_Extended_Advertising_Parameters_Command,
126
+ HCI_LE_Set_Host_Feature_Command,
117
127
  HCI_LE_Set_PHY_Command,
118
128
  HCI_LE_Set_Random_Address_Command,
119
129
  HCI_LE_Set_Scan_Enable_Command,
@@ -128,6 +138,7 @@ from .hci import (
128
138
  HCI_Switch_Role_Command,
129
139
  HCI_Set_Connection_Encryption_Command,
130
140
  HCI_StatusError,
141
+ HCI_SynchronousDataPacket,
131
142
  HCI_User_Confirmation_Request_Negative_Reply_Command,
132
143
  HCI_User_Confirmation_Request_Reply_Command,
133
144
  HCI_User_Passkey_Request_Negative_Reply_Command,
@@ -159,6 +170,7 @@ from .core import (
159
170
  from .utils import (
160
171
  AsyncRunner,
161
172
  CompositeEventEmitter,
173
+ EventWatcher,
162
174
  setup_event_forwarding,
163
175
  composite_listener,
164
176
  deprecated,
@@ -425,6 +437,38 @@ class AdvertisingType(IntEnum):
425
437
  )
426
438
 
427
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
+
428
472
  # -----------------------------------------------------------------------------
429
473
  class LePhyOptions:
430
474
  # Coded PHY preference
@@ -440,8 +484,11 @@ class LePhyOptions:
440
484
 
441
485
 
442
486
  # -----------------------------------------------------------------------------
487
+ _PROXY_CLASS = TypeVar('_PROXY_CLASS', bound=gatt_client.ProfileServiceProxy)
488
+
489
+
443
490
  class Peer:
444
- def __init__(self, connection):
491
+ def __init__(self, connection: Connection) -> None:
445
492
  self.connection = connection
446
493
 
447
494
  # Create a GATT client for the connection
@@ -449,77 +496,113 @@ class Peer:
449
496
  connection.gatt_client = self.gatt_client
450
497
 
451
498
  @property
452
- def services(self):
499
+ def services(self) -> List[gatt_client.ServiceProxy]:
453
500
  return self.gatt_client.services
454
501
 
455
- async def request_mtu(self, mtu):
502
+ async def request_mtu(self, mtu: int) -> int:
456
503
  mtu = await self.gatt_client.request_mtu(mtu)
457
504
  self.connection.emit('connection_att_mtu_update')
458
505
  return mtu
459
506
 
460
- async def discover_service(self, uuid):
507
+ async def discover_service(
508
+ self, uuid: Union[core.UUID, str]
509
+ ) -> List[gatt_client.ServiceProxy]:
461
510
  return await self.gatt_client.discover_service(uuid)
462
511
 
463
- async def discover_services(self, uuids=()):
512
+ async def discover_services(
513
+ self, uuids: Iterable[core.UUID] = ()
514
+ ) -> List[gatt_client.ServiceProxy]:
464
515
  return await self.gatt_client.discover_services(uuids)
465
516
 
466
- async def discover_included_services(self, service):
517
+ async def discover_included_services(
518
+ self, service: gatt_client.ServiceProxy
519
+ ) -> List[gatt_client.ServiceProxy]:
467
520
  return await self.gatt_client.discover_included_services(service)
468
521
 
469
- async def discover_characteristics(self, uuids=(), service=None):
522
+ async def discover_characteristics(
523
+ self,
524
+ uuids: Iterable[Union[core.UUID, str]] = (),
525
+ service: Optional[gatt_client.ServiceProxy] = None,
526
+ ) -> List[gatt_client.CharacteristicProxy]:
470
527
  return await self.gatt_client.discover_characteristics(
471
528
  uuids=uuids, service=service
472
529
  )
473
530
 
474
531
  async def discover_descriptors(
475
- self, characteristic=None, start_handle=None, end_handle=None
532
+ self,
533
+ characteristic: Optional[gatt_client.CharacteristicProxy] = None,
534
+ start_handle: Optional[int] = None,
535
+ end_handle: Optional[int] = None,
476
536
  ):
477
537
  return await self.gatt_client.discover_descriptors(
478
538
  characteristic, start_handle, end_handle
479
539
  )
480
540
 
481
- async def discover_attributes(self):
541
+ async def discover_attributes(self) -> List[gatt_client.AttributeProxy]:
482
542
  return await self.gatt_client.discover_attributes()
483
543
 
484
- async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
544
+ async def subscribe(
545
+ self,
546
+ characteristic: gatt_client.CharacteristicProxy,
547
+ subscriber: Optional[Callable[[bytes], Any]] = None,
548
+ prefer_notify: bool = True,
549
+ ) -> None:
485
550
  return await self.gatt_client.subscribe(
486
551
  characteristic, subscriber, prefer_notify
487
552
  )
488
553
 
489
- async def unsubscribe(self, characteristic, subscriber=None):
554
+ async def unsubscribe(
555
+ self,
556
+ characteristic: gatt_client.CharacteristicProxy,
557
+ subscriber: Optional[Callable[[bytes], Any]] = None,
558
+ ) -> None:
490
559
  return await self.gatt_client.unsubscribe(characteristic, subscriber)
491
560
 
492
- async def read_value(self, attribute):
561
+ async def read_value(
562
+ self, attribute: Union[int, gatt_client.AttributeProxy]
563
+ ) -> bytes:
493
564
  return await self.gatt_client.read_value(attribute)
494
565
 
495
- async def write_value(self, attribute, value, with_response=False):
566
+ async def write_value(
567
+ self,
568
+ attribute: Union[int, gatt_client.AttributeProxy],
569
+ value: bytes,
570
+ with_response: bool = False,
571
+ ) -> None:
496
572
  return await self.gatt_client.write_value(attribute, value, with_response)
497
573
 
498
- async def read_characteristics_by_uuid(self, uuid, service=None):
574
+ async def read_characteristics_by_uuid(
575
+ self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
576
+ ) -> List[bytes]:
499
577
  return await self.gatt_client.read_characteristics_by_uuid(uuid, service)
500
578
 
501
- def get_services_by_uuid(self, uuid):
579
+ def get_services_by_uuid(self, uuid: core.UUID) -> List[gatt_client.ServiceProxy]:
502
580
  return self.gatt_client.get_services_by_uuid(uuid)
503
581
 
504
- def get_characteristics_by_uuid(self, uuid, service=None):
582
+ def get_characteristics_by_uuid(
583
+ self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
584
+ ) -> List[gatt_client.CharacteristicProxy]:
505
585
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
506
586
 
507
- def create_service_proxy(self, proxy_class):
508
- return proxy_class.from_client(self.gatt_client)
587
+ def create_service_proxy(self, proxy_class: Type[_PROXY_CLASS]) -> _PROXY_CLASS:
588
+ return cast(_PROXY_CLASS, proxy_class.from_client(self.gatt_client))
509
589
 
510
- async def discover_service_and_create_proxy(self, proxy_class):
590
+ async def discover_service_and_create_proxy(
591
+ self, proxy_class: Type[_PROXY_CLASS]
592
+ ) -> Optional[_PROXY_CLASS]:
511
593
  # Discover the first matching service and its characteristics
512
594
  services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
513
595
  if services:
514
596
  service = services[0]
515
597
  await service.discover_characteristics()
516
598
  return self.create_service_proxy(proxy_class)
599
+ return None
517
600
 
518
- async def sustain(self, timeout=None):
601
+ async def sustain(self, timeout: Optional[float] = None) -> None:
519
602
  await self.connection.sustain(timeout)
520
603
 
521
604
  # [Classic only]
522
- async def request_name(self):
605
+ async def request_name(self) -> str:
523
606
  return await self.connection.request_remote_name()
524
607
 
525
608
  async def __aenter__(self):
@@ -532,7 +615,7 @@ class Peer:
532
615
  async def __aexit__(self, exc_type, exc_value, traceback):
533
616
  pass
534
617
 
535
- def __str__(self):
618
+ def __str__(self) -> str:
536
619
  return f'{self.connection.peer_address} as {self.connection.role_name}'
537
620
 
538
621
 
@@ -551,6 +634,46 @@ class ConnectionParametersPreferences:
551
634
  ConnectionParametersPreferences.default = ConnectionParametersPreferences()
552
635
 
553
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
+
554
677
  # -----------------------------------------------------------------------------
555
678
  class Connection(CompositeEventEmitter):
556
679
  device: Device
@@ -567,6 +690,9 @@ class Connection(CompositeEventEmitter):
567
690
  gatt_client: gatt_client.Client
568
691
  pairing_peer_io_capability: Optional[int]
569
692
  pairing_peer_authentication_requirements: Optional[int]
693
+ advertiser_after_disconnection: Union[
694
+ LegacyAdvertiser, ExtendedAdvertiser, None
695
+ ] = None
570
696
 
571
697
  @composite_listener
572
698
  class Listener:
@@ -732,7 +858,7 @@ class Connection(CompositeEventEmitter):
732
858
  async def switch_role(self, role: int) -> None:
733
859
  return await self.device.switch_role(self, role)
734
860
 
735
- async def sustain(self, timeout=None):
861
+ async def sustain(self, timeout: Optional[float] = None) -> None:
736
862
  """Idles the current task waiting for a disconnect or timeout"""
737
863
 
738
864
  abort = asyncio.get_running_loop().create_future()
@@ -829,6 +955,7 @@ class DeviceConfiguration:
829
955
  self.keystore = None
830
956
  self.gatt_services: List[Dict[str, Any]] = []
831
957
  self.address_resolution_offload = False
958
+ self.cis_enabled = False
832
959
 
833
960
  def load_from_dict(self, config: Dict[str, Any]) -> None:
834
961
  # Load simple properties
@@ -864,6 +991,7 @@ class DeviceConfiguration:
864
991
  self.address_resolution_offload = config.get(
865
992
  'address_resolution_offload', self.address_resolution_offload
866
993
  )
994
+ self.cis_enabled = config.get('cis_enabled', self.cis_enabled)
867
995
 
868
996
  # Load or synthesize an IRK
869
997
  irk = config.get('irk')
@@ -970,7 +1098,11 @@ class Device(CompositeEventEmitter):
970
1098
  ]
971
1099
  advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
972
1100
  config: DeviceConfiguration
973
- 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]]
974
1106
 
975
1107
  @composite_listener
976
1108
  class Listener:
@@ -1045,10 +1177,7 @@ class Device(CompositeEventEmitter):
1045
1177
 
1046
1178
  self._host = None
1047
1179
  self.powered_on = False
1048
- self.advertising = False
1049
- self.advertising_type = None
1050
1180
  self.auto_restart_inquiry = True
1051
- self.auto_restart_advertising = False
1052
1181
  self.command_timeout = 10 # seconds
1053
1182
  self.gatt_server = gatt_server.Server(self)
1054
1183
  self.sdp_server = sdp.Server(self)
@@ -1063,16 +1192,19 @@ class Device(CompositeEventEmitter):
1063
1192
  self.disconnecting = False
1064
1193
  self.connections = {} # Connections, by connection handle
1065
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
1066
1198
  self.classic_enabled = False
1067
1199
  self.inquiry_response = None
1068
1200
  self.address_resolver = None
1069
1201
  self.classic_pending_accepts = {
1070
1202
  Address.ANY: []
1071
1203
  } # Futures, by BD address OR [Futures] for Address.ANY
1072
- self.extended_advertising_handles = set()
1204
+ self.legacy_advertiser = None
1205
+ self.extended_advertisers = {}
1073
1206
 
1074
1207
  # Own address type cache
1075
- self.advertising_own_address_type = None
1076
1208
  self.connect_own_address_type = None
1077
1209
 
1078
1210
  # Use the initial config or a default
@@ -1092,6 +1224,7 @@ class Device(CompositeEventEmitter):
1092
1224
  self.le_enabled = config.le_enabled
1093
1225
  self.classic_enabled = config.classic_enabled
1094
1226
  self.le_simultaneous_enabled = config.le_simultaneous_enabled
1227
+ self.cis_enabled = config.cis_enabled
1095
1228
  self.classic_sc_enabled = config.classic_sc_enabled
1096
1229
  self.classic_ssp_enabled = config.classic_ssp_enabled
1097
1230
  self.classic_smp_enabled = config.classic_smp_enabled
@@ -1332,7 +1465,7 @@ class Device(CompositeEventEmitter):
1332
1465
  await self.host.reset()
1333
1466
 
1334
1467
  # Try to get the public address from the controller
1335
- 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())
1336
1469
  if response.return_parameters.status == HCI_SUCCESS:
1337
1470
  logger.debug(
1338
1471
  color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')
@@ -1355,7 +1488,7 @@ class Device(CompositeEventEmitter):
1355
1488
  HCI_Write_LE_Host_Support_Command(
1356
1489
  le_supported_host=int(self.le_enabled),
1357
1490
  simultaneous_le_host=int(self.le_simultaneous_enabled),
1358
- ) # type: ignore[call-arg]
1491
+ )
1359
1492
  )
1360
1493
 
1361
1494
  if self.le_enabled:
@@ -1365,7 +1498,7 @@ class Device(CompositeEventEmitter):
1365
1498
  if self.host.supports_command(HCI_LE_RAND_COMMAND):
1366
1499
  # Get 8 random bytes
1367
1500
  response = await self.send_command(
1368
- HCI_LE_Rand_Command(), check_result=True # type: ignore[call-arg]
1501
+ HCI_LE_Rand_Command(), check_result=True
1369
1502
  )
1370
1503
 
1371
1504
  # Ensure the address bytes can be a static random address
@@ -1386,7 +1519,7 @@ class Device(CompositeEventEmitter):
1386
1519
  await self.send_command(
1387
1520
  HCI_LE_Set_Random_Address_Command(
1388
1521
  random_address=self.random_address
1389
- ), # type: ignore[call-arg]
1522
+ ),
1390
1523
  check_result=True,
1391
1524
  )
1392
1525
 
@@ -1399,25 +1532,35 @@ class Device(CompositeEventEmitter):
1399
1532
  await self.send_command(
1400
1533
  HCI_LE_Set_Address_Resolution_Enable_Command(
1401
1534
  address_resolution_enable=1
1402
- ) # 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
+ )
1403
1546
  )
1404
1547
 
1405
1548
  if self.classic_enabled:
1406
1549
  await self.send_command(
1407
- 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'))
1408
1551
  )
1409
1552
  await self.send_command(
1410
- 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)
1411
1554
  )
1412
1555
  await self.send_command(
1413
1556
  HCI_Write_Simple_Pairing_Mode_Command(
1414
1557
  simple_pairing_mode=int(self.classic_ssp_enabled)
1415
- ) # type: ignore[call-arg]
1558
+ )
1416
1559
  )
1417
1560
  await self.send_command(
1418
1561
  HCI_Write_Secure_Connections_Host_Support_Command(
1419
1562
  secure_connections_host_support=int(self.classic_sc_enabled)
1420
- ) # type: ignore[call-arg]
1563
+ )
1421
1564
  )
1422
1565
  await self.set_connectable(self.connectable)
1423
1566
  await self.set_discoverable(self.discoverable)
@@ -1441,7 +1584,7 @@ class Device(CompositeEventEmitter):
1441
1584
  self.address_resolver = smp.AddressResolver(resolving_keys)
1442
1585
 
1443
1586
  if self.address_resolution_offload:
1444
- 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())
1445
1588
 
1446
1589
  for irk, address in resolving_keys:
1447
1590
  await self.send_command(
@@ -1450,7 +1593,7 @@ class Device(CompositeEventEmitter):
1450
1593
  peer_identity_address=address,
1451
1594
  peer_irk=irk,
1452
1595
  local_irk=self.irk,
1453
- ) # type: ignore[call-arg]
1596
+ )
1454
1597
  )
1455
1598
 
1456
1599
  def supports_le_feature(self, feature):
@@ -1469,6 +1612,7 @@ class Device(CompositeEventEmitter):
1469
1612
 
1470
1613
  return self.host.supports_le_feature(feature_map[phy])
1471
1614
 
1615
+ @deprecated("Please use start_legacy_advertising.")
1472
1616
  async def start_advertising(
1473
1617
  self,
1474
1618
  advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
@@ -1476,16 +1620,50 @@ class Device(CompositeEventEmitter):
1476
1620
  own_address_type: int = OwnAddressType.RANDOM,
1477
1621
  auto_restart: bool = False,
1478
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
+
1479
1657
  # If we're advertising, stop first
1480
- if self.advertising:
1658
+ if self.legacy_advertiser:
1481
1659
  await self.stop_advertising()
1482
1660
 
1483
1661
  # Set/update the advertising data if the advertising type allows it
1484
1662
  if advertising_type.has_data:
1485
1663
  await self.send_command(
1486
1664
  HCI_LE_Set_Advertising_Data_Command(
1487
- advertising_data=self.advertising_data
1488
- ), # type: ignore[call-arg]
1665
+ advertising_data=advertising_data or self.advertising_data or b''
1666
+ ),
1489
1667
  check_result=True,
1490
1668
  )
1491
1669
 
@@ -1493,8 +1671,10 @@ class Device(CompositeEventEmitter):
1493
1671
  if advertising_type.is_scannable:
1494
1672
  await self.send_command(
1495
1673
  HCI_LE_Set_Scan_Response_Data_Command(
1496
- scan_response_data=self.scan_response_data
1497
- ), # type: ignore[call-arg]
1674
+ scan_response_data=scan_response_data
1675
+ or self.scan_response_data
1676
+ or b''
1677
+ ),
1498
1678
  check_result=True,
1499
1679
  )
1500
1680
 
@@ -1520,55 +1700,67 @@ class Device(CompositeEventEmitter):
1520
1700
  peer_address=peer_address,
1521
1701
  advertising_channel_map=7,
1522
1702
  advertising_filter_policy=0,
1523
- ), # type: ignore[call-arg]
1703
+ ),
1524
1704
  check_result=True,
1525
1705
  )
1526
1706
 
1527
1707
  # Enable advertising
1528
1708
  await self.send_command(
1529
- HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1), # type: ignore[call-arg]
1709
+ HCI_LE_Set_Advertising_Enable_Command(advertising_enable=1),
1530
1710
  check_result=True,
1531
1711
  )
1532
1712
 
1533
- self.advertising_type = advertising_type
1534
- self.advertising_own_address_type = own_address_type
1535
- self.advertising = True
1536
- 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
1537
1722
 
1723
+ @deprecated("Please use stop_legacy_advertising.")
1538
1724
  async def stop_advertising(self) -> None:
1725
+ await self.stop_legacy_advertising()
1726
+
1727
+ async def stop_legacy_advertising(self) -> None:
1539
1728
  # Disable advertising
1540
- if self.advertising:
1729
+ if self.legacy_advertiser:
1541
1730
  await self.send_command(
1542
- HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0), # type: ignore[call-arg]
1731
+ HCI_LE_Set_Advertising_Enable_Command(advertising_enable=0),
1543
1732
  check_result=True,
1544
1733
  )
1545
1734
 
1546
- self.advertising_type = None
1547
- self.advertising_own_address_type = None
1548
- self.advertising = False
1549
- self.auto_restart_advertising = False
1735
+ self.legacy_advertiser = None
1550
1736
 
1551
1737
  @experimental('Extended Advertising is still experimental - Might be changed soon.')
1552
1738
  async def start_extended_advertising(
1553
1739
  self,
1554
1740
  advertising_properties: HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties = HCI_LE_Set_Extended_Advertising_Parameters_Command.AdvertisingProperties.CONNECTABLE_ADVERTISING,
1555
1741
  target: Address = Address.ANY,
1556
- own_address_type: int = OwnAddressType.RANDOM,
1557
- scan_response: Optional[bytes] = None,
1742
+ own_address_type: OwnAddressType = OwnAddressType.RANDOM,
1743
+ auto_restart: bool = True,
1558
1744
  advertising_data: Optional[bytes] = None,
1559
- ) -> int:
1745
+ scan_response_data: Optional[bytes] = None,
1746
+ ) -> ExtendedAdvertiser:
1560
1747
  """Starts an extended advertising set.
1561
1748
 
1562
1749
  Args:
1563
1750
  advertising_properties: Properties to pass in HCI_LE_Set_Extended_Advertising_Parameters_Command
1564
1751
  target: Directed advertising target. Directed property should be set in advertising_properties arg.
1565
1752
  own_address_type: own address type to use in the advertising.
1566
- 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.
1567
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.
1568
1756
 
1569
1757
  Returns:
1570
- Handle of the new advertising set.
1758
+ ExtendedAdvertiser object containing the metadata of advertisement.
1571
1759
  """
1760
+ if self.legacy_advertiser:
1761
+ logger.warning(
1762
+ 'Trying to start Legacy and Extended Advertising at the same time!'
1763
+ )
1572
1764
 
1573
1765
  adv_handle = -1
1574
1766
  # Find a free handle
@@ -1576,7 +1768,7 @@ class Device(CompositeEventEmitter):
1576
1768
  DEVICE_MIN_EXTENDED_ADVERTISING_SET_HANDLE,
1577
1769
  DEVICE_MAX_EXTENDED_ADVERTISING_SET_HANDLE + 1,
1578
1770
  ):
1579
- if i not in self.extended_advertising_handles:
1771
+ if i not in self.extended_advertisers:
1580
1772
  adv_handle = i
1581
1773
  break
1582
1774
 
@@ -1606,7 +1798,7 @@ class Device(CompositeEventEmitter):
1606
1798
  secondary_advertising_phy=1, # LE 1M
1607
1799
  advertising_sid=0,
1608
1800
  scan_request_notification_enable=0,
1609
- ), # type: ignore[call-arg]
1801
+ ),
1610
1802
  check_result=True,
1611
1803
  )
1612
1804
 
@@ -1618,19 +1810,19 @@ class Device(CompositeEventEmitter):
1618
1810
  operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
1619
1811
  fragment_preference=0x01, # Should not fragment
1620
1812
  advertising_data=advertising_data,
1621
- ), # type: ignore[call-arg]
1813
+ ),
1622
1814
  check_result=True,
1623
1815
  )
1624
1816
 
1625
1817
  # Set the scan response if present
1626
- if scan_response is not None:
1818
+ if scan_response_data is not None:
1627
1819
  await self.send_command(
1628
1820
  HCI_LE_Set_Extended_Scan_Response_Data_Command(
1629
1821
  advertising_handle=adv_handle,
1630
1822
  operation=HCI_LE_Set_Extended_Advertising_Data_Command.Operation.COMPLETE_DATA,
1631
1823
  fragment_preference=0x01, # Should not fragment
1632
- scan_response_data=scan_response,
1633
- ), # type: ignore[call-arg]
1824
+ scan_response_data=scan_response_data,
1825
+ ),
1634
1826
  check_result=True,
1635
1827
  )
1636
1828
 
@@ -1642,7 +1834,7 @@ class Device(CompositeEventEmitter):
1642
1834
  HCI_LE_Set_Advertising_Set_Random_Address_Command(
1643
1835
  advertising_handle=adv_handle,
1644
1836
  random_address=self.random_address,
1645
- ), # type: ignore[call-arg]
1837
+ ),
1646
1838
  check_result=True,
1647
1839
  )
1648
1840
 
@@ -1653,19 +1845,27 @@ class Device(CompositeEventEmitter):
1653
1845
  advertising_handles=[adv_handle],
1654
1846
  durations=[0], # Forever
1655
1847
  max_extended_advertising_events=[0], # Infinite
1656
- ), # type: ignore[call-arg]
1848
+ ),
1657
1849
  check_result=True,
1658
1850
  )
1659
1851
  except HCI_Error as error:
1660
1852
  # When any step fails, cleanup the advertising handle.
1661
1853
  await self.send_command(
1662
- 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),
1663
1855
  check_result=False,
1664
1856
  )
1665
1857
  raise error
1666
1858
 
1667
- self.extended_advertising_handles.add(adv_handle)
1668
- 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
1669
1869
 
1670
1870
  @experimental('Extended Advertising is still experimental - Might be changed soon.')
1671
1871
  async def stop_extended_advertising(self, adv_handle: int) -> None:
@@ -1681,19 +1881,19 @@ class Device(CompositeEventEmitter):
1681
1881
  advertising_handles=[adv_handle],
1682
1882
  durations=[0],
1683
1883
  max_extended_advertising_events=[0],
1684
- ), # type: ignore[call-arg]
1884
+ ),
1685
1885
  check_result=True,
1686
1886
  )
1687
1887
  # Remove advertising set
1688
1888
  await self.send_command(
1689
- 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),
1690
1890
  check_result=True,
1691
1891
  )
1692
- self.extended_advertising_handles.remove(adv_handle)
1892
+ del self.extended_advertisers[adv_handle]
1693
1893
 
1694
1894
  @property
1695
1895
  def is_advertising(self):
1696
- return self.advertising
1896
+ return self.legacy_advertiser or self.extended_advertisers
1697
1897
 
1698
1898
  async def start_scanning(
1699
1899
  self,
@@ -1754,7 +1954,7 @@ class Device(CompositeEventEmitter):
1754
1954
  scan_types=[scan_type] * scanning_phy_count,
1755
1955
  scan_intervals=[int(scan_window / 0.625)] * scanning_phy_count,
1756
1956
  scan_windows=[int(scan_window / 0.625)] * scanning_phy_count,
1757
- ), # type: ignore[call-arg]
1957
+ ),
1758
1958
  check_result=True,
1759
1959
  )
1760
1960
 
@@ -1765,7 +1965,7 @@ class Device(CompositeEventEmitter):
1765
1965
  filter_duplicates=1 if filter_duplicates else 0,
1766
1966
  duration=0, # TODO allow other values
1767
1967
  period=0, # TODO allow other values
1768
- ), # type: ignore[call-arg]
1968
+ ),
1769
1969
  check_result=True,
1770
1970
  )
1771
1971
  else:
@@ -1783,7 +1983,7 @@ class Device(CompositeEventEmitter):
1783
1983
  le_scan_window=int(scan_window / 0.625),
1784
1984
  own_address_type=own_address_type,
1785
1985
  scanning_filter_policy=HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY,
1786
- ), # type: ignore[call-arg]
1986
+ ),
1787
1987
  check_result=True,
1788
1988
  )
1789
1989
 
@@ -1791,7 +1991,7 @@ class Device(CompositeEventEmitter):
1791
1991
  await self.send_command(
1792
1992
  HCI_LE_Set_Scan_Enable_Command(
1793
1993
  le_scan_enable=1, filter_duplicates=1 if filter_duplicates else 0
1794
- ), # type: ignore[call-arg]
1994
+ ),
1795
1995
  check_result=True,
1796
1996
  )
1797
1997
 
@@ -1804,12 +2004,12 @@ class Device(CompositeEventEmitter):
1804
2004
  await self.send_command(
1805
2005
  HCI_LE_Set_Extended_Scan_Enable_Command(
1806
2006
  enable=0, filter_duplicates=0, duration=0, period=0
1807
- ), # type: ignore[call-arg]
2007
+ ),
1808
2008
  check_result=True,
1809
2009
  )
1810
2010
  else:
1811
2011
  await self.send_command(
1812
- 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),
1813
2013
  check_result=True,
1814
2014
  )
1815
2015
 
@@ -1829,7 +2029,7 @@ class Device(CompositeEventEmitter):
1829
2029
 
1830
2030
  async def start_discovery(self, auto_restart: bool = True) -> None:
1831
2031
  await self.send_command(
1832
- 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),
1833
2033
  check_result=True,
1834
2034
  )
1835
2035
 
@@ -1838,7 +2038,7 @@ class Device(CompositeEventEmitter):
1838
2038
  lap=HCI_GENERAL_INQUIRY_LAP,
1839
2039
  inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
1840
2040
  num_responses=0, # Unlimited number of responses.
1841
- ) # type: ignore[call-arg]
2041
+ )
1842
2042
  )
1843
2043
  if response.status != HCI_Command_Status_Event.PENDING:
1844
2044
  self.discovering = False
@@ -1849,7 +2049,7 @@ class Device(CompositeEventEmitter):
1849
2049
 
1850
2050
  async def stop_discovery(self) -> None:
1851
2051
  if self.discovering:
1852
- 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)
1853
2053
  self.auto_restart_inquiry = True
1854
2054
  self.discovering = False
1855
2055
 
@@ -1897,7 +2097,7 @@ class Device(CompositeEventEmitter):
1897
2097
  await self.send_command(
1898
2098
  HCI_Write_Extended_Inquiry_Response_Command(
1899
2099
  fec_required=0, extended_inquiry_response=self.inquiry_response
1900
- ), # type: ignore[call-arg]
2100
+ ),
1901
2101
  check_result=True,
1902
2102
  )
1903
2103
  await self.set_scan_enable(
@@ -2086,7 +2286,7 @@ class Device(CompositeEventEmitter):
2086
2286
  supervision_timeouts=supervision_timeouts,
2087
2287
  min_ce_lengths=min_ce_lengths,
2088
2288
  max_ce_lengths=max_ce_lengths,
2089
- ) # type: ignore[call-arg]
2289
+ )
2090
2290
  )
2091
2291
  else:
2092
2292
  if HCI_LE_1M_PHY not in connection_parameters_preferences:
@@ -2115,7 +2315,7 @@ class Device(CompositeEventEmitter):
2115
2315
  supervision_timeout=int(prefs.supervision_timeout / 10),
2116
2316
  min_ce_length=int(prefs.min_ce_length / 0.625),
2117
2317
  max_ce_length=int(prefs.max_ce_length / 0.625),
2118
- ) # type: ignore[call-arg]
2318
+ )
2119
2319
  )
2120
2320
  else:
2121
2321
  # Save pending connection
@@ -2132,7 +2332,7 @@ class Device(CompositeEventEmitter):
2132
2332
  clock_offset=0x0000,
2133
2333
  allow_role_switch=0x01,
2134
2334
  reserved=0,
2135
- ) # type: ignore[call-arg]
2335
+ )
2136
2336
  )
2137
2337
 
2138
2338
  if result.status != HCI_Command_Status_Event.PENDING:
@@ -2151,10 +2351,10 @@ class Device(CompositeEventEmitter):
2151
2351
  )
2152
2352
  except asyncio.TimeoutError:
2153
2353
  if transport == BT_LE_TRANSPORT:
2154
- 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())
2155
2355
  else:
2156
2356
  await self.send_command(
2157
- HCI_Create_Connection_Cancel_Command(bd_addr=peer_address) # type: ignore[call-arg]
2357
+ HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
2158
2358
  )
2159
2359
 
2160
2360
  try:
@@ -2268,7 +2468,7 @@ class Device(CompositeEventEmitter):
2268
2468
  try:
2269
2469
  # Accept connection request
2270
2470
  await self.send_command(
2271
- 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)
2272
2472
  )
2273
2473
 
2274
2474
  # Wait for connection complete
@@ -2325,7 +2525,9 @@ class Device(CompositeEventEmitter):
2325
2525
  check_result=True,
2326
2526
  )
2327
2527
 
2328
- async def disconnect(self, connection, reason):
2528
+ async def disconnect(
2529
+ self, connection: Union[Connection, ScoLink, CisLink], reason: int
2530
+ ) -> None:
2329
2531
  # Create a future so that we can wait for the disconnection's result
2330
2532
  pending_disconnection = asyncio.get_running_loop().create_future()
2331
2533
  connection.on('disconnection', pending_disconnection.set_result)
@@ -2364,7 +2566,7 @@ class Device(CompositeEventEmitter):
2364
2566
  connection_handle=connection.handle,
2365
2567
  tx_octets=tx_octets,
2366
2568
  tx_time=tx_time,
2367
- ), # type: ignore[call-arg]
2569
+ ),
2368
2570
  check_result=True,
2369
2571
  )
2370
2572
 
@@ -2410,7 +2612,7 @@ class Device(CompositeEventEmitter):
2410
2612
  supervision_timeout=supervision_timeout,
2411
2613
  min_ce_length=min_ce_length,
2412
2614
  max_ce_length=max_ce_length,
2413
- ) # type: ignore[call-arg]
2615
+ )
2414
2616
  )
2415
2617
  if result.status != HCI_Command_Status_Event.PENDING:
2416
2618
  raise HCI_StatusError(result)
@@ -2738,7 +2940,7 @@ class Device(CompositeEventEmitter):
2738
2940
 
2739
2941
  try:
2740
2942
  result = await self.send_command(
2741
- 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)
2742
2944
  )
2743
2945
  if result.status != HCI_COMMAND_STATUS_PENDING:
2744
2946
  logger.warning(
@@ -2780,7 +2982,7 @@ class Device(CompositeEventEmitter):
2780
2982
  page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R2,
2781
2983
  reserved=0,
2782
2984
  clock_offset=0, # TODO investigate non-0 values
2783
- ) # type: ignore[call-arg]
2985
+ )
2784
2986
  )
2785
2987
 
2786
2988
  if result.status != HCI_COMMAND_STATUS_PENDING:
@@ -2796,6 +2998,150 @@ class Device(CompositeEventEmitter):
2796
2998
  self.remove_listener('remote_name', handler)
2797
2999
  self.remove_listener('remote_name_failure', failure_handler)
2798
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
+
2799
3145
  @host_event_handler
2800
3146
  def on_flush(self):
2801
3147
  self.emit('flush')
@@ -2888,13 +3234,18 @@ class Device(CompositeEventEmitter):
2888
3234
  # Guess which own address type is used for this connection.
2889
3235
  # This logic is somewhat correct but may need to be improved
2890
3236
  # when multiple advertising are run simultaneously.
3237
+ advertiser = None
2891
3238
  if self.connect_own_address_type is not None:
2892
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
2893
3246
  else:
2894
- own_address_type = self.advertising_own_address_type
2895
-
2896
- # We are no longer advertising
2897
- self.advertising = False
3247
+ # For extended advertisement, determining own address type later.
3248
+ own_address_type = OwnAddressType.RANDOM
2898
3249
 
2899
3250
  if own_address_type in (
2900
3251
  OwnAddressType.PUBLIC,
@@ -2916,6 +3267,7 @@ class Device(CompositeEventEmitter):
2916
3267
  connection_parameters,
2917
3268
  ConnectionPHY(HCI_LE_1M_PHY, HCI_LE_1M_PHY),
2918
3269
  )
3270
+ connection.advertiser_after_disconnection = advertiser
2919
3271
  self.connections[connection_handle] = connection
2920
3272
 
2921
3273
  # If supported, read which PHY we're connected with before
@@ -2947,10 +3299,10 @@ class Device(CompositeEventEmitter):
2947
3299
  # For directed advertising, this means a timeout
2948
3300
  if (
2949
3301
  transport == BT_LE_TRANSPORT
2950
- and self.advertising
2951
- and self.advertising_type.is_directed
3302
+ and self.legacy_advertiser
3303
+ and self.legacy_advertiser.advertising_type.is_directed
2952
3304
  ):
2953
- self.advertising = False
3305
+ self.legacy_advertiser = None
2954
3306
 
2955
3307
  # Notify listeners
2956
3308
  error = core.ConnectionError(
@@ -3000,30 +3352,49 @@ class Device(CompositeEventEmitter):
3000
3352
  )
3001
3353
 
3002
3354
  @host_event_handler
3003
- @with_connection_from_handle
3004
- def on_disconnection(self, connection, reason):
3005
- logger.debug(
3006
- f'*** Disconnection: [0x{connection.handle:04X}] '
3007
- f'{connection.peer_address} as {connection.role_name}, reason={reason}'
3008
- )
3009
- connection.emit('disconnection', reason)
3010
-
3011
- # Remove the connection from the map
3012
- del self.connections[connection.handle]
3013
-
3014
- # Cleanup subsystems that maintain per-connection state
3015
- self.gatt_server.on_disconnection(connection)
3016
-
3017
- # Restart advertising if auto-restart is enabled
3018
- if self.auto_restart_advertising:
3019
- logger.debug('restarting advertising')
3020
- self.abort_on(
3021
- 'flush',
3022
- self.start_advertising(
3023
- advertising_type=self.advertising_type,
3024
- own_address_type=self.advertising_own_address_type,
3025
- auto_restart=True,
3026
- ),
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} ***'
3027
3398
  )
3028
3399
 
3029
3400
  @host_event_handler
@@ -3174,7 +3545,7 @@ class Device(CompositeEventEmitter):
3174
3545
  try:
3175
3546
  if await connection.abort_on('disconnection', method()):
3176
3547
  await self.host.send_command(
3177
- HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
3548
+ HCI_User_Confirmation_Request_Reply_Command(
3178
3549
  bd_addr=connection.peer_address
3179
3550
  )
3180
3551
  )
@@ -3183,7 +3554,7 @@ class Device(CompositeEventEmitter):
3183
3554
  logger.warning(f'exception while confirming: {error}')
3184
3555
 
3185
3556
  await self.host.send_command(
3186
- HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
3557
+ HCI_User_Confirmation_Request_Negative_Reply_Command(
3187
3558
  bd_addr=connection.peer_address
3188
3559
  )
3189
3560
  )
@@ -3204,7 +3575,7 @@ class Device(CompositeEventEmitter):
3204
3575
  )
3205
3576
  if number is not None:
3206
3577
  await self.host.send_command(
3207
- HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
3578
+ HCI_User_Passkey_Request_Reply_Command(
3208
3579
  bd_addr=connection.peer_address, numeric_value=number
3209
3580
  )
3210
3581
  )
@@ -3213,7 +3584,7 @@ class Device(CompositeEventEmitter):
3213
3584
  logger.warning(f'exception while asking for pass-key: {error}')
3214
3585
 
3215
3586
  await self.host.send_command(
3216
- HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
3587
+ HCI_User_Passkey_Request_Negative_Reply_Command(
3217
3588
  bd_addr=connection.peer_address
3218
3589
  )
3219
3590
  )
@@ -3302,6 +3673,131 @@ class Device(CompositeEventEmitter):
3302
3673
  connection.emit('remote_name_failure', error)
3303
3674
  self.emit('remote_name_failure', address, error)
3304
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
+
3305
3801
  @host_event_handler
3306
3802
  @with_connection_from_handle
3307
3803
  def on_connection_encryption_change(self, connection, encryption):