bumble 0.0.169__py3-none-any.whl → 0.0.172__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.
bumble/l2cap.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
19
  import asyncio
20
+ import enum
20
21
  import logging
21
22
  import struct
22
23
 
@@ -676,56 +677,35 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
676
677
 
677
678
  # -----------------------------------------------------------------------------
678
679
  class Channel(EventEmitter):
679
- # States
680
- CLOSED = 0x00
681
- WAIT_CONNECT = 0x01
682
- WAIT_CONNECT_RSP = 0x02
683
- OPEN = 0x03
684
- WAIT_DISCONNECT = 0x04
685
- WAIT_CREATE = 0x05
686
- WAIT_CREATE_RSP = 0x06
687
- WAIT_MOVE = 0x07
688
- WAIT_MOVE_RSP = 0x08
689
- WAIT_MOVE_CONFIRM = 0x09
690
- WAIT_CONFIRM_RSP = 0x0A
691
-
692
- # CONFIG substates
693
- WAIT_CONFIG = 0x10
694
- WAIT_SEND_CONFIG = 0x11
695
- WAIT_CONFIG_REQ_RSP = 0x12
696
- WAIT_CONFIG_RSP = 0x13
697
- WAIT_CONFIG_REQ = 0x14
698
- WAIT_IND_FINAL_RSP = 0x15
699
- WAIT_FINAL_RSP = 0x16
700
- WAIT_CONTROL_IND = 0x17
701
-
702
- STATE_NAMES = {
703
- CLOSED: 'CLOSED',
704
- WAIT_CONNECT: 'WAIT_CONNECT',
705
- WAIT_CONNECT_RSP: 'WAIT_CONNECT_RSP',
706
- OPEN: 'OPEN',
707
- WAIT_DISCONNECT: 'WAIT_DISCONNECT',
708
- WAIT_CREATE: 'WAIT_CREATE',
709
- WAIT_CREATE_RSP: 'WAIT_CREATE_RSP',
710
- WAIT_MOVE: 'WAIT_MOVE',
711
- WAIT_MOVE_RSP: 'WAIT_MOVE_RSP',
712
- WAIT_MOVE_CONFIRM: 'WAIT_MOVE_CONFIRM',
713
- WAIT_CONFIRM_RSP: 'WAIT_CONFIRM_RSP',
714
- WAIT_CONFIG: 'WAIT_CONFIG',
715
- WAIT_SEND_CONFIG: 'WAIT_SEND_CONFIG',
716
- WAIT_CONFIG_REQ_RSP: 'WAIT_CONFIG_REQ_RSP',
717
- WAIT_CONFIG_RSP: 'WAIT_CONFIG_RSP',
718
- WAIT_CONFIG_REQ: 'WAIT_CONFIG_REQ',
719
- WAIT_IND_FINAL_RSP: 'WAIT_IND_FINAL_RSP',
720
- WAIT_FINAL_RSP: 'WAIT_FINAL_RSP',
721
- WAIT_CONTROL_IND: 'WAIT_CONTROL_IND',
722
- }
680
+ class State(enum.IntEnum):
681
+ # States
682
+ CLOSED = 0x00
683
+ WAIT_CONNECT = 0x01
684
+ WAIT_CONNECT_RSP = 0x02
685
+ OPEN = 0x03
686
+ WAIT_DISCONNECT = 0x04
687
+ WAIT_CREATE = 0x05
688
+ WAIT_CREATE_RSP = 0x06
689
+ WAIT_MOVE = 0x07
690
+ WAIT_MOVE_RSP = 0x08
691
+ WAIT_MOVE_CONFIRM = 0x09
692
+ WAIT_CONFIRM_RSP = 0x0A
693
+
694
+ # CONFIG substates
695
+ WAIT_CONFIG = 0x10
696
+ WAIT_SEND_CONFIG = 0x11
697
+ WAIT_CONFIG_REQ_RSP = 0x12
698
+ WAIT_CONFIG_RSP = 0x13
699
+ WAIT_CONFIG_REQ = 0x14
700
+ WAIT_IND_FINAL_RSP = 0x15
701
+ WAIT_FINAL_RSP = 0x16
702
+ WAIT_CONTROL_IND = 0x17
723
703
 
