Skip to content

Commit

Permalink
Merge pull request #326 from google/gbg/android-benchmark-app
Browse files Browse the repository at this point in the history
Android benchmarking app
  • Loading branch information
barbibulle authored Nov 22, 2023
2 parents e08c84d + 3dc2e40 commit 284cc8a
Show file tree
Hide file tree
Showing 65 changed files with 2,200 additions and 71 deletions.
204 changes: 180 additions & 24 deletions apps/bench.py

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions apps/controller_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
HCI_LE_Read_Suggested_Default_Data_Length_Command,
)
from bumble.host import Host
from bumble.transport import open_transport_or_link
Expand Down Expand Up @@ -117,6 +119,18 @@ async def get_le_info(host):
'\n',
)

if host.supports_command(HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND):
response = await host.send_command(
HCI_LE_Read_Suggested_Default_Data_Length_Command()
)
if command_succeeded(response):
print(
color('Suggested Default Data Length:', 'yellow'),
f'{response.return_parameters.suggested_max_tx_octets}/'
f'{response.return_parameters.suggested_max_tx_time}',
'\n',
)

print(color('LE Features:', 'yellow'))
for feature in host.supported_le_features:
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
Expand Down
6 changes: 3 additions & 3 deletions bumble/avdtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,15 @@ async def find_avdtp_service_with_sdp_client(

# -----------------------------------------------------------------------------
async def find_avdtp_service_with_connection(
device: device.Device, connection: device.Connection
connection: device.Connection,
) -> Optional[Tuple[int, int]]:
'''
Find an AVDTP service, for a connection, and return its version,
or None if none is found
'''

sdp_client = sdp.Client(device)
await sdp_client.connect(connection)
sdp_client = sdp.Client(connection)
await sdp_client.connect()
service_version = await find_avdtp_service_with_sdp_client(sdp_client)
await sdp_client.disconnect()

Expand Down
20 changes: 20 additions & 0 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
HCI_LE_Set_Advertising_Data_Command,
HCI_LE_Set_Advertising_Enable_Command,
HCI_LE_Set_Advertising_Parameters_Command,
HCI_LE_Set_Data_Length_Command,
HCI_LE_Set_Default_PHY_Command,
HCI_LE_Set_Extended_Scan_Enable_Command,
HCI_LE_Set_Extended_Scan_Parameters_Command,
Expand Down Expand Up @@ -736,6 +737,9 @@ async def sustain(self, timeout=None):
self.remove_listener('disconnection', abort.set_result)
self.remove_listener('disconnection_failure', abort.set_exception)

async def set_data_length(self, tx_octets, tx_time) -> None:
return await self.device.set_data_length(self, tx_octets, tx_time)

async def update_parameters(
self,
connection_interval_min,
Expand Down Expand Up @@ -2193,6 +2197,22 @@ async def disconnect(self, connection, reason):
)
self.disconnecting = False

async def set_data_length(self, connection, tx_octets, tx_time) -> None:
if tx_octets < 0x001B or tx_octets > 0x00FB:
raise ValueError('tx_octets must be between 0x001B and 0x00FB')

if tx_time < 0x0148 or tx_time > 0x4290:
raise ValueError('tx_time must be between 0x0148 and 0x4290')

return await self.send_command(
HCI_LE_Set_Data_Length_Command(
connection_handle=connection.handle,
tx_octets=tx_octets,
tx_time=tx_time,
), # type: ignore[call-arg]
check_result=True,
)

async def update_connection_parameters(
self,
connection,
Expand Down
5 changes: 2 additions & 3 deletions bumble/rfcomm.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,8 +889,7 @@ class Client:
multiplexer: Optional[Multiplexer]
l2cap_channel: Optional[l2cap.ClassicChannel]

def __init__(self, device: Device, connection: Connection) -> None:
self.device = device
def __init__(self, connection: Connection) -> None:
self.connection = connection
self.l2cap_channel = None
self.multiplexer = None
Expand All @@ -906,7 +905,7 @@ async def start(self) -> Multiplexer:
raise

assert self.l2cap_channel is not None
# Create a mutliplexer to manage DLCs with the server
# Create a multiplexer to manage DLCs with the server
self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.Role.INITIATOR)

# Connect the multiplexer
Expand Down
8 changes: 4 additions & 4 deletions bumble/sdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,13 +760,13 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
class Client:
channel: Optional[l2cap.ClassicChannel]

def __init__(self, device: Device) -> None:
self.device = device
def __init__(self, connection: Connection) -> None:
self.connection = connection
self.pending_request = None
self.channel = None

async def connect(self, connection: Connection) -> None:
self.channel = await connection.create_l2cap_channel(
async def connect(self) -> None:
self.channel = await self.connection.create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(SDP_PSM)
)

Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ nav:
- Extras:
- extras/index.md
- Android Remote HCI: extras/android_remote_hci.md
- Android BT Bench: extras/android_bt_bench.md
- Hive:
- hive/index.md
- Speaker: hive/web/speaker/speaker.html
Expand Down
64 changes: 64 additions & 0 deletions docs/mkdocs/src/extras/android_bt_bench.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
ANDROID BENCH APP
=================

This Android app that is compatible with the Bumble `bench` command line app.
This app can be used to test the throughput and latency between two Android
devices, or between an Android device and another device running the Bumble
`bench` app.
Only the RFComm Client, RFComm Server, L2CAP Client and L2CAP Server modes are
supported.

Building
--------

You can build the app by running `./gradlew build` (use `gradlew.bat` on Windows) from the `BtBench` top level directory.
You can also build with Android Studio: open the `BtBench` project. You can build and/or debug from there.

If the build succeeds, you can find the app APKs (debug and release) at:

* [Release] ``app/build/outputs/apk/release/app-release-unsigned.apk``
* [Debug] ``app/build/outputs/apk/debug/app-debug.apk``


Running
-------

### Starting the app
You can start the app from the Android launcher, from Android Studio, or with `adb`

#### Launching from the launcher
Just tap the app icon on the launcher, check the parameters, and tap
one of the benchmark action buttons.

#### Launching with `adb`
Using the `am` command, you can start the activity, and pass it arguments so that you can
automatically start the benchmark test, and/or set the parameters.

| Parameter Name | Parameter Type | Description
|------------------------|----------------|------------
| autostart | String | Benchmark to start. (rfcomm-client, rfcomm-server, l2cap-client or l2cap-server)
| packet-count | Integer | Number of packets to send (rfcomm-client and l2cap-client only)
| packet-size | Integer | Number of bytes per packet (rfcomm-client and l2cap-client only)
| peer-bluetooth-address | Integer | Peer Bluetooth address to connect to (rfcomm-client and l2cap-client | only)


!!! tip "Launching from adb with auto-start"
In this example, we auto-start the Rfcomm Server bench action.
```bash
$ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-server
```

!!! tip "Launching from adb with auto-start and some parameters"
In this example, we auto-start the Rfcomm Client bench action, set the packet count to 100,
and the packet size to 1024, and connect to DA:4C:10:DE:17:02
```bash
$ adb shell am start -n com.github.google.bumble.btbench/.MainActivity --es autostart rfcomm-client --ei packet-count 100 --ei packet-size 1024 --es peer-bluetooth-address DA:4C:10:DE:17:02
```

#### Selecting a Peer Bluetooth Address
The app's main activity has a "Peer Bluetooth Address" setting where you can change the address.

!!! note "Bluetooth Address for L2CAP vs RFComm"
For BLE (L2CAP mode), the address of a device typically changes regularly (it is randomized for privacy), whereas the Bluetooth Classic addresses will remain the same (RFComm mode).
If two devices are paired and bonded, then they will each "see" a non-changing address for each other even with BLE (Resolvable Private Address)

10 changes: 9 additions & 1 deletion docs/mkdocs/src/extras/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ Android Remote HCI

Allows using an Android phone's built-in Bluetooth controller with a Bumble
stack running on a development machine.
See [Android Remote HCI](android_remote_hci.md) for details.
See [Android Remote HCI](android_remote_hci.md) for details.

Android BT Bench
----------------

An Android app that is compatible with the Bumble `bench` command line app.
This app can be used to test the throughput and latency between two Android
devices, or between an Android device and another device running the Bumble
`bench` app.
8 changes: 4 additions & 4 deletions examples/run_a2dp_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ def sdp_records():

# -----------------------------------------------------------------------------
# pylint: disable-next=too-many-nested-blocks
async def find_a2dp_service(device, connection):
async def find_a2dp_service(connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
sdp_client = SDP_Client(connection)
await sdp_client.connect()

# Search for services with an Audio Sink service class
search_result = await sdp_client.search_attributes(
Expand Down Expand Up @@ -177,7 +177,7 @@ async def main():
print('*** Encryption on')

# Look for an A2DP service
avdtp_version = await find_a2dp_service(device, connection)
avdtp_version = await find_a2dp_service(connection)
if not avdtp_version:
print(color('!!! no AVDTP service found'))
return
Expand Down
4 changes: 1 addition & 3 deletions examples/run_a2dp_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,7 @@ async def read(byte_count):
print('*** Encryption on')

# Look for an A2DP service
avdtp_version = await find_avdtp_service_with_connection(
device, connection
)
avdtp_version = await find_avdtp_service_with_connection(connection)
if not avdtp_version:
print(color('!!! no A2DP service found'))
return
Expand Down
4 changes: 2 additions & 2 deletions examples/run_classic_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ async def connect(target_address):
print(f'=== Connected to {connection.peer_address}!')

# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
sdp_client = SDP_Client(connection)
await sdp_client.connect()

# List all services in the root browse group
service_record_handles = await sdp_client.search_services(
Expand Down
6 changes: 3 additions & 3 deletions examples/run_hfp_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
# pylint: disable-next=too-many-nested-blocks
async def list_rfcomm_channels(device, connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
sdp_client = SDP_Client(connection)
await sdp_client.connect()

# Search for services that support the Handsfree Profile
search_result = await sdp_client.search_attributes(
Expand Down Expand Up @@ -184,7 +184,7 @@ async def main():

# Create a client and start it
print('@@@ Starting to RFCOMM client...')
rfcomm_client = rfcomm.Client(device, connection)
rfcomm_client = rfcomm.Client(connection)
rfcomm_mux = await rfcomm_client.start()
print('@@@ Started')

Expand Down
13 changes: 4 additions & 9 deletions examples/run_hid_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,16 @@

from bumble.colors import color

import bumble.core
from bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.core import (
BT_L2CAP_PROTOCOL_ID,
BT_HIDP_PROTOCOL_ID,
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
BT_BR_EDR_TRANSPORT,
)
from bumble.hci import Address
from bumble.hid import Host, Message
from bumble.sdp import (
Client as SDP_Client,
DataElement,
ServiceAttribute,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
Expand Down Expand Up @@ -75,11 +70,11 @@
# -----------------------------------------------------------------------------


async def get_hid_device_sdp_record(device, connection):
async def get_hid_device_sdp_record(connection):

# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
sdp_client = SDP_Client(connection)
await sdp_client.connect()
if sdp_client:
print(color('Connected to SDP Server', 'blue'))
else:
Expand Down Expand Up @@ -348,7 +343,7 @@ def on_hid_virtual_cable_unplug_cb():
await connection.encrypt()
print('*** Encryption on')

await get_hid_device_sdp_record(device, connection)
await get_hid_device_sdp_record(connection)

# Create HID host and start it
print('@@@ Starting HID Host...')
Expand Down
10 changes: 5 additions & 5 deletions examples/run_rfcomm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@


# -----------------------------------------------------------------------------
async def list_rfcomm_channels(device, connection):
async def list_rfcomm_channels(connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
sdp_client = SDP_Client(connection)
await sdp_client.connect()

# Search for services with an L2CAP service attribute
search_result = await sdp_client.search_attributes(
Expand Down Expand Up @@ -194,7 +194,7 @@ async def main():

channel = sys.argv[4]
if channel == 'discover':
await list_rfcomm_channels(device, connection)
await list_rfcomm_channels(connection)
return

# Request authentication
Expand All @@ -209,7 +209,7 @@ async def main():

# Create a client and start it
print('@@@ Starting RFCOMM client...')
rfcomm_client = Client(device, connection)
rfcomm_client = Client(connection)
rfcomm_mux = await rfcomm_client.start()
print('@@@ Started')

Expand Down
15 changes: 15 additions & 0 deletions extras/android/BtBench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
1 change: 1 addition & 0 deletions extras/android/BtBench/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
Loading

0 comments on commit 284cc8a

Please sign in to comment.