bumble 0.0.219__py3-none-any.whl → 0.0.221__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 (104) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -479
  4. bumble/apps/bench.py +4 -5
  5. bumble/apps/console.py +5 -10
  6. bumble/apps/controller_info.py +12 -7
  7. bumble/apps/controller_loopback.py +1 -2
  8. bumble/apps/device_info.py +2 -3
  9. bumble/apps/gatt_dump.py +0 -1
  10. bumble/apps/lea_unicast/app.py +1 -1
  11. bumble/apps/pair.py +49 -46
  12. bumble/apps/pandora_server.py +2 -2
  13. bumble/apps/player/player.py +10 -12
  14. bumble/apps/rfcomm_bridge.py +10 -11
  15. bumble/apps/scan.py +1 -3
  16. bumble/apps/speaker/speaker.py +3 -4
  17. bumble/at.py +4 -5
  18. bumble/att.py +91 -25
  19. bumble/audio/io.py +8 -6
  20. bumble/avc.py +1 -2
  21. bumble/avctp.py +2 -3
  22. bumble/avdtp.py +53 -57
  23. bumble/avrcp.py +25 -27
  24. bumble/codecs.py +15 -15
  25. bumble/colors.py +7 -8
  26. bumble/controller.py +1201 -643
  27. bumble/core.py +41 -49
  28. bumble/crypto/__init__.py +2 -1
  29. bumble/crypto/builtin.py +2 -8
  30. bumble/data_types.py +2 -1
  31. bumble/decoder.py +2 -3
  32. bumble/device.py +278 -325
  33. bumble/drivers/__init__.py +3 -2
  34. bumble/drivers/intel.py +6 -8
  35. bumble/drivers/rtk.py +1 -1
  36. bumble/gatt.py +9 -9
  37. bumble/gatt_adapters.py +6 -6
  38. bumble/gatt_client.py +110 -60
  39. bumble/gatt_server.py +209 -139
  40. bumble/hci.py +87 -74
  41. bumble/helpers.py +5 -5
  42. bumble/hfp.py +27 -26
  43. bumble/hid.py +9 -9
  44. bumble/host.py +44 -50
  45. bumble/keys.py +17 -17
  46. bumble/l2cap.py +1015 -218
  47. bumble/link.py +54 -284
  48. bumble/ll.py +200 -0
  49. bumble/lmp.py +324 -0
  50. bumble/pairing.py +14 -15
  51. bumble/pandora/__init__.py +2 -2
  52. bumble/pandora/device.py +6 -4
  53. bumble/pandora/host.py +19 -10
  54. bumble/pandora/l2cap.py +8 -9
  55. bumble/pandora/security.py +18 -16
  56. bumble/pandora/utils.py +4 -4
  57. bumble/profiles/aics.py +6 -8
  58. bumble/profiles/ams.py +3 -5
  59. bumble/profiles/ancs.py +11 -11
  60. bumble/profiles/ascs.py +5 -5
  61. bumble/profiles/asha.py +10 -9
  62. bumble/profiles/bass.py +9 -3
  63. bumble/profiles/battery_service.py +1 -2
  64. bumble/profiles/csip.py +9 -10
  65. bumble/profiles/device_information_service.py +16 -17
  66. bumble/profiles/gap.py +3 -4
  67. bumble/profiles/gatt_service.py +0 -1
  68. bumble/profiles/gmap.py +12 -13
  69. bumble/profiles/hap.py +3 -3
  70. bumble/profiles/heart_rate_service.py +7 -8
  71. bumble/profiles/le_audio.py +1 -1
  72. bumble/profiles/mcp.py +28 -28
  73. bumble/profiles/pacs.py +13 -17
  74. bumble/profiles/pbp.py +16 -0
  75. bumble/profiles/vcs.py +2 -2
  76. bumble/profiles/vocs.py +6 -9
  77. bumble/rfcomm.py +19 -18
  78. bumble/sdp.py +12 -11
  79. bumble/smp.py +20 -30
  80. bumble/snoop.py +12 -5
  81. bumble/tools/generate_company_id_list.py +1 -1
  82. bumble/tools/intel_util.py +2 -2
  83. bumble/tools/rtk_fw_download.py +1 -1
  84. bumble/tools/rtk_util.py +1 -1
  85. bumble/transport/__init__.py +1 -2
  86. bumble/transport/android_emulator.py +2 -3
  87. bumble/transport/android_netsim.py +49 -40
  88. bumble/transport/common.py +9 -9
  89. bumble/transport/file.py +1 -2
  90. bumble/transport/hci_socket.py +2 -3
  91. bumble/transport/pty.py +3 -5
  92. bumble/transport/pyusb.py +8 -5
  93. bumble/transport/serial.py +1 -2
  94. bumble/transport/vhci.py +1 -2
  95. bumble/transport/ws_server.py +2 -3
  96. bumble/utils.py +23 -14
  97. bumble/vendor/android/hci.py +4 -2
  98. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/METADATA +4 -3
  99. bumble-0.0.221.dist-info/RECORD +185 -0
  100. bumble-0.0.219.dist-info/RECORD +0 -183
  101. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
  102. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
  103. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
  104. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -25,20 +25,15 @@ import itertools
25
25
  import json
26
26
  import logging
27
27
  import secrets
28
- import sys
29
- from collections.abc import Iterable, Sequence
28
+ from collections.abc import Awaitable, Callable, Iterable, Sequence
30
29
  from contextlib import AsyncExitStack, asynccontextmanager, closing
31
30
  from dataclasses import dataclass, field
32
31
  from enum import Enum, IntEnum
33
32
  from typing import (
34
33
  TYPE_CHECKING,
35
34
  Any,
36
- Awaitable,
37
- Callable,
38
35
  ClassVar,
39
- Optional,
40
36
  TypeVar,
41
- Union,
42
37
  cast,
43
38
  overload,
44
39
  )
