bumble 0.0.210__py3-none-any.whl → 0.0.212__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 (44) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +8 -4
  3. bumble/apps/console.py +2 -2
  4. bumble/apps/pair.py +185 -32
  5. bumble/att.py +13 -12
  6. bumble/avctp.py +2 -2
  7. bumble/avdtp.py +122 -68
  8. bumble/avrcp.py +11 -5
  9. bumble/core.py +13 -7
  10. bumble/{crypto.py → crypto/__init__.py} +11 -95
  11. bumble/crypto/builtin.py +652 -0
  12. bumble/crypto/cryptography.py +84 -0
  13. bumble/device.py +365 -185
  14. bumble/drivers/intel.py +3 -0
  15. bumble/gatt.py +3 -5
  16. bumble/gatt_client.py +5 -3
  17. bumble/gatt_server.py +8 -6
  18. bumble/hci.py +81 -4
  19. bumble/hfp.py +44 -20
  20. bumble/hid.py +24 -12
  21. bumble/host.py +24 -0
  22. bumble/keys.py +64 -48
  23. bumble/l2cap.py +19 -9
  24. bumble/pandora/host.py +11 -11
  25. bumble/pandora/l2cap.py +2 -2
  26. bumble/pandora/security.py +72 -56
  27. bumble/profiles/aics.py +3 -5
  28. bumble/profiles/ancs.py +3 -1
  29. bumble/profiles/ascs.py +11 -5
  30. bumble/profiles/asha.py +11 -6
  31. bumble/profiles/csip.py +1 -3
  32. bumble/profiles/gatt_service.py +1 -3
  33. bumble/profiles/hap.py +16 -33
  34. bumble/profiles/mcp.py +12 -9
  35. bumble/profiles/vcs.py +5 -5
  36. bumble/profiles/vocs.py +6 -9
  37. bumble/rfcomm.py +17 -8
  38. bumble/smp.py +14 -8
  39. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/METADATA +4 -4
  40. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/RECORD +44 -42
  41. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/WHEEL +1 -1
  42. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/entry_points.txt +0 -0
  43. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/licenses/LICENSE +0 -0
  44. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/top_level.txt +0 -0
bumble/drivers/intel.py CHANGED
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
50
50
 
51
51
  INTEL_USB_PRODUCTS = {
52
52
  (0x8087, 0x0032), # AX210
53
+ (0x8087, 0x0033), # AX211
53
54
  (0x8087, 0x0036), # BE200
54
55
  }
55
56
 
@@ -293,6 +294,7 @@ class HardwareVariant(utils.OpenIntEnum):
293
294
  # This is a just a partial list.
294
295
  # Add other constants here as new hardware is encountered and tested.
295
296
  TYPHOON_PEAK = 0x17
297
+ GARFIELD_PEAK = 0x19
296
298
  GALE_PEAK = 0x1C
297
299
 
298
300
 
@@ -471,6 +473,7 @@ class Driver(common.Driver):
471
473
  raise DriverError("hardware platform not supported")
472
474
  if hardware_info.variant not in (
473
475
  HardwareVariant.TYPHOON_PEAK,
476
+ HardwareVariant.GARFIELD_PEAK,
474
477
  HardwareVariant.GALE_PEAK,
475
478
  ):
476
479
  raise DriverError("hardware variant not supported")
bumble/gatt.py CHANGED
@@ -448,6 +448,8 @@ class Characteristic(Attribute[_T]):
448
448
  uuid: UUID
449
449
  properties: Characteristic.Properties
450
450
 
451
+ EVENT_SUBSCRIPTION = "subscription"
452
+
451
453
  class Properties(enum.IntFlag):
452
454
  """Property flags"""
453
455
 
@@ -577,11 +579,7 @@ class Descriptor(Attribute):
577
579
  if isinstance(self.value, bytes):
578
580
  value_str = self.value.hex()
579
581
  elif isinstance(self.value, CharacteristicValue):
580
- value = self.value.read(None)
581
- if isinstance(value, bytes):
582
- value_str = value.hex()
583
- else:
584
- value_str = '<async>'
582
+ value_str = '<dynamic>'
585
583
  else:
586
584
  value_str = '<...>'
