diff --git a/apps/controller_info.py b/apps/controller_info.py index 47079837..d6a50573 100644 --- a/apps/controller_info.py +++ b/apps/controller_info.py @@ -24,6 +24,10 @@ from bumble.colors import color from bumble.core import name_or_number from bumble.hci import ( + HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND, + HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND, + HCI_Read_Local_Extended_Features_Command, + HCI_Read_Local_Supported_Features_Command, map_null_terminated_utf8_string, HCI_SUCCESS, HCI_LE_SUPPORTED_FEATURES_NAMES, @@ -56,6 +60,36 @@ def command_succeeded(response): return False +# ----------------------------------------------------------------------------- +async def get_common_info(host): + if host.supports_command(HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND): + response = await host.send_command(HCI_Read_Local_Supported_Features_Command()) + if response.return_parameters.status == HCI_SUCCESS: + print() + print(color('LMP Features:', 'yellow')) + # TODO: support printing discrete enum values + print(' ', response.return_parameters.lmp_features.hex()) + + if host.supports_command(HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND): + response = await host.send_command( + HCI_Read_Local_Extended_Features_Command(page_number=0) + ) + if response.return_parameters.status == HCI_SUCCESS: + if response.return_parameters.max_page_number > 0: + print() + print(color('Extended LMP Features:', 'yellow')) + + for page in range(1, response.return_parameters.max_page_number + 1): + response = await host.send_command( + HCI_Read_Local_Extended_Features_Command(page_number=page) + ) + + if response.return_parameters.status == HCI_SUCCESS: + # TODO: support printing discrete enum values + print(f' Page {page}:') + print(' ', response.return_parameters.extended_lmp_features.hex()) + + # ----------------------------------------------------------------------------- async def get_classic_info(host): if host.supports_command(HCI_READ_BD_ADDR_COMMAND): @@ -147,6 +181,9 @@ async def async_main(transport): ) print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion) + # Get the common info + await get_common_info(host) + # Get the Classic info await get_classic_info(host) diff --git a/apps/controllers.py b/apps/controllers.py index ac6477ec..fc74f734 100644 --- a/apps/controllers.py +++ b/apps/controllers.py @@ -17,31 +17,25 @@ # ----------------------------------------------------------------------------- import logging import asyncio -import sys import os -from bumble.controller import Controller +import click + +from bumble.controller import Controller, Options from bumble.link import LocalLink from bumble.transport import open_transport_or_link # ----------------------------------------------------------------------------- -async def async_main(): - if len(sys.argv) != 3: - print( - 'Usage: controllers.py ' - '[ ...]' - ) - print('example: python controllers.py pty:ble1 pty:ble2') - return - +async def async_main(extended_advertising, transport_names): # Create a local link to attach the controllers to link = LocalLink() # Create a transport and controller for all requested names transports = [] controllers = [] - for index, transport_name in enumerate(sys.argv[1:]): + options = Options(extended_advertising=extended_advertising) + for index, transport_name in enumerate(transport_names): transport = await open_transport_or_link(transport_name) transports.append(transport) controller = Controller( @@ -49,6 +43,7 @@ async def async_main(): host_source=transport.source, host_sink=transport.sink, link=link, + options=options, ) controllers.append(controller) @@ -61,9 +56,14 @@ async def async_main(): # ----------------------------------------------------------------------------- -def main(): - logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) - asyncio.run(async_main()) +@click.command() +@click.option( + '--extended-advertising', is_flag=True, help="Enable extended advertising" +) +@click.argument('transports', nargs=-1, required=True) +def main(extended_advertising, transports): + logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) + asyncio.run(async_main(extended_advertising, transports)) # ----------------------------------------------------------------------------- diff --git a/apps/link_relay/link_relay.py b/apps/link_relay/link_relay.py index 6036fa0f..b58b6a91 100644 --- a/apps/link_relay/link_relay.py +++ b/apps/link_relay/link_relay.py @@ -253,7 +253,7 @@ async def serve(self, websocket, path): # ---------------------------------------------------------------------------- -def main(): +async def async_main(): # Check the Python version if sys.version_info < (3, 6, 1): print('ERROR: Python 3.6.1 or higher is required') @@ -280,8 +280,13 @@ def main(): # Start a relay relay = Relay(args.port) - asyncio.get_event_loop().run_until_complete(relay.start()) - asyncio.get_event_loop().run_forever() + async with relay.start(): + await asyncio.Future() + + +# ---------------------------------------------------------------------------- +def main(): + asyncio.run(async_main()) # ---------------------------------------------------------------------------- diff --git a/bumble/controller.py b/bumble/controller.py index 9b2960a3..bd311827 100644 --- a/bumble/controller.py +++ b/bumble/controller.py @@ -22,6 +22,9 @@ import itertools import random import struct +import time +from dataclasses import dataclass + from bumble.colors import color from bumble.core import ( BT_CENTRAL_ROLE, @@ -35,15 +38,104 @@ HCI_COMMAND_DISALLOWED_ERROR, HCI_COMMAND_PACKET, HCI_COMMAND_STATUS_PENDING, + HCI_CONNECTION_PARAMETERS_REQUEST_PROCEDURE_LE_SUPPORTED_FEATURE, HCI_CONNECTION_TIMEOUT_ERROR, HCI_CONTROLLER_BUSY_ERROR, + HCI_DISCONNECT_COMMAND, HCI_EVENT_PACKET, + HCI_EXTENDED_REJECT_INDICATION_LE_SUPPORTED_FEATURE, + HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE, + HCI_LE_CLEAR_ADVERTISING_SETS_COMMAND, + HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE, + HCI_HOST_BUFFER_SIZE_COMMAND, + HCI_HOST_NUMBER_OF_COMPLETED_PACKETS_COMMAND, HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR, HCI_LE_1M_PHY, + HCI_LE_ADD_DEVICE_TO_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_ADD_DEVICE_TO_RESOLVING_LIST_COMMAND, + HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_CLEAR_RESOLVING_LIST_COMMAND, + HCI_LE_CONNECTION_UPDATE_COMMAND, + HCI_LE_CREATE_CONNECTION_CANCEL_COMMAND, + HCI_LE_CREATE_CONNECTION_COMMAND, + HCI_LE_DATA_PACKET_LENGTH_EXTENSION_LE_SUPPORTED_FEATURE, + HCI_LE_ENABLE_ENCRYPTION_COMMAND, + HCI_LE_ENCRYPT_COMMAND, + HCI_LE_ENCRYPTION_LE_SUPPORTED_FEATURE, + HCI_LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_COMMAND, + HCI_LE_LONG_TERM_KEY_REQUEST_REPLY_COMMAND, + HCI_LE_PING_LE_SUPPORTED_FEATURE, + HCI_LE_RAND_COMMAND, + HCI_LE_READ_ADVERTISING_PHYSICAL_CHANNEL_TX_POWER_COMMAND, + HCI_LE_READ_BUFFER_SIZE_COMMAND, + HCI_LE_READ_CHANNEL_MAP_COMMAND, + HCI_LE_READ_FILTER_ACCEPT_LIST_SIZE_COMMAND, + HCI_LE_READ_LOCAL_RESOLVABLE_ADDRESS_COMMAND, + HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND, + HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND, + HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND, + HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND, + HCI_LE_READ_PEER_RESOLVABLE_ADDRESS_COMMAND, + HCI_LE_READ_PHY_COMMAND, + HCI_LE_READ_REMOTE_FEATURES_COMMAND, + HCI_LE_READ_RESOLVING_LIST_SIZE_COMMAND, + HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND, + HCI_LE_READ_SUPPORTED_STATES_COMMAND, + HCI_LE_READ_TRANSMIT_POWER_COMMAND, + HCI_LE_RECEIVER_TEST_COMMAND, + HCI_LE_RECEIVER_TEST_V2_COMMAND, + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_NEGATIVE_REPLY_COMMAND, + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_REPLY_COMMAND, + HCI_LE_REMOVE_ADVERTISING_SET_COMMAND, + HCI_LE_REMOVE_DEVICE_FROM_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_REMOVE_DEVICE_FROM_RESOLVING_LIST_COMMAND, + HCI_LE_SET_ADDRESS_RESOLUTION_ENABLE_COMMAND, + HCI_LE_SET_ADVERTISING_DATA_COMMAND, + HCI_LE_SET_ADVERTISING_ENABLE_COMMAND, + HCI_LE_SET_ADVERTISING_PARAMETERS_COMMAND, + HCI_LE_SET_ADVERTISING_SET_RANDOM_ADDRESS_COMMAND, + HCI_LE_SET_DATA_LENGTH_COMMAND, + HCI_LE_SET_DEFAULT_PHY_COMMAND, + HCI_LE_SET_EVENT_MASK_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_DATA_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_ENABLE_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_PARAMETERS_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_RESPONSE_DATA_COMMAND, + HCI_LE_SET_HOST_CHANNEL_CLASSIFICATION_COMMAND, + HCI_LE_SET_PHY_COMMAND, + HCI_LE_SET_PRIVACY_MODE_COMMAND, + HCI_LE_SET_RANDOM_ADDRESS_COMMAND, + HCI_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT_COMMAND, + HCI_LE_SET_SCAN_ENABLE_COMMAND, + HCI_LE_SET_SCAN_PARAMETERS_COMMAND, + HCI_LE_SET_SCAN_RESPONSE_DATA_COMMAND, + HCI_LE_TEST_END_COMMAND, + HCI_LE_TRANSMITTER_TEST_COMMAND, + HCI_LE_TRANSMITTER_TEST_V2_COMMAND, + HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND, + HCI_LL_PRIVACY_LE_SUPPORTED_FEATURE, + HCI_PERIPHERAL_INITIATED_FEATURE_EXCHANGE_LE_SUPPORTED_FEATURE, + HCI_READ_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND, + HCI_READ_BD_ADDR_COMMAND, + HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND, + HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND, + HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND, + HCI_READ_RSSI_COMMAND, + HCI_READ_TRANSMIT_POWER_LEVEL_COMMAND, + HCI_RESET_COMMAND, + HCI_SET_CONTROLLER_TO_HOST_FLOW_CONTROL_COMMAND, + HCI_SET_EVENT_MASK_COMMAND, + HCI_SET_EVENT_MASK_PAGE_2_COMMAND, HCI_SUCCESS, + HCI_SUPPORTED_COMMANDS_FLAGS, + HCI_UNKNOWN_ADVERTISING_IDENTIFIER_ERROR, HCI_UNKNOWN_HCI_COMMAND_ERROR, HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR, + HCI_MEMORY_CAPACITY_EXCEEDED_ERROR, HCI_VERSION_BLUETOOTH_CORE_5_0, + HCI_WRITE_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND, Address, HCI_AclDataPacket, HCI_AclDataPacketAssembler, @@ -55,6 +147,7 @@ HCI_Encryption_Change_Event, HCI_LE_Advertising_Report_Event, HCI_LE_Connection_Complete_Event, + HCI_LE_Extended_Advertising_Report_Event, HCI_LE_Read_Remote_Features_Complete_Event, HCI_Number_Of_Completed_Packets_Event, HCI_Packet, @@ -78,6 +171,25 @@ class DataObject: pass +# ----------------------------------------------------------------------------- +def le_supported_features_as_bytes(supported_features): + return struct.pack(' 0 + and self.extended_advertising_events >= self.max_extended_advertising_events + ): + self.next_advertising_time = 0 + return + + now = time.time() + if self.extended_advertising_events == 0: + self.first_advertising_time = now + + if self.duration: + elapsed = now - self.first_advertising_time + if elapsed > self.duration / 100.0: + self.next_advertising_time = 0 + return + + self.extended_advertising_events += 1 + link.send_extended_advertising_data( + self.address, + self.parameters.advertising_event_properties, + self.data, + self.scan_response_data, + ) + self.schedule() + + # ----------------------------------------------------------------------------- class Controller: def __init__( @@ -111,10 +336,12 @@ def __init__( host_sink: Optional[TransportSink] = None, link=None, public_address: Optional[Union[bytes, str, Address]] = None, + options: Optional[Options] = None, ): self.name = name self.hci_sink = None self.link = link + self.options = options or Options() self.central_connections: Dict[ Address, Connection @@ -138,13 +365,105 @@ def __init__( self.hc_total_num_le_data_packets = 64 self.event_mask = 0 self.event_mask_page_2 = 0 - self.supported_commands = bytes.fromhex( - '2000800000c000000000e40000002822000000000000040000f7ffff7f000000' - '30f0f9ff01008004000000000000000000000000000000000000000000000000' - ) + supported_commands = [ + HCI_DISCONNECT_COMMAND, + HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND, + HCI_SET_EVENT_MASK_COMMAND, + HCI_RESET_COMMAND, + HCI_READ_TRANSMIT_POWER_LEVEL_COMMAND, + HCI_SET_CONTROLLER_TO_HOST_FLOW_CONTROL_COMMAND, + HCI_HOST_BUFFER_SIZE_COMMAND, + HCI_HOST_NUMBER_OF_COMPLETED_PACKETS_COMMAND, + HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND, + HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND, + HCI_READ_BD_ADDR_COMMAND, + HCI_READ_RSSI_COMMAND, + HCI_SET_EVENT_MASK_PAGE_2_COMMAND, + HCI_LE_SET_EVENT_MASK_COMMAND, + HCI_LE_READ_BUFFER_SIZE_COMMAND, + HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND, + HCI_LE_SET_RANDOM_ADDRESS_COMMAND, + HCI_LE_SET_ADVERTISING_PARAMETERS_COMMAND, + HCI_LE_READ_ADVERTISING_PHYSICAL_CHANNEL_TX_POWER_COMMAND, + HCI_LE_SET_ADVERTISING_DATA_COMMAND, + HCI_LE_SET_SCAN_RESPONSE_DATA_COMMAND, + HCI_LE_SET_ADVERTISING_ENABLE_COMMAND, + HCI_LE_SET_SCAN_PARAMETERS_COMMAND, + HCI_LE_SET_SCAN_ENABLE_COMMAND, + HCI_LE_CREATE_CONNECTION_COMMAND, + HCI_LE_CREATE_CONNECTION_CANCEL_COMMAND, + HCI_LE_READ_FILTER_ACCEPT_LIST_SIZE_COMMAND, + HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_ADD_DEVICE_TO_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_REMOVE_DEVICE_FROM_FILTER_ACCEPT_LIST_COMMAND, + HCI_LE_CONNECTION_UPDATE_COMMAND, + HCI_LE_SET_HOST_CHANNEL_CLASSIFICATION_COMMAND, + HCI_LE_READ_CHANNEL_MAP_COMMAND, + HCI_LE_READ_REMOTE_FEATURES_COMMAND, + HCI_LE_ENCRYPT_COMMAND, + HCI_LE_RAND_COMMAND, + HCI_LE_ENABLE_ENCRYPTION_COMMAND, + HCI_LE_LONG_TERM_KEY_REQUEST_REPLY_COMMAND, + HCI_LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_COMMAND, + HCI_LE_READ_SUPPORTED_STATES_COMMAND, + HCI_LE_RECEIVER_TEST_COMMAND, + HCI_LE_TRANSMITTER_TEST_COMMAND, + HCI_LE_TEST_END_COMMAND, + HCI_READ_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND, + HCI_WRITE_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND, + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_REPLY_COMMAND, + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_NEGATIVE_REPLY_COMMAND, + HCI_LE_SET_DATA_LENGTH_COMMAND, + HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND, + HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND, + HCI_LE_ADD_DEVICE_TO_RESOLVING_LIST_COMMAND, + HCI_LE_REMOVE_DEVICE_FROM_RESOLVING_LIST_COMMAND, + HCI_LE_CLEAR_RESOLVING_LIST_COMMAND, + HCI_LE_READ_RESOLVING_LIST_SIZE_COMMAND, + HCI_LE_READ_PEER_RESOLVABLE_ADDRESS_COMMAND, + HCI_LE_READ_LOCAL_RESOLVABLE_ADDRESS_COMMAND, + HCI_LE_SET_ADDRESS_RESOLUTION_ENABLE_COMMAND, + HCI_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT_COMMAND, + HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND, + HCI_LE_READ_PHY_COMMAND, + HCI_LE_SET_DEFAULT_PHY_COMMAND, + HCI_LE_SET_PHY_COMMAND, + HCI_LE_RECEIVER_TEST_V2_COMMAND, + HCI_LE_TRANSMITTER_TEST_V2_COMMAND, + HCI_LE_READ_TRANSMIT_POWER_COMMAND, + HCI_LE_SET_PRIVACY_MODE_COMMAND, + ] + if self.options.extended_advertising: + supported_commands.extend( + [ + HCI_LE_SET_ADVERTISING_SET_RANDOM_ADDRESS_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_DATA_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_RESPONSE_DATA_COMMAND, + HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND, + HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND, + HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND, + HCI_LE_REMOVE_ADVERTISING_SET_COMMAND, + HCI_LE_CLEAR_ADVERTISING_SETS_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_PARAMETERS_COMMAND, + HCI_LE_SET_EXTENDED_SCAN_ENABLE_COMMAND, + ] + ) + self.supported_commands = supported_commands_as_bytes(supported_commands) self.le_event_mask = 0 - self.advertising_parameters = None - self.le_features = bytes.fromhex('ff49010000000000') + le_features = [ + HCI_LE_ENCRYPTION_LE_SUPPORTED_FEATURE, + HCI_CONNECTION_PARAMETERS_REQUEST_PROCEDURE_LE_SUPPORTED_FEATURE, + HCI_EXTENDED_REJECT_INDICATION_LE_SUPPORTED_FEATURE, + HCI_PERIPHERAL_INITIATED_FEATURE_EXCHANGE_LE_SUPPORTED_FEATURE, + HCI_LE_PING_LE_SUPPORTED_FEATURE, + HCI_LE_DATA_PACKET_LENGTH_EXTENSION_LE_SUPPORTED_FEATURE, + HCI_LL_PRIVACY_LE_SUPPORTED_FEATURE, + HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE, + ] + if self.options.extended_advertising: + le_features.append(HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE) + self.le_features = le_supported_features_as_bytes(le_features) self.le_states = bytes.fromhex('ffff3fffff030000') self.advertising_channel_tx_power = 0 self.filter_accept_list_size = 8 @@ -163,16 +482,13 @@ def __init__( self.le_scan_enable = 0 self.le_scan_own_address_type = Address.RANDOM_DEVICE_ADDRESS self.le_scanning_filter_policy = 0 - self.le_scan_response_data = None self.le_address_resolution = False self.le_rpa_timeout = 0 + self.le_maximum_advertising_data_length = 0x0672 + self.le_number_of_supported_advertising_sets = 64 self.sync_flow_control = False self.local_name = 'Bumble' - self.advertising_interval = 2000 # Fixed for now - self.advertising_data = None - self.advertising_timer_handle = None - self._random_address = Address('00:00:00:00:00:00') if isinstance(public_address, Address): self._public_address = public_address @@ -183,6 +499,10 @@ def __init__( else: self._public_address = Address('00:00:00:00:00:00') + self.advertising_timer_handle = None + self.legacy_advertiser = LegacyAdvertiser(None, self.random_address) + self.extended_advertisers: Dict[int, Exception] = {} # Advertisers, by handle + # Set the source and sink interfaces if host_source: host_source.set_packet_sink(self) @@ -511,12 +831,12 @@ def on_link_acl_data(self, sender_address, transport, data): acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data) self.send_hci_packet(acl_packet) - def on_link_advertising_data(self, sender_address, data): + def on_link_advertising_data(self, sender_address, data, scan_response): # Ignore if we're not scanning if self.le_scan_enable == 0: return - # Send a scan report + # Send an advertising report report = HCI_LE_Advertising_Report_Event.Report( HCI_LE_Advertising_Report_Event.Report.FIELDS, event_type=HCI_LE_Advertising_Report_Event.ADV_IND, @@ -533,11 +853,62 @@ def on_link_advertising_data(self, sender_address, data): event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP, address_type=sender_address.address_type, address=sender_address, - data=data, + data=scan_response, rssi=-50, ) self.send_hci_packet(HCI_LE_Advertising_Report_Event([report])) + def on_link_extended_advertising_data( + self, sender_address, event_properties, data, scan_response + ): + # Ignore if we're not scanning + if self.le_scan_enable == 0: + return + + # Send an advertising report + event_type = ( + 1 << HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING + ) + report = HCI_LE_Extended_Advertising_Report_Event.Report( + HCI_LE_Extended_Advertising_Report_Event.Report.FIELDS, + event_type=event_type, + address_type=sender_address.address_type, + address=sender_address, + primary_phy=HCI_LE_1M_PHY, + secondary_phy=HCI_LE_1M_PHY, + advertising_sid=0, + tx_power=0, + rssi=-50, + periodic_advertising_interval=0, + direct_address_type=0, + direct_address=Address.NIL, + data=data, + ) + self.send_hci_packet(HCI_LE_Extended_Advertising_Report_Event([report])) + + # Simulate a scan response if needed + if event_properties & (1 << 1) == 0: + # The event is not scannable + return + + event_type |= 1 << HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE + report = HCI_LE_Extended_Advertising_Report_Event.Report( + HCI_LE_Extended_Advertising_Report_Event.Report.FIELDS, + event_type=event_type, + address_type=sender_address.address_type, + address=sender_address, + primary_phy=HCI_LE_1M_PHY, + secondary_phy=HCI_LE_1M_PHY, + advertising_sid=0, + tx_power=0, + rssi=-50, + periodic_advertising_interval=0, + direct_address_type=0, + direct_address=Address.NIL, + data=scan_response, + ) + self.send_hci_packet(HCI_LE_Extended_Advertising_Report_Event([report])) + ############################################################ # Classic link connections ############################################################ @@ -623,32 +994,42 @@ def on_classic_role_change(self, peer_address, new_role): # Advertising support ############################################################ def on_advertising_timer_fired(self): - self.send_advertising_data() - self.advertising_timer_handle = asyncio.get_running_loop().call_later( - self.advertising_interval / 1000.0, self.on_advertising_timer_fired - ) + self.advertising_timer_handle = None - def start_advertising(self): - # Stop any ongoing advertising before we start again - self.stop_advertising() + self.send_advertising_data() - # Advertise now - self.advertising_timer_handle = asyncio.get_running_loop().call_soon( - self.on_advertising_timer_fired + # Compute the time of the next advertisement + next_advertisement = min( + ( + advertiser.next_advertising_time + for advertiser in itertools.chain( + (self.legacy_advertiser,), self.extended_advertisers.values() + ) + if advertiser.enabled + ), + default=0, ) - def stop_advertising(self): - if self.advertising_timer_handle is not None: - self.advertising_timer_handle.cancel() - self.advertising_timer_handle = None + if next_advertisement: + # We have at least one advertiser + delay = max(next_advertisement - time.time(), 0) + self.advertising_timer_handle = asyncio.get_running_loop().call_later( + delay, self.on_advertising_timer_fired + ) + + def start_advertising_timer(self): + if self.advertising_timer_handle is None: + self.advertising_timer_handle = asyncio.get_running_loop().call_soon( + self.on_advertising_timer_fired + ) def send_advertising_data(self): - if self.link and self.advertising_data: - self.link.send_advertising_data(self.random_address, self.advertising_data) + # Legacy advertising + self.legacy_advertiser.send_advertising_data(self.link) - @property - def is_advertising(self): - return self.advertising_timer_handle is not None + # Extended advertising + for advertiser in self.extended_advertisers.values(): + advertiser.send_advertising_data(self.link) ############################################################ # HCI handlers @@ -925,6 +1306,12 @@ def on_hci_read_bd_addr_command(self, _command): ) return bytes([HCI_SUCCESS]) + bd_addr + def on_hci_read_local_extended_features_command(self, _command): + ''' + See Bluetooth spec @ 7.4.4 Read Local Extended Features Command + ''' + return bytes([HCI_SUCCESS]) + bytes(8) + def on_hci_le_set_event_mask_command(self, command): ''' See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command @@ -961,7 +1348,7 @@ def on_hci_le_set_advertising_parameters_command(self, command): ''' See Bluetooth spec Vol 4, Part E - 7.8.5 LE Set Advertising Parameters Command ''' - self.advertising_parameters = command + self.legacy_advertiser.parameters = command return bytes([HCI_SUCCESS]) def on_hci_le_read_advertising_physical_channel_tx_power_command(self, _command): @@ -975,14 +1362,14 @@ def on_hci_le_set_advertising_data_command(self, command): ''' See Bluetooth spec Vol 4, Part E - 7.8.7 LE Set Advertising Data Command ''' - self.advertising_data = command.advertising_data + self.legacy_advertiser.data = command.advertising_data return bytes([HCI_SUCCESS]) def on_hci_le_set_scan_response_data_command(self, command): ''' See Bluetooth spec Vol 4, Part E - 7.8.8 LE Set Scan Response Data Command ''' - self.le_scan_response_data = command.scan_response_data + self.legacy_advertiser.scan_response_data = command.scan_response_data return bytes([HCI_SUCCESS]) def on_hci_le_set_advertising_enable_command(self, command): @@ -990,9 +1377,10 @@ def on_hci_le_set_advertising_enable_command(self, command): See Bluetooth spec Vol 4, Part E - 7.8.9 LE Set Advertising Enable Command ''' if command.advertising_enable: - self.start_advertising() + self.legacy_advertiser.enabled = True + self.start_advertising_timer() else: - self.stop_advertising() + self.legacy_advertiser.enabled = True return bytes([HCI_SUCCESS]) @@ -1255,6 +1643,190 @@ def on_hci_le_set_default_phy_command(self, command): } return bytes([HCI_SUCCESS]) + def on_hci_le_set_advertising_set_random_address_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.52 LE Set Advertising Set Random Address + Command + ''' + if ( + advertiser := self.extended_advertisers.get( + command.advertising_handle, None + ) + ) is None: + return bytes([HCI_UNKNOWN_ADVERTISING_IDENTIFIER_ERROR]) + + if advertiser.enabled and advertiser.is_connectable: + return bytes([HCI_COMMAND_DISALLOWED_ERROR]) + + advertiser.address = command.random_address + + return bytes([HCI_SUCCESS]) + + def on_hci_le_set_extended_advertising_parameters_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.53 LE Set Extended Advertising Parameters + Command + ''' + # Check if the advertiser already exists + if advertiser := self.extended_advertisers.get( + command.advertising_handle, None + ): + # We cannot update an advertiser that's currently enabled + if advertiser.enabled: + return bytes([HCI_COMMAND_DISALLOWED_ERROR, 0]) + + # Update the advertiser + advertiser.parameters = command + else: + # Try to create a new advertiser + if ( + len(self.extended_advertisers) + >= self.le_number_of_supported_advertising_sets + ): + logger.warning('too many advertisers') + return bytes([HCI_MEMORY_CAPACITY_EXCEEDED_ERROR, 0]) + + logger.debug(f'new advertiser: {command.advertising_handle}') + # TODO: allow other addresses + advertiser = ExtendedAdvertiser(command, self.random_address) + self.extended_advertisers[command.advertising_handle] = advertiser + + return bytes([HCI_SUCCESS, advertiser.tx_power]) + + def on_hci_le_set_extended_advertising_data_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.54 LE Set Extended Advertising Data + Command + ''' + if ( + advertiser := self.extended_advertisers.get( + command.advertising_handle, None + ) + ) is None: + return bytes([HCI_UNKNOWN_ADVERTISING_IDENTIFIER_ERROR]) + + if command.operation not in (3, 4) and not command.advertising_data: + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + + if advertiser.enabled and command.operation not in (3, 4): + return bytes([HCI_COMMAND_DISALLOWED_ERROR]) + + updated_data = None + if command.operation == 0: + # Intermediate fragment of fragmented extended advertising data + updated_data = advertiser.data + command.advertising_data + elif command.operation == 1: + # First fragment of fragmented extended advertising data + updated_data = command.advertising_data + elif command.operation == 2: + # Last fragment of fragmented extended advertising data + updated_data = advertiser.data + command.advertising_data + elif command.operation == 3: + # Complete extended advertising data + updated_data = command.advertising_data + elif command.operation == 4: + # Unchanged data (just update the Advertising DID) + if ( + not advertiser.enabled + or not advertiser.data + or advertiser.is_legacy + or command.advertising_data + ): + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + else: + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + + if updated_data is not None: + if len(updated_data) > self.le_maximum_advertising_data_length: + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + advertiser.data = updated_data + logger.debug(f'updating advertiser data: {updated_data.hex()}') + + return bytes([HCI_SUCCESS]) + + def on_hci_le_set_extended_scan_response_data_command(self, _command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.55 LE Set Extended Scan Response Data + Command + ''' + # TODO: not implemented yet + return bytes([HCI_SUCCESS]) + + def on_hci_le_set_extended_advertising_enable_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.56 LE Set Extended Advertising Enable + Command + ''' + for advertising_handle in command.advertising_handles: + if ( + advertiser := self.extended_advertisers.get(advertising_handle, None) + ) is None: + return bytes([HCI_UNKNOWN_ADVERTISING_IDENTIFIER_ERROR]) + + for i, advertising_handle in enumerate(command.advertising_handles): + advertiser = self.extended_advertisers[advertising_handle] + if command.enable: + advertiser.enabled = True + advertiser.duration = command.durations[i] + advertiser.max_extended_advertising_events = ( + command.max_extended_advertising_events[i] + ) + self.start_advertising_timer() + else: + advertiser.enabled = False + + return bytes([HCI_SUCCESS]) + + def on_hci_le_read_maximum_advertising_data_length_command(self, _command): + ''' + See Bluetooth spec Vol 2, Part E - 7.8.57 LE Read Maximum Advertising Data + Length Command + ''' + return struct.pack(' None: and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets ): packet = self.acl_packet_queue.pop() - self.send_hci_packet(packet) self.acl_packets_in_flight += 1 + self.send_hci_packet(packet) def supports_command(self, command): # Find the support flag position for this command @@ -550,7 +550,7 @@ def on_hci_number_of_completed_packets_event(self, event): else: logger.warning( color( - '!!! {total_packets} completed but only ' + f'!!! {total_packets} completed but only ' f'{self.acl_packets_in_flight} in flight' ) ) diff --git a/bumble/link.py b/bumble/link.py index 85ad96e4..d0277580 100644 --- a/bumble/link.py +++ b/bumble/link.py @@ -95,11 +95,21 @@ def get_pending_connection(self): def on_address_changed(self, controller): pass - def send_advertising_data(self, sender_address, data): + def send_advertising_data(self, sender_address, data, scan_response): # Send the advertising data to all controllers, except the sender for controller in self.controllers: if controller.random_address != sender_address: - controller.on_link_advertising_data(sender_address, data) + controller.on_link_advertising_data(sender_address, data, scan_response) + + def send_extended_advertising_data( + self, sender_address, event_type, data, scan_response + ): + # Send the advertising data to all controllers, except the sender + for controller in self.controllers: + if controller.random_address != sender_address: + controller.on_link_extended_advertising_data( + sender_address, event_type, data, scan_response + ) def send_acl_data(self, sender_controller, destination_address, transport, data): # Send the data to the first controller with a matching address @@ -173,8 +183,12 @@ def disconnect(self, central_address, peripheral_address, disconnect_command): f'$$$ DISCONNECTION {central_address} -> ' f'{peripheral_address}: reason = {disconnect_command.reason}' ) - args = [central_address, peripheral_address, disconnect_command] - asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args) + asyncio.get_running_loop().call_soon( + self.on_disconnection_complete, + central_address, + peripheral_address, + disconnect_command, + ) # pylint: disable=too-many-arguments def on_connection_encrypted( @@ -384,7 +398,7 @@ async def on_message_received(self, message): async def on_advertisement_message_received(self, sender, advertisement): try: self.controller.on_link_advertising_data( - Address(sender), bytes.fromhex(advertisement) + Address(sender), bytes.fromhex(advertisement), b'' ) except Exception: logger.exception('exception') @@ -471,7 +485,7 @@ def on_address_changed(self, controller): async def send_advertising_data_to_relay(self, data): await self.send_targeted_message('*', f'advertisement:{data.hex()}') - def send_advertising_data(self, _, data): + def send_advertising_data(self, _, data, scan_response): self.execute(partial(self.send_advertising_data_to_relay, data)) async def send_acl_data_to_relay(self, peer_address, data):