@@ -46,6 +41,7 @@ from typing import (
46
41
  from typing_extensions import Self
47
42
 
48
43
  from bumble import (
44
+ att,
49
45
  core,
50
46
  data_types,
51
47
  gatt,
@@ -58,7 +54,6 @@ from bumble import (
58
54
  smp,
59
55
  utils,
60
56
  )
61
- from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
62
57
  from bumble.colors import color
63
58
  from bumble.core import (
64
59
  AdvertisingData,
@@ -176,6 +171,7 @@ class Advertisement:
176
171
  )
177
172
  sid: int = 0
178
173
  data_bytes: bytes = b''
174
+ data: AdvertisingData = field(init=False)
179
175
 
180
176
  # Constants
181
177
  TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
@@ -189,7 +185,7 @@ class Advertisement:
189
185
  self.data = AdvertisingData.from_bytes(self.data_bytes)
190
186
 
191
187
  @classmethod
192
- def from_advertising_report(cls, report) -> Optional[Advertisement]:
188
+ def from_advertising_report(cls, report) -> Advertisement | None:
193
189
  if isinstance(report, hci.HCI_LE_Advertising_Report_Event.Report):
194
190
  return LegacyAdvertisement.from_advertising_report(report)
195
191
 
@@ -265,12 +261,22 @@ class ExtendedAdvertisement(Advertisement):
265
261
 
266
262
  # -----------------------------------------------------------------------------
267
263
  class AdvertisementDataAccumulator:
264
+ last_advertisement: Advertisement | None
265
+ last_data: bytes
266
+ passive: bool
267
+
268
268
  def __init__(self, passive: bool = False):
269
269
  self.passive = passive
270
270
  self.last_advertisement = None
271
271
  self.last_data = b''
272
272
 
273
- def update(self, report):
273
+ def update(
274
+ self,
275
+ report: (
276
+ hci.HCI_LE_Advertising_Report_Event.Report
277
+ | hci.HCI_LE_Extended_Advertising_Report_Event.Report
278
+ ),
279
+ ) -> Advertisement | None:
274
280
  advertisement = Advertisement.from_advertising_report(report)
275
281
  if advertisement is None:
276
282
  return None
@@ -283,10 +289,12 @@ class AdvertisementDataAccumulator:
283
289
  and not self.last_advertisement.is_scan_response
284
290
  ):
285
291
  # This is the response to a scannable advertisement
286
- result = Advertisement.from_advertising_report(report)
287
- result.is_connectable = self.last_advertisement.is_connectable
288
- result.is_scannable = True
289
- result.data = AdvertisingData.from_bytes(self.last_data + report.data)
292
+ if result := Advertisement.from_advertising_report(report):
293
+ result.is_connectable = self.last_advertisement.is_connectable
294
+ result.is_scannable = True
295
+ result.data = AdvertisingData.from_bytes(
296
+ self.last_data + report.data
297
+ )
290
298
  self.last_data = b''
291
299
  else:
292
300
  if (
@@ -473,6 +481,7 @@ class PeriodicAdvertisement:
473
481
  rssi: int = hci.HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
474
482
  is_truncated: bool = False
475
483
  data_bytes: bytes = b''
484
+ data: AdvertisingData | None = field(init=False)
476
485
 
477
486
  # Constants
478
487
  TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
@@ -593,11 +602,11 @@ class AdvertisingSet(utils.EventEmitter):
593
602
  device: Device
594
603
  advertising_handle: int
595
604
  auto_restart: bool
596
- random_address: Optional[hci.Address]
605
+ random_address: hci.Address | None
597
606
  advertising_parameters: AdvertisingParameters
598
607
  advertising_data: bytes
599
608
  scan_response_data: bytes
600
- periodic_advertising_parameters: Optional[PeriodicAdvertisingParameters]
609
+ periodic_advertising_parameters: PeriodicAdvertisingParameters | None
601
610
  periodic_advertising_data: bytes
602
611
  selected_tx_power: int = 0
603
612
  enabled: bool = False
@@ -844,7 +853,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
844
853
  TERMINATED = 6
845
854
 
846
855
  _state: State
847
- sync_handle: Optional[int]
856
+ sync_handle: int | None
848
857
  advertiser_address: hci.Address
849
858
  sid: int
850
859
  skip: int
@@ -857,8 +866,8 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
857
866
 
858
867
  EVENT_STATE_CHANGE = "state_change"
859
868
  EVENT_ESTABLISHMENT = "establishment"
869
+ EVENT_ESTABLISHMENT_ERROR = "establishment_error"
860
870
  EVENT_CANCELLATION = "cancellation"
861
- EVENT_ERROR = "error"
862
871
  EVENT_LOSS = "loss"
863
872
  EVENT_PERIODIC_ADVERTISEMENT = "periodic_advertisement"
864
873
  EVENT_BIGINFO_ADVERTISEMENT = "biginfo_advertisement"
@@ -907,7 +916,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
907
916
  hci.HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
908
917
  )
909
918
 
910
- response = await self.device.send_command(
919
+ await self.device.send_command(
911
920
  hci.HCI_LE_Periodic_Advertising_Create_Sync_Command(
912
921
  options=options,
913
922
  advertising_sid=self.sid,
@@ -916,10 +925,9 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
916
925
  skip=self.skip,
917
926
  sync_timeout=int(self.sync_timeout * 100),
918
927
  sync_cte_type=0,
919
- )
928
+ ),
929
+ check_result=True,
920
930
  )
921
- if response.status != hci.HCI_Command_Status_Event.PENDING:
922
- raise hci.HCI_StatusError(response)
923
931
 
924
932
  self.state = self.State.PENDING
925
933
 
@@ -992,7 +1000,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
992
1000
  return
993
1001
 
994
1002
  self.state = self.State.ERROR
995
- self.emit(self.EVENT_ERROR)
1003
+ self.emit(self.EVENT_ESTABLISHMENT_ERROR)
996
1004
 
997
1005
  def on_loss(self):
998
1006
  self.state = self.State.LOST
@@ -1093,6 +1101,7 @@ class Big(utils.EventEmitter):
1093
1101
  max_pdu: int = 0
1094
1102
  iso_interval: float = 0.0 # ISO interval, in milliseconds
1095
1103
  bis_links: Sequence[BisLink] = ()
1104
+ device: Device = field(init=False)
1096
1105
 
1097
1106
  def __post_init__(self) -> None:
1098
1107
  super().__init__()
@@ -1154,6 +1163,7 @@ class BigSync(utils.EventEmitter):
1154
1163
  max_pdu: int = 0
1155
1164
  iso_interval: float = 0.0
1156
1165
  bis_links: Sequence[BisLink] = ()
1166
+ device: Device = field(init=False)
1157
1167
 
1158
1168
  def __post_init__(self) -> None:
1159
1169
  super().__init__()
@@ -1272,7 +1282,7 @@ class Peer:
1272
1282
  return mtu
1273
1283
 
1274
1284
  async def discover_service(
1275
- self, uuid: Union[core.UUID, str]
1285
+ self, uuid: core.UUID | str
1276
1286
  ) -> list[gatt_client.ServiceProxy]:
1277
1287
  return await self.gatt_client.discover_service(uuid)
1278
1288
 
@@ -1288,8 +1298,8 @@ class Peer:
1288
1298
 
1289
1299
  async def discover_characteristics(
1290
1300
  self,
1291
- uuids: Iterable[Union[core.UUID, str]] = (),
1292
- service: Optional[gatt_client.ServiceProxy] = None,
1301
+ uuids: Iterable[core.UUID | str] = (),
1302
+ service: gatt_client.ServiceProxy | None = None,
1293
1303
  ) -> list[gatt_client.CharacteristicProxy[bytes]]:
1294
1304
  return await self.gatt_client.discover_characteristics(
1295
1305
  uuids=uuids, service=service
@@ -1297,9 +1307,9 @@ class Peer:
1297
1307
 
1298
1308
  async def discover_descriptors(
1299
1309
  self,
1300
- characteristic: Optional[gatt_client.CharacteristicProxy] = None,
1301
- start_handle: Optional[int] = None,
1302
- end_handle: Optional[int] = None,
1310
+ characteristic: gatt_client.CharacteristicProxy | None = None,
1311
+ start_handle: int | None = None,
1312
+ end_handle: int | None = None,
1303
1313
  ):
1304
1314
  return await self.gatt_client.discover_descriptors(
1305
1315
  characteristic, start_handle, end_handle
@@ -1320,7 +1330,7 @@ class Peer:
1320
1330
  async def subscribe(
1321
1331
  self,
1322
1332
  characteristic: gatt_client.CharacteristicProxy,
1323
- subscriber: Optional[Callable[[bytes], Any]] = None,
1333
+ subscriber: Callable[[bytes], Any] | None = None,
1324
1334
  prefer_notify: bool = True,
1325
1335
  ) -> None:
1326
1336
  return await self.gatt_client.subscribe(
@@ -1330,25 +1340,23 @@ class Peer:
1330
1340
  async def unsubscribe(
1331
1341
  self,
1332
1342
  characteristic: gatt_client.CharacteristicProxy,
1333
- subscriber: Optional[Callable[[bytes], Any]] = None,
1343
+ subscriber: Callable[[bytes], Any] | None = None,
1334
1344
  ) -> None:
1335
1345
  return await self.gatt_client.unsubscribe(characteristic, subscriber)
1336
1346
 
1337
- async def read_value(
1338
- self, attribute: Union[int, gatt_client.AttributeProxy]
1339
- ) -> bytes:
1347
+ async def read_value(self, attribute: int | gatt_client.AttributeProxy) -> bytes:
1340
1348
  return await self.gatt_client.read_value(attribute)
1341
1349
 
1342
1350
  async def write_value(
1343
1351
  self,
1344
- attribute: Union[int, gatt_client.AttributeProxy],
1352
+ attribute: int | gatt_client.AttributeProxy,
1345
1353
  value: bytes,
1346
1354
  with_response: bool = False,
1347
1355
  ) -> None:
1348
1356
  return await self.gatt_client.write_value(attribute, value, with_response)
1349
1357
 
1350
1358
  async def read_characteristics_by_uuid(
1351
- self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
1359
+ self, uuid: core.UUID, service: gatt_client.ServiceProxy | None = None
1352
1360
  ) -> list[bytes]:
1353
1361
  return await self.gatt_client.read_characteristics_by_uuid(uuid, service)
1354
1362
 
@@ -1358,7 +1366,7 @@ class Peer:
1358
1366
  def get_characteristics_by_uuid(
1359
1367
  self,
1360
1368
  uuid: core.UUID,
1361
- service: Optional[Union[gatt_client.ServiceProxy, core.UUID]] = None,
1369
+ service: gatt_client.ServiceProxy | core.UUID | None = None,
1362
1370
  ) -> list[gatt_client.CharacteristicProxy[bytes]]:
1363
1371
  if isinstance(service, core.UUID):
1364
1372
  return list(
@@ -1374,7 +1382,7 @@ class Peer:
1374
1382
 
1375
1383
  def create_service_proxy(
1376
1384
  self, proxy_class: type[_PROXY_CLASS]
1377
- ) -> Optional[_PROXY_CLASS]:
1385
+ ) -> _PROXY_CLASS | None:
1378
1386
  if proxy := proxy_class.from_client(self.gatt_client):
1379
1387
  return cast(_PROXY_CLASS, proxy)
1380
1388
 
@@ -1382,7 +1390,7 @@ class Peer:
1382
1390
 
1383
1391
  async def discover_service_and_create_proxy(
1384
1392
  self, proxy_class: type[_PROXY_CLASS]
1385
- ) -> Optional[_PROXY_CLASS]:
1393
+ ) -> _PROXY_CLASS | None:
1386
1394
  # Discover the first matching service and its characteristics
1387
1395
  services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
1388
1396
  if services:
@@ -1391,7 +1399,7 @@ class Peer:
1391
1399
  return self.create_service_proxy(proxy_class)
1392
1400
  return None
1393
1401
 
1394
- async def sustain(self, timeout: Optional[float] = None) -> None:
1402
+ async def sustain(self, timeout: float | None = None) -> None:
1395
1403
  await self.connection.sustain(timeout)
1396
1404
 
1397
1405
  # [Classic only]
@@ -1434,7 +1442,7 @@ class ScoLink(utils.CompositeEventEmitter):
1434
1442
  acl_connection: Connection
1435
1443
  handle: int
1436
1444
  link_type: int
1437
- sink: Optional[Callable[[hci.HCI_SynchronousDataPacket], Any]] = None
1445
+ sink: Callable[[hci.HCI_SynchronousDataPacket], Any] | None = None
1438
1446
 
1439
1447
  EVENT_DISCONNECTION: ClassVar[str] = "disconnection"
1440
1448
  EVENT_DISCONNECTION_FAILURE: ClassVar[str] = "disconnection_failure"
@@ -1617,8 +1625,8 @@ class CisLink(utils.EventEmitter, _IsoLink):
1617
1625
  cis_sync_delay: int = 0 # CIS sync delay, in microseconds
1618
1626
  transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
1619
1627
  transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
1620
- phy_c_to_p: Optional[hci.Phy] = None
1621
- phy_p_to_c: Optional[hci.Phy] = None
1628
+ phy_c_to_p: hci.Phy | None = None
1629
+ phy_p_to_c: hci.Phy | None = None
1622
1630
  nse: int = 0
1623
1631
  bn_c_to_p: int = 0
1624
1632
  bn_p_to_c: int = 0
@@ -1651,6 +1659,7 @@ class BisLink(_IsoLink):
1651
1659
  handle: int
1652
1660
  big: Big | BigSync
1653
1661
  sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
1662
+ device: Device = field(init=False)
1654
1663
 
1655
1664
  def __post_init__(self) -> None:
1656
1665
  super().__init__()
@@ -1706,11 +1715,11 @@ class Connection(utils.CompositeEventEmitter):
1706
1715
  handle: int
1707
1716
  transport: core.PhysicalTransport
1708
1717
  self_address: hci.Address
1709
- self_resolvable_address: Optional[hci.Address]
1718
+ self_resolvable_address: hci.Address | None
1710
1719
  peer_address: hci.Address
1711
- peer_name: Optional[str]
1712
- peer_resolvable_address: Optional[hci.Address]
1713
- peer_le_features: Optional[hci.LeFeatureMask]
1720
+ peer_name: str | None
1721
+ peer_resolvable_address: hci.Address | None
1722
+ peer_le_features: hci.LeFeatureMask | None
1714
1723
  role: hci.Role
1715
1724
  parameters: Parameters
1716
1725
  encryption: int
@@ -1718,8 +1727,8 @@ class Connection(utils.CompositeEventEmitter):
1718
1727
  authenticated: bool
1719
1728
  sc: bool
1720
1729
  gatt_client: gatt_client.Client
1721
- pairing_peer_io_capability: Optional[int]
1722
- pairing_peer_authentication_requirements: Optional[int]
1730
+ pairing_peer_io_capability: int | None
1731
+ pairing_peer_authentication_requirements: int | None
1723
1732
  cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
1724
1733
  cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
1725
1734
  classic_mode: int = hci.HCI_Mode_Change_Event.Mode.ACTIVE
@@ -1739,7 +1748,6 @@ class Connection(utils.CompositeEventEmitter):
1739
1748
  EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE = "connection_parameters_update_failure"
1740
1749
  EVENT_CONNECTION_PHY_UPDATE = "connection_phy_update"
1741
1750
  EVENT_CONNECTION_PHY_UPDATE_FAILURE = "connection_phy_update_failure"
1742
- EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
1743
1751
  EVENT_CONNECTION_DATA_LENGTH_CHANGE = "connection_data_length_change"
1744
1752
  EVENT_CHANNEL_SOUNDING_CAPABILITIES_FAILURE = (
1745
1753
  "channel_sounding_capabilities_failure"
@@ -1821,9 +1829,9 @@ class Connection(utils.CompositeEventEmitter):
1821
1829
  handle: int,
1822
1830
  transport: core.PhysicalTransport,
1823
1831
  self_address: hci.Address,
1824
- self_resolvable_address: Optional[hci.Address],
1832
+ self_resolvable_address: hci.Address | None,
1825
1833
  peer_address: hci.Address,
1826
- peer_resolvable_address: Optional[hci.Address],
1834
+ peer_resolvable_address: hci.Address | None,
1827
1835
  role: hci.Role,
1828
1836
  parameters: Parameters,
1829
1837
  ):
@@ -1842,7 +1850,7 @@ class Connection(utils.CompositeEventEmitter):
1842
1850
  self.encryption_key_size = 0
1843
1851
  self.authenticated = False
1844
1852
  self.sc = False
1845
- self.att_mtu = ATT_DEFAULT_MTU
1853
+ self.att_mtu = att.ATT_DEFAULT_MTU
1846
1854
  self.data_length = DEVICE_DEFAULT_DATA_LENGTH
1847
1855
  self.gatt_client = gatt_client.Client(self) # Per-connection client
1848
1856
  self.gatt_server = (
@@ -1886,8 +1894,8 @@ class Connection(utils.CompositeEventEmitter):
1886
1894
  ) -> l2cap.LeCreditBasedChannel: ...
1887
1895
 
1888
1896
  async def create_l2cap_channel(
1889
- self, spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec]
1890
- ) -> Union[l2cap.ClassicChannel, l2cap.LeCreditBasedChannel]:
1897
+ self, spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec
1898
+ ) -> l2cap.ClassicChannel | l2cap.LeCreditBasedChannel:
1891
1899
  return await self.device.create_l2cap_channel(connection=self, spec=spec)
1892
1900
 
1893
1901
  async def disconnect(
@@ -1911,20 +1919,17 @@ class Connection(utils.CompositeEventEmitter):
1911
1919
  async def switch_role(self, role: hci.Role) -> None:
1912
1920
  return await self.device.switch_role(self, role)
1913
1921
 
1914
- async def sustain(self, timeout: Optional[float] = None) -> None:
1922
+ async def sustain(self, timeout: float | None = None) -> None:
1915
1923
  """Idles the current task waiting for a disconnect or timeout"""
1916
1924
 
1917
1925
  abort = asyncio.get_running_loop().create_future()
1918
- self.on(self.EVENT_DISCONNECTION, abort.set_result)
1919
- self.on(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
1926
+ with closing(utils.EventWatcher()) as watcher:
1927
+ watcher.on(self, self.EVENT_DISCONNECTION, abort.set_result)
1928
+ watcher.on(self, self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
1920
1929
 
1921
- try:
1922
1930
  await asyncio.wait_for(
1923
1931
  utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout
1924
1932
  )
1925
- finally:
1926
- self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
1927
- self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
1928
1933
 
1929
1934
  async def set_data_length(self, tx_octets: int, tx_time: int) -> None:
1930
1935
  return await self.device.set_data_length(self, tx_octets, tx_time)
@@ -1958,8 +1963,8 @@ class Connection(utils.CompositeEventEmitter):
1958
1963
 
1959
1964
  async def set_phy(
1960
1965
  self,
1961
- tx_phys: Optional[Iterable[hci.Phy]] = None,
1962
- rx_phys: Optional[Iterable[hci.Phy]] = None,
1966
+ tx_phys: Iterable[hci.Phy] | None = None,
1967
+ rx_phys: Iterable[hci.Phy] | None = None,
1963
1968
  phy_options: int = 0,
1964
1969
  ):
1965
1970
  return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
@@ -1995,6 +2000,15 @@ class Connection(utils.CompositeEventEmitter):
1995
2000
  self.peer_le_features = await self.device.get_remote_le_features(self)
1996
2001
  return self.peer_le_features
1997
2002
 
2003
+ def on_att_mtu_update(self, mtu: int):
2004
+ logger.debug(
2005
+ f'*** Connection ATT MTU Update: [0x{self.handle:04X}] '
2006
+ f'{self.peer_address} as {self.role_name}, '
2007
+ f'{mtu}'
2008
+ )
2009
+ self.att_mtu = mtu
2010
+ self.emit(self.EVENT_CONNECTION_ATT_MTU_UPDATE)
2011
+
1998
2012
  @property
1999
2013
  def data_packet_queue(self) -> DataPacketQueue | None:
2000
2014
  return self.device.host.get_data_packet_queue(self.handle)
@@ -2063,18 +2077,26 @@ class DeviceConfiguration:
2063
2077
  AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
2064
2078
  )
2065
2079
  irk: bytes = bytes(16) # This really must be changed for any level of security
2066
- keystore: Optional[str] = None
2080
+ keystore: str | None = None
2067
2081
  address_resolution_offload: bool = False
2068
2082
  address_generation_offload: bool = False
2069
2083
  cis_enabled: bool = False
2070
2084
  channel_sounding_enabled: bool = False
2071
- identity_address_type: Optional[int] = None
2085
+ identity_address_type: int | None = None
2072
2086
  io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
2073
2087
  gap_service_enabled: bool = True
2074
2088
  gatt_service_enabled: bool = True
2089
+ enhanced_retransmission_supported: bool = False
2090
+ l2cap_extended_features: Sequence[int] = (
2091
+ l2cap.L2CAP_Information_Request.ExtendedFeatures.FIXED_CHANNELS,
2092
+ l2cap.L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION,
2093
+ l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE,
2094
+ )
2095
+ eatt_enabled: bool = False
2096
+ gatt_services: list[dict[str, Any]] = field(init=False)
2075
2097
 
2076
2098
  def __post_init__(self) -> None:
2077
- self.gatt_services: list[dict[str, Any]] = []
2099
+ self.gatt_services = []
2078
2100
 
2079
2101
  def load_from_dict(self, config: dict[str, Any]) -> None:
2080
2102
  config = copy.deepcopy(config)
@@ -2130,7 +2152,7 @@ class DeviceConfiguration:
2130
2152
  setattr(self, key, value)
2131
2153
 
2132
2154
  def load_from_file(self, filename: str) -> None:
2133
- with open(filename, 'r', encoding='utf-8') as file:
2155
+ with open(filename, encoding='utf-8') as file:
2134
2156
  self.load_from_dict(json.load(file))
2135
2157
 
2136
2158
  @classmethod
@@ -2241,12 +2263,12 @@ class Device(utils.CompositeEventEmitter):
2241
2263
  pending_connections: dict[hci.Address, Connection]
2242
2264
  classic_pending_accepts: dict[
2243
2265
  hci.Address,
2244
- list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
2266
+ list[asyncio.Future[Connection | tuple[hci.Address, int, int]]],
2245
2267
  ]
2246
2268
  advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
2247
2269
  periodic_advertising_syncs: list[PeriodicAdvertisingSync]
2248
2270
  config: DeviceConfiguration
2249
- legacy_advertiser: Optional[LegacyAdvertiser]
2271
+ legacy_advertiser: LegacyAdvertiser | None
2250
2272
  sco_links: dict[int, ScoLink]
2251
2273
  cis_links: dict[int, CisLink]
2252
2274
  bigs: dict[int, Big]
@@ -2254,6 +2276,7 @@ class Device(utils.CompositeEventEmitter):
2254
2276
  big_syncs: dict[int, BigSync]
2255
2277
  _pending_cis: dict[int, tuple[int, int]]
2256
2278
  gatt_service: gatt_service.GenericAttributeProfileService | None = None
2279
+ keystore: KeyStore | None = None
2257
2280
 
2258
2281
  EVENT_ADVERTISEMENT = "advertisement"
2259
2282
  EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER = "periodic_advertising_sync_transfer"
@@ -2334,13 +2357,17 @@ class Device(utils.CompositeEventEmitter):
2334
2357
 
2335
2358
  def __init__(
2336
2359
  self,
2337
- name: Optional[str] = None,
2338
- address: Optional[hci.Address] = None,
2339
- config: Optional[DeviceConfiguration] = None,
2340
- host: Optional[Host] = None,
2360
+ name: str | None = None,
2361
+ address: hci.Address | None = None,
2362
+ config: DeviceConfiguration | None = None,
2363
+ host: Host | None = None,
2341
2364
  ) -> None:
2342
2365
  super().__init__()
2343
2366
 
2367
+ # Use the initial config or a default
2368
+ config = config or DeviceConfiguration()
2369
+ self.config = config
2370
+
2344
2371
  self._host = None
2345
2372
  self.powered_on = False
2346
2373
  self.auto_restart_inquiry = True
@@ -2348,7 +2375,7 @@ class Device(utils.CompositeEventEmitter):
2348
2375
  self.gatt_server = gatt_server.Server(self)
2349
2376
  self.sdp_server = sdp.Server(self)
2350
2377
  self.l2cap_channel_manager = l2cap.ChannelManager(
2351
- [l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]
2378
+ config.l2cap_extended_features
2352
2379
  )
2353
2380
  self.advertisement_accumulators = {} # Accumulators, by address
2354
2381
  self.periodic_advertising_syncs = []
@@ -2374,19 +2401,11 @@ class Device(utils.CompositeEventEmitter):
2374
2401
  hci.Address.ANY: []
2375
2402
  } # Futures, by BD address OR [Futures] for hci.Address.ANY
2376
2403
 
2377
- # In Python <= 3.9 + Rust Runtime, asyncio.Lock cannot be properly initiated.
2378
- if sys.version_info >= (3, 10):
2379
- self._cis_lock = asyncio.Lock()
2380
- else:
2381
- self._cis_lock = AsyncExitStack()
2404
+ self._cis_lock = asyncio.Lock()
2382
2405
 
2383
2406
  # Own address type cache
2384
2407
  self.connect_own_address_type = None
2385
2408
 
2386
- # Use the initial config or a default
2387
- config = config or DeviceConfiguration()
2388
- self.config = config
2389
-
2390
2409
  self.name = config.name
2391
2410
  self.public_address = hci.Address.ANY
2392
2411
  self.random_address = config.address
@@ -2398,7 +2417,7 @@ class Device(utils.CompositeEventEmitter):
2398
2417
  self.le_simultaneous_enabled = config.le_simultaneous_enabled
2399
2418
  self.le_privacy_enabled = config.le_privacy_enabled
2400
2419
  self.le_rpa_timeout = config.le_rpa_timeout
2401
- self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
2420
+ self.le_rpa_periodic_update_task: asyncio.Task | None = None
2402
2421
  self.le_subrate_enabled = config.le_subrate_enabled
2403
2422
  self.classic_enabled = config.classic_enabled
2404
2423
  self.cis_enabled = config.cis_enabled
@@ -2422,8 +2441,8 @@ class Device(utils.CompositeEventEmitter):
2422
2441
  # can be initialized from a config object, and for backward compatibility for
2423
2442
  # client code that may set those values directly before calling
2424
2443
  # start_advertising().
2425
- self.legacy_advertising_set: Optional[AdvertisingSet] = None
2426
- self.legacy_advertiser: Optional[LegacyAdvertiser] = None
2444
+ self.legacy_advertising_set: AdvertisingSet | None = None
2445
+ self.legacy_advertiser: LegacyAdvertiser | None = None
2427
2446
  self.advertising_data = config.advertising_data
2428
2447
  self.scan_response_data = config.scan_response_data
2429
2448
  self.advertising_interval_min = config.advertising_interval_min
@@ -2494,7 +2513,10 @@ class Device(utils.CompositeEventEmitter):
2494
2513
  add_gap_service=config.gap_service_enabled,
2495
2514
  add_gatt_service=config.gatt_service_enabled,
2496
2515
  )
2497
- self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
2516
+ self.l2cap_channel_manager.register_fixed_channel(att.ATT_CID, self.on_gatt_pdu)
2517
+
2518
+ if self.config.eatt_enabled:
2519
+ self.gatt_server.register_eatt()
2498
2520
 
2499
2521
  # Forward some events
2500
2522
  utils.setup_event_forwarding(
@@ -2541,7 +2563,7 @@ class Device(utils.CompositeEventEmitter):
2541
2563
  def sdp_service_records(self, service_records):
2542
2564
  self.sdp_server.service_records = service_records
2543
2565
 
2544
- def lookup_connection(self, connection_handle: int) -> Optional[Connection]:
2566
+ def lookup_connection(self, connection_handle: int) -> Connection | None:
2545
2567
  if connection := self.connections.get(connection_handle):
2546
2568
  return connection
2547
2569
 
@@ -2550,9 +2572,9 @@ class Device(utils.CompositeEventEmitter):
2550
2572
  def find_connection_by_bd_addr(
2551
2573
  self,
2552
2574
  bd_addr: hci.Address,
2553
- transport: Optional[int] = None,
2575
+ transport: int | None = None,
2554
2576
  check_address_type: bool = False,
2555
- ) -> Optional[Connection]:
2577
+ ) -> Connection | None:
2556
2578
  for connection in self.connections.values():
2557
2579
  if bytes(connection.peer_address) == bytes(bd_addr):
2558
2580
  if (
@@ -2567,7 +2589,7 @@ class Device(utils.CompositeEventEmitter):
2567
2589
 
2568
2590
  def lookup_periodic_advertising_sync(
2569
2591
  self, sync_handle: int
2570
- ) -> Optional[PeriodicAdvertisingSync]:
2592
+ ) -> PeriodicAdvertisingSync | None:
2571
2593
  return next(
2572
2594
  (
2573
2595
  sync
@@ -2605,8 +2627,8 @@ class Device(utils.CompositeEventEmitter):
2605
2627
  async def create_l2cap_channel(
2606
2628
  self,
2607
2629
  connection: Connection,
2608
- spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec],
2609
- ) -> Union[l2cap.ClassicChannel, l2cap.LeCreditBasedChannel]:
2630
+ spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec,
2631
+ ) -> l2cap.ClassicChannel | l2cap.LeCreditBasedChannel:
2610
2632
  if isinstance(spec, l2cap.ClassicChannelSpec):
2611
2633
  return await self.l2cap_channel_manager.create_classic_channel(
2612
2634
  connection=connection, spec=spec
@@ -2620,25 +2642,25 @@ class Device(utils.CompositeEventEmitter):
2620
2642
  def create_l2cap_server(
2621
2643
  self,
2622
2644
  spec: l2cap.ClassicChannelSpec,
2623
- handler: Optional[Callable[[l2cap.ClassicChannel], Any]] = None,
2645
+ handler: Callable[[l2cap.ClassicChannel], Any] | None = None,
2624
2646
  ) -> l2cap.ClassicChannelServer: ...
2625
2647
 
2626
2648
  @overload
2627
2649
  def create_l2cap_server(
2628
2650
  self,
2629
2651
  spec: l2cap.LeCreditBasedChannelSpec,
2630
- handler: Optional[Callable[[l2cap.LeCreditBasedChannel], Any]] = None,
2652
+ handler: Callable[[l2cap.LeCreditBasedChannel], Any] | None = None,
2631
2653
  ) -> l2cap.LeCreditBasedChannelServer: ...
2632
2654
 
2633
2655
  def create_l2cap_server(
2634
2656
  self,
2635
- spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec],
2636
- handler: Union[
2637
- Callable[[l2cap.ClassicChannel], Any],
2638
- Callable[[l2cap.LeCreditBasedChannel], Any],
2639
- None,
2640
- ] = None,
2641
- ) -> Union[l2cap.ClassicChannelServer, l2cap.LeCreditBasedChannelServer]:
2657
+ spec: l2cap.ClassicChannelSpec | l2cap.LeCreditBasedChannelSpec,
2658
+ handler: (
2659
+ Callable[[l2cap.ClassicChannel], Any]
2660
+ | Callable[[l2cap.LeCreditBasedChannel], Any]
2661
+ | None
2662
+ ) = None,
2663
+ ) -> l2cap.ClassicChannelServer | l2cap.LeCreditBasedChannelServer:
2642
2664
  if isinstance(spec, l2cap.ClassicChannelSpec):