587
585
  return (
bumble/gatt_client.py CHANGED
@@ -202,6 +202,8 @@ class CharacteristicProxy(AttributeProxy[_T]):
202
202
  descriptors: List[DescriptorProxy]
203
203
  subscribers: Dict[Any, Callable[[_T], Any]]
204
204
 
205
+ EVENT_UPDATE = "update"
206
+
205
207
  def __init__(
206
208
  self,
207
209
  client: Client,
@@ -308,7 +310,7 @@ class Client:
308
310
  self.services = []
309
311
  self.cached_values = {}
310
312
 
311
- connection.on('disconnection', self.on_disconnection)
313
+ connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
312
314
 
313
315
  def send_gatt_pdu(self, pdu: bytes) -> None:
314
316
  self.connection.send_l2cap_pdu(ATT_CID, pdu)
@@ -1142,7 +1144,7 @@ class Client:
1142
1144
  if callable(subscriber):
1143
1145
  subscriber(notification.attribute_value)
1144
1146
  else:
1145
- subscriber.emit('update', notification.attribute_value)
1147
+ subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
1146
1148
 
1147
1149
  def on_att_handle_value_indication(self, indication):
1148
1150
  # Call all subscribers
@@ -1157,7 +1159,7 @@ class Client:
1157
1159
  if callable(subscriber):
1158
1160
  subscriber(indication.attribute_value)
1159
1161
  else:
1160
- subscriber.emit('update', indication.attribute_value)
1162
+ subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
1161
1163
 
1162
1164
  # Confirm that we received the indication
1163
1165
  self.send_confirmation(ATT_Handle_Value_Confirmation())
bumble/gatt_server.py CHANGED
@@ -110,6 +110,8 @@ class Server(utils.EventEmitter):
110
110
  indication_semaphores: defaultdict[int, asyncio.Semaphore]
111
111
  pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
112
112
 
113
+ EVENT_CHARACTERISTIC_SUBSCRIPTION = "characteristic_subscription"
114
+
113
115
  def __init__(self, device: Device) -> None:
114
116
  super().__init__()
115
117
  self.device = device
@@ -313,11 +315,8 @@ class Server(utils.EventEmitter):
313
315
  self.add_service(service)
314
316
 
315
317
  def read_cccd(
316
- self, connection: Optional[Connection], characteristic: Characteristic
318
+ self, connection: Connection, characteristic: Characteristic
317
319
  ) -> bytes:
318
- if connection is None:
319
- return bytes([0, 0])
320
-
321
320
  subscribers = self.subscribers.get(connection.handle)
322
321
  cccd = None
323
322
  if subscribers:
@@ -347,10 +346,13 @@ class Server(utils.EventEmitter):
347
346
  notify_enabled = value[0] & 0x01 != 0
348
347
  indicate_enabled = value[0] & 0x02 != 0
349
348
  characteristic.emit(
350
- 'subscription', connection, notify_enabled, indicate_enabled
349
+ characteristic.EVENT_SUBSCRIPTION,
350
+ connection,
351
+ notify_enabled,
352
+ indicate_enabled,
351
353
  )
352
354
  self.emit(
353
- 'characteristic_subscription',
355
+ self.EVENT_CHARACTERISTIC_SUBSCRIPTION,
354
356
  connection,
355
357
  characteristic,
356
358
  notify_enabled,
bumble/hci.py CHANGED
@@ -29,13 +29,12 @@ from typing_extensions import Self
29
29
  from bumble import crypto
30
30
  from bumble.colors import color
31
31
  from bumble.core import (
32
- PhysicalTransport,
33
32
  AdvertisingData,
34
33
  DeviceClass,
35
34
  InvalidArgumentError,
36
35
  InvalidPacketError,
37
- ProtocolError,
38
36
  PhysicalTransport,
37
+ ProtocolError,
39
38
  bit_flags_to_strings,
40
39
  name_or_number,
41
40
  padded_bytes,
@@ -225,6 +224,7 @@ HCI_CONNECTIONLESS_PERIPHERAL_BROADCAST_CHANNEL_MAP_CHANGE_EVENT = 0X55
225
224
  HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
226
225
  HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
227
226
  HCI_SAM_STATUS_CHANGE_EVENT = 0X58
227
+ HCI_ENCRYPTION_CHANGE_V2_EVENT = 0x59
228
228
 
229
229
  HCI_VENDOR_EVENT = 0xFF
230
230
 
@@ -3364,6 +3364,20 @@ class HCI_Set_Event_Mask_Page_2_Command(HCI_Command):
3364
3364
  See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
3365
3365
  '''
3366
3366
 
3367
+ @staticmethod
3368
+ def mask(event_codes: Iterable[int]) -> bytes:
3369
+ '''
3370
+ Compute the event mask value for a list of events.
3371
+ '''
3372
+ # NOTE: this implementation takes advantage of the fact that as of version 6.0
3373
+ # of the core specification, the bit number for each event code is equal to 64
3374
+ # less than the event code.
3375
+ # If future versions of the specification deviate from that, a different
3376
+ # implementation would be needed.
3377
+ return sum((1 << event_code - 64) for event_code in event_codes).to_bytes(
3378
+ 8, 'little'
3379
+ )
3380
+
3367
3381
 
3368
3382
  # -----------------------------------------------------------------------------
3369
3383
  @HCI_Command.command(
@@ -5825,12 +5839,18 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
5825
5839
  return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
5826
5840
 
5827
5841
  def to_string(self, indentation='', _=None):
5842
+ def data_to_str(data):
5843
+ try:
5844
+ return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
5845
+ except Exception:
5846
+ return data.hex()
5847
+
5828
5848
  return super().to_string(
5829
5849
  indentation,
5830
5850
  {
5831
5851
  'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
5832
5852
  'address_type': Address.address_type_name,
5833
- 'data': lambda x: str(AdvertisingData.from_bytes(x)),
5853
+ 'data': data_to_str,
5834
5854
  },
5835
5855
  )
5836
5856
 
@@ -5965,6 +5985,33 @@ class HCI_LE_Enhanced_Connection_Complete_Event(HCI_LE_Meta_Event):
5965
5985
  '''
5966
5986
 
5967
5987
 
5988
+ # -----------------------------------------------------------------------------
5989
+ @HCI_LE_Meta_Event.event(
5990
+ [
5991
+ ('status', STATUS_SPEC),
5992
+ ('connection_handle', 2),
5993
+ (
5994
+ 'role',
5995
+ {'size': 1, 'mapper': lambda x: 'CENTRAL' if x == 0 else 'PERIPHERAL'},
5996
+ ),
5997
+ ('peer_address_type', Address.ADDRESS_TYPE_SPEC),
5998
+ ('peer_address', Address.parse_address_preceded_by_type),
5999
+ ('local_resolvable_private_address', Address.parse_random_address),
6000
+ ('peer_resolvable_private_address', Address.parse_random_address),
6001
+ ('connection_interval', 2),
6002
+ ('peripheral_latency', 2),
6003
+ ('supervision_timeout', 2),
6004
+ ('central_clock_accuracy', 1),
6005
+ ('advertising_handle', 1),
6006
+ ('sync_handle', 2),
6007
+ ]
6008
+ )
6009
+ class HCI_LE_Enhanced_Connection_Complete_V2_Event(HCI_LE_Meta_Event):
6010
+ '''
6011
+ See Bluetooth spec @ 7.7.65.10 LE Enhanced Connection Complete Event
6012
+ '''
6013
+
6014
+
5968
6015
  # -----------------------------------------------------------------------------
5969
6016
  @HCI_LE_Meta_Event.event(
5970
6017
  [
@@ -6055,12 +6102,18 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
6055
6102
 
6056
6103
  def to_string(self, indentation='', _=None):
6057
6104
  # pylint: disable=line-too-long
6105
+ def data_to_str(data):
6106
+ try:
6107
+ return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
6108
+ except Exception:
6109
+ return data.hex()
6110
+
6058
6111
  return super().to_string(
6059
6112
  indentation,
6060
6113
  {
6061
6114
  'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
6062
6115
  'address_type': Address.address_type_name,
6063
- 'data': lambda x: str(AdvertisingData.from_bytes(x)),
6116
+ 'data': data_to_str,
6064
6117
  },
6065
6118
  )
6066
6119
 
@@ -6938,6 +6991,30 @@ class HCI_Encryption_Change_Event(HCI_Event):
6938
6991
  )
6939
6992
 
6940
6993
 
6994
+ # -----------------------------------------------------------------------------
6995
+ @HCI_Event.event(
6996
+ [
6997
+ ('status', STATUS_SPEC),
6998
+ ('connection_handle', 2),
6999
+ (
7000
+ 'encryption_enabled',
7001
+ {
7002
+ 'size': 1,
7003
+ # pylint: disable-next=unnecessary-lambda
7004
+ 'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
7005
+ x
7006
+ ),
7007
+ },
7008
+ ),
7009
+ ('encryption_key_size', 1),
7010
+ ]
7011
+ )
7012
+ class HCI_Encryption_Change_V2_Event(HCI_Event):
7013
+ '''
7014
+ See Bluetooth spec @ 7.7.8 Encryption Change Event
7015
+ '''
7016
+
7017
+
6941
7018
  # -----------------------------------------------------------------------------
6942
7019
  @HCI_Event.event(
6943
7020
  [('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
bumble/hfp.py CHANGED
@@ -720,6 +720,14 @@ class HfProtocol(utils.EventEmitter):
720
720
  vrec: VoiceRecognitionState
721
721
  """
722
722
 
723
+ EVENT_CODEC_NEGOTIATION = "codec_negotiation"
724
+ EVENT_AG_INDICATOR = "ag_indicator"
725
+ EVENT_SPEAKER_VOLUME = "speaker_volume"
726
+ EVENT_MICROPHONE_VOLUME = "microphone_volume"
727
+ EVENT_RING = "ring"
728
+ EVENT_CLI_NOTIFICATION = "cli_notification"
729
+ EVENT_VOICE_RECOGNITION = "voice_recognition"
730
+
723
731
  class HfLoopTermination(HfpProtocolError):
724
732
  """Termination signal for run() loop."""
725
733
 
@@ -777,7 +785,8 @@ class HfProtocol(utils.EventEmitter):
777
785
  self.dlc.sink = self._read_at
778
786
  # Stop the run() loop when L2CAP is closed.
779
787
  self.dlc.multiplexer.l2cap_channel.on(
780
- 'close', lambda: self.unsolicited_queue.put_nowait(None)
788
+ self.dlc.multiplexer.l2cap_channel.EVENT_CLOSE,
789
+ lambda: self.unsolicited_queue.put_nowait(None),
781
790
  )
782
791
 
783
792
  def supports_hf_feature(self, feature: HfFeature) -> bool:
@@ -1034,7 +1043,7 @@ class HfProtocol(utils.EventEmitter):
1034
1043
  # ID. The HF shall be ready to accept the synchronous connection
1035
1044
  # establishment as soon as it has sent the AT commands AT+BCS=<Codec ID>.
1036
1045
  self.active_codec = AudioCodec(codec_id)
1037
- self.emit('codec_negotiation', self.active_codec)
1046
+ self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
1038
1047
 
1039
1048
  logger.info("codec connection setup completed")
1040
1049
 
@@ -1095,7 +1104,7 @@ class HfProtocol(utils.EventEmitter):
1095
1104
  # CIEV is in 1-index, while ag_indicators is in 0-index.
1096
1105
  ag_indicator = self.ag_indicators[index - 1]
1097
1106
  ag_indicator.current_status = value
1098
- self.emit('ag_indicator', ag_indicator)
1107
+ self.emit(self.EVENT_AG_INDICATOR, ag_indicator)
1099
1108
  logger.info(f"AG indicator updated: {ag_indicator.indicator}, {value}")
1100
1109
 
1101
1110
  async def handle_unsolicited(self):
@@ -1110,19 +1119,21 @@ class HfProtocol(utils.EventEmitter):
1110
1119
  int(result.parameters[0]), int(result.parameters[1])
1111
1120
  )
1112
1121
  elif result.code == "+VGS":
1113
- self.emit('speaker_volume', int(result.parameters[0]))
1122
+ self.emit(self.EVENT_SPEAKER_VOLUME, int(result.parameters[0]))
1114
1123
  elif result.code == "+VGM":
1115
- self.emit('microphone_volume', int(result.parameters[0]))
1124
+ self.emit(self.EVENT_MICROPHONE_VOLUME, int(result.parameters[0]))
1116
1125
  elif result.code == "RING":
1117
- self.emit('ring')
1126
+ self.emit(self.EVENT_RING)
1118
1127
  elif result.code == "+CLIP":
1119
1128
  self.emit(
1120
- 'cli_notification', CallLineIdentification.parse_from(result.parameters)
1129
+ self.EVENT_CLI_NOTIFICATION,
1130
+ CallLineIdentification.parse_from(result.parameters),
1121
1131
  )
1122
1132
  elif result.code == "+BVRA":
1123
1133
  # TODO: Support Enhanced Voice Recognition.
1124
1134
  self.emit(
1125
- 'voice_recognition', VoiceRecognitionState(int(result.parameters[0]))
1135
+ self.EVENT_VOICE_RECOGNITION,
1136
+ VoiceRecognitionState(int(result.parameters[0])),
1126
1137
  )
1127
1138
  else:
1128
1139
  logging.info(f"unhandled unsolicited response {result.code}")
@@ -1179,6 +1190,19 @@ class AgProtocol(utils.EventEmitter):
1179
1190
  volume: Int
1180
1191
  """
1181
1192
 
1193
+ EVENT_SLC_COMPLETE = "slc_complete"
1194
+ EVENT_SUPPORTED_AUDIO_CODECS = "supported_audio_codecs"
1195
+ EVENT_CODEC_NEGOTIATION = "codec_negotiation"
1196
+ EVENT_VOICE_RECOGNITION = "voice_recognition"
1197
+ EVENT_CALL_HOLD = "call_hold"
1198
+ EVENT_HF_INDICATOR = "hf_indicator"
1199
+ EVENT_CODEC_CONNECTION_REQUEST = "codec_connection_request"
1200
+ EVENT_ANSWER = "answer"
1201
+ EVENT_DIAL = "dial"
1202
+ EVENT_HANG_UP = "hang_up"
1203
+ EVENT_SPEAKER_VOLUME = "speaker_volume"
1204
+ EVENT_MICROPHONE_VOLUME = "microphone_volume"
1205
+
1182
1206
  supported_hf_features: int
1183
1207
  supported_hf_indicators: Set[HfIndicator]
1184
1208
  supported_audio_codecs: List[AudioCodec]
@@ -1371,7 +1395,7 @@ class AgProtocol(utils.EventEmitter):
1371
1395
 
1372
1396
  def _check_remained_slc_commands(self) -> None:
1373
1397
  if not self._remained_slc_setup_features:
1374
- self.emit('slc_complete')
1398
+ self.emit(self.EVENT_SLC_COMPLETE)
1375
1399
 
1376
1400
  def _on_brsf(self, hf_features: bytes) -> None:
1377
1401
  self.supported_hf_features = int(hf_features)
@@ -1390,17 +1414,17 @@ class AgProtocol(utils.EventEmitter):
1390
1414
 
1391
1415
  def _on_bac(self, *args) -> None:
1392
1416
  self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
1393
- self.emit('supported_audio_codecs', self.supported_audio_codecs)
1417
+ self.emit(self.EVENT_SUPPORTED_AUDIO_CODECS, self.supported_audio_codecs)
1394
1418
  self.send_ok()
1395
1419
 
1396
1420
  def _on_bcs(self, codec: bytes) -> None:
1397
1421
  self.active_codec = AudioCodec(int(codec))
1398
1422
  self.send_ok()
1399
- self.emit('codec_negotiation', self.active_codec)
1423
+ self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
1400
1424
 
1401
1425
  def _on_bvra(self, vrec: bytes) -> None:
1402
1426
  self.send_ok()
1403
- self.emit('voice_recognition', VoiceRecognitionState(int(vrec)))
1427
+ self.emit(self.EVENT_VOICE_RECOGNITION, VoiceRecognitionState(int(vrec)))
1404
1428
 
1405
1429
  def _on_chld(self, operation_code: bytes) -> None:
1406
1430
  call_index: Optional[int] = None
@@ -1427,7 +1451,7 @@ class AgProtocol(utils.EventEmitter):
1427
1451
  # Real three-way calls have more complicated situations, but this is not a popular issue - let users to handle the remaining :)
1428
1452
 
1429
1453
  self.send_ok()
1430
- self.emit('call_hold', operation, call_index)
1454
+ self.emit(self.EVENT_CALL_HOLD, operation, call_index)
1431
1455
 
1432
1456
  def _on_chld_test(self) -> None:
1433
1457
  if not self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
@@ -1553,7 +1577,7 @@ class AgProtocol(utils.EventEmitter):
1553
1577
  return
1554
1578
 
1555
1579
  self.hf_indicators[index].current_status = int(value_bytes)
1556
- self.emit('hf_indicator', self.hf_indicators[index])
1580
+ self.emit(self.EVENT_HF_INDICATOR, self.hf_indicators[index])
1557
1581
  self.send_ok()
1558
1582
 
1559
1583
  def _on_bia(self, *args) -> None:
@@ -1562,21 +1586,21 @@ class AgProtocol(utils.EventEmitter):
1562
1586
  self.send_ok()
1563
1587
 
1564
1588
  def _on_bcc(self) -> None:
1565
- self.emit('codec_connection_request')
1589
+ self.emit(self.EVENT_CODEC_CONNECTION_REQUEST)
1566
1590
  self.send_ok()
1567
1591
 
1568
1592
  def _on_a(self) -> None:
1569
1593
  """ATA handler."""
1570
- self.emit('answer')
1594
+ self.emit(self.EVENT_ANSWER)
1571
1595
  self.send_ok()
1572
1596
 
1573
1597
  def _on_d(self, number: bytes) -> None:
1574
1598
  """ATD handler."""
1575
- self.emit('dial', number.decode())
1599
+ self.emit(self.EVENT_DIAL, number.decode())
1576
1600
  self.send_ok()
1577
1601
 
1578
1602
  def _on_chup(self) -> None:
1579
- self.emit('hang_up')
1603
+ self.emit(self.EVENT_HANG_UP)
1580
1604
  self.send_ok()
1581
1605
 
1582
1606
  def _on_clcc(self) -> None:
@@ -1602,11 +1626,11 @@ class AgProtocol(utils.EventEmitter):
1602
1626
  self.send_ok()
1603
1627
 
1604
1628
  def _on_vgs(self, level: bytes) -> None:
1605
- self.emit('speaker_volume', int(level))
1629
+ self.emit(self.EVENT_SPEAKER_VOLUME, int(level))
1606
1630
  self.send_ok()
1607
1631
 
1608
1632
  def _on_vgm(self, level: bytes) -> None:
1609
- self.emit('microphone_volume', int(level))
1633
+ self.emit(self.EVENT_MICROPHONE_VOLUME, int(level))
1610
1634
  self.send_ok()
1611
1635
 
1612
1636
 
bumble/hid.py CHANGED
@@ -201,6 +201,13 @@ class HID(ABC, utils.EventEmitter):
201
201
  l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
202
202
  connection: Optional[device.Connection] = None
203
203
 
204
+ EVENT_INTERRUPT_DATA = "interrupt_data"
205
+ EVENT_CONTROL_DATA = "control_data"
206
+ EVENT_SUSPEND = "suspend"
207
+ EVENT_EXIT_SUSPEND = "exit_suspend"
208
+ EVENT_VIRTUAL_CABLE_UNPLUG = "virtual_cable_unplug"
209
+ EVENT_HANDSHAKE = "handshake"
210
+
204
211
  class Role(enum.IntEnum):
205
212
  HOST = 0x00
206
213
  DEVICE = 0x01
@@ -215,7 +222,7 @@ class HID(ABC, utils.EventEmitter):
215
222
  device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
216
223
  device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
217
224
 
218
- device.on('connection', self.on_device_connection)
225
+ device.on(device.EVENT_CONNECTION, self.on_device_connection)
219
226
 
220
227
  async def connect_control_channel(self) -> None:
221
228
  # Create a new L2CAP connection - control channel
@@ -258,15 +265,20 @@ class HID(ABC, utils.EventEmitter):
258
265
  def on_device_connection(self, connection: device.Connection) -> None:
259
266
  self.connection = connection
260
267
  self.remote_device_bd_address = connection.peer_address
261
- connection.on('disconnection', self.on_device_disconnection)
268
+ connection.on(connection.EVENT_DISCONNECTION, self.on_device_disconnection)
262
269
 
263
270
  def on_device_disconnection(self, reason: int) -> None:
264
271
  self.connection = None
265
272
 
266
273
  def on_l2cap_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
267
274
  logger.debug(f'+++ New L2CAP connection: {l2cap_channel}')
268
- l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
269
- l2cap_channel.on('close', lambda: self.on_l2cap_channel_close(l2cap_channel))
275
+ l2cap_channel.on(
276
+ l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
277
+ )
278
+ l2cap_channel.on(
279
+ l2cap_channel.EVENT_CLOSE,
280
+ lambda: self.on_l2cap_channel_close(l2cap_channel),
281
+ )
270
282
 
271
283
  def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
272
284
  if l2cap_channel.psm == HID_CONTROL_PSM:
@@ -290,7 +302,7 @@ class HID(ABC, utils.EventEmitter):
290
302
 
291
303
  def on_intr_pdu(self, pdu: bytes) -> None:
292
304
  logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}')
293
- self.emit("interrupt_data", pdu)
305
+ self.emit(self.EVENT_INTERRUPT_DATA, pdu)
294
306
 
295
307
  def send_pdu_on_ctrl(self, msg: bytes) -> None:
296
308
  assert self.l2cap_ctrl_channel
@@ -363,17 +375,17 @@ class Device(HID):
363
375
  self.handle_set_protocol(pdu)
364
376
  elif message_type == Message.MessageType.DATA:
365
377
  logger.debug('<<< HID CONTROL DATA')
366
- self.emit('control_data', pdu)
378
+ self.emit(self.EVENT_CONTROL_DATA, pdu)
367
379
  elif message_type == Message.MessageType.CONTROL:
368
380
  if param == Message.ControlCommand.SUSPEND:
369
381
  logger.debug('<<< HID SUSPEND')
370
- self.emit('suspend')
382
+ self.emit(self.EVENT_SUSPEND)
371
383
  elif param == Message.ControlCommand.EXIT_SUSPEND:
372
384
  logger.debug('<<< HID EXIT SUSPEND')
373
- self.emit('exit_suspend')
385
+ self.emit(self.EVENT_EXIT_SUSPEND)
374
386
  elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
375
387
  logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
376
- self.emit('virtual_cable_unplug')
388
+ self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
377
389
  else:
378
390
  logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
379
391
  else:
@@ -538,14 +550,14 @@ class Host(HID):
538
550
  message_type = pdu[0] >> 4
539
551
  if message_type == Message.MessageType.HANDSHAKE:
540
552
  logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}')
541
- self.emit('handshake', Message.Handshake(param))
553
+ self.emit(self.EVENT_HANDSHAKE, Message.Handshake(param))
542
554
  elif message_type == Message.MessageType.DATA:
543
555
  logger.debug('<<< HID CONTROL DATA')
544
- self.emit('control_data', pdu)
556
+ self.emit(self.EVENT_CONTROL_DATA, pdu)
545
557
  elif message_type == Message.MessageType.CONTROL:
546
558
  if param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
547
559
  logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
548
- self.emit('virtual_cable_unplug')
560
+ self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
549
561
  else:
550
562
  logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
551
563
  else:
bumble/host.py CHANGED
@@ -435,6 +435,14 @@ class Host(utils.EventEmitter):
435
435
  )
436
436
  )
437
437
  )
438
+ if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
439
+ await self.send_command(
440
+ hci.HCI_Set_Event_Mask_Page_2_Command(
441
+ event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
442
+ [hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
443
+ )
444
+ )
445
+ )
438
446
 
439
447
  if (
440
448
  self.local_version is not None
@@ -456,6 +464,7 @@ class Host(utils.EventEmitter):
456
464
  hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
457
465
  hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
458
466
  hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
467
+ hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
459
468
  hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
460
469
  hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
461
470
  hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
@@ -1383,6 +1392,21 @@ class Host(utils.EventEmitter):
1383
1392
  'connection_encryption_change',
1384
1393
  event.connection_handle,
1385
1394
  event.encryption_enabled,
1395
+ 0,
1396
+ )
1397
+ else:
1398
+ self.emit(
1399
+ 'connection_encryption_failure', event.connection_handle, event.status
1400
+ )
1401
+
1402
+ def on_hci_encryption_change_v2_event(self, event):
1403
+ # Notify the client
1404
+ if event.status == hci.HCI_SUCCESS:
1405
+ self.emit(
1406
+ 'connection_encryption_change',
1407
+ event.connection_handle,
1408
+ event.encryption_enabled,
1409
+ event.encryption_key_size,
1386
1410
  )
1387
1411
  else:
1388
1412
  self.emit(