bumble 0.0.212__py3-none-any.whl → 0.0.213__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 (86) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +11 -9
  5. bumble/apps/bench.py +480 -31
  6. bumble/apps/console.py +3 -3
  7. bumble/apps/controller_info.py +47 -10
  8. bumble/apps/controller_loopback.py +7 -3
  9. bumble/apps/controllers.py +2 -2
  10. bumble/apps/device_info.py +2 -2
  11. bumble/apps/gatt_dump.py +2 -2
  12. bumble/apps/gg_bridge.py +2 -2
  13. bumble/apps/hci_bridge.py +2 -2
  14. bumble/apps/l2cap_bridge.py +2 -2
  15. bumble/apps/lea_unicast/app.py +6 -1
  16. bumble/apps/pair.py +19 -11
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/rfcomm_bridge.py +1 -1
  19. bumble/apps/scan.py +2 -2
  20. bumble/apps/show.py +4 -2
  21. bumble/apps/speaker/speaker.html +1 -0
  22. bumble/apps/speaker/speaker.js +113 -62
  23. bumble/apps/speaker/speaker.py +126 -18
  24. bumble/at.py +4 -4
  25. bumble/att.py +2 -6
  26. bumble/avc.py +7 -7
  27. bumble/avctp.py +3 -3
  28. bumble/avdtp.py +16 -20
  29. bumble/avrcp.py +41 -53
  30. bumble/colors.py +2 -2
  31. bumble/controller.py +84 -23
  32. bumble/device.py +348 -182
  33. bumble/drivers/__init__.py +2 -2
  34. bumble/drivers/common.py +0 -2
  35. bumble/drivers/intel.py +37 -40
  36. bumble/drivers/rtk.py +28 -35
  37. bumble/gatt.py +4 -4
  38. bumble/gatt_adapters.py +4 -5
  39. bumble/gatt_client.py +26 -31
  40. bumble/gatt_server.py +7 -11
  41. bumble/hci.py +2601 -2909
  42. bumble/helpers.py +4 -5
  43. bumble/hfp.py +32 -37
  44. bumble/host.py +94 -35
  45. bumble/keys.py +5 -5
  46. bumble/l2cap.py +310 -394
  47. bumble/link.py +6 -270
  48. bumble/pairing.py +23 -20
  49. bumble/pandora/__init__.py +1 -1
  50. bumble/pandora/config.py +2 -2
  51. bumble/pandora/device.py +6 -6
  52. bumble/pandora/host.py +27 -28
  53. bumble/pandora/l2cap.py +2 -2
  54. bumble/pandora/security.py +6 -6
  55. bumble/pandora/utils.py +3 -3
  56. bumble/profiles/ascs.py +132 -131
  57. bumble/profiles/asha.py +2 -2
  58. bumble/profiles/bap.py +3 -4
  59. bumble/profiles/csip.py +2 -2
  60. bumble/profiles/device_information_service.py +2 -2
  61. bumble/profiles/gap.py +2 -2
  62. bumble/profiles/hap.py +34 -33
  63. bumble/profiles/le_audio.py +4 -4
  64. bumble/profiles/mcp.py +4 -4
  65. bumble/profiles/vcs.py +3 -5
  66. bumble/rfcomm.py +10 -10
  67. bumble/rtp.py +1 -2
  68. bumble/sdp.py +2 -2
  69. bumble/smp.py +57 -61
  70. bumble/tools/rtk_util.py +2 -2
  71. bumble/transport/__init__.py +2 -16
  72. bumble/transport/android_netsim.py +5 -5
  73. bumble/transport/common.py +4 -4
  74. bumble/transport/pyusb.py +2 -2
  75. bumble/utils.py +2 -5
  76. bumble/vendor/android/hci.py +118 -200
  77. bumble/vendor/zephyr/hci.py +32 -27
  78. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/METADATA +2 -2
  79. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/RECORD +83 -86
  80. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
  81. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
  82. bumble/apps/link_relay/__init__.py +0 -0
  83. bumble/apps/link_relay/link_relay.py +0 -289
  84. bumble/apps/link_relay/logging.yml +0 -21
  85. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
  86. {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -35,12 +35,10 @@ import secrets
35
35
  import sys
36
36
  from typing import (
37
37
  Any,
38
+ Awaitable,
38
39
  Callable,
39
40
  ClassVar,
40
- Deque,
41
- Dict,
42
41
  Optional,
43
- Type,
44
42
  TypeVar,
45
43
  Union,
46
44
  cast,
@@ -87,6 +85,7 @@ from bumble.profiles import gatt_service
87
85
  if TYPE_CHECKING:
88
86
  from bumble.transport.common import TransportSource, TransportSink
89
87
 
88
+ _T = TypeVar('_T')
90
89
 
91
90
  # -----------------------------------------------------------------------------
92
91
  # Logging
@@ -99,9 +98,9 @@ logger = logging.getLogger(__name__)
99
98
  # fmt: off
100
99
  # pylint: disable=line-too-long
101
100
 
102
- DEVICE_MIN_SCAN_INTERVAL = 25
101
+ DEVICE_MIN_SCAN_INTERVAL = 2.5
103
102
  DEVICE_MAX_SCAN_INTERVAL = 10240
104
- DEVICE_MIN_SCAN_WINDOW = 25
103
+ DEVICE_MIN_SCAN_WINDOW = 2.5
105
104
  DEVICE_MAX_SCAN_WINDOW = 10240
106
105
  DEVICE_MIN_LE_RSSI = -127
107
106
  DEVICE_MAX_LE_RSSI = 20
@@ -140,6 +139,9 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
140
139
  DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
141
140
  DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
142
141
  DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
142
+ DEVICE_DEFAULT_ISO_CIS_MAX_SDU = 251
143
+ DEVICE_DEFAULT_ISO_CIS_RTN = 10
144
+ DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY = 100
143
145
 
144
146
  # fmt: on
145
147
  # pylint: enable=line-too-long
@@ -202,25 +204,35 @@ class Advertisement:
202
204
  # -----------------------------------------------------------------------------
203
205
  class LegacyAdvertisement(Advertisement):
204
206
  @classmethod
205
- def from_advertising_report(cls, report):
207
+ def from_advertising_report(
208
+ cls, report: hci.HCI_LE_Advertising_Report_Event.Report
209
+ ) -> Self:
206
210
  return cls(
207
211
  address=report.address,
208
212
  rssi=report.rssi,
209
213
  is_legacy=True,
210
- is_connectable=report.event_type
211
- in (
212
- hci.HCI_LE_Advertising_Report_Event.ADV_IND,
213
- hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
214
+ is_connectable=(
215
+ report.event_type
216
+ in (
217
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
218
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
219
+ )
220
+ ),
221
+ is_directed=(
222
+ report.event_type
223
+ == hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
224
+ ),
225
+ is_scannable=(
226
+ report.event_type
227
+ in (
228
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
229
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
230
+ )
214
231
  ),
215
- is_directed=report.event_type
216
- == hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
217
- is_scannable=report.event_type
218
- in (
219
- hci.HCI_LE_Advertising_Report_Event.ADV_IND,
220
- hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
232
+ is_scan_response=(
233
+ report.event_type
234
+ == hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
221
235
  ),
222
- is_scan_response=report.event_type
223
- == hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
224
236
  data_bytes=report.data,
225
237
  )
226
238
 
@@ -228,18 +240,20 @@ class LegacyAdvertisement(Advertisement):
228
240
  # -----------------------------------------------------------------------------
229
241
  class ExtendedAdvertisement(Advertisement):
230
242
  @classmethod
231
- def from_advertising_report(cls, report):
243
+ def from_advertising_report(
244
+ cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
245
+ ) -> Self:
232
246
  # fmt: off
233
247
  # pylint: disable=line-too-long
234
248
  return cls(
235
249
  address = report.address,
236
250
  rssi = report.rssi,
237
- is_legacy = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
251
+ is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0,
238
252
  is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
239
- is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
240
- is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
241
- is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
242
- is_scan_response = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
253
+ is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0,
254
+ is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0,
255
+ is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0,
256
+ is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0,
243
257
  is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
244
258
  is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
245
259
  primary_phy = report.primary_phy,
@@ -436,7 +450,7 @@ class AdvertisingEventProperties:
436
450
 
437
451
  @classmethod
438
452
  def from_advertising_type(
439
- cls: Type[AdvertisingEventProperties],
453
+ cls: type[AdvertisingEventProperties],
440
454
  advertising_type: AdvertisingType,
441
455
  ) -> AdvertisingEventProperties:
442
456
  return cls(
@@ -478,7 +492,18 @@ class PeriodicAdvertisement:
478
492
 
479
493
  # -----------------------------------------------------------------------------
480
494
  @dataclass
481
- class BIGInfoAdvertisement:
495
+ class BigInfoAdvertisement:
496
+ class Framing(utils.OpenIntEnum):
497
+ # fmt: off
498
+ UNFRAMED = 0X00
499
+ FRAMED_SEGMENTABLE_MODE = 0X01
500
+ FRAMED_UNSEGMENTED_MODE = 0X02
501
+
502
+ class Encryption(utils.OpenIntEnum):
503
+ # fmt: off
504
+ UNENCRYPTED = 0x00
505
+ ENCRYPTED = 0x01
506
+
482
507
  address: hci.Address
483
508
  sid: int
484
509
  num_bis: int
@@ -491,8 +516,8 @@ class BIGInfoAdvertisement:
491
516
  sdu_interval: int
492
517
  max_sdu: int
493
518
  phy: hci.Phy
494
- framed: bool
495
- encrypted: bool
519
+ framing: Framing
520
+ encryption: Encryption
496
521
 
497
522
  @classmethod
498
523
  def from_report(cls, address: hci.Address, sid: int, report) -> Self:
@@ -509,8 +534,8 @@ class BIGInfoAdvertisement:
509
534
  report.sdu_interval,
510
535
  report.max_sdu,
511
536
  hci.Phy(report.phy),
512
- report.framing != 0,
513
- report.encryption != 0,
537
+ cls.Framing(report.framing),
538
+ cls.Encryption(report.encryption),
514
539
  )
515
540
 
516
541
 
@@ -1002,7 +1027,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
1002
1027
  def on_biginfo_advertising_report(self, report) -> None:
1003
1028
  self.emit(
1004
1029
  self.EVENT_BIGINFO_ADVERTISEMENT,
1005
- BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
1030
+ BigInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
1006
1031
  )
1007
1032
 
1008
1033
  def __str__(self) -> str:
@@ -1020,14 +1045,24 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
1020
1045
  # -----------------------------------------------------------------------------
1021
1046
  @dataclass
1022
1047
  class BigParameters:
1048
+ class Packing(utils.OpenIntEnum):
1049
+ # fmt: off
1050
+ SEQUENTIAL = 0x00
1051
+ INTERLEAVED = 0x01
1052
+
1053
+ class Framing(utils.OpenIntEnum):
1054
+ # fmt: off
1055
+ UNFRAMED = 0x00
1056
+ FRAMED = 0x01
1057
+
1023
1058
  num_bis: int
1024
- sdu_interval: int
1059
+ sdu_interval: int # SDU interval, in microseconds
1025
1060
  max_sdu: int
1026
- max_transport_latency: int
1061
+ max_transport_latency: int # Max transport latency, in milliseconds
1027
1062
  rtn: int
1028
1063
  phy: hci.PhyBit = hci.PhyBit.LE_2M
1029
- packing: int = 0
1030
- framing: int = 0
1064
+ packing: Packing = Packing.SEQUENTIAL
1065
+ framing: Framing = Framing.UNFRAMED
1031
1066
  broadcast_code: bytes | None = None
1032
1067
 
1033
1068
 
@@ -1050,15 +1085,15 @@ class Big(utils.EventEmitter):
1050
1085
  state: State = State.PENDING
1051
1086
 
1052
1087
  # Attributes provided by BIG Create Complete event
1053
- big_sync_delay: int = 0
1054
- transport_latency_big: int = 0
1055
- phy: int = 0
1088
+ big_sync_delay: int = 0 # Sync delay, in microseconds
1089
+ transport_latency_big: int = 0 # Transport latency, in microseconds
1090
+ phy: hci.Phy = hci.Phy.LE_1M
1056
1091
  nse: int = 0
1057
1092
  bn: int = 0
1058
1093
  pto: int = 0
1059
1094
  irc: int = 0
1060
1095
  max_pdu: int = 0
1061
- iso_interval: float = 0.0
1096
+ iso_interval: float = 0.0 # ISO interval, in milliseconds
1062
1097
  bis_links: Sequence[BisLink] = ()
1063
1098
 
1064
1099
  def __post_init__(self) -> None:
@@ -1343,7 +1378,7 @@ class Peer:
1343
1378
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
1344
1379
 
1345
1380
  def create_service_proxy(
1346
- self, proxy_class: Type[_PROXY_CLASS]
1381
+ self, proxy_class: type[_PROXY_CLASS]
1347
1382
  ) -> Optional[_PROXY_CLASS]:
1348
1383
  if proxy := proxy_class.from_client(self.gatt_client):
1349
1384
  return cast(_PROXY_CLASS, proxy)
@@ -1351,7 +1386,7 @@ class Peer:
1351
1386
  return None
1352
1387
 
1353
1388
  async def discover_service_and_create_proxy(
1354
- self, proxy_class: Type[_PROXY_CLASS]
1389
+ self, proxy_class: type[_PROXY_CLASS]
1355
1390
  ) -> Optional[_PROXY_CLASS]:
1356
1391
  # Discover the first matching service and its characteristics
1357
1392
  services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
@@ -1464,7 +1499,7 @@ class _IsoLink:
1464
1499
  check_result=True,
1465
1500
  )
1466
1501
 
1467
- async def remove_data_path(self, direction: _IsoLink.Direction) -> int:
1502
+ async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
1468
1503
  """Remove a data path with controller on given direction.
1469
1504
 
1470
1505
  Args:
@@ -1476,7 +1511,9 @@ class _IsoLink:
1476
1511
  response = await self.device.send_command(
1477
1512
  hci.HCI_LE_Remove_ISO_Data_Path_Command(
1478
1513
  connection_handle=self.handle,
1479
- data_path_direction=direction,
1514
+ data_path_direction=sum(
1515
+ 1 << direction for direction in set(directions)
1516
+ ),
1480
1517
  ),
1481
1518
  check_result=False,
1482
1519
  )
@@ -1486,10 +1523,74 @@ class _IsoLink:
1486
1523
  """Write an ISO SDU."""
1487
1524
  self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
1488
1525
 
1526
+ async def get_tx_time_stamp(self) -> tuple[int, int, int]:
1527
+ response = await self.device.host.send_command(
1528
+ hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle),
1529
+ check_result=True,
1530
+ )
1531
+ return (
1532
+ response.return_parameters.packet_sequence_number,
1533
+ response.return_parameters.tx_time_stamp,
1534
+ response.return_parameters.time_offset,
1535
+ )
1536
+
1489
1537
  @property
1490
1538
  def data_packet_queue(self) -> DataPacketQueue | None:
1491
1539
  return self.device.host.get_data_packet_queue(self.handle)
1492
1540
 
1541
+ async def drain(self) -> None:
1542
+ if data_packet_queue := self.data_packet_queue:
1543
+ await data_packet_queue.drain(self.handle)
1544
+
1545
+
1546
+ # -----------------------------------------------------------------------------
1547
+ @dataclass
1548
+ class CigParameters:
1549
+ class WorstCaseSca(utils.OpenIntEnum):
1550
+ # fmt: off
1551
+ SCA_251_TO_500_PPM = 0x00
1552
+ SCA_151_TO_250_PPM = 0x01
1553
+ SCA_101_TO_150_PPM = 0x02
1554
+ SCA_76_TO_100_PPM = 0x03
1555
+ SCA_51_TO_75_PPM = 0x04
1556
+ SCA_31_TO_50_PPM = 0x05
1557
+ SCA_21_TO_30_PPM = 0x06
1558
+ SCA_0_TO_20_PPM = 0x07
1559
+
1560
+ class Packing(utils.OpenIntEnum):
1561
+ # fmt: off
1562
+ SEQUENTIAL = 0x00
1563
+ INTERLEAVED = 0x01
1564
+
1565
+ class Framing(utils.OpenIntEnum):
1566
+ # fmt: off
1567
+ UNFRAMED = 0x00
1568
+ FRAMED = 0x01
1569
+
1570
+ @dataclass
1571
+ class CisParameters:
1572
+ cis_id: int
1573
+ max_sdu_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
1574
+ max_sdu_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
1575
+ phy_c_to_p: hci.PhyBit = hci.PhyBit.LE_2M
1576
+ phy_p_to_c: hci.PhyBit = hci.PhyBit.LE_2M
1577
+ rtn_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of C->P retransmissions
1578
+ rtn_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of P->C retransmissions
1579
+
1580
+ cig_id: int
1581
+ cis_parameters: list[CisParameters]
1582
+ sdu_interval_c_to_p: int # C->P SDU interval, in microseconds
1583
+ sdu_interval_p_to_c: int # P->C SDU interval, in microseconds
1584
+ worst_case_sca: WorstCaseSca = WorstCaseSca.SCA_251_TO_500_PPM
1585
+ packing: Packing = Packing.SEQUENTIAL
1586
+ framing: Framing = Framing.UNFRAMED
1587
+ max_transport_latency_c_to_p: int = (
1588
+ DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
1589
+ )
1590
+ max_transport_latency_p_to_c: int = (
1591
+ DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
1592
+ )
1593
+
1493
1594
 
1494
1595
  # -----------------------------------------------------------------------------
1495
1596
  @dataclass
@@ -1503,6 +1604,20 @@ class CisLink(utils.EventEmitter, _IsoLink):
1503
1604
  handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
1504
1605
  cis_id: int # CIS ID assigned by Central device
1505
1606
  cig_id: int # CIG ID assigned by Central device
1607
+ cig_sync_delay: int = 0 # CIG sync delay, in microseconds
1608
+ cis_sync_delay: int = 0 # CIS sync delay, in microseconds
1609
+ transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
1610
+ transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
1611
+ phy_c_to_p: Optional[hci.Phy] = None
1612
+ phy_p_to_c: Optional[hci.Phy] = None
1613
+ nse: int = 0
1614
+ bn_c_to_p: int = 0
1615
+ bn_p_to_c: int = 0
1616
+ ft_c_to_p: int = 0
1617
+ ft_p_to_c: int = 0
1618
+ max_pdu_c_to_p: int = 0
1619
+ max_pdu_p_to_c: int = 0
1620
+ iso_interval: float = 0.0 # ISO interval, in milliseconds
1506
1621
  state: State = State.PENDING
1507
1622
  sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
1508
1623
 
@@ -1545,7 +1660,7 @@ class IsoPacketStream:
1545
1660
  self.iso_link = iso_link
1546
1661
  self.data_packet_queue = iso_link.data_packet_queue
1547
1662
  self.data_packet_queue.on('flow', self._on_flow)
1548
- self._thresholds: Deque[int] = collections.deque()
1663
+ self._thresholds: collections.deque[int] = collections.deque()
1549
1664
  self._semaphore = asyncio.Semaphore(max_queue_size)
1550
1665
 
1551
1666
  def _on_flow(self) -> None:
@@ -1585,6 +1700,7 @@ class Connection(utils.CompositeEventEmitter):
1585
1700
  peer_resolvable_address: Optional[hci.Address]
1586
1701
  peer_le_features: Optional[hci.LeFeatureMask]
1587
1702
  role: hci.Role
1703
+ parameters: Parameters
1588
1704
  encryption: int
1589
1705
  encryption_key_size: int
1590
1706
  authenticated: bool
@@ -1594,6 +1710,8 @@ class Connection(utils.CompositeEventEmitter):
1594
1710
  pairing_peer_authentication_requirements: Optional[int]
1595
1711
  cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
1596
1712
  cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
1713
+ classic_mode: int = hci.HCI_Mode_Change_Event.Mode.ACTIVE
1714
+ classic_interval: int = 0
1597
1715
 
1598
1716
  EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
1599
1717
  EVENT_DISCONNECTION = "disconnection"
@@ -1620,6 +1738,8 @@ class Connection(utils.CompositeEventEmitter):
1620
1738
  EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED = "channel_sounding_config_removed"
1621
1739
  EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE = "channel_sounding_procedure_failure"
1622
1740
  EVENT_CHANNEL_SOUNDING_PROCEDURE = "channel_sounding_procedure"
1741
+ EVENT_MODE_CHANGE = "mode_change"
1742
+ EVENT_MODE_CHANGE_FAILURE = "mode_change_failure"
1623
1743
  EVENT_ROLE_CHANGE = "role_change"
1624
1744
  EVENT_ROLE_CHANGE_FAILURE = "role_change_failure"
1625
1745
  EVENT_CLASSIC_PAIRING = "classic_pairing"
@@ -1629,6 +1749,9 @@ class Connection(utils.CompositeEventEmitter):
1629
1749
  EVENT_PAIRING_FAILURE = "pairing_failure"
1630
1750
  EVENT_SECURITY_REQUEST = "security_request"
1631
1751
  EVENT_LINK_KEY = "link_key"
1752
+ EVENT_CIS_REQUEST = "cis_request"
1753
+ EVENT_CIS_ESTABLISHMENT = "cis_establishment"
1754
+ EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
1632
1755
 
1633
1756
  @utils.composite_listener
1634
1757
  class Listener:
@@ -1884,6 +2007,12 @@ class Connection(utils.CompositeEventEmitter):
1884
2007
  def data_packet_queue(self) -> DataPacketQueue | None:
1885
2008
  return self.device.host.get_data_packet_queue(self.handle)
1886
2009
 
2010
+ def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
2011
+ """
2012
+ Helper method to call `utils.cancel_on_event` for the 'disconnection' event
2013
+ """
2014
+ return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
2015
+
1887
2016
  async def __aenter__(self):
1888
2017
  return self
1889
2018
 
@@ -1954,9 +2083,9 @@ class DeviceConfiguration:
1954
2083
  gatt_service_enabled: bool = True
1955
2084
 
1956
2085
  def __post_init__(self) -> None:
1957
- self.gatt_services: list[Dict[str, Any]] = []
2086
+ self.gatt_services: list[dict[str, Any]] = []
1958
2087
 
1959
- def load_from_dict(self, config: Dict[str, Any]) -> None:
2088
+ def load_from_dict(self, config: dict[str, Any]) -> None:
1960
2089
  config = copy.deepcopy(config)
1961
2090
 
1962
2091
  # Load simple properties
@@ -2016,13 +2145,13 @@ class DeviceConfiguration:
2016
2145
  self.load_from_dict(json.load(file))
2017
2146
 
2018
2147
  @classmethod
2019
- def from_file(cls: Type[Self], filename: str) -> Self:
2148
+ def from_file(cls: type[Self], filename: str) -> Self:
2020
2149
  config = cls()
2021
2150
  config.load_from_file(filename)
2022
2151
  return config
2023
2152
 
2024
2153
  @classmethod
2025
- def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
2154
+ def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
2026
2155
  device_config = cls()
2027
2156
  device_config.load_from_dict(config)
2028
2157
  return device_config
@@ -2119,22 +2248,22 @@ class Device(utils.CompositeEventEmitter):
2119
2248
  advertising_data: bytes
2120
2249
  scan_response_data: bytes
2121
2250
  cs_capabilities: ChannelSoundingCapabilities | None = None
2122
- connections: Dict[int, Connection]
2123
- pending_connections: Dict[hci.Address, Connection]
2124
- classic_pending_accepts: Dict[
2251
+ connections: dict[int, Connection]
2252
+ pending_connections: dict[hci.Address, Connection]
2253
+ classic_pending_accepts: dict[
2125
2254
  hci.Address,
2126
2255
  list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
2127
2256
  ]
2128
- advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
2257
+ advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
2129
2258
  periodic_advertising_syncs: list[PeriodicAdvertisingSync]
2130
2259
  config: DeviceConfiguration
2131
2260
  legacy_advertiser: Optional[LegacyAdvertiser]
2132
- sco_links: Dict[int, ScoLink]
2133
- cis_links: Dict[int, CisLink]
2261
+ sco_links: dict[int, ScoLink]
2262
+ cis_links: dict[int, CisLink]
2134
2263
  bigs: dict[int, Big]
2135
2264
  bis_links: dict[int, BisLink]
2136
2265
  big_syncs: dict[int, BigSync]
2137
- _pending_cis: Dict[int, tuple[int, int]]
2266
+ _pending_cis: dict[int, tuple[int, int]]
2138
2267
  gatt_service: gatt_service.GenericAttributeProfileService | None = None
2139
2268
 
2140
2269
  EVENT_ADVERTISEMENT = "advertisement"
@@ -2294,8 +2423,8 @@ class Device(utils.CompositeEventEmitter):
2294
2423
  self.address_generation_offload = config.address_generation_offload
2295
2424
 
2296
2425
  # Extended advertising.
2297
- self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2298
- self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2426
+ self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
2427
+ self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
2299
2428
 
2300
2429
  # Legacy advertising.
2301
2430
  # The advertising and scan response data, as well as the advertising interval
@@ -4271,11 +4400,11 @@ class Device(utils.CompositeEventEmitter):
4271
4400
  self.smp_manager.pairing_config_factory = pairing_config_factory
4272
4401
 
4273
4402
  @property
4274
- def smp_session_proxy(self) -> Type[smp.Session]:
4403
+ def smp_session_proxy(self) -> type[smp.Session]:
4275
4404
  return self.smp_manager.session_proxy
4276
4405
 
4277
4406
  @smp_session_proxy.setter
4278
- def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
4407
+ def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
4279
4408
  self.smp_manager.session_proxy = session_proxy
4280
4409
 
4281
4410
  async def pair(self, connection):
@@ -4359,9 +4488,7 @@ class Device(utils.CompositeEventEmitter):
4359
4488
  raise hci.HCI_StatusError(result)
4360
4489
 
4361
4490
  # Wait for the authentication to complete
4362
- await utils.cancel_on_event(
4363
- connection, Connection.EVENT_DISCONNECTION, pending_authentication
4364
- )
4491
+ await connection.cancel_on_disconnection(pending_authentication)
4365
4492
  finally:
4366
4493
  connection.remove_listener(
4367
4494
  connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
@@ -4448,9 +4575,7 @@ class Device(utils.CompositeEventEmitter):
4448
4575
  raise hci.HCI_StatusError(result)
4449
4576
 
4450
4577
  # Wait for the result
4451
- await utils.cancel_on_event(
4452
- connection, Connection.EVENT_DISCONNECTION, pending_encryption
4453
- )
4578
+ await connection.cancel_on_disconnection(pending_encryption)
4454
4579
  finally:
4455
4580
  connection.remove_listener(
4456
4581
  connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
@@ -4494,9 +4619,7 @@ class Device(utils.CompositeEventEmitter):
4494
4619
  f'{hci.HCI_Constant.error_name(result.status)}'
4495
4620
  )
4496
4621
  raise hci.HCI_StatusError(result)
4497
- await utils.cancel_on_event(
4498
- connection, Connection.EVENT_DISCONNECTION, pending_role_change
4499
- )
4622
+ await connection.cancel_on_disconnection(pending_role_change)
4500
4623
  finally:
4501
4624
  connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
4502
4625
  connection.remove_listener(
@@ -4556,48 +4679,39 @@ class Device(utils.CompositeEventEmitter):
4556
4679
  @utils.experimental('Only for testing.')
4557
4680
  async def setup_cig(
4558
4681
  self,
4559
- cig_id: int,
4560
- cis_id: Sequence[int],
4561
- sdu_interval: tuple[int, int],
4562
- framing: int,
4563
- max_sdu: tuple[int, int],
4564
- retransmission_number: int,
4565
- max_transport_latency: tuple[int, int],
4682
+ parameters: CigParameters,
4566
4683
  ) -> list[int]:
4567
4684
  """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
4568
4685
 
4569
4686
  Args:
4570
- cig_id: CIG_ID.
4571
- cis_id: CID ID list.
4572
- sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
4573
- framing: Un-framing(0) or Framing(1).
4574
- max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
4575
- retransmission_number: retransmission_number.
4576
- max_transport_latency: Max transport latencies of
4577
- (Central->Peripheral, Peripheral->Cental).
4687
+ parameters: CIG parameters.
4578
4688
 
4579
4689
  Returns:
4580
4690
  List of created CIS handles corresponding to the same order of [cid_id].
4581
4691
  """
4582
- num_cis = len(cis_id)
4692
+ num_cis = len(parameters.cis_parameters)
4583
4693
 
4584
4694
  response = await self.send_command(
4585
4695
  hci.HCI_LE_Set_CIG_Parameters_Command(
4586
- cig_id=cig_id,
4587
- sdu_interval_c_to_p=sdu_interval[0],
4588
- sdu_interval_p_to_c=sdu_interval[1],
4589
- worst_case_sca=0x00, # 251-500 ppm
4590
- packing=0x00, # Sequential
4591
- framing=framing,
4592
- max_transport_latency_c_to_p=max_transport_latency[0],
4593
- max_transport_latency_p_to_c=max_transport_latency[1],
4594
- cis_id=cis_id,
4595
- max_sdu_c_to_p=[max_sdu[0]] * num_cis,
4596
- max_sdu_p_to_c=[max_sdu[1]] * num_cis,
4597
- phy_c_to_p=[hci.HCI_LE_2M_PHY] * num_cis,
4598
- phy_p_to_c=[hci.HCI_LE_2M_PHY] * num_cis,
4599
- rtn_c_to_p=[retransmission_number] * num_cis,
4600
- rtn_p_to_c=[retransmission_number] * num_cis,
4696
+ cig_id=parameters.cig_id,
4697
+ sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
4698
+ sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
4699
+ worst_case_sca=parameters.worst_case_sca,
4700
+ packing=int(parameters.packing),
4701
+ framing=int(parameters.framing),
4702
+ max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
4703
+ max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
4704
+ cis_id=[cis.cis_id for cis in parameters.cis_parameters],
4705
+ max_sdu_c_to_p=[
4706
+ cis.max_sdu_c_to_p for cis in parameters.cis_parameters
4707
+ ],
4708
+ max_sdu_p_to_c=[
4709
+ cis.max_sdu_p_to_c for cis in parameters.cis_parameters
4710
+ ],
4711
+ phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
4712
+ phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
4713
+ rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
4714
+ rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
4601
4715
  ),
4602
4716
  check_result=True,
4603
4717
  )
@@ -4605,19 +4719,17 @@ class Device(utils.CompositeEventEmitter):
4605
4719
  # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
4606
4720
  # Server, so here it only provides a basic functionality for testing.
4607
4721
  cis_handles = response.return_parameters.connection_handle[:]
4608
- for id, cis_handle in zip(cis_id, cis_handles):
4609
- self._pending_cis[cis_handle] = (id, cig_id)
4722
+ for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
4723
+ self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
4610
4724
 
4611
4725
  return cis_handles
4612
4726
 
4613
4727
  # [LE only]
4614
4728
  @utils.experimental('Only for testing.')
4615
4729
  async def create_cis(
4616
- self, cis_acl_pairs: Sequence[tuple[int, int]]
4730
+ self, cis_acl_pairs: Sequence[tuple[int, Connection]]
4617
4731
  ) -> list[CisLink]:
4618
- for cis_handle, acl_handle in cis_acl_pairs:
4619
- acl_connection = self.lookup_connection(acl_handle)
4620
- assert acl_connection
4732
+ for cis_handle, acl_connection in cis_acl_pairs:
4621
4733
  cis_id, cig_id = self._pending_cis.pop(cis_handle)
4622
4734
  self.cis_links[cis_handle] = CisLink(
4623
4735
  device=self,
@@ -4637,8 +4749,8 @@ class Device(utils.CompositeEventEmitter):
4637
4749
  if pending_future := pending_cis_establishments.get(cis_link.handle):
4638
4750
  pending_future.set_result(cis_link)
4639
4751
 
4640
- def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
4641
- if pending_future := pending_cis_establishments.get(cis_handle):
4752
+ def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
4753
+ if pending_future := pending_cis_establishments.get(cis_link.handle):
4642
4754
  pending_future.set_exception(hci.HCI_Error(status))
4643
4755
 
4644
4756
  watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
@@ -4648,7 +4760,7 @@ class Device(utils.CompositeEventEmitter):
4648
4760
  await self.send_command(
4649
4761
  hci.HCI_LE_Create_CIS_Command(
4650
4762
  cis_connection_handle=[p[0] for p in cis_acl_pairs],
4651
- acl_connection_handle=[p[1] for p in cis_acl_pairs],
4763
+ acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
4652
4764
  ),
4653
4765
  check_result=True,
4654
4766
  )
@@ -4657,26 +4769,21 @@ class Device(utils.CompositeEventEmitter):
4657
4769
 
4658
4770
  # [LE only]
4659
4771
  @utils.experimental('Only for testing.')
4660
- async def accept_cis_request(self, handle: int) -> CisLink:
4772
+ async def accept_cis_request(self, cis_link: CisLink) -> None:
4661
4773
  """[LE Only] Accepts an incoming CIS request.
4662
4774
 
4663
- When the specified CIS handle is already created, this method returns the
4664
- existed CIS link object immediately.
4775
+ This method returns when the CIS is established, or raises an exception if
4776
+ the CIS establishment fails.
4665
4777
 
4666
4778
  Args:
4667
4779
  handle: CIS handle to accept.
4668
-
4669
- Returns:
4670
- CIS link object on the given handle.
4671
4780
  """
4672
- if not (cis_link := self.cis_links.get(handle)):
4673
- raise InvalidStateError(f'No pending CIS request of handle {handle}')
4674
4781
 
4675
4782
  # There might be multiple ASE sharing a CIS channel.
4676
4783
  # If one of them has accepted the request, the others should just leverage it.
4677
4784
  async with self._cis_lock:
4678
4785
  if cis_link.state == CisLink.State.ESTABLISHED:
4679
- return cis_link
4786
+ return
4680
4787
 
4681
4788
  with closing(utils.EventWatcher()) as watcher:
4682
4789
  pending_establishment = asyncio.get_running_loop().create_future()
@@ -4695,26 +4802,24 @@ class Device(utils.CompositeEventEmitter):
4695
4802
  )
4696
4803
 
4697
4804
  await self.send_command(
4698
- hci.HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
4805
+ hci.HCI_LE_Accept_CIS_Request_Command(
4806
+ connection_handle=cis_link.handle
4807
+ ),
4699
4808
  check_result=True,
4700
4809
  )
4701
4810
 
4702
4811
  await pending_establishment
4703
- return cis_link
4704
-
4705
- # Mypy believes this is reachable when context is an ExitStack.
4706
- raise UnreachableError()
4707
4812
 
4708
4813
  # [LE only]
4709
4814
  @utils.experimental('Only for testing.')
4710
4815
  async def reject_cis_request(
4711
4816
  self,
4712
- handle: int,
4817
+ cis_link: CisLink,
4713
4818
  reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
4714
4819
  ) -> None:
4715
4820
  await self.send_command(
4716
4821
  hci.HCI_LE_Reject_CIS_Request_Command(
4717
- connection_handle=handle, reason=reason
4822
+ connection_handle=cis_link.handle, reason=reason
4718
4823
  ),
4719
4824
  check_result=True,
4720
4825
  )
@@ -5071,8 +5176,8 @@ class Device(utils.CompositeEventEmitter):
5071
5176
  # Store the keys in the key store
5072
5177
  if self.keystore:
5073
5178
  authenticated = key_type in (
5074
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
5075
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
5179
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
5180
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
5076
5181
  )
5077
5182
  pairing_keys = PairingKeys(
5078
5183
  link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
@@ -5252,7 +5357,7 @@ class Device(utils.CompositeEventEmitter):
5252
5357
  big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
5253
5358
  big.big_sync_delay = big_sync_delay
5254
5359
  big.transport_latency_big = transport_latency_big
5255
- big.phy = phy
5360
+ big.phy = hci.Phy(phy)
5256
5361
  big.nse = nse
5257
5362
  big.bn = bn
5258
5363
  big.pto = pto
@@ -5519,8 +5624,8 @@ class Device(utils.CompositeEventEmitter):
5519
5624
 
5520
5625
  # Handle SCO request.
5521
5626
  if link_type in (
5522
- hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE,
5523
- hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
5627
+ hci.HCI_Connection_Complete_Event.LinkType.SCO,
5628
+ hci.HCI_Connection_Complete_Event.LinkType.ESCO,
5524
5629
  ):
5525
5630
  if connection := self.find_connection_by_bd_addr(
5526
5631
  bd_addr, transport=PhysicalTransport.BR_EDR
@@ -5628,7 +5733,7 @@ class Device(utils.CompositeEventEmitter):
5628
5733
  # [Classic only]
5629
5734
  @host_event_handler
5630
5735
  @with_connection_from_address
5631
- def on_authentication_io_capability_request(self, connection):
5736
+ def on_authentication_io_capability_request(self, connection: Connection):
5632
5737
  # Ask what the pairing config should be for this connection
5633
5738
  pairing_config = self.pairing_config_factory(connection)
5634
5739
 
@@ -5636,13 +5741,13 @@ class Device(utils.CompositeEventEmitter):
5636
5741
  authentication_requirements = (
5637
5742
  # No Bonding
5638
5743
  (
5639
- hci.HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5640
- hci.HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5744
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
5745
+ hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
5641
5746
  ),
5642
5747
  # General Bonding
5643
5748
  (
5644
- hci.HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5645
- hci.HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5749
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
5750
+ hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
5646
5751
  ),
5647
5752
  )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
5648
5753
 
@@ -5697,30 +5802,30 @@ class Device(utils.CompositeEventEmitter):
5697
5802
  raise UnreachableError()
5698
5803
 
5699
5804
  # See Bluetooth spec @ Vol 3, Part C 5.2.2.6
5700
- methods = {
5701
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: {
5702
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
5703
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
5704
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5705
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5805
+ methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
5806
+ hci.IoCapability.DISPLAY_ONLY: {
5807
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5808
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5809
+ hci.IoCapability.KEYBOARD_ONLY: na,
5810
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5706
5811
  },
5707
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: {
5708
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
5709
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
5710
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5711
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5812
+ hci.IoCapability.DISPLAY_YES_NO: {
5813
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5814
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5815
+ hci.IoCapability.KEYBOARD_ONLY: na,
5816
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5712
5817
  },
5713
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: {
5714
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: na,
5715
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: na,
5716
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5717
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5818
+ hci.IoCapability.KEYBOARD_ONLY: {
5819
+ hci.IoCapability.DISPLAY_ONLY: na,
5820
+ hci.IoCapability.DISPLAY_YES_NO: na,
5821
+ hci.IoCapability.KEYBOARD_ONLY: na,
5822
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5718
5823
  },
5719
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
5720
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: confirm,
5721
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: confirm,
5722
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: auto_confirm,
5723
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5824
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: {
5825
+ hci.IoCapability.DISPLAY_ONLY: confirm,
5826
+ hci.IoCapability.DISPLAY_YES_NO: confirm,
5827
+ hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
5828
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5724
5829
  },
5725
5830
  }
5726
5831
 
@@ -5728,9 +5833,7 @@ class Device(utils.CompositeEventEmitter):
5728
5833
 
5729
5834
  async def reply() -> None:
5730
5835
  try:
5731
- if await utils.cancel_on_event(
5732
- connection, Connection.EVENT_DISCONNECTION, method()
5733
- ):
5836
+ if await connection.cancel_on_disconnection(method()):
5734
5837
  await self.host.send_command(
5735
5838
  hci.HCI_User_Confirmation_Request_Reply_Command(
5736
5839
  bd_addr=connection.peer_address
@@ -5757,10 +5860,8 @@ class Device(utils.CompositeEventEmitter):
5757
5860
 
5758
5861
  async def reply() -> None:
5759
5862
  try:
5760
- number = await utils.cancel_on_event(
5761
- connection,
5762
- Connection.EVENT_DISCONNECTION,
5763
- pairing_config.delegate.get_number(),
5863
+ number = await connection.cancel_on_disconnection(
5864
+ pairing_config.delegate.get_number()
5764
5865
  )
5765
5866
  if number is not None:
5766
5867
  await self.host.send_command(
@@ -5780,6 +5881,19 @@ class Device(utils.CompositeEventEmitter):
5780
5881
 
5781
5882
  utils.AsyncRunner.spawn(reply())
5782
5883
 
5884
+ # [Classic only]
5885
+ @host_event_handler
5886
+ @with_connection_from_handle
5887
+ def on_mode_change(
5888
+ self, connection: Connection, status: int, current_mode: int, interval: int
5889
+ ):
5890
+ if status == hci.HCI_SUCCESS:
5891
+ connection.classic_mode = current_mode
5892
+ connection.classic_interval = interval
5893
+ connection.emit(connection.EVENT_MODE_CHANGE)
5894
+ else:
5895
+ connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
5896
+
5783
5897
  # [Classic only]
5784
5898
  @host_event_handler
5785
5899
  @with_connection_from_address
@@ -5790,13 +5904,11 @@ class Device(utils.CompositeEventEmitter):
5790
5904
  io_capability = pairing_config.delegate.classic_io_capability
5791
5905
 
5792
5906
  # Respond
5793
- if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
5907
+ if io_capability == hci.IoCapability.KEYBOARD_ONLY:
5794
5908
  # Ask the user to enter a string
5795
5909
  async def get_pin_code():
5796
- pin_code = await utils.cancel_on_event(
5797
- connection,
5798
- Connection.EVENT_DISCONNECTION,
5799
- pairing_config.delegate.get_string(16),
5910
+ pin_code = await connection.cancel_on_disconnection(
5911
+ pairing_config.delegate.get_string(16)
5800
5912
  )
5801
5913
 
5802
5914
  if pin_code is not None:
@@ -5834,10 +5946,8 @@ class Device(utils.CompositeEventEmitter):
5834
5946
  pairing_config = self.pairing_config_factory(connection)
5835
5947
 
5836
5948
  # Show the passkey to the user
5837
- utils.cancel_on_event(
5838
- connection,
5839
- Connection.EVENT_DISCONNECTION,
5840
- pairing_config.delegate.display_number(passkey, digits=6),
5949
+ connection.cancel_on_disconnection(
5950
+ pairing_config.delegate.display_number(passkey, digits=6)
5841
5951
  )
5842
5952
 
5843
5953
  # [Classic only]
@@ -5924,24 +6034,63 @@ class Device(utils.CompositeEventEmitter):
5924
6034
  f'cis_id=[0x{cis_id:02X}] ***'
5925
6035
  )
5926
6036
  # LE_CIS_Established event doesn't provide info, so we must store them here.
5927
- self.cis_links[cis_handle] = CisLink(
6037
+ cis_link = CisLink(
5928
6038
  device=self,
5929
6039
  acl_connection=acl_connection,
5930
6040
  handle=cis_handle,
5931
6041
  cig_id=cig_id,
5932
6042
  cis_id=cis_id,
5933
6043
  )
5934
- self.emit(self.EVENT_CIS_REQUEST, acl_connection, cis_handle, cig_id, cis_id)
6044
+ self.cis_links[cis_handle] = cis_link
6045
+ acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
6046
+ self.emit(self.EVENT_CIS_REQUEST, cis_link)
5935
6047
 
5936
6048
  # [LE only]
5937
6049
  @host_event_handler
5938
6050
  @utils.experimental('Only for testing')
5939
- def on_cis_establishment(self, cis_handle: int) -> None:
6051
+ def on_cis_establishment(
6052
+ self,
6053
+ cis_handle: int,
6054
+ cig_sync_delay: int,
6055
+ cis_sync_delay: int,
6056
+ transport_latency_c_to_p: int,
6057
+ transport_latency_p_to_c: int,
6058
+ phy_c_to_p: int,
6059
+ phy_p_to_c: int,
6060
+ nse: int,
6061
+ bn_c_to_p: int,
6062
+ bn_p_to_c: int,
6063
+ ft_c_to_p: int,
6064
+ ft_p_to_c: int,
6065
+ max_pdu_c_to_p: int,
6066
+ max_pdu_p_to_c: int,
6067
+ iso_interval: int,
6068
+ ) -> None:
6069
+ if cis_handle not in self.cis_links:
6070
+ logger.warning("CIS link not found")
6071
+ return
6072
+
5940
6073
  cis_link = self.cis_links[cis_handle]
5941
6074
  cis_link.state = CisLink.State.ESTABLISHED
5942
6075
 
5943
6076
  assert cis_link.acl_connection
5944
6077
 
6078
+ # Update the CIS
6079
+ cis_link.cig_sync_delay = cig_sync_delay
6080
+ cis_link.cis_sync_delay = cis_sync_delay
6081
+ cis_link.transport_latency_c_to_p = transport_latency_c_to_p
6082
+ cis_link.transport_latency_p_to_c = transport_latency_p_to_c
6083
+ cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
6084
+ cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
6085
+ cis_link.nse = nse
6086
+ cis_link.bn_c_to_p = bn_c_to_p
6087
+ cis_link.bn_p_to_c = bn_p_to_c
6088
+ cis_link.ft_c_to_p = ft_c_to_p
6089
+ cis_link.ft_p_to_c = ft_p_to_c
6090
+ cis_link.max_pdu_c_to_p = max_pdu_c_to_p
6091
+ cis_link.max_pdu_p_to_c = max_pdu_p_to_c
6092
+ cis_link.iso_interval = iso_interval * 1.25
6093
+
5945
6094
  logger.debug(
5946
6095
  f'*** CIS Establishment '
5947
6096
  f'{cis_link.acl_connection.peer_address}, '
@@ -5951,16 +6100,27 @@ class Device(utils.CompositeEventEmitter):
5951
6100
  )
5952
6101
 
5953
6102
  cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
6103
+ cis_link.acl_connection.emit(
6104
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
6105
+ )
5954
6106
  self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
5955
6107
 
5956
6108
  # [LE only]
5957
6109
  @host_event_handler
5958
6110
  @utils.experimental('Only for testing')
5959
6111
  def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
6112
+ if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
6113
+ logger.warning("CIS link not found")
6114
+ return
6115
+
5960
6116
  logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
5961
- if cis_link := self.cis_links.pop(cis_handle):
5962
- cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
5963
- self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_handle, status)
6117
+ cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
6118
+ cis_link.acl_connection.emit(
6119
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
6120
+ cis_link,
6121
+ status,
6122
+ )
6123
+ self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
5964
6124
 
5965
6125
  # [LE only]
5966
6126
  @host_event_handler
@@ -5974,7 +6134,7 @@ class Device(utils.CompositeEventEmitter):
5974
6134
  @host_event_handler
5975
6135
  @with_connection_from_handle
5976
6136
  def on_connection_encryption_change(
5977
- self, connection, encryption, encryption_key_size
6137
+ self, connection: Connection, encryption: int, encryption_key_size: int
5978
6138
  ):
5979
6139
  logger.debug(
5980
6140
  f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
@@ -5987,14 +6147,14 @@ class Device(utils.CompositeEventEmitter):
5987
6147
  if (
5988
6148
  not connection.authenticated
5989
6149
  and connection.transport == PhysicalTransport.BR_EDR
5990
- and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
6150
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
5991
6151
  ):
5992
6152
  connection.authenticated = True
5993
6153
  connection.sc = True
5994
6154
  if (
5995
6155
  not connection.authenticated
5996
6156
  and connection.transport == PhysicalTransport.LE
5997
- and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
6157
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
5998
6158
  ):
5999
6159
  connection.authenticated = True
6000
6160
  connection.sc = True
@@ -6021,13 +6181,19 @@ class Device(utils.CompositeEventEmitter):
6021
6181
 
6022
6182
  @host_event_handler
6023
6183
  @with_connection_from_handle
6024
- def on_connection_parameters_update(self, connection, connection_parameters):
6184
+ def on_connection_parameters_update(
6185
+ self, connection: Connection, connection_parameters: core.ConnectionParameters
6186
+ ):
6025
6187
  logger.debug(
6026
6188
  f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
6027
6189
  f'{connection.peer_address} as {connection.role_name}, '
6028
6190
  f'{connection_parameters}'
6029
6191
  )
6030
- connection.parameters = connection_parameters
6192
+ connection.parameters = Connection.Parameters(
6193
+ connection_parameters.connection_interval * 1.25,
6194
+ connection_parameters.peripheral_latency,
6195
+ connection_parameters.supervision_timeout * 10.0,
6196
+ )
6031
6197
  connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
6032
6198
 
6033
6199
  @host_event_handler