From 57fbad6fa44dd21995a43c3e9b606701bd7db632 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 11:31:15 -0700 Subject: [PATCH 1/6] add LE advertisement and HR service --- apps/pair.py | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/apps/pair.py b/apps/pair.py index 5ed7679..1f303a0 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -36,6 +36,8 @@ from bumble.core import ( from bumble.gatt import ( GATT_DEVICE_NAME_CHARACTERISTIC, GATT_GENERIC_ACCESS_SERVICE, + GATT_HEART_RATE_SERVICE, + GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC, Service, Characteristic, CharacteristicValue, @@ -225,15 +227,6 @@ def read_with_error(connection): raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR) -def write_with_error(connection, _value): - if not connection.is_encrypted: - raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR) - - if not AUTHENTICATION_ERROR_RETURNED[1]: - AUTHENTICATION_ERROR_RETURNED[1] = True - raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR) - - # ----------------------------------------------------------------------------- def on_connection(connection, request): print(color(f'<<< Connection: {connection}', 'green')) @@ -332,16 +325,13 @@ async def pair( device.le_enabled = True device.add_service( Service( - '50DB505C-8AC4-4738-8448-3B1D9CC09CC5', + GATT_HEART_RATE_SERVICE, [ Characteristic( - '552957FB-CF1F-4A31-9535-E78847E1A714', - Characteristic.Properties.READ - | Characteristic.Properties.WRITE, - Characteristic.READABLE | Characteristic.WRITEABLE, - CharacteristicValue( - read=read_with_error, write=write_with_error - ), + GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC, + Characteristic.Properties.READ, + Characteristic.READ_REQUIRES_AUTHENTICATION, + bytes(1), ) ], ) @@ -437,7 +427,27 @@ async def pair( else: if mode == 'le': - # Advertise so that peers can find us and connect + # Advertise so that peers can find us and connect. + # Include the heart rate service UUID in the advertisement data + # so that devices like iPhones can show this device in their + # Bluetooth selector. + device.advertising_data = bytes( + AdvertisingData( + [ + ( + AdvertisingData.FLAGS, + bytes( + [AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG] + ), + ), + (AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()), + ( + AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, + bytes(GATT_HEART_RATE_SERVICE), + ), + ] + ) + ) await device.start_advertising(auto_restart=True) else: # Become discoverable and connectable From 088bcbed0b69164e1ee48c451738d8f56457c0e8 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 11:31:15 -0700 Subject: [PATCH 2/6] resolve merge conflicts --- apps/pair.py | 177 ++++++++++++++++++++++++++++++++------ bumble/core.py | 10 ++- bumble/device.py | 25 +++++- bumble/hci.py | 39 +++++++++ bumble/host.py | 23 +++++ examples/run_a2dp_sink.py | 6 -- 6 files changed, 244 insertions(+), 36 deletions(-) diff --git a/apps/pair.py b/apps/pair.py index 1f303a0..284aec1 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -18,9 +18,12 @@ import asyncio import os import logging +import struct + import click from prompt_toolkit.shortcuts import PromptSession +from bumble.a2dp import make_audio_sink_service_sdp_records from bumble.colors import color from bumble.device import Device, Peer from bumble.transport import open_transport_or_link @@ -30,8 +33,10 @@ from bumble.smp import error_name as smp_error_name from bumble.keys import JsonKeyStore from bumble.core import ( AdvertisingData, + Appearance, ProtocolError, PhysicalTransport, + UUID, ) from bumble.gatt import ( GATT_DEVICE_NAME_CHARACTERISTIC, @@ -40,8 +45,8 @@ from bumble.gatt import ( GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC, Service, Characteristic, - CharacteristicValue, ) +from bumble.hci import OwnAddressType from bumble.att import ( ATT_Error, ATT_INSUFFICIENT_AUTHENTICATION_ERROR, @@ -195,7 +200,7 @@ class Delegate(PairingDelegate): # ----------------------------------------------------------------------------- async def get_peer_name(peer, mode): - if mode == 'classic': + if peer.connection.transport == PhysicalTransport.BR_EDR: return await peer.request_name() # Try to get the peer name from GATT @@ -227,6 +232,16 @@ def read_with_error(connection): raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR) +# ----------------------------------------------------------------------------- +def sdp_records(): + service_record_handle = 0x00010001 + return { + service_record_handle: make_audio_sink_service_sdp_records( + service_record_handle + ) + } + + # ----------------------------------------------------------------------------- def on_connection(connection, request): print(color(f'<<< Connection: {connection}', 'green')) @@ -298,6 +313,7 @@ async def pair( mitm, bond, ctkd, + advertising_address, identity_address, linger, io, @@ -306,6 +322,8 @@ async def pair( request, print_keys, keystore_file, + advertise_service_uuids, + advertise_appearance, device_config, hci_transport, address_or_name, @@ -321,8 +339,7 @@ async def pair( # Expose a GATT characteristic that can be used to trigger pairing by # responding with an authentication error when read - if mode == 'le': - device.le_enabled = True + if mode in ('le', 'dual'): device.add_service( Service( GATT_HEART_RATE_SERVICE, @@ -337,10 +354,18 @@ async def pair( ) ) - # Select LE or Classic - if mode == 'classic': + # LE and Classic support + if mode in ('classic', 'dual'): device.classic_enabled = True device.classic_smp_enabled = ctkd + if mode in ('le', 'dual'): + device.le_enabled = True + if mode == 'dual': + device.le_simultaneous_enabled = True + + # Setup SDP + if mode in ('classic', 'dual'): + device.sdp_service_records = sdp_records() # Get things going await device.power_on() @@ -426,33 +451,109 @@ async def pair( print(color(f'Pairing failed: {error}', 'red')) else: - if mode == 'le': + if mode in ('le', 'dual'): # Advertise so that peers can find us and connect. # Include the heart rate service UUID in the advertisement data # so that devices like iPhones can show this device in their # Bluetooth selector. - device.advertising_data = bytes( - AdvertisingData( - [ - ( - AdvertisingData.FLAGS, - bytes( - [AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG] - ), - ), - (AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()), - ( - AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, - bytes(GATT_HEART_RATE_SERVICE), - ), - ] + service_uuids_16 = [] + service_uuids_32 = [] + service_uuids_128 = [] + if advertise_service_uuids: + for uuid in advertise_service_uuids: + uuid = uuid.replace("-", "") + if len(uuid) == 4: + service_uuids_16.append(UUID(uuid)) + elif len(uuid) == 8: + service_uuids_32.append(UUID(uuid)) + elif len(uuid) == 32: + service_uuids_128.append(UUID(uuid)) + else: + print(color('Invalid UUID format', 'red')) + return + else: + service_uuids_16.append(GATT_HEART_RATE_SERVICE) + + flags = AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE + if mode == 'le': + flags |= AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED + if mode == 'dual': + flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE + + ad_structs = [ + ( + AdvertisingData.FLAGS, + bytes([flags]), + ), + (AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()), + ] + if service_uuids_16: + ad_structs.append( + ( + AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, + b"".join(bytes(uuid) for uuid in service_uuids_16), + ) ) + if service_uuids_32: + ad_structs.append( + ( + AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS, + b"".join(bytes(uuid) for uuid in service_uuids_32), + ) + ) + if service_uuids_128: + ad_structs.append( + ( + AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + b"".join(bytes(uuid) for uuid in service_uuids_128), + ) + ) + + if advertise_appearance: + advertise_appearance = advertise_appearance.upper() + try: + advertise_appearance_int = int(advertise_appearance) + except ValueError: + category, subcategory = advertise_appearance.split('/') + try: + category_enum = Appearance.Category[category] + except ValueError: + print( + color(f'Invalid appearance category {category}', 'red') + ) + return + subcategory_class = Appearance.SUBCATEGORY_CLASSES[ + category_enum + ] + try: + subcategory_enum = subcategory_class[subcategory] + except ValueError: + print(color(f'Invalid subcategory {subcategory}', 'red')) + return + advertise_appearance_int = int( + Appearance(category_enum, subcategory_enum) + ) + ad_structs.append( + ( + AdvertisingData.APPEARANCE, + struct.pack('>>>>>> fdf90c6 (add LE advertisement and HR service) ) # [Classic only] @@ -5950,13 +5967,17 @@ class Device(utils.CompositeEventEmitter): @host_event_handler @with_connection_from_handle - def on_connection_encryption_change(self, connection, encryption): + def on_connection_encryption_change( + self, connection, encryption, encryption_key_size + ): logger.debug( f'*** Connection Encryption Change: [0x{connection.handle:04X}] ' f'{connection.peer_address} as {connection.role_name}, ' - f'encryption={encryption}' + f'encryption={encryption}, ' + f'key_size={encryption_key_size}' ) connection.encryption = encryption + connection.encryption_key_size = encryption_key_size if ( not connection.authenticated and connection.transport == PhysicalTransport.BR_EDR diff --git a/bumble/hci.py b/bumble/hci.py index 2ab46e4..6943c1c 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -225,6 +225,7 @@ HCI_CONNECTIONLESS_PERIPHERAL_BROADCAST_CHANNEL_MAP_CHANGE_EVENT = 0X55 HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56 HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57 HCI_SAM_STATUS_CHANGE_EVENT = 0X58 +HCI_ENCRYPTION_CHANGE_V2_EVENT = 0x59 HCI_VENDOR_EVENT = 0xFF @@ -3364,6 +3365,20 @@ class HCI_Set_Event_Mask_Page_2_Command(HCI_Command): See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command ''' + @staticmethod + def mask(event_codes: Iterable[int]) -> bytes: + ''' + Compute the event mask value for a list of events. + ''' + # NOTE: this implementation takes advantage of the fact that as of version 6.0 + # of the core specification, the bit number for each event code is equal to 64 + # less than the event code. + # If future versions of the specification deviate from that, a different + # implementation would be needed. + return sum((1 << event_code - 64) for event_code in event_codes).to_bytes( + 8, 'little' + ) + # ----------------------------------------------------------------------------- @HCI_Command.command( @@ -6977,6 +6992,30 @@ class HCI_Encryption_Change_Event(HCI_Event): ) +# ----------------------------------------------------------------------------- +@HCI_Event.event( + [ + ('status', STATUS_SPEC), + ('connection_handle', 2), + ( + 'encryption_enabled', + { + 'size': 1, + # pylint: disable-next=unnecessary-lambda + 'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name( + x + ), + }, + ), + ('encryption_key_size', 1), + ] +) +class HCI_Encryption_Change_V2_Event(HCI_Event): + ''' + See Bluetooth spec @ 7.7.8 Encryption Change Event + ''' + + # ----------------------------------------------------------------------------- @HCI_Event.event( [('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)] diff --git a/bumble/host.py b/bumble/host.py index 183c5a7..755732a 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -435,6 +435,14 @@ class Host(utils.EventEmitter): ) ) ) + if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND): + await self.send_command( + hci.HCI_Set_Event_Mask_Page_2_Command( + event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask( + [hci.HCI_ENCRYPTION_CHANGE_V2_EVENT] + ) + ) + ) if ( self.local_version is not None @@ -1384,6 +1392,21 @@ class Host(utils.EventEmitter): 'connection_encryption_change', event.connection_handle, event.encryption_enabled, + 0, + ) + else: + self.emit( + 'connection_encryption_failure', event.connection_handle, event.status + ) + + def on_hci_encryption_change_v2_event(self, event): + # Notify the client + if event.status == hci.HCI_SUCCESS: + self.emit( + 'connection_encryption_change', + event.connection_handle, + event.encryption_enabled, + event.encryption_key_size, ) else: self.emit( diff --git a/examples/run_a2dp_sink.py b/examples/run_a2dp_sink.py index a3bcb29..f5d337c 100644 --- a/examples/run_a2dp_sink.py +++ b/examples/run_a2dp_sink.py @@ -33,12 +33,6 @@ from bumble.avdtp import ( from bumble.a2dp import ( make_audio_sink_service_sdp_records, A2DP_SBC_CODEC_TYPE, - SBC_MONO_CHANNEL_MODE, - SBC_DUAL_CHANNEL_MODE, - SBC_SNR_ALLOCATION_METHOD, - SBC_LOUDNESS_ALLOCATION_METHOD, - SBC_STEREO_CHANNEL_MODE, - SBC_JOINT_STEREO_CHANNEL_MODE, SbcMediaCodecInformation, ) From 9f1e95d87f14d90f5ee27d14940ccb34ca649b61 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 11:31:15 -0700 Subject: [PATCH 3/6] more merge fixes --- bumble/device.py | 69 ++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index 8c1d477..c5f8e09 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1811,7 +1811,7 @@ class Connection(utils.CompositeEventEmitter): try: await asyncio.wait_for( - utils.cancel_on_event(self.device, 'flush', abort), timeout + utils.cancel_on_event(self.device, Device.EVENT_FLUSH, abort), timeout ) finally: self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result) @@ -3758,7 +3758,9 @@ class Device(utils.CompositeEventEmitter): self.le_connecting = True if timeout is None: - return await utils.cancel_on_event(self, 'flush', pending_connection) + return await utils.cancel_on_event( + self, Device.EVENT_FLUSH, pending_connection + ) try: return await asyncio.wait_for( @@ -3776,7 +3778,7 @@ class Device(utils.CompositeEventEmitter): try: return await utils.cancel_on_event( - self, 'flush', pending_connection + self, Device.EVENT_FLUSH, pending_connection ) except core.ConnectionError as error: raise core.TimeoutError() from error @@ -3833,7 +3835,9 @@ class Device(utils.CompositeEventEmitter): try: # Wait for a request or a completed connection - pending_request = utils.cancel_on_event(self, 'flush', pending_request_fut) + pending_request = utils.cancel_on_event( + self, Device.EVENT_FLUSH, pending_request_fut + ) result = await ( asyncio.wait_for(pending_request, timeout) if timeout @@ -3895,7 +3899,9 @@ class Device(utils.CompositeEventEmitter): ) # Wait for connection complete - return await utils.cancel_on_event(self, 'flush', pending_connection) + return await utils.cancel_on_event( + self, Device.EVENT_FLUSH, pending_connection + ) finally: self.remove_listener(self.EVENT_CONNECTION, on_connection) @@ -3971,7 +3977,9 @@ class Device(utils.CompositeEventEmitter): # Wait for the disconnection process to complete self.disconnecting = True - return await utils.cancel_on_event(self, 'flush', pending_disconnection) + return await utils.cancel_on_event( + self, Device.EVENT_FLUSH, pending_disconnection + ) finally: connection.remove_listener( connection.EVENT_DISCONNECTION, pending_disconnection.set_result @@ -4195,7 +4203,7 @@ class Device(utils.CompositeEventEmitter): else: return None - return await utils.cancel_on_event(self, 'flush', peer_address) + return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address) finally: if listener is not None: self.remove_listener(event_name, listener) @@ -4245,7 +4253,7 @@ class Device(utils.CompositeEventEmitter): if not self.scanning: await self.start_scanning(filter_duplicates=True) - return await utils.cancel_on_event(self, 'flush', peer_address) + return await utils.cancel_on_event(self, Device.EVENT_FLUSH, peer_address) finally: if listener is not None: self.remove_listener(event_name, listener) @@ -4353,7 +4361,7 @@ class Device(utils.CompositeEventEmitter): # Wait for the authentication to complete await utils.cancel_on_event( - connection, 'disconnection', pending_authentication + connection, Connection.EVENT_DISCONNECTION, pending_authentication ) finally: connection.remove_listener( @@ -4441,7 +4449,9 @@ class Device(utils.CompositeEventEmitter): raise hci.HCI_StatusError(result) # Wait for the result - await utils.cancel_on_event(connection, 'disconnection', pending_encryption) + await utils.cancel_on_event( + connection, Connection.EVENT_DISCONNECTION, pending_encryption + ) finally: connection.remove_listener( connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change @@ -4486,7 +4496,7 @@ class Device(utils.CompositeEventEmitter): ) raise hci.HCI_StatusError(result) await utils.cancel_on_event( - connection, 'disconnection', pending_role_change + connection, Connection.EVENT_DISCONNECTION, pending_role_change ) finally: connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change) @@ -4538,7 +4548,7 @@ class Device(utils.CompositeEventEmitter): raise hci.HCI_StatusError(result) # Wait for the result - return await utils.cancel_on_event(self, 'flush', pending_name) + return await utils.cancel_on_event(self, Device.EVENT_FLUSH, pending_name) finally: self.remove_listener(self.EVENT_REMOTE_NAME, handler) self.remove_listener(self.EVENT_REMOTE_NAME_FAILURE, failure_handler) @@ -5080,7 +5090,7 @@ class Device(utils.CompositeEventEmitter): ) utils.cancel_on_event( - self, 'flush', self.update_keys(str(bd_addr), pairing_keys) + self, Device.EVENT_FLUSH, self.update_keys(str(bd_addr), pairing_keys) ) if connection := self.find_connection_by_bd_addr( @@ -5350,8 +5360,10 @@ class Device(utils.CompositeEventEmitter): # Setup auto-restart of the advertising set if needed. if advertising_set.auto_restart: connection.once( - 'disconnection', - lambda _: utils.cancel_on_event(self, 'flush', advertising_set.start()), + Connection.EVENT_DISCONNECTION, + lambda _: utils.cancel_on_event( + self, Device.EVENT_FLUSH, advertising_set.start() + ), ) self.emit(self.EVENT_CONNECTION, connection) @@ -5465,8 +5477,10 @@ class Device(utils.CompositeEventEmitter): if self.legacy_advertiser.auto_restart: advertiser = self.legacy_advertiser connection.once( - 'disconnection', - lambda _: utils.cancel_on_event(self, 'flush', advertiser.start()), + Connection.EVENT_DISCONNECTION, + lambda _: utils.cancel_on_event( + self, Device.EVENT_FLUSH, advertiser.start() + ), ) else: self.legacy_advertiser = None @@ -5725,7 +5739,9 @@ class Device(utils.CompositeEventEmitter): async def reply() -> None: try: - if await utils.cancel_on_event(connection, 'disconnection', method()): + if await utils.cancel_on_event( + connection, Connection.EVENT_DISCONNECTION, method() + ): await self.host.send_command( hci.HCI_User_Confirmation_Request_Reply_Command( bd_addr=connection.peer_address @@ -5753,7 +5769,9 @@ class Device(utils.CompositeEventEmitter): async def reply() -> None: try: number = await utils.cancel_on_event( - connection, 'disconnection', pairing_config.delegate.get_number() + connection, + Connection.EVENT_DISCONNECTION, + pairing_config.delegate.get_number(), ) if number is not None: await self.host.send_command( @@ -5787,7 +5805,9 @@ class Device(utils.CompositeEventEmitter): # Ask the user to enter a string async def get_pin_code(): pin_code = await utils.cancel_on_event( - connection, 'disconnection', pairing_config.delegate.get_string(16) + connection, + Connection.EVENT_DISCONNECTION, + pairing_config.delegate.get_string(16), ) if pin_code is not None: @@ -5825,13 +5845,10 @@ class Device(utils.CompositeEventEmitter): pairing_config = self.pairing_config_factory(connection) # Show the passkey to the user -<<<<<<< HEAD utils.cancel_on_event( - connection, 'disconnection', pairing_config.delegate.display_number(passkey) -======= - connection.abort_on( - 'disconnection', pairing_config.delegate.display_number(passkey, digits=6) ->>>>>>> fdf90c6 (add LE advertisement and HR service) + connection, + Connection.EVENT_DISCONNECTION, + pairing_config.delegate.display_number(passkey, digits=6), ) # [Classic only] From ce04c163db013a2d8d7be365e7f7bb7d6169d327 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 11:32:25 -0700 Subject: [PATCH 4/6] fix merge conflict --- apps/pair.py | 28 +++++++++++++++++++++++----- bumble/device.py | 8 ++++++-- bumble/hci.py | 3 +-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/pair.py b/apps/pair.py index 284aec1..13dc06d 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -69,7 +69,7 @@ class Waiter: self.linger = linger def terminate(self): - if not self.linger: + if not self.linger and not self.done.done: self.done.set_result(None) async def wait_until_terminated(self): @@ -247,15 +247,19 @@ def on_connection(connection, request): print(color(f'<<< Connection: {connection}', 'green')) # Listen for pairing events - connection.on('pairing_start', on_pairing_start) - connection.on('pairing', lambda keys: on_pairing(connection, keys)) + connection.on(connection.EVENT_PAIRING_START, on_pairing_start) + connection.on(connection.EVENT_PAIRING, lambda keys: on_pairing(connection, keys)) connection.on( - 'pairing_failure', lambda reason: on_pairing_failure(connection, reason) + connection.EVENT_CLASSIC_PAIRING, lambda: on_classic_pairing(connection) + ) + connection.on( + connection.EVENT_PAIRING_FAILURE, + lambda reason: on_pairing_failure(connection, reason), ) # Listen for encryption changes connection.on( - 'connection_encryption_change', + connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, lambda: on_connection_encryption_change(connection), ) @@ -296,6 +300,20 @@ async def on_pairing(connection, keys): Waiter.instance.terminate() +# ----------------------------------------------------------------------------- +@AsyncRunner.run_in_task() +async def on_classic_pairing(connection): + print(color('***-----------------------------------', 'cyan')) + print( + color( + f'*** Paired [Classic]! (peer identity={connection.peer_address})', 'cyan' + ) + ) + print(color('***-----------------------------------', 'cyan')) + await asyncio.sleep(POST_PAIRING_DELAY) + Waiter.instance.terminate() + + # ----------------------------------------------------------------------------- @AsyncRunner.run_in_task() async def on_pairing_failure(connection, reason): diff --git a/bumble/device.py b/bumble/device.py index c5f8e09..1353e75 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1589,7 +1589,8 @@ class Connection(utils.CompositeEventEmitter): encryption_key_size: int authenticated: bool sc: bool - link_key_type: Optional[int] + link_key: Optional[bytes] # [Classic only] + link_key_type: Optional[int] # [Classic only] gatt_client: gatt_client.Client pairing_peer_io_capability: Optional[int] pairing_peer_authentication_requirements: Optional[int] @@ -1629,6 +1630,7 @@ class Connection(utils.CompositeEventEmitter): EVENT_PAIRING = "pairing" EVENT_PAIRING_FAILURE = "pairing_failure" EVENT_SECURITY_REQUEST = "security_request" + EVENT_LINK_KEY = "link_key" @utils.composite_listener class Listener: @@ -1692,6 +1694,7 @@ class Connection(utils.CompositeEventEmitter): self.encryption_key_size = 0 self.authenticated = False self.sc = False + self.link_key = None self.link_key_type = None self.att_mtu = ATT_DEFAULT_MTU self.data_length = DEVICE_DEFAULT_DATA_LENGTH @@ -5096,8 +5099,9 @@ class Device(utils.CompositeEventEmitter): if connection := self.find_connection_by_bd_addr( bd_addr, transport=PhysicalTransport.BR_EDR ): + connection.link_key = link_key connection.link_key_type = key_type - connection.emit('pairing', pairing_keys) + connection.emit(connection.EVENT_LINK_KEY) def add_service(self, service): self.gatt_server.add_service(service) diff --git a/bumble/hci.py b/bumble/hci.py index 6943c1c..029c2dc 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -29,13 +29,12 @@ from typing_extensions import Self from bumble import crypto from bumble.colors import color from bumble.core import ( - PhysicalTransport, AdvertisingData, DeviceClass, InvalidArgumentError, InvalidPacketError, - ProtocolError, PhysicalTransport, + ProtocolError, bit_flags_to_strings, name_or_number, padded_bytes, From dcc72e49a25ced330811590e7f2e650de26b682d Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 11:34:11 -0700 Subject: [PATCH 5/6] forward legacy constants --- bumble/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bumble/core.py b/bumble/core.py index eee7944..b5822a4 100644 --- a/bumble/core.py +++ b/bumble/core.py @@ -1413,11 +1413,11 @@ class AdvertisingData: THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA - LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01 - LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02 - BR_EDR_NOT_SUPPORTED_FLAG = 0x04 - BR_EDR_CONTROLLER_FLAG = 0x08 - BR_EDR_HOST_FLAG = 0x10 + LE_LIMITED_DISCOVERABLE_MODE_FLAG = Flags.LE_LIMITED_DISCOVERABLE_MODE + LE_GENERAL_DISCOVERABLE_MODE_FLAG = Flags.LE_GENERAL_DISCOVERABLE_MODE + BR_EDR_NOT_SUPPORTED_FLAG = Flags.BR_EDR_NOT_SUPPORTED + BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE + BR_EDR_HOST_FLAG = 0x10 # Deprecated ad_structures: list[tuple[int, bytes]] From 8b59b4f515f506c381df6093f272985d2fc71972 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Sun, 4 May 2025 17:50:00 -0700 Subject: [PATCH 6/6] address PR comments --- apps/bench.py | 2 ++ bumble/device.py | 13 ------------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/bench.py b/apps/bench.py index 0c74f72..e2200c1 100644 --- a/apps/bench.py +++ b/apps/bench.py @@ -1256,6 +1256,7 @@ class Central(Connection.Listener): self.device.classic_enabled = self.classic # Set up a pairing config factory with minimal requirements. + self.device.config.keystore = "JsonKeyStore" self.device.pairing_config_factory = lambda _: PairingConfig( sc=False, mitm=False, bonding=False ) @@ -1408,6 +1409,7 @@ class Peripheral(Device.Listener, Connection.Listener): self.device.classic_enabled = self.classic # Set up a pairing config factory with minimal requirements. + self.device.config.keystore = "JsonKeyStore" self.device.pairing_config_factory = lambda _: PairingConfig( sc=False, mitm=False, bonding=False ) diff --git a/bumble/device.py b/bumble/device.py index 1353e75..775046f 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -1589,7 +1589,6 @@ class Connection(utils.CompositeEventEmitter): encryption_key_size: int authenticated: bool sc: bool - link_key: Optional[bytes] # [Classic only] link_key_type: Optional[int] # [Classic only] gatt_client: gatt_client.Client pairing_peer_io_capability: Optional[int] @@ -1630,7 +1629,6 @@ class Connection(utils.CompositeEventEmitter): EVENT_PAIRING = "pairing" EVENT_PAIRING_FAILURE = "pairing_failure" EVENT_SECURITY_REQUEST = "security_request" - EVENT_LINK_KEY = "link_key" @utils.composite_listener class Listener: @@ -1694,7 +1692,6 @@ class Connection(utils.CompositeEventEmitter): self.encryption_key_size = 0 self.authenticated = False self.sc = False - self.link_key = None self.link_key_type = None self.att_mtu = ATT_DEFAULT_MTU self.data_length = DEVICE_DEFAULT_DATA_LENGTH @@ -5072,15 +5069,6 @@ class Device(utils.CompositeEventEmitter): # [Classic only] @host_event_handler def on_link_key(self, bd_addr, link_key, key_type): - authenticated = key_type in ( - hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE, - hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE, - ) - pairing_keys = PairingKeys() - pairing_keys.link_key = PairingKeys.Key( - value=link_key, authenticated=authenticated - ) - # Store the keys in the key store if self.keystore: authenticated = key_type in ( @@ -5099,7 +5087,6 @@ class Device(utils.CompositeEventEmitter): if connection := self.find_connection_by_bd_addr( bd_addr, transport=PhysicalTransport.BR_EDR ): - connection.link_key = link_key connection.link_key_type = key_type connection.emit(connection.EVENT_LINK_KEY)