bumble 0.0.211__py3-none-any.whl → 0.0.213__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +11 -9
  5. bumble/apps/bench.py +482 -31
  6. bumble/apps/console.py +5 -5
  7. bumble/apps/controller_info.py +47 -10
  8. bumble/apps/controller_loopback.py +7 -3
  9. bumble/apps/controllers.py +2 -2
  10. bumble/apps/device_info.py +2 -2
  11. bumble/apps/gatt_dump.py +2 -2
  12. bumble/apps/gg_bridge.py +2 -2
  13. bumble/apps/hci_bridge.py +2 -2
  14. bumble/apps/l2cap_bridge.py +2 -2
  15. bumble/apps/lea_unicast/app.py +6 -1
  16. bumble/apps/pair.py +204 -43
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/rfcomm_bridge.py +1 -1
  19. bumble/apps/scan.py +2 -2
  20. bumble/apps/show.py +4 -2
  21. bumble/apps/speaker/speaker.html +1 -0
  22. bumble/apps/speaker/speaker.js +113 -62
  23. bumble/apps/speaker/speaker.py +126 -18
  24. bumble/at.py +4 -4
  25. bumble/att.py +15 -18
  26. bumble/avc.py +7 -7
  27. bumble/avctp.py +5 -5
  28. bumble/avdtp.py +138 -88
  29. bumble/avrcp.py +52 -58
  30. bumble/colors.py +2 -2
  31. bumble/controller.py +84 -23
  32. bumble/core.py +13 -7
  33. bumble/{crypto.py → crypto/__init__.py} +11 -95
  34. bumble/crypto/builtin.py +652 -0
  35. bumble/crypto/cryptography.py +84 -0
  36. bumble/device.py +688 -345
  37. bumble/drivers/__init__.py +2 -2
  38. bumble/drivers/common.py +0 -2
  39. bumble/drivers/intel.py +40 -40
  40. bumble/drivers/rtk.py +28 -35
  41. bumble/gatt.py +7 -9
  42. bumble/gatt_adapters.py +4 -5
  43. bumble/gatt_client.py +31 -34
  44. bumble/gatt_server.py +15 -17
  45. bumble/hci.py +2635 -2878
  46. bumble/helpers.py +4 -5
  47. bumble/hfp.py +76 -57
  48. bumble/hid.py +24 -12
  49. bumble/host.py +117 -34
  50. bumble/keys.py +68 -52
  51. bumble/l2cap.py +329 -403
  52. bumble/link.py +6 -270
  53. bumble/pairing.py +23 -20
  54. bumble/pandora/__init__.py +1 -1
  55. bumble/pandora/config.py +2 -2
  56. bumble/pandora/device.py +6 -6
  57. bumble/pandora/host.py +38 -39
  58. bumble/pandora/l2cap.py +4 -4
  59. bumble/pandora/security.py +73 -57
  60. bumble/pandora/utils.py +3 -3
  61. bumble/profiles/aics.py +3 -5
  62. bumble/profiles/ancs.py +3 -1
  63. bumble/profiles/ascs.py +143 -136
  64. bumble/profiles/asha.py +13 -8
  65. bumble/profiles/bap.py +3 -4
  66. bumble/profiles/csip.py +3 -5
  67. bumble/profiles/device_information_service.py +2 -2
  68. bumble/profiles/gap.py +2 -2
  69. bumble/profiles/gatt_service.py +1 -3
  70. bumble/profiles/hap.py +42 -58
  71. bumble/profiles/le_audio.py +4 -4
  72. bumble/profiles/mcp.py +16 -13
  73. bumble/profiles/vcs.py +8 -10
  74. bumble/profiles/vocs.py +6 -9
  75. bumble/rfcomm.py +27 -18
  76. bumble/rtp.py +1 -2
  77. bumble/sdp.py +2 -2
  78. bumble/smp.py +71 -69
  79. bumble/tools/rtk_util.py +2 -2
  80. bumble/transport/__init__.py +2 -16
  81. bumble/transport/android_netsim.py +5 -5
  82. bumble/transport/common.py +4 -4
  83. bumble/transport/pyusb.py +2 -2
  84. bumble/utils.py +2 -5
  85. bumble/vendor/android/hci.py +118 -200
  86. bumble/vendor/zephyr/hci.py +32 -27
  87. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/METADATA +5 -5
  88. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/RECORD +92 -93
  89. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
  90. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
  91. bumble/apps/link_relay/__init__.py +0 -0
  92. bumble/apps/link_relay/link_relay.py +0 -289
  93. bumble/apps/link_relay/logging.yml +0 -21
  94. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
  95. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/device.py CHANGED
@@ -35,12 +35,10 @@ import secrets
35
35
  import sys
