add AdvertisingType

This commit is contained in:
Gilles Boccon-Gibod
2022-10-15 21:43:08 -07:00
parent d10dda7e10
commit d5eebc2101
6 changed files with 192 additions and 54 deletions

View File

@@ -76,7 +76,7 @@ class Controller:
self.supported_commands = bytes.fromhex('2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000') self.supported_commands = bytes.fromhex('2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000')
self.le_features = bytes.fromhex('ff49010000000000') self.le_features = bytes.fromhex('ff49010000000000')
self.le_states = bytes.fromhex('ffff3fffff030000') self.le_states = bytes.fromhex('ffff3fffff030000')
self.avertising_channel_tx_power = 0 self.advertising_channel_tx_power = 0
self.filter_accept_list_size = 8 self.filter_accept_list_size = 8
self.resolving_list_size = 8 self.resolving_list_size = 8
self.supported_max_tx_octets = 27 self.supported_max_tx_octets = 27
@@ -650,7 +650,7 @@ class Controller:
''' '''
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power Command See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power Command
''' '''
return bytes([HCI_SUCCESS, self.avertising_channel_tx_power]) return bytes([HCI_SUCCESS, self.advertising_channel_tx_power])
def on_hci_le_set_advertising_data_command(self, command): def on_hci_le_set_advertising_data_command(self, command):
''' '''

View File

@@ -15,6 +15,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from enum import IntEnum
import json import json
import asyncio import asyncio
import logging import logging
@@ -66,6 +67,9 @@ DEVICE_MIN_SCAN_INTERVAL = 25
DEVICE_MAX_SCAN_INTERVAL = 10240 DEVICE_MAX_SCAN_INTERVAL = 10240
DEVICE_MIN_SCAN_WINDOW = 25 DEVICE_MIN_SCAN_WINDOW = 25
DEVICE_MAX_SCAN_WINDOW = 10240 DEVICE_MAX_SCAN_WINDOW = 10240
DEVICE_MIN_LE_RSSI = -127
DEVICE_MAX_LE_RSSI = 20
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Classes # Classes
@@ -199,6 +203,45 @@ class AdvertisementDataAccumulator:
return result return result
# -----------------------------------------------------------------------------
class AdvertisingType(IntEnum):
UNDIRECTED_CONNECTABLE_SCANNABLE = 0x00 # Undirected, connectable, scannable
DIRECTED_CONNECTABLE_HIGH_DUTY = 0x01 # Directed, connectable, non-scannable
UNDIRECTED_SCANNABLE = 0x02 # Undirected, non-connectable, scannable
UNDIRECTED = 0x03 # Undirected, non-connectable, non-scannable
DIRECTED_CONNECTABLE_LOW_DUTY = 0x04 # Directed, connectable, non-scannable
@property
def has_data(self):
return self in {
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
AdvertisingType.UNDIRECTED_SCANNABLE,
AdvertisingType.UNDIRECTED
}
@property
def is_connectable(self):
return self in {
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
}
@property
def is_scannable(self):
return self in {
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
AdvertisingType.UNDIRECTED_SCANNABLE
}
@property
def is_directed(self):
return self in {
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Peer: class Peer:
def __init__(self, connection): def __init__(self, connection):
@@ -298,7 +341,8 @@ class ConnectionParametersPreferences:
min_ce_length: int = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH min_ce_length: int = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH
max_ce_length: int = DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH max_ce_length: int = DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH
DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES = ConnectionParametersPreferences()
ConnectionParametersPreferences.default = ConnectionParametersPreferences()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -592,6 +636,7 @@ class Device(CompositeEventEmitter):
self._host = None self._host = None
self.powered_on = False self.powered_on = False
self.advertising = False self.advertising = False
self.advertising_type = None
self.auto_restart_advertising = False self.auto_restart_advertising = False
self.command_timeout = 10 # seconds self.command_timeout = 10 # seconds
self.gatt_server = gatt_server.Server(self) self.gatt_server = gatt_server.Server(self)
@@ -811,32 +856,48 @@ class Device(CompositeEventEmitter):
return self.host.supports_le_feature(feature_map[phy]) return self.host.supports_le_feature(feature_map[phy])
async def start_advertising(self, auto_restart=False): async def start_advertising(
self.auto_restart_advertising = auto_restart self,
advertising_type=AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
target=None,
auto_restart=False
):
# If we're advertising, stop first # If we're advertising, stop first
if self.advertising: if self.advertising:
await self.stop_advertising() await self.stop_advertising()
# Set/update the advertising data # Set/update the advertising data if the advertising type allows it
await self.send_command(HCI_LE_Set_Advertising_Data_Command( if advertising_type.has_data:
advertising_data = self.advertising_data await self.send_command(HCI_LE_Set_Advertising_Data_Command(
), check_result=True) advertising_data = self.advertising_data
), check_result=True)
# Set/update the scan response data # Set/update the scan response data if the advertising is scannable
await self.send_command(HCI_LE_Set_Scan_Response_Data_Command( if advertising_type.is_scannable:
scan_response_data = self.scan_response_data await self.send_command(HCI_LE_Set_Scan_Response_Data_Command(
), check_result=True) scan_response_data = self.scan_response_data
), check_result=True)
# Decide what peer address to use
if advertising_type.is_directed:
if target is None:
raise ValueError('directed advertising requires a target address')
peer_address = target
peer_address_type = target.address_type
else:
peer_address = Address('00:00:00:00:00:00')
peer_address_type = Address.PUBLIC_DEVICE_ADDRESS
# Set the advertising parameters # Set the advertising parameters
await self.send_command(HCI_LE_Set_Advertising_Parameters_Command( await self.send_command(HCI_LE_Set_Advertising_Parameters_Command(
# TODO: use real values, not fixed ones # TODO: use real values, not fixed ones
advertising_interval_min = self.advertising_interval_min, advertising_interval_min = self.advertising_interval_min,
advertising_interval_max = self.advertising_interval_max, advertising_interval_max = self.advertising_interval_max,
advertising_type = HCI_LE_Set_Advertising_Parameters_Command.ADV_IND, advertising_type = int(advertising_type),
own_address_type = Address.RANDOM_DEVICE_ADDRESS, # TODO: allow using the public address own_address_type = Address.RANDOM_DEVICE_ADDRESS, # TODO: allow using the public address
peer_address_type = Address.PUBLIC_DEVICE_ADDRESS, peer_address_type = peer_address_type,
peer_address = Address('00:00:00:00:00:00'), peer_address = peer_address,
advertising_channel_map = 7, advertising_channel_map = 7,
advertising_filter_policy = 0 advertising_filter_policy = 0
), check_result=True) ), check_result=True)
@@ -846,7 +907,9 @@ class Device(CompositeEventEmitter):
advertising_enable = 1 advertising_enable = 1
), check_result=True) ), check_result=True)
self.advertising = True self.auto_restart_advertising = auto_restart
self.advertising_type = advertising_type
self.advertising = True
async def stop_advertising(self): async def stop_advertising(self):
# Disable advertising # Disable advertising
@@ -855,7 +918,9 @@ class Device(CompositeEventEmitter):
advertising_enable = 0 advertising_enable = 0
), check_result=True) ), check_result=True)
self.advertising = False self.advertising = False
self.advertising_type = None
self.auto_restart_advertising = False
@property @property
def is_advertising(self): def is_advertising(self):
@@ -1088,9 +1153,9 @@ class Device(CompositeEventEmitter):
if connection_parameters_preferences is None: if connection_parameters_preferences is None:
if connection_parameters_preferences is None: if connection_parameters_preferences is None:
connection_parameters_preferences = { connection_parameters_preferences = {
HCI_LE_1M_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES, HCI_LE_1M_PHY: ConnectionParametersPreferences.default,
HCI_LE_2M_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES, HCI_LE_2M_PHY: ConnectionParametersPreferences.default,
HCI_LE_CODED_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES HCI_LE_CODED_PHY: ConnectionParametersPreferences.default
} }
if self.host.supports_command(HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND): if self.host.supports_command(HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND):
@@ -1610,7 +1675,13 @@ class Device(CompositeEventEmitter):
@host_event_handler @host_event_handler
def on_connection_failure(self, connection_handle, error_code): def on_connection_failure(self, connection_handle, error_code):
logger.debug(f'*** Connection failed: {error_code}') logger.debug(f'*** Connection failed: {HCI_Constant.error_name(error_code)}')
# For directed advertising, this means a timeout
if self.advertising and self.advertising_type.is_directed:
self.advertising = False
# Notify listeners
error = ConnectionError( error = ConnectionError(
error_code, error_code,
'hci', 'hci',
@@ -1633,7 +1704,10 @@ class Device(CompositeEventEmitter):
# Restart advertising if auto-restart is enabled # Restart advertising if auto-restart is enabled
if self.auto_restart_advertising: if self.auto_restart_advertising:
logger.debug('restarting advertising') logger.debug('restarting advertising')
asyncio.create_task(self.start_advertising(auto_restart=self.auto_restart_advertising)) asyncio.create_task(self.start_advertising(
advertising_type = self.advertising_type,
auto_restart = True
))
@host_event_handler @host_event_handler
@with_connection_from_handle @with_connection_from_handle

View File

@@ -1491,7 +1491,7 @@ class HCI_Object:
def dict_to_bytes(object, fields): def dict_to_bytes(object, fields):
result = bytearray() result = bytearray()
for (field_name, field_type) in fields: for (field_name, field_type) in fields:
# The field_type may be a dictionnary with a mapper, parser, serializer, and/or size # The field_type may be a dictionary with a mapper, parser, serializer, and/or size
serializer = None serializer = None
if type(field_type) is dict: if type(field_type) is dict:
if 'serializer' in field_type: if 'serializer' in field_type:
@@ -2826,18 +2826,18 @@ class HCI_LE_Set_Advertising_Parameters_Command(HCI_Command):
See Bluetooth spec @ 7.8.5 LE Set Advertising Parameters Command See Bluetooth spec @ 7.8.5 LE Set Advertising Parameters Command
''' '''
ADV_IND = 0x00 ADV_IND = 0x00
ADV_DIRECT_IND = 0x01 ADV_DIRECT_IND = 0x01
ADV_SCAN_IND = 0x02 ADV_SCAN_IND = 0x02
ADV_NONCONN_IND = 0x03 ADV_NONCONN_IND = 0x03
ADV_DIRECT_IND = 0x04 ADV_DIRECT_IND_LOW_DUTY = 0x04
ADVERTISING_TYPE_NAMES = { ADVERTISING_TYPE_NAMES = {
ADV_IND: 'ADV_IND', ADV_IND: 'ADV_IND',
ADV_DIRECT_IND: 'ADV_DIRECT_IND', ADV_DIRECT_IND: 'ADV_DIRECT_IND',
ADV_SCAN_IND: 'ADV_SCAN_IND', ADV_SCAN_IND: 'ADV_SCAN_IND',
ADV_NONCONN_IND: 'ADV_NONCONN_IND', ADV_NONCONN_IND: 'ADV_NONCONN_IND',
ADV_DIRECT_IND: 'ADV_DIRECT_IND' ADV_DIRECT_IND_LOW_DUTY: 'ADV_DIRECT_IND_LOW_DUTY'
} }
@classmethod @classmethod
@@ -4346,7 +4346,7 @@ class HCI_Encryption_Change_Event(HCI_Event):
E0_OR_AES_CCM = 0x01 E0_OR_AES_CCM = 0x01
AES_CCM = 0x02 AES_CCM = 0x02
ENCYRPTION_ENABLED_NAMES = { ENCRYPTION_ENABLED_NAMES = {
OFF: 'OFF', OFF: 'OFF',
E0_OR_AES_CCM: 'E0_OR_AES_CCM', E0_OR_AES_CCM: 'E0_OR_AES_CCM',
AES_CCM: 'AES_CCM' AES_CCM: 'AES_CCM'
@@ -4354,7 +4354,7 @@ class HCI_Encryption_Change_Event(HCI_Event):
@staticmethod @staticmethod
def encryption_enabled_name(encryption_enabled): def encryption_enabled_name(encryption_enabled):
return name_or_number(HCI_Encryption_Change_Event.ENCYRPTION_ENABLED_NAMES, encryption_enabled) return name_or_number(HCI_Encryption_Change_Event.ENCRYPTION_ENABLED_NAMES, encryption_enabled)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -4701,7 +4701,7 @@ class HCI_Synchronous_Connection_Complete_Event(HCI_Event):
U_LAW_LOG_AIR_MODE: 'u-law log', U_LAW_LOG_AIR_MODE: 'u-law log',
A_LAW_LOG_AIR_MORE: 'A-law log', A_LAW_LOG_AIR_MORE: 'A-law log',
CVSD_AIR_MODE: 'CVSD', CVSD_AIR_MODE: 'CVSD',
TRANSPARENT_DATA_AIR_MODE: 'Transparend Data' TRANSPARENT_DATA_AIR_MODE: 'Transparent Data'
} }
@staticmethod @staticmethod

View File

@@ -29,18 +29,31 @@ from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def main(): async def main():
if len(sys.argv) != 3: if len(sys.argv) < 3:
print('Usage: run_advertiser.py <config-file> <transport-spec>') print('Usage: run_advertiser.py <config-file> <transport-spec> [type] [address]')
print('example: run_advertiser.py device1.json link-relay:ws://localhost:8888/test') print('example: run_advertiser.py device1.json usb:0')
return return
if len(sys.argv) >= 4:
advertising_type = AdvertisingType(int(sys.argv[3]))
else:
advertising_type = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE
if advertising_type.is_directed:
if len(sys.argv) < 5:
print('<address> required for directed advertising')
return
target = Address(sys.argv[4])
else:
target = None
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink): async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
await device.power_on() await device.power_on()
await device.start_advertising() await device.start_advertising(advertising_type=advertising_type, target=target)
await hci_source.wait_for_termination() await hci_source.wait_for_termination()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -27,8 +27,8 @@ def basic_check(x):
parsed_str = str(parsed) parsed_str = str(parsed)
print(x_str) print(x_str)
parsed_bytes = parsed.to_bytes() parsed_bytes = parsed.to_bytes()
assert(x_str == parsed_str) assert x_str == parsed_str
assert(packet == parsed_bytes) assert packet == parsed_bytes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -133,7 +133,7 @@ def test_HCI_Command_Complete_Event():
) )
basic_check(event) basic_check(event)
event = HCI_Packet.from_bytes(event.to_bytes()) event = HCI_Packet.from_bytes(event.to_bytes())
assert(event.return_parameters == 7) assert event.return_parameters == 7
# With a simple status as an integer status # With a simple status as an integer status
event = HCI_Command_Complete_Event( event = HCI_Command_Complete_Event(
@@ -142,7 +142,7 @@ def test_HCI_Command_Complete_Event():
return_parameters = 9 return_parameters = 9
) )
basic_check(event) basic_check(event)
assert(event.return_parameters == 9) assert event.return_parameters == 9
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -383,20 +383,20 @@ def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_address(): def test_address():
a = Address('C4:F2:17:1A:1D:BB') a = Address('C4:F2:17:1A:1D:BB')
assert(not a.is_public) assert not a.is_public
assert(a.is_random) assert a.is_random
assert(a.address_type == Address.RANDOM_DEVICE_ADDRESS) assert a.address_type == Address.RANDOM_DEVICE_ADDRESS
assert(not a.is_resolvable) assert not a.is_resolvable
assert(not a.is_resolved) assert not a.is_resolved
assert(a.is_static) assert a.is_static
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_custom(): def test_custom():
data = bytes([0x77, 0x02, 0x01, 0x03]) data = bytes([0x77, 0x02, 0x01, 0x03])
packet = HCI_CustomPacket(data) packet = HCI_CustomPacket(data)
assert(packet.hci_packet_type == 0x77) assert packet.hci_packet_type == 0x77
assert(packet.payload == data) assert packet.payload == data
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -62,6 +62,57 @@ def test_import():
assert utils assert utils
# -----------------------------------------------------------------------------
def test_app_imports():
from bumble.apps.console import main
assert main
from bumble.apps.controller_info import main
assert main
from bumble.apps.controllers import main
assert main
from bumble.apps.gatt_dump import main
assert main
from bumble.apps.gg_bridge import main
assert main
from bumble.apps.hci_bridge import main
assert main
from bumble.apps.pair import main
assert main
from bumble.apps.scan import main
assert main
from bumble.apps.show import main
assert main
from bumble.apps.unbond import main
assert main
from bumble.apps.usb_probe import main
assert main
# -----------------------------------------------------------------------------
def test_profiles_imports():
from bumble.profiles import (
battery_service,
device_information_service,
heart_rate_service
)
assert battery_service
assert device_information_service
assert heart_rate_service
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
if __name__ == '__main__': if __name__ == '__main__':
test_import() test_import()
test_app_imports()
test_profiles_imports()