2643
2665
  return self.l2cap_channel_manager.create_classic_server(
2644
2666
  spec=spec,
@@ -2887,7 +2909,9 @@ class Device(utils.CompositeEventEmitter):
2887
2909
  self.address_resolver = smp.AddressResolver(resolving_keys)
2888
2910
 
2889
2911
  if self.address_resolution_offload or self.address_generation_offload:
2890
- await self.send_command(hci.HCI_LE_Clear_Resolving_List_Command())
2912
+ await self.send_command(
2913
+ hci.HCI_LE_Clear_Resolving_List_Command(), check_result=True
2914
+ )
2891
2915
 
2892
2916
  # Add an empty entry for non-directed address generation.
2893
2917
  await self.send_command(
@@ -2896,7 +2920,8 @@ class Device(utils.CompositeEventEmitter):
2896
2920
  peer_identity_address=hci.Address.ANY,
2897
2921
  peer_irk=bytes(16),
2898
2922
  local_irk=self.irk,
2899
- )
2923
+ ),
2924
+ check_result=True,
2900
2925
  )
2901
2926
 
2902
2927
  for irk, address in resolving_keys:
@@ -2906,7 +2931,8 @@ class Device(utils.CompositeEventEmitter):
2906
2931
  peer_identity_address=address,
2907
2932
  peer_irk=irk,
