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/pandora/host.py CHANGED
@@ -73,7 +73,6 @@ from pandora.host_pb2 import (
73
73
  ConnectResponse,
74
74
  DataTypes,
75
75
  DisconnectRequest,
76
- DiscoverabilityMode,
77
76
  InquiryResponse,
78
77
  PrimaryPhy,
79
78
  ReadLocalAddressResponse,
@@ -86,9 +85,9 @@ from pandora.host_pb2 import (
86
85
  WaitConnectionResponse,
87
86
  WaitDisconnectionRequest,
88
87
  )
89
- from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple, cast
88
+ from typing import AsyncGenerator, Optional, cast
90
89
 
91
- PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
90
+ PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
92
91
  # Default value reported by Bumble for legacy Advertising reports.
93
92
  # FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
94
93
  0: PRIMARY_1M,
@@ -96,26 +95,26 @@ PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
96
95
  3: PRIMARY_CODED,
97
96
  }
98
97
 
99
- SECONDARY_PHY_MAP: Dict[int, SecondaryPhy] = {
98
+ SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = {
100
99
  0: SECONDARY_NONE,
101
100
  1: SECONDARY_1M,
102
101
  2: SECONDARY_2M,
103
102
  3: SECONDARY_CODED,
104
103
  }
105
104
 
106
- PRIMARY_PHY_TO_BUMBLE_PHY_MAP: Dict[PrimaryPhy, Phy] = {
105
+ PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = {
107
106
  PRIMARY_1M: Phy.LE_1M,
108
107
  PRIMARY_CODED: Phy.LE_CODED,
109
108
  }
110
109
 
111
- SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
110
+ SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = {
112
111
  SECONDARY_NONE: Phy.LE_1M,
113
112
  SECONDARY_1M: Phy.LE_1M,
114
113
  SECONDARY_2M: Phy.LE_2M,
115
114
  SECONDARY_CODED: Phy.LE_CODED,
116
115
  }
117
116
 
118
- OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
117
+ OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
119
118
  host_pb2.PUBLIC: OwnAddressType.PUBLIC,
120
119
  host_pb2.RANDOM: OwnAddressType.RANDOM,
121
120
  host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
@@ -124,7 +123,7 @@ OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
124
123
 
125
124
 
126
125
  class HostService(HostServicer):
127
- waited_connections: Set[int]
126
+ waited_connections: set[int]
128
127
 
129
128
  def __init__(
130
129
  self, grpc_server: grpc.aio.Server, device: Device, config: Config
@@ -296,12 +295,12 @@ class HostService(HostServicer):
296
295
  def on_disconnection(_: None) -> None:
297
296
  disconnection_future.set_result(None)
298
297
 
299
- connection.on('disconnection', on_disconnection)
298
+ connection.on(connection.EVENT_DISCONNECTION, on_disconnection)
300
299
  try:
301
300
  await disconnection_future
302
301
  self.log.debug("Disconnected")
303
302
  finally:
304
- connection.remove_listener('disconnection', on_disconnection) # type: ignore
303
+ connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
305
304
 
306
305
  return empty_pb2.Empty()
307
306
 
@@ -383,7 +382,7 @@ class HostService(HostServicer):
383
382
  ):
384
383
  connections.put_nowait(connection)
385
384
 
386
- self.device.on('connection', on_connection)
385
+ self.device.on(self.device.EVENT_CONNECTION, on_connection)
387
386
 
388
387
  try:
389
388
  # Advertise until RPC is canceled
@@ -501,7 +500,7 @@ class HostService(HostServicer):
501
500
  ):
502
501
  connections.put_nowait(connection)
503
502
 
504
- self.device.on('connection', on_connection)
503
+ self.device.on(self.device.EVENT_CONNECTION, on_connection)
505
504
 
506
505
  try:
507
506
  while True:
@@ -531,7 +530,7 @@ class HostService(HostServicer):
531
530
  await asyncio.sleep(1)
532
531
  finally:
533
532
  if request.connectable:
534
- self.device.remove_listener('connection', on_connection) # type: ignore
533
+ self.device.remove_listener(self.device.EVENT_CONNECTION, on_connection) # type: ignore
535
534
 
536
535
  try:
537
536
  self.log.debug('Stop advertising')
@@ -557,7 +556,7 @@ class HostService(HostServicer):
557
556
  scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
558
557
 
559
558
  scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
560
- handler = self.device.on('advertisement', scan_queue.put_nowait)
559
+ handler = self.device.on(self.device.EVENT_ADVERTISEMENT, scan_queue.put_nowait)
561
560
  await self.device.start_scanning(
562
561
  legacy=request.legacy,
563
562
  active=not request.passive,
@@ -602,7 +601,7 @@ class HostService(HostServicer):
602
601
  yield sr
603
602
 
604
603
  finally:
605
- self.device.remove_listener('advertisement', handler) # type: ignore
604
+ self.device.remove_listener(self.device.EVENT_ADVERTISEMENT, handler) # type: ignore
606
605
  try:
607
606
  self.log.debug('Stop scanning')
608
607
  await bumble.utils.cancel_on_event(
@@ -618,13 +617,13 @@ class HostService(HostServicer):
618
617
  self.log.debug('Inquiry')
619
618
 
620
619
  inquiry_queue: asyncio.Queue[
621
- Optional[Tuple[Address, int, AdvertisingData, int]]
620
+ Optional[tuple[Address, int, AdvertisingData, int]]
622
621
  ] = asyncio.Queue()
623
622
  complete_handler = self.device.on(
624
- 'inquiry_complete', lambda: inquiry_queue.put_nowait(None)
623
+ self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
625
624
  )
626
625
  result_handler = self.device.on( # type: ignore
627
- 'inquiry_result',
626
+ self.device.EVENT_INQUIRY_RESULT,
628
627
  lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
629
628
  (address, class_of_device, eir_data, rssi) # type: ignore
630
629
  ),
@@ -643,8 +642,8 @@ class HostService(HostServicer):
643
642
  )
644
643
 
645
644
  finally:
646
- self.device.remove_listener('inquiry_complete', complete_handler) # type: ignore
647
- self.device.remove_listener('inquiry_result', result_handler) # type: ignore
645
+ self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
646
+ self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
648
647
  try:
649
648
  self.log.debug('Stop inquiry')
650
649
  await bumble.utils.cancel_on_event(
@@ -670,10 +669,10 @@ class HostService(HostServicer):
670
669
  return empty_pb2.Empty()
671
670
 
672
671
  def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
673
- ad_structures: List[Tuple[int, bytes]] = []
672
+ ad_structures: list[tuple[int, bytes]] = []
674
673
 
675
- uuids: List[str]
676
- datas: Dict[str, bytes]
674
+ uuids: list[str]
675
+ datas: dict[str, bytes]
677
676
 
678
677
  def uuid128_from_str(uuid: str) -> bytes:
679
678
  """Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
@@ -887,50 +886,50 @@ class HostService(HostServicer):
887
886
 
888
887
  def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
889
888
  dt = DataTypes()
890
- uuids: List[UUID]
889
+ uuids: list[UUID]
891
890
  s: str
892
891
  i: int
893
- ij: Tuple[int, int]
894
- uuid_data: Tuple[UUID, bytes]
892
+ ij: tuple[int, int]
893
+ uuid_data: tuple[UUID, bytes]
895
894
  data: bytes
896
895
 
897
896
  if uuids := cast(
898
- List[UUID],
897
+ list[UUID],
899
898
  ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
900
899
  ):
901
900
  dt.incomplete_service_class_uuids16.extend(
902
901
  list(map(lambda x: x.to_hex_str('-'), uuids))
903
902
  )
904
903
  if uuids := cast(
905
- List[UUID],
904
+ list[UUID],
906
905
  ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
907
906
  ):
908
907
  dt.complete_service_class_uuids16.extend(
909
908
  list(map(lambda x: x.to_hex_str('-'), uuids))
910
909
  )
911
910
  if uuids := cast(
912
- List[UUID],
911
+ list[UUID],
913
912
  ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
914
913
  ):
915
914
  dt.incomplete_service_class_uuids32.extend(
916
915
  list(map(lambda x: x.to_hex_str('-'), uuids))
917
916
  )
918
917
  if uuids := cast(
919
- List[UUID],
918
+ list[UUID],
920
919
  ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
921
920
  ):
922
921
  dt.complete_service_class_uuids32.extend(
923
922
  list(map(lambda x: x.to_hex_str('-'), uuids))
924
923
  )
925
924
  if uuids := cast(
926
- List[UUID],
925
+ list[UUID],
927
926
  ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
928
927
  ):
929
928
  dt.incomplete_service_class_uuids128.extend(
930
929
  list(map(lambda x: x.to_hex_str('-'), uuids))
931
930
  )
932
931
  if uuids := cast(
933
- List[UUID],
932
+ list[UUID],
934
933
  ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
935
934
  ):
936
935
  dt.complete_service_class_uuids128.extend(
@@ -945,42 +944,42 @@ class HostService(HostServicer):
945
944
  if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
946
945
  dt.class_of_device = i
947
946
  if ij := cast(
948
- Tuple[int, int],
947
+ tuple[int, int],
949
948
  ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
950
949
  ):
951
950
  dt.peripheral_connection_interval_min = ij[0]
952
951
  dt.peripheral_connection_interval_max = ij[1]
953
952
  if uuids := cast(
954
- List[UUID],
953
+ list[UUID],
955
954
  ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
956
955
  ):
957
956
  dt.service_solicitation_uuids16.extend(
958
957
  list(map(lambda x: x.to_hex_str('-'), uuids))
959
958
  )
960
959
  if uuids := cast(
961
- List[UUID],
960
+ list[UUID],
962
961
  ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
963
962
  ):
964
963
  dt.service_solicitation_uuids32.extend(
965
964
  list(map(lambda x: x.to_hex_str('-'), uuids))
966
965
  )
967
966
  if uuids := cast(
968
- List[UUID],
967
+ list[UUID],
969
968
  ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
970
969
  ):
971
970
  dt.service_solicitation_uuids128.extend(
972
971
  list(map(lambda x: x.to_hex_str('-'), uuids))
973
972
  )
974
973
  if uuid_data := cast(
975
- Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
974
+ tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
976
975
  ):
977
976
  dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
978
977
  if uuid_data := cast(
979
- Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
978
+ tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
980
979
  ):
981
980
  dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
982
981
  if uuid_data := cast(
983
- Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
982
+ tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
984
983
  ):
985
984
  dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
986
985
  if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):
bumble/pandora/l2cap.py CHANGED
@@ -51,7 +51,7 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
51
51
  WaitDisconnectionRequest,
52
52
  WaitDisconnectionResponse,
53
53
  )
54
- from typing import AsyncGenerator, Dict, Optional, Union
54
+ from typing import AsyncGenerator, Optional, Union
55
55
  from dataclasses import dataclass
56
56
 
57
57
  L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
@@ -70,7 +70,7 @@ class L2CAPService(L2CAPServicer):
70
70
  )
71
71
  self.device = device
72
72
  self.config = config
73
- self.channels: Dict[bytes, ChannelContext] = {}
73
+ self.channels: dict[bytes, ChannelContext] = {}
74
74
 
75
75
  def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext:
76
76
  close_future = asyncio.get_running_loop().create_future()
@@ -83,7 +83,7 @@ class L2CAPService(L2CAPServicer):
83
83
  close_future.set_result(None)
84
84
 
85
85
  l2cap_channel.sink = on_channel_sdu
86
- l2cap_channel.on('close', on_close)
86
+ l2cap_channel.on(l2cap_channel.EVENT_CLOSE, on_close)
87
87
 
88
88
  return ChannelContext(close_future, sdu_queue)
89
89
 
@@ -151,7 +151,7 @@ class L2CAPService(L2CAPServicer):
151
151
  spec=spec, handler=on_l2cap_channel
152
152
  )
153
153
  else:
154
- l2cap_server.on('connection', on_l2cap_channel)
154
+ l2cap_server.on(l2cap_server.EVENT_CONNECTION, on_l2cap_channel)
155
155
 
156
156
  try:
157
157
  self.log.debug('Waiting for a channel connection.')
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
  import asyncio
17
17
  import contextlib
18
+ from collections.abc import Awaitable
18
19
  import grpc
19
20
  import logging
20
21
 
@@ -24,6 +25,7 @@ from bumble import hci
24
25
  from bumble.core import (
25
26
  PhysicalTransport,
26
27
  ProtocolError,
28
+ InvalidArgumentError,
27
29
  )
28
30
  import bumble.utils
29
31
  from bumble.device import Connection as BumbleConnection, Device
@@ -55,7 +57,7 @@ from pandora.security_pb2 import (
55
57
  WaitSecurityRequest,
56
58
  WaitSecurityResponse,
57
59
  )
58
- from typing import Any, AsyncGenerator, AsyncIterator, Callable, Dict, Optional, Union
60
+ from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
59
61
 
60
62
 
61
63
  class PairingDelegate(BasePairingDelegate):
@@ -188,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
188
190
  self.service.event_queue.put_nowait(event)
189
191
 
190
192
 
191
- BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
192
- LEVEL0: lambda connection: True,
193
- LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
194
- LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
195
- LEVEL3: lambda connection: connection.encryption != 0
196
- and connection.authenticated
197
- and connection.link_key_type
198
- in (
199
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
200
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
201
- ),
202
- LEVEL4: lambda connection: connection.encryption
203
- == hci.HCI_Encryption_Change_Event.AES_CCM
204
- and connection.authenticated
205
- and connection.link_key_type
206
- == hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
207
- }
208
-
209
- LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
210
- LE_LEVEL1: lambda connection: True,
211
- LE_LEVEL2: lambda connection: connection.encryption != 0,
212
- LE_LEVEL3: lambda connection: connection.encryption != 0
213
- and connection.authenticated,
214
- LE_LEVEL4: lambda connection: connection.encryption != 0
215
- and connection.authenticated
216
- and connection.sc,
217
- }
218
-
219
-
220
193
  class SecurityService(SecurityServicer):
221
194
  def __init__(self, device: Device, config: Config) -> None:
222
195
  self.log = utils.BumbleServerLoggerAdapter(
@@ -248,6 +221,59 @@ class SecurityService(SecurityServicer):
248
221
 
249
222
  self.device.pairing_config_factory = pairing_config_factory
250
223
 
224
+ async def _classic_level_reached(
225
+ self, level: SecurityLevel, connection: BumbleConnection
226
+ ) -> bool:
227
+ if level == LEVEL0:
228
+ return True
229
+ if level == LEVEL1:
230
+ return connection.encryption == 0 or connection.authenticated
231
+ if level == LEVEL2:
232
+ return connection.encryption != 0 and connection.authenticated
233
+
234
+ link_key_type: Optional[int] = None
235
+ if (keystore := connection.device.keystore) and (
236
+ keys := await keystore.get(str(connection.peer_address))
237
+ ):
238
+ link_key_type = keys.link_key_type
239
+ self.log.debug("link_key_type: %d", link_key_type)
240
+
241
+ if level == LEVEL3:
242
+ return (
243
+ connection.encryption != 0
244
+ and connection.authenticated
245
+ and link_key_type
246
+ in (
247
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
248
+ hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
249
+ )
250
+ )
251
+ if level == LEVEL4:
252
+ return (
253
+ connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
254
+ and connection.authenticated
255
+ and link_key_type
256
+ == hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256
257
+ )
258
+ raise InvalidArgumentError(f"Unexpected level {level}")
259
+
260
+ def _le_level_reached(
261
+ self, level: LESecurityLevel, connection: BumbleConnection
262
+ ) -> bool:
263
+ if level == LE_LEVEL1:
264
+ return True
265
+ if level == LE_LEVEL2:
266
+ return connection.encryption != 0
267
+ if level == LE_LEVEL3:
268
+ return connection.encryption != 0 and connection.authenticated
269
+ if level == LE_LEVEL4:
270
+ return (
271
+ connection.encryption != 0
272
+ and connection.authenticated
273
+ and connection.sc
274
+ )
275
+ raise InvalidArgumentError(f"Unexpected level {level}")
276
+
251
277
  @utils.rpc
252
278
  async def OnPairing(
253
279
  self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
@@ -290,7 +316,7 @@ class SecurityService(SecurityServicer):
290
316
  ] == oneof
291
317
 
292
318
  # security level already reached
293
- if self.reached_security_level(connection, level):
319
+ if await self.reached_security_level(connection, level):
294
320
  return SecureResponse(success=empty_pb2.Empty())
295
321
 
296
322
  # trigger pairing if needed
@@ -302,15 +328,15 @@ class SecurityService(SecurityServicer):
302
328
 
303
329
  with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
304
330
 
305
- @watcher.on(connection, 'pairing')
331
+ @watcher.on(connection, connection.EVENT_PAIRING)
306
332
  def on_pairing(*_: Any) -> None:
307
333
  security_result.set_result('success')
308
334
 
309
- @watcher.on(connection, 'pairing_failure')
335
+ @watcher.on(connection, connection.EVENT_PAIRING_FAILURE)
310
336
  def on_pairing_failure(*_: Any) -> None:
311
337
  security_result.set_result('pairing_failure')
312
338
 
313
- @watcher.on(connection, 'disconnection')
339
+ @watcher.on(connection, connection.EVENT_DISCONNECTION)
314
340
  def on_disconnection(*_: Any) -> None:
315
341
  security_result.set_result('connection_died')
316
342
 
@@ -361,7 +387,7 @@ class SecurityService(SecurityServicer):
361
387
  return SecureResponse(encryption_failure=empty_pb2.Empty())
362
388
 
363
389
  # security level has been reached ?
364
- if self.reached_security_level(connection, level):
390
+ if await self.reached_security_level(connection, level):
365
391
  return SecureResponse(success=empty_pb2.Empty())
366
392
  return SecureResponse(not_reached=empty_pb2.Empty())
367
393
 
@@ -388,13 +414,10 @@ class SecurityService(SecurityServicer):
388
414
  pair_task: Optional[asyncio.Future[None]] = None
389
415
 
390
416
  async def authenticate() -> None:
391
- assert connection
392
417
  if (encryption := connection.encryption) != 0:
393
418
  self.log.debug('Disable encryption...')
394
- try:
419
+ with contextlib.suppress(Exception):
395
420
  await connection.encrypt(enable=False)
396
- except:
397
- pass
398
421
  self.log.debug('Disable encryption: done')
399
422
 
400
423
  self.log.debug('Authenticate...')
@@ -413,15 +436,13 @@ class SecurityService(SecurityServicer):
413
436
 
414
437
  return wrapper
415
438
 
416
- def try_set_success(*_: Any) -> None:
417
- assert connection
418
- if self.reached_security_level(connection, level):
439
+ async def try_set_success(*_: Any) -> None:
440
+ if await self.reached_security_level(connection, level):
419
441
  self.log.debug('Wait for security: done')
420
442
  wait_for_security.set_result('success')
421
443
 
422
- def on_encryption_change(*_: Any) -> None:
423
- assert connection
424
- if self.reached_security_level(connection, level):
444
+ async def on_encryption_change(*_: Any) -> None:
445
+ if await self.reached_security_level(connection, level):
425
446
  self.log.debug('Wait for security: done')
426
447
  wait_for_security.set_result('success')
427
448
  elif (
@@ -436,7 +457,7 @@ class SecurityService(SecurityServicer):
436
457
  if self.need_pairing(connection, level):
437
458
  pair_task = asyncio.create_task(connection.pair())
438
459
 
439
- listeners: Dict[str, Callable[..., None]] = {
460
+ listeners: dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
440
461
  'disconnection': set_failure('connection_died'),
441
462
  'pairing_failure': set_failure('pairing_failure'),
442
463
  'connection_authentication_failure': set_failure('authentication_failure'),
@@ -455,7 +476,7 @@ class SecurityService(SecurityServicer):
455
476
  watcher.on(connection, event, listener)
456
477
 
457
478
  # security level already reached
458
- if self.reached_security_level(connection, level):
479
+ if await self.reached_security_level(connection, level):
459
480
  return WaitSecurityResponse(success=empty_pb2.Empty())
460
481
 
461
482
  self.log.debug('Wait for security...')
@@ -465,24 +486,20 @@ class SecurityService(SecurityServicer):
465
486
  # wait for `authenticate` to finish if any
466
487
  if authenticate_task is not None:
467
488
  self.log.debug('Wait for authentication...')
468
- try:
489
+ with contextlib.suppress(Exception):
469
490
  await authenticate_task # type: ignore
470
- except:
471
- pass
472
491
  self.log.debug('Authenticated')
473
492
 
474
493
  # wait for `pair` to finish if any
475
494
  if pair_task is not None:
476
495
  self.log.debug('Wait for authentication...')
477
- try:
496
+ with contextlib.suppress(Exception):
478
497
  await pair_task # type: ignore
479
- except:
480
- pass
481
498
  self.log.debug('paired')
482
499
 
483
500
  return WaitSecurityResponse(**kwargs)
484
501
 
485
- def reached_security_level(
502
+ async def reached_security_level(
486
503
  self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
487
504
  ) -> bool:
488
505
  self.log.debug(
@@ -492,15 +509,14 @@ class SecurityService(SecurityServicer):
492
509
  'encryption': connection.encryption,
493
510
  'authenticated': connection.authenticated,
494
511
  'sc': connection.sc,
495
- 'link_key_type': connection.link_key_type,
496
512
  }
497
513
  )
498
514
  )
499
515
 
500
516
  if isinstance(level, LESecurityLevel):
501
- return LE_LEVEL_REACHED[level](connection)
517
+ return self._le_level_reached(level, connection)
502
518
 
503
- return BR_LEVEL_REACHED[level](connection)
519
+ return await self._classic_level_reached(level, connection)
504
520
 
505
521
  def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
506
522
  if connection.transport == PhysicalTransport.LE:
bumble/pandora/utils.py CHANGED
@@ -22,9 +22,9 @@ import logging
22
22
  from bumble.device import Device
23
23
  from bumble.hci import Address, AddressType
24
24
  from google.protobuf.message import Message # pytype: disable=pyi-error
25
- from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
25
+ from typing import Any, Generator, MutableMapping, Optional
26
26
 
27
- ADDRESS_TYPES: Dict[str, AddressType] = {
27
+ ADDRESS_TYPES: dict[str, AddressType] = {
28
28
  "public": Address.PUBLIC_DEVICE_ADDRESS,
29
29
  "random": Address.RANDOM_DEVICE_ADDRESS,
30
30
  "public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
@@ -43,7 +43,7 @@ class BumbleServerLoggerAdapter(logging.LoggerAdapter): # type: ignore
43
43
 
44
44
  def process(
45
45
  self, msg: str, kwargs: MutableMapping[str, Any]
46
- ) -> Tuple[str, MutableMapping[str, Any]]:
46
+ ) -> tuple[str, MutableMapping[str, Any]]:
47
47
  assert self.extra
48
48
  service_name = self.extra['service_name']
49
49
  assert isinstance(service_name, str)
bumble/profiles/aics.py CHANGED
@@ -198,8 +198,7 @@ class AudioInputControlPoint:
198
198
  audio_input_state: AudioInputState
199
199
  gain_settings_properties: GainSettingsProperties
200
200
 
201
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
202
- assert connection
201
+ async def on_write(self, connection: Connection, value: bytes) -> None:
203
202
 
204
203
  opcode = AudioInputControlPointOpCode(value[0])
205
204
 
@@ -320,11 +319,10 @@ class AudioInputDescription:
320
319
  audio_input_description: str = "Bluetooth"
321
320
  attribute: Optional[Attribute] = None
322
321
 
323
- def on_read(self, _connection: Optional[Connection]) -> str:
322
+ def on_read(self, _connection: Connection) -> str:
324
323
  return self.audio_input_description
325
324
 
326
- async def on_write(self, connection: Optional[Connection], value: str) -> None:
327
- assert connection
325
+ async def on_write(self, connection: Connection, value: str) -> None:
328
326
  assert self.attribute
329
327
 
330
328
  self.audio_input_description = value
bumble/profiles/ancs.py CHANGED
@@ -250,6 +250,8 @@ class AncsClient(utils.EventEmitter):
250
250
  _expected_response_tuples: int
251
251
  _response_accumulator: bytes
252
252
 
253
+ EVENT_NOTIFICATION = "notification"
254
+
253
255
  def __init__(self, ancs_proxy: AncsProxy) -> None:
254
256
  super().__init__()
255
257
  self._ancs_proxy = ancs_proxy
@@ -284,7 +286,7 @@ class AncsClient(utils.EventEmitter):
284
286
 
285
287
  def _on_notification(self, notification: Notification) -> None:
286
288
  logger.debug(f"ANCS NOTIFICATION: {notification}")
287
- self.emit("notification", notification)
289
+ self.emit(self.EVENT_NOTIFICATION, notification)
288
290
 
289
291
  def _on_data(self, data: bytes) -> None:
290
292
  logger.debug(f"ANCS DATA: {data.hex()}")