bumble 0.0.212__py3-none-any.whl → 0.0.214__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 (92) 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 +14 -11
  5. bumble/apps/bench.py +482 -37
  6. bumble/apps/console.py +3 -3
  7. bumble/apps/controller_info.py +44 -12
  8. bumble/apps/controller_loopback.py +7 -7
  9. bumble/apps/controllers.py +4 -5
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +5 -5
  12. bumble/apps/gg_bridge.py +5 -5
  13. bumble/apps/hci_bridge.py +5 -4
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +8 -3
  16. bumble/apps/pair.py +19 -11
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/player/player.py +2 -3
  19. bumble/apps/rfcomm_bridge.py +3 -4
  20. bumble/apps/scan.py +4 -5
  21. bumble/apps/show.py +6 -4
  22. bumble/apps/speaker/speaker.html +1 -0
  23. bumble/apps/speaker/speaker.js +113 -62
  24. bumble/apps/speaker/speaker.py +123 -19
  25. bumble/apps/unbond.py +2 -3
  26. bumble/apps/usb_probe.py +2 -3
  27. bumble/at.py +4 -4
  28. bumble/att.py +2 -6
  29. bumble/avc.py +7 -7
  30. bumble/avctp.py +3 -3
  31. bumble/avdtp.py +16 -20
  32. bumble/avrcp.py +42 -54
  33. bumble/colors.py +2 -2
  34. bumble/controller.py +174 -45
  35. bumble/device.py +398 -182
  36. bumble/drivers/__init__.py +2 -2
  37. bumble/drivers/common.py +0 -2
  38. bumble/drivers/intel.py +37 -40
  39. bumble/drivers/rtk.py +28 -35
  40. bumble/gatt.py +4 -4
  41. bumble/gatt_adapters.py +4 -5
  42. bumble/gatt_client.py +26 -31
  43. bumble/gatt_server.py +7 -11
  44. bumble/hci.py +2648 -2909
  45. bumble/helpers.py +4 -5
  46. bumble/hfp.py +32 -37
  47. bumble/host.py +104 -35
  48. bumble/keys.py +5 -5
  49. bumble/l2cap.py +312 -409
  50. bumble/link.py +16 -280
  51. bumble/logging.py +65 -0
  52. bumble/pairing.py +23 -20
  53. bumble/pandora/__init__.py +2 -2
  54. bumble/pandora/config.py +2 -2
  55. bumble/pandora/device.py +6 -6
  56. bumble/pandora/host.py +27 -28
  57. bumble/pandora/l2cap.py +2 -2
  58. bumble/pandora/security.py +6 -6
  59. bumble/pandora/utils.py +3 -3
  60. bumble/profiles/ams.py +404 -0
  61. bumble/profiles/ascs.py +142 -131
  62. bumble/profiles/asha.py +2 -2
  63. bumble/profiles/bap.py +3 -4
  64. bumble/profiles/csip.py +2 -2
  65. bumble/profiles/device_information_service.py +2 -2
  66. bumble/profiles/gap.py +2 -2
  67. bumble/profiles/hap.py +34 -33
  68. bumble/profiles/le_audio.py +4 -4
  69. bumble/profiles/mcp.py +4 -4
  70. bumble/profiles/vcs.py +3 -5
  71. bumble/rfcomm.py +10 -10
  72. bumble/rtp.py +1 -2
  73. bumble/sdp.py +2 -2
  74. bumble/smp.py +62 -63
  75. bumble/tools/intel_util.py +3 -2
  76. bumble/tools/rtk_util.py +6 -5
  77. bumble/transport/__init__.py +2 -16
  78. bumble/transport/android_netsim.py +5 -5
  79. bumble/transport/common.py +4 -4
  80. bumble/transport/pyusb.py +2 -2
  81. bumble/utils.py +2 -5
  82. bumble/vendor/android/hci.py +118 -200
  83. bumble/vendor/zephyr/hci.py +32 -27
  84. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/METADATA +4 -3
  85. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
  86. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
  87. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -1
  88. bumble/apps/link_relay/__init__.py +0 -0
  89. bumble/apps/link_relay/link_relay.py +0 -289
  90. bumble/apps/link_relay/logging.yml +0 -21
  91. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
  92. {bumble-0.0.212.dist-info → bumble-0.0.214.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
+ )
214
220
  ),
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,
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
+ )
231
+ ),
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,11 @@ 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"
1755
+ EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
1756
+ EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
1632
1757
 