2908
2933
  local_irk=self.irk,
2909
- )
2934
+ ),
2935
+ check_result=True,
2910
2936
  )
2911
2937
 
2912
2938
  def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
@@ -2936,13 +2962,13 @@ class Device(utils.CompositeEventEmitter):
2936
2962
  async def start_advertising(
2937
2963
  self,
2938
2964
  advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
2939
- target: Optional[hci.Address] = None,
2965
+ target: hci.Address | None = None,
2940
2966
  own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
2941
2967
  auto_restart: bool = False,
2942
- advertising_data: Optional[bytes] = None,
2943
- scan_response_data: Optional[bytes] = None,
2944
- advertising_interval_min: Optional[float] = None,
2945
- advertising_interval_max: Optional[float] = None,
2968
+ advertising_data: bytes | None = None,
2969
+ scan_response_data: bytes | None = None,
2970
+ advertising_interval_min: float | None = None,
2971
+ advertising_interval_max: float | None = None,
2946
2972
  ) -> None:
2947
2973
  """Start legacy advertising.
2948
2974
 
@@ -3046,11 +3072,11 @@ class Device(utils.CompositeEventEmitter):
3046
3072
 
3047
3073
  async def create_advertising_set(
3048
3074
  self,
3049
- advertising_parameters: Optional[AdvertisingParameters] = None,
3050
- random_address: Optional[hci.Address] = None,
3075
+ advertising_parameters: AdvertisingParameters | None = None,
3076
+ random_address: hci.Address | None = None,
3051
3077
  advertising_data: bytes = b'',
3052
3078
  scan_response_data: bytes = b'',
3053
- periodic_advertising_parameters: Optional[PeriodicAdvertisingParameters] = None,
3079
+ periodic_advertising_parameters: PeriodicAdvertisingParameters | None = None,
3054
3080
  periodic_advertising_data: bytes = b'',
3055
3081
  auto_start: bool = True,
3056
3082
  auto_restart: bool = False,
@@ -3337,7 +3363,13 @@ class Device(utils.CompositeEventEmitter):
3337
3363
  return self.scanning
3338
3364
 
3339
3365
  @host_event_handler
3340
- def on_advertising_report(self, report):
3366
+ def on_advertising_report(
3367
+ self,
3368
+ report: (
3369
+ hci.HCI_LE_Advertising_Report_Event.Report
3370
+ | hci.HCI_LE_Extended_Advertising_Report_Event.Report
3371
+ ),
3372
+ ) -> None:
3341
3373
  if not (accumulator := self.advertisement_accumulators.get(report.address)):
3342
3374
  accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive)
3343
3375
  self.advertisement_accumulators[report.address] = accumulator
@@ -3501,16 +3533,15 @@ class Device(utils.CompositeEventEmitter):
3501
3533
  check_result=True,
3502
3534
  )
3503
3535
 
3504
- response = await self.send_command(
3536
+ self.discovering = False
3537
+ await self.send_command(
3505
3538
  hci.HCI_Inquiry_Command(
3506
3539
  lap=hci.HCI_GENERAL_INQUIRY_LAP,
3507
3540
  inquiry_length=DEVICE_DEFAULT_INQUIRY_LENGTH,
3508
3541
  num_responses=0, # Unlimited number of responses.
3509
- )
3542
+ ),
3543
+ check_result=True,
3510
3544
  )
3511
- if response.status != hci.HCI_Command_Status_Event.PENDING:
3512
- self.discovering = False
3513
- raise hci.HCI_StatusError(response)
3514
3545
 
3515
3546
  self.auto_restart_inquiry = auto_restart
3516
3547
  self.discovering = True
@@ -3546,7 +3577,8 @@ class Device(utils.CompositeEventEmitter):
3546
3577
  scan_enable = 0x00
3547
3578
 
3548
3579
  return await self.send_command(
3549
- hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable)
3580
+ hci.HCI_Write_Scan_Enable_Command(scan_enable=scan_enable),
3581
+ check_result=True,
3550
3582
  )
3551
3583
 
3552
3584
  async def set_discoverable(self, discoverable: bool = True) -> None:
@@ -3580,13 +3612,13 @@ class Device(utils.CompositeEventEmitter):
3580
3612
 
3581
3613
  async def connect(
3582
3614
  self,
3583
- peer_address: Union[hci.Address, str],
3615
+ peer_address: hci.Address | str,
3584
3616
  transport: core.PhysicalTransport = PhysicalTransport.LE,
3585
- connection_parameters_preferences: Optional[
3586
- dict[hci.Phy, ConnectionParametersPreferences]
3587
- ] = None,
3617
+ connection_parameters_preferences: (
3618
+ dict[hci.Phy, ConnectionParametersPreferences] | None
3619
+ ) = None,
3588
3620
  own_address_type: hci.OwnAddressType = hci.OwnAddressType.RANDOM,
3589
- timeout: Optional[float] = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3621
+ timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3590
3622
  always_resolve: bool = False,
3591
3623
  ) -> Connection:
3592
3624
  '''
@@ -3775,7 +3807,7 @@ class Device(utils.CompositeEventEmitter):
3775
3807
  for phy in phys
3776
3808
  ]
3777
3809
 
3778
- result = await self.send_command(
3810
+ await self.send_command(
3779
3811
  hci.HCI_LE_Extended_Create_Connection_Command(
3780
3812
  initiator_filter_policy=0,
3781
3813
  own_address_type=own_address_type,
@@ -3796,14 +3828,15 @@ class Device(utils.CompositeEventEmitter):
3796
3828
  supervision_timeouts=supervision_timeouts,
3797
3829
  min_ce_lengths=min_ce_lengths,
3798
3830
  max_ce_lengths=max_ce_lengths,
3799
- )
3831
+ ),
3832
+ check_result=True,
3800
3833
  )
3801
3834
  else:
3802
3835
  if hci.HCI_LE_1M_PHY not in connection_parameters_preferences:
3803
3836
  raise InvalidArgumentError('1M PHY preferences required')
3804
3837
 
3805
3838
  prefs = connection_parameters_preferences[hci.HCI_LE_1M_PHY]
3806
- result = await self.send_command(
3839
+ await self.send_command(
3807
3840
  hci.HCI_LE_Create_Connection_Command(
3808
3841
  le_scan_interval=int(
3809
3842
  DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625
@@ -3825,7 +3858,8 @@ class Device(utils.CompositeEventEmitter):
3825
3858
  supervision_timeout=int(prefs.supervision_timeout / 10),
3826
3859
  min_ce_length=int(prefs.min_ce_length / 0.625),
3827
3860
  max_ce_length=int(prefs.max_ce_length / 0.625),
3828
- )
3861
+ ),
3862
+ check_result=True,
3829
3863
  )
3830
3864
  else:
3831
3865
  # Save pending connection
@@ -3842,7 +3876,7 @@ class Device(utils.CompositeEventEmitter):
3842
3876
  )
3843
3877
 
3844
3878
  # TODO: allow passing other settings
3845
- result = await self.send_command(
3879
+ await self.send_command(
3846
3880
  hci.HCI_Create_Connection_Command(
3847
3881
  bd_addr=peer_address,
3848
3882
  packet_type=0xCC18, # FIXME: change
@@ -3850,12 +3884,10 @@ class Device(utils.CompositeEventEmitter):
3850
3884
  clock_offset=0x0000,
3851
3885
  allow_role_switch=0x01,
3852
3886
  reserved=0,
3853
- )
3887
+ ),
3888
+ check_result=True,
3854
3889
  )
3855
3890
 
3856
- if result.status != hci.HCI_Command_Status_Event.PENDING:
3857
- raise hci.HCI_StatusError(result)
3858
-
3859
3891
  # Wait for the connection process to complete
3860
3892
  if transport == PhysicalTransport.LE:
3861
3893
  self.le_connecting = True
@@ -3896,9 +3928,9 @@ class Device(utils.CompositeEventEmitter):
3896
3928
 
3897
3929
  async def accept(
3898
3930
  self,
3899
- peer_address: Union[hci.Address, str] = hci.Address.ANY,
3931
+ peer_address: hci.Address | str = hci.Address.ANY,
3900
3932
  role: hci.Role = hci.Role.PERIPHERAL,
3901
- timeout: Optional[float] = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3933
+ timeout: float | None = DEVICE_DEFAULT_CONNECT_TIMEOUT,
3902
3934
  ) -> Connection:
3903
3935
  '''
3904
3936
  Wait and accept any incoming connection or a connection from `peer_address` when
@@ -4007,7 +4039,8 @@ class Device(utils.CompositeEventEmitter):
4007
4039
  await self.send_command(
4008
4040
  hci.HCI_Accept_Connection_Request_Command(
4009
4041
  bd_addr=peer_address, role=role
4010
- )
4042
+ ),
4043
+ check_result=True,
4011
4044
  )
4012
4045
 
4013
4046
  # Wait for connection complete
@@ -4021,7 +4054,7 @@ class Device(utils.CompositeEventEmitter):
4021
4054
  self.pending_connections.pop(peer_address, None)
4022
4055
 
4023
4056
  @asynccontextmanager
4024
- async def connect_as_gatt(self, peer_address: Union[hci.Address, str]):
4057
+ async def connect_as_gatt(self, peer_address: hci.Address | str):
4025
4058
  async with AsyncExitStack() as stack:
4026
4059
  connection = await stack.enter_async_context(
4027
4060
  await self.connect(peer_address)
@@ -4068,7 +4101,7 @@ class Device(utils.CompositeEventEmitter):
4068
4101
  )
4069
4102
 
4070
4103
  async def disconnect(
4071
- self, connection: Union[Connection, ScoLink, CisLink], reason: int
4104
+ self, connection: Connection | ScoLink | CisLink, reason: int
4072
4105
  ) -> None:
4073
4106
  # Create a future so that we can wait for the disconnection's result
4074
4107
  pending_disconnection = asyncio.get_running_loop().create_future()
@@ -4077,19 +4110,17 @@ class Device(utils.CompositeEventEmitter):
4077
4110
  connection.EVENT_DISCONNECTION_FAILURE, pending_disconnection.set_exception
4078
4111
  )
4079
4112
 
4080
- # Request a disconnection
4081
- result = await self.send_command(
4082
- hci.HCI_Disconnect_Command(
4083
- connection_handle=connection.handle, reason=reason
4084
- )
4085
- )
4086
-
4087
4113
  try:
4088
- if result.status != hci.HCI_Command_Status_Event.PENDING:
4089
- raise hci.HCI_StatusError(result)
4090
-
4091
4114
  # Wait for the disconnection process to complete
4092
4115
  self.disconnecting = True
4116
+
4117
+ # Request a disconnection
4118
+ await self.send_command(
4119
+ hci.HCI_Disconnect_Command(
4120
+ connection_handle=connection.handle, reason=reason
4121
+ ),
4122
+ check_result=True,
4123
+ )
4093
4124
  return await utils.cancel_on_event(
4094
4125
  self, Device.EVENT_FLUSH, pending_disconnection
4095
4126
  )
@@ -4175,7 +4206,7 @@ class Device(utils.CompositeEventEmitter):
4175
4206
 
4176
4207
  return
4177
4208
 
4178
- result = await self.send_command(
4209
+ await self.send_command(
4179
4210
  hci.HCI_LE_Connection_Update_Command(
4180
4211
  connection_handle=connection.handle,
4181
4212
  connection_interval_min=connection_interval_min,
@@ -4184,10 +4215,9 @@ class Device(utils.CompositeEventEmitter):
4184
4215
  supervision_timeout=supervision_timeout,
4185
4216
  min_ce_length=min_ce_length,
4186
4217
  max_ce_length=max_ce_length,
4187
- )
4218
+ ),
4219
+ check_result=True,
4188
4220
  )
4189
- if result.status != hci.HCI_Command_Status_Event.PENDING:
4190
- raise hci.HCI_StatusError(result)
4191
4221
 
4192
4222
  async def get_connection_rssi(self, connection):
4193
4223
  result = await self.send_command(
@@ -4210,8 +4240,8 @@ class Device(utils.CompositeEventEmitter):
4210
4240
  async def set_connection_phy(
4211
4241
  self,
4212
4242
  connection: Connection,
4213
- tx_phys: Optional[Iterable[hci.Phy]] = None,
4214
- rx_phys: Optional[Iterable[hci.Phy]] = None,
4243
+ tx_phys: Iterable[hci.Phy] | None = None,
4244
+ rx_phys: Iterable[hci.Phy] | None = None,
4215
4245
  phy_options: int = 0,
4216
4246
  ):
4217
4247
  if not self.host.supports_command(hci.HCI_LE_SET_PHY_COMMAND):
@@ -4222,27 +4252,21 @@ class Device(utils.CompositeEventEmitter):
4222
4252
  (1 if rx_phys is None else 0) << 1
4223
4253
  )
4224
4254
 
4225
- result = await self.send_command(
4255
+ await self.send_command(
4226
4256
  hci.HCI_LE_Set_PHY_Command(
4227
4257
  connection_handle=connection.handle,
4228
4258
  all_phys=all_phys_bits,
4229
4259
  tx_phys=hci.phy_list_to_bits(tx_phys),
4230
4260
  rx_phys=hci.phy_list_to_bits(rx_phys),
4231
4261
  phy_options=phy_options,
4232
- )
4262
+ ),
4263
+ check_result=True,
4233
4264
  )
4234
4265
 
4235
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4236
- logger.warning(
4237
- 'HCI_LE_Set_PHY_Command failed: '
4238
- f'{hci.HCI_Constant.error_name(result.status)}'
4239
- )
4240
- raise hci.HCI_StatusError(result)
4241
-
4242
4266
  async def set_default_phy(
4243
4267
  self,
4244
- tx_phys: Optional[Iterable[hci.Phy]] = None,
4245
- rx_phys: Optional[Iterable[hci.Phy]] = None,
4268
+ tx_phys: Iterable[hci.Phy] | None = None,
4269
+ rx_phys: Iterable[hci.Phy] | None = None,
4246
4270
  ):
4247
4271
  all_phys_bits = (1 if tx_phys is None else 0) | (
4248
4272
  (1 if rx_phys is None else 0) << 1
@@ -4296,7 +4320,7 @@ class Device(utils.CompositeEventEmitter):
4296
4320
  if local_name == name:
4297
4321
  peer_address.set_result(address)
4298
4322
 
4299
- listener: Optional[Callable[..., None]] = None
4323
+ listener: Callable[..., None] | None = None
4300
4324
  was_scanning = self.scanning
4301
4325
  was_discovering = self.discovering
4302
4326
  try:
@@ -4410,7 +4434,7 @@ class Device(utils.CompositeEventEmitter):
4410
4434
 
4411
4435
  async def get_long_term_key(
4412
4436
  self, connection_handle: int, rand: bytes, ediv: int
4413
- ) -> Optional[bytes]:
4437
+ ) -> bytes | None:
4414
4438
  if (connection := self.lookup_connection(connection_handle)) is None:
4415
4439
  return None
4416
4440
 
@@ -4434,7 +4458,7 @@ class Device(utils.CompositeEventEmitter):
4434
4458
  return keys.ltk_peripheral.value
4435
4459
  return None
4436
4460
 
4437
- async def get_link_key(self, address: hci.Address) -> Optional[bytes]:
4461
+ async def get_link_key(self, address: hci.Address) -> bytes | None:
4438
4462
  if self.keystore is None:
4439
4463
  return None
4440
4464
 
@@ -4455,43 +4479,26 @@ class Device(utils.CompositeEventEmitter):
4455
4479
  async def authenticate(self, connection: Connection) -> None:
4456
4480
  # Set up event handlers
4457
4481
  pending_authentication = asyncio.get_running_loop().create_future()
4482
+ with closing(utils.EventWatcher()) as watcher:
4458
4483
 
4459
- def on_authentication():
4460
- pending_authentication.set_result(None)
4461
-
4462
- def on_authentication_failure(error_code):
4463
- pending_authentication.set_exception(hci.HCI_Error(error_code))
4484
+ @watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION)
4485
+ def on_authentication() -> None:
4486
+ pending_authentication.set_result(None)
4464
4487
 
4465
- connection.on(connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication)
4466
- connection.on(
4467
- connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
4468
- on_authentication_failure,
4469
- )
4488
+ @watcher.on(connection, connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE)
4489
+ def on_authentication_failure(error_code: int) -> None:
4490
+ pending_authentication.set_exception(hci.HCI_Error(error_code))
4470
4491
 
4471
- # Request the authentication
4472
- try:
4473
- result = await self.send_command(
4492
+ # Request the authentication
4493
+ await self.send_command(
4474
4494
  hci.HCI_Authentication_Requested_Command(
4475
4495
  connection_handle=connection.handle
4476
- )
4496
+ ),
4497
+ check_result=True,
4477
4498
  )
4478
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4479
- logger.warning(
4480
- 'HCI_Authentication_Requested_Command failed: '
4481
- f'{hci.HCI_Constant.error_name(result.status)}'
4482
- )
4483
- raise hci.HCI_StatusError(result)
4484
4499
 
4485
4500
  # Wait for the authentication to complete
4486
4501
  await connection.cancel_on_disconnection(pending_authentication)
4487
- finally:
4488
- connection.remove_listener(
4489
- connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
4490
- )
4491
- connection.remove_listener(
4492
- connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
4493
- on_authentication_failure,
4494
- )
4495
4502
 
4496
4503
  async def encrypt(self, connection: Connection, enable: bool = True):
4497
4504
  if not enable and connection.transport == PhysicalTransport.LE:
@@ -4500,21 +4507,17 @@ class Device(utils.CompositeEventEmitter):
4500
4507
  # Set up event handlers
4501
4508
  pending_encryption = asyncio.get_running_loop().create_future()
4502
4509
 
4503
- def on_encryption_change():
4504
- pending_encryption.set_result(None)
4510
+ # Request the encryption
4511
+ with closing(utils.EventWatcher()) as watcher:
4505
4512
 
4506
- def on_encryption_failure(error_code: int):
4507
- pending_encryption.set_exception(hci.HCI_Error(error_code))
4513
+ @watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
4514
+ def _() -> None:
4515
+ pending_encryption.set_result(None)
4508
4516
 
4509
- connection.on(
4510
- connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
4511
- )
4512
- connection.on(
4513
- connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
4514
- )
4517
+ @watcher.on(connection, connection.EVENT_CONNECTION_ENCRYPTION_FAILURE)
4518
+ def _(error_code: int):
4519
+ pending_encryption.set_exception(hci.HCI_Error(error_code))
4515
4520
 
4516
- # Request the encryption
4517
- try:
4518
4521
  if connection.transport == PhysicalTransport.LE:
4519
4522
  # Look for a key in the key store
4520
4523
  if self.keystore is None:
@@ -4531,53 +4534,34 @@ class Device(utils.CompositeEventEmitter):
4531
4534
  ediv = 0
4532
4535
  elif keys.ltk_central is not None:
4533
4536
  ltk = keys.ltk_central.value
4534
- rand = keys.ltk_central.rand
4535
- ediv = keys.ltk_central.ediv
4537
+ rand = keys.ltk_central.rand or b''
4538
+ ediv = keys.ltk_central.ediv or 0
4536
4539
  else:
4537
4540
  raise InvalidOperationError('no LTK found for peer')
4538
4541
 
4539
4542
  if connection.role != hci.Role.CENTRAL:
4540
4543
  raise InvalidStateError('only centrals can start encryption')
4541
4544
 
4542
- result = await self.send_command(
4545
+ await self.send_command(
4543
4546
  hci.HCI_LE_Enable_Encryption_Command(
4544
4547
  connection_handle=connection.handle,
4545
4548
  random_number=rand,
4546
4549
  encrypted_diversifier=ediv,
4547
4550
  long_term_key=ltk,
4548
- )
4551
+ ),
4552
+ check_result=True,
4549
4553
  )
4550
-
4551
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4552
- logger.warning(
4553
- 'HCI_LE_Enable_Encryption_Command failed: '
4554
- f'{hci.HCI_Constant.error_name(result.status)}'
4555
- )
4556
- raise hci.HCI_StatusError(result)
4557
4554
  else:
4558
- result = await self.send_command(
4555
+ await self.send_command(
4559
4556
  hci.HCI_Set_Connection_Encryption_Command(
4560
4557
  connection_handle=connection.handle,
4561
4558
  encryption_enable=0x01 if enable else 0x00,
4562
- )
4559
+ ),
4560
+ check_result=True,
4563
4561
  )
4564
4562
 
4565
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4566
- logger.warning(
4567
- 'HCI_Set_Connection_Encryption_Command failed: '
4568
- f'{hci.HCI_Constant.error_name(result.status)}'
4569
- )
4570
- raise hci.HCI_StatusError(result)
4571
-
4572
4563
  # Wait for the result
4573
4564
  await connection.cancel_on_disconnection(pending_encryption)
4574
- finally:
4575
- connection.remove_listener(
4576
- connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
4577
- )
4578
- connection.remove_listener(
4579
- connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
4580
- )
4581
4565
 
4582
4566
  async def update_keys(self, address: str, keys: PairingKeys) -> None:
4583
4567
  if self.keystore is None:
@@ -4595,80 +4579,55 @@ class Device(utils.CompositeEventEmitter):
4595
4579
  async def switch_role(self, connection: Connection, role: hci.Role):
4596
4580
  pending_role_change = asyncio.get_running_loop().create_future()
4597
4581
 
4598
- def on_role_change(new_role: hci.Role):
4599
- pending_role_change.set_result(new_role)
4582
+ with closing(utils.EventWatcher()) as watcher:
4600
4583
 
4601
- def on_role_change_failure(error_code: int):
4602
- pending_role_change.set_exception(hci.HCI_Error(error_code))
4584
+ @watcher.on(connection, connection.EVENT_ROLE_CHANGE)
4585
+ def _(new_role: hci.Role):
4586
+ pending_role_change.set_result(new_role)
4603
4587
 
4604
- connection.on(connection.EVENT_ROLE_CHANGE, on_role_change)
4605
- connection.on(connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure)
4588
+ @watcher.on(connection, connection.EVENT_ROLE_CHANGE_FAILURE)
4589
+ def _(error_code: int):
4590
+ pending_role_change.set_exception(hci.HCI_Error(error_code))
4606
4591
 
4607
- try:
4608
- result = await self.send_command(
4609
- hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role)
4592
+ await self.send_command(
4593
+ hci.HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role),
4594
+ check_result=True,
4610
4595
  )
4611
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4612
- logger.warning(
4613
- 'HCI_Switch_Role_Command failed: '
4614
- f'{hci.HCI_Constant.error_name(result.status)}'
4615
- )
4616
- raise hci.HCI_StatusError(result)
4617
4596
  await connection.cancel_on_disconnection(pending_role_change)
4618
- finally:
4619
- connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
4620
- connection.remove_listener(
4621
- connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure
4622
- )
4623
4597
 
4624
4598
  # [Classic only]
4625
- async def request_remote_name(self, remote: Union[hci.Address, Connection]) -> str:
4599
+ async def request_remote_name(self, remote: hci.Address | Connection) -> str:
4626
4600
  # Set up event handlers
4627
- pending_name = asyncio.get_running_loop().create_future()
4601
+ pending_name: asyncio.Future[str] = asyncio.get_running_loop().create_future()
4628
4602
 
4629
4603
  peer_address = (
4630
4604
  remote if isinstance(remote, hci.Address) else remote.peer_address
4631
4605
  )
4632
4606
 
4633
- handler = self.on(
4634
- self.EVENT_REMOTE_NAME,
4635
- lambda address, remote_name: (
4636
- pending_name.set_result(remote_name)
4637
- if address == peer_address
4638
- else None
4639
- ),
4640
- )
4641
- failure_handler = self.on(
4642
- self.EVENT_REMOTE_NAME_FAILURE,
4643
- lambda address, error_code: (
4644
- pending_name.set_exception(hci.HCI_Error(error_code))
4645
- if address == peer_address
4646
- else None
4647
- ),
4648
- )
4607
+ with closing(utils.EventWatcher()) as watcher:
4649
4608
 
4650
- try:
4651
- result = await self.send_command(
4609
+ @watcher.on(self, self.EVENT_REMOTE_NAME)
4610
+ def _(address: hci.Address, remote_name: str) -> None:
4611
+ if address == peer_address:
4612
+ pending_name.set_result(remote_name)
4613
+
4614
+ @watcher.on(self, self.EVENT_REMOTE_NAME_FAILURE)
4615
+ def _(address: hci.Address, error_code: int) -> None:
4616
+ if address == peer_address:
4617
+ pending_name.set_exception(hci.HCI_Error(error_code))
4618
+
4619
+ await self.send_command(
4652
4620
  hci.HCI_Remote_Name_Request_Command(
4653
4621
  bd_addr=peer_address,
4654
4622
  page_scan_repetition_mode=hci.HCI_Remote_Name_Request_Command.R2,
4655
4623
  reserved=0,
4656
4624
  clock_offset=0, # TODO investigate non-0 values
4657
- )
4625
+ ),
4626
+ check_result=True,
4658
4627
  )
4659
4628
 
4660
- if result.status != hci.HCI_COMMAND_STATUS_PENDING:
4661
- logger.warning(
4662
- 'HCI_Remote_Name_Request_Command failed: '
4663
- f'{hci.HCI_Constant.error_name(result.status)}'
4664
- )
4665
- raise hci.HCI_StatusError(result)
4666
-
4667
4629
  # Wait for the result
4668
4630
  return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name)
4669
- finally:
4670
- self.remove_listener(self.EVENT_REMOTE_NAME, handler)
4671
- self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
4672
4631
 
4673
4632
  # [LE only]
4674
4633
  @utils.experimental('Only for testing.')
@@ -4684,8 +4643,6 @@ class Device(utils.CompositeEventEmitter):
4684
4643
  Returns:
4685
4644
  List of created CIS handles corresponding to the same order of [cid_id].
4686
4645
  """