36
36
  from typing import (
37
37
  Any,
38
+ Awaitable,
38
39
  Callable,
39
40
  ClassVar,
40
- Deque,
41
- Dict,
42
41
  Optional,
43
- Type,
44
42
  TypeVar,
45
43
  Union,
46
44
  cast,
@@ -61,7 +59,6 @@ from bumble.core import (
61
59
  BaseBumbleError,
62
60
  ConnectionParameterUpdateError,
63
61
  CommandTimeoutError,
64
- ConnectionParameters,
65
62
  ConnectionPHY,
66
63
  InvalidArgumentError,
67
64
  InvalidOperationError,
@@ -88,6 +85,7 @@ from bumble.profiles import gatt_service
88
85
  if TYPE_CHECKING:
89
86
  from bumble.transport.common import TransportSource, TransportSink
90
87
 
88
+ _T = TypeVar('_T')
91
89
 
92
90
  # -----------------------------------------------------------------------------
93
91
  # Logging
@@ -100,9 +98,9 @@ logger = logging.getLogger(__name__)
100
98
  # fmt: off
101
99
  # pylint: disable=line-too-long
102
100
 
103
- DEVICE_MIN_SCAN_INTERVAL = 25
101
+ DEVICE_MIN_SCAN_INTERVAL = 2.5
104
102
  DEVICE_MAX_SCAN_INTERVAL = 10240
105
- DEVICE_MIN_SCAN_WINDOW = 25
103
+ DEVICE_MIN_SCAN_WINDOW = 2.5
106
104
  DEVICE_MAX_SCAN_WINDOW = 10240
107
105
  DEVICE_MIN_LE_RSSI = -127
108
106
  DEVICE_MAX_LE_RSSI = 20
@@ -141,6 +139,9 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
141
139
  DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
142
140
  DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
143
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
144
145
 
145
146
  # fmt: on
146
147
  # pylint: enable=line-too-long
@@ -203,25 +204,35 @@ class Advertisement:
203
204
  # -----------------------------------------------------------------------------
204
205
  class LegacyAdvertisement(Advertisement):
205
206
  @classmethod
206
- def from_advertising_report(cls, report):
207
+ def from_advertising_report(
208
+ cls, report: hci.HCI_LE_Advertising_Report_Event.Report
209
+ ) -> Self:
207
210
  return cls(
208
211
  address=report.address,
209
212
  rssi=report.rssi,
210
213
  is_legacy=True,
211
- is_connectable=report.event_type
212
- in (
213
- hci.HCI_LE_Advertising_Report_Event.ADV_IND,
214
- hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
214
+ is_connectable=(
215
+ report.event_type
216
+ in (
217
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
218
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
219
+ )
220
+ ),
221
+ is_directed=(
222
+ report.event_type
223
+ == hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
224
+ ),
225
+ is_scannable=(
226
+ report.event_type
227
+ in (
228
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
229
+ hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
230
+ )
215
231
  ),
216
- is_directed=report.event_type
217
- == hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
218
- is_scannable=report.event_type
219
- in (
220
- hci.HCI_LE_Advertising_Report_Event.ADV_IND,
221
- hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
232
+ is_scan_response=(
233
+ report.event_type
234
+ == hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
222
235
  ),
223
- is_scan_response=report.event_type
224
- == hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
225
236
  data_bytes=report.data,
226
237
  )
227
238
 
@@ -229,18 +240,20 @@ class LegacyAdvertisement(Advertisement):
229
240
  # -----------------------------------------------------------------------------
230
241
  class ExtendedAdvertisement(Advertisement):
231
242
  @classmethod
232
- def from_advertising_report(cls, report):
243
+ def from_advertising_report(
244
+ cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
245
+ ) -> Self:
233
246
  # fmt: off
234
247
  # pylint: disable=line-too-long
235
248
  return cls(
236
249
  address = report.address,
237
250
  rssi = report.rssi,
238
- 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,
239
252
  is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
240
- is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
241
- is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
242
- is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
243
- 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,
244
257
  is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
245
258
  is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
246
259
  primary_phy = report.primary_phy,
@@ -437,7 +450,7 @@ class AdvertisingEventProperties:
437
450
 
438
451
  @classmethod
439
452
  def from_advertising_type(
440
- cls: Type[AdvertisingEventProperties],
453
+ cls: type[AdvertisingEventProperties],
441
454
  advertising_type: AdvertisingType,
442
455
  ) -> AdvertisingEventProperties:
443
456
  return cls(
@@ -479,12 +492,23 @@ class PeriodicAdvertisement:
479
492
 
480
493
  # -----------------------------------------------------------------------------
481
494
  @dataclass
482
- 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
+
483
507
  address: hci.Address
484
508
  sid: int
485
509
  num_bis: int
486
510
  nse: int
487
- iso_interval: int
511
+ iso_interval: float
488
512
  bn: int
489
513
  pto: int
490
514
  irc: int
@@ -492,8 +516,8 @@ class BIGInfoAdvertisement:
492
516
  sdu_interval: int
493
517
  max_sdu: int
494
518
  phy: hci.Phy
495
- framed: bool
496
- encrypted: bool
519
+ framing: Framing
520
+ encryption: Encryption
497
521
 
498
522
  @classmethod
499
523
  def from_report(cls, address: hci.Address, sid: int, report) -> Self:
@@ -502,7 +526,7 @@ class BIGInfoAdvertisement:
502
526
  sid,
503
527
  report.num_bis,
504
528
  report.nse,
505
- report.iso_interval,
529
+ report.iso_interval * 1.25,
506
530
  report.bn,
507
531
  report.pto,
508
532
  report.irc,
@@ -510,8 +534,8 @@ class BIGInfoAdvertisement:
510
534
  report.sdu_interval,
511
535
  report.max_sdu,
512
536
  hci.Phy(report.phy),
513
- report.framing != 0,
514
- report.encryption != 0,
537
+ cls.Framing(report.framing),
538
+ cls.Encryption(report.encryption),
515
539
  )
516
540
 
517
541
 
@@ -529,8 +553,8 @@ class AdvertisingParameters:
529
553
  advertising_event_properties: AdvertisingEventProperties = field(
530
554
  default_factory=AdvertisingEventProperties
531
555
  )
532
- primary_advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
533
- primary_advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
556
+ primary_advertising_interval_min: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
557
+ primary_advertising_interval_max: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
534
558
  primary_advertising_channel_map: (
535
559
  hci.HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap
536
560
  ) = (
@@ -554,8 +578,8 @@ class AdvertisingParameters:
554
578
  # -----------------------------------------------------------------------------
555
579
  @dataclass
556
580
  class PeriodicAdvertisingParameters:
557
- periodic_advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
558
- periodic_advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
581
+ periodic_advertising_interval_min: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
582
+ periodic_advertising_interval_max: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
559
583
  periodic_advertising_properties: (
560
584
  hci.HCI_LE_Set_Periodic_Advertising_Parameters_Command.Properties
561
585
  ) = field(
@@ -581,6 +605,12 @@ class AdvertisingSet(utils.EventEmitter):
581
605
  enabled: bool = False
582
606
  periodic_enabled: bool = False
583
607
 
608
+ EVENT_START = "start"
609
+ EVENT_STOP = "stop"
610
+ EVENT_START_PERIODIC = "start_periodic"
611
+ EVENT_STOP_PERIODIC = "stop_periodic"
612
+ EVENT_TERMINATION = "termination"
613
+
584
614
  def __post_init__(self) -> None:
585
615
  super().__init__()
586
616
 
@@ -679,8 +709,12 @@ class AdvertisingSet(utils.EventEmitter):
679
709
  await self.device.send_command(
680
710
  hci.HCI_LE_Set_Periodic_Advertising_Parameters_Command(
681
711
  advertising_handle=self.advertising_handle,
682
- periodic_advertising_interval_min=advertising_parameters.periodic_advertising_interval_min,
683
- periodic_advertising_interval_max=advertising_parameters.periodic_advertising_interval_max,
712
+ periodic_advertising_interval_min=int(
713
+ advertising_parameters.periodic_advertising_interval_min / 1.25
714
+ ),
715
+ periodic_advertising_interval_max=int(
716
+ advertising_parameters.periodic_advertising_interval_max / 1.25
717
+ ),
684
718
  periodic_advertising_properties=advertising_parameters.periodic_advertising_properties,
685
719
  ),
686
720
  check_result=True,
@@ -731,7 +765,7 @@ class AdvertisingSet(utils.EventEmitter):
731
765
  )
732
766
  self.enabled = True
733
767
 
734
- self.emit('start')
768
+ self.emit(self.EVENT_START)
735
769
 
736
770
  async def stop(self) -> None:
737
771
  await self.device.send_command(
@@ -745,7 +779,7 @@ class AdvertisingSet(utils.EventEmitter):
745
779
  )
746
780
  self.enabled = False
747
781
 
748
- self.emit('stop')
782
+ self.emit(self.EVENT_STOP)
749
783
 
750
784
  async def start_periodic(self, include_adi: bool = False) -> None:
751
785
  if self.periodic_enabled:
@@ -759,7 +793,7 @@ class AdvertisingSet(utils.EventEmitter):
759
793
  )
760
794
  self.periodic_enabled = True
761
795
 
762
- self.emit('start_periodic')
796
+ self.emit(self.EVENT_START_PERIODIC)
763
797
 
764
798
  async def stop_periodic(self) -> None:
765
799
  if not self.periodic_enabled:
@@ -773,7 +807,7 @@ class AdvertisingSet(utils.EventEmitter):
773
807
  )
774
808
  self.periodic_enabled = False
775
809
 
776
- self.emit('stop_periodic')
810
+ self.emit(self.EVENT_STOP_PERIODIC)
777
811
 
778
812
  async def remove(self) -> None:
779
813
  await self.device.send_command(
@@ -797,7 +831,7 @@ class AdvertisingSet(utils.EventEmitter):
797
831
 
798
832
  def on_termination(self, status: int) -> None:
799
833
  self.enabled = False
800
- self.emit('termination', status)
834
+ self.emit(self.EVENT_TERMINATION, status)
801
835
 
802
836
 
803
837
  # -----------------------------------------------------------------------------
@@ -820,9 +854,17 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
820
854
  filter_duplicates: bool
821
855
  status: int
822
856
  advertiser_phy: int
823
- periodic_advertising_interval: int
857
+ periodic_advertising_interval: float # Advertising interval, in milliseconds
824
858
  advertiser_clock_accuracy: int
825
859
 
860
+ EVENT_STATE_CHANGE = "state_change"
861
+ EVENT_ESTABLISHMENT = "establishment"
862
+ EVENT_CANCELLATION = "cancellation"
863
+ EVENT_ERROR = "error"
864
+ EVENT_LOSS = "loss"
865
+ EVENT_PERIODIC_ADVERTISEMENT = "periodic_advertisement"
866
+ EVENT_BIGINFO_ADVERTISEMENT = "biginfo_advertisement"
867
+
826
868
  def __init__(
827
869
  self,
828
870
  device: Device,
@@ -855,7 +897,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
855
897
  def state(self, state: State) -> None:
856
898
  logger.debug(f'{self} -> {state.name}')
857
899
  self._state = state
858
- self.emit('state_change')
900
+ self.emit(self.EVENT_STATE_CHANGE)
859
901
 
860
902
  async def establish(self) -> None:
861
903
  if self.state != self.State.INIT:
@@ -936,10 +978,10 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
936
978
  if status == hci.HCI_SUCCESS:
937
979
  self.sync_handle = sync_handle
938
980
  self.advertiser_phy = advertiser_phy
939
- self.periodic_advertising_interval = periodic_advertising_interval
981
+ self.periodic_advertising_interval = periodic_advertising_interval * 1.25
940
982
  self.advertiser_clock_accuracy = advertiser_clock_accuracy
941
983
  self.state = self.State.ESTABLISHED
942
- self.emit('establishment')
984
+ self.emit(self.EVENT_ESTABLISHMENT)
943
985
  return
944
986
 
945
987
  # We don't need to keep a reference anymore
@@ -948,15 +990,15 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
948
990
 
949
991
  if status == hci.HCI_OPERATION_CANCELLED_BY_HOST_ERROR:
950
992
  self.state = self.State.CANCELLED
951
- self.emit('cancellation')
993
+ self.emit(self.EVENT_CANCELLATION)
952
994
  return
953
995
 
954
996
  self.state = self.State.ERROR
955
- self.emit('error')
997
+ self.emit(self.EVENT_ERROR)
956
998
 
957
999
  def on_loss(self):
958
1000
  self.state = self.State.LOST
959
- self.emit('loss')
1001
+ self.emit(self.EVENT_LOSS)
960
1002
 
961
1003
  def on_periodic_advertising_report(self, report) -> None:
962
1004
  self.data_accumulator += report.data
@@ -967,7 +1009,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
967
1009
  return
968
1010
 
969
1011
  self.emit(
970
- 'periodic_advertisement',
1012
+ self.EVENT_PERIODIC_ADVERTISEMENT,
971
1013
  PeriodicAdvertisement(
972
1014
  self.advertiser_address,
973
1015
  self.sid,
@@ -984,8 +1026,8 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
984
1026
 
985
1027
  def on_biginfo_advertising_report(self, report) -> None:
986
1028
  self.emit(
987
- 'biginfo_advertisement',
988
- BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
1029
+ self.EVENT_BIGINFO_ADVERTISEMENT,
1030
+ BigInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
989
1031
  )
990
1032
 
991
1033
  def __str__(self) -> str:
@@ -1003,14 +1045,24 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
1003
1045
  # -----------------------------------------------------------------------------
1004
1046
  @dataclass
1005
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
+
1006
1058
  num_bis: int
1007
- sdu_interval: int
1059
+ sdu_interval: int # SDU interval, in microseconds
1008
1060
  max_sdu: int
1009
- max_transport_latency: int
1061
+ max_transport_latency: int # Max transport latency, in milliseconds
1010
1062
  rtn: int
1011
1063
  phy: hci.PhyBit = hci.PhyBit.LE_2M
1012
- packing: int = 0
1013
- framing: int = 0
1064
+ packing: Packing = Packing.SEQUENTIAL
1065
+ framing: Framing = Framing.UNFRAMED
1014
1066
  broadcast_code: bytes | None = None
1015
1067
 
1016
1068
 
@@ -1033,15 +1085,15 @@ class Big(utils.EventEmitter):
1033
1085
  state: State = State.PENDING
1034
1086
 
1035
1087
  # Attributes provided by BIG Create Complete event
1036
- big_sync_delay: int = 0
1037
- transport_latency_big: int = 0
1038
- 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
1039
1091
  nse: int = 0
1040
1092
  bn: int = 0
1041
1093
  pto: int = 0
1042
1094
  irc: int = 0
1043
1095
  max_pdu: int = 0
1044
- iso_interval: int = 0
1096
+ iso_interval: float = 0.0 # ISO interval, in milliseconds
1045
1097
  bis_links: Sequence[BisLink] = ()
1046
1098
 
1047
1099
  def __post_init__(self) -> None:
@@ -1102,7 +1154,7 @@ class BigSync(utils.EventEmitter):
1102
1154
  pto: int = 0
1103
1155
  irc: int = 0
1104
1156
  max_pdu: int = 0
1105
- iso_interval: int = 0
1157
+ iso_interval: float = 0.0
1106
1158
  bis_links: Sequence[BisLink] = ()
1107
1159
 
1108
1160
  def __post_init__(self) -> None:
@@ -1183,11 +1235,11 @@ class ChannelSoundingProcedure:
1183
1235
  selected_tx_power: int
1184
1236
  subevent_len: int
1185
1237
  subevents_per_event: int
1186
- subevent_interval: int
1238
+ subevent_interval: float # milliseconds.
1187
1239
  event_interval: int
1188
1240
  procedure_interval: int
1189
1241
  procedure_count: int
1190
- max_procedure_len: int
1242
+ max_procedure_len: float # milliseconds.
1191
1243
 
1192
1244
 
1193
1245
  # -----------------------------------------------------------------------------
@@ -1212,9 +1264,8 @@ class Peer:
1212
1264
  def __init__(self, connection: Connection) -> None:
1213
1265
  self.connection = connection
1214
1266
 
1215
- # Create a GATT client for the connection
1216
- self.gatt_client = gatt_client.Client(connection)
1217
- connection.gatt_client = self.gatt_client
1267
+ # Shortcut to the connection's GATT client
1268
+ self.gatt_client = connection.gatt_client
1218
1269
 
1219
1270
  @property
1220
1271
  def services(self) -> list[gatt_client.ServiceProxy]:
@@ -1222,7 +1273,7 @@ class Peer:
1222
1273
 
1223
1274
  async def request_mtu(self, mtu: int) -> int:
1224
1275
  mtu = await self.gatt_client.request_mtu(mtu)
1225
- self.connection.emit('connection_att_mtu_update')
1276
+ self.connection.emit(self.connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
1226
1277
  return mtu
1227
1278
 
1228
1279
  async def discover_service(
@@ -1327,7 +1378,7 @@ class Peer:
1327
1378
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
1328
1379
 
1329
1380
  def create_service_proxy(
1330
- self, proxy_class: Type[_PROXY_CLASS]
1381
+ self, proxy_class: type[_PROXY_CLASS]
1331
1382
  ) -> Optional[_PROXY_CLASS]:
1332
1383
  if proxy := proxy_class.from_client(self.gatt_client):
1333
1384
  return cast(_PROXY_CLASS, proxy)
@@ -1335,7 +1386,7 @@ class Peer:
1335
1386
  return None
1336
1387
 
1337
1388
  async def discover_service_and_create_proxy(
1338
- self, proxy_class: Type[_PROXY_CLASS]
1389
+ self, proxy_class: type[_PROXY_CLASS]
1339
1390
  ) -> Optional[_PROXY_CLASS]:
1340
1391
  # Discover the first matching service and its characteristics
1341
1392
  services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
@@ -1390,6 +1441,9 @@ class ScoLink(utils.CompositeEventEmitter):
1390
1441
  link_type: int
1391
1442
  sink: Optional[Callable[[hci.HCI_SynchronousDataPacket], Any]] = None
1392
1443
 
1444
+ EVENT_DISCONNECTION: ClassVar[str] = "disconnection"
1445
+ EVENT_DISCONNECTION_FAILURE: ClassVar[str] = "disconnection_failure"
1446
+
1393
1447
  def __post_init__(self) -> None:
1394
1448
  super().__init__()
1395
1449
 
@@ -1445,7 +1499,7 @@ class _IsoLink:
1445
1499
  check_result=True,
1446
1500
  )
1447
1501
 
1448
- async def remove_data_path(self, direction: _IsoLink.Direction) -> int:
1502
+ async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
1449
1503
  """Remove a data path with controller on given direction.
1450
1504
 
1451
1505
  Args:
@@ -1457,7 +1511,9 @@ class _IsoLink:
1457
1511
  response = await self.device.send_command(
1458
1512
  hci.HCI_LE_Remove_ISO_Data_Path_Command(
1459
1513
  connection_handle=self.handle,
1460
- data_path_direction=direction,
1514
+ data_path_direction=sum(
1515
+ 1 << direction for direction in set(directions)
1516
+ ),
1461
1517
  ),
1462
1518
  check_result=False,
1463
1519
  )
@@ -1467,10 +1523,74 @@ class _IsoLink:
1467
1523
  """Write an ISO SDU."""
1468
1524
  self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
1469
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
+
1470
1537
  @property
1471
1538
  def data_packet_queue(self) -> DataPacketQueue | None:
1472
1539
  return self.device.host.get_data_packet_queue(self.handle)
1473
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
+
1474
1594
 
1475
1595
  # -----------------------------------------------------------------------------
1476
1596
  @dataclass
@@ -1484,9 +1604,28 @@ class CisLink(utils.EventEmitter, _IsoLink):
1484
1604
  handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
1485
1605
  cis_id: int # CIS ID assigned by Central device
1486
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
1487
1621
  state: State = State.PENDING
1488
1622
  sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
1489
1623
 
1624
+ EVENT_DISCONNECTION: ClassVar[str] = "disconnection"
1625
+ EVENT_DISCONNECTION_FAILURE: ClassVar[str] = "disconnection_failure"
1626
+ EVENT_ESTABLISHMENT: ClassVar[str] = "establishment"
1627
+ EVENT_ESTABLISHMENT_FAILURE: ClassVar[str] = "establishment_failure"
1628
+
1490
1629
  def __post_init__(self) -> None:
1491
1630
  super().__init__()
1492
1631
 
@@ -1521,7 +1660,7 @@ class IsoPacketStream:
1521
1660
  self.iso_link = iso_link
1522
1661
  self.data_packet_queue = iso_link.data_packet_queue
1523
1662
  self.data_packet_queue.on('flow', self._on_flow)
1524
- self._thresholds: Deque[int] = collections.deque()
1663
+ self._thresholds: collections.deque[int] = collections.deque()
1525
1664
  self._semaphore = asyncio.Semaphore(max_queue_size)
1526
1665
 
1527
1666
  def _on_flow(self) -> None:
@@ -1561,15 +1700,58 @@ class Connection(utils.CompositeEventEmitter):
1561
1700
  peer_resolvable_address: Optional[hci.Address]
1562
1701
  peer_le_features: Optional[hci.LeFeatureMask]
1563
1702
  role: hci.Role
1703
+ parameters: Parameters
1564
1704
  encryption: int
1705
+ encryption_key_size: int
1565
1706
  authenticated: bool
1566
1707
  sc: bool
1567
- link_key_type: int
1568
1708
  gatt_client: gatt_client.Client
1569
1709
  pairing_peer_io_capability: Optional[int]
1570
1710
  pairing_peer_authentication_requirements: Optional[int]
1571
1711
  cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
1572
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
1715
+
1716
+ EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
1717
+ EVENT_DISCONNECTION = "disconnection"
1718
+ EVENT_DISCONNECTION_FAILURE = "disconnection_failure"
1719
+ EVENT_CONNECTION_AUTHENTICATION = "connection_authentication"
1720
+ EVENT_CONNECTION_AUTHENTICATION_FAILURE = "connection_authentication_failure"
1721
+ EVENT_REMOTE_NAME = "remote_name"
1722
+ EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
1723
+ EVENT_CONNECTION_ENCRYPTION_CHANGE = "connection_encryption_change"
1724
+ EVENT_CONNECTION_ENCRYPTION_FAILURE = "connection_encryption_failure"
1725
+ EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH = "connection_encryption_key_refresh"
1726
+ EVENT_CONNECTION_PARAMETERS_UPDATE = "connection_parameters_update"
1727
+ EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE = "connection_parameters_update_failure"
1728
+ EVENT_CONNECTION_PHY_UPDATE = "connection_phy_update"
1729
+ EVENT_CONNECTION_PHY_UPDATE_FAILURE = "connection_phy_update_failure"
1730
+ EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
1731
+ EVENT_CONNECTION_DATA_LENGTH_CHANGE = "connection_data_length_change"
1732
+ EVENT_CHANNEL_SOUNDING_CAPABILITIES_FAILURE = (
1733
+ "channel_sounding_capabilities_failure"
1734
+ )
1735
+ EVENT_CHANNEL_SOUNDING_CAPABILITIES = "channel_sounding_capabilities"
1736
+ EVENT_CHANNEL_SOUNDING_CONFIG_FAILURE = "channel_sounding_config_failure"
1737
+ EVENT_CHANNEL_SOUNDING_CONFIG = "channel_sounding_config"
1738
+ EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED = "channel_sounding_config_removed"
1739
+ EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE = "channel_sounding_procedure_failure"
1740
+ EVENT_CHANNEL_SOUNDING_PROCEDURE = "channel_sounding_procedure"
1741
+ EVENT_MODE_CHANGE = "mode_change"
1742
+ EVENT_MODE_CHANGE_FAILURE = "mode_change_failure"
1743
+ EVENT_ROLE_CHANGE = "role_change"
1744
+ EVENT_ROLE_CHANGE_FAILURE = "role_change_failure"
1745
+ EVENT_CLASSIC_PAIRING = "classic_pairing"
1746
+ EVENT_CLASSIC_PAIRING_FAILURE = "classic_pairing_failure"
1747
+ EVENT_PAIRING_START = "pairing_start"
1748
+ EVENT_PAIRING = "pairing"
1749
+ EVENT_PAIRING_FAILURE = "pairing_failure"
1750
+ EVENT_SECURITY_REQUEST = "security_request"
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"
1573
1755
 
1574
1756
  @utils.composite_listener
1575
1757
  class Listener:
@@ -1600,17 +1782,23 @@ class Connection(utils.CompositeEventEmitter):
1600
1782
  def on_connection_encryption_key_refresh(self):
1601
1783
  pass
1602
1784
 
1785
+ @dataclass
1786
+ class Parameters:
1787
+ connection_interval: float # Connection interval, in milliseconds. [LE only]
1788
+ peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
1789
+ supervision_timeout: float # Supervision timeout, in milliseconds.
1790
+
1603
1791
  def __init__(
1604
1792
  self,
1605
- device,
1606
- handle,
1607
- transport,
1608
- self_address,
1609
- self_resolvable_address,
1610
- peer_address,
1611
- peer_resolvable_address,
1612
- role,
1613
- parameters,
1793
+ device: Device,
1794
+ handle: int,
1795
+ transport: core.PhysicalTransport,
1796
+ self_address: hci.Address,
1797
+ self_resolvable_address: Optional[hci.Address],
1798
+ peer_address: hci.Address,
1799
+ peer_resolvable_address: Optional[hci.Address],
1800
+ role: hci.Role,
1801
+ parameters: Parameters,
1614
1802
  ):
1615
1803
  super().__init__()
1616
1804
  self.device = device
@@ -1624,12 +1812,12 @@ class Connection(utils.CompositeEventEmitter):
1624
1812
  self.role = role
1625
1813
  self.parameters = parameters
1626
1814
  self.encryption = 0
1815
+ self.encryption_key_size = 0
1627
1816
  self.authenticated = False
1628
1817
  self.sc = False
1629
- self.link_key_type = None
1630
1818
  self.att_mtu = ATT_DEFAULT_MTU
1631
1819
  self.data_length = DEVICE_DEFAULT_DATA_LENGTH
1632
- self.gatt_client = None # Per-connection client
1820
+ self.gatt_client = gatt_client.Client(self) # Per-connection client
1633
1821
  self.gatt_server = (
1634
1822
  device.gatt_server
1635
1823
  ) # By default, use the device's shared server
@@ -1740,28 +1928,38 @@ class Connection(utils.CompositeEventEmitter):
1740
1928
  """Idles the current task waiting for a disconnect or timeout"""
1741
1929
 
1742
1930
  abort = asyncio.get_running_loop().create_future()
1743
- self.on('disconnection', abort.set_result)
1744
- self.on('disconnection_failure', abort.set_exception)
1931
+ self.on(self.EVENT_DISCONNECTION, abort.set_result)
1932
+ self.on(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
1745
1933
 
1746
1934
  try:
1747
1935
  await asyncio.wait_for(
1748
- utils.cancel_on_event(self.device, 'flush', abort), timeout
1936
+ utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout
1749
1937
  )
1750
1938
  finally:
1751
- self.remove_listener('disconnection', abort.set_result)
1752
- self.remove_listener('disconnection_failure', abort.set_exception)
1939
+ self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
1940
+ self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
1753
1941
 
1754
1942
  async def set_data_length(self, tx_octets, tx_time) -> None:
1755
1943
  return await self.device.set_data_length(self, tx_octets, tx_time)
1756
1944
 
1757
1945
  async def update_parameters(
1758
1946
  self,
1759
- connection_interval_min,
1760
- connection_interval_max,
1761
- max_latency,
1762
- supervision_timeout,
1947
+ connection_interval_min: float,
1948
+ connection_interval_max: float,
1949
+ max_latency: int,
1950
+ supervision_timeout: float,
1763
1951
  use_l2cap=False,
1764
- ):
1952
+ ) -> None:
1953
+ """
1954
+ Request an update of the connection parameters.
1955
+
1956
+ Args:
1957
+ connection_interval_min: Minimum interval, in milliseconds.
1958
+ connection_interval_max: Maximum interval, in milliseconds.
1959
+ max_latency: Latency, in number of intervals.
1960
+ supervision_timeout: Timeout, in milliseconds.
1961
+ use_l2cap: Request the update via L2CAP.
1962
+ """
1765
1963
  return await self.device.update_connection_parameters(
1766
1964
  self,
1767
1965
  connection_interval_min,
@@ -1809,6 +2007,12 @@ class Connection(utils.CompositeEventEmitter):
1809
2007
  def data_packet_queue(self) -> DataPacketQueue | None:
1810
2008
  return self.device.host.get_data_packet_queue(self.handle)
1811
2009
 
2010
+ def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
2011
+ """
2012
+ Helper method to call `utils.cancel_on_event` for the 'disconnection' event
2013
+ """
2014
+ return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
2015
+
1812
2016
  async def __aenter__(self):
1813
2017
  return self
1814
2018
 
@@ -1848,8 +2052,8 @@ class DeviceConfiguration:
1848
2052
  address: hci.Address = hci.Address(DEVICE_DEFAULT_ADDRESS)
1849
2053
  class_of_device: int = DEVICE_DEFAULT_CLASS_OF_DEVICE
1850
2054
  scan_response_data: bytes = DEVICE_DEFAULT_SCAN_RESPONSE_DATA
1851
- advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1852
- advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
2055
+ advertising_interval_min: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
2056
+ advertising_interval_max: float = DEVICE_DEFAULT_ADVERTISING_INTERVAL
1853
2057
  le_enabled: bool = True
1854
2058
  le_simultaneous_enabled: bool = False
1855
2059
  le_privacy_enabled: bool = False
@@ -1879,9 +2083,9 @@ class DeviceConfiguration:
1879
2083
  gatt_service_enabled: bool = True
1880
2084
 
1881
2085
  def __post_init__(self) -> None:
1882
- self.gatt_services: list[Dict[str, Any]] = []
2086
+ self.gatt_services: list[dict[str, Any]] = []
1883
2087
 
1884
- def load_from_dict(self, config: Dict[str, Any]) -> None:
2088
+ def load_from_dict(self, config: dict[str, Any]) -> None:
1885
2089
  config = copy.deepcopy(config)
1886
2090
 
1887
2091
  # Load simple properties
@@ -1941,13 +2145,13 @@ class DeviceConfiguration:
1941
2145
  self.load_from_dict(json.load(file))
1942
2146
 
1943
2147
  @classmethod
1944
- def from_file(cls: Type[Self], filename: str) -> Self:
2148
+ def from_file(cls: type[Self], filename: str) -> Self:
1945
2149
  config = cls()
1946
2150
  config.load_from_file(filename)
1947
2151
  return config
1948
2152
 
1949
2153
  @classmethod
1950
- def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
2154
+ def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
1951
2155
  device_config = cls()
1952
2156
  device_config.load_from_dict(config)
1953
2157
  return device_config
@@ -2044,24 +2248,44 @@ class Device(utils.CompositeEventEmitter):
2044
2248
  advertising_data: bytes
2045
2249
  scan_response_data: bytes
2046
2250
  cs_capabilities: ChannelSoundingCapabilities | None = None
2047
- connections: Dict[int, Connection]
2048
- pending_connections: Dict[hci.Address, Connection]
2049
- classic_pending_accepts: Dict[
2251
+ connections: dict[int, Connection]
2252
+ pending_connections: dict[hci.Address, Connection]
2253
+ classic_pending_accepts: dict[
2050
2254
  hci.Address,
2051
2255
  list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
2052
2256
  ]
2053
- advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
2257
+ advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
2054
2258
  periodic_advertising_syncs: list[PeriodicAdvertisingSync]
2055
2259
  config: DeviceConfiguration
2056
2260
  legacy_advertiser: Optional[LegacyAdvertiser]
2057
- sco_links: Dict[int, ScoLink]
2058
- cis_links: Dict[int, CisLink]
2261
+ sco_links: dict[int, ScoLink]
2262
+ cis_links: dict[int, CisLink]
2059
2263
  bigs: dict[int, Big]
2060
2264
  bis_links: dict[int, BisLink]
2061
2265
  big_syncs: dict[int, BigSync]
2062
- _pending_cis: Dict[int, tuple[int, int]]
2266
+ _pending_cis: dict[int, tuple[int, int]]
2063
2267
  gatt_service: gatt_service.GenericAttributeProfileService | None = None
2064
2268
 
2269
+ EVENT_ADVERTISEMENT = "advertisement"
2270
+ EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER = "periodic_advertising_sync_transfer"
2271
+ EVENT_KEY_STORE_UPDATE = "key_store_update"
2272
+ EVENT_FLUSH = "flush"
2273
+ EVENT_CONNECTION = "connection"
2274
+ EVENT_CONNECTION_FAILURE = "connection_failure"
2275
+ EVENT_SCO_REQUEST = "sco_request"
2276
+ EVENT_INQUIRY_COMPLETE = "inquiry_complete"
2277
+ EVENT_REMOTE_NAME = "remote_name"
2278
+ EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
2279
+ EVENT_SCO_CONNECTION = "sco_connection"
2280
+ EVENT_SCO_CONNECTION_FAILURE = "sco_connection_failure"
2281
+ EVENT_CIS_REQUEST = "cis_request"
2282
+ EVENT_CIS_ESTABLISHMENT = "cis_establishment"
2283
+ EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
2284
+ EVENT_ROLE_CHANGE_FAILURE = "role_change_failure"
2285
+ EVENT_INQUIRY_RESULT = "inquiry_result"
2286
+ EVENT_REMOTE_NAME = "remote_name"
2287
+ EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
2288
+
2065
2289
  @utils.composite_listener
2066
2290
  class Listener:
2067
2291
  def on_advertisement(self, advertisement):
@@ -2199,8 +2423,8 @@ class Device(utils.CompositeEventEmitter):
2199
2423
  self.address_generation_offload = config.address_generation_offload
2200
2424
 
2201
2425
  # Extended advertising.
2202
- self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2203
- self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
2426
+ self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
2427
+ self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
2204
2428
 
2205
2429
  # Legacy advertising.
2206
2430
  # The advertising and scan response data, as well as the advertising interval
@@ -2748,8 +2972,8 @@ class Device(utils.CompositeEventEmitter):
2748
2972
  auto_restart: bool = False,
2749
2973
  advertising_data: Optional[bytes] = None,
2750
2974
  scan_response_data: Optional[bytes] = None,
2751
- advertising_interval_min: Optional[int] = None,
2752
- advertising_interval_max: Optional[int] = None,
2975
+ advertising_interval_min: Optional[float] = None,
2976
+ advertising_interval_max: Optional[float] = None,
2753
2977
  ) -> None:
2754
2978
  """Start legacy advertising.
2755
2979
 
@@ -3149,7 +3373,7 @@ class Device(utils.CompositeEventEmitter):
3149
3373
  accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive)
3150
3374
  self.advertisement_accumulators[report.address] = accumulator
3151
3375
  if advertisement := accumulator.update(report):
3152
- self.emit('advertisement', advertisement)
3376
+ self.emit(self.EVENT_ADVERTISEMENT, advertisement)
3153
3377
 
3154
3378
  async def create_periodic_advertising_sync(
3155
3379
  self,
@@ -3273,7 +3497,7 @@ class Device(utils.CompositeEventEmitter):
3273
3497
  periodic_advertising_interval=periodic_advertising_interval,
3274
3498
  advertiser_clock_accuracy=advertiser_clock_accuracy,
3275
3499
  )
3276
- self.emit('periodic_advertising_sync_transfer', pa_sync, connection)
3500
+ self.emit(self.EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER, pa_sync, connection)
3277
3501
 
3278
3502
  @host_event_handler
3279
3503
  @with_periodic_advertising_sync_from_handle
@@ -3331,7 +3555,7 @@ class Device(utils.CompositeEventEmitter):
3331
3555
  @host_event_handler
3332
3556
  def on_inquiry_result(self, address, class_of_device, data, rssi):
3333
3557
  self.emit(
3334
- 'inquiry_result',
3558
+ self.EVENT_INQUIRY_RESULT,
3335
3559
  address,
3336
3560
  class_of_device,
3337
3561
  AdvertisingData.from_bytes(data),
@@ -3508,8 +3732,8 @@ class Device(utils.CompositeEventEmitter):
3508
3732
 
3509
3733
  # Create a future so that we can wait for the connection's result
3510
3734
  pending_connection = asyncio.get_running_loop().create_future()
3511
- self.on('connection', on_connection)
3512
- self.on('connection_failure', on_connection_failure)
3735
+ self.on(self.EVENT_CONNECTION, on_connection)
3736
+ self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3513
3737
 
3514
3738
  try:
3515
3739
  # Tell the controller to connect
@@ -3662,7 +3886,9 @@ class Device(utils.CompositeEventEmitter):
3662
3886
  self.le_connecting = True
3663
3887
 
3664
3888
  if timeout is None:
3665
- return await utils.cancel_on_event(self, 'flush', pending_connection)
3889
+ return await utils.cancel_on_event(
3890
+ self, Device.EVENT_FLUSH, pending_connection
3891
+ )
3666
3892
 
3667
3893
  try:
3668
3894
  return await asyncio.wait_for(
@@ -3680,13 +3906,13 @@ class Device(utils.CompositeEventEmitter):
3680
3906
 
3681
3907
  try:
3682
3908
  return await utils.cancel_on_event(
3683
- self, 'flush', pending_connection
3909
+ self, Device.EVENT_FLUSH, pending_connection
3684
3910
  )
3685
3911
  except core.ConnectionError as error:
3686
3912
  raise core.TimeoutError() from error
3687
3913
  finally:
3688
- self.remove_listener('connection', on_connection)
3689
- self.remove_listener('connection_failure', on_connection_failure)
3914
+ self.remove_listener(self.EVENT_CONNECTION, on_connection)
3915
+ self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3690
3916
  if transport == PhysicalTransport.LE:
3691
3917
  self.le_connecting = False
3692
3918
  self.connect_own_address_type = None
@@ -3737,7 +3963,9 @@ class Device(utils.CompositeEventEmitter):
3737
3963
 
3738
3964
  try:
3739
3965
  # Wait for a request or a completed connection
3740
- pending_request = utils.cancel_on_event(self, 'flush', pending_request_fut)
3966
+ pending_request = utils.cancel_on_event(
3967
+ self, Device.EVENT_FLUSH, pending_request_fut
3968
+ )
3741
3969
  result = await (
3742
3970
  asyncio.wait_for(pending_request, timeout)
3743
3971
  if timeout
@@ -3779,8 +4007,8 @@ class Device(utils.CompositeEventEmitter):
3779
4007
  ):
3780
4008
  pending_connection.set_exception(error)
3781
4009
 
3782
- self.on('connection', on_connection)
3783
- self.on('connection_failure', on_connection_failure)
4010
+ self.on(self.EVENT_CONNECTION, on_connection)
4011
+ self.on(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3784
4012
 
3785
4013
  # Save pending connection, with the Peripheral hci.role.
3786
4014
  # Even if we requested a role switch in the hci.HCI_Accept_Connection_Request
@@ -3799,11 +4027,13 @@ class Device(utils.CompositeEventEmitter):
3799
4027
  )
3800
4028
 
3801
4029
  # Wait for connection complete
3802
- return await utils.cancel_on_event(self, 'flush', pending_connection)
4030
+ return await utils.cancel_on_event(
4031
+ self, Device.EVENT_FLUSH, pending_connection
4032
+ )
3803
4033
 
3804
4034
  finally:
3805
- self.remove_listener('connection', on_connection)
3806
- self.remove_listener('connection_failure', on_connection_failure)
4035
+ self.remove_listener(self.EVENT_CONNECTION, on_connection)
4036
+ self.remove_listener(self.EVENT_CONNECTION_FAILURE, on_connection_failure)
3807
4037
  self.pending_connections.pop(peer_address, None)
3808
4038
 
3809
4039
  @asynccontextmanager
@@ -3857,8 +4087,10 @@ class Device(utils.CompositeEventEmitter):
3857
4087
  ) -> None:
3858
4088
  # Create a future so that we can wait for the disconnection's result
3859
4089
  pending_disconnection = asyncio.get_running_loop().create_future()
3860
- connection.on('disconnection', pending_disconnection.set_result)
3861
- connection.on('disconnection_failure', pending_disconnection.set_exception)
4090
+ connection.on(connection.EVENT_DISCONNECTION, pending_disconnection.set_result)
4091
+ connection.on(
4092
+ connection.EVENT_DISCONNECTION_FAILURE, pending_disconnection.set_exception
4093
+ )
3862
4094
 
3863
4095
  # Request a disconnection
3864
4096
  result = await self.send_command(
@@ -3873,13 +4105,16 @@ class Device(utils.CompositeEventEmitter):
3873
4105
 
3874
4106
  # Wait for the disconnection process to complete
3875
4107
  self.disconnecting = True
3876
- return await utils.cancel_on_event(self, 'flush', pending_disconnection)
4108
+ return await utils.cancel_on_event(
4109
+ self, Device.EVENT_FLUSH, pending_disconnection
4110
+ )
3877
4111
  finally:
3878
4112
  connection.remove_listener(
3879
- 'disconnection', pending_disconnection.set_result
4113
+ connection.EVENT_DISCONNECTION, pending_disconnection.set_result
3880
4114
  )
3881
4115
  connection.remove_listener(
3882
- 'disconnection_failure', pending_disconnection.set_exception
4116
+ connection.EVENT_DISCONNECTION_FAILURE,
4117
+ pending_disconnection.set_exception,
3883
4118
  )
3884
4119
  self.disconnecting = False
3885
4120
 
@@ -3901,20 +4136,39 @@ class Device(utils.CompositeEventEmitter):
3901
4136
 
3902
4137
  async def update_connection_parameters(
3903
4138
  self,
3904
- connection,
3905
- connection_interval_min,
3906
- connection_interval_max,
3907
- max_latency,
3908
- supervision_timeout,
3909
- min_ce_length=0,
3910
- max_ce_length=0,
3911
- use_l2cap=False,
4139
+ connection: Connection,
4140
+ connection_interval_min: float,
4141
+ connection_interval_max: float,
4142
+ max_latency: int,
4143
+ supervision_timeout: float,
4144
+ min_ce_length: float = 0.0,
4145
+ max_ce_length: float = 0.0,
4146
+ use_l2cap: bool = False,
3912
4147
  ) -> None:
3913
4148
  '''
4149
+ Request an update of the connection parameters.
4150
+
4151
+ Args:
4152
+ connection: The connection to update
4153
+ connection_interval_min: Minimum interval, in milliseconds.
4154
+ connection_interval_max: Maximum interval, in milliseconds.
4155
+ max_latency: Latency, in number of intervals.
4156
+ supervision_timeout: Timeout, in milliseconds.
4157
+ min_ce_length: Minimum connection event length, in milliseconds.
4158
+ max_ce_length: Maximum connection event length, in milliseconds.
4159
+ use_l2cap: Request the update via L2CAP.
4160
+
3914
4161
  NOTE: the name of the parameters may look odd, but it just follows the names
3915
4162
  used in the Bluetooth spec.
3916
4163
  '''
3917
4164
 
4165
+ # Convert the input parameters
4166
+ connection_interval_min = int(connection_interval_min / 1.25)
4167
+ connection_interval_max = int(connection_interval_max / 1.25)
4168
+ supervision_timeout = int(supervision_timeout / 10)
4169
+ min_ce_length = int(min_ce_length / 0.625)
4170
+ max_ce_length = int(max_ce_length / 0.625)
4171
+
3918
4172
  if use_l2cap:
3919
4173
  if connection.role != hci.Role.PERIPHERAL:
3920
4174
  raise InvalidStateError(
@@ -3932,6 +4186,8 @@ class Device(utils.CompositeEventEmitter):
3932
4186
  if l2cap_result != l2cap.L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT:
3933
4187
  raise ConnectionParameterUpdateError(l2cap_result)
3934
4188
 
4189
+ return
4190
+
3935
4191
  result = await self.send_command(
3936
4192
  hci.HCI_LE_Connection_Update_Command(
3937
4193
  connection_handle=connection.handle,
@@ -4075,7 +4331,7 @@ class Device(utils.CompositeEventEmitter):
4075
4331
  else:
4076
4332
  return None
4077
4333
 
4078
- return await utils.cancel_on_event(self, 'flush', peer_address)
4334
+ return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address)
4079
4335
  finally:
4080
4336
  if listener is not None:
4081
4337
  self.remove_listener(event_name, listener)
@@ -4125,7 +4381,7 @@ class Device(utils.CompositeEventEmitter):
4125
4381
  if not self.scanning:
4126
4382
  await self.start_scanning(filter_duplicates=True)
4127
4383
 
4128
- return await utils.cancel_on_event(self, 'flush', peer_address)
4384
+ return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address)
4129
4385
  finally:
4130
4386
  if listener is not None:
4131
4387
  self.remove_listener(event_name, listener)
@@ -4144,11 +4400,11 @@ class Device(utils.CompositeEventEmitter):
4144
4400
  self.smp_manager.pairing_config_factory = pairing_config_factory
4145
4401
 
4146
4402
  @property
4147
- def smp_session_proxy(self) -> Type[smp.Session]:
4403
+ def smp_session_proxy(self) -> type[smp.Session]:
4148
4404
  return self.smp_manager.session_proxy
4149
4405
 
4150
4406
  @smp_session_proxy.setter
4151
- def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
4407
+ def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
4152
4408
  self.smp_manager.session_proxy = session_proxy
4153
4409
 
4154
4410
  async def pair(self, connection):
@@ -4201,7 +4457,7 @@ class Device(utils.CompositeEventEmitter):
4201
4457
  return keys.link_key.value
4202
4458
 
4203
4459
  # [Classic only]
4204
- async def authenticate(self, connection):
4460
+ async def authenticate(self, connection: Connection) -> None:
4205
4461
  # Set up event handlers
4206
4462
  pending_authentication = asyncio.get_running_loop().create_future()
4207
4463
 
@@ -4211,8 +4467,11 @@ class Device(utils.CompositeEventEmitter):
4211
4467
  def on_authentication_failure(error_code):
4212
4468
  pending_authentication.set_exception(hci.HCI_Error(error_code))
4213
4469
 
4214
- connection.on('connection_authentication', on_authentication)
4215
- connection.on('connection_authentication_failure', on_authentication_failure)
4470
+ connection.on(connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication)
4471
+ connection.on(
4472
+ connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
4473
+ on_authentication_failure,
4474
+ )
4216
4475
 
4217
4476
  # Request the authentication
4218
4477
  try:
@@ -4229,13 +4488,14 @@ class Device(utils.CompositeEventEmitter):
4229
4488
  raise hci.HCI_StatusError(result)
4230
4489
 
4231
4490
  # Wait for the authentication to complete
4232
- await utils.cancel_on_event(
4233
- connection, 'disconnection', pending_authentication
4234
- )
4491
+ await connection.cancel_on_disconnection(pending_authentication)
4235
4492
  finally:
4236
- connection.remove_listener('connection_authentication', on_authentication)
4237
4493
  connection.remove_listener(
4238
- 'connection_authentication_failure', on_authentication_failure
4494
+ connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
4495
+ )
4496
+ connection.remove_listener(
4497
+ connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE,
4498
+ on_authentication_failure,
4239
4499
  )
4240
4500
 
4241
4501
  async def encrypt(self, connection, enable=True):
@@ -4251,8 +4511,12 @@ class Device(utils.CompositeEventEmitter):
4251
4511
  def on_encryption_failure(error_code):
4252
4512
  pending_encryption.set_exception(hci.HCI_Error(error_code))
4253
4513
 
4254
- connection.on('connection_encryption_change', on_encryption_change)
4255
- connection.on('connection_encryption_failure', on_encryption_failure)
4514
+ connection.on(
4515
+ connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
4516
+ )
4517
+ connection.on(
4518
+ connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
4519
+ )
4256
4520
 
4257
4521
  # Request the encryption
4258
4522
  try:
@@ -4311,13 +4575,13 @@ class Device(utils.CompositeEventEmitter):
4311
4575
  raise hci.HCI_StatusError(result)
4312
4576
 
4313
4577
  # Wait for the result
4314
- await utils.cancel_on_event(connection, 'disconnection', pending_encryption)
4578
+ await connection.cancel_on_disconnection(pending_encryption)
4315
4579
  finally:
4316
4580
  connection.remove_listener(
4317
- 'connection_encryption_change', on_encryption_change
4581
+ connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
4318
4582
  )
4319
4583
  connection.remove_listener(
4320
- 'connection_encryption_failure', on_encryption_failure
4584
+ connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, on_encryption_failure
4321
4585
  )
4322
4586
 
4323
4587
  async def update_keys(self, address: str, keys: PairingKeys) -> None:
@@ -4330,7 +4594,7 @@ class Device(utils.CompositeEventEmitter):
4330
4594
  except Exception as error:
4331
4595
  logger.warning(f'!!! error while storing keys: {error}')
4332
4596
  else:
4333
- self.emit('key_store_update')
4597
+ self.emit(self.EVENT_KEY_STORE_UPDATE)
4334
4598
 
4335
4599
  # [Classic only]
4336
4600
  async def switch_role(self, connection: Connection, role: hci.Role):
@@ -4342,8 +4606,8 @@ class Device(utils.CompositeEventEmitter):
4342
4606
  def on_role_change_failure(error_code):
4343
4607
  pending_role_change.set_exception(hci.HCI_Error(error_code))
4344
4608
 
4345
- connection.on('role_change', on_role_change)
4346
- connection.on('role_change_failure', on_role_change_failure)
4609
+ connection.on(connection.EVENT_ROLE_CHANGE, on_role_change)
4610
+ connection.on(connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure)
4347
4611
 
4348
4612
  try:
4349
4613
  result = await self.send_command(
@@ -4355,12 +4619,12 @@ class Device(utils.CompositeEventEmitter):
4355
4619
  f'{hci.HCI_Constant.error_name(result.status)}'
4356
4620
  )
4357
4621
  raise hci.HCI_StatusError(result)
4358
- await utils.cancel_on_event(
4359
- connection, 'disconnection', pending_role_change
4360
- )
4622
+ await connection.cancel_on_disconnection(pending_role_change)
4361
4623
  finally:
4362
- connection.remove_listener('role_change', on_role_change)
4363
- connection.remove_listener('role_change_failure', on_role_change_failure)
4624
+ connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
4625
+ connection.remove_listener(
4626
+ connection.EVENT_ROLE_CHANGE_FAILURE, on_role_change_failure
4627
+ )
4364
4628
 
4365
4629
  # [Classic only]
4366
4630
  async def request_remote_name(self, remote: Union[hci.Address, Connection]) -> str:
@@ -4372,7 +4636,7 @@ class Device(utils.CompositeEventEmitter):
4372
4636
  )
4373
4637
 
4374
4638
  handler = self.on(
4375
- 'remote_name',
4639
+ self.EVENT_REMOTE_NAME,
4376
4640
  lambda address, remote_name: (
4377
4641
  pending_name.set_result(remote_name)
4378
4642
  if address == peer_address
@@ -4380,7 +4644,7 @@ class Device(utils.CompositeEventEmitter):
4380
4644
  ),
4381
4645
  )
4382
4646
  failure_handler = self.on(
4383
- 'remote_name_failure',
4647
+ self.EVENT_REMOTE_NAME_FAILURE,
4384
4648
  lambda address, error_code: (
4385
4649
  pending_name.set_exception(hci.HCI_Error(error_code))
4386
4650
  if address == peer_address
@@ -4406,57 +4670,48 @@ class Device(utils.CompositeEventEmitter):
4406
4670
  raise hci.HCI_StatusError(result)
4407
4671
 
4408
4672
  # Wait for the result
4409
- return await utils.cancel_on_event(self, 'flush', pending_name)
4673
+ return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name)
4410
4674
  finally:
4411
- self.remove_listener('remote_name', handler)
4412
- self.remove_listener('remote_name_failure', failure_handler)
4675
+ self.remove_listener(self.EVENT_REMOTE_NAME, handler)
4676
+ self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler)
4413
4677
 
4414
4678
  # [LE only]
4415
4679
  @utils.experimental('Only for testing.')
4416
4680
  async def setup_cig(
4417
4681
  self,
4418
- cig_id: int,
4419
- cis_id: Sequence[int],
4420
- sdu_interval: tuple[int, int],
4421
- framing: int,
4422
- max_sdu: tuple[int, int],
4423
- retransmission_number: int,
4424
- max_transport_latency: tuple[int, int],
4682
+ parameters: CigParameters,
4425
4683
  ) -> list[int]:
4426
4684
  """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
4427
4685
 
4428
4686
  Args:
4429
- cig_id: CIG_ID.
4430
- cis_id: CID ID list.
4431
- sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
4432
- framing: Un-framing(0) or Framing(1).
4433
- max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
4434
- retransmission_number: retransmission_number.
4435
- max_transport_latency: Max transport latencies of
4436
- (Central->Peripheral, Peripheral->Cental).
4687
+ parameters: CIG parameters.
4437
4688
 
4438
4689
  Returns:
4439
4690
  List of created CIS handles corresponding to the same order of [cid_id].
4440
4691
  """
4441
- num_cis = len(cis_id)
4692
+ num_cis = len(parameters.cis_parameters)
4442
4693
 
4443
4694
  response = await self.send_command(
4444
4695
  hci.HCI_LE_Set_CIG_Parameters_Command(
4445
- cig_id=cig_id,
4446
- sdu_interval_c_to_p=sdu_interval[0],
4447
- sdu_interval_p_to_c=sdu_interval[1],
4448
- worst_case_sca=0x00, # 251-500 ppm
4449
- packing=0x00, # Sequential
4450
- framing=framing,
4451
- max_transport_latency_c_to_p=max_transport_latency[0],
4452
- max_transport_latency_p_to_c=max_transport_latency[1],
4453
- cis_id=cis_id,
4454
- max_sdu_c_to_p=[max_sdu[0]] * num_cis,
4455
- max_sdu_p_to_c=[max_sdu[1]] * num_cis,
4456
- phy_c_to_p=[hci.HCI_LE_2M_PHY] * num_cis,
4457
- phy_p_to_c=[hci.HCI_LE_2M_PHY] * num_cis,
4458
- rtn_c_to_p=[retransmission_number] * num_cis,
4459
- rtn_p_to_c=[retransmission_number] * num_cis,
4696
+ cig_id=parameters.cig_id,
4697
+ sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
4698
+ sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
4699
+ worst_case_sca=parameters.worst_case_sca,
4700
+ packing=int(parameters.packing),
4701
+ framing=int(parameters.framing),
4702
+ max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
4703
+ max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
4704
+ cis_id=[cis.cis_id for cis in parameters.cis_parameters],
4705
+ max_sdu_c_to_p=[
4706
+ cis.max_sdu_c_to_p for cis in parameters.cis_parameters
4707
+ ],
4708
+ max_sdu_p_to_c=[
4709
+ cis.max_sdu_p_to_c for cis in parameters.cis_parameters
4710
+ ],
4711
+ phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
4712
+ phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
4713
+ rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
4714
+ rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
4460
4715
  ),
4461
4716
  check_result=True,
4462
4717
  )
@@ -4464,19 +4719,17 @@ class Device(utils.CompositeEventEmitter):
4464
4719
  # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
4465
4720
  # Server, so here it only provides a basic functionality for testing.
4466
4721
  cis_handles = response.return_parameters.connection_handle[:]
4467
- for id, cis_handle in zip(cis_id, cis_handles):
4468
- self._pending_cis[cis_handle] = (id, cig_id)
4722
+ for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
4723
+ self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
4469
4724
 
4470
4725
  return cis_handles
4471
4726
 
4472
4727
  # [LE only]
4473
4728
  @utils.experimental('Only for testing.')
4474
4729
  async def create_cis(
4475
- self, cis_acl_pairs: Sequence[tuple[int, int]]
4730
+ self, cis_acl_pairs: Sequence[tuple[int, Connection]]
4476
4731
  ) -> list[CisLink]:
4477
- for cis_handle, acl_handle in cis_acl_pairs:
4478
- acl_connection = self.lookup_connection(acl_handle)
4479
- assert acl_connection
4732
+ for cis_handle, acl_connection in cis_acl_pairs:
4480
4733
  cis_id, cig_id = self._pending_cis.pop(cis_handle)
4481
4734
  self.cis_links[cis_handle] = CisLink(
4482
4735
  device=self,
@@ -4496,16 +4749,18 @@ class Device(utils.CompositeEventEmitter):
4496
4749
  if pending_future := pending_cis_establishments.get(cis_link.handle):
4497
4750
  pending_future.set_result(cis_link)
4498
4751
 
4499
- def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
4500
- if pending_future := pending_cis_establishments.get(cis_handle):
4752
+ def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
4753
+ if pending_future := pending_cis_establishments.get(cis_link.handle):
4501
4754
  pending_future.set_exception(hci.HCI_Error(status))
4502
4755
 
4503
- watcher.on(self, 'cis_establishment', on_cis_establishment)
4504
- watcher.on(self, 'cis_establishment_failure', on_cis_establishment_failure)
4756
+ watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
4757
+ watcher.on(
4758
+ self, self.EVENT_CIS_ESTABLISHMENT_FAILURE, on_cis_establishment_failure
4759
+ )
4505
4760
  await self.send_command(
4506
4761
  hci.HCI_LE_Create_CIS_Command(
4507
4762
  cis_connection_handle=[p[0] for p in cis_acl_pairs],
4508
- acl_connection_handle=[p[1] for p in cis_acl_pairs],
4763
+ acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
4509
4764
  ),
4510
4765
  check_result=True,
4511
4766
  )
@@ -4514,26 +4769,21 @@ class Device(utils.CompositeEventEmitter):
4514
4769
 
4515
4770
  # [LE only]
4516
4771
  @utils.experimental('Only for testing.')
4517
- async def accept_cis_request(self, handle: int) -> CisLink:
4772
+ async def accept_cis_request(self, cis_link: CisLink) -> None:
4518
4773
  """[LE Only] Accepts an incoming CIS request.
4519
4774
 
4520
- When the specified CIS handle is already created, this method returns the
4521
- existed CIS link object immediately.
4775
+ This method returns when the CIS is established, or raises an exception if
4776
+ the CIS establishment fails.
4522
4777
 
4523
4778
  Args:
4524
4779
  handle: CIS handle to accept.
4525
-
4526
- Returns:
4527
- CIS link object on the given handle.
4528
4780
  """
4529
- if not (cis_link := self.cis_links.get(handle)):
4530
- raise InvalidStateError(f'No pending CIS request of handle {handle}')
4531
4781
 
4532
4782
  # There might be multiple ASE sharing a CIS channel.
4533
4783
  # If one of them has accepted the request, the others should just leverage it.
4534
4784
  async with self._cis_lock:
4535
4785
  if cis_link.state == CisLink.State.ESTABLISHED:
4536
- return cis_link
4786
+ return
4537
4787
 
4538
4788
  with closing(utils.EventWatcher()) as watcher:
4539
4789
  pending_establishment = asyncio.get_running_loop().create_future()
@@ -4544,30 +4794,32 @@ class Device(utils.CompositeEventEmitter):
4544
4794
  def on_establishment_failure(status: int) -> None:
4545
4795
  pending_establishment.set_exception(hci.HCI_Error(status))
4546
4796
 
4547
- watcher.on(cis_link, 'establishment', on_establishment)
4548
- watcher.on(cis_link, 'establishment_failure', on_establishment_failure)
4797
+ watcher.on(cis_link, cis_link.EVENT_ESTABLISHMENT, on_establishment)
4798
+ watcher.on(
4799
+ cis_link,
4800
+ cis_link.EVENT_ESTABLISHMENT_FAILURE,
4801
+ on_establishment_failure,
4802
+ )
4549
4803
 
4550
4804
  await self.send_command(
4551
- hci.HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
4805
+ hci.HCI_LE_Accept_CIS_Request_Command(
4806
+ connection_handle=cis_link.handle
4807
+ ),
4552
4808
  check_result=True,
4553
4809
  )
4554
4810
 
4555
4811
  await pending_establishment
4556
- return cis_link
4557
-
4558
- # Mypy believes this is reachable when context is an ExitStack.
4559
- raise UnreachableError()
4560
4812
 
4561
4813
  # [LE only]
4562
4814
  @utils.experimental('Only for testing.')
4563
4815
  async def reject_cis_request(
4564
4816
  self,
4565
- handle: int,
4817
+ cis_link: CisLink,
4566
4818
  reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
4567
4819
  ) -> None:
4568
4820
  await self.send_command(
4569
4821
  hci.HCI_LE_Reject_CIS_Request_Command(
4570
- connection_handle=handle, reason=reason
4822
+ connection_handle=cis_link.handle, reason=reason
4571
4823
  ),
4572
4824
  check_result=True,
4573
4825
  )
@@ -4913,33 +5165,33 @@ class Device(utils.CompositeEventEmitter):
4913
5165
 
4914
5166
  @host_event_handler
4915
5167
  def on_flush(self):
4916
- self.emit('flush')
5168
+ self.emit(self.EVENT_FLUSH)
4917
5169
  for _, connection in self.connections.items():
4918
- connection.emit('disconnection', 0)
5170
+ connection.emit(connection.EVENT_DISCONNECTION, 0)
4919
5171
  self.connections = {}
4920
5172
 
4921
5173
  # [Classic only]
4922
5174
  @host_event_handler
4923
- def on_link_key(self, bd_addr, link_key, key_type):
5175
+ def on_link_key(self, bd_addr: hci.Address, link_key: bytes, key_type: int) -> None:
4924
5176
  # Store the keys in the key store
4925
5177
  if self.keystore:
4926
5178
  authenticated = key_type in (
4927
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
4928
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
5179
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
5180
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
4929
5181
  )
4930
- pairing_keys = PairingKeys()
4931
- pairing_keys.link_key = PairingKeys.Key(
4932
- value=link_key, authenticated=authenticated
5182
+ pairing_keys = PairingKeys(
5183
+ link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
5184
+ link_key_type=key_type,
4933
5185
  )
4934
5186
 
4935
5187
  utils.cancel_on_event(
4936
- self, 'flush', self.update_keys(str(bd_addr), pairing_keys)
5188
+ self, Device.EVENT_FLUSH, self.update_keys(str(bd_addr), pairing_keys)
4937
5189
  )
4938
5190
 
4939
5191
  if connection := self.find_connection_by_bd_addr(
4940
5192
  bd_addr, transport=PhysicalTransport.BR_EDR
4941
5193
  ):
4942
- connection.link_key_type = key_type
5194
+ connection.emit(connection.EVENT_LINK_KEY)
4943
5195
 
4944
5196
  def add_service(self, service):
4945
5197
  self.gatt_server.add_service(service)
@@ -5105,13 +5357,13 @@ class Device(utils.CompositeEventEmitter):
5105
5357
  big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
5106
5358
  big.big_sync_delay = big_sync_delay
5107
5359
  big.transport_latency_big = transport_latency_big
5108
- big.phy = phy
5360
+ big.phy = hci.Phy(phy)
5109
5361
  big.nse = nse
5110
5362
  big.bn = bn
5111
5363
  big.pto = pto
5112
5364
  big.irc = irc
5113
5365
  big.max_pdu = max_pdu
5114
- big.iso_interval = iso_interval
5366
+ big.iso_interval = iso_interval * 1.25
5115
5367
  big.state = Big.State.ACTIVE
5116
5368
 
5117
5369
  for bis_link in big.bis_links:
@@ -5160,7 +5412,7 @@ class Device(utils.CompositeEventEmitter):
5160
5412
  big_sync.pto = pto
5161
5413
  big_sync.irc = irc
5162
5414
  big_sync.max_pdu = max_pdu
5163
- big_sync.iso_interval = iso_interval
5415
+ big_sync.iso_interval = iso_interval * 1.25
5164
5416
  big_sync.bis_links = [
5165
5417
  BisLink(handle=handle, big=big_sync) for handle in bis_handles
5166
5418
  ]
@@ -5202,11 +5454,13 @@ class Device(utils.CompositeEventEmitter):
5202
5454
  # Setup auto-restart of the advertising set if needed.
5203
5455
  if advertising_set.auto_restart:
5204
5456
  connection.once(
5205
- 'disconnection',
5206
- lambda _: utils.cancel_on_event(self, 'flush', advertising_set.start()),
5457
+ Connection.EVENT_DISCONNECTION,
5458
+ lambda _: utils.cancel_on_event(
5459
+ self, Device.EVENT_FLUSH, advertising_set.start()
5460
+ ),
5207
5461
  )
5208
5462
 
5209
- self.emit('connection', connection)
5463
+ self.emit(self.EVENT_CONNECTION, connection)
5210
5464
 
5211
5465
  @host_event_handler
5212
5466
  def on_connection(
@@ -5217,7 +5471,7 @@ class Device(utils.CompositeEventEmitter):
5217
5471
  self_resolvable_address: Optional[hci.Address],
5218
5472
  peer_resolvable_address: Optional[hci.Address],
5219
5473
  role: hci.Role,
5220
- connection_parameters: ConnectionParameters,
5474
+ connection_parameters: Optional[core.ConnectionParameters],
5221
5475
  ) -> None:
5222
5476
  # Convert all-zeros addresses into None.
5223
5477
  if self_resolvable_address == hci.Address.ANY_RANDOM:
@@ -5244,10 +5498,12 @@ class Device(utils.CompositeEventEmitter):
5244
5498
  self.connections[connection_handle] = connection
5245
5499
 
5246
5500
  # Emit an event to notify listeners of the new connection
5247
- self.emit('connection', connection)
5501
+ self.emit(self.EVENT_CONNECTION, connection)
5248
5502
 
5249
5503
  return
5250
5504
 
5505
+ assert connection_parameters is not None
5506
+
5251
5507
  if peer_resolvable_address is None:
5252
5508
  # Resolve the peer address if we can
5253
5509
  if self.address_resolver:
@@ -5303,7 +5559,11 @@ class Device(utils.CompositeEventEmitter):
5303
5559
  peer_address,
5304
5560
  peer_resolvable_address,
5305
5561
  role,
5306
- connection_parameters,
5562
+ Connection.Parameters(
5563
+ connection_parameters.connection_interval * 1.25,
5564
+ connection_parameters.peripheral_latency,
5565
+ connection_parameters.supervision_timeout * 10.0,
5566
+ ),
5307
5567
  )
5308
5568
  self.connections[connection_handle] = connection
5309
5569
 
@@ -5311,15 +5571,17 @@ class Device(utils.CompositeEventEmitter):
5311
5571
  if self.legacy_advertiser.auto_restart:
5312
5572
  advertiser = self.legacy_advertiser
5313
5573
  connection.once(
5314
- 'disconnection',
5315
- lambda _: utils.cancel_on_event(self, 'flush', advertiser.start()),
5574
+ Connection.EVENT_DISCONNECTION,
5575
+ lambda _: utils.cancel_on_event(
5576
+ self, Device.EVENT_FLUSH, advertiser.start()
5577
+ ),
5316
5578
  )
5317
5579
  else:
5318
5580
  self.legacy_advertiser = None
5319
5581
 
5320
5582
  if role == hci.Role.CENTRAL or not self.supports_le_extended_advertising:
5321
5583
  # We can emit now, we have all the info we need
5322
- self.emit('connection', connection)
5584
+ self.emit(self.EVENT_CONNECTION, connection)
5323
5585
  return
5324
5586
 
5325
5587
  if role == hci.Role.PERIPHERAL and self.supports_le_extended_advertising:
@@ -5353,7 +5615,7 @@ class Device(utils.CompositeEventEmitter):
5353
5615
  'hci',
5354
5616
  hci.HCI_Constant.error_name(error_code),
5355
5617
  )
5356
- self.emit('connection_failure', error)
5618
+ self.emit(self.EVENT_CONNECTION_FAILURE, error)
5357
5619
 
5358
5620
  # FIXME: Explore a delegate-model for BR/EDR wait connection #56.
5359
5621
  @host_event_handler
@@ -5362,13 +5624,13 @@ class Device(utils.CompositeEventEmitter):
5362
5624
 
5363
5625
  # Handle SCO request.
5364
5626
  if link_type in (
5365
- hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE,
5366
- hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
5627
+ hci.HCI_Connection_Complete_Event.LinkType.SCO,
5628
+ hci.HCI_Connection_Complete_Event.LinkType.ESCO,
5367
5629
  ):
5368
5630
  if connection := self.find_connection_by_bd_addr(
5369
5631
  bd_addr, transport=PhysicalTransport.BR_EDR
5370
5632
  ):
5371
- self.emit('sco_request', connection, link_type)
5633
+ self.emit(self.EVENT_SCO_REQUEST, connection, link_type)
5372
5634
  else:
5373
5635
  logger.error(f'SCO request from a non-connected device {bd_addr}')
5374
5636
  return
@@ -5412,14 +5674,14 @@ class Device(utils.CompositeEventEmitter):
5412
5674
  f'*** Disconnection: [0x{connection.handle:04X}] '
5413
5675
  f'{connection.peer_address} as {connection.role_name}, reason={reason}'
5414
5676
  )
5415
- connection.emit('disconnection', reason)
5677
+ connection.emit(connection.EVENT_DISCONNECTION, reason)
5416
5678
 
5417
5679
  # Cleanup subsystems that maintain per-connection state
5418
5680
  self.gatt_server.on_disconnection(connection)
5419
5681
  elif sco_link := self.sco_links.pop(connection_handle, None):
5420
- sco_link.emit('disconnection', reason)
5682
+ sco_link.emit(sco_link.EVENT_DISCONNECTION, reason)
5421
5683
  elif cis_link := self.cis_links.pop(connection_handle, None):
5422
- cis_link.emit('disconnection', reason)
5684
+ cis_link.emit(cis_link.EVENT_DISCONNECTION, reason)
5423
5685
  else:
5424
5686
  logger.error(
5425
5687
  f'*** Unknown disconnection handle=0x{connection_handle}, reason={reason} ***'
@@ -5427,7 +5689,7 @@ class Device(utils.CompositeEventEmitter):
5427
5689
 
5428
5690
  @host_event_handler
5429
5691
  @with_connection_from_handle
5430
- def on_disconnection_failure(self, connection, error_code):
5692
+ def on_disconnection_failure(self, connection: Connection, error_code: int):
5431
5693
  logger.debug(f'*** Disconnection failed: {error_code}')
5432
5694
  error = core.ConnectionError(
5433
5695
  error_code,
@@ -5436,7 +5698,7 @@ class Device(utils.CompositeEventEmitter):
5436
5698
  'hci',
5437
5699
  hci.HCI_Constant.error_name(error_code),
5438
5700
  )
5439
- connection.emit('disconnection_failure', error)
5701
+ connection.emit(connection.EVENT_DISCONNECTION_FAILURE, error)
5440
5702
 
5441
5703
  @host_event_handler
5442
5704
  @utils.AsyncRunner.run_in_task()
@@ -5447,7 +5709,7 @@ class Device(utils.CompositeEventEmitter):
5447
5709
  else:
5448
5710
  self.auto_restart_inquiry = True
5449
5711
  self.discovering = False
5450
- self.emit('inquiry_complete')
5712
+ self.emit(self.EVENT_INQUIRY_COMPLETE)
5451
5713
 
5452
5714
  @host_event_handler
5453
5715
  @with_connection_from_handle
@@ -5457,7 +5719,7 @@ class Device(utils.CompositeEventEmitter):
5457
5719
  f'{connection.peer_address} as {connection.role_name}'
5458
5720
  )
5459
5721
  connection.authenticated = True
5460
- connection.emit('connection_authentication')
5722
+ connection.emit(connection.EVENT_CONNECTION_AUTHENTICATION)
5461
5723
 
5462
5724
  @host_event_handler
5463
5725
  @with_connection_from_handle
@@ -5466,12 +5728,12 @@ class Device(utils.CompositeEventEmitter):
5466
5728
  f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
5467
5729
  f'{connection.peer_address} as {connection.role_name}, error={error}'
5468
5730
  )
5469
- connection.emit('connection_authentication_failure', error)
5731
+ connection.emit(connection.EVENT_CONNECTION_AUTHENTICATION_FAILURE, error)
5470
5732
 
5471
5733
  # [Classic only]
5472
5734
  @host_event_handler
5473
5735
  @with_connection_from_address
5474
- def on_authentication_io_capability_request(self, connection):
5736
+ def on_authentication_io_capability_request(self, connection: Connection):
5475
5737
  # Ask what the pairing config should be for this connection
5476
5738
  pairing_config = self.pairing_config_factory(connection)
5477
5739
 
@@ -5479,13 +5741,13 @@ class Device(utils.CompositeEventEmitter):
5479
5741
  authentication_requirements = (
5480
5742
  # No Bonding
5481
5743
  (
5482
- hci.HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5483
- hci.HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
5744
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
5745
+ hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
5484
5746
  ),
5485
5747
  # General Bonding
5486
5748
  (
5487
- hci.HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5488
- hci.HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
5749
+ hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
5750
+ hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
5489
5751
  ),
5490
5752
  )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
5491
5753
 
@@ -5540,30 +5802,30 @@ class Device(utils.CompositeEventEmitter):
5540
5802
  raise UnreachableError()
5541
5803
 
5542
5804
  # See Bluetooth spec @ Vol 3, Part C 5.2.2.6
5543
- methods = {
5544
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: {
5545
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
5546
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
5547
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5548
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5805
+ methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
5806
+ hci.IoCapability.DISPLAY_ONLY: {
5807
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5808
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5809
+ hci.IoCapability.KEYBOARD_ONLY: na,
5810
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5549
5811
  },
5550
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: {
5551
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
5552
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
5553
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5554
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5812
+ hci.IoCapability.DISPLAY_YES_NO: {
5813
+ hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
5814
+ hci.IoCapability.DISPLAY_YES_NO: display_confirm,
5815
+ hci.IoCapability.KEYBOARD_ONLY: na,
5816
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5555
5817
  },
5556
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: {
5557
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: na,
5558
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: na,
5559
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
5560
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5818
+ hci.IoCapability.KEYBOARD_ONLY: {
5819
+ hci.IoCapability.DISPLAY_ONLY: na,
5820
+ hci.IoCapability.DISPLAY_YES_NO: na,
5821
+ hci.IoCapability.KEYBOARD_ONLY: na,
5822
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5561
5823
  },
5562
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
5563
- hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: confirm,
5564
- hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: confirm,
5565
- hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: auto_confirm,
5566
- hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
5824
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: {
5825
+ hci.IoCapability.DISPLAY_ONLY: confirm,
5826
+ hci.IoCapability.DISPLAY_YES_NO: confirm,
5827
+ hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
5828
+ hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
5567
5829
  },
5568
5830
  }
5569
5831
 
@@ -5571,7 +5833,7 @@ class Device(utils.CompositeEventEmitter):
5571
5833
 
5572
5834
  async def reply() -> None:
5573
5835
  try:
5574
- if await utils.cancel_on_event(connection, 'disconnection', method()):
5836
+ if await connection.cancel_on_disconnection(method()):
5575
5837
  await self.host.send_command(
5576
5838
  hci.HCI_User_Confirmation_Request_Reply_Command(
5577
5839
  bd_addr=connection.peer_address
@@ -5598,8 +5860,8 @@ class Device(utils.CompositeEventEmitter):
5598
5860
 
5599
5861
  async def reply() -> None:
5600
5862
  try:
5601
- number = await utils.cancel_on_event(
5602
- connection, 'disconnection', pairing_config.delegate.get_number()
5863
+ number = await connection.cancel_on_disconnection(
5864
+ pairing_config.delegate.get_number()
5603
5865
  )
5604
5866
  if number is not None:
5605
5867
  await self.host.send_command(
@@ -5619,6 +5881,19 @@ class Device(utils.CompositeEventEmitter):
5619
5881
 
5620
5882
  utils.AsyncRunner.spawn(reply())
5621
5883
 
5884
+ # [Classic only]
5885
+ @host_event_handler
5886
+ @with_connection_from_handle
5887
+ def on_mode_change(
5888
+ self, connection: Connection, status: int, current_mode: int, interval: int
5889
+ ):
5890
+ if status == hci.HCI_SUCCESS:
5891
+ connection.classic_mode = current_mode
5892
+ connection.classic_interval = interval
5893
+ connection.emit(connection.EVENT_MODE_CHANGE)
5894
+ else:
5895
+ connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
5896
+
5622
5897
  # [Classic only]
5623
5898
  @host_event_handler
5624
5899
  @with_connection_from_address
@@ -5629,11 +5904,11 @@ class Device(utils.CompositeEventEmitter):
5629
5904
  io_capability = pairing_config.delegate.classic_io_capability
5630
5905
 
5631
5906
  # Respond
5632
- if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
5907
+ if io_capability == hci.IoCapability.KEYBOARD_ONLY:
5633
5908
  # Ask the user to enter a string
5634
5909
  async def get_pin_code():
5635
- pin_code = await utils.cancel_on_event(
5636
- connection, 'disconnection', pairing_config.delegate.get_string(16)
5910
+ pin_code = await connection.cancel_on_disconnection(
5911
+ pairing_config.delegate.get_string(16)
5637
5912
  )
5638
5913
 
5639
5914
  if pin_code is not None:
@@ -5671,8 +5946,8 @@ class Device(utils.CompositeEventEmitter):
5671
5946
  pairing_config = self.pairing_config_factory(connection)
5672
5947
 
5673
5948
  # Show the passkey to the user
5674
- utils.cancel_on_event(
5675
- connection, 'disconnection', pairing_config.delegate.display_number(passkey)
5949
+ connection.cancel_on_disconnection(
5950
+ pairing_config.delegate.display_number(passkey, digits=6)
5676
5951
  )
5677
5952
 
5678
5953
  # [Classic only]
@@ -5684,22 +5959,22 @@ class Device(utils.CompositeEventEmitter):
5684
5959
  remote_name = remote_name.decode('utf-8')
5685
5960
  if connection:
5686
5961
  connection.peer_name = remote_name
5687
- connection.emit('remote_name')
5688
- self.emit('remote_name', address, remote_name)
5962
+ connection.emit(connection.EVENT_REMOTE_NAME)
5963
+ self.emit(self.EVENT_REMOTE_NAME, address, remote_name)
5689
5964
  except UnicodeDecodeError as error:
5690
5965
  logger.warning('peer name is not valid UTF-8')
5691
5966
  if connection:
5692
- connection.emit('remote_name_failure', error)
5967
+ connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
5693
5968
  else:
5694
- self.emit('remote_name_failure', address, error)
5969
+ self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
5695
5970
 
5696
5971
  # [Classic only]
5697
5972
  @host_event_handler
5698
5973
  @try_with_connection_from_address
5699
5974
  def on_remote_name_failure(self, connection: Connection, address, error):
5700
5975
  if connection:
5701
- connection.emit('remote_name_failure', error)
5702
- self.emit('remote_name_failure', address, error)
5976
+ connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
5977
+ self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
5703
5978
 
5704
5979
  # [Classic only]
5705
5980
  @host_event_handler
@@ -5719,7 +5994,7 @@ class Device(utils.CompositeEventEmitter):
5719
5994
  handle=sco_handle,
5720
5995
  link_type=link_type,
5721
5996
  )
5722
- self.emit('sco_connection', sco_link)
5997
+ self.emit(self.EVENT_SCO_CONNECTION, sco_link)
5723
5998
 
5724
5999
  # [Classic only]
5725
6000
  @host_event_handler
@@ -5729,7 +6004,7 @@ class Device(utils.CompositeEventEmitter):
5729
6004
  self, acl_connection: Connection, status: int
5730
6005
  ) -> None:
5731
6006
  logger.debug(f'*** SCO connection failure: {acl_connection.peer_address}***')
5732
- self.emit('sco_connection_failure')
6007
+ self.emit(self.EVENT_SCO_CONNECTION_FAILURE)
5733
6008
 
5734
6009
  # [Classic only]
5735
6010
  @host_event_handler
@@ -5759,24 +6034,63 @@ class Device(utils.CompositeEventEmitter):
5759
6034
  f'cis_id=[0x{cis_id:02X}] ***'
5760
6035
  )
5761
6036
  # LE_CIS_Established event doesn't provide info, so we must store them here.
5762
- self.cis_links[cis_handle] = CisLink(
6037
+ cis_link = CisLink(
5763
6038
  device=self,
5764
6039
  acl_connection=acl_connection,
5765
6040
  handle=cis_handle,
5766
6041
  cig_id=cig_id,
5767
6042
  cis_id=cis_id,
5768
6043
  )
5769
- self.emit('cis_request', acl_connection, cis_handle, cig_id, cis_id)
6044
+ self.cis_links[cis_handle] = cis_link
6045
+ acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
6046
+ self.emit(self.EVENT_CIS_REQUEST, cis_link)
5770
6047
 
5771
6048
  # [LE only]
5772
6049
  @host_event_handler
5773
6050
  @utils.experimental('Only for testing')
5774
- def on_cis_establishment(self, cis_handle: int) -> None:
6051
+ def on_cis_establishment(
6052
+ self,
6053
+ cis_handle: int,
6054
+ cig_sync_delay: int,
6055
+ cis_sync_delay: int,
6056
+ transport_latency_c_to_p: int,
6057
+ transport_latency_p_to_c: int,
6058
+ phy_c_to_p: int,
6059
+ phy_p_to_c: int,
6060
+ nse: int,
6061
+ bn_c_to_p: int,
6062
+ bn_p_to_c: int,
6063
+ ft_c_to_p: int,
6064
+ ft_p_to_c: int,
6065
+ max_pdu_c_to_p: int,
6066
+ max_pdu_p_to_c: int,
6067
+ iso_interval: int,
6068
+ ) -> None:
6069
+ if cis_handle not in self.cis_links:
6070
+ logger.warning("CIS link not found")
6071
+ return
6072
+
5775
6073
  cis_link = self.cis_links[cis_handle]
5776
6074
  cis_link.state = CisLink.State.ESTABLISHED
5777
6075
 
5778
6076
  assert cis_link.acl_connection
5779
6077
 
6078
+ # Update the CIS
6079
+ cis_link.cig_sync_delay = cig_sync_delay
6080
+ cis_link.cis_sync_delay = cis_sync_delay
6081
+ cis_link.transport_latency_c_to_p = transport_latency_c_to_p
6082
+ cis_link.transport_latency_p_to_c = transport_latency_p_to_c
6083
+ cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
6084
+ cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
6085
+ cis_link.nse = nse
6086
+ cis_link.bn_c_to_p = bn_c_to_p
6087
+ cis_link.bn_p_to_c = bn_p_to_c
6088
+ cis_link.ft_c_to_p = ft_c_to_p
6089
+ cis_link.ft_p_to_c = ft_p_to_c
6090
+ cis_link.max_pdu_c_to_p = max_pdu_c_to_p
6091
+ cis_link.max_pdu_p_to_c = max_pdu_p_to_c
6092
+ cis_link.iso_interval = iso_interval * 1.25
6093
+
5780
6094
  logger.debug(
5781
6095
  f'*** CIS Establishment '
5782
6096
  f'{cis_link.acl_connection.peer_address}, '
@@ -5785,17 +6099,28 @@ class Device(utils.CompositeEventEmitter):
5785
6099
  f'cis_id=[0x{cis_link.cis_id:02X}] ***'
5786
6100
  )
5787
6101
 
5788
- cis_link.emit('establishment')
5789
- self.emit('cis_establishment', cis_link)
6102
+ cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
6103
+ cis_link.acl_connection.emit(
6104
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
6105
+ )
6106
+ self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
5790
6107
 
5791
6108
  # [LE only]
5792
6109
  @host_event_handler
5793
6110
  @utils.experimental('Only for testing')
5794
6111
  def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
6112
+ if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
6113
+ logger.warning("CIS link not found")
6114
+ return
6115
+
5795
6116
  logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
5796
- if cis_link := self.cis_links.pop(cis_handle):
5797
- cis_link.emit('establishment_failure', status)
5798
- self.emit('cis_establishment_failure', cis_handle, status)
6117
+ cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
6118
+ cis_link.acl_connection.emit(
6119
+ cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
6120
+ cis_link,
6121
+ status,
6122
+ )
6123
+ self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
5799
6124
 
5800
6125
  # [LE only]
5801
6126
  @host_event_handler
@@ -5808,28 +6133,32 @@ class Device(utils.CompositeEventEmitter):
5808
6133
 
5809
6134
  @host_event_handler
5810
6135
  @with_connection_from_handle
5811
- def on_connection_encryption_change(self, connection, encryption):
6136
+ def on_connection_encryption_change(
6137
+ self, connection: Connection, encryption: int, encryption_key_size: int
6138
+ ):
5812
6139
  logger.debug(
5813
6140
  f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
5814
6141
  f'{connection.peer_address} as {connection.role_name}, '
5815
- f'encryption={encryption}'
6142
+ f'encryption={encryption}, '
6143
+ f'key_size={encryption_key_size}'
5816
6144
  )
5817
6145
  connection.encryption = encryption
6146
+ connection.encryption_key_size = encryption_key_size
5818
6147
  if (
5819
6148
  not connection.authenticated
5820
6149
  and connection.transport == PhysicalTransport.BR_EDR
5821
- and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
6150
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
5822
6151
  ):
5823
6152
  connection.authenticated = True
5824
6153
  connection.sc = True
5825
6154
  if (
5826
6155
  not connection.authenticated
5827
6156
  and connection.transport == PhysicalTransport.LE
5828
- and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
6157
+ and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
5829
6158
  ):
5830
6159
  connection.authenticated = True
5831
6160
  connection.sc = True
5832
- connection.emit('connection_encryption_change')
6161
+ connection.emit(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
5833
6162
 
5834
6163
  @host_event_handler
5835
6164
  @with_connection_from_handle
@@ -5839,7 +6168,7 @@ class Device(utils.CompositeEventEmitter):
5839
6168
  f'{connection.peer_address} as {connection.role_name}, '
5840
6169
  f'error={error}'
5841
6170
  )
5842
- connection.emit('connection_encryption_failure', error)
6171
+ connection.emit(connection.EVENT_CONNECTION_ENCRYPTION_FAILURE, error)
5843
6172
 
5844
6173
  @host_event_handler
5845
6174
  @with_connection_from_handle
@@ -5848,18 +6177,24 @@ class Device(utils.CompositeEventEmitter):
5848
6177
  f'*** Connection Key Refresh: [0x{connection.handle:04X}] '
5849
6178
  f'{connection.peer_address} as {connection.role_name}'
5850
6179
  )
5851
- connection.emit('connection_encryption_key_refresh')
6180
+ connection.emit(connection.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH)
5852
6181
 
5853
6182
  @host_event_handler
5854
6183
  @with_connection_from_handle
5855
- def on_connection_parameters_update(self, connection, connection_parameters):
6184
+ def on_connection_parameters_update(
6185
+ self, connection: Connection, connection_parameters: core.ConnectionParameters
6186
+ ):
5856
6187
  logger.debug(
5857
6188
  f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
5858
6189
  f'{connection.peer_address} as {connection.role_name}, '
5859
6190
  f'{connection_parameters}'
5860
6191
  )
5861
- connection.parameters = connection_parameters
5862
- connection.emit('connection_parameters_update')
6192
+ connection.parameters = Connection.Parameters(
6193
+ connection_parameters.connection_interval * 1.25,
6194
+ connection_parameters.peripheral_latency,
6195
+ connection_parameters.supervision_timeout * 10.0,
6196
+ )
6197
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
5863
6198
 
5864
6199
  @host_event_handler
5865
6200
  @with_connection_from_handle
@@ -5869,7 +6204,7 @@ class Device(utils.CompositeEventEmitter):
5869
6204
  f'{connection.peer_address} as {connection.role_name}, '
5870
6205
  f'error={error}'
5871
6206
  )
5872
- connection.emit('connection_parameters_update_failure', error)
6207
+ connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE_FAILURE, error)
5873
6208
 
5874
6209
  @host_event_handler
5875
6210
  @with_connection_from_handle
@@ -5879,7 +6214,7 @@ class Device(utils.CompositeEventEmitter):
5879
6214
  f'{connection.peer_address} as {connection.role_name}, '
5880
6215
  f'{phy}'
5881
6216
  )
5882
- connection.emit('connection_phy_update', phy)
6217
+ connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE, phy)
5883
6218
 
5884
6219
  @host_event_handler
5885
6220
  @with_connection_from_handle
@@ -5889,7 +6224,7 @@ class Device(utils.CompositeEventEmitter):
5889
6224
  f'{connection.peer_address} as {connection.role_name}, '
5890
6225
  f'error={error}'
5891
6226
  )
5892
- connection.emit('connection_phy_update_failure', error)
6227
+ connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error)
5893
6228
 
5894
6229
  @host_event_handler
5895
6230
  @with_connection_from_handle
@@ -5900,7 +6235,7 @@ class Device(utils.CompositeEventEmitter):
5900
6235
  f'{att_mtu}'
5901
6236
  )
5902
6237
  connection.att_mtu = att_mtu
5903
- connection.emit('connection_att_mtu_update')
6238
+ connection.emit(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
5904
6239
 
5905
6240
  @host_event_handler
5906
6241
  @with_connection_from_handle
@@ -5917,7 +6252,7 @@ class Device(utils.CompositeEventEmitter):
5917
6252
  max_rx_octets,
5918
6253
  max_rx_time,
5919
6254
  )
5920
- connection.emit('connection_data_length_change')
6255
+ connection.emit(connection.EVENT_CONNECTION_DATA_LENGTH_CHANGE)
5921
6256
 
5922
6257
  @host_event_handler
5923
6258
  def on_cs_remote_supported_capabilities(
@@ -5927,7 +6262,9 @@ class Device(utils.CompositeEventEmitter):
5927
6262
  return
5928
6263
 
5929
6264
  if event.status != hci.HCI_SUCCESS:
5930
- connection.emit('channel_sounding_capabilities_failure', event.status)
6265
+ connection.emit(
6266
+ connection.EVENT_CHANNEL_SOUNDING_CAPABILITIES_FAILURE, event.status
6267
+ )
5931
6268
  return
5932
6269
 
5933
6270
  capabilities = ChannelSoundingCapabilities(
@@ -5952,7 +6289,7 @@ class Device(utils.CompositeEventEmitter):
5952
6289
  t_sw_time_supported=event.t_sw_time_supported,
5953
6290
  tx_snr_capability=event.tx_snr_capability,
5954
6291
  )
5955
- connection.emit('channel_sounding_capabilities', capabilities)
6292
+ connection.emit(connection.EVENT_CHANNEL_SOUNDING_CAPABILITIES, capabilities)
5956
6293
 
5957
6294
  @host_event_handler
5958
6295
  def on_cs_config(self, event: hci.HCI_LE_CS_Config_Complete_Event):
@@ -5960,7 +6297,9 @@ class Device(utils.CompositeEventEmitter):
5960
6297
  return
5961
6298
 
5962
6299
  if event.status != hci.HCI_SUCCESS:
5963
- connection.emit('channel_sounding_config_failure', event.status)
6300
+ connection.emit(
6301
+ connection.EVENT_CHANNEL_SOUNDING_CONFIG_FAILURE, event.status
6302
+ )
5964
6303
  return
5965
6304
  if event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.CREATED:
5966
6305
  config = ChannelSoundingConfig(
@@ -5986,11 +6325,13 @@ class Device(utils.CompositeEventEmitter):
5986
6325
  t_pm_time=event.t_pm_time,
5987
6326
  )
5988
6327
  connection.cs_configs[event.config_id] = config
5989
- connection.emit('channel_sounding_config', config)
6328
+ connection.emit(connection.EVENT_CHANNEL_SOUNDING_CONFIG, config)
5990
6329
  elif event.action == hci.HCI_LE_CS_Config_Complete_Event.Action.REMOVED:
5991
6330
  try:
5992
6331
  config = connection.cs_configs.pop(event.config_id)
5993
- connection.emit('channel_sounding_config_removed', config.config_id)
6332
+ connection.emit(
6333
+ connection.EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED, config.config_id
6334
+ )
5994
6335
  except KeyError:
5995
6336
  logger.error('Removing unknown config %d', event.config_id)
5996
6337
 
@@ -6000,7 +6341,9 @@ class Device(utils.CompositeEventEmitter):
6000
6341
  return
6001
6342
 
6002
6343
  if event.status != hci.HCI_SUCCESS:
6003
- connection.emit('channel_sounding_procedure_failure', event.status)
6344
+ connection.emit(
6345
+ connection.EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE, event.status
6346
+ )
6004
6347
  return
6005
6348
 
6006
6349
  procedure = ChannelSoundingProcedure(
@@ -6017,37 +6360,37 @@ class Device(utils.CompositeEventEmitter):
6017
6360
  max_procedure_len=event.max_procedure_len,
6018
6361
  )
6019
6362
  connection.cs_procedures[procedure.config_id] = procedure
6020
- connection.emit('channel_sounding_procedure', procedure)
6363
+ connection.emit(connection.EVENT_CHANNEL_SOUNDING_PROCEDURE, procedure)
6021
6364
 
6022
6365
  # [Classic only]
6023
6366
  @host_event_handler
6024
6367
  @with_connection_from_address
6025
6368
  def on_role_change(self, connection, new_role):
6026
6369
  connection.role = new_role
6027
- connection.emit('role_change', new_role)
6370
+ connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
6028
6371
 
6029
6372
  # [Classic only]
6030
6373
  @host_event_handler
6031
6374
  @try_with_connection_from_address
6032
6375
  def on_role_change_failure(self, connection, address, error):
6033
6376
  if connection:
6034
- connection.emit('role_change_failure', error)
6035
- self.emit('role_change_failure', address, error)
6377
+ connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
6378
+ self.emit(self.EVENT_ROLE_CHANGE_FAILURE, address, error)
6036
6379
 
6037
6380
  # [Classic only]
6038
6381
  @host_event_handler
6039
6382
  @with_connection_from_address
6040
6383
  def on_classic_pairing(self, connection: Connection) -> None:
6041
- connection.emit('classic_pairing')
6384
+ connection.emit(connection.EVENT_CLASSIC_PAIRING)
6042
6385
 
6043
6386
  # [Classic only]
6044
6387
  @host_event_handler
6045
6388
  @with_connection_from_address
6046
6389
  def on_classic_pairing_failure(self, connection: Connection, status) -> None:
6047
- connection.emit('classic_pairing_failure', status)
6390
+ connection.emit(connection.EVENT_CLASSIC_PAIRING_FAILURE, status)
6048
6391
 
6049
6392
  def on_pairing_start(self, connection: Connection) -> None:
6050
- connection.emit('pairing_start')
6393
+ connection.emit(connection.EVENT_PAIRING_START)
6051
6394
 
6052
6395
  def on_pairing(
6053
6396
  self,
@@ -6061,10 +6404,10 @@ class Device(utils.CompositeEventEmitter):
6061
6404
  connection.peer_address = identity_address
6062
6405
  connection.sc = sc
6063
6406
  connection.authenticated = True
6064
- connection.emit('pairing', keys)
6407
+ connection.emit(connection.EVENT_PAIRING, keys)
6065
6408
 
6066
6409
  def on_pairing_failure(self, connection: Connection, reason: int) -> None:
6067
- connection.emit('pairing_failure', reason)
6410
+ connection.emit(connection.EVENT_PAIRING_FAILURE, reason)
6068
6411
 
6069
6412
  @with_connection_from_handle
6070
6413
  def on_gatt_pdu(self, connection, pdu):