724
704
  connection_result: Optional[asyncio.Future[None]]
725
705
  disconnection_result: Optional[asyncio.Future[None]]
726
706
  response: Optional[asyncio.Future[bytes]]
727
707
  sink: Optional[Callable[[bytes], Any]]
728
- state: int
708
+ state: State
729
709
  connection: Connection
730
710
 
731
711
  def __init__(
@@ -741,7 +721,7 @@ class Channel(EventEmitter):
741
721
  self.manager = manager
742
722
  self.connection = connection
743
723
  self.signaling_cid = signaling_cid
744
- self.state = Channel.CLOSED
724
+ self.state = self.State.CLOSED
745
725
  self.mtu = mtu
746
726
  self.psm = psm
747
727
  self.source_cid = source_cid
@@ -751,10 +731,8 @@ class Channel(EventEmitter):
751
731
  self.disconnection_result = None
752
732
  self.sink = None
753
733
 
754
- def change_state(self, new_state: int) -> None:
755
- logger.debug(
756
- f'{self} state change -> {color(Channel.STATE_NAMES[new_state], "cyan")}'
757
- )
734
+ def _change_state(self, new_state: State) -> None:
735
+ logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
758
736
  self.state = new_state
759
737
 
760
738
  def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
@@ -767,7 +745,7 @@ class Channel(EventEmitter):
767
745
  # Check that there isn't already a request pending
768
746
  if self.response:
769
747
  raise InvalidStateError('request already pending')
770
- if self.state != Channel.OPEN:
748
+ if self.state != self.State.OPEN:
771
749
  raise InvalidStateError('channel not open')
772
750
 
773
751
  self.response = asyncio.get_running_loop().create_future()
@@ -787,14 +765,14 @@ class Channel(EventEmitter):
787
765
  )
788
766
 
789
767
  async def connect(self) -> None:
790
- if self.state != Channel.CLOSED:
768
+ if self.state != self.State.CLOSED:
791
769
  raise InvalidStateError('invalid state')
792
770
 
793
771
  # Check that we can start a new connection
794
772
  if self.connection_result:
795
773
  raise RuntimeError('connection already pending')
796
774
 