4687
- num_cis = len(parameters.cis_parameters)
4688
-
4689
4646
  response = await self.send_command(
4690
4647
  hci.HCI_LE_Set_CIG_Parameters_Command(
4691
4648
  cig_id=parameters.cig_id,
@@ -5202,14 +5159,18 @@ class Device(utils.CompositeEventEmitter):
5202
5159
  if add_gap_service:
5203
5160
  self.gatt_server.add_service(GenericAccessService(self.name))
5204
5161
  if add_gatt_service:
5205
- self.gatt_service = gatt_service.GenericAttributeProfileService()
5162
+ self.gatt_service = gatt_service.GenericAttributeProfileService(
5163
+ gatt.ServerSupportedFeatures.EATT_SUPPORTED
5164
+ if self.config.eatt_enabled
5165
+ else None
5166
+ )
5206
5167
  self.gatt_server.add_service(self.gatt_service)
5207
5168
 
5208
5169
  async def notify_subscriber(
5209
5170
  self,
5210
5171
  connection: Connection,
5211
5172
  attribute: Attribute,
5212
- value: Optional[Any] = None,
5173
+ value: Any | None = None,
5213
5174
  force: bool = False,
5214
5175
  ) -> None:
5215
5176
  """
@@ -5228,7 +5189,7 @@ class Device(utils.CompositeEventEmitter):
5228
5189
  await self.gatt_server.notify_subscriber(connection, attribute, value, force)
5229
5190
 
5230
5191
  async def notify_subscribers(
5231
- self, attribute: Attribute, value=None, force=False
5192
+ self, attribute: Attribute, value: Any | None = None, force: bool = False
5232
5193
  ) -> None:
5233
5194
  """
5234
5195
  Send a notification to all the subscribers of an attribute.
@@ -5248,7 +5209,7 @@ class Device(utils.CompositeEventEmitter):
5248
5209
  self,
5249
5210
  connection: Connection,
5250
5211
  attribute: Attribute,
5251
- value: Optional[Any] = None,
5212
+ value: Any | None = None,
5252
5213
  force: bool = False,
5253
5214
  ):
5254
5215
  """
@@ -5269,7 +5230,7 @@ class Device(utils.CompositeEventEmitter):
5269
5230
  await self.gatt_server.indicate_subscriber(connection, attribute, value, force)
5270
5231
 
5271
5232
  async def indicate_subscribers(
5272
- self, attribute: Attribute, value: Optional[Any] = None, force: bool = False
5233
+ self, attribute: Attribute, value: Any | None = None, force: bool = False
5273
5234
  ):
5274
5235
  """
5275
5236
  Send an indication to all the subscribers of an attribute.
@@ -5304,8 +5265,7 @@ class Device(utils.CompositeEventEmitter):
5304
5265
 
5305
5266
  if status != hci.HCI_SUCCESS:
5306
5267
  logger.debug(
5307
- f'advertising set {advertising_handle} '
5308
- f'terminated with status {status}'
5268
+ f'advertising set {advertising_handle} terminated with status {status}'
5309
5269
  )
5310
5270
  return
5311
5271
 
@@ -5494,8 +5454,8 @@ class Device(utils.CompositeEventEmitter):
5494
5454
  self,
5495
5455
  connection_handle: int,
5496
5456
  peer_address: hci.Address,
5497
- self_resolvable_address: Optional[hci.Address],
5498
- peer_resolvable_address: Optional[hci.Address],
5457
+ self_resolvable_address: hci.Address | None,
5458
+ peer_resolvable_address: hci.Address | None,
5499
5459
  role: hci.Role,
5500
5460
  connection_interval: int,
5501
5461
  peripheral_latency: int,
@@ -5530,7 +5490,7 @@ class Device(utils.CompositeEventEmitter):
5530
5490
  peer_address = resolved_address
5531
5491
 
5532
5492
  self_address = None
5533
- own_address_type: Optional[hci.OwnAddressType] = None
5493
+ own_address_type: hci.OwnAddressType | None = None
5534
5494
  if role == hci.Role.CENTRAL:
5535
5495
  own_address_type = self.connect_own_address_type
5536
5496
  assert own_address_type is not None
@@ -5684,7 +5644,8 @@ class Device(utils.CompositeEventEmitter):
5684
5644
 
5685
5645
  self.host.send_command_sync(
5686
5646
  hci.HCI_Accept_Connection_Request_Command(
5687
- bd_addr=bd_addr, role=0x01 # Remain the peripheral
5647
+ bd_addr=bd_addr,
5648
+ role=0x01, # Remain the peripheral
5688
5649
  )
5689
5650
  )
5690
5651
 
@@ -5753,9 +5714,7 @@ class Device(utils.CompositeEventEmitter):
5753
5714
 
5754
5715
  @host_event_handler
5755
5716
  @with_connection_from_handle
5756
- def on_connection_authentication_failure(
5757
- self, connection: Connection, error: core.ConnectionError
5758
- ):
5717
+ def on_connection_authentication_failure(self, connection: Connection, error: int):
5759
5718
  logger.debug(
5760
5719
  f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
5761
5720
  f'{connection.peer_address} as {connection.role_name}, error={error}'
@@ -5810,11 +5769,15 @@ class Device(utils.CompositeEventEmitter):
5810
5769
  # [Classic only]
5811
5770
  @host_event_handler
5812
5771
  @with_connection_from_address
5813
- def on_authentication_user_confirmation_request(self, connection, code) -> None:
5772
+ def on_authentication_user_confirmation_request(
5773
+ self, connection: Connection, code: int
5774
+ ) -> None:
5814
5775
  # Ask what the pairing config should be for this connection
5815
5776
  pairing_config = self.pairing_config_factory(connection)
5816
5777
  io_capability = pairing_config.delegate.classic_io_capability
5817
5778
  peer_io_capability = connection.pairing_peer_io_capability
5779
+ if peer_io_capability is None:
5780
+ raise core.InvalidStateError("Unknown pairing_peer_io_capability")
5818
5781
 
5819
5782
  async def confirm() -> bool:
5820
5783
  # Ask the user to confirm the pairing, without display
@@ -5941,15 +5904,16 @@ class Device(utils.CompositeEventEmitter):
5941
5904
  # Respond
5942
5905
  if io_capability == hci.IoCapability.KEYBOARD_ONLY:
5943
5906
  # Ask the user to enter a string
5944
- async def get_pin_code():
5945
- pin_code = await connection.cancel_on_disconnection(
5907
+ async def get_pin_code() -> None:
5908
+ pin_code_str = await connection.cancel_on_disconnection(
5946
5909
  pairing_config.delegate.get_string(16)
5947
5910
  )
5948
5911
 
5949
- if pin_code is not None:
5950
- pin_code = bytes(pin_code, encoding='utf-8')
5912
+ if pin_code_str is not None:
5913
+ pin_code = bytes(pin_code_str, encoding='utf-8')
5951
5914
  pin_code_len = len(pin_code)
5952
- assert 0 < pin_code_len <= 16, "pin_code should be 1-16 bytes"
5915
+ if not 1 <= pin_code_len <= 16:
5916
+ raise core.InvalidArgumentError("pin_code should be 1-16 bytes")
5953
5917
  await self.host.send_command(
5954
5918
  hci.HCI_PIN_Code_Request_Reply_Command(
5955
5919
  bd_addr=connection.peer_address,
@@ -5991,7 +5955,7 @@ class Device(utils.CompositeEventEmitter):
5991
5955
  @host_event_handler
5992
5956
  @try_with_connection_from_address
5993
5957
  def on_remote_name(
5994
- self, connection: Optional[Connection], address: hci.Address, remote_name: bytes
5958
+ self, connection: Connection | None, address: hci.Address, remote_name: bytes
5995
5959
  ):
5996
5960
  # Try to decode the name
5997
5961
  try:
@@ -6010,7 +5974,7 @@ class Device(utils.CompositeEventEmitter):
6010
5974
  @host_event_handler
6011
5975
  @try_with_connection_from_address
6012
5976
  def on_remote_name_failure(
6013
- self, connection: Optional[Connection], address: hci.Address, error: int
5977
+ self, connection: Connection | None, address: hci.Address, error: int
6014
5978
  ):
6015
5979
  if connection:
6016
5980
  connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
@@ -6299,17 +6263,6 @@ class Device(utils.CompositeEventEmitter):
6299
6263
  )
6300
6264
  connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
6301
6265
 
6302
- @host_event_handler
6303
- @with_connection_from_handle
6304
- def on_connection_att_mtu_update(self, connection: Connection, att_mtu: int):
6305
- logger.debug(
6306
- f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
6307
- f'{connection.peer_address} as {connection.role_name}, '
6308
- f'{att_mtu}'
6309
- )
6310
- connection.att_mtu = att_mtu
6311
- connection.emit(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
6312
-
6313
6266
  @host_event_handler
6314
6267
  @with_connection_from_handle
6315
6268
  def on_connection_data_length_change(
@@ -6455,7 +6408,7 @@ class Device(utils.CompositeEventEmitter):
6455
6408
  @host_event_handler
6456
6409
  @try_with_connection_from_address
6457
6410
  def on_role_change_failure(
6458
- self, connection: Optional[Connection], address: hci.Address, error: int
6411
+ self, connection: Connection | None, address: hci.Address, error: int
6459
6412
  ):
6460
6413
  if connection:
6461
6414
  connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
@@ -6479,7 +6432,7 @@ class Device(utils.CompositeEventEmitter):
6479
6432
  def on_pairing(
6480
6433
  self,
6481
6434
  connection: Connection,
6482
- identity_address: Optional[hci.Address],
6435
+ identity_address: hci.Address | None,
6483
6436
  keys: PairingKeys,
6484
6437
  sc: bool,
6485
6438
  ) -> None:
@@ -6496,7 +6449,7 @@ class Device(utils.CompositeEventEmitter):
6496
6449
  @with_connection_from_handle
6497
6450
  def on_gatt_pdu(self, connection: Connection, pdu: bytes):
6498
6451
  # Parse the L2CAP payload into an ATT PDU object
6499
- att_pdu = ATT_PDU.from_bytes(pdu)
6452
+ att_pdu = att.ATT_PDU.from_bytes(pdu)
6500
6453
 
6501
6454
  # Conveniently, even-numbered op codes are client->server and
6502
6455
  # odd-numbered ones are server->client