1633
1758
  @utils.composite_listener
1634
1759
  class Listener:
@@ -1664,6 +1789,12 @@ class Connection(utils.CompositeEventEmitter):
1664
1789
  connection_interval: float # Connection interval, in milliseconds. [LE only]
1665
1790
  peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
1666
1791
  supervision_timeout: float # Supervision timeout, in milliseconds.
1792
+ subrate_factor: int = (
1793
+ 1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
1794
+ )
1795
+ continuation_number: int = (
1796
+ 0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
1797
+ )
1667
1798
 
1668
1799
  def __init__(
1669
1800
  self,
@@ -1884,6 +2015,12 @@ class Connection(utils.CompositeEventEmitter):
1884
2015
  def data_packet_queue(self) -> DataPacketQueue | None:
1885
2016
  return self.device.host.get_data_packet_queue(self.handle)
1886
2017
 
2018
+ def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
2019
+ """
2020
+ Helper method to call `utils.cancel_on_event` for the 'disconnection' event
2021
+ """
2022
+ return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
2023
+
1887
2024
  async def __aenter__(self):
1888
2025
  return self
1889
2026
 
@@ -1929,6 +2066,7 @@ class DeviceConfiguration:
1929
2066
  le_simultaneous_enabled: bool = False
1930
2067
  le_privacy_enabled: bool = False
1931
2068
  le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
2069
+ le_subrate_enabled: bool = False
1932
2070
  classic_enabled: bool = False
1933
2071
  classic_sc_enabled: bool = True
1934
2072
  classic_ssp_enabled: bool = True
@@ -1954,9 +2092,9 @@ class DeviceConfiguration:
1954
2092
  gatt_service_enabled: bool = True
1955
2093
 
1956
2094
  def __post_init__(self) -> None:
1957
- self.gatt_services: list[Dict[str, Any]] = []
2095
+ self.gatt_services: list[dict[str, Any]] = []
1958
2096
 
1959
- def load_from_dict(self, config: Dict[str, Any]) -> None:
2097
+ def load_from_dict(self, config: dict[str, Any]) -> None:
1960
2098
  config = copy.deepcopy(config)
1961
2099
 
1962
2100
  # Load simple properties
@@ -2016,13 +2154,13 @@ class DeviceConfiguration:
2016
2154
  self.load_from_dict(json.load(file))
2017
2155
 
2018
2156
  @classmethod
2019
- def from_file(cls: Type[Self], filename: str) -> Self:
2157
+ def from_file(cls: type[Self], filename: str) -> Self:
2020
2158
  config = cls()
2021
2159
  config.load_from_file(filename)
2022
2160
  return config
2023
2161
 
2024
2162
  @classmethod
2025
- def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
2163
+ def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
2026
2164
  device_config = cls()
2027
2165
  device_config.load_from_dict(config)
2028
2166
  return device_config
@@ -2119,22 +2257,22 @@ class Device(utils.CompositeEventEmitter):
2119
2257
  advertising_data: bytes
2120
2258
  scan_response_data: bytes
2121
2259
  cs_capabilities: ChannelSoundingCapabilities | None = None
2122
- connections: Dict[int, Connection]
2123
- pending_connections: Dict[hci.Address, Connection]
2124
- classic_pending_accepts: Dict[
2260
+ connections: dict[int, Connection]
2261
+ pending_connections: dict[hci.Address, Connection]
2262
+ classic_pending_accepts: dict[
2125
2263
  hci.Address,
2126
2264
  list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
2127
2265
  ]
2128
- advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
2266
+ advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
2129
2267
  periodic_advertising_syncs: list[PeriodicAdvertisingSync]
2130
2268
  config: DeviceConfiguration
2131
2269
  legacy_advertiser: Optional[LegacyAdvertiser]
2132
- sco_links: Dict[int, ScoLink]
2133
- cis_links: Dict[int, CisLink]
2270
+ sco_links: dict[int, ScoLink]
2271
+ cis_links: dict[int, CisLink]
2134
2272
  bigs: dict[int, Big]
2135
2273
  bis_links: dict[int, BisLink]
2136
2274
  big_syncs: dict[int, BigSync]
2137
- _pending_cis: Dict[int, tuple[int, int]]
2275
+ _pending_cis: dict[int, tuple[int, int]]
2138
2276
  gatt_service: gatt_service.GenericAttributeProfileService | None = None
2139
2277
 
2140
2278
  EVENT_ADVERTISEMENT = "advertisement"
@@ -2281,6 +2419,7 @@ class Device(utils.CompositeEventEmitter):
2281
2419
  self.le_privacy_enabled = config.le_privacy_enabled
2282
2420
  self.le_rpa_timeout = config.le_rpa_timeout
2283
2421
  self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
2422
+ self.le_subrate_enabled = config.le_subrate_enabled
2284
2423
  self.classic_enabled = config.classic_enabled
2285
2424
  self.cis_enabled = config.cis_enabled
2286
2425
  self.classic_sc_enabled = config.classic_sc_enabled
@@ -2294,8 +2433,8 @@ class Device(utils.CompositeEventEmitter):
2294
2433
  self.address_generation_offload = config.address_generation_offload
2295
2434
 
2296
2435
  # Extended advertising.
2297
- self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2298
- self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2436
+ self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
2437
+ self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
2299
2438
 
2300
2439
  # Legacy advertising.
2301
2440
  # The advertising and scan response data, as well as the advertising interval
@@ -2660,6 +2799,15 @@ class Device(utils.CompositeEventEmitter):
2660
2799
  check_result=True,
2661
2800
  )
2662
2801
 
2802
+ if self.le_subrate_enabled:
2803
+ await self.send_command(
2804
+ hci.HCI_LE_Set_Host_Feature_Command(
2805
+ bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
2806
+ bit_value=1,
2807
+ ),
2808
+ check_result=True,
2809
+ )
2810
+
2663
2811
  if self.config.channel_sounding_enabled:
2664
2812
  await self.send_command(
2665
2813
  hci.HCI_LE_Set_Host_Feature_Command(
@@ -4271,11 +4419,11 @@ class Device(utils.CompositeEventEmitter):
4271
4419
  self.smp_manager.pairing_config_factory = pairing_config_factory
4272
4420
 
4273
4421
  @property
4274
- def smp_session_proxy(self) -> Type[smp.Session]:
4422
+ def smp_session_proxy(self) -> type[smp.Session]:
4275
4423
  return self.smp_manager.session_proxy
4276
4424
 
4277
4425
  @smp_session_proxy.setter
4278
- def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
4426
+ def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
4279
4427
  self.smp_manager.session_proxy = session_proxy
4280
4428
 
4281
4429
  async def pair(self, connection):
@@ -4359,9 +4507,7 @@ class Device(utils.CompositeEventEmitter):
4359
4507
  raise hci.HCI_StatusError(result)
4360
4508
 
4361
4509
  # Wait for the authentication to complete
4362
- await utils.cancel_on_event(
4363
- connection, Connection.EVENT_DISCONNECTION, pending_authentication
4364
- )
4510
+ await connection.cancel_on_disconnection(pending_authentication)
4365
4511
  finally:
4366
4512
  connection.remove_listener(
4367
4513
  connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
@@ -4448,9 +4594,7 @@ class Device(utils.CompositeEventEmitter):
4448
4594
  raise hci.HCI_StatusError(result)
4449
4595
 
4450
4596
  # Wait for the result
4451
- await utils.cancel_on_event(
4452
- connection, Connection.EVENT_DISCONNECTION, pending_encryption
4453
- )
4597
+ await connection.cancel_on_disconnection(pending_encryption)
4454
4598
  finally:
4455
4599
  connection.remove_listener(
4456
4600
  connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
@@ -4494,9 +4638,7 @@ class Device(utils.CompositeEventEmitter):
4494
4638
  f'{hci.HCI_Constant.error_name(result.status)}'
4495
4639
  )
4496
4640
  raise hci.HCI_StatusError(result)
4497
- await utils.cancel_on_event(
4498
- connection, Connection.EVENT_DISCONNECTION, pending_role_change
4499
- )
4641
+ await connection.cancel_on_disconnection(pending_role_change)
4500
4642
  finally:
4501
4643
  connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
4502
4644
  connection.remove_listener(
@@ -4556,48 +4698,39 @@ class Device(utils.CompositeEventEmitter):
4556
4698
  @utils.experimental('Only for testing.')
4557
4699
  async def setup_cig(
4558
4700
  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],
4701
+ parameters: CigParameters,
4566
4702
  ) -> list[int]:
4567
4703
  """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
4568
4704
 
4569
4705
  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).
4706
+ parameters: CIG parameters.
4578
4707
 
4579
4708
  Returns:
4580
4709
  List of created CIS handles corresponding to the same order of [cid_id].
4581
4710
  """
4582
- num_cis = len(cis_id)
4711
+ num_cis = len(parameters.cis_parameters)
4583
4712
 
4584
4713
  response = await self.send_command(
4585
4714
  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,
4715
+ cig_id=parameters.cig_id,
4716
+ sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
4717
+ sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
4718
+ worst_case_sca=parameters.worst_case_sca,
4719
+ packing=int(parameters.packing),
4720
+ framing=int(parameters.framing),
4721
+ max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
4722
+ max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
4723
+ cis_id=[cis.cis_id for cis in parameters.cis_parameters],
4724
+ max_sdu_c_to_p=[
4725
+ cis.max_sdu_c_to_p for cis in parameters.cis_parameters
4726
+ ],
4727
+ max_sdu_p_to_c=[
4728
+ cis.max_sdu_p_to_c for cis in parameters.cis_parameters
4729
+ ],
4730
+ phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
4731
+ phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
4732
+ rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
4733
+ rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
4601
4734
  ),
4602
4735
  check_result=True,
4603
4736
  )
@@ -4605,19 +4738,17 @@ class Device(utils.CompositeEventEmitter):
4605
4738
  # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
4606
4739
  # Server, so here it only provides a basic functionality for testing.
4607
4740
  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)
4741
+ for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
4742
+ self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
4610
4743
 
4611
4744
  return cis_handles
4612
4745
 
4613
4746
  # [LE only]
4614
4747
  @utils.experimental('Only for testing.')
4615
4748
  async def create_cis(
4616
- self, cis_acl_pairs: Sequence[tuple[int, int]]
4749
+ self, cis_acl_pairs: Sequence[tuple[int, Connection]]
4617
4750
  ) -> list[CisLink]:
4618
- for cis_handle, acl_handle in cis_acl_pairs:
4619
- acl_connection = self.lookup_connection(acl_handle)
4620
- assert acl_connection
4751
+ for cis_handle, acl_connection in cis_acl_pairs:
4621
4752
  cis_id, cig_id = self._pending_cis.pop(cis_handle)
4622
4753
  self.cis_links[cis_handle] = CisLink(
4623
4754
  device=self,
@@ -4637,8 +4768,8 @@ class Device(utils.CompositeEventEmitter):
4637
4768
  if pending_future := pending_cis_establishments.get(cis_link.handle):
4638
4769
  pending_future.set_result(cis_link)
4639
4770
 
4640
- def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
4641
- if pending_future := pending_cis_establishments.get(cis_handle):
4771
+ def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
4772
+ if pending_future := pending_cis_establishments.get(cis_link.handle):
4642
4773
  pending_future.set_exception(hci.HCI_Error(status))
4643
4774
 
4644
4775
  watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
@@ -4648,7 +4779,7 @@ class Device(utils.CompositeEventEmitter):
4648
4779
  await self.send_command(
4649
4780
  hci.HCI_LE_Create_CIS_Command(
4650
4781
  cis_connection_handle=[p[0] for p in cis_acl_pairs],
4651
- acl_connection_handle=[p[1] for p in cis_acl_pairs],
4782
+ acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
4652
4783
  ),
4653
4784
  check_result=True,
4654
4785
  )
@@ -4657,26 +4788,21 @@ class Device(utils.CompositeEventEmitter):
4657
4788
 
4658
4789
  # [LE only]
4659
4790
  @utils.experimental('Only for testing.')
4660
- async def accept_cis_request(self, handle: int) -> CisLink:
4791
+ async def accept_cis_request(self, cis_link: CisLink) -> None:
4661
4792
  """[LE Only] Accepts an incoming CIS request.
4662
4793
 
4663
- When the specified CIS handle is already created, this method returns the
4664
- existed CIS link object immediately.
4794
+ This method returns when the CIS is established, or raises an exception if
4795
+ the CIS establishment fails.
4665
4796
 
4666
4797
  Args:
4667
4798
  handle: CIS handle to accept.
4668
-
4669
- Returns:
4670
- CIS link object on the given handle.
4671
4799
  """
4672
- if not (cis_link := self.cis_links.get(handle)):
4673
- raise InvalidStateError(f'No pending CIS request of handle {handle}')
4674
4800
 
4675
4801
  # There might be multiple ASE sharing a CIS channel.
4676
4802
  # If one of them has accepted the request, the others should just leverage it.
4677
4803
  async with self._cis_lock:
4678
4804
  if cis_link.state == CisLink.State.ESTABLISHED:
4679
- return cis_link
4805
+ return
4680
4806
 
4681
4807
  with closing(utils.EventWatcher()) as watcher:
4682
4808
  pending_establishment = asyncio.get_running_loop().create_future()
@@ -4695,26 +4821,24 @@ class Device(utils.CompositeEventEmitter):
4695
4821
  )
4696
4822
 
4697
4823
  await self.send_command(
4698
- hci.HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
4824
+ hci.HCI_LE_Accept_CIS_Request_Command(
4825
+ connection_handle=cis_link.handle
4826
+ ),
4699
4827
  check_result=True,
4700
4828
  )
4701
4829
 
4702
4830
  await pending_establishment
4703
- return cis_link
4704
-
4705
- # Mypy believes this is reachable when context is an ExitStack.
4706
- raise UnreachableError()
4707
4831
 
4708
4832
  # [LE only]
4709
4833
  @utils.experimental('Only for testing.')
4710
4834
  async def reject_cis_request(
4711
4835
  self,
4712
- handle: int,
4836
+ cis_link: CisLink,
4713
4837
  reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
4714
4838
  ) -> None:
4715
4839
  await self.send_command(
4716
4840
  hci.HCI_LE_Reject_CIS_Request_Command(
4717
- connection_handle=handle, reason=reason
4841
+ connection_handle=cis_link.handle, reason=reason
4718
4842
  ),
4719
4843
  check_result=True,
4720
4844
  )
@@ -5071,8 +5195,8 @@ class Device(utils.CompositeEventEmitter):
5071
5195
  # Store the keys in the key store
5072
5196
  if self.keystore:
5073
5197
  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,
5198
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
5199
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
5076
5200
  )
5077
5201
  pairing_keys = PairingKeys(
5078
5202
  link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
@@ -5252,7 +5376,7 @@ class Device(utils.CompositeEventEmitter):
5252
5376
  big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
5253
5377
  big.big_sync_delay = big_sync_delay
5254
5378
  big.transport_latency_big = transport_latency_big
5255
- big.phy = phy
5379
+ big.phy = hci.Phy(phy)
5256
5380
  big.nse = nse
5257
5381
  big.bn = bn
5258
5382
  big.pto = pto
@@ -5519,8 +5643,8 @@ class Device(utils.CompositeEventEmitter):
5519
5643
 
5520
5644
  # Handle SCO request.
5521
5645
  if link_type in (
5522
- hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE,
5523
- hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
5646
+ hci.HCI_Connection_Complete_Event.LinkType.SCO,
5647
+ hci.HCI_Connection_Complete_Event.LinkType.ESCO,
5524
5648
  ):
5525
5649
  if connection := self.find_connection_by_bd_addr(
5526
5650
  bd_addr, transport=PhysicalTransport.BR_EDR
@@ -5628,7 +5752,7 @@ class Device(utils.CompositeEventEmitter):
5628
5752
  # [Classic only]
5629
5753
  @host_event_handler
5630
5754
  @with_connection_from_address
5631
- def on_authentication_io_capability_request(self, connection):
5755
+ def on_authentication_io_capability_request(self, connection: Connection):
5632
5756
  # Ask what the pairing config should be for this connection
5633
5757
  pairing_config = self.pairing_config_factory(connection)
5634
5758
 
@@ -5636,13 +5760,13 @@ class Device(utils.CompositeEventEmitter):
5636
5760
  authentication_requirements = (
5637
5761
  # No Bonding
5638
5762
  (
5639
- hci.HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5640
- hci.HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5763
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
5764
+ hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
5641
5765
  ),
5642
5766
  # General Bonding
5643
5767
  (
5644
- hci.HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5645
- hci.HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5768
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
5769
+ hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
5646
5770
  ),
5647
5771
  )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
5648
5772
 
@@ -5697,30 +5821,30 @@ class Device(utils.CompositeEventEmitter):
5697
5821
  raise UnreachableError()
5698
5822
 
5699
5823
  # 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,
5824
+ methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
5825
+ hci.IoCapability.DISPLAY_ONLY: {
5826
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5827
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5828
+ hci.IoCapability.KEYBOARD_ONLY: na,
5829
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5706
5830
  },
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,
5831
+ hci.IoCapability.DISPLAY_YES_NO: {
5832
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5833
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5834
+ hci.IoCapability.KEYBOARD_ONLY: na,
5835
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5712
5836
  },
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,
5837
+ hci.IoCapability.KEYBOARD_ONLY: {
5838
+ hci.IoCapability.DISPLAY_ONLY: na,
5839
+ hci.IoCapability.DISPLAY_YES_NO: na,
5840
+ hci.IoCapability.KEYBOARD_ONLY: na,
5841
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5718
5842
  },
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,
5843
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: {
5844
+ hci.IoCapability.DISPLAY_ONLY: confirm,
5845
+ hci.IoCapability.DISPLAY_YES_NO: confirm,
5846
+ hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
5847
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5724
5848
  },
5725
5849
  }
5726
5850
 
@@ -5728,9 +5852,7 @@ class Device(utils.CompositeEventEmitter):
5728
5852
 
5729
5853
  async def reply() -> None:
5730
5854
  try:
5731
- if await utils.cancel_on_event(
5732
- connection, Connection.EVENT_DISCONNECTION, method()
5733
- ):
5855
+ if await connection.cancel_on_disconnection(method()):
5734
5856
  await self.host.send_command(
5735
5857
  hci.HCI_User_Confirmation_Request_Reply_Command(
5736
5858
  bd_addr=connection.peer_address
@@ -5757,10 +5879,8 @@ class Device(utils.CompositeEventEmitter):
5757
5879
 
5758
5880
  async def reply() -> None:
5759
5881
  try:
5760
- number = await utils.cancel_on_event(
5761
- connection,
5762
- Connection.EVENT_DISCONNECTION,
5763
- pairing_config.delegate.get_number(),
5882
+ number = await connection.cancel_on_disconnection(
5883
+ pairing_config.delegate.get_number()
5764
5884
  )
5765
5885
  if number is not None:
5766
5886
  await self.host.send_command(
@@ -5780,6 +5900,19 @@ class Device(utils.CompositeEventEmitter):
5780
5900
 
5781
5901
  utils.AsyncRunner.spawn(reply())
5782
5902
 
5903
+ # [Classic only]
5904
+ @host_event_handler
5905
+ @with_connection_from_handle
5906
+ def on_mode_change(
5907
+ self, connection: Connection, status: int, current_mode: int, interval: int
5908
+ ):
5909
+ if status == hci.HCI_SUCCESS:
5910
+ connection.classic_mode = current_mode
5911
+ connection.classic_interval = interval
5912
+ connection.emit(connection.EVENT_MODE_CHANGE)
5913
+ else:
5914
+ connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
5915
+
5783
5916
  # [Classic only]
5784
5917
  @host_event_handler
5785
5918
  @with_connection_from_address
@@ -5790,13 +5923,11 @@ class Device(utils.CompositeEventEmitter):
5790
5923
  io_capability = pairing_config.delegate.classic_io_capability
5791
5924
 
5792
5925
  # Respond
5793
- if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
5926
+ if io_capability == hci.IoCapability.KEYBOARD_ONLY:
5794
5927
  # Ask the user to enter a string
5795
5928
  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),
5929
+ pin_code = await connection.cancel_on_disconnection(
5930
+ pairing_config.delegate.get_string(16)
5800
5931
  )
5801
5932
 
5802
5933
  if pin_code is not None:
@@ -5834,10 +5965,8 @@ class Device(utils.CompositeEventEmitter):
5834
5965
  pairing_config = self.pairing_config_factory(connection)
5835
5966
 
5836
5967
  # 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),
5968
+ connection.cancel_on_disconnection(
5969
+ pairing_config.delegate.display_number(passkey, digits=6)
5841
5970
  )
5842
5971
 
5843
5972
  # [Classic only]
@@ -5924,24 +6053,63 @@ class Device(utils.CompositeEventEmitter):
5924
6053
  f'cis_id=[0x{cis_id:02X}] ***'
5925
6054
  )
5926
6055
  # LE_CIS_Established event doesn't provide info, so we must store them here.
5927
- self.cis_links[cis_handle] = CisLink(
6056
+ cis_link = CisLink(
5928
6057
  device=self,
5929
6058
  acl_connection=acl_connection,
5930
6059
  handle=cis_handle,
5931
6060
  cig_id=cig_id,
5932
6061
  cis_id=cis_id,
5933
6062
  )
5934
- self.emit(self.EVENT_CIS_REQUEST, acl_connection, cis_handle, cig_id, cis_id)
6063
+ self.cis_links[cis_handle] = cis_link
6064
+ acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
6065
+ self.emit(self.EVENT_CIS_REQUEST, cis_link)
5935
6066
 
5936
6067
  # [LE only]
5937
6068
  @host_event_handler
5938
6069
  @utils.experimental('Only for testing')
5939
- def on_cis_establishment(self, cis_handle: int) -> None:
6070
+ def on_cis_establishment(
6071
+ self,
6072
+ cis_handle: int,
6073
+ cig_sync_delay: int,
6074
+ cis_sync_delay: int,
6075
+ transport_latency_c_to_p: int,
6076
+ transport_latency_p_to_c: int,
6077
+ phy_c_to_p: int,
6078
+ phy_p_to_c: int,
6079
+ nse: int,
6080
+ bn_c_to_p: int,
6081
+ bn_p_to_c: int,
6082
+ ft_c_to_p: int,
6083
+ ft_p_to_c: int,
6084
+ max_pdu_c_to_p: int,
6085
+ max_pdu_p_to_c: int,
6086
+ iso_interval: int,
6087
+ ) -> None:
6088
+ if cis_handle not in self.cis_links:
6089
+ logger.warning("CIS link not found")
6090
+ return
6091
+
5940
6092
  cis_link = self.cis_links[cis_handle]
5941
6093
  cis_link.state = CisLink.State.ESTABLISHED
5942
6094
 
5943
6095
  assert cis_link.acl_connection
5944
6096
 
6097
+ # Update the CIS
6098
+ cis_link.cig_sync_delay = cig_sync_delay
6099
+ cis_link.cis_sync_delay = cis_sync_delay
6100
+ cis_link.transport_latency_c_to_p = transport_latency_c_to_p
6101
+ cis_link.transport_latency_p_to_c = transport_latency_p_to_c
6102
+ cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
6103
+ cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
6104
+ cis_link.nse = nse
6105
+ cis_link.bn_c_to_p = bn_c_to_p
6106
+ cis_link.bn_p_to_c = bn_p_to_c
6107
+ cis_link.ft_c_to_p = ft_c_to_p
6108
+ cis_link.ft_p_to_c = ft_p_to_c
6109
+ cis_link.max_pdu_c_to_p = max_pdu_c_to_p
6110
+ cis_link.max_pdu_p_to_c = max_pdu_p_to_c
6111
+ cis_link.iso_interval = iso_interval * 1.25
6112
+
5945
6113
  logger.debug(
5946
6114
  f'*** CIS Establishment '
5947
6115
  f'{cis_link.acl_connection.peer_address}, '
@@ -5951,16 +6119,27 @@ class Device(utils.CompositeEventEmitter):
5951
6119
  )
5952
6120
 
5953
6121
  cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
6122
+ cis_link.acl_connection.emit(
6123
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
6124
+ )
5954
6125
  self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
5955
6126
 
5956
6127
  # [LE only]
5957
6128
  @host_event_handler
5958
6129
  @utils.experimental('Only for testing')
5959
6130
  def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
6131
+ if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
6132
+ logger.warning("CIS link not found")
6133
+ return
6134
+
5960
6135
  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)
6136
+ cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
6137
+ cis_link.acl_connection.emit(
6138
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
6139
+ cis_link,
6140
+ status,
6141
+ )
6142
+ self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
5964
6143
 
5965
6144
  # [LE only]
5966
6145
  @host_event_handler
@@ -5974,7 +6153,7 @@ class Device(utils.CompositeEventEmitter):
5974
6153
  @host_event_handler
5975
6154
  @with_connection_from_handle
5976
6155
  def on_connection_encryption_change(
5977
- self, connection, encryption, encryption_key_size
6156
+ self, connection: Connection, encryption: int, encryption_key_size: int
5978
6157
  ):
5979
6158
  logger.debug(
5980
6159
  f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
@@ -5987,14 +6166,14 @@ class Device(utils.CompositeEventEmitter):
5987
6166
  if (
5988
6167
  not connection.authenticated
5989
6168
  and connection.transport == PhysicalTransport.BR_EDR
5990
- and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
6169
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
5991
6170
  ):
5992
6171
  connection.authenticated = True
5993
6172
  connection.sc = True
5994
6173
  if (
5995
6174
  not connection.authenticated
5996
6175
  and connection.transport == PhysicalTransport.LE
5997
- and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
6176
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
5998
6177
  ):
5999
6178
  connection.authenticated = True
6000
6179
  connection.sc = True
@@ -6021,13 +6200,31 @@ class Device(utils.CompositeEventEmitter):
6021
6200
 
6022
6201
  @host_event_handler
6023
6202
  @with_connection_from_handle
6024
- def on_connection_parameters_update(self, connection, connection_parameters):
6203
+ def on_connection_parameters_update(
6204
+ self, connection: Connection, connection_parameters: core.ConnectionParameters
6205
+ ):
6025
6206
  logger.debug(
6026
6207
  f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
6027
6208
  f'{connection.peer_address} as {connection.role_name}, '
6028
6209
  f'{connection_parameters}'
6029
6210
  )
6030
- connection.parameters = connection_parameters
6211
+ if (
6212
+ connection.parameters.connection_interval
6213
+ != connection_parameters.connection_interval * 1.25
6214
+ ):
6215
+ connection.parameters = Connection.Parameters(
6216
+ connection_parameters.connection_interval * 1.25,
6217
+ connection_parameters.peripheral_latency,
6218
+ connection_parameters.supervision_timeout * 10.0,
6219
+ )
6220
+ else:
6221
+ connection.parameters = Connection.Parameters(
6222
+ connection_parameters.connection_interval * 1.25,
6223
+ connection_parameters.peripheral_latency,
6224
+ connection_parameters.supervision_timeout * 10.0,
6225
+ connection.parameters.subrate_factor,
6226
+ connection.parameters.continuation_number,
6227
+ )
6031
6228
  connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
6032
6229
 
6033
6230
  @host_event_handler
@@ -6060,6 +6257,25 @@ class Device(utils.CompositeEventEmitter):
6060
6257
  )
6061
6258
  connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error)
6062
6259
 
6260
+ @host_event_handler
6261
+ @with_connection_from_handle
6262
+ def on_le_subrate_change(
6263
+ self,
6264
+ connection: Connection,
6265
+ subrate_factor: int,
6266
+ peripheral_latency: int,
6267
+ continuation_number: int,
6268
+ supervision_timeout: int,
6269
+ ):
6270
+ connection.parameters = Connection.Parameters(
6271
+ connection.parameters.connection_interval,
6272
+ peripheral_latency,
6273
+ supervision_timeout * 10.0,
6274
+ subrate_factor,
6275
+ continuation_number,
6276
+ )
6277
+ connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
6278
+
6063
6279
  @host_event_handler
6064
6280
  @with_connection_from_handle
6065
6281
  def on_connection_att_mtu_update(self, connection, att_mtu):