797
- self.change_state(Channel.WAIT_CONNECT_RSP)
775
+ self._change_state(self.State.WAIT_CONNECT_RSP)
798
776
  self.send_control_frame(
799
777
  L2CAP_Connection_Request(
800
778
  identifier=self.manager.next_identifier(self.connection),
@@ -814,10 +792,10 @@ class Channel(EventEmitter):
814
792
  self.connection_result = None
815
793
 
816
794
  async def disconnect(self) -> None:
817
- if self.state != Channel.OPEN:
795
+ if self.state != self.State.OPEN:
818
796
  raise InvalidStateError('invalid state')
819
797
 
820
- self.change_state(Channel.WAIT_DISCONNECT)
798
+ self._change_state(self.State.WAIT_DISCONNECT)
821
799
  self.send_control_frame(
822
800
  L2CAP_Disconnection_Request(
823
801
  identifier=self.manager.next_identifier(self.connection),
@@ -832,8 +810,8 @@ class Channel(EventEmitter):
832
810
  return await self.disconnection_result
833
811
 
834
812
  def abort(self) -> None:
835
- if self.state == self.OPEN:
836
- self.change_state(self.CLOSED)
813
+ if self.state == self.State.OPEN:
814
+ self._change_state(self.State.CLOSED)
837
815
  self.emit('close')
838
816
 
839
817
  def send_configure_request(self) -> None:
@@ -856,7 +834,7 @@ class Channel(EventEmitter):
856
834
 
857
835
  def on_connection_request(self, request) -> None:
858
836
  self.destination_cid = request.source_cid
859
- self.change_state(Channel.WAIT_CONNECT)
837
+ self._change_state(self.State.WAIT_CONNECT)
860
838
  self.send_control_frame(
861
839
  L2CAP_Connection_Response(
862
840
  identifier=request.identifier,
@@ -866,24 +844,24 @@ class Channel(EventEmitter):
866
844
  status=0x0000,
867
845
  )
868
846
  )
869
- self.change_state(Channel.WAIT_CONFIG)
847
+ self._change_state(self.State.WAIT_CONFIG)
870
848
  self.send_configure_request()
871
- self.change_state(Channel.WAIT_CONFIG_REQ_RSP)
849
+ self._change_state(self.State.WAIT_CONFIG_REQ_RSP)
872
850
 
873
851
  def on_connection_response(self, response):
874
- if self.state != Channel.WAIT_CONNECT_RSP:
852
+ if self.state != self.State.WAIT_CONNECT_RSP:
875
853
  logger.warning(color('invalid state', 'red'))
876
854
  return
877
855
 
878
856
  if response.result == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL:
879
857
  self.destination_cid = response.destination_cid
880
- self.change_state(Channel.WAIT_CONFIG)
858
+ self._change_state(self.State.WAIT_CONFIG)
881
859
  self.send_configure_request()
882
- self.change_state(Channel.WAIT_CONFIG_REQ_RSP)
860
+ self._change_state(self.State.WAIT_CONFIG_REQ_RSP)
883
861
  elif response.result == L2CAP_Connection_Response.CONNECTION_PENDING:
884
862
  pass
885
863
  else:
886
- self.change_state(Channel.CLOSED)
864
+ self._change_state(self.State.CLOSED)
887
865
  self.connection_result.set_exception(
888
866
  ProtocolError(
889
867
  response.result,
@@ -895,9 +873,9 @@ class Channel(EventEmitter):
895
873
 
896
874
  def on_configure_request(self, request) -> None:
897
875
  if self.state not in (
898
- Channel.WAIT_CONFIG,
899
- Channel.WAIT_CONFIG_REQ,
900
- Channel.WAIT_CONFIG_REQ_RSP,
876
+ self.State.WAIT_CONFIG,
877
+ self.State.WAIT_CONFIG_REQ,
878
+ self.State.WAIT_CONFIG_REQ_RSP,
901
879
  ):
902
880
  logger.warning(color('invalid state', 'red'))
903
881
  return
@@ -918,25 +896,28 @@ class Channel(EventEmitter):
918
896
  options=request.options, # TODO: don't accept everything blindly
919
897
  )
920
898
  )
921
- if self.state == Channel.WAIT_CONFIG:
922
- self.change_state(Channel.WAIT_SEND_CONFIG)
899
+ if self.state == self.State.WAIT_CONFIG:
900
+ self._change_state(self.State.WAIT_SEND_CONFIG)
923
901
  self.send_configure_request()
924
- self.change_state(Channel.WAIT_CONFIG_RSP)
925
- elif self.state == Channel.WAIT_CONFIG_REQ:
926
- self.change_state(Channel.OPEN)
902
+ self._change_state(self.State.WAIT_CONFIG_RSP)
903
+ elif self.state == self.State.WAIT_CONFIG_REQ:
904
+ self._change_state(self.State.OPEN)
927
905
  if self.connection_result:
928
906
  self.connection_result.set_result(None)
929
907
  self.connection_result = None
930
908
  self.emit('open')
931
- elif self.state == Channel.WAIT_CONFIG_REQ_RSP:
932
- self.change_state(Channel.WAIT_CONFIG_RSP)
909
+ elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
910
+ self._change_state(self.State.WAIT_CONFIG_RSP)
933
911
 
934
912
  def on_configure_response(self, response) -> None:
935
913
  if response.result == L2CAP_Configure_Response.SUCCESS:
936
- if self.state == Channel.WAIT_CONFIG_REQ_RSP:
937
- self.change_state(Channel.WAIT_CONFIG_REQ)
938
- elif self.state in (Channel.WAIT_CONFIG_RSP, Channel.WAIT_CONTROL_IND):
939
- self.change_state(Channel.OPEN)
914
+ if self.state == self.State.WAIT_CONFIG_REQ_RSP:
915
+ self._change_state(self.State.WAIT_CONFIG_REQ)
916
+ elif self.state in (
917
+ self.State.WAIT_CONFIG_RSP,
918
+ self.State.WAIT_CONTROL_IND,
919
+ ):
920
+ self._change_state(self.State.OPEN)
940
921
  if self.connection_result:
941
922
  self.connection_result.set_result(None)
942
923
  self.connection_result = None
@@ -966,7 +947,7 @@ class Channel(EventEmitter):
966
947
  # TODO: decide how to fail gracefully
967
948
 
968
949
  def on_disconnection_request(self, request) -> None:
969
- if self.state in (Channel.OPEN, Channel.WAIT_DISCONNECT):
950
+ if self.state in (self.State.OPEN, self.State.WAIT_DISCONNECT):
970
951
  self.send_control_frame(
971
952
  L2CAP_Disconnection_Response(
972
953
  identifier=request.identifier,
@@ -974,14 +955,14 @@ class Channel(EventEmitter):
974
955
  source_cid=request.source_cid,
975
956
  )
976
957
  )
977
- self.change_state(Channel.CLOSED)
958
+ self._change_state(self.State.CLOSED)
978
959
  self.emit('close')
979
960
  self.manager.on_channel_closed(self)
980
961
  else:
981
962
  logger.warning(color('invalid state', 'red'))
982
963
 
983
964
  def on_disconnection_response(self, response) -> None:
984
- if self.state != Channel.WAIT_DISCONNECT:
965
+ if self.state != self.State.WAIT_DISCONNECT:
985
966
  logger.warning(color('invalid state', 'red'))
986
967
  return
987
968
 
@@ -992,7 +973,7 @@ class Channel(EventEmitter):
992
973
  logger.warning('unexpected source or destination CID')
993
974
  return
994
975
 
995
- self.change_state(Channel.CLOSED)
976
+ self._change_state(self.State.CLOSED)
996
977
  if self.disconnection_result:
997
978
  self.disconnection_result.set_result(None)
998
979
  self.disconnection_result = None
@@ -1004,7 +985,7 @@ class Channel(EventEmitter):
1004
985
  f'Channel({self.source_cid}->{self.destination_cid}, '
1005
986
  f'PSM={self.psm}, '
1006
987
  f'MTU={self.mtu}, '
1007
- f'state={Channel.STATE_NAMES[self.state]})'
988
+ f'state={self.state.name})'
1008
989
  )
1009
990
 
1010
991
 
@@ -1014,33 +995,21 @@ class LeConnectionOrientedChannel(EventEmitter):
1014
995
  LE Credit-based Connection Oriented Channel
1015
996
  """
1016
997
 
1017
- INIT = 0
1018
- CONNECTED = 1
1019
- CONNECTING = 2
1020
- DISCONNECTING = 3
1021
- DISCONNECTED = 4
1022
- CONNECTION_ERROR = 5
1023
-
1024
- STATE_NAMES = {
1025
- INIT: 'INIT',
1026
- CONNECTED: 'CONNECTED',
1027
- CONNECTING: 'CONNECTING',
1028
- DISCONNECTING: 'DISCONNECTING',
1029
- DISCONNECTED: 'DISCONNECTED',
1030
- CONNECTION_ERROR: 'CONNECTION_ERROR',
1031
- }
998
+ class State(enum.IntEnum):
999
+ INIT = 0
1000
+ CONNECTED = 1
1001
+ CONNECTING = 2
1002
+ DISCONNECTING = 3
1003
+ DISCONNECTED = 4
1004
+ CONNECTION_ERROR = 5
1032
1005
 
1033
1006
  out_queue: Deque[bytes]
1034
1007
  connection_result: Optional[asyncio.Future[LeConnectionOrientedChannel]]
1035
1008
  disconnection_result: Optional[asyncio.Future[None]]
1036
1009
  out_sdu: Optional[bytes]
1037
- state: int
1010
+ state: State
1038
1011
  connection: Connection
1039
1012
 
1040
- @staticmethod
1041
- def state_name(state: int) -> str:
1042
- return name_or_number(LeConnectionOrientedChannel.STATE_NAMES, state)
1043
-
1044
1013
  def __init__(
1045
1014
  self,
1046
1015
  manager: ChannelManager,
@@ -1083,19 +1052,17 @@ class LeConnectionOrientedChannel(EventEmitter):
1083
1052
  self.drained.set()
1084
1053
 
1085
1054
  if connected:
1086
- self.state = LeConnectionOrientedChannel.CONNECTED
1055
+ self.state = self.State.CONNECTED
1087
1056
  else:
1088
- self.state = LeConnectionOrientedChannel.INIT
1057
+ self.state = self.State.INIT
1089
1058
 
1090
- def change_state(self, new_state: int) -> None:
1091
- logger.debug(
1092
- f'{self} state change -> {color(self.state_name(new_state), "cyan")}'
1093
- )
1059
+ def _change_state(self, new_state: State) -> None:
1060
+ logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
1094
1061
  self.state = new_state
1095
1062
 
1096
- if new_state == self.CONNECTED:
1063
+ if new_state == self.State.CONNECTED:
1097
1064
  self.emit('open')
1098
- elif new_state == self.DISCONNECTED:
1065
+ elif new_state == self.State.DISCONNECTED:
1099
1066
  self.emit('close')
1100
1067
 
1101
1068
  def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
@@ -1106,7 +1073,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1106
1073
 
1107
1074
  async def connect(self) -> LeConnectionOrientedChannel:
1108
1075
  # Check that we're in the right state
1109
- if self.state != self.INIT:
1076
+ if self.state != self.State.INIT:
1110
1077
  raise InvalidStateError('not in a connectable state')
1111
1078
 
1112
1079
  # Check that we can start a new connection
@@ -1114,7 +1081,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1114
1081
  if identifier in self.manager.le_coc_requests:
1115
1082
  raise RuntimeError('too many concurrent connection requests')
1116
1083
 
1117
- self.change_state(self.CONNECTING)
1084
+ self._change_state(self.State.CONNECTING)
1118
1085
  request = L2CAP_LE_Credit_Based_Connection_Request(
1119
1086
  identifier=identifier,
1120
1087
  le_psm=self.le_psm,
@@ -1134,10 +1101,10 @@ class LeConnectionOrientedChannel(EventEmitter):
1134
1101
 
1135
1102
  async def disconnect(self) -> None:
1136
1103
  # Check that we're connected
1137
- if self.state != self.CONNECTED:
1104
+ if self.state != self.State.CONNECTED:
1138
1105
  raise InvalidStateError('not connected')
1139
1106
 
1140
- self.change_state(self.DISCONNECTING)
1107
+ self._change_state(self.State.DISCONNECTING)
1141
1108
  self.flush_output()
1142
1109
  self.send_control_frame(
1143
1110
  L2CAP_Disconnection_Request(
@@ -1153,15 +1120,15 @@ class LeConnectionOrientedChannel(EventEmitter):
1153
1120
  return await self.disconnection_result
1154
1121
 
1155
1122
  def abort(self) -> None:
1156
- if self.state == self.CONNECTED:
1157
- self.change_state(self.DISCONNECTED)
1123
+ if self.state == self.State.CONNECTED:
1124
+ self._change_state(self.State.DISCONNECTED)
1158
1125
 
1159
1126
  def on_pdu(self, pdu: bytes) -> None:
1160
1127
  if self.sink is None:
1161
1128
  logger.warning('received pdu without a sink')
1162
1129
  return
1163
1130
 
1164
- if self.state != self.CONNECTED:
1131
+ if self.state != self.State.CONNECTED:
1165
1132
  logger.warning('received PDU while not connected, dropping')
1166
1133
 
1167
1134
  # Manage the peer credits
@@ -1240,7 +1207,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1240
1207
  self.credits = response.initial_credits
1241
1208
  self.connected = True
1242
1209
  self.connection_result.set_result(self)
1243
- self.change_state(self.CONNECTED)
1210
+ self._change_state(self.State.CONNECTED)
1244
1211
  else:
1245
1212
  self.connection_result.set_exception(
1246
1213
  ProtocolError(
@@ -1251,7 +1218,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1251
1218
  ),
1252
1219
  )
1253
1220
  )
1254
- self.change_state(self.CONNECTION_ERROR)
1221
+ self._change_state(self.State.CONNECTION_ERROR)
1255
1222
 
1256
1223
  # Cleanup
1257
1224
  self.connection_result = None
@@ -1271,11 +1238,11 @@ class LeConnectionOrientedChannel(EventEmitter):
1271
1238
  source_cid=request.source_cid,
1272
1239
  )
1273
1240
  )
1274
- self.change_state(self.DISCONNECTED)
1241
+ self._change_state(self.State.DISCONNECTED)
1275
1242
  self.flush_output()
1276
1243
 
1277
1244
  def on_disconnection_response(self, response) -> None:
1278
- if self.state != self.DISCONNECTING:
1245
+ if self.state != self.State.DISCONNECTING:
1279
1246
  logger.warning(color('invalid state', 'red'))
1280
1247
  return
1281
1248
 
@@ -1286,7 +1253,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1286
1253
  logger.warning('unexpected source or destination CID')
1287
1254
  return
1288
1255
 
1289
- self.change_state(self.DISCONNECTED)
1256
+ self._change_state(self.State.DISCONNECTED)
1290
1257
  if self.disconnection_result:
1291
1258
  self.disconnection_result.set_result(None)
1292
1259
  self.disconnection_result = None
@@ -1339,7 +1306,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1339
1306
  return
1340
1307
 
1341
1308
  def write(self, data: bytes) -> None:
1342
- if self.state != self.CONNECTED:
1309
+ if self.state != self.State.CONNECTED:
1343
1310
  logger.warning('not connected, dropping data')
1344
1311
  return
1345
1312
 
@@ -1367,7 +1334,7 @@ class LeConnectionOrientedChannel(EventEmitter):
1367
1334
  def __str__(self) -> str:
1368
1335
  return (
1369
1336
  f'CoC({self.source_cid}->{self.destination_cid}, '
1370
- f'State={self.state_name(self.state)}, '
1337
+ f'State={self.state.name}, '
1371
1338
  f'PSM={self.le_psm}, '
1372
1339
  f'MTU={self.mtu}/{self.peer_mtu}, '
1373
1340
  f'MPS={self.mps}/{self.peer_mps}, '
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import asyncio
16
+ import contextlib
16
17
  import grpc
17
18
  import logging
18
19
 
@@ -27,8 +28,8 @@ from bumble.core import (
27
28
  )
28
29
  from bumble.device import Connection as BumbleConnection, Device
29
30
  from bumble.hci import HCI_Error
31
+ from bumble.utils import EventWatcher
30
32
  from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
31
- from contextlib import suppress
32
33
  from google.protobuf import any_pb2 # pytype: disable=pyi-error
33
34
  from google.protobuf import empty_pb2 # pytype: disable=pyi-error
34
35
  from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
@@ -232,7 +233,11 @@ class SecurityService(SecurityServicer):
232
233
  sc=config.pairing_sc_enable,
233
234
  mitm=config.pairing_mitm_enable,
234
235
  bonding=config.pairing_bonding_enable,
235
- identity_address_type=config.identity_address_type,
236
+ identity_address_type=(
237
+ PairingConfig.AddressType.PUBLIC
238
+ if connection.self_address.is_public
239
+ else config.identity_address_type
240
+ ),
236
241
  delegate=PairingDelegate(
237
242
  connection,
238
243
  self,
@@ -294,23 +299,35 @@ class SecurityService(SecurityServicer):
294
299
  try:
295
300
  self.log.debug('Pair...')
296
301
 
297
- if (
298
- connection.transport == BT_LE_TRANSPORT
299
- and connection.role == BT_PERIPHERAL_ROLE
300
- ):
301
- wait_for_security: asyncio.Future[
302
- bool
303
- ] = asyncio.get_running_loop().create_future()
304
- connection.on("pairing", lambda *_: wait_for_security.set_result(True)) # type: ignore
305
- connection.on("pairing_failure", wait_for_security.set_exception)
302
+ security_result = asyncio.get_running_loop().create_future()
303
+
304
+ with contextlib.closing(EventWatcher()) as watcher:
305
+
306
+ @watcher.on(connection, 'pairing')
307
+ def on_pairing(*_: Any) -> None:
308
+ security_result.set_result('success')
306
309
 
307
- connection.request_pairing()
310
+ @watcher.on(connection, 'pairing_failure')
311
+ def on_pairing_failure(*_: Any) -> None:
312
+ security_result.set_result('pairing_failure')
308
313
 
309
- await wait_for_security
310
- else:
311
- await connection.pair()
314
+ @watcher.on(connection, 'disconnection')
315
+ def on_disconnection(*_: Any) -> None:
316
+ security_result.set_result('connection_died')
312
317
 
313
- self.log.debug('Paired')
318
+ if (
319
+ connection.transport == BT_LE_TRANSPORT
320
+ and connection.role == BT_PERIPHERAL_ROLE
321
+ ):
322
+ connection.request_pairing()
323
+ else:
324
+ await connection.pair()
325
+
326
+ result = await security_result
327
+
328
+ self.log.debug(f'Pairing session complete, status={result}')
329
+ if result != 'success':
330
+ return SecureResponse(**{result: empty_pb2.Empty()})
314
331
  except asyncio.CancelledError:
315
332
  self.log.warning("Connection died during encryption")
316
333
  return SecureResponse(connection_died=empty_pb2.Empty())
@@ -369,6 +386,7 @@ class SecurityService(SecurityServicer):
369
386
  str
370
387
  ] = asyncio.get_running_loop().create_future()
371
388
  authenticate_task: Optional[asyncio.Future[None]] = None
389
+ pair_task: Optional[asyncio.Future[None]] = None
372
390
 
373
391
  async def authenticate() -> None:
374
392
  assert connection
@@ -415,6 +433,10 @@ class SecurityService(SecurityServicer):
415
433
  if authenticate_task is None:
416
434
  authenticate_task = asyncio.create_task(authenticate())
417
435
 
436
+ def pair(*_: Any) -> None:
437
+ if self.need_pairing(connection, level):
438
+ pair_task = asyncio.create_task(connection.pair())
439
+
418
440
  listeners: Dict[str, Callable[..., None]] = {
419
441
  'disconnection': set_failure('connection_died'),
420
442
  'pairing_failure': set_failure('pairing_failure'),
@@ -425,6 +447,7 @@ class SecurityService(SecurityServicer):
425
447
  'connection_encryption_change': on_encryption_change,
426
448
  'classic_pairing': try_set_success,
427
449
  'classic_pairing_failure': set_failure('pairing_failure'),
450
+ 'security_request': pair,
428
451
  }
429
452
 
430
453
  # register event handlers
@@ -452,6 +475,15 @@ class SecurityService(SecurityServicer):
452
475
  pass
453
476
  self.log.debug('Authenticated')
454
477
 
478
+ # wait for `pair` to finish if any
479
+ if pair_task is not None:
480
+ self.log.debug('Wait for authentication...')
481
+ try:
482
+ await pair_task # type: ignore
483
+ except:
484
+ pass
485
+ self.log.debug('paired')
486
+
455
487
  return WaitSecurityResponse(**kwargs)
456
488
 
457
489
  def reached_security_level(
@@ -523,7 +555,7 @@ class SecurityStorageService(SecurityStorageServicer):
523
555
  self.log.debug(f"DeleteBond: {address}")
524
556
 
525
557
  if self.device.keystore is not None:
526
- with suppress(KeyError):
558
+ with contextlib.suppress(KeyError):
527
559
  await self.device.keystore.delete(str(address))
528
560
 
529
561
  return empty_pb2.Empty()
bumble/smp.py CHANGED
@@ -37,6 +37,7 @@ from typing import (
37
37
  Optional,
38
38
  Tuple,
39
39
  Type,
40
+ cast,
40
41
  )
41
42
 
42
43
  from pyee import EventEmitter
@@ -1771,7 +1772,26 @@ class Manager(EventEmitter):
1771
1772
  cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
1772
1773
  connection.send_l2cap_pdu(cid, command.to_bytes())
1773
1774
 
1775
+ def on_smp_security_request_command(
1776
+ self, connection: Connection, request: SMP_Security_Request_Command
1777
+ ) -> None:
1778
+ connection.emit('security_request', request.auth_req)
1779
+
1774
1780
  def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
1781
+ # Parse the L2CAP payload into an SMP Command object
1782
+ command = SMP_Command.from_bytes(pdu)
1783
+ logger.debug(
1784
+ f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
1785
+ f'{connection.peer_address}: {command}'
1786
+ )
1787
+
1788
+ # Security request is more than just pairing, so let applications handle them
1789
+ if command.code == SMP_SECURITY_REQUEST_COMMAND:
1790
+ self.on_smp_security_request_command(
1791
+ connection, cast(SMP_Security_Request_Command, command)
1792
+ )
1793
+ return
1794
+
1775
1795
  # Look for a session with this connection, and create one if none exists
1776
1796
  if not (session := self.sessions.get(connection.handle)):
1777
1797
  if connection.role == BT_CENTRAL_ROLE:
@@ -1782,13 +1802,6 @@ class Manager(EventEmitter):
1782
1802
  )
1783
1803
  self.sessions[connection.handle] = session
1784
1804
 
1785
- # Parse the L2CAP payload into an SMP Command object
1786
- command = SMP_Command.from_bytes(pdu)
1787
- logger.debug(
1788
- f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
1789
- f'{connection.peer_address}: {command}'
1790
- )
1791
-
1792
1805
  # Delegate the handling of the command to the session
1793
1806
  session.on_smp_command(command)
1794
1807
 
@@ -97,10 +97,13 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
97
97
  raise ValueError('invalid mode')
98
98
 
99
99
  # Create the transport object
100
- transport = PumpedTransport(
101
- PumpedPacketSource(hci_device.read),
102
- PumpedPacketSink(hci_device.write),
103
- channel.close,
100
+ class EmulatorTransport(PumpedTransport):
101
+ async def close(self):
102
+ await super().close()
103
+ await channel.close()
104
+
105
+ transport = EmulatorTransport(
106
+ PumpedPacketSource(hci_device.read), PumpedPacketSink(hci_device.write)
104
107
  )
105
108
  transport.start()
106
109