This commit is contained in:
Gilles Boccon-Gibod
2022-10-03 16:50:42 -07:00
parent c316e8805f
commit d10dda7e10
10 changed files with 936 additions and 435 deletions

View File

@@ -28,11 +28,16 @@ import click
from collections import OrderedDict from collections import OrderedDict
import colors import colors
from bumble.core import UUID, AdvertisingData from bumble.core import UUID, AdvertisingData, TimeoutError, BT_LE_TRANSPORT
from bumble.device import Device, Connection, Peer from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.transport import open_transport_or_link from bumble.transport import open_transport_or_link
from bumble.gatt import Characteristic from bumble.gatt import Characteristic
from bumble.hci import (
HCI_LE_1M_PHY,
HCI_LE_2M_PHY,
HCI_LE_CODED_PHY,
)
from prompt_toolkit import Application from prompt_toolkit import Application
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
@@ -43,6 +48,7 @@ from prompt_toolkit.styles import Style
from prompt_toolkit.filters import Condition from prompt_toolkit.filters import Condition
from prompt_toolkit.widgets import TextArea, Frame from prompt_toolkit.widgets import TextArea, Frame
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
from prompt_toolkit.data_structures import Point
from prompt_toolkit.layout import ( from prompt_toolkit.layout import (
Layout, Layout,
HSplit, HSplit,
@@ -51,17 +57,20 @@ from prompt_toolkit.layout import (
Float, Float,
FormattedTextControl, FormattedTextControl,
FloatContainer, FloatContainer,
ConditionalContainer ConditionalContainer,
Dimension
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
BUMBLE_USER_DIR = os.path.expanduser('~/.bumble') BUMBLE_USER_DIR = os.path.expanduser('~/.bumble')
DEFAULT_PROMPT_HEIGHT = 20 DEFAULT_RSSI_BAR_WIDTH = 20
DEFAULT_RSSI_BAR_WIDTH = 20 DEFAULT_CONNECTION_TIMEOUT = 30.0
DISPLAY_MIN_RSSI = -100 DISPLAY_MIN_RSSI = -100
DISPLAY_MAX_RSSI = -30 DISPLAY_MAX_RSSI = -30
RSSI_MONITOR_INTERVAL = 5.0 # Seconds
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Globals # Globals
@@ -69,16 +78,57 @@ DISPLAY_MAX_RSSI = -30
App = None App = None
# -----------------------------------------------------------------------------
# Utils
# -----------------------------------------------------------------------------
def le_phy_name(phy_id):
return {
HCI_LE_1M_PHY: '1M',
HCI_LE_2M_PHY: '2M',
HCI_LE_CODED_PHY: 'CODED'
}.get(phy_id, HCI_Constant.le_phy_name(phy_id))
def rssi_bar(rssi):
blocks = ['', '', '', '', '', '', '', '']
bar_width = (rssi - DISPLAY_MIN_RSSI) / (DISPLAY_MAX_RSSI - DISPLAY_MIN_RSSI)
bar_width = min(max(bar_width, 0), 1)
bar_ticks = int(bar_width * DEFAULT_RSSI_BAR_WIDTH * 8)
bar_blocks = ('' * int(bar_ticks / 8)) + blocks[bar_ticks % 8]
return f'{rssi:4} {bar_blocks}'
def parse_phys(phys):
if phys.lower() == '*':
return None
else:
phy_list = []
elements = phys.lower().split(',')
for element in elements:
if element == '1m':
phy_list.append(HCI_LE_1M_PHY)
elif element == '2m':
phy_list.append(HCI_LE_2M_PHY)
elif element == 'coded':
phy_list.append(HCI_LE_CODED_PHY)
else:
raise ValueError('invalid PHY name')
return phy_list
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Console App # Console App
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class ConsoleApp: class ConsoleApp:
def __init__(self): def __init__(self):
self.known_addresses = set() self.known_addresses = set()
self.known_attributes = [] self.known_attributes = []
self.device = None self.device = None
self.connected_peer = None self.connected_peer = None
self.top_tab = 'scan' self.top_tab = 'scan'
self.monitor_rssi = False
self.connection_rssi = None
style = Style.from_dict({ style = Style.from_dict({
'output-field': 'bg:#000044 #ffffff', 'output-field': 'bg:#000044 #ffffff',
@@ -106,6 +156,10 @@ class ConsoleApp:
'on': None, 'on': None,
'off': None 'off': None
}, },
'rssi': {
'on': None,
'off': None
},
'show': { 'show': {
'scan': None, 'scan': None,
'services': None, 'services': None,
@@ -120,10 +174,17 @@ class ConsoleApp:
'services': None, 'services': None,
'attributes': None 'attributes': None
}, },
'request-mtu': None,
'read': LiveCompleter(self.known_attributes), 'read': LiveCompleter(self.known_attributes),
'write': LiveCompleter(self.known_attributes), 'write': LiveCompleter(self.known_attributes),
'subscribe': LiveCompleter(self.known_attributes), 'subscribe': LiveCompleter(self.known_attributes),
'unsubscribe': LiveCompleter(self.known_attributes), 'unsubscribe': LiveCompleter(self.known_attributes),
'set-phy': {
'1m': None,
'2m': None,
'coded': None
},
'set-default-phy': None,
'quit': None, 'quit': None,
'exit': None 'exit': None
}) })
@@ -139,14 +200,16 @@ class ConsoleApp:
self.input_field.accept_handler = self.accept_input self.input_field.accept_handler = self.accept_input
self.output_height = 7 self.output_height = Dimension(min=7, max=7, weight=1)
self.output_lines = [] self.output_lines = []
self.output = FormattedTextControl() self.output = FormattedTextControl(get_cursor_position=lambda: Point(0, max(0, len(self.output_lines) - 1)))
self.output_max_lines = 20
self.scan_results_text = FormattedTextControl() self.scan_results_text = FormattedTextControl()
self.services_text = FormattedTextControl() self.services_text = FormattedTextControl()
self.attributes_text = FormattedTextControl() self.attributes_text = FormattedTextControl()
self.log_text = FormattedTextControl() self.log_text = FormattedTextControl(get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1)))
self.log_height = 20 self.log_height = Dimension(min=7, weight=4)
self.log_max_lines = 100
self.log_lines = [] self.log_lines = []
container = HSplit([ container = HSplit([
@@ -163,11 +226,10 @@ class ConsoleApp:
filter=Condition(lambda: self.top_tab == 'attributes') filter=Condition(lambda: self.top_tab == 'attributes')
), ),
ConditionalContainer( ConditionalContainer(
Frame(Window(self.log_text), title='Log'), Frame(Window(self.log_text, height=self.log_height), title='Log'),
filter=Condition(lambda: self.top_tab == 'log') filter=Condition(lambda: self.top_tab == 'log')
), ),
Frame(Window(self.output), height=self.output_height), Frame(Window(self.output, height=self.output_height)),
# HorizontalLine(),
FormattedTextToolbar(text=self.get_status_bar_text, style='reverse'), FormattedTextToolbar(text=self.get_status_bar_text, style='reverse'),
self.input_field self.input_field
]) ])
@@ -199,6 +261,8 @@ class ConsoleApp:
) )
async def run_async(self, device_config, transport): async def run_async(self, device_config, transport):
rssi_monitoring_task = asyncio.create_task(self.rssi_monitor_loop())
async with await open_transport_or_link(transport) as (hci_source, hci_sink): async with await open_transport_or_link(transport) as (hci_source, hci_sink):
if device_config: if device_config:
self.device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) self.device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
@@ -210,6 +274,8 @@ class ConsoleApp:
# Run the UI # Run the UI
await self.ui.run_async() await self.ui.run_async()
rssi_monitoring_task.cancel()
def add_known_address(self, address): def add_known_address(self, address):
self.known_addresses.add(address) self.known_addresses.add(address)
@@ -224,22 +290,33 @@ class ConsoleApp:
connection_state = 'NONE' connection_state = 'NONE'
encryption_state = '' encryption_state = ''
att_mtu = ''
rssi = '' if self.connection_rssi is None else rssi_bar(self.connection_rssi)
if self.device: if self.device:
if self.device.is_connecting: if self.device.is_connecting:
connection_state = 'CONNECTING' connection_state = 'CONNECTING'
elif self.connected_peer: elif self.connected_peer:
connection = self.connected_peer.connection connection = self.connected_peer.connection
connection_parameters = f'{connection.parameters.connection_interval}/{connection.parameters.connection_latency}/{connection.parameters.supervision_timeout}' connection_parameters = f'{connection.parameters.connection_interval}/{connection.parameters.peripheral_latency}/{connection.parameters.supervision_timeout}'
connection_state = f'{connection.peer_address} {connection_parameters} {connection.data_length}' if connection.transport == BT_LE_TRANSPORT:
phy_state = f' RX={le_phy_name(connection.phy.rx_phy)}/TX={le_phy_name(connection.phy.tx_phy)}'
else:
phy_state = ''
connection_state = f'{connection.peer_address} {connection_parameters} {connection.data_length}{phy_state}'
encryption_state = 'ENCRYPTED' if connection.is_encrypted else 'NOT ENCRYPTED' encryption_state = 'ENCRYPTED' if connection.is_encrypted else 'NOT ENCRYPTED'
att_mtu = f'ATT_MTU: {connection.att_mtu}'
return [ return [
('ansigreen', f' SCAN: {scanning} '), ('ansigreen', f' SCAN: {scanning} '),
('', ' '), ('', ' '),
('ansiblue', f' CONNECTION: {connection_state} '), ('ansiblue', f' CONNECTION: {connection_state} '),
('', ' '), ('', ' '),
('ansimagenta', f' {encryption_state} ') ('ansimagenta', f' {encryption_state} '),
('', ' '),
('ansicyan', f' {att_mtu} '),
('', ' '),
('ansiyellow', f' {rssi} ')
] ]
def show_error(self, title, details = None): def show_error(self, title, details = None):
@@ -286,7 +363,7 @@ class ConsoleApp:
def append_to_output(self, line, invalidate=True): def append_to_output(self, line, invalidate=True):
if type(line) is str: if type(line) is str:
line = [('', line)] line = [('', line)]
self.output_lines = self.output_lines[-(self.output_height - 3):] self.output_lines = self.output_lines[-self.output_max_lines:]
self.output_lines.append(line) self.output_lines.append(line)
formatted_text = [] formatted_text = []
for line in self.output_lines: for line in self.output_lines:
@@ -298,7 +375,7 @@ class ConsoleApp:
def append_to_log(self, lines, invalidate=True): def append_to_log(self, lines, invalidate=True):
self.log_lines.extend(lines.split('\n')) self.log_lines.extend(lines.split('\n'))
self.log_lines = self.log_lines[-(self.log_height - 3):] self.log_lines = self.log_lines[-self.log_max_lines:]
self.log_text.text = ANSI('\n'.join(self.log_lines)) self.log_text.text = ANSI('\n'.join(self.log_lines))
if invalidate: if invalidate:
self.ui.invalidate() self.ui.invalidate()
@@ -351,6 +428,12 @@ class ConsoleApp:
if characteristic.handle == attribute_handle: if characteristic.handle == attribute_handle:
return characteristic return characteristic
async def rssi_monitor_loop(self):
while True:
if self.monitor_rssi and self.connected_peer:
self.connection_rssi = await self.connected_peer.connection.get_rssi()
await asyncio.sleep(RSSI_MONITOR_INTERVAL)
async def command(self, command): async def command(self, command):
try: try:
(keyword, *params) = command.strip().split(' ') (keyword, *params) = command.strip().split(' ')
@@ -379,39 +462,73 @@ class ConsoleApp:
else: else:
self.show_error('unsupported arguments for scan command') self.show_error('unsupported arguments for scan command')
async def do_rssi(self, params):
if len(params) == 0:
# Toggle monitoring
self.monitor_rssi = not self.monitor_rssi
elif params[0] == 'on':
self.monitor_rssi = True
elif params[0] == 'off':
self.monitor_rssi = False
else:
self.show_error('unsupported arguments for rssi command')
async def do_connect(self, params): async def do_connect(self, params):
if len(params) != 1: if len(params) != 1 and len(params) != 2:
self.show_error('invalid syntax', 'expected connect <address>') self.show_error('invalid syntax', 'expected connect <address> [phys]')
return return
if len(params) == 1:
phys = None
else:
phys = parse_phys(params[1])
if phys is None:
connection_parameters_preferences = None
else:
connection_parameters_preferences = {
phy: ConnectionParametersPreferences()
for phy in phys
}
self.append_to_output('connecting...') self.append_to_output('connecting...')
await self.device.connect(params[0])
self.top_tab = 'services' try:
await self.device.connect(
params[0],
connection_parameters_preferences=connection_parameters_preferences,
timeout=DEFAULT_CONNECTION_TIMEOUT
)
self.top_tab = 'services'
except TimeoutError:
self.show_error('connection timed out')
async def do_disconnect(self, params): async def do_disconnect(self, params):
if not self.connected_peer: if self.device.connecting:
self.show_error('not connected') await self.device.cancel_connection()
return else:
if not self.connected_peer:
self.show_error('not connected')
return
await self.connected_peer.connection.disconnect() await self.connected_peer.connection.disconnect()
async def do_update_parameters(self, params): async def do_update_parameters(self, params):
if len(params) != 1 or len(params[0].split('/')) != 3: if len(params) != 1 or len(params[0].split('/')) != 3:
self.show_error('invalid syntax', 'expected update-parameters <interval-min>-<interval-max>/<latency>/<supervision>') self.show_error('invalid syntax', 'expected update-parameters <interval-min>-<interval-max>/<max-latency>/<supervision>')
return return
if not self.connected_peer: if not self.connected_peer:
self.show_error('not connected') self.show_error('not connected')
return return
connection_intervals, connection_latency, supervision_timeout = params[0].split('/') connection_intervals, max_latency, supervision_timeout = params[0].split('/')
connection_interval_min, connection_interval_max = [int(x) for x in connection_intervals.split('-')] connection_interval_min, connection_interval_max = [int(x) for x in connection_intervals.split('-')]
connection_latency = int(connection_latency) max_latency = int(max_latency)
supervision_timeout = int(supervision_timeout) supervision_timeout = int(supervision_timeout)
await self.connected_peer.connection.update_parameters( await self.connected_peer.connection.update_parameters(
connection_interval_min, connection_interval_min,
connection_interval_max, connection_interval_max,
connection_latency, max_latency,
supervision_timeout supervision_timeout
) )
@@ -442,6 +559,25 @@ class ConsoleApp:
self.top_tab = params[0] self.top_tab = params[0]
self.ui.invalidate() self.ui.invalidate()
async def do_get_phy(self, params):
if not self.connected_peer:
self.show_error('not connected')
return
phy = await self.connected_peer.connection.get_phy()
self.append_to_output(f'PHY: RX={HCI_Constant.le_phy_name(phy[0])}, TX={HCI_Constant.le_phy_name(phy[1])}')
async def do_request_mtu(self, params):
if len(params) != 1:
self.show_error('invalid syntax', 'expected request-mtu <mtu>')
return
if not self.connected_peer:
self.show_error('not connected')
return
await self.connected_peer.request_mtu(int(params[0]))
async def do_discover(self, params): async def do_discover(self, params):
if not params: if not params:
self.show_error('invalid syntax', 'expected discover services|attributes') self.show_error('invalid syntax', 'expected discover services|attributes')
@@ -454,14 +590,14 @@ class ConsoleApp:
await self.discover_attributes() await self.discover_attributes()
async def do_read(self, params): async def do_read(self, params):
if not self.connected_peer:
self.show_error('not connected')
return
if len(params) != 1: if len(params) != 1:
self.show_error('invalid syntax', 'expected read <attribute>') self.show_error('invalid syntax', 'expected read <attribute>')
return return
if not self.connected_peer:
self.show_error('not connected')
return
characteristic = self.find_characteristic(params[0]) characteristic = self.find_characteristic(params[0])
if characteristic is None: if characteristic is None:
self.show_error('no such characteristic') self.show_error('no such characteristic')
@@ -530,6 +666,42 @@ class ConsoleApp:
await characteristic.unsubscribe() await characteristic.unsubscribe()
async def do_set_phy(self, params):
if len(params) != 1:
self.show_error('invalid syntax', 'expected set-phy <tx_rx_phys>|<tx_phys>/<rx_phys>')
return
if not self.connected_peer:
self.show_error('not connected')
return
if '/' in params[0]:
tx_phys, rx_phys = params[0].split('/')
else:
tx_phys = params[0]
rx_phys = tx_phys
await self.connected_peer.connection.set_phy(
tx_phys=parse_phys(tx_phys),
rx_phys=parse_phys(rx_phys)
)
async def do_set_default_phy(self, params):
if len(params) != 1:
self.show_error('invalid syntax', 'expected set-default-phy <tx_rx_phys>|<tx_phys>/<rx_phys>')
return
if '/' in params[0]:
tx_phys, rx_phys = params[0].split('/')
else:
tx_phys = params[0]
rx_phys = tx_phys
await self.device.set_default_phy(
tx_phys=parse_phys(tx_phys),
rx_phys=parse_phys(rx_phys)
)
async def do_exit(self, params): async def do_exit(self, params):
self.ui.exit() self.ui.exit()
@@ -548,12 +720,14 @@ class DeviceListener(Device.Listener, Connection.Listener):
@AsyncRunner.run_in_task() @AsyncRunner.run_in_task()
async def on_connection(self, connection): async def on_connection(self, connection):
self.app.connected_peer = Peer(connection) self.app.connected_peer = Peer(connection)
self.app.connection_rssi = None
self.app.append_to_output(f'connected to {self.app.connected_peer}') self.app.append_to_output(f'connected to {self.app.connected_peer}')
connection.listener = self connection.listener = self
def on_disconnection(self, reason): def on_disconnection(self, reason):
self.app.append_to_output(f'disconnected from {self.app.connected_peer}, reason: {HCI_Constant.error_name(reason)}') self.app.append_to_output(f'disconnected from {self.app.connected_peer}, reason: {HCI_Constant.error_name(reason)}')
self.app.connected_peer = None self.app.connected_peer = None
self.app.connection_rssi = None
def on_connection_parameters_update(self): def on_connection_parameters_update(self):
self.app.append_to_output(f'connection parameters update: {self.app.connected_peer.connection.parameters}') self.app.append_to_output(f'connection parameters update: {self.app.connected_peer.connection.parameters}')
@@ -576,7 +750,7 @@ class DeviceListener(Device.Listener, Connection.Listener):
if entry: if entry:
entry.ad_data = advertisement.data entry.ad_data = advertisement.data
entry.rssi = advertisement.rssi entry.rssi = advertisement.rssi
entry.connectable = advertisement.connectable entry.connectable = advertisement.is_connectable
else: else:
self.app.add_known_address(str(advertisement.address)) self.app.add_known_address(str(advertisement.address))
self.scan_results[entry_key] = ScanResult(advertisement.address, advertisement.address.address_type, advertisement.data, advertisement.rssi, advertisement.is_connectable) self.scan_results[entry_key] = ScanResult(advertisement.address, advertisement.address.address_type, advertisement.data, advertisement.rssi, advertisement.is_connectable)
@@ -616,12 +790,7 @@ class ScanResult:
name = '' name = ''
# RSSI bar # RSSI bar
blocks = ['', '', '', '', '', '', '', ''] bar_string = rssi_bar(self.rssi)
bar_width = (self.rssi - DISPLAY_MIN_RSSI) / (DISPLAY_MAX_RSSI - DISPLAY_MIN_RSSI)
bar_width = min(max(bar_width, 0), 1)
bar_ticks = int(bar_width * DEFAULT_RSSI_BAR_WIDTH * 8)
bar_blocks = ('' * int(bar_ticks / 8)) + blocks[bar_ticks % 8]
bar_string = f'{self.rssi} {bar_blocks}'
bar_padding = ' ' * (DEFAULT_RSSI_BAR_WIDTH + 5 - len(bar_string)) bar_padding = ' ' * (DEFAULT_RSSI_BAR_WIDTH + 5 - len(bar_string))
return f'{address_color(str(self.address))} [{type_color(address_type_string)}] {bar_string} {bar_padding} {name}' return f'{address_color(str(self.address))} [{type_color(address_type_string)}] {bar_string} {bar_padding} {name}'

View File

@@ -25,8 +25,8 @@ from bumble.device import Device
from bumble.transport import open_transport_or_link from bumble.transport import open_transport_or_link
from bumble.keys import JsonKeyStore from bumble.keys import JsonKeyStore
from bumble.smp import AddressResolver from bumble.smp import AddressResolver
from bumble.hci import HCI_LE_Advertising_Report_Event from bumble.device import Advertisement
from bumble.core import AdvertisingData from bumble.hci import HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -48,16 +48,19 @@ class AdvertisementPrinter:
self.min_rssi = min_rssi self.min_rssi = min_rssi
self.resolver = resolver self.resolver = resolver
def print_advertisement(self, address, address_color, ad_data, rssi): def print_advertisement(self, advertisement):
if self.min_rssi is not None and rssi < self.min_rssi: address = advertisement.address
address_color = 'yellow' if advertisement.is_connectable else 'red'
if self.min_rssi is not None and advertisement.rssi < self.min_rssi:
return return
address_qualifier = '' address_qualifier = ''
resolution_qualifier = '' resolution_qualifier = ''
if self.resolver and address.is_resolvable: if self.resolver and advertisement.address.is_resolvable:
resolved = self.resolver.resolve(address) resolved = self.resolver.resolve(advertisement.address)
if resolved is not None: if resolved is not None:
resolution_qualifier = f'(resolved from {address})' resolution_qualifier = f'(resolved from {advertisement.address})'
address = resolved address = resolved
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type] address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type]
@@ -74,18 +77,30 @@ class AdvertisementPrinter:
type_color = 'blue' type_color = 'blue'
address_qualifier = '(non-resolvable)' address_qualifier = '(non-resolvable)'
rssi_bar = make_rssi_bar(rssi)
separator = '\n ' separator = '\n '
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}RSSI:{rssi:4} {rssi_bar}{separator}{ad_data.to_string(separator)}\n') rssi_bar = make_rssi_bar(advertisement.rssi)
if not advertisement.is_legacy:
phy_info = (
f'PHY: {HCI_Constant.le_phy_name(advertisement.primary_phy)}/'
f'{HCI_Constant.le_phy_name(advertisement.secondary_phy)} '
f'{separator}'
)
else:
phy_info = ''
print(
f'>>> {color(address, address_color)} '
f'[{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}'
f'{phy_info}'
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
f'{advertisement.data.to_string(separator)}\n')
def on_advertisement(self, advertisement): def on_advertisement(self, advertisement):
address_color = 'yellow' if advertisement.is_connectable else 'red' self.print_advertisement(advertisement)
self.print_advertisement(advertisement.address, address_color, advertisement.data, advertisement.rssi)
def on_advertising_report(self, report): def on_advertising_report(self, report):
print(f'{color("EVENT", "green")}: {HCI_LE_Advertising_Report_Event.event_type_name(report.event_type)}') print(f'{color("EVENT", "green")}: {report.event_type_string()}')
data = AdvertisingData.from_bytes(report.data) self.print_advertisement(Advertisement.from_advertising_report(report))
self.print_advertisement(report.address, 'yellow', data, report.rssi)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -94,6 +109,7 @@ async def scan(
passive, passive,
scan_interval, scan_interval,
scan_window, scan_window,
phy,
filter_duplicates, filter_duplicates,
raw, raw,
keystore_file, keystore_file,
@@ -126,11 +142,18 @@ async def scan(
device.on('advertisement', printer.on_advertisement) device.on('advertisement', printer.on_advertisement)
await device.power_on() await device.power_on()
if phy is None:
scanning_phys = [HCI_LE_1M_PHY, HCI_LE_CODED_PHY]
else:
scanning_phys = [{'1m': HCI_LE_1M_PHY, 'coded': HCI_LE_CODED_PHY}[phy]]
await device.start_scanning( await device.start_scanning(
active=(not passive), active=(not passive),
scan_interval=scan_interval, scan_interval=scan_interval,
scan_window=scan_window, scan_window=scan_window,
filter_duplicates=filter_duplicates filter_duplicates=filter_duplicates,
scanning_phys=scanning_phys
) )
await hci_source.wait_for_termination() await hci_source.wait_for_termination()
@@ -142,14 +165,15 @@ async def scan(
@click.option('--passive', is_flag=True, default=False, help='Perform passive scanning') @click.option('--passive', is_flag=True, default=False, help='Perform passive scanning')
@click.option('--scan-interval', type=int, default=60, help='Scan interval') @click.option('--scan-interval', type=int, default=60, help='Scan interval')
@click.option('--scan-window', type=int, default=60, help='Scan window') @click.option('--scan-window', type=int, default=60, help='Scan window')
@click.option('--phy', type=click.Choice(['1m', 'coded']), help='Only scan on the specified PHY')
@click.option('--filter-duplicates', type=bool, default=True, help='Filter duplicates at the controller level') @click.option('--filter-duplicates', type=bool, default=True, help='Filter duplicates at the controller level')
@click.option('--raw', is_flag=True, default=False, help='Listen for raw advertising reports instead of processed ones') @click.option('--raw', is_flag=True, default=False, help='Listen for raw advertising reports instead of processed ones')
@click.option('--keystore-file', help='Keystore file to use when resolving addresses') @click.option('--keystore-file', help='Keystore file to use when resolving addresses')
@click.option('--device-config', help='Device config file for the scanning device') @click.option('--device-config', help='Device config file for the scanning device')
@click.argument('transport') @click.argument('transport')
def main(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport): def main(min_rssi, passive, scan_interval, scan_window, phy, filter_duplicates, raw, keystore_file, device_config, transport):
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
asyncio.run(scan(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport)) asyncio.run(scan(min_rssi, passive, scan_interval, scan_window, phy, filter_duplicates, raw, keystore_file, device_config, transport))
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -259,15 +259,15 @@ class Controller:
# Then say that the connection has completed # Then say that the connection has completed
self.send_hci_packet(HCI_LE_Connection_Complete_Event( self.send_hci_packet(HCI_LE_Connection_Complete_Event(
status = HCI_SUCCESS, status = HCI_SUCCESS,
connection_handle = connection.handle, connection_handle = connection.handle,
role = connection.role, role = connection.role,
peer_address_type = peer_address_type, peer_address_type = peer_address_type,
peer_address = peer_address, peer_address = peer_address,
conn_interval = 10, # FIXME connection_interval = 10, # FIXME
conn_latency = 0, # FIXME peripheral_latency = 0, # FIXME
supervision_timeout = 10, # FIXME supervision_timeout = 10, # FIXME
master_clock_accuracy = 7 # FIXME central_clock_accuracy = 7 # FIXME
)) ))
def on_link_central_disconnected(self, peer_address, reason): def on_link_central_disconnected(self, peer_address, reason):
@@ -313,15 +313,15 @@ class Controller:
# Say that the connection has completed # Say that the connection has completed
self.send_hci_packet(HCI_LE_Connection_Complete_Event( self.send_hci_packet(HCI_LE_Connection_Complete_Event(
status = status, status = status,
connection_handle = connection.handle if connection else 0, connection_handle = connection.handle if connection else 0,
role = BT_CENTRAL_ROLE, role = BT_CENTRAL_ROLE,
peer_address_type = le_create_connection_command.peer_address_type, peer_address_type = le_create_connection_command.peer_address_type,
peer_address = le_create_connection_command.peer_address, peer_address = le_create_connection_command.peer_address,
conn_interval = le_create_connection_command.conn_interval_min, connection_interval = le_create_connection_command.connection_interval_min,
conn_latency = le_create_connection_command.conn_latency, peripheral_latency = le_create_connection_command.max_latency,
supervision_timeout = le_create_connection_command.supervision_timeout, supervision_timeout = le_create_connection_command.supervision_timeout,
master_clock_accuracy = 0 central_clock_accuracy = 0
)) ))
def on_link_peripheral_disconnection_complete(self, disconnection_command, status): def on_link_peripheral_disconnection_complete(self, disconnection_command, status):

View File

@@ -831,13 +831,17 @@ class AdvertisingData:
# Connection Parameters # Connection Parameters
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class ConnectionParameters: class ConnectionParameters:
def __init__(self, connection_interval, connection_latency, supervision_timeout): def __init__(self, connection_interval, peripheral_latency, supervision_timeout):
self.connection_interval = connection_interval self.connection_interval = connection_interval
self.connection_latency = connection_latency self.peripheral_latency = peripheral_latency
self.supervision_timeout = supervision_timeout self.supervision_timeout = supervision_timeout
def __str__(self): def __str__(self):
return f'ConnectionParameters(connection_interval={self.connection_interval}, connection_latency={self.connection_latency}, supervision_timeout={self.supervision_timeout}' return (
f'ConnectionParameters(connection_interval={self.connection_interval}, '
f'peripheral_latency={self.peripheral_latency}, '
f'supervision_timeout={self.supervision_timeout}'
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -19,6 +19,7 @@ import json
import asyncio import asyncio
import logging import logging
from contextlib import asynccontextmanager, AsyncExitStack from contextlib import asynccontextmanager, AsyncExitStack
from dataclasses import dataclass
from .hci import * from .hci import *
from .host import Host from .host import Host
@@ -41,20 +42,30 @@ logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
DEVICE_DEFAULT_ADDRESS = '00:00:00:00:00:00' DEVICE_DEFAULT_ADDRESS = '00:00:00:00:00:00'
DEVICE_DEFAULT_ADVERTISING_INTERVAL = 1000 # ms DEVICE_DEFAULT_ADVERTISING_INTERVAL = 1000 # ms
DEVICE_DEFAULT_ADVERTISING_DATA = '' DEVICE_DEFAULT_ADVERTISING_DATA = ''
DEVICE_DEFAULT_NAME = 'Bumble' DEVICE_DEFAULT_NAME = 'Bumble'
DEVICE_DEFAULT_INQUIRY_LENGTH = 8 # 10.24 seconds DEVICE_DEFAULT_INQUIRY_LENGTH = 8 # 10.24 seconds
DEVICE_DEFAULT_CLASS_OF_DEVICE = 0 DEVICE_DEFAULT_CLASS_OF_DEVICE = 0
DEVICE_DEFAULT_SCAN_RESPONSE_DATA = b'' DEVICE_DEFAULT_SCAN_RESPONSE_DATA = b''
DEVICE_DEFAULT_DATA_LENGTH = (27, 328, 27, 328) DEVICE_DEFAULT_DATA_LENGTH = (27, 328, 27, 328)
DEVICE_DEFAULT_SCAN_INTERVAL = 60 # ms DEVICE_DEFAULT_SCAN_INTERVAL = 60 # ms
DEVICE_DEFAULT_SCAN_WINDOW = 60 # ms DEVICE_DEFAULT_SCAN_WINDOW = 60 # ms
DEVICE_MIN_SCAN_INTERVAL = 25 DEVICE_DEFAULT_CONNECT_TIMEOUT = None # No timeout
DEVICE_MAX_SCAN_INTERVAL = 10240 DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL = 60 # ms
DEVICE_MIN_SCAN_WINDOW = 25 DEVICE_DEFAULT_CONNECT_SCAN_WINDOW = 60 # ms
DEVICE_MAX_SCAN_WINDOW = 10240 DEVICE_DEFAULT_CONNECTION_INTERVAL_MIN = 15 # ms
DEVICE_DEFAULT_CONNECTION_INTERVAL_MAX = 30 # ms
DEVICE_DEFAULT_CONNECTION_MAX_LATENCY = 0
DEVICE_DEFAULT_CONNECTION_SUPERVISION_TIMEOUT = 720 # ms
DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH = 0 # ms
DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH = 0 # ms
DEVICE_MIN_SCAN_INTERVAL = 25
DEVICE_MAX_SCAN_INTERVAL = 10240
DEVICE_MIN_SCAN_WINDOW = 25
DEVICE_MAX_SCAN_WINDOW = 10240
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Classes # Classes
@@ -83,6 +94,8 @@ class Advertisement:
is_directed = False, is_directed = False,
is_scannable = False, is_scannable = False,
is_scan_response = False, is_scan_response = False,
is_complete = True,
is_truncated = False,
primary_phy = 0, primary_phy = 0,
secondary_phy = 0, secondary_phy = 0,
tx_power = HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE, tx_power = HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE,
@@ -97,6 +110,8 @@ class Advertisement:
self.is_directed = is_directed self.is_directed = is_directed
self.is_scannable = is_scannable self.is_scannable = is_scannable
self.is_scan_response = is_scan_response self.is_scan_response = is_scan_response
self.is_complete = is_complete
self.is_truncated = is_truncated
self.primary_phy = primary_phy self.primary_phy = primary_phy
self.secondary_phy = secondary_phy self.secondary_phy = secondary_phy
self.tx_power = tx_power self.tx_power = tx_power
@@ -139,6 +154,8 @@ class ExtendedAdvertisement(Advertisement):
is_directed = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0, is_directed = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
is_scannable = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0, is_scannable = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
is_scan_response = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0, is_scan_response = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
is_complete = (report.event_type >> 5 & 3) == HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
is_truncated = (report.event_type >> 5 & 3) == HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
primary_phy = report.primary_phy, primary_phy = report.primary_phy,
secondary_phy = report.secondary_phy, secondary_phy = report.secondary_phy,
tx_power = report.tx_power, tx_power = report.tx_power,
@@ -150,46 +167,36 @@ class ExtendedAdvertisement(Advertisement):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class AdvertisementDataAccumulator: class AdvertisementDataAccumulator:
def __init__(self, passive=False): def __init__(self, passive=False):
self.passive = passive self.passive = passive
self.last_event_type = None self.last_advertisement = None
self.advertisement = None self.last_data = b''
self.data = b''
def update(self, report): def update(self, report):
if isinstance(report, HCI_LE_Advertising_Report_Event.Report): advertisement = Advertisement.from_advertising_report(report)
if report.event_type == HCI_LE_Advertising_Report_Event.SCAN_RSP: result = None
if self.last_event_type in {
HCI_LE_Advertising_Report_Event.ADV_IND,
HCI_LE_Advertising_Report_Event.ADV_SCAN_IND
}:
# This is the response to a scannable advertisement
self.advertisement = Advertisement.from_advertising_report(report)
self.advertisement.data = AdvertisingData.from_bytes(self.data + report.data)
self.advertisement.is_connectable = (self.last_event_type == HCI_LE_Advertising_Report_Event.ADV_IND)
self.advertisement.is_scannable = True
else:
# Unexpected scan response
self.advertisement = None
# Reset the data if advertisement.is_scan_response:
self.data = b'' if self.last_advertisement is not None and not self.last_advertisement.is_scan_response:
else: # This is the response to a scannable advertisement
self.data = report.data result = Advertisement.from_advertising_report(report)
result.is_connectable = self.last_advertisement.is_connectable
result.is_scannable = True
result.data = AdvertisingData.from_bytes(self.last_data + report.data)
self.last_data = b''
else:
if (
self.passive or
(not advertisement.is_scannable) or
(self.last_advertisement is not None and not self.last_advertisement.is_scan_response)
):
# Don't wait for a scan response
result = Advertisement.from_advertising_report(report)
if self.passive or report.event_type in { self.last_data = report.data
HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
HCI_LE_Advertising_Report_Event.ADV_NONCONN_IND
} or self.last_event_type not in {
None,
HCI_LE_Advertising_Report_Event.SCAN_RSP
}:
# Don't wait for a scan response
self.advertisement = Advertisement.from_advertising_report(report)
else:
# Wait for a scan response
self.advertisement = None
self.last_event_type = report.event_type self.last_advertisement = advertisement
return result
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -206,7 +213,9 @@ class Peer:
return self.gatt_client.services return self.gatt_client.services
async def request_mtu(self, mtu): async def request_mtu(self, mtu):
return await self.gatt_client.request_mtu(mtu) mtu = await self.gatt_client.request_mtu(mtu)
self.connection.att_mtu = mtu
self.connection.emit('connection_att_mtu_update')
async def discover_service(self, uuid): async def discover_service(self, uuid):
return await self.gatt_client.discover_service(uuid) return await self.gatt_client.discover_service(uuid)
@@ -275,11 +284,23 @@ class Peer:
async def __aexit__(self, exc_type, exc_value, traceback): async def __aexit__(self, exc_type, exc_value, traceback):
pass pass
def __str__(self): def __str__(self):
return f'{self.connection.peer_address} as {self.connection.role_name}' return f'{self.connection.peer_address} as {self.connection.role_name}'
# -----------------------------------------------------------------------------
@dataclass
class ConnectionParametersPreferences:
connection_interval_min: int = DEVICE_DEFAULT_CONNECTION_INTERVAL_MIN
connection_interval_max: int = DEVICE_DEFAULT_CONNECTION_INTERVAL_MAX
max_latency: int = DEVICE_DEFAULT_CONNECTION_MAX_LATENCY
supervision_timeout: int = DEVICE_DEFAULT_CONNECTION_SUPERVISION_TIMEOUT
min_ce_length: int = DEVICE_DEFAULT_CONNECTION_MIN_CE_LENGTH
max_ce_length: int = DEVICE_DEFAULT_CONNECTION_MAX_CE_LENGTH
DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES = ConnectionParametersPreferences()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Connection(CompositeEventEmitter): class Connection(CompositeEventEmitter):
@composite_listener @composite_listener
@@ -308,7 +329,17 @@ class Connection(CompositeEventEmitter):
def on_connection_encryption_key_refresh(self): def on_connection_encryption_key_refresh(self):
pass pass
def __init__(self, device, handle, transport, peer_address, peer_resolvable_address, role, parameters): def __init__(
self,
device,
handle,
transport,
peer_address,
peer_resolvable_address,
role,
parameters,
phy
):
super().__init__() super().__init__()
self.device = device self.device = device
self.handle = handle self.handle = handle
@@ -320,7 +351,7 @@ class Connection(CompositeEventEmitter):
self.parameters = parameters self.parameters = parameters
self.encryption = 0 self.encryption = 0
self.authenticated = False self.authenticated = False
self.phy = ConnectionPHY(HCI_LE_1M_PHY, HCI_LE_1M_PHY) self.phy = phy
self.att_mtu = ATT_DEFAULT_MTU self.att_mtu = ATT_DEFAULT_MTU
self.data_length = DEVICE_DEFAULT_DATA_LENGTH self.data_length = DEVICE_DEFAULT_DATA_LENGTH
self.gatt_client = None # Per-connection client self.gatt_client = None # Per-connection client
@@ -373,19 +404,28 @@ class Connection(CompositeEventEmitter):
async def update_parameters( async def update_parameters(
self, self,
conn_interval_min, connection_interval_min,
conn_interval_max, connection_interval_max,
conn_latency, max_latency,
supervision_timeout supervision_timeout
): ):
return await self.device.update_connection_parameters( return await self.device.update_connection_parameters(
self, self,
conn_interval_min, connection_interval_min,
conn_interval_max, connection_interval_max,
conn_latency, max_latency,
supervision_timeout supervision_timeout
) )
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
async def get_rssi(self):
return await self.device.get_connection_rssi(self)
async def get_phy(self):
return await self.device.get_connection_phy(self)
# [Classic only] # [Classic only]
async def request_remote_name(self): async def request_remote_name(self):
return await self.device.request_remote_name(self) return await self.device.request_remote_name(self)
@@ -549,25 +589,26 @@ class Device(CompositeEventEmitter):
def __init__(self, name = None, address = None, config = None, host = None, generic_access_service = True): def __init__(self, name = None, address = None, config = None, host = None, generic_access_service = True):
super().__init__() super().__init__()
self._host = None self._host = None
self.powered_on = False self.powered_on = False
self.advertising = False self.advertising = False
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)
self.sdp_server = sdp.Server(self) self.sdp_server = sdp.Server(self)
self.l2cap_channel_manager = l2cap.ChannelManager( self.l2cap_channel_manager = l2cap.ChannelManager(
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]) [l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]
self.advertisement_data = {} )
self.scanning = False self.advertisement_accumulators = {} # Accumulators, by address
self.scanning_is_passive = False self.scanning = False
self.discovering = False self.scanning_is_passive = False
self.connecting = False self.discovering = False
self.disconnecting = False self.connecting = False
self.connections = {} # Connections, by connection handle self.disconnecting = False
self.classic_enabled = False self.connections = {} # Connections, by connection handle
self.inquiry_response = None self.classic_enabled = False
self.address_resolver = None self.inquiry_response = None
self.address_resolver = None
# Use the initial config or a default # Use the initial config or a default
self.public_address = Address('00:00:00:00:00:00') self.public_address = Address('00:00:00:00:00:00')
@@ -676,9 +717,12 @@ class Device(CompositeEventEmitter):
def send_l2cap_pdu(self, connection_handle, cid, pdu): def send_l2cap_pdu(self, connection_handle, cid, pdu):
self.host.send_l2cap_pdu(connection_handle, cid, pdu) self.host.send_l2cap_pdu(connection_handle, cid, pdu)
async def send_command(self, command): async def send_command(self, command, check_result=False):
try: try:
return await asyncio.wait_for(self.host.send_command(command), self.command_timeout) return await asyncio.wait_for(
self.host.send_command(command, check_result),
self.command_timeout
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning('!!! Command timed out') logger.warning('!!! Command timed out')
@@ -701,10 +745,10 @@ class Device(CompositeEventEmitter):
# Set the controller address # Set the controller address
await self.send_command(HCI_LE_Set_Random_Address_Command( await self.send_command(HCI_LE_Set_Random_Address_Command(
random_address = self.random_address random_address = self.random_address
)) ), check_result=True)
# Load the address resolving list # Load the address resolving list
if self.keystore: if self.keystore and self.host.supports_command(HCI_LE_CLEAR_RESOLVING_LIST_COMMAND):
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) await self.send_command(HCI_LE_Clear_Resolving_List_Command())
resolving_keys = await self.keystore.get_resolving_keys() resolving_keys = await self.keystore.get_resolving_keys()
@@ -754,6 +798,19 @@ class Device(CompositeEventEmitter):
def supports_le_feature(self, feature): def supports_le_feature(self, feature):
return self.host.supports_le_feature(feature) return self.host.supports_le_feature(feature)
def supports_le_phy(self, phy):
if phy == HCI_LE_1M_PHY:
return True
feature_map = {
HCI_LE_2M_PHY: HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
HCI_LE_CODED_PHY: HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE
}
if phy not in feature_map:
raise ValueError('invalid 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=False):
self.auto_restart_advertising = auto_restart self.auto_restart_advertising = auto_restart
@@ -764,12 +821,12 @@ class Device(CompositeEventEmitter):
# Set/update the advertising data # Set/update the advertising data
await self.send_command(HCI_LE_Set_Advertising_Data_Command( await self.send_command(HCI_LE_Set_Advertising_Data_Command(
advertising_data = self.advertising_data advertising_data = self.advertising_data
)) ), check_result=True)
# Set/update the scan response data # Set/update the scan response data
await self.send_command(HCI_LE_Set_Scan_Response_Data_Command( await self.send_command(HCI_LE_Set_Scan_Response_Data_Command(
scan_response_data = self.scan_response_data scan_response_data = self.scan_response_data
)) ), check_result=True)
# 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(
@@ -782,12 +839,12 @@ class Device(CompositeEventEmitter):
peer_address = Address('00:00:00:00:00:00'), peer_address = Address('00:00:00:00:00:00'),
advertising_channel_map = 7, advertising_channel_map = 7,
advertising_filter_policy = 0 advertising_filter_policy = 0
)) ), check_result=True)
# Enable advertising # Enable advertising
await self.send_command(HCI_LE_Set_Advertising_Enable_Command( await self.send_command(HCI_LE_Set_Advertising_Enable_Command(
advertising_enable = 1 advertising_enable = 1
)) ), check_result=True)
self.advertising = True self.advertising = True
@@ -796,7 +853,7 @@ class Device(CompositeEventEmitter):
if self.advertising: if self.advertising:
await self.send_command(HCI_LE_Set_Advertising_Enable_Command( await self.send_command(HCI_LE_Set_Advertising_Enable_Command(
advertising_enable = 0 advertising_enable = 0
)) ), check_result=True)
self.advertising = False self.advertising = False
@@ -810,7 +867,8 @@ class Device(CompositeEventEmitter):
scan_interval=DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms scan_interval=DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms
scan_window=DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms scan_window=DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms
own_address_type=Address.RANDOM_DEVICE_ADDRESS, own_address_type=Address.RANDOM_DEVICE_ADDRESS,
filter_duplicates=False filter_duplicates=False,
scanning_phys=(HCI_LE_1M_PHY, HCI_LE_CODED_PHY)
): ):
# Check that the arguments are legal # Check that the arguments are legal
if scan_interval < scan_window: if scan_interval < scan_window:
@@ -820,25 +878,36 @@ class Device(CompositeEventEmitter):
if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW: if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW:
raise ValueError('scan_interval out of range') raise ValueError('scan_interval out of range')
# Reset the accumulators
self.advertisement_accumulator = {}
# Enable scanning
if self.supports_le_feature(HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE): if self.supports_le_feature(HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE):
# Set the scanning parameters # Set the scanning parameters
scan_type = HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING if active else HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING scan_type = HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING if active else HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING
scanning_filter_policy = HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY # TODO: support other types scanning_filter_policy = HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY # TODO: support other types
scanning_phys = 1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_1M_PHY scanning_phy_count = 0
scanning_phy_count = 1 scanning_phys_bits = 0
if self.supports_le_feature(HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE): if HCI_LE_1M_PHY in scanning_phys:
scanning_phys |= 1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_CODED_PHY scanning_phys_bits |= 1 << HCI_LE_1M_PHY_BIT
scanning_phy_count += 1 scanning_phy_count += 1
if HCI_LE_CODED_PHY in scanning_phys:
if self.supports_le_feature(HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE):
scanning_phys_bits |= 1 << HCI_LE_CODED_PHY_BIT
scanning_phy_count += 1
if scanning_phy_count == 0:
raise ValueError('at least one scanning PHY must be enabled')
await self.send_command(HCI_LE_Set_Extended_Scan_Parameters_Command( await self.send_command(HCI_LE_Set_Extended_Scan_Parameters_Command(
own_address_type = own_address_type, own_address_type = own_address_type,
scanning_filter_policy = scanning_filter_policy, scanning_filter_policy = scanning_filter_policy,
scanning_phys = scanning_phys, scanning_phys = scanning_phys_bits,
scan_types = [scan_type] * scanning_phy_count, scan_types = [scan_type] * scanning_phy_count,
scan_intervals = [int(scan_window / 0.625)] * scanning_phy_count, scan_intervals = [int(scan_window / 0.625)] * scanning_phy_count,
scan_windows = [int(scan_window / 0.625)] * scanning_phy_count scan_windows = [int(scan_window / 0.625)] * scanning_phy_count
)) ), check_result=True)
# Enable scanning # Enable scanning
await self.send_command(HCI_LE_Set_Extended_Scan_Enable_Command( await self.send_command(HCI_LE_Set_Extended_Scan_Enable_Command(
@@ -846,7 +915,7 @@ class Device(CompositeEventEmitter):
filter_duplicates = 1 if filter_duplicates else 0, filter_duplicates = 1 if filter_duplicates else 0,
duration = 0, # TODO allow other values duration = 0, # TODO allow other values
period = 0 # TODO allow other values period = 0 # TODO allow other values
)) ), check_result=True)
else: else:
# Set the scanning parameters # Set the scanning parameters
scan_type = HCI_LE_Set_Scan_Parameters_Command.ACTIVE_SCANNING if active else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING scan_type = HCI_LE_Set_Scan_Parameters_Command.ACTIVE_SCANNING if active else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING
@@ -856,22 +925,32 @@ class Device(CompositeEventEmitter):
le_scan_window = int(scan_window / 0.625), le_scan_window = int(scan_window / 0.625),
own_address_type = own_address_type, own_address_type = own_address_type,
scanning_filter_policy = HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY scanning_filter_policy = HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY
)) ), check_result=True)
# Enable scanning # Enable scanning
await self.send_command(HCI_LE_Set_Scan_Enable_Command( await self.send_command(HCI_LE_Set_Scan_Enable_Command(
le_scan_enable = 1, le_scan_enable = 1,
filter_duplicates = 1 if filter_duplicates else 0 filter_duplicates = 1 if filter_duplicates else 0
)) ), check_result=True)
self.scanning_is_passive = not active self.scanning_is_passive = not active
self.scanning = True self.scanning = True
async def stop_scanning(self): async def stop_scanning(self):
await self.send_command(HCI_LE_Set_Scan_Enable_Command( # Disable scanning
le_scan_enable = 0, if self.supports_le_feature(HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE):
filter_duplicates = 0 await self.send_command(HCI_LE_Set_Extended_Scan_Enable_Command(
)) enable = 0,
filter_duplicates = 0,
duration = 0,
period = 0
), check_result=True)
else:
await self.send_command(HCI_LE_Set_Scan_Enable_Command(
le_scan_enable = 0,
filter_duplicates = 0
), check_result=True)
self.scanning = False self.scanning = False
@property @property
@@ -880,21 +959,22 @@ class Device(CompositeEventEmitter):
@host_event_handler @host_event_handler
def on_advertising_report(self, report): def on_advertising_report(self, report):
if not (accumulator := self.advertisement_data.get(report.address)): if not (accumulator := self.advertisement_accumulators.get(report.address)):
accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive) accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive)
self.advertisement_data[report.address] = accumulator self.advertisement_accumulators[report.address] = accumulator
accumulator.update(report) if advertisement := accumulator.update(report):
if accumulator.advertisement is not None: self.emit('advertisement', advertisement)
self.emit('advertisement', accumulator.advertisement)
async def start_discovery(self): async def start_discovery(self):
await self.host.send_command(HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE)) await self.send_command(HCI_Write_Inquiry_Mode_Command(
inquiry_mode=HCI_EXTENDED_INQUIRY_MODE
), check_result=True)
response = await self.send_command(HCI_Inquiry_Command( response = await self.send_command(HCI_Inquiry_Command(
lap = HCI_GENERAL_INQUIRY_LAP, lap = HCI_GENERAL_INQUIRY_LAP,
inquiry_length = DEVICE_DEFAULT_INQUIRY_LENGTH, inquiry_length = DEVICE_DEFAULT_INQUIRY_LENGTH,
num_responses = 0 # Unlimited number of responses. num_responses = 0 # Unlimited number of responses.
)) ), check_result=True)
if response.status != HCI_Command_Status_Event.PENDING: if response.status != HCI_Command_Status_Event.PENDING:
self.discovering = False self.discovering = False
raise HCI_StatusError(response) raise HCI_StatusError(response)
@@ -902,7 +982,7 @@ class Device(CompositeEventEmitter):
self.discovering = True self.discovering = True
async def stop_discovery(self): async def stop_discovery(self):
await self.send_command(HCI_Inquiry_Cancel_Command()) await self.send_command(HCI_Inquiry_Cancel_Command(), check_result=True)
self.discovering = False self.discovering = False
@host_event_handler @host_event_handler
@@ -939,11 +1019,12 @@ class Device(CompositeEventEmitter):
) )
# Update the controller # Update the controller
await self.host.send_command( await self.send_command(
HCI_Write_Extended_Inquiry_Response_Command( HCI_Write_Extended_Inquiry_Response_Command(
fec_required = 0, fec_required = 0,
extended_inquiry_response = self.inquiry_response extended_inquiry_response = self.inquiry_response
) ),
check_result=True
) )
await self.set_scan_enable( await self.set_scan_enable(
inquiry_scan_enabled = self.discoverable, inquiry_scan_enabled = self.discoverable,
@@ -958,12 +1039,26 @@ class Device(CompositeEventEmitter):
page_scan_enabled = self.connectable page_scan_enabled = self.connectable
) )
async def connect(self, peer_address, transport=BT_LE_TRANSPORT): async def connect(
self,
peer_address,
transport=BT_LE_TRANSPORT,
connection_parameters_preferences=None,
timeout=DEVICE_DEFAULT_CONNECT_TIMEOUT
):
''' '''
Request a connection to a peer. Request a connection to a peer.
This method cannot be called if there is already a pending connection. This method cannot be called if there is already a pending connection.
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
* None: use all PHYs with default parameters
* map: each entry has a PHY as key and a ConnectionParametersPreferences object as value
''' '''
# Check parameters
if transport not in {BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT}:
raise ValueError('invalid transport')
# Adjust the transport automatically if we need to # Adjust the transport automatically if we need to
if transport == BT_LE_TRANSPORT and not self.le_enabled: if transport == BT_LE_TRANSPORT and not self.le_enabled:
transport = BT_BR_EDR_TRANSPORT transport = BT_BR_EDR_TRANSPORT
@@ -980,49 +1075,117 @@ class Device(CompositeEventEmitter):
except ValueError: except ValueError:
# If the address is not parsable, assume it is a name instead # If the address is not parsable, assume it is a name instead
logger.debug('looking for peer by name') logger.debug('looking for peer by name')
peer_address = await self.find_peer_by_name(peer_address, transport) peer_address = await self.find_peer_by_name(peer_address, transport) # TODO: timeout
# Create a future so that we can wait for the connection's result # Create a future so that we can wait for the connection's result
pending_connection = asyncio.get_running_loop().create_future() pending_connection = asyncio.get_running_loop().create_future()
self.on('connection', pending_connection.set_result) self.on('connection', pending_connection.set_result)
self.on('connection_failure', pending_connection.set_exception) self.on('connection_failure', pending_connection.set_exception)
# Tell the controller to connect
if transport == BT_LE_TRANSPORT:
# TODO: use real values, not fixed ones
result = await self.send_command(HCI_LE_Create_Connection_Command(
le_scan_interval = 96,
le_scan_window = 96,
initiator_filter_policy = 0,
peer_address_type = peer_address.address_type,
peer_address = peer_address,
own_address_type = Address.RANDOM_DEVICE_ADDRESS,
conn_interval_min = 12,
conn_interval_max = 24,
conn_latency = 0,
supervision_timeout = 72,
minimum_ce_length = 0,
maximum_ce_length = 0
))
else:
# TODO: use real values, not fixed ones
result = await self.send_command(HCI_Create_Connection_Command(
bd_addr = peer_address,
packet_type = 0xCC18, # FIXME: change
page_scan_repetition_mode = HCI_R2_PAGE_SCAN_REPETITION_MODE,
clock_offset = 0x0000,
allow_role_switch = 0x01,
reserved = 0
))
try: try:
# Tell the controller to connect
if transport == BT_LE_TRANSPORT:
if connection_parameters_preferences is None:
if connection_parameters_preferences is None:
connection_parameters_preferences = {
HCI_LE_1M_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES,
HCI_LE_2M_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES,
HCI_LE_CODED_PHY: DEVICE_DEFAULT_CONNECTION_PARAMETER_PREFERENCES
}
if self.host.supports_command(HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND):
# Only keep supported PHYs
phys = sorted(list(set(filter(self.supports_le_phy, connection_parameters_preferences.keys()))))
if not phys:
raise ValueError('least one supported PHY needed')
phy_count = len(phys)
initiating_phys = phy_list_to_bits(phys)
connection_interval_mins = [
int(connection_parameters_preferences[phy].connection_interval_min / 1.25) for phy in phys
]
connection_interval_maxs = [
int(connection_parameters_preferences[phy].connection_interval_max / 1.25) for phy in phys
]
max_latencies = [
connection_parameters_preferences[phy].max_latency for phy in phys
]
supervision_timeouts = [
int(connection_parameters_preferences[phy].supervision_timeout / 10) for phy in phys
]
min_ce_lengths = [
int(connection_parameters_preferences[phy].min_ce_length / 0.625) for phy in phys
]
max_ce_lengths = [
int(connection_parameters_preferences[phy].max_ce_length / 0.625) for phy in phys
]
result = await self.send_command(HCI_LE_Extended_Create_Connection_Command(
initiator_filter_policy = 0,
own_address_type = Address.RANDOM_DEVICE_ADDRESS,
peer_address_type = peer_address.address_type,
peer_address = peer_address,
initiating_phys = initiating_phys,
scan_intervals = (int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),) * phy_count,
scan_windows = (int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),) * phy_count,
connection_interval_mins = connection_interval_mins,
connection_interval_maxs = connection_interval_maxs,
max_latencies = max_latencies,
supervision_timeouts = supervision_timeouts,
min_ce_lengths = min_ce_lengths,
max_ce_lengths = max_ce_lengths
))
else:
if HCI_LE_1M_PHY not in connection_parameters_preferences:
raise ValueError('1M PHY preferences required')
prefs = connection_parameters_preferences[HCI_LE_1M_PHY]
result = await self.send_command(HCI_LE_Create_Connection_Command(
le_scan_interval = int(DEVICE_DEFAULT_CONNECT_SCAN_INTERVAL / 0.625),
le_scan_window = int(DEVICE_DEFAULT_CONNECT_SCAN_WINDOW / 0.625),
initiator_filter_policy = 0,
peer_address_type = peer_address.address_type,
peer_address = peer_address,
own_address_type = Address.RANDOM_DEVICE_ADDRESS,
connection_interval_min = int(prefs.connection_interval_min / 1.25),
connection_interval_max = int(prefs.connection_interval_max / 1.25),
max_latency = prefs.max_latency,
supervision_timeout = int(prefs.supervision_timeout / 10),
min_ce_length = int(prefs.min_ce_length / 0.625),
max_ce_length = int(prefs.max_ce_length / 0.625),
))
else:
# TODO: allow passing other settings
result = await self.send_command(HCI_Create_Connection_Command(
bd_addr = peer_address,
packet_type = 0xCC18, # FIXME: change
page_scan_repetition_mode = HCI_R2_PAGE_SCAN_REPETITION_MODE,
clock_offset = 0x0000,
allow_role_switch = 0x01,
reserved = 0
))
if result.status != HCI_Command_Status_Event.PENDING: if result.status != HCI_Command_Status_Event.PENDING:
raise HCI_StatusError(result) raise HCI_StatusError(result)
# Wait for the connection process to complete # Wait for the connection process to complete
self.connecting = True self.connecting = True
return await pending_connection if timeout is None:
return await pending_connection
else:
try:
return await asyncio.wait_for(asyncio.shield(pending_connection), timeout)
except asyncio.TimeoutError:
if transport == BT_LE_TRANSPORT:
await self.send_command(HCI_LE_Create_Connection_Cancel_Command())
else:
await self.send_command(HCI_Create_Connection_Cancel_Command(bd_addr=peer_address))
try:
return await pending_connection
except ConnectionError:
raise TimeoutError()
finally: finally:
self.remove_listener('connection', pending_connection.set_result) self.remove_listener('connection', pending_connection.set_result)
self.remove_listener('connection_failure', pending_connection.set_exception) self.remove_listener('connection_failure', pending_connection.set_exception)
@@ -1047,7 +1210,7 @@ class Device(CompositeEventEmitter):
async def cancel_connection(self): async def cancel_connection(self):
if not self.is_connecting: if not self.is_connecting:
return return
await self.send_command(HCI_LE_Create_Connection_Cancel_Command()) await self.send_command(HCI_LE_Create_Connection_Cancel_Command(), check_result=True)
async def disconnect(self, connection, reason): async def disconnect(self, connection, reason):
# Create a future so that we can wait for the disconnection's result # Create a future so that we can wait for the disconnection's result
@@ -1056,7 +1219,9 @@ class Device(CompositeEventEmitter):
connection.on('disconnection_failure', pending_disconnection.set_exception) connection.on('disconnection_failure', pending_disconnection.set_exception)
# Request a disconnection # Request a disconnection
result = await self.send_command(HCI_Disconnect_Command(connection_handle = connection.handle, reason = reason)) result = await self.send_command(HCI_Disconnect_Command(
connection_handle = connection.handle, reason = reason
))
try: try:
if result.status != HCI_Command_Status_Event.PENDING: if result.status != HCI_Command_Status_Event.PENDING:
@@ -1073,26 +1238,66 @@ class Device(CompositeEventEmitter):
async def update_connection_parameters( async def update_connection_parameters(
self, self,
connection, connection,
conn_interval_min, connection_interval_min,
conn_interval_max, connection_interval_max,
conn_latency, max_latency,
supervision_timeout, supervision_timeout,
minimum_ce_length = 0, min_ce_length = 0,
maximum_ce_length = 0 max_ce_length = 0
): ):
''' '''
NOTE: the name of the parameters may look odd, but it just follows the names used in the Bluetooth spec. NOTE: the name of the parameters may look odd, but it just follows the names used in the Bluetooth spec.
''' '''
await self.send_command(HCI_LE_Connection_Update_Command( await self.send_command(HCI_LE_Connection_Update_Command(
connection_handle = connection.handle, connection_handle = connection.handle,
conn_interval_min = conn_interval_min, connection_interval_min = connection_interval_min,
conn_interval_max = conn_interval_max, connection_interval_max = connection_interval_max,
conn_latency = conn_latency, max_latency = max_latency,
supervision_timeout = supervision_timeout, supervision_timeout = supervision_timeout,
minimum_ce_length = minimum_ce_length, min_ce_length = min_ce_length,
maximum_ce_length = maximum_ce_length max_ce_length = max_ce_length
)) ), check_result=True)
# TODO: check result
async def get_connection_rssi(self, connection):
result = await self.send_command(HCI_Read_RSSI_Command(handle = connection.handle), check_result=True)
return result.return_parameters.rssi
async def get_connection_phy(self, connection):
result = await self.send_command(
HCI_LE_Read_PHY_Command(connection_handle = connection.handle),
check_result=True
)
return (result.return_parameters.tx_phy, result.return_parameters.rx_phy)
async def set_connection_phy(
self,
connection,
tx_phys=None,
rx_phys=None,
phy_options=None
):
all_phys_bits = (1 if tx_phys is None else 0) | ((1 if rx_phys is None else 0) << 1)
return await self.send_command(
HCI_LE_Set_PHY_Command(
connection_handle = connection.handle,
all_phys = all_phys_bits,
tx_phys = phy_list_to_bits(tx_phys),
rx_phys = phy_list_to_bits(rx_phys),
phy_options = 0 # TODO: parse from function argument
)
)
async def set_default_phy(self, tx_phys=None, rx_phys=None):
all_phys_bits = (1 if tx_phys is None else 0) | ((1 if rx_phys is None else 0) << 1)
return await self.send_command(
HCI_LE_Set_Default_PHY_Command(
all_phys = all_phys_bits,
tx_phys = phy_list_to_bits(tx_phys),
rx_phys = phy_list_to_bits(rx_phys)
), check_result=True
)
async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT): async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT):
""" """
@@ -1116,8 +1321,7 @@ class Device(CompositeEventEmitter):
event_name = 'advertisement' event_name = 'advertisement'
handler = self.on( handler = self.on(
event_name, event_name,
lambda address, ad_data, rssi, connectable: lambda advertisement: on_peer_found(advertisement.address, advertisement.data)
on_peer_found(address, ad_data)
) )
was_scanning = self.scanning was_scanning = self.scanning
@@ -1371,26 +1575,41 @@ class Device(CompositeEventEmitter):
peer_resolvable_address = peer_address peer_resolvable_address = peer_address
peer_address = resolved_address peer_address = resolved_address
# Create a new connection
connection = Connection(
self,
connection_handle,
transport,
peer_address,
peer_resolvable_address,
role,
connection_parameters
)
self.connections[connection_handle] = connection
# We are no longer advertising # We are no longer advertising
self.advertising = False self.advertising = False
# Emit an event to notify listeners of the new connection # Create and notify of the new connection asynchronously
self.emit('connection', connection) async def new_connection():
# Figure out which PHY we're connected with
if self.host.supports_command(HCI_LE_READ_PHY_COMMAND):
result = await self.send_command(
HCI_LE_Read_PHY_Command(connection_handle=connection_handle),
check_result=True
)
phy = ConnectionPHY(result.return_parameters.tx_phy, result.return_parameters.rx_phy)
else:
phy = ConnectionPHY(HCI_LE_1M_PHY, HCI_LE_1M_PHY)
# Create a new connection
connection = Connection(
self,
connection_handle,
transport,
peer_address,
peer_resolvable_address,
role,
connection_parameters,
phy
)
self.connections[connection_handle] = connection
# Emit an event to notify listeners of the new connection
self.emit('connection', connection)
asyncio.create_task(new_connection())
@host_event_handler @host_event_handler
def on_connection_failure(self, error_code): def on_connection_failure(self, connection_handle, error_code):
logger.debug(f'*** Connection failed: {error_code}') logger.debug(f'*** Connection failed: {error_code}')
error = ConnectionError( error = ConnectionError(
error_code, error_code,

View File

@@ -62,6 +62,18 @@ def map_class_of_device(class_of_device):
return f'[{class_of_device:06X}] Services({",".join(DeviceClass.service_class_labels(service_classes))}),Class({DeviceClass.major_device_class_name(major_device_class)}|{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)})' return f'[{class_of_device:06X}] Services({",".join(DeviceClass.service_class_labels(service_classes))}),Class({DeviceClass.major_device_class_name(major_device_class)}|{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)})'
def phy_list_to_bits(phys):
if phys is None:
return 0
else:
phy_bits = 0
for phy in phys:
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
raise ValueError('invalid PHY')
phy_bits |= (1 << HCI_LE_PHY_TYPE_TO_BIT[phy])
return phy_bits
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -674,6 +686,18 @@ HCI_LE_PHY_NAMES = {
HCI_LE_CODED_PHY: 'LE Coded' HCI_LE_CODED_PHY: 'LE Coded'
} }
HCI_LE_1M_PHY_BIT = 0
HCI_LE_2M_PHY_BIT = 1
HCI_LE_CODED_PHY_BIT = 2
HCI_LE_PHY_BIT_NAMES = ['LE_1M_PHY', 'LE_2M_PHY', 'LE_CODED_PHY']
HCI_LE_PHY_TYPE_TO_BIT = {
HCI_LE_1M_PHY: HCI_LE_1M_PHY_BIT,
HCI_LE_2M_PHY: HCI_LE_2M_PHY_BIT,
HCI_LE_CODED_PHY: HCI_LE_CODED_PHY_BIT
}
# Connection Parameters # Connection Parameters
HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25 HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25
HCI_CONNECTION_LATENCY_MS_PER_UNIT = 1.25 HCI_CONNECTION_LATENCY_MS_PER_UNIT = 1.25
@@ -1884,6 +1908,22 @@ class HCI_Disconnect_Command(HCI_Command):
''' '''
# -----------------------------------------------------------------------------
@HCI_Command.command(
fields=[
('bd_addr', Address.parse_address)
],
return_parameters_fields=[
('status', STATUS_SPEC),
('bd_addr', Address.parse_address)
]
)
class HCI_Create_Connection_Cancel_Command(HCI_Command):
'''
See Bluetooth spec @ 7.1.7 Create Connection Cancel Command
'''
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command([ @HCI_Command.command([
('bd_addr', Address.parse_address), ('bd_addr', Address.parse_address),
@@ -2300,7 +2340,7 @@ class HCI_Read_Local_Name_Command(HCI_Command):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command([ @HCI_Command.command([
('conn_accept_timeout', 2) ('connection_accept_timeout', 2)
]) ])
class HCI_Write_Connection_Accept_Timeout_Command(HCI_Command): class HCI_Write_Connection_Accept_Timeout_Command(HCI_Command):
''' '''
@@ -2693,6 +2733,23 @@ class HCI_Read_Local_Supported_Codecs_Command(HCI_Command):
''' '''
# -----------------------------------------------------------------------------
@HCI_Command.command(
fields=[
('handle', 2)
],
return_parameters_fields=[
('status', STATUS_SPEC),
('handle', 2),
('rssi', -1)
]
)
class HCI_Read_RSSI_Command(HCI_Command):
'''
See Bluetooth spec @ 7.5.4 Read RSSI Command
'''
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command( @HCI_Command.command(
fields=[ fields=[
@@ -2872,12 +2929,12 @@ class HCI_LE_Set_Scan_Enable_Command(HCI_Command):
('peer_address_type', Address.ADDRESS_TYPE_SPEC), ('peer_address_type', Address.ADDRESS_TYPE_SPEC),
('peer_address', Address.parse_address_preceded_by_type), ('peer_address', Address.parse_address_preceded_by_type),
('own_address_type', Address.ADDRESS_TYPE_SPEC), ('own_address_type', Address.ADDRESS_TYPE_SPEC),
('conn_interval_min', 2), ('connection_interval_min', 2),
('conn_interval_max', 2), ('connection_interval_max', 2),
('conn_latency', 2), ('max_latency', 2),
('supervision_timeout', 2), ('supervision_timeout', 2),
('minimum_ce_length', 2), ('min_ce_length', 2),
('maximum_ce_length', 2) ('max_ce_length', 2)
]) ])
class HCI_LE_Create_Connection_Command(HCI_Command): class HCI_LE_Create_Connection_Command(HCI_Command):
''' '''
@@ -2933,13 +2990,13 @@ class HCI_LE_Remove_Device_From_Filter_Accept_List_Command(HCI_Command):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command([ @HCI_Command.command([
('connection_handle', 2), ('connection_handle', 2),
('conn_interval_min', 2), ('connection_interval_min', 2),
('conn_interval_max', 2), ('connection_interval_max', 2),
('conn_latency', 2), ('max_latency', 2),
('supervision_timeout', 2), ('supervision_timeout', 2),
('minimum_ce_length', 2), ('min_ce_length', 2),
('maximum_ce_length', 2) ('max_ce_length', 2)
]) ])
class HCI_LE_Connection_Update_Command(HCI_Command): class HCI_LE_Connection_Update_Command(HCI_Command):
''' '''
@@ -3005,10 +3062,10 @@ class HCI_LE_Read_Supported_States_Command(HCI_Command):
('connection_handle', 2), ('connection_handle', 2),
('interval_min', 2), ('interval_min', 2),
('interval_max', 2), ('interval_max', 2),
('latency', 2), ('max_latency', 2),
('timeout', 2), ('timeout', 2),
('minimum_ce_length', 2), ('min_ce_length', 2),
('maximum_ce_length', 2) ('max_ce_length', 2)
]) ])
class HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(HCI_Command): class HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(HCI_Command):
''' '''
@@ -3089,18 +3146,6 @@ class HCI_LE_Clear_Resolving_List_Command(HCI_Command):
''' '''
# -----------------------------------------------------------------------------
@HCI_Command.command([
('all_phys', 1),
('tx_phys', 1),
('rx_phys', 1)
])
class HCI_LE_Set_Default_PHY_Command(HCI_Command):
'''
See Bluetooth spec @ 7.8.48 LE Set Default PHY Command
'''
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command([ @HCI_Command.command([
('address_resolution_enable', 1) ('address_resolution_enable', 1)
@@ -3152,18 +3197,38 @@ class HCI_LE_Read_PHY_Command(HCI_Command):
''' '''
# -----------------------------------------------------------------------------
@HCI_Command.command([
('all_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_Set_Default_PHY_Command.ANY_PHY_BIT_NAMES)}),
('tx_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_PHY_BIT_NAMES)}),
('rx_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_PHY_BIT_NAMES)})
])
class HCI_LE_Set_Default_PHY_Command(HCI_Command):
'''
See Bluetooth spec @ 7.8.48 LE Set Default PHY Command
'''
ANY_TX_PHY_BIT = 0
ANY_RX_PHY_BIT = 1
ANY_PHY_BIT_NAMES = ['Any TX', 'Any RX']
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Command.command([ @HCI_Command.command([
('connection_handle', 2), ('connection_handle', 2),
('all_phys', 1), ('all_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_Set_PHY_Command.ANY_PHY_BIT_NAMES)}),
('tx_phys', 1), ('tx_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_PHY_BIT_NAMES)}),
('rx_phys', 1), ('rx_phys', {'size': 1, 'mapper': lambda x: bit_flags_to_strings(x, HCI_LE_PHY_BIT_NAMES)}),
('phy_options', 2) ('phy_options', 2)
]) ])
class HCI_LE_Set_PHY_Command(HCI_Command): class HCI_LE_Set_PHY_Command(HCI_Command):
''' '''
See Bluetooth spec @ 7.8.49 LE Set PHY Command See Bluetooth spec @ 7.8.49 LE Set PHY Command
''' '''
ANY_TX_PHY_BIT = 0
ANY_RX_PHY_BIT = 1
ANY_PHY_BIT_NAMES = ['Any TX', 'Any RX']
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -3422,11 +3487,6 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
EXTENDED_UNFILTERED_POLICY = 0x02 EXTENDED_UNFILTERED_POLICY = 0x02
EXTENDED_FILTERED_POLICY = 0x03 EXTENDED_FILTERED_POLICY = 0x03
LE_1M_PHY = 0x00
LE_CODED_PHY = 0x02
SCANNING_PHY_NAMES = ['LE_1M_PHY', '', 'LE_CODED_PHY']
@classmethod @classmethod
def from_parameters(cls, parameters): def from_parameters(cls, parameters):
own_address_type = parameters[0] own_address_type = parameters[0]
@@ -3474,7 +3534,7 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
self.parameters += struct.pack('<BHH', scan_types[i], scan_intervals[i], scan_windows[i]) self.parameters += struct.pack('<BHH', scan_types[i], scan_intervals[i], scan_windows[i])
def __str__(self): def __str__(self):
scanning_phys_strs = bit_flags_to_strings(self.scanning_phys, self.SCANNING_PHY_NAMES) scanning_phys_strs = bit_flags_to_strings(self.scanning_phys, HCI_LE_PHY_BIT_NAMES)
fields = [ fields = [
('own_address_type: ', Address.address_type_name(self.own_address_type)), ('own_address_type: ', Address.address_type_name(self.own_address_type)),
('scanning_filter_policy:', self.scanning_filter_policy), ('scanning_filter_policy:', self.scanning_filter_policy),
@@ -3510,17 +3570,12 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
See Bluetooth spec @ 7.8.66 LE Extended Create Connection Command See Bluetooth spec @ 7.8.66 LE Extended Create Connection Command
''' '''
LE_1M_PHY = 0x00
LE_2M_PHY = 0x01
LE_CODED_PHY = 0x02
INITIATING_PHY_NAMES = ['LE_1M_PHY', 'LE_2M_PHY', 'LE_CODED_PHY']
@classmethod @classmethod
def from_parameters(cls, parameters): def from_parameters(cls, parameters):
initiator_filter_policy = parameters[0] initiator_filter_policy = parameters[0]
own_address_type = parameters[1] own_address_type = parameters[1]
peer_address = Address.parse_address_preceded_by_type(parameters, 2)[1] peer_address_type = parameters[2]
peer_address = Address.parse_address_preceded_by_type(parameters, 3)[1]
initiating_phys = parameters[9] initiating_phys = parameters[9]
phy_bits_set = bin(initiating_phys).count('1') phy_bits_set = bin(initiating_phys).count('1')
@@ -3531,6 +3586,7 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
return cls( return cls(
initiator_filter_policy = initiator_filter_policy, initiator_filter_policy = initiator_filter_policy,
own_address_type = own_address_type, own_address_type = own_address_type,
peer_address_type = peer_address_type,
peer_address = peer_address, peer_address = peer_address,
initiating_phys = initiating_phys, initiating_phys = initiating_phys,
scan_intervals = read_parameter_list(10), scan_intervals = read_parameter_list(10),
@@ -3545,22 +3601,24 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
def __init__( def __init__(
self, self,
initiator_filter_policy, initiator_filter_policy,
own_address_type, own_address_type,
peer_address, peer_address_type,
initiating_phys, peer_address,
scan_intervals, initiating_phys,
scan_windows, scan_intervals,
connection_interval_mins, scan_windows,
connection_interval_maxs, connection_interval_mins,
max_latencies, connection_interval_maxs,
supervision_timeouts, max_latencies,
min_ce_lengths, supervision_timeouts,
max_ce_lengths, min_ce_lengths,
max_ce_lengths
): ):
super().__init__(HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND) super().__init__(HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND)
self.initiator_filter_policy = initiator_filter_policy self.initiator_filter_policy = initiator_filter_policy
self.own_address_type = own_address_type self.own_address_type = own_address_type
self.peer_address_type = peer_address_type
self.peer_address = peer_address self.peer_address = peer_address
self.initiating_phys = initiating_phys self.initiating_phys = initiating_phys
self.scan_intervals = scan_intervals self.scan_intervals = scan_intervals
@@ -3575,7 +3633,7 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
self.parameters = bytes([ self.parameters = bytes([
initiator_filter_policy, initiator_filter_policy,
own_address_type, own_address_type,
peer_address.address_type peer_address_type
]) + bytes(peer_address) + bytes([initiating_phys]) ]) + bytes(peer_address) + bytes([initiating_phys])
phy_bits_set = bin(initiating_phys).count('1') phy_bits_set = bin(initiating_phys).count('1')
@@ -3593,10 +3651,13 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
) )
def __str__(self): def __str__(self):
initiating_phys_strs = bit_flags_to_strings(self.initiating_phys, self.INITIATING_PHY_NAMES) initiating_phys_strs = bit_flags_to_strings(self.initiating_phys, HCI_LE_PHY_BIT_NAMES)
fields = [ fields = [
('own_address_type:', Address.address_type_name(self.own_address_type)), ('initiator_filter_policy:', self.initiator_filter_policy),
('scanning_phys: ', ','.join(initiating_phys_strs)), ('own_address_type: ', Address.address_type_name(self.own_address_type)),
('peer_address_type: ', Address.address_type_name(self.peer_address_type)),
('peer_address: ', str(self.peer_address)),
('initiating_phys: ', ','.join(initiating_phys_strs)),
] ]
for (i, initiating_phys_str) in enumerate(initiating_phys_strs): for (i, initiating_phys_str) in enumerate(initiating_phys_strs):
fields.append((f'{initiating_phys_str}.scan_interval: ', self.scan_intervals[i])), fields.append((f'{initiating_phys_str}.scan_interval: ', self.scan_intervals[i])),
@@ -3823,15 +3884,15 @@ class HCI_LE_Meta_Event(HCI_Event):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_LE_Meta_Event.event([ @HCI_LE_Meta_Event.event([
('status', STATUS_SPEC), ('status', STATUS_SPEC),
('connection_handle', 2), ('connection_handle', 2),
('role', {'size': 1, 'mapper': lambda x: 'CENTRAL' if x == 0 else 'PERIPHERAL'}), ('role', {'size': 1, 'mapper': lambda x: 'CENTRAL' if x == 0 else 'PERIPHERAL'}),
('peer_address_type', Address.ADDRESS_TYPE_SPEC), ('peer_address_type', Address.ADDRESS_TYPE_SPEC),
('peer_address', Address.parse_address_preceded_by_type), ('peer_address', Address.parse_address_preceded_by_type),
('conn_interval', 2), ('connection_interval', 2),
('conn_latency', 2), ('peripheral_latency', 2),
('supervision_timeout', 2), ('supervision_timeout', 2),
('master_clock_accuracy', 1) ('central_clock_accuracy', 1)
]) ])
class HCI_LE_Connection_Complete_Event(HCI_LE_Meta_Event): class HCI_LE_Connection_Complete_Event(HCI_LE_Meta_Event):
''' '''
@@ -3874,6 +3935,9 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
def from_parameters(cls, parameters, offset): def from_parameters(cls, parameters, offset):
return cls.from_bytes(parameters, offset, cls.FIELDS) return cls.from_bytes(parameters, offset, cls.FIELDS)
def event_type_string(self):
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
def to_string(self, prefix): def to_string(self, prefix):
return super().to_string(prefix, { return super().to_string(prefix, {
'event_type': HCI_LE_Advertising_Report_Event.event_type_name, 'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
@@ -3917,8 +3981,8 @@ HCI_Event.meta_event_classes[HCI_LE_ADVERTISING_REPORT_EVENT] = HCI_LE_Advertisi
@HCI_LE_Meta_Event.event([ @HCI_LE_Meta_Event.event([
('status', STATUS_SPEC), ('status', STATUS_SPEC),
('connection_handle', 2), ('connection_handle', 2),
('conn_interval', 2), ('connection_interval', 2),
('conn_latency', 2), ('peripheral_latency', 2),
('supervision_timeout', 2) ('supervision_timeout', 2)
]) ])
class HCI_LE_Connection_Update_Complete_Event(HCI_LE_Meta_Event): class HCI_LE_Connection_Update_Complete_Event(HCI_LE_Meta_Event):
@@ -3956,7 +4020,7 @@ class HCI_LE_Long_Term_Key_Request_Event(HCI_LE_Meta_Event):
('connection_handle', 2), ('connection_handle', 2),
('interval_min', 2), ('interval_min', 2),
('interval_max', 2), ('interval_max', 2),
('latency', 2), ('max_latency', 2),
('timeout', 2) ('timeout', 2)
]) ])
class HCI_LE_Remote_Connection_Parameter_Request_Event(HCI_LE_Meta_Event): class HCI_LE_Remote_Connection_Parameter_Request_Event(HCI_LE_Meta_Event):
@@ -3988,10 +4052,10 @@ class HCI_LE_Data_Length_Change_Event(HCI_LE_Meta_Event):
('peer_address', Address.parse_address_preceded_by_type), ('peer_address', Address.parse_address_preceded_by_type),
('local_resolvable_private_address', Address.parse_address), ('local_resolvable_private_address', Address.parse_address),
('peer_resolvable_private_address', Address.parse_address), ('peer_resolvable_private_address', Address.parse_address),
('conn_interval', 2), ('connection_interval', 2),
('conn_latency', 2), ('peripheral_latency', 2),
('supervision_timeout', 2), ('supervision_timeout', 2),
('master_clock_accuracy', 1) ('central_clock_accuracy', 1)
]) ])
class HCI_LE_Enhanced_Connection_Complete_Event(HCI_LE_Meta_Event): class HCI_LE_Enhanced_Connection_Complete_Event(HCI_LE_Meta_Event):
''' '''
@@ -4073,31 +4137,35 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
def from_parameters(cls, parameters, offset): def from_parameters(cls, parameters, offset):
return cls.from_bytes(parameters, offset, cls.FIELDS) return cls.from_bytes(parameters, offset, cls.FIELDS)
def event_type_string(self):
return HCI_LE_Extended_Advertising_Report_Event.event_type_string(self.event_type)
def to_string(self, prefix): def to_string(self, prefix):
def event_type_string(event_type):
event_type_flags = bit_flags_to_strings(
event_type & 0x1F,
HCI_LE_Extended_Advertising_Report_Event.EVENT_TYPE_FLAG_NAMES,
)
event_type_flags.append(('COMPLETE', 'INCOMPLETE+', 'INCOMPLETE#', '?')[(event_type >> 5) & 3])
if event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED):
legacy_pdu_type = HCI_LE_Extended_Advertising_Report_Event.LEGACY_PDU_TYPE_MAP.get(event_type & 0x0F)
if legacy_pdu_type is not None:
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
else:
legacy_info_string = ''
else:
legacy_info_string = ''
return f'0x{event_type:04X} [{",".join(event_type_flags)}]{legacy_info_string}'
return super().to_string(prefix, { return super().to_string(prefix, {
'event_type': event_type_string, 'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
'address_type': Address.address_type_name, 'address_type': Address.address_type_name,
'data': lambda x: str(AdvertisingData.from_bytes(x)) 'data': lambda x: str(AdvertisingData.from_bytes(x))
}) })
@staticmethod
def event_type_string(event_type):
event_type_flags = bit_flags_to_strings(
event_type & 0x1F,
HCI_LE_Extended_Advertising_Report_Event.EVENT_TYPE_FLAG_NAMES,
)
event_type_flags.append(('COMPLETE', 'INCOMPLETE+', 'INCOMPLETE#', '?')[(event_type >> 5) & 3])
if event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED):
legacy_pdu_type = HCI_LE_Extended_Advertising_Report_Event.LEGACY_PDU_TYPE_MAP.get(event_type & 0x0F)
if legacy_pdu_type is not None:
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
else:
legacy_info_string = ''
else:
legacy_info_string = ''
return f'0x{event_type:04X} [{",".join(event_type_flags)}]{legacy_info_string}'
@classmethod @classmethod
def from_parameters(cls, parameters): def from_parameters(cls, parameters):
num_reports = parameters[1] num_reports = parameters[1]
@@ -4546,7 +4614,7 @@ class HCI_Page_Scan_Repetition_Mode_Change_Event(HCI_Event):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@HCI_Event.registered @HCI_Event.registered
class HCI_Inquiry_Result_With_Rssi_Event(HCI_Event): class HCI_Inquiry_Result_With_RSSI_Event(HCI_Event):
''' '''
See Bluetooth spec @ 7.7.33 Inquiry Result with RSSI Event See Bluetooth spec @ 7.7.33 Inquiry Result with RSSI Event
''' '''
@@ -4566,11 +4634,11 @@ class HCI_Inquiry_Result_With_Rssi_Event(HCI_Event):
responses = [] responses = []
offset = 1 offset = 1
for _ in range(num_responses): for _ in range(num_responses):
response = HCI_Object.from_bytes(parameters, offset, HCI_Inquiry_Result_With_Rssi_Event.RESPONSE_FIELDS) response = HCI_Object.from_bytes(parameters, offset, HCI_Inquiry_Result_With_RSSI_Event.RESPONSE_FIELDS)
offset += 14 offset += 14
responses.append(response) responses.append(response)
return HCI_Inquiry_Result_With_Rssi_Event(responses) return HCI_Inquiry_Result_With_RSSI_Event(responses)
def __init__(self, responses): def __init__(self, responses):
self.responses = responses[:] self.responses = responses[:]

View File

@@ -76,7 +76,7 @@ class Host(EventEmitter):
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
self.acl_packet_queue = collections.deque() self.acl_packet_queue = collections.deque()
self.acl_packets_in_flight = 0 self.acl_packets_in_flight = 0
self.local_version = HCI_VERSION_BLUETOOTH_CORE_4_0 self.local_version = None
self.local_supported_commands = bytes(64) self.local_supported_commands = bytes(64)
self.local_le_features = 0 self.local_le_features = 0
self.command_semaphore = asyncio.Semaphore(1) self.command_semaphore = asyncio.Semaphore(1)
@@ -91,32 +91,23 @@ class Host(EventEmitter):
self.set_packet_sink(controller_sink) self.set_packet_sink(controller_sink)
async def reset(self): async def reset(self):
await self.send_command(HCI_Reset_Command()) await self.send_command(HCI_Reset_Command(), check_result=True)
self.ready = True self.ready = True
response = await self.send_command(HCI_Read_Local_Supported_Commands_Command()) response = await self.send_command(HCI_Read_Local_Supported_Commands_Command(), check_result=True)
if response.return_parameters.status == HCI_SUCCESS: self.local_supported_commands = response.return_parameters.supported_commands
self.local_supported_commands = response.return_parameters.supported_commands
else:
logger.warn(f'HCI_Read_Local_Supported_Commands_Command failed: {response.return_parameters.status}')
if self.supports_command(HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND): if self.supports_command(HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
response = await self.send_command(HCI_LE_Read_Local_Supported_Features_Command()) response = await self.send_command(HCI_LE_Read_Local_Supported_Features_Command(), check_result=True)
if response.return_parameters.status == HCI_SUCCESS: self.local_le_features = struct.unpack('<Q', response.return_parameters.le_features)[0]
self.local_le_features = struct.unpack('<Q', response.return_parameters.le_features)[0]
else:
logger.warn(f'HCI_LE_Read_Supported_Features_Command failed: {response.return_parameters.status}')
if self.supports_command(HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND): if self.supports_command(HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND):
response = await self.send_command(HCI_Read_Local_Version_Information_Command()) response = await self.send_command(HCI_Read_Local_Version_Information_Command(), check_result=True)
if response.return_parameters.status == HCI_SUCCESS: self.local_version = response.return_parameters
self.local_version = response.return_parameters
else:
logger.warn(f'HCI_Read_Local_Version_Information_Command failed: {response.return_parameters.status}')
await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFF3F'))) await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFF3F')))
if self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0: if self.local_version is not None and self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0:
# Some older controllers don't like event masks with bits they don't understand # Some older controllers don't like event masks with bits they don't understand
le_event_mask = bytes.fromhex('1F00000000000000') le_event_mask = bytes.fromhex('1F00000000000000')
else: else:
@@ -124,20 +115,14 @@ class Host(EventEmitter):
await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = le_event_mask)) await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = le_event_mask))
if self.supports_command(HCI_READ_BUFFER_SIZE_COMMAND): if self.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
response = await self.send_command(HCI_Read_Buffer_Size_Command()) response = await self.send_command(HCI_Read_Buffer_Size_Command(), check_result=True)
if response.return_parameters.status == HCI_SUCCESS: self.hc_acl_data_packet_length = response.return_parameters.hc_acl_data_packet_length
self.hc_acl_data_packet_length = response.return_parameters.hc_acl_data_packet_length self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_acl_data_packets
self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_acl_data_packets
else:
logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
if self.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND): if self.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
response = await self.send_command(HCI_LE_Read_Buffer_Size_Command()) response = await self.send_command(HCI_LE_Read_Buffer_Size_Command(), check_result=True)
if response.return_parameters.status == HCI_SUCCESS: self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
else:
logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0: if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0:
# LE and Classic share the same values # LE and Classic share the same values
@@ -171,7 +156,7 @@ class Host(EventEmitter):
def send_hci_packet(self, packet): def send_hci_packet(self, packet):
self.hci_sink.on_packet(packet.to_bytes()) self.hci_sink.on_packet(packet.to_bytes())
async def send_command(self, command): async def send_command(self, command, check_result=False):
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}') logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
# Wait until we can send (only one pending command at a time) # Wait until we can send (only one pending command at a time)
@@ -186,11 +171,22 @@ class Host(EventEmitter):
try: try:
self.send_hci_packet(command) self.send_hci_packet(command)
response = await self.pending_response response = await self.pending_response
# TODO: check error values
# Check the return parameters if required
if check_result:
if type(response.return_parameters) is int:
status = response.return_parameters
else:
status = response.return_parameters.status
if status != HCI_SUCCESS:
logger.warning(f'{command.name} failed ({HCI_Constant.error_name(status)})')
raise HCI_Error(status)
return response return response
except Exception as error: except Exception as error:
logger.warning(f'{color("!!! Exception while sending HCI packet:", "red")} {error}') logger.warning(f'{color("!!! Exception while sending HCI packet:", "red")} {error}')
# raise error raise error
finally: finally:
self.pending_command = None self.pending_command = None
self.pending_response = None self.pending_response = None
@@ -370,8 +366,8 @@ class Host(EventEmitter):
# Notify the client # Notify the client
connection_parameters = ConnectionParameters( connection_parameters = ConnectionParameters(
event.conn_interval, event.connection_interval,
event.conn_latency, event.peripheral_latency,
event.supervision_timeout event.supervision_timeout
) )
self.emit( self.emit(
@@ -387,7 +383,7 @@ class Host(EventEmitter):
logger.debug(f'### CONNECTION FAILED: {event.status}') logger.debug(f'### CONNECTION FAILED: {event.status}')
# Notify the listeners # Notify the listeners
self.emit('connection_failure', event.status) self.emit('connection_failure', event.connection_handle, event.status)
def on_hci_le_enhanced_connection_complete_event(self, event): def on_hci_le_enhanced_connection_complete_event(self, event):
# Just use the same implementation as for the non-enhanced event for now # Just use the same implementation as for the non-enhanced event for now
@@ -435,7 +431,7 @@ class Host(EventEmitter):
logger.debug(f'### DISCONNECTION FAILED: {event.status}') logger.debug(f'### DISCONNECTION FAILED: {event.status}')
# Notify the listeners # Notify the listeners
self.emit('disconnection_failure', event.status) self.emit('disconnection_failure', event.connection_handle, event.status)
def on_hci_le_connection_update_complete_event(self, event): def on_hci_le_connection_update_complete_event(self, event):
if (connection := self.connections.get(event.connection_handle)) is None: if (connection := self.connections.get(event.connection_handle)) is None:
@@ -445,8 +441,8 @@ class Host(EventEmitter):
# Notify the client # Notify the client
if event.status == HCI_SUCCESS: if event.status == HCI_SUCCESS:
connection_parameters = ConnectionParameters( connection_parameters = ConnectionParameters(
event.conn_interval, event.connection_interval,
event.conn_latency, event.peripheral_latency,
event.supervision_timeout event.supervision_timeout
) )
self.emit('connection_parameters_update', connection.handle, connection_parameters) self.emit('connection_parameters_update', connection.handle, connection_parameters)
@@ -470,8 +466,7 @@ class Host(EventEmitter):
self.emit('advertising_report', report) self.emit('advertising_report', report)
def on_hci_le_extended_advertising_report_event(self, event): def on_hci_le_extended_advertising_report_event(self, event):
# TODO self.on_hci_le_advertising_report_event(event)
pass
def on_hci_le_remote_connection_parameter_request_event(self, event): def on_hci_le_remote_connection_parameter_request_event(self, event):
if event.connection_handle not in self.connections: if event.connection_handle not in self.connections:
@@ -487,8 +482,8 @@ class Host(EventEmitter):
interval_max = event.interval_max, interval_max = event.interval_max,
latency = event.latency, latency = event.latency,
timeout = event.timeout, timeout = event.timeout,
minimum_ce_length = 0, min_ce_length = 0,
maximum_ce_length = 0 max_ce_length = 0
) )
) )

View File

@@ -462,10 +462,10 @@ class L2CAP_Information_Response(L2CAP_Control_Frame):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@L2CAP_Control_Frame.subclass([ @L2CAP_Control_Frame.subclass([
('interval_min', 2), ('interval_min', 2),
('interval_max', 2), ('interval_max', 2),
('slave_latency', 2), ('latency', 2),
('timeout_multiplier', 2) ('timeout', 2)
]) ])
class L2CAP_Connection_Parameter_Update_Request(L2CAP_Control_Frame): class L2CAP_Connection_Parameter_Update_Request(L2CAP_Control_Frame):
''' '''
@@ -857,10 +857,10 @@ class ChannelManager:
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256 identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
self.identifiers[connection.handle] = identifier self.identifiers[connection.handle] = identifier
return identifier return identifier
def register_fixed_channel(self, cid, handler): def register_fixed_channel(self, cid, handler):
self.fixed_channels[cid] = handler self.fixed_channels[cid] = handler
def deregister_fixed_channel(self, cid): def deregister_fixed_channel(self, cid):
if cid in self.fixed_channels: if cid in self.fixed_channels:
del self.fixed_channels[cid] del self.fixed_channels[cid]
@@ -1057,13 +1057,13 @@ class ChannelManager:
) )
) )
self.host.send_command_sync(HCI_LE_Connection_Update_Command( self.host.send_command_sync(HCI_LE_Connection_Update_Command(
connection_handle = connection.handle, connection_handle = connection.handle,
conn_interval_min = request.interval_min, connection_interval_min = request.interval_min,
conn_interval_max = request.interval_max, connection_interval_max = request.interval_max,
conn_latency = request.slave_latency, max_latency = request.latency,
supervision_timeout = request.timeout_multiplier, supervision_timeout = request.timeout,
minimum_ce_length = 0, min_ce_length = 0,
maximum_ce_length = 0 max_ce_length = 0
)) ))
else: else:
self.send_control_frame( self.send_control_frame(

View File

@@ -5,8 +5,9 @@ The Android emulator transport either connects, as a host, to a "Root Canal" vir
("host" mode), or attaches a virtual controller to the Android Bluetooth host stack ("controller" mode). ("host" mode), or attaches a virtual controller to the Android Bluetooth host stack ("controller" mode).
## Moniker ## Moniker
The moniker syntax for an Android Emulator transport is: `android-emulator:[mode=<host|controller>][mode=<host|controller>]`. The moniker syntax for an Android Emulator transport is: `android-emulator:[mode=<host|controller>][<hostname>:<port>]`, where
Both the `mode=<host|controller>` and `mode=<host|controller>` parameters are optional (so the moniker `android-emulator` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the emulator) the `mode` parameter can specify running as a host or a controller, and `<hostname>:<port>` can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator.
Both the `mode=<host|controller>` and `<hostname>:<port>` parameters are optional (so the moniker `android-emulator` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the emulator).
!!! example Example !!! example Example
`android-emulator` `android-emulator`

View File

@@ -49,10 +49,10 @@ def test_HCI_LE_Connection_Complete_Event():
role = 1, role = 1,
peer_address_type = 1, peer_address_type = 1,
peer_address = address, peer_address = address,
conn_interval = 3, connection_interval = 3,
conn_latency = 4, peripheral_latency = 4,
supervision_timeout = 5, supervision_timeout = 5,
master_clock_accuracy = 6 central_clock_accuracy = 6
) )
basic_check(event) basic_check(event)
@@ -60,8 +60,8 @@ def test_HCI_LE_Connection_Complete_Event():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_HCI_LE_Advertising_Report_Event(): def test_HCI_LE_Advertising_Report_Event():
address = Address('00:11:22:33:44:55') address = Address('00:11:22:33:44:55')
report = HCI_Object( report = HCI_LE_Advertising_Report_Event.Report(
HCI_LE_Advertising_Report_Event.REPORT_FIELDS, HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type = HCI_LE_Advertising_Report_Event.ADV_IND, event_type = HCI_LE_Advertising_Report_Event.ADV_IND,
address_type = Address.PUBLIC_DEVICE_ADDRESS, address_type = Address.PUBLIC_DEVICE_ADDRESS,
address = address, address = address,
@@ -87,8 +87,8 @@ def test_HCI_LE_Connection_Update_Complete_Event():
event = HCI_LE_Connection_Update_Complete_Event( event = HCI_LE_Connection_Update_Complete_Event(
status = HCI_SUCCESS, status = HCI_SUCCESS,
connection_handle = 0x007, connection_handle = 0x007,
conn_interval = 10, connection_interval = 10,
conn_latency = 3, peripheral_latency = 3,
supervision_timeout = 5 supervision_timeout = 5
) )
basic_check(event) basic_check(event)
@@ -283,12 +283,32 @@ def test_HCI_LE_Create_Connection_Command():
peer_address_type = 1, peer_address_type = 1,
peer_address = Address('00:11:22:33:44:55'), peer_address = Address('00:11:22:33:44:55'),
own_address_type = 2, own_address_type = 2,
conn_interval_min = 7, connection_interval_min = 7,
conn_interval_max = 8, connection_interval_max = 8,
conn_latency = 9, max_latency = 9,
supervision_timeout = 10, supervision_timeout = 10,
minimum_ce_length = 11, min_ce_length = 11,
maximum_ce_length = 12 max_ce_length = 12
)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Extended_Create_Connection_Command():
command = HCI_LE_Extended_Create_Connection_Command(
initiator_filter_policy = 0,
own_address_type = 0,
peer_address_type = 1,
peer_address = Address('00:11:22:33:44:55'),
initiating_phys = 3,
scan_intervals = (10, 11),
scan_windows = (12, 13),
connection_interval_mins = (14, 15),
connection_interval_maxs = (16, 17),
max_latencies = (18, 19),
supervision_timeouts = (20, 21),
min_ce_lengths = (100, 101),
max_ce_lengths = (102, 103)
) )
basic_check(command) basic_check(command)
@@ -314,13 +334,13 @@ def test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_HCI_LE_Connection_Update_Command(): def test_HCI_LE_Connection_Update_Command():
command = HCI_LE_Connection_Update_Command( command = HCI_LE_Connection_Update_Command(
connection_handle = 0x0002, connection_handle = 0x0002,
conn_interval_min = 10, connection_interval_min = 10,
conn_interval_max = 20, connection_interval_max = 20,
conn_latency = 7, max_latency = 7,
supervision_timeout = 3, supervision_timeout = 3,
minimum_ce_length = 100, min_ce_length = 100,
maximum_ce_length = 200 max_ce_length = 200
) )
basic_check(command) basic_check(command)
@@ -348,7 +368,7 @@ def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
command = HCI_LE_Set_Extended_Scan_Parameters_Command( command = HCI_LE_Set_Extended_Scan_Parameters_Command(
own_address_type=Address.RANDOM_DEVICE_ADDRESS, own_address_type=Address.RANDOM_DEVICE_ADDRESS,
scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY, scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
scanning_phys=(1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_1M_PHY | 1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_CODED_PHY | 1 << 4), scanning_phys=(1 << HCI_LE_1M_PHY_BIT | 1 << HCI_LE_CODED_PHY_BIT | 1 << 4),
scan_types=[ scan_types=[
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING, HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING, HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
@@ -408,6 +428,7 @@ def run_test_commands():
test_HCI_LE_Set_Scan_Parameters_Command() test_HCI_LE_Set_Scan_Parameters_Command()
test_HCI_LE_Set_Scan_Enable_Command() test_HCI_LE_Set_Scan_Enable_Command()
test_HCI_LE_Create_Connection_Command() test_HCI_LE_Create_Connection_Command()
test_HCI_LE_Extended_Create_Connection_Command()
test_HCI_LE_Add_Device_To_Filter_Accept_List_Command() test_HCI_LE_Add_Device_To_Filter_Accept_List_Command()
test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command() test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command()
test_HCI_LE_Connection_Update_Command() test_HCI_LE_Connection_Update_Command()