This commit is contained in:
Gilles Boccon-Gibod
2024-05-12 11:54:16 -07:00
parent f910a696ad
commit 999d7b07e1
4 changed files with 98 additions and 30 deletions

View File

@@ -40,6 +40,8 @@ from bumble.hci import (
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
HCI_LE_2M_PHY, HCI_LE_2M_PHY,
HCI_LE_CODED_PHY, HCI_LE_CODED_PHY,
HCI_CENTRAL_ROLE,
HCI_PERIPHERAL_ROLE,
HCI_Constant, HCI_Constant,
HCI_Error, HCI_Error,
HCI_StatusError, HCI_StatusError,
@@ -57,6 +59,7 @@ from bumble.transport import open_transport_or_link
import bumble.rfcomm import bumble.rfcomm
import bumble.core import bumble.core
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.pairing import PairingConfig
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -128,40 +131,34 @@ def le_phy_name(phy_id):
def print_connection(connection): def print_connection(connection):
params = []
if connection.transport == BT_LE_TRANSPORT: if connection.transport == BT_LE_TRANSPORT:
phy_state = ( params.append(
'PHY=' 'PHY='
f'TX:{le_phy_name(connection.phy.tx_phy)}/' f'TX:{le_phy_name(connection.phy.tx_phy)}/'
f'RX:{le_phy_name(connection.phy.rx_phy)}' f'RX:{le_phy_name(connection.phy.rx_phy)}'
) )
data_length = ( params.append(
'DL=(' 'DL=('
f'TX:{connection.data_length[0]}/{connection.data_length[1]},' f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
f'RX:{connection.data_length[2]}/{connection.data_length[3]}' f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
')' ')'
) )
connection_parameters = (
params.append(
'Parameters=' 'Parameters='
f'{connection.parameters.connection_interval * 1.25:.2f}/' f'{connection.parameters.connection_interval * 1.25:.2f}/'
f'{connection.parameters.peripheral_latency}/' f'{connection.parameters.peripheral_latency}/'
f'{connection.parameters.supervision_timeout * 10} ' f'{connection.parameters.supervision_timeout * 10} '
) )
params.append(f'MTU={connection.att_mtu}')
else: else:
phy_state = '' params.append(f'Role={HCI_Constant.role_name(connection.role)}')
data_length = ''
connection_parameters = ''
mtu = connection.att_mtu logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
logging.info(
f'{color("@@@ Connection:", "yellow")} '
f'{connection_parameters} '
f'{data_length} '
f'{phy_state} '
f'MTU={mtu}'
)
def make_sdp_records(channel): def make_sdp_records(channel):
@@ -214,6 +211,17 @@ def log_stats(title, stats):
) )
async def switch_roles(connection, role):
target_role = HCI_CENTRAL_ROLE if role == "central" else HCI_PERIPHERAL_ROLE
if connection.role != target_role:
logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
try:
await connection.switch_role(target_role)
logging.info(color('### Role switch complete', 'cyan'))
except HCI_Error as error:
logging.info(f'{color("### Role switch failed:", "red")} {error}')
class PacketType(enum.IntEnum): class PacketType(enum.IntEnum):
RESET = 0 RESET = 0
SEQUENCE = 1 SEQUENCE = 1
@@ -1000,6 +1008,8 @@ class RfcommServer(StreamedPacketIO):
self.max_credits = max_credits self.max_credits = max_credits
self.credits_threshold = credits_threshold self.credits_threshold = credits_threshold
self.dlc = None self.dlc = None
self.max_credits = max_credits
self.credits_threshold = credits_threshold
self.ready = asyncio.Event() self.ready = asyncio.Event()
# Create and register a server # Create and register a server
@@ -1034,6 +1044,10 @@ class RfcommServer(StreamedPacketIO):
def on_dlc(self, dlc): def on_dlc(self, dlc):
logging.info(color(f'*** DLC connected: {dlc}', 'blue')) logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
if self.credits_threshold is not None:
dlc.rx_threshold = self.credits_threshold
if self.max_credits is not None:
dlc.rx_max_credits = self.max_credits
dlc.sink = self.on_packet dlc.sink = self.on_packet
self.io_sink = dlc.write self.io_sink = dlc.write
self.dlc = dlc self.dlc = dlc
@@ -1063,6 +1077,7 @@ class Central(Connection.Listener):
authenticate, authenticate,
encrypt, encrypt,
extended_data_length, extended_data_length,
role_switch,
): ):
super().__init__() super().__init__()
self.transport = transport self.transport = transport
@@ -1073,6 +1088,7 @@ class Central(Connection.Listener):
self.authenticate = authenticate self.authenticate = authenticate
self.encrypt = encrypt or authenticate self.encrypt = encrypt or authenticate
self.extended_data_length = extended_data_length self.extended_data_length = extended_data_length
self.role_switch = role_switch
self.device = None self.device = None
self.connection = None self.connection = None
@@ -1123,6 +1139,11 @@ class Central(Connection.Listener):
role = self.role_factory(mode) role = self.role_factory(mode)
self.device.classic_enabled = self.classic self.device.classic_enabled = self.classic
# Set up a pairing config factory with minimal requirements.
self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
await self.device.power_on() await self.device.power_on()
if self.classic: if self.classic:
@@ -1151,6 +1172,10 @@ class Central(Connection.Listener):
self.connection.listener = self self.connection.listener = self
print_connection(self.connection) print_connection(self.connection)
# Switch roles if needed.
if self.role_switch:
await switch_roles(self.connection, self.role_switch)
# Wait a bit after the connection, some controllers aren't very good when # Wait a bit after the connection, some controllers aren't very good when
# we start sending data right away while some connection parameters are # we start sending data right away while some connection parameters are
# updated post connection # updated post connection
@@ -1212,20 +1237,30 @@ class Central(Connection.Listener):
def on_connection_data_length_change(self): def on_connection_data_length_change(self):
print_connection(self.connection) print_connection(self.connection)
def on_role_change(self):
print_connection(self.connection)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Peripheral # Peripheral
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Peripheral(Device.Listener, Connection.Listener): class Peripheral(Device.Listener, Connection.Listener):
def __init__( def __init__(
self, transport, classic, extended_data_length, role_factory, mode_factory self,
transport,
role_factory,
mode_factory,
classic,
extended_data_length,
role_switch,
): ):
self.transport = transport self.transport = transport
self.classic = classic self.classic = classic
self.extended_data_length = extended_data_length
self.role_factory = role_factory self.role_factory = role_factory
self.role = None
self.mode_factory = mode_factory self.mode_factory = mode_factory
self.extended_data_length = extended_data_length
self.role_switch = role_switch
self.role = None
self.mode = None self.mode = None
self.device = None self.device = None
self.connection = None self.connection = None
@@ -1248,6 +1283,11 @@ class Peripheral(Device.Listener, Connection.Listener):
self.role = self.role_factory(self.mode) self.role = self.role_factory(self.mode)
self.device.classic_enabled = self.classic self.device.classic_enabled = self.classic
# Set up a pairing config factory with minimal requirements.
self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
await self.device.power_on() await self.device.power_on()
if self.classic: if self.classic:
@@ -1274,6 +1314,7 @@ class Peripheral(Device.Listener, Connection.Listener):
await self.connected.wait() await self.connected.wait()
logging.info(color('### Connected', 'cyan')) logging.info(color('### Connected', 'cyan'))
print_connection(self.connection)
await self.mode.on_connection(self.connection) await self.mode.on_connection(self.connection)
await self.role.run() await self.role.run()
@@ -1290,7 +1331,7 @@ class Peripheral(Device.Listener, Connection.Listener):
AsyncRunner.spawn(self.device.set_connectable(False)) AsyncRunner.spawn(self.device.set_connectable(False))
# Request a new data length if needed # Request a new data length if needed
if self.extended_data_length: if not self.classic and self.extended_data_length:
logging.info("+++ Requesting extended data length") logging.info("+++ Requesting extended data length")
AsyncRunner.spawn( AsyncRunner.spawn(
connection.set_data_length( connection.set_data_length(
@@ -1298,6 +1339,10 @@ class Peripheral(Device.Listener, Connection.Listener):
) )
) )
# Switch roles if needed.
if self.role_switch:
AsyncRunner.spawn(switch_roles(connection, self.role_switch))
def on_disconnection(self, reason): def on_disconnection(self, reason):
logging.info(color(f'!!! Disconnection: reason={reason}', 'red')) logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
self.connection = None self.connection = None
@@ -1319,6 +1364,9 @@ class Peripheral(Device.Listener, Connection.Listener):
def on_connection_data_length_change(self): def on_connection_data_length_change(self):
print_connection(self.connection) print_connection(self.connection)
def on_role_change(self):
print_connection(self.connection)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def create_mode_factory(ctx, default_mode): def create_mode_factory(ctx, default_mode):
@@ -1448,6 +1496,11 @@ def create_role_factory(ctx, default_role):
'--extended-data-length', '--extended-data-length',
help='Request a data length upon connection, specified as tx_octets/tx_time', help='Request a data length upon connection, specified as tx_octets/tx_time',
) )
@click.option(
'--role-switch',
type=click.Choice(['central', 'peripheral']),
help='Request role switch upon connection (central or peripheral)',
)
@click.option( @click.option(
'--rfcomm-channel', '--rfcomm-channel',
type=int, type=int,
@@ -1484,6 +1537,11 @@ def create_role_factory(ctx, default_role):
type=int, type=int,
help='RFComm credits threshold', help='RFComm credits threshold',
) )
@click.option(
'--rfcomm-send-credits-threshold',
type=int,
help='RFComm send credits threshold',
)
@click.option( @click.option(
'--l2cap-psm', '--l2cap-psm',
type=int, type=int,
@@ -1512,7 +1570,7 @@ def create_role_factory(ctx, default_role):
'--packet-size', '--packet-size',
'-s', '-s',
metavar='SIZE', metavar='SIZE',
type=click.IntRange(8, 4096), type=click.IntRange(8, 8192),
default=500, default=500,
help='Packet size (client or ping role)', help='Packet size (client or ping role)',
) )
@@ -1572,6 +1630,7 @@ def bench(
mode, mode,
att_mtu, att_mtu,
extended_data_length, extended_data_length,
role_switch,
packet_size, packet_size,
packet_count, packet_count,
start_delay, start_delay,
@@ -1614,12 +1673,12 @@ def bench(
ctx.obj['repeat_delay'] = repeat_delay ctx.obj['repeat_delay'] = repeat_delay
ctx.obj['pace'] = pace ctx.obj['pace'] = pace
ctx.obj['linger'] = linger ctx.obj['linger'] = linger
ctx.obj['extended_data_length'] = ( ctx.obj['extended_data_length'] = (
[int(x) for x in extended_data_length.split('/')] [int(x) for x in extended_data_length.split('/')]
if extended_data_length if extended_data_length
else None else None
) )
ctx.obj['role_switch'] = role_switch
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server') ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
@@ -1663,6 +1722,7 @@ def central(
authenticate, authenticate,
encrypt or authenticate, encrypt or authenticate,
ctx.obj['extended_data_length'], ctx.obj['extended_data_length'],
ctx.obj['role_switch'],
).run() ).run()
asyncio.run(run_central()) asyncio.run(run_central())
@@ -1679,10 +1739,11 @@ def peripheral(ctx, transport):
async def run_peripheral(): async def run_peripheral():
await Peripheral( await Peripheral(
transport, transport,
ctx.obj['classic'],
ctx.obj['extended_data_length'],
role_factory, role_factory,
mode_factory, mode_factory,
ctx.obj['classic'],
ctx.obj['extended_data_length'],
ctx.obj['role_switch'],
).run() ).run()
asyncio.run(run_peripheral()) asyncio.run(run_peripheral())

View File

@@ -27,7 +27,7 @@ from bumble.colors import color
from bumble.core import name_or_number from bumble.core import name_or_number
from bumble.hci import ( from bumble.hci import (
map_null_terminated_utf8_string, map_null_terminated_utf8_string,
LeFeatureMask, LeFeature,
HCI_SUCCESS, HCI_SUCCESS,
HCI_VERSION_NAMES, HCI_VERSION_NAMES,
LMP_VERSION_NAMES, LMP_VERSION_NAMES,
@@ -140,7 +140,7 @@ async def get_le_info(host: Host) -> None:
print(color('LE Features:', 'yellow')) print(color('LE Features:', 'yellow'))
for feature in host.supported_le_features: for feature in host.supported_le_features:
print(LeFeatureMask(feature).name) print(f' {LeFeature(feature).name}')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -224,7 +224,7 @@ async def async_main(latency_probes, transport):
print() print()
print(color('Supported Commands:', 'yellow')) print(color('Supported Commands:', 'yellow'))
for command in host.supported_commands: for command in host.supported_commands:
print(' ', HCI_Command.command_name(command)) print(f' {HCI_Command.command_name(command)}')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -26,8 +26,8 @@ import struct
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union, ClassVar from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union, ClassVar
from bumble import crypto from bumble import crypto
from .colors import color from bumble.colors import color
from .core import ( from bumble.core import (
BT_BR_EDR_TRANSPORT, BT_BR_EDR_TRANSPORT,
AdvertisingData, AdvertisingData,
DeviceClass, DeviceClass,
@@ -36,6 +36,7 @@ from .core import (
name_or_number, name_or_number,
padded_bytes, padded_bytes,
) )
from bumble.utils import OpenIntEnum
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -1104,7 +1105,7 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
# LE Supported Features # LE Supported Features
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT # See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
class LeFeature(enum.IntEnum): class LeFeature(OpenIntEnum):
LE_ENCRYPTION = 0 LE_ENCRYPTION = 0
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1
EXTENDED_REJECT_INDICATION = 2 EXTENDED_REJECT_INDICATION = 2

View File

@@ -734,7 +734,13 @@ class DLC(EventEmitter):
self.emit('close') self.emit('close')
def __str__(self) -> str: def __str__(self) -> str:
return f'DLC(dlci={self.dlci},state={self.state.name})' return (
f'DLC(dlci={self.dlci}, '
f'state={self.state.name}, '
f'max_frame_size={self.max_frame_size}, '
f'window_size={self.window_size}'
')'
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------