forked from auracaster/bumble_mirror
Merge pull request #147 from google/gbg/btbench
add benchmark tool and doc
This commit is contained in:
1207
apps/bench.py
Normal file
1207
apps/bench.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,8 @@ from bumble.hci import (
|
|||||||
HCI_VERSION_NAMES,
|
HCI_VERSION_NAMES,
|
||||||
LMP_VERSION_NAMES,
|
LMP_VERSION_NAMES,
|
||||||
HCI_Command,
|
HCI_Command,
|
||||||
|
HCI_Command_Complete_Event,
|
||||||
|
HCI_Command_Status_Event,
|
||||||
HCI_READ_BD_ADDR_COMMAND,
|
HCI_READ_BD_ADDR_COMMAND,
|
||||||
HCI_Read_BD_ADDR_Command,
|
HCI_Read_BD_ADDR_Command,
|
||||||
HCI_READ_LOCAL_NAME_COMMAND,
|
HCI_READ_LOCAL_NAME_COMMAND,
|
||||||
@@ -45,11 +47,20 @@ from bumble.host import Host
|
|||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def command_succeeded(response):
|
||||||
|
if isinstance(response, HCI_Command_Status_Event):
|
||||||
|
return response.status == HCI_SUCCESS
|
||||||
|
if isinstance(response, HCI_Command_Complete_Event):
|
||||||
|
return response.return_parameters.status == HCI_SUCCESS
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_classic_info(host):
|
async def get_classic_info(host):
|
||||||
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||||
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
||||||
@@ -57,7 +68,7 @@ async def get_classic_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
||||||
response = await host.send_command(HCI_Read_Local_Name_Command())
|
response = await host.send_command(HCI_Read_Local_Name_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Local Name:', 'yellow'),
|
color('Local Name:', 'yellow'),
|
||||||
@@ -73,7 +84,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
||||||
response.return_parameters.num_supported_advertising_sets,
|
response.return_parameters.num_supported_advertising_sets,
|
||||||
@@ -84,7 +95,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Maximum Advertising Data Length:', 'yellow'),
|
color('LE Maximum Advertising Data Length:', 'yellow'),
|
||||||
response.return_parameters.max_advertising_data_length,
|
response.return_parameters.max_advertising_data_length,
|
||||||
@@ -93,7 +104,7 @@ async def get_le_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
||||||
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('Maximum Data Length:', 'yellow'),
|
color('Maximum Data Length:', 'yellow'),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from .hci import (
|
|||||||
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
||||||
HCI_LE_RAND_COMMAND,
|
HCI_LE_RAND_COMMAND,
|
||||||
HCI_LE_READ_PHY_COMMAND,
|
HCI_LE_READ_PHY_COMMAND,
|
||||||
|
HCI_LE_SET_PHY_COMMAND,
|
||||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
@@ -1245,6 +1246,11 @@ class Device(CompositeEventEmitter):
|
|||||||
# Done
|
# Done
|
||||||
self.powered_on = True
|
self.powered_on = True
|
||||||
|
|
||||||
|
async def power_off(self) -> None:
|
||||||
|
if self.powered_on:
|
||||||
|
await self.host.flush()
|
||||||
|
self.powered_on = False
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -1669,7 +1675,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not phys:
|
if not phys:
|
||||||
raise ValueError('least one supported PHY needed')
|
raise ValueError('at least one supported PHY needed')
|
||||||
|
|
||||||
phy_count = len(phys)
|
phy_count = len(phys)
|
||||||
initiating_phys = phy_list_to_bits(phys)
|
initiating_phys = phy_list_to_bits(phys)
|
||||||
@@ -1810,7 +1816,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.abort_on('flush', pending_connection)
|
return await self.abort_on('flush', pending_connection)
|
||||||
except ConnectionError as error:
|
except core.ConnectionError as error:
|
||||||
raise core.TimeoutError() from error
|
raise core.TimeoutError() from error
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener('connection', on_connection)
|
self.remove_listener('connection', on_connection)
|
||||||
@@ -2044,21 +2050,31 @@ class Device(CompositeEventEmitter):
|
|||||||
async def set_connection_phy(
|
async def set_connection_phy(
|
||||||
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
||||||
):
|
):
|
||||||
|
if not self.host.supports_command(HCI_LE_SET_PHY_COMMAND):
|
||||||
|
logger.warning('ignoring request, command not supported')
|
||||||
|
return
|
||||||
|
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Set_PHY_Command(
|
HCI_LE_Set_PHY_Command(
|
||||||
connection_handle=connection.handle,
|
connection_handle=connection.handle,
|
||||||
all_phys=all_phys_bits,
|
all_phys=all_phys_bits,
|
||||||
tx_phys=phy_list_to_bits(tx_phys),
|
tx_phys=phy_list_to_bits(tx_phys),
|
||||||
rx_phys=phy_list_to_bits(rx_phys),
|
rx_phys=phy_list_to_bits(rx_phys),
|
||||||
phy_options=0 if phy_options is None else int(phy_options),
|
phy_options=0 if phy_options is None else int(phy_options),
|
||||||
),
|
)
|
||||||
check_result=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
|
logger.warning(
|
||||||
|
'HCI_LE_Set_PHY_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
|
)
|
||||||
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
@@ -2497,7 +2513,7 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising = False
|
self.advertising = False
|
||||||
|
|
||||||
# Notify listeners
|
# Notify listeners
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
transport,
|
transport,
|
||||||
peer_address,
|
peer_address,
|
||||||
@@ -2570,7 +2586,7 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_disconnection_failure(self, connection, error_code):
|
def on_disconnection_failure(self, connection, error_code):
|
||||||
logger.debug(f'*** Disconnection failed: {error_code}')
|
logger.debug(f'*** Disconnection failed: {error_code}')
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
connection.transport,
|
connection.transport,
|
||||||
connection.peer_address,
|
connection.peer_address,
|
||||||
|
|||||||
@@ -691,7 +691,7 @@ class Server(EventEmitter):
|
|||||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"not found {request}")
|
logging.debug(f"not found {request}")
|
||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
|
|||||||
@@ -796,6 +796,11 @@ class Channel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.OPEN:
|
||||||
|
self.change_state(self.CLOSED)
|
||||||
|
self.emit('close')
|
||||||
|
|
||||||
def send_configure_request(self):
|
def send_configure_request(self):
|
||||||
options = L2CAP_Control_Frame.encode_configuration_options(
|
options = L2CAP_Control_Frame.encode_configuration_options(
|
||||||
[
|
[
|
||||||
@@ -1105,6 +1110,10 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.CONNECTED:
|
||||||
|
self.change_state(self.DISCONNECTED)
|
||||||
|
|
||||||
def on_pdu(self, pdu):
|
def on_pdu(self, pdu):
|
||||||
if self.sink is None:
|
if self.sink is None:
|
||||||
logger.warning('received pdu without a sink')
|
logger.warning('received pdu without a sink')
|
||||||
@@ -1492,8 +1501,12 @@ class ChannelManager:
|
|||||||
def on_disconnection(self, connection_handle, _reason):
|
def on_disconnection(self, connection_handle, _reason):
|
||||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||||
if connection_handle in self.channels:
|
if connection_handle in self.channels:
|
||||||
|
for _, channel in self.channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.channels[connection_handle]
|
del self.channels[connection_handle]
|
||||||
if connection_handle in self.le_coc_channels:
|
if connection_handle in self.le_coc_channels:
|
||||||
|
for _, channel in self.le_coc_channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.le_coc_channels[connection_handle]
|
del self.le_coc_channels[connection_handle]
|
||||||
if connection_handle in self.identifiers:
|
if connection_handle in self.identifiers:
|
||||||
del self.identifiers[connection_handle]
|
del self.identifiers[connection_handle]
|
||||||
|
|||||||
@@ -852,17 +852,27 @@ class Server(EventEmitter):
|
|||||||
# Register ourselves with the L2CAP channel manager
|
# Register ourselves with the L2CAP channel manager
|
||||||
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
||||||
|
|
||||||
def listen(self, acceptor):
|
def listen(self, acceptor, channel=0):
|
||||||
# Find a free channel number
|
if channel:
|
||||||
for channel in range(
|
if channel in self.acceptors:
|
||||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1
|
# Busy
|
||||||
):
|
return 0
|
||||||
if channel not in self.acceptors:
|
else:
|
||||||
self.acceptors[channel] = acceptor
|
# Find a free channel number
|
||||||
return channel
|
for candidate in range(
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START,
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1,
|
||||||
|
):
|
||||||
|
if candidate not in self.acceptors:
|
||||||
|
channel = candidate
|
||||||
|
break
|
||||||
|
|
||||||
# All channels used...
|
if channel == 0:
|
||||||
return 0
|
# All channels used...
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self.acceptors[channel] = acceptor
|
||||||
|
return channel
|
||||||
|
|
||||||
def on_connection(self, l2cap_channel):
|
def on_connection(self, l2cap_channel):
|
||||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import collections
|
import collections
|
||||||
import sys
|
import sys
|
||||||
from typing import Awaitable, TypeVar
|
from typing import Awaitable, Set, TypeVar
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
@@ -157,6 +157,9 @@ class AsyncRunner:
|
|||||||
# Shared default queue
|
# Shared default queue
|
||||||
default_queue = WorkQueue()
|
default_queue = WorkQueue()
|
||||||
|
|
||||||
|
# Shared set of running tasks
|
||||||
|
running_tasks: Set[Awaitable] = set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run_in_task(queue=None):
|
def run_in_task(queue=None):
|
||||||
"""
|
"""
|
||||||
@@ -187,6 +190,19 @@ class AsyncRunner:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def spawn(coroutine):
|
||||||
|
"""
|
||||||
|
Spawn a task to run a coroutine in a "fire and forget" mode.
|
||||||
|
|
||||||
|
Using this method instead of just calling `asyncio.create_task(coroutine)`
|
||||||
|
is necessary when you don't keep a reference to the task, because `asyncio`
|
||||||
|
only keeps weak references to alive tasks.
|
||||||
|
"""
|
||||||
|
task = asyncio.create_task(coroutine)
|
||||||
|
AsyncRunner.running_tasks.add(task)
|
||||||
|
task.add_done_callback(AsyncRunner.running_tasks.remove)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class FlowControlAsyncPipe:
|
class FlowControlAsyncPipe:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ nav:
|
|||||||
- Apps & Tools:
|
- Apps & Tools:
|
||||||
- Overview: apps_and_tools/index.md
|
- Overview: apps_and_tools/index.md
|
||||||
- Console: apps_and_tools/console.md
|
- Console: apps_and_tools/console.md
|
||||||
- Link Relay: apps_and_tools/link_relay.md
|
- Bench: apps_and_tools/bench.md
|
||||||
- HCI Bridge: apps_and_tools/hci_bridge.md
|
- HCI Bridge: apps_and_tools/hci_bridge.md
|
||||||
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
||||||
- Show: apps_and_tools/show.md
|
- Show: apps_and_tools/show.md
|
||||||
@@ -51,6 +51,7 @@ nav:
|
|||||||
- Pair: apps_and_tools/pair.md
|
- Pair: apps_and_tools/pair.md
|
||||||
- Unbond: apps_and_tools/unbond.md
|
- Unbond: apps_and_tools/unbond.md
|
||||||
- USB Probe: apps_and_tools/usb_probe.md
|
- USB Probe: apps_and_tools/usb_probe.md
|
||||||
|
- Link Relay: apps_and_tools/link_relay.md
|
||||||
- Hardware:
|
- Hardware:
|
||||||
- Overview: hardware/index.md
|
- Overview: hardware/index.md
|
||||||
- Platforms:
|
- Platforms:
|
||||||
@@ -62,7 +63,7 @@ nav:
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Overview: examples/index.md
|
- Overview: examples/index.md
|
||||||
|
|
||||||
copyright: Copyright 2021-2022 Google LLC
|
copyright: Copyright 2021-2023 Google LLC
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: 'material'
|
name: 'material'
|
||||||
|
|||||||
158
docs/mkdocs/src/apps_and_tools/bench.md
Normal file
158
docs/mkdocs/src/apps_and_tools/bench.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
BENCH TOOL
|
||||||
|
==========
|
||||||
|
|
||||||
|
The "bench" tool implements a number of different ways of measuring the
|
||||||
|
throughput and/or latency between two devices.
|
||||||
|
|
||||||
|
# General Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: bench.py [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--device-config FILENAME Device configuration file
|
||||||
|
--role [sender|receiver|ping|pong]
|
||||||
|
--mode [gatt-client|gatt-server|l2cap-client|l2cap-server|rfcomm-client|rfcomm-server]
|
||||||
|
--att-mtu MTU GATT MTU (gatt-client mode) [23<=x<=517]
|
||||||
|
-s, --packet-size SIZE Packet size (server role) [8<=x<=4096]
|
||||||
|
-c, --packet-count COUNT Packet count (server role)
|
||||||
|
-sd, --start-delay SECONDS Start delay (server role)
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
central Run as a central (initiates the connection)
|
||||||
|
peripheral Run as a peripheral (waits for a connection)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options for the ``central`` Command
|
||||||
|
```
|
||||||
|
Usage: bumble-bench central [OPTIONS] TRANSPORT
|
||||||
|
|
||||||
|
Run as a central (initiates the connection)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--peripheral ADDRESS_OR_NAME Address or name to connect to
|
||||||
|
--connection-interval, --ci CONNECTION_INTERVAL
|
||||||
|
Connection interval (in ms)
|
||||||
|
--phy [1m|2m|coded] PHY to use
|
||||||
|
--help Show this message and exit.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To test once device against another, one of the two devices must be running
|
||||||
|
the ``peripheral`` command and the other the ``central`` command. The device
|
||||||
|
running the ``peripheral`` command will accept connections from the device
|
||||||
|
running the ``central`` command.
|
||||||
|
When using Bluetooth LE (all modes except for ``rfcomm-server`` and ``rfcomm-client``utils),
|
||||||
|
the default addresses configured in the tool should be sufficient. But when using
|
||||||
|
Bluetooth Classic, the address of the Peripheral must be specified on the Central
|
||||||
|
using the ``--peripheral`` option. The address will be printed by the Peripheral when
|
||||||
|
it starts.
|
||||||
|
|
||||||
|
Independently of whether the device is the Central or Peripheral, each device selects a
|
||||||
|
``mode`` and and ``role`` to run as. The ``mode`` and ``role`` of the Central and Peripheral
|
||||||
|
must be compatible.
|
||||||
|
|
||||||
|
Device 1 mode | Device 2 mode
|
||||||
|
------------------|------------------
|
||||||
|
``gatt-client`` | ``gatt-server``
|
||||||
|
``l2cap-client`` | ``l2cap-server``
|
||||||
|
``rfcomm-client`` | ``rfcomm-server``
|
||||||
|
|
||||||
|
Device 1 role | Device 2 role
|
||||||
|
--------------|--------------
|
||||||
|
``sender`` | ``receiver``
|
||||||
|
``ping`` | ``pong``
|
||||||
|
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
In the following examples, we have two USB Bluetooth controllers, one on `usb:0` and
|
||||||
|
the other on `usb:1`, and two consoles/terminals. We will run a command in each.
|
||||||
|
|
||||||
|
!!! example "GATT Throughput"
|
||||||
|
Using the default mode and role for the Central and Peripheral.
|
||||||
|
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
In this default configuration, the Central runs a Sender, as a GATT client,
|
||||||
|
connecting to the Peripheral running a Receiver, as a GATT server.
|
||||||
|
|
||||||
|
!!! example "L2CAP Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "RFComm Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: the BT address of the Peripheral will be printed out, use it with the
|
||||||
|
``--peripheral`` option for the Central.
|
||||||
|
|
||||||
|
In this example, we use a larger packet size and packet count than the default.
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-client --packet-size 2000 --packet-count 100 central --peripheral 00:16:A4:5A:40:F2 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Ping/Pong Latency"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role pong peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role ping central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with GATT and custom connection interval"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-server central --ci 10 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with L2CAP and custom PHY"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server central --phy 2m usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed roles with L2CAP"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client --role sender peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server --role receiver central usb:1
|
||||||
|
```
|
||||||
@@ -5,6 +5,7 @@ Included in the project are a few apps and tools, built on top of the core libra
|
|||||||
These include:
|
These include:
|
||||||
|
|
||||||
* [Console](console.md) - an interactive text-based console
|
* [Console](console.md) - an interactive text-based console
|
||||||
|
* [Bench](bench.md) - Speed and Latency benchmarking between two devices (LE and Classic)
|
||||||
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
||||||
* [Unbond](unbond.md) - Remove a previously established bond
|
* [Unbond](unbond.md) - Remove a previously established bond
|
||||||
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ The project initially only supported BLE (Bluetooth Low Energy), but support for
|
|||||||
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
This project is still very much experimental and in an alpha state where a lot of things are still missing or broken, and what's there changes frequently.
|
This project is still in an early state of development where some things are still missing or broken, and what's implemented may change and evolve frequently.
|
||||||
Also, there are still a few hardcoded values/parameters in some of the examples and apps which need to be changed (those will eventually be command line arguments, as appropriate)
|
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ console_scripts =
|
|||||||
bumble-unbond = bumble.apps.unbond:main
|
bumble-unbond = bumble.apps.unbond:main
|
||||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||||
|
bumble-bench = bumble.apps.bench:main
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
* = py.typed, *.pyi
|
* = py.typed, *.pyi
|
||||||
|
|||||||
Reference in New Issue
Block a user