This commit is contained in:
Gilles Boccon-Gibod
2024-09-22 18:33:08 -07:00
parent b190069f48
commit 55f99e6887
21 changed files with 732 additions and 144 deletions

View File

@@ -19,6 +19,7 @@ import asyncio
import enum import enum
import logging import logging
import os import os
import statistics
import struct import struct
import time import time
@@ -194,17 +195,19 @@ def make_sdp_records(channel):
} }
def log_stats(title, stats): def log_stats(title, stats, precision=2):
stats_min = min(stats) stats_min = min(stats)
stats_max = max(stats) stats_max = max(stats)
stats_avg = sum(stats) / len(stats) stats_avg = statistics.mean(stats)
stats_stdev = statistics.stdev(stats)
logging.info( logging.info(
color( color(
( (
f'### {title} stats: ' f'### {title} stats: '
f'min={stats_min:.2f}, ' f'min={stats_min:.{precision}f}, '
f'max={stats_max:.2f}, ' f'max={stats_max:.{precision}f}, '
f'average={stats_avg:.2f}' f'average={stats_avg:.{precision}f}, '
f'stdev={stats_stdev:.{precision}f}'
), ),
'cyan', 'cyan',
) )
@@ -448,9 +451,9 @@ class Ping:
self.repeat_delay = repeat_delay self.repeat_delay = repeat_delay
self.pace = pace self.pace = pace
self.done = asyncio.Event() self.done = asyncio.Event()
self.current_packet_index = 0 self.ping_times = []
self.ping_sent_time = 0.0 self.rtts = []
self.latencies = [] self.next_expected_packet_index = 0
self.min_stats = [] self.min_stats = []
self.max_stats = [] self.max_stats = []
self.avg_stats = [] self.avg_stats = []
@@ -477,60 +480,57 @@ class Ping:
logging.info(color('=== Sending RESET', 'magenta')) logging.info(color('=== Sending RESET', 'magenta'))
await self.packet_io.send_packet(bytes([PacketType.RESET])) await self.packet_io.send_packet(bytes([PacketType.RESET]))
self.current_packet_index = 0 packet_interval = self.pace / 1000
self.latencies = [] start_time = time.time()
await self.send_next_ping() self.next_expected_packet_index = 0
for i in range(self.tx_packet_count):
target_time = start_time + (i * packet_interval)
now = time.time()
if now < target_time:
await asyncio.sleep(target_time - now)
packet = struct.pack(
'>bbI',
PacketType.SEQUENCE,
(PACKET_FLAG_LAST if i == self.tx_packet_count - 1 else 0),
i,
) + bytes(self.tx_packet_size - 6)
logging.info(color(f'Sending packet {i}', 'yellow'))
self.ping_times.append(time.time())
await self.packet_io.send_packet(packet)
await self.done.wait() await self.done.wait()
min_latency = min(self.latencies) min_rtt = min(self.rtts)
max_latency = max(self.latencies) max_rtt = max(self.rtts)
avg_latency = sum(self.latencies) / len(self.latencies) avg_rtt = statistics.mean(self.rtts)
stdev_rtt = statistics.stdev(self.rtts)
logging.info( logging.info(
color( color(
'@@@ Latencies: ' '@@@ RTTs: '
f'min={min_latency:.2f}, ' f'min={min_rtt:.2f}, '
f'max={max_latency:.2f}, ' f'max={max_rtt:.2f}, '
f'average={avg_latency:.2f}' f'average={avg_rtt:.2f}, '
f'stdev={stdev_rtt:.2f}'
) )
) )
self.min_stats.append(min_latency) self.min_stats.append(min_rtt)
self.max_stats.append(max_latency) self.max_stats.append(max_rtt)
self.avg_stats.append(avg_latency) self.avg_stats.append(avg_rtt)
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else '' run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
logging.info(color(f'=== {run_counter} Done!', 'magenta')) logging.info(color(f'=== {run_counter} Done!', 'magenta'))
if self.repeat: if self.repeat:
log_stats('Min Latency', self.min_stats) log_stats('Min RTT', self.min_stats)
log_stats('Max Latency', self.max_stats) log_stats('Max RTT', self.max_stats)
log_stats('Average Latency', self.avg_stats) log_stats('Average RTT', self.avg_stats)
if self.repeat: if self.repeat:
logging.info(color('--- End of runs', 'blue')) logging.info(color('--- End of runs', 'blue'))
async def send_next_ping(self):
if self.pace:
await asyncio.sleep(self.pace / 1000)
packet = struct.pack(
'>bbI',
PacketType.SEQUENCE,
(
PACKET_FLAG_LAST
if self.current_packet_index == self.tx_packet_count - 1
else 0
),
self.current_packet_index,
) + bytes(self.tx_packet_size - 6)
logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
self.ping_sent_time = time.time()
await self.packet_io.send_packet(packet)
def on_packet_received(self, packet): def on_packet_received(self, packet):
elapsed = time.time() - self.ping_sent_time
try: try:
packet_type, packet_data = parse_packet(packet) packet_type, packet_data = parse_packet(packet)
except ValueError: except ValueError:
@@ -542,21 +542,23 @@ class Ping:
return return
if packet_type == PacketType.ACK: if packet_type == PacketType.ACK:
latency = elapsed * 1000 elapsed = time.time() - self.ping_times[packet_index]
self.latencies.append(latency) rtt = elapsed * 1000
self.rtts.append(rtt)
logging.info( logging.info(
color( color(
f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms', f'<<< Received ACK [{packet_index}], RTT={rtt:.2f}ms',
'green', 'green',
) )
) )
if packet_index == self.current_packet_index: if packet_index == self.next_expected_packet_index:
self.current_packet_index += 1 self.next_expected_packet_index += 1
else: else:
logging.info( logging.info(
color( color(
f'!!! Unexpected packet, expected {self.current_packet_index} ' f'!!! Unexpected packet, '
f'expected {self.next_expected_packet_index} '
f'but received {packet_index}' f'but received {packet_index}'
) )
) )
@@ -565,8 +567,6 @@ class Ping:
self.done.set() self.done.set()
return return
AsyncRunner.spawn(self.send_next_ping())
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Pong # Pong
@@ -583,8 +583,11 @@ class Pong:
def reset(self): def reset(self):
self.expected_packet_index = 0 self.expected_packet_index = 0
self.receive_times = []
def on_packet_received(self, packet): def on_packet_received(self, packet):
self.receive_times.append(time.time())
try: try:
packet_type, packet_data = parse_packet(packet) packet_type, packet_data = parse_packet(packet)
except ValueError: except ValueError:
@@ -599,10 +602,16 @@ class Pong:
packet_flags, packet_index = parse_packet_sequence(packet_data) packet_flags, packet_index = parse_packet_sequence(packet_data)
except ValueError: except ValueError:
return return
interval = (
self.receive_times[-1] - self.receive_times[-2]
if len(self.receive_times) >= 2
else 0
)
logging.info( logging.info(
color( color(
f'<<< Received packet {packet_index}: ' f'<<< Received packet {packet_index}: '
f'flags=0x{packet_flags:02X}, {len(packet)} bytes', f'flags=0x{packet_flags:02X}, {len(packet)} bytes, '
f'interval={interval:.4f}',
'green', 'green',
) )
) )
@@ -623,8 +632,35 @@ class Pong:
) )
) )
if packet_flags & PACKET_FLAG_LAST and not self.linger: if packet_flags & PACKET_FLAG_LAST:
self.done.set() if len(self.receive_times) >= 3:
# Show basic stats
intervals = [
self.receive_times[i + 1] - self.receive_times[i]
for i in range(len(self.receive_times) - 1)
]
log_stats('Packet intervals', intervals, 3)
# Show a histogram
bin_count = 20
bins = [0] * bin_count
interval_min = min(intervals)
interval_max = max(intervals)
interval_range = interval_max - interval_min
bin_thresholds = [
interval_min + i * (interval_range / bin_count)
for i in range(bin_count)
]
for interval in intervals:
for i in reversed(range(bin_count)):
if interval >= bin_thresholds[i]:
bins[i] += 1
break
for i in range(bin_count):
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
if not self.linger:
self.done.set()
async def run(self): async def run(self):
await self.done.wait() await self.done.wait()
@@ -942,9 +978,12 @@ class RfcommClient(StreamedPacketIO):
channel = await bumble.rfcomm.find_rfcomm_channel_with_uuid( channel = await bumble.rfcomm.find_rfcomm_channel_with_uuid(
connection, self.uuid connection, self.uuid
) )
logging.info(color(f'@@@ Channel number = {channel}', 'cyan')) if channel:
if channel == 0: logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
logging.info(color('!!! No RFComm service with this UUID found', 'red')) else:
logging.warning(
color('!!! No RFComm service with this UUID found', 'red')
)
await connection.disconnect() await connection.disconnect()
return return
@@ -1054,6 +1093,8 @@ class RfcommServer(StreamedPacketIO):
if self.credits_threshold is not None: if self.credits_threshold is not None:
dlc.rx_credits_threshold = self.credits_threshold dlc.rx_credits_threshold = self.credits_threshold
self.ready.set()
async def drain(self): async def drain(self):
assert self.dlc assert self.dlc
await self.dlc.drain() await self.dlc.drain()
@@ -1503,7 +1544,7 @@ def create_role_factory(ctx, default_role):
'--rfcomm-channel', '--rfcomm-channel',
type=int, type=int,
default=DEFAULT_RFCOMM_CHANNEL, default=DEFAULT_RFCOMM_CHANNEL,
help='RFComm channel to use', help='RFComm channel to use (specify 0 for channel discovery via SDP)',
) )
@click.option( @click.option(
'--rfcomm-uuid', '--rfcomm-uuid',
@@ -1743,7 +1784,11 @@ def peripheral(ctx, transport):
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) logging.basicConfig(
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
bench() bench()

View File

@@ -237,6 +237,7 @@ class ClientBridge:
address: str, address: str,
tcp_host: str, tcp_host: str,
tcp_port: int, tcp_port: int,
authenticate: bool,
encrypt: bool, encrypt: bool,
): ):
self.channel = channel self.channel = channel
@@ -245,6 +246,7 @@ class ClientBridge:
self.address = address self.address = address
self.tcp_host = tcp_host self.tcp_host = tcp_host
self.tcp_port = tcp_port self.tcp_port = tcp_port
self.authenticate = authenticate
self.encrypt = encrypt self.encrypt = encrypt
self.device: Optional[Device] = None self.device: Optional[Device] = None
self.connection: Optional[Connection] = None self.connection: Optional[Connection] = None
@@ -274,6 +276,11 @@ class ClientBridge:
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue")) print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
self.connection.on("disconnection", self.on_disconnection) self.connection.on("disconnection", self.on_disconnection)
if self.authenticate:
print(color("@@@ Authenticating Bluetooth connection", "blue"))
await self.connection.authenticate()
print(color("@@@ Bluetooth connection authenticated", "blue"))
if self.encrypt: if self.encrypt:
print(color("@@@ Encrypting Bluetooth connection", "blue")) print(color("@@@ Encrypting Bluetooth connection", "blue"))
await self.connection.encrypt() await self.connection.encrypt()
@@ -491,8 +498,9 @@ def server(context, tcp_host, tcp_port):
@click.argument("bluetooth-address") @click.argument("bluetooth-address")
@click.option("--tcp-host", help="TCP host", default="_") @click.option("--tcp-host", help="TCP host", default="_")
@click.option("--tcp-port", help="TCP port", default=DEFAULT_CLIENT_TCP_PORT) @click.option("--tcp-port", help="TCP port", default=DEFAULT_CLIENT_TCP_PORT)
@click.option("--authenticate", is_flag=True, help="Authenticate the connection")
@click.option("--encrypt", is_flag=True, help="Encrypt the connection") @click.option("--encrypt", is_flag=True, help="Encrypt the connection")
def client(context, bluetooth_address, tcp_host, tcp_port, encrypt): def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt):
bridge = ClientBridge( bridge = ClientBridge(
context.obj["channel"], context.obj["channel"],
context.obj["uuid"], context.obj["uuid"],
@@ -500,6 +508,7 @@ def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
bluetooth_address, bluetooth_address,
tcp_host, tcp_host,
tcp_port, tcp_port,
authenticate,
encrypt, encrypt,
) )
asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge)) asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge))

View File

@@ -60,7 +60,8 @@ dependencies {
implementation(libs.ui.graphics) implementation(libs.ui.graphics)
implementation(libs.ui.tooling.preview) implementation(libs.ui.tooling.preview)
implementation(libs.material3) implementation(libs.material3)
implementation("com.google.android.mobly:mobly-snippet-lib:1.4.0") implementation(libs.mobly.snippet)
implementation(libs.androidx.core)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.espresso.core)

View File

@@ -0,0 +1,105 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.github.google.bumble.btbench;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.renderscript.RSInvalidStateException;
import android.util.Log;
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.rpc.Rpc;
import androidx.test.core.app.ApplicationProvider;
import org.json.JSONException;
import org.json.JSONObject;
public class AutomationSnippet implements Snippet {
private static final String TAG = "btbench.snippet";
private final BluetoothAdapter mBluetoothAdapter;
private AppViewModel rfcommServerModel;
private RfcommServer rfcommServer;
private AppViewModel l2capServerModel;
private L2capServer l2capServer;
public AutomationSnippet() {
Context context = ApplicationProvider.getApplicationContext();
BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
throw new RuntimeException("bluetooth not supported");
}
}
private static JSONObject throughputStats(AppViewModel model) throws JSONException {
JSONObject result = new JSONObject();
JSONObject stats = new JSONObject();
result.put("stats", stats);
JSONObject throughputStats = new JSONObject();
stats.put("throughput", throughputStats);
throughputStats.put("average", model.getThroughput());
return result;
}
@Rpc(description = "Run an RFComm client throughput test")
public JSONObject runRfcommClient(String peerBluetoothAddress, int packetCount, int packetSize) throws JSONException {
assert(mBluetoothAdapter != null);
AppViewModel model = new AppViewModel();
model.setPeerBluetoothAddress(peerBluetoothAddress);
model.setSenderPacketCount(packetCount);
model.setSenderPacketSize(packetSize);
//RfcommClient rfCommClient = new RfcommClient(model, mBluetoothAdapter);
//rfCommClient.run(true);
return throughputStats(model);
}
@Rpc(description = "Run an L2CAP client throughput test")
public JSONObject runL2capClient(String peerBluetoothAddress, int psm, boolean use_2m_phy, int packetCount, int packetSize) throws JSONException {
assert(mBluetoothAdapter != null);
AppViewModel model = new AppViewModel();
model.setPeerBluetoothAddress(peerBluetoothAddress);
model.setL2capPsm(psm);
model.setUse2mPhy(use_2m_phy);
model.setSenderPacketCount(packetCount);
model.setSenderPacketSize(packetSize);
Context context = ApplicationProvider.getApplicationContext();
//L2capClient l2capClient = new L2capClient(model, mBluetoothAdapter, context);
//l2capClient.run(true);
return throughputStats(model);
}
@Rpc(description = "Run an RFComm server")
public JSONObject runRfcommServer() throws JSONException {
assert(mBluetoothAdapter != null);
if (rfcommServerModel != null) {
rfcommServerModel.abort();
rfcommServerModel = null;
rfcommServer = null;
}
rfcommServerModel = new AppViewModel();
//rfcommServer = new RfcommServer(rfcommServerModel, mBluetoothAdapter);
//rfcommServer.run(true);
return new JSONObject();
}
@Override
public void shutdown() {
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.github.google.bumble.btbench
interface IoClient {
fun run()
fun abort()
}

View File

@@ -29,10 +29,11 @@ private val Log = Logger.getLogger("btbench.l2cap-client")
class L2capClient( class L2capClient(
private val viewModel: AppViewModel, private val viewModel: AppViewModel,
private val bluetoothAdapter: BluetoothAdapter, private val bluetoothAdapter: BluetoothAdapter,
private val context: Context private val context: Context,
) { private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun run() { override fun run(blocking: Boolean) {
viewModel.running = true viewModel.running = true
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P") val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
val address = viewModel.peerBluetoothAddress.take(17) val address = viewModel.peerBluetoothAddress.take(17)
@@ -75,6 +76,7 @@ class L2capClient(
) { ) {
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) { if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
if (viewModel.use2mPhy) { if (viewModel.use2mPhy) {
Log.info("requesting 2M PHY")
gatt.setPreferredPhy( gatt.setPreferredPhy(
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_LE_2M_MASK,
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_LE_2M_MASK,
@@ -95,7 +97,7 @@ class L2capClient(
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm) val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
val client = SocketClient(viewModel, socket) val client = SocketClient(viewModel, socket, createIoClient)
client.run() client.run(blocking)
} }
} }

View File

@@ -27,11 +27,15 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.l2cap-server") private val Log = Logger.getLogger("btbench.l2cap-server")
class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) { class L2capServer(
private val viewModel: AppViewModel,
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun run() { override fun run(blocking: Boolean) {
// Advertise so that the peer can find us and connect. // Advertise so that the peer can find us and connect.
val callback = object: AdvertiseCallback() { val callback = object : AdvertiseCallback() {
override fun onStartFailure(errorCode: Int) { override fun onStartFailure(errorCode: Int) {
Log.warning("failed to start advertising: $errorCode") Log.warning("failed to start advertising: $errorCode")
} }
@@ -55,7 +59,11 @@ class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdap
viewModel.l2capPsm = serverSocket.psm viewModel.l2capPsm = serverSocket.psm
Log.info("psm = $serverSocket.psm") Log.info("psm = $serverSocket.psm")
val server = SocketServer(viewModel, serverSocket) val server = SocketServer(viewModel, serverSocket, createIoClient)
server.run({ advertiser.stopAdvertising(callback) }, { advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) }) server.run(
{ advertiser.stopAdvertising(callback) },
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) },
blocking
)
} }
} }

View File

@@ -34,12 +34,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -54,6 +57,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -69,6 +73,9 @@ private val Log = Logger.getLogger("bumble.main-activity")
const val PEER_BLUETOOTH_ADDRESS_PREF_KEY = "peer_bluetooth_address" const val PEER_BLUETOOTH_ADDRESS_PREF_KEY = "peer_bluetooth_address"
const val SENDER_PACKET_COUNT_PREF_KEY = "sender_packet_count" const val SENDER_PACKET_COUNT_PREF_KEY = "sender_packet_count"
const val SENDER_PACKET_SIZE_PREF_KEY = "sender_packet_size" const val SENDER_PACKET_SIZE_PREF_KEY = "sender_packet_size"
const val SENDER_PACKET_INTERVAL_PREF_KEY = "sender_packet_interval"
const val SCENARIO_PREF_KEY = "scenario"
const val MODE_PREF_KEY = "mode"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val appViewModel = AppViewModel() private val appViewModel = AppViewModel()
@@ -139,10 +146,7 @@ class MainActivity : ComponentActivity() {
MainView( MainView(
appViewModel, appViewModel,
::becomeDiscoverable, ::becomeDiscoverable,
::runRfcommClient, ::runScenario
::runRfcommServer,
::runL2capClient,
::runL2capServer,
) )
} }
@@ -159,6 +163,10 @@ class MainActivity : ComponentActivity() {
if (packetSize > 0) { if (packetSize > 0) {
appViewModel.senderPacketSize = packetSize appViewModel.senderPacketSize = packetSize
} }
val packetInterval = intent.getIntExtra("packet-interval", 0)
if (packetInterval > 0) {
appViewModel.senderPacketInterval = packetInterval
}
appViewModel.updateSenderPacketSizeSlider() appViewModel.updateSenderPacketSizeSlider()
intent.getStringExtra("autostart")?.let { intent.getStringExtra("autostart")?.let {
when (it) { when (it) {
@@ -172,24 +180,54 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun runScenario() {
if (bluetoothAdapter == null) {
return
}
val runner = when (appViewModel.mode) {
RFCOMM_CLIENT_MODE -> RfcommClient(appViewModel, bluetoothAdapter!!, ::createIoClient)
RFCOMM_SERVER_MODE -> RfcommServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
L2CAP_CLIENT_MODE -> L2capClient(
appViewModel,
bluetoothAdapter!!,
baseContext,
::createIoClient
)
L2CAP_SERVER_MODE -> L2capServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
else -> throw IllegalStateException()
}
runner.run(false)
}
private fun createIoClient(packetIo: PacketIO): IoClient {
return when (appViewModel.scenario) {
SEND_SCENARIO -> Sender(appViewModel, packetIo)
RECEIVE_SCENARIO -> Receiver(appViewModel, packetIo)
PING_SCENARIO -> Pinger(appViewModel, packetIo)
PONG_SCENARIO -> Ponger(appViewModel, packetIo)
else -> throw IllegalStateException()
}
}
private fun runRfcommClient() { private fun runRfcommClient() {
val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) } // val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) }
rfcommClient?.run() // rfcommClient?.run()
} }
private fun runRfcommServer() { private fun runRfcommServer() {
val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) } // val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) }
rfcommServer?.run() // rfcommServer?.run()
} }
private fun runL2capClient() { private fun runL2capClient() {
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) } // val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) }
l2capClient?.run() // l2capClient?.run()
} }
private fun runL2capServer() { private fun runL2capServer() {
val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) } // val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) }
l2capServer?.run() // l2capServer?.run()
} }
private fun runScan(startScan: Boolean) { private fun runScan(startScan: Boolean) {
@@ -210,10 +248,7 @@ class MainActivity : ComponentActivity() {
fun MainView( fun MainView(
appViewModel: AppViewModel, appViewModel: AppViewModel,
becomeDiscoverable: () -> Unit, becomeDiscoverable: () -> Unit,
runRfcommClient: () -> Unit, runScenario: () -> Unit,
runRfcommServer: () -> Unit,
runL2capClient: () -> Unit,
runL2capServer: () -> Unit,
) { ) {
BTBenchTheme { BTBenchTheme {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -239,7 +274,9 @@ fun MainView(
Text(text = "Peer Bluetooth Address") Text(text = "Peer Bluetooth Address")
}, },
value = appViewModel.peerBluetoothAddress, value = appViewModel.peerBluetoothAddress,
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
), ),
@@ -249,14 +286,18 @@ fun MainView(
keyboardActions = KeyboardActions(onDone = { keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
}) }),
enabled = (appViewModel.mode == RFCOMM_CLIENT_MODE) or (appViewModel.mode == L2CAP_CLIENT_MODE)
) )
Divider() Divider()
TextField(label = { TextField(
Text(text = "L2CAP PSM") label = {
}, Text(text = "L2CAP PSM")
},
value = appViewModel.l2capPsm.toString(), value = appViewModel.l2capPsm.toString(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
), ),
@@ -271,7 +312,8 @@ fun MainView(
keyboardActions = KeyboardActions(onDone = { keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
}) }),
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE)
) )
Divider() Divider()
Slider( Slider(
@@ -290,6 +332,32 @@ fun MainView(
) )
Text(text = "Packet Size: " + appViewModel.senderPacketSize.toString()) Text(text = "Packet Size: " + appViewModel.senderPacketSize.toString())
Divider() Divider()
TextField(
label = {
Text(text = "Packet Interval (ms)")
},
value = appViewModel.senderPacketInterval.toString(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
onValueChange = {
if (it.isNotEmpty()) {
val interval = it.toIntOrNull()
if (interval != null) {
appViewModel.updateSenderPacketInterval(interval)
}
}
},
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus()
}),
enabled = (appViewModel.scenario == PING_SCENARIO)
)
Divider()
ActionButton( ActionButton(
text = "Become Discoverable", onClick = becomeDiscoverable, true text = "Become Discoverable", onClick = becomeDiscoverable, true
) )
@@ -300,25 +368,78 @@ fun MainView(
Text(text = "2M PHY") Text(text = "2M PHY")
Spacer(modifier = Modifier.padding(start = 8.dp)) Spacer(modifier = Modifier.padding(start = 8.dp))
Switch( Switch(
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == L2CAP_SERVER_MODE),
checked = appViewModel.use2mPhy, checked = appViewModel.use2mPhy,
onCheckedChange = { appViewModel.use2mPhy = it } onCheckedChange = { appViewModel.use2mPhy = it }
) )
} }
Row { Row {
ActionButton( Column(Modifier.selectableGroup()) {
text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running listOf(
) RFCOMM_CLIENT_MODE,
ActionButton( RFCOMM_SERVER_MODE,
text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running L2CAP_CLIENT_MODE,
) L2CAP_SERVER_MODE
).forEach { text ->
Row(
Modifier
.selectable(
selected = (text == appViewModel.mode),
onClick = { appViewModel.updateMode(text) },
role = Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (text == appViewModel.mode),
onClick = null
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
Column(Modifier.selectableGroup()) {
listOf(
SEND_SCENARIO,
RECEIVE_SCENARIO,
PING_SCENARIO,
PONG_SCENARIO
).forEach { text ->
Row(
Modifier
.selectable(
selected = (text == appViewModel.scenario),
onClick = { appViewModel.updateScenario(text) },
role = Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (text == appViewModel.scenario),
onClick = null
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
} }
Row { Row {
ActionButton( ActionButton(
text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running text = "Start", onClick = runScenario, enabled = !appViewModel.running
) )
ActionButton( ActionButton(
text = "L2CAP Server", onClick = runL2capServer, !appViewModel.running text = "Stop", onClick = appViewModel::abort, enabled = appViewModel.running
) )
} }
Divider() Divider()
@@ -337,9 +458,8 @@ fun MainView(
Text( Text(
text = "Throughput: ${appViewModel.throughput}" text = "Throughput: ${appViewModel.throughput}"
) )
Divider() Text(
ActionButton( text = "Stats: ${appViewModel.stats}"
text = "Abort", onClick = appViewModel::abort, appViewModel.running
) )
} }
} }

View File

@@ -0,0 +1,19 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.github.google.bumble.btbench
interface Mode {
fun run(blocking: Boolean)
}

View File

@@ -27,10 +27,23 @@ val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF
const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF" const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
const val DEFAULT_SENDER_PACKET_COUNT = 100 const val DEFAULT_SENDER_PACKET_COUNT = 100
const val DEFAULT_SENDER_PACKET_SIZE = 1024 const val DEFAULT_SENDER_PACKET_SIZE = 1024
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
const val DEFAULT_PSM = 128 const val DEFAULT_PSM = 128
const val L2CAP_CLIENT_MODE = "L2CAP Client"
const val L2CAP_SERVER_MODE = "L2CAP Server"
const val RFCOMM_CLIENT_MODE = "RFCOMM Client"
const val RFCOMM_SERVER_MODE = "RFCOMM Server"
const val SEND_SCENARIO = "Send"
const val RECEIVE_SCENARIO = "Receive"
const val PING_SCENARIO = "Ping"
const val PONG_SCENARIO = "Pong"
class AppViewModel : ViewModel() { class AppViewModel : ViewModel() {
private var preferences: SharedPreferences? = null private var preferences: SharedPreferences? = null
var mode by mutableStateOf(RFCOMM_SERVER_MODE)
var scenario by mutableStateOf(RECEIVE_SCENARIO)
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS) var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
var l2capPsm by mutableIntStateOf(DEFAULT_PSM) var l2capPsm by mutableIntStateOf(DEFAULT_PSM)
var use2mPhy by mutableStateOf(true) var use2mPhy by mutableStateOf(true)
@@ -41,9 +54,11 @@ class AppViewModel : ViewModel() {
var senderPacketSizeSlider by mutableFloatStateOf(0.0F) var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT) var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
var senderPacketSize by mutableIntStateOf(DEFAULT_SENDER_PACKET_SIZE) var senderPacketSize by mutableIntStateOf(DEFAULT_SENDER_PACKET_SIZE)
var senderPacketInterval by mutableIntStateOf(DEFAULT_SENDER_PACKET_INTERVAL)
var packetsSent by mutableIntStateOf(0) var packetsSent by mutableIntStateOf(0)
var packetsReceived by mutableIntStateOf(0) var packetsReceived by mutableIntStateOf(0)
var throughput by mutableIntStateOf(0) var throughput by mutableIntStateOf(0)
var stats by mutableStateOf("")
var running by mutableStateOf(false) var running by mutableStateOf(false)
var aborter: (() -> Unit)? = null var aborter: (() -> Unit)? = null
@@ -66,6 +81,21 @@ class AppViewModel : ViewModel() {
senderPacketSize = savedSenderPacketSize senderPacketSize = savedSenderPacketSize
} }
updateSenderPacketSizeSlider() updateSenderPacketSizeSlider()
val savedSenderPacketInterval = preferences.getInt(SENDER_PACKET_INTERVAL_PREF_KEY, -1)
if (savedSenderPacketInterval != -1) {
senderPacketInterval = savedSenderPacketInterval
}
val savedMode = preferences.getString(MODE_PREF_KEY, null)
if (savedMode != null) {
mode = savedMode
}
val savedScenario = preferences.getString(SCENARIO_PREF_KEY, null)
if (savedScenario != null) {
scenario = savedScenario
}
} }
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) { fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
@@ -164,6 +194,30 @@ class AppViewModel : ViewModel() {
} }
} }
fun updateSenderPacketInterval(senderPacketInterval: Int) {
this.senderPacketInterval = senderPacketInterval
with(preferences!!.edit()) {
putInt(SENDER_PACKET_INTERVAL_PREF_KEY, senderPacketInterval)
apply()
}
}
fun updateScenario(scenario: String) {
this.scenario = scenario
with(preferences!!.edit()) {
putString(SCENARIO_PREF_KEY, scenario)
apply()
}
}
fun updateMode(mode: String) {
this.mode = mode
with(preferences!!.edit()) {
putString(MODE_PREF_KEY, mode)
apply()
}
}
fun abort() { fun abort() {
aborter?.let { it() } aborter?.let { it() }
} }

View File

@@ -74,13 +74,13 @@ abstract class PacketSink {
fun onPacket(packet: Packet) { fun onPacket(packet: Packet) {
when (packet) { when (packet) {
is ResetPacket -> onResetPacket() is ResetPacket -> onResetPacket()
is AckPacket -> onAckPacket() is AckPacket -> onAckPacket(packet)
is SequencePacket -> onSequencePacket(packet) is SequencePacket -> onSequencePacket(packet)
} }
} }
abstract fun onResetPacket() abstract fun onResetPacket()
abstract fun onAckPacket() abstract fun onAckPacket(packet: AckPacket)
abstract fun onSequencePacket(packet: SequencePacket) abstract fun onSequencePacket(packet: SequencePacket)
} }

View File

@@ -0,0 +1,106 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.github.google.bumble.btbench
import java.util.concurrent.Semaphore
import java.util.logging.Logger
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
private const val DEFAULT_STARTUP_DELAY = 3000
private val Log = Logger.getLogger("btbench.pinger")
class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
PacketSink() {
private val pingTimes: ArrayList<TimeSource.Monotonic.ValueTimeMark> = ArrayList()
private val rtts: ArrayList<Long> = ArrayList()
private val done = Semaphore(0)
init {
packetIO.packetSink = this
}
override fun run() {
viewModel.packetsSent = 0
viewModel.packetsReceived = 0
viewModel.stats = ""
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
Log.info("running")
Log.info("sending reset")
packetIO.sendPacket(ResetPacket())
val packetCount = viewModel.senderPacketCount
val packetSize = viewModel.senderPacketSize
val startTime = TimeSource.Monotonic.markNow()
for (i in 0..<packetCount) {
val now = TimeSource.Monotonic.markNow()
val targetTime = startTime + (i * viewModel.senderPacketInterval).milliseconds
val delay = targetTime - now
if (delay.isPositive()) {
Log.info("sleeping ${delay.inWholeMilliseconds} ms")
Thread.sleep(delay.inWholeMilliseconds)
}
pingTimes.add(TimeSource.Monotonic.markNow())
packetIO.sendPacket(
SequencePacket(
if (i < packetCount - 1) 0 else Packet.LAST_FLAG,
i,
ByteArray(packetSize - 6)
)
)
viewModel.packetsSent = i + 1
}
// Wait for the last ACK
Log.info("waiting for last ACK")
done.acquire()
Log.info("got last ACK")
}
override fun abort() {
done.release()
}
override fun onResetPacket() {
}
override fun onAckPacket(packet: AckPacket) {
val now = TimeSource.Monotonic.markNow()
viewModel.packetsReceived += 1
if (packet.sequenceNumber < pingTimes.size) {
val rtt = (now - pingTimes[packet.sequenceNumber]).inWholeMilliseconds
rtts.add(rtt)
Log.info("received ACK ${packet.sequenceNumber}, RTT=$rtt")
} else {
Log.warning("received ACK with unexpected sequence ${packet.sequenceNumber}")
}
if (packet.flags and Packet.LAST_FLAG != 0) {
Log.info("last packet received")
val stats = "RTTs: min=${rtts.min()}, max=${rtts.max()}, avg=${rtts.sum() / rtts.size}"
Log.info(stats)
viewModel.stats = stats
done.release()
}
}
override fun onSequencePacket(packet: SequencePacket) {
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.github.google.bumble.btbench
import java.util.logging.Logger
import kotlin.time.TimeSource
private val Log = Logger.getLogger("btbench.receiver")
class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient, PacketSink() {
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
private var expectedSequenceNumber: Int = 0
init {
packetIO.packetSink = this
}
override fun run() {}
override fun abort() {}
override fun onResetPacket() {
startTime = TimeSource.Monotonic.markNow()
lastPacketTime = startTime
expectedSequenceNumber = 0
viewModel.packetsSent = 0
viewModel.packetsReceived = 0
viewModel.stats = ""
}
override fun onAckPacket(packet: AckPacket) {
}
override fun onSequencePacket(packet: SequencePacket) {
val now = TimeSource.Monotonic.markNow()
lastPacketTime = now
viewModel.packetsReceived += 1
if (packet.sequenceNumber != expectedSequenceNumber) {
Log.warning("unexpected packet sequence number (expected ${expectedSequenceNumber}, got ${packet.sequenceNumber})")
}
expectedSequenceNumber += 1
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
viewModel.packetsSent += 1
}
}

View File

@@ -20,7 +20,7 @@ import kotlin.time.TimeSource
private val Log = Logger.getLogger("btbench.receiver") private val Log = Logger.getLogger("btbench.receiver")
class Receiver(private val viewModel: AppViewModel, private val packetIO: PacketIO) : PacketSink() { class Receiver(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient, PacketSink() {
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
private var bytesReceived = 0 private var bytesReceived = 0
@@ -29,6 +29,10 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
packetIO.packetSink = this packetIO.packetSink = this
} }
override fun run() {}
override fun abort() {}
override fun onResetPacket() { override fun onResetPacket() {
startTime = TimeSource.Monotonic.markNow() startTime = TimeSource.Monotonic.markNow()
lastPacketTime = startTime lastPacketTime = startTime
@@ -36,9 +40,10 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
viewModel.throughput = 0 viewModel.throughput = 0
viewModel.packetsSent = 0 viewModel.packetsSent = 0
viewModel.packetsReceived = 0 viewModel.packetsReceived = 0
viewModel.stats = ""
} }
override fun onAckPacket() { override fun onAckPacket(packet: AckPacket) {
} }

View File

@@ -16,22 +16,24 @@ package com.github.google.bumble.btbench
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import java.io.IOException
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.rfcomm-client") private val Log = Logger.getLogger("btbench.rfcomm-client")
class RfcommClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) { class RfcommClient(
private val viewModel: AppViewModel,
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun run() { override fun run(blocking: Boolean) {
val address = viewModel.peerBluetoothAddress.take(17) val address = viewModel.peerBluetoothAddress.take(17)
val remoteDevice = bluetoothAdapter.getRemoteDevice(address) val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord( val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
DEFAULT_RFCOMM_UUID DEFAULT_RFCOMM_UUID
) )
val client = SocketClient(viewModel, socket) val client = SocketClient(viewModel, socket, createIoClient)
client.run() client.run(blocking)
} }
} }

View File

@@ -22,14 +22,18 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.rfcomm-server") private val Log = Logger.getLogger("btbench.rfcomm-server")
class RfcommServer(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) { class RfcommServer(
private val viewModel: AppViewModel,
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun run() { override fun run(blocking: Boolean) {
val serverSocket = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord( val serverSocket = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
"BumbleBench", DEFAULT_RFCOMM_UUID "BumbleBench", DEFAULT_RFCOMM_UUID
) )
val server = SocketServer(viewModel, serverSocket) val server = SocketServer(viewModel, serverSocket, createIoClient)
server.run({}, {}) server.run({}, {}, blocking)
} }
} }

View File

@@ -19,9 +19,12 @@ import java.util.logging.Logger
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.TimeSource import kotlin.time.TimeSource
private const val DEFAULT_STARTUP_DELAY = 3000
private val Log = Logger.getLogger("btbench.sender") private val Log = Logger.getLogger("btbench.sender")
class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO) : PacketSink() { class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
PacketSink() {
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
private var bytesSent = 0 private var bytesSent = 0
private val done = Semaphore(0) private val done = Semaphore(0)
@@ -30,10 +33,15 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
packetIO.packetSink = this packetIO.packetSink = this
} }
fun run() { override fun run() {
viewModel.packetsSent = 0 viewModel.packetsSent = 0
viewModel.packetsReceived = 0 viewModel.packetsReceived = 0
viewModel.throughput = 0 viewModel.throughput = 0
viewModel.stats = ""
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
Log.info("running")
Log.info("sending reset") Log.info("sending reset")
packetIO.sendPacket(ResetPacket()) packetIO.sendPacket(ResetPacket())
@@ -63,14 +71,14 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
Log.info("got ACK") Log.info("got ACK")
} }
fun abort() { override fun abort() {
done.release() done.release()
} }
override fun onResetPacket() { override fun onResetPacket() {
} }
override fun onAckPacket() { override fun onAckPacket(packet: AckPacket) {
Log.info("received ACK") Log.info("received ACK")
val elapsed = TimeSource.Monotonic.markNow() - startTime val elapsed = TimeSource.Monotonic.markNow() - startTime
val throughput = (bytesSent / elapsed.toDouble(DurationUnit.SECONDS)).toInt() val throughput = (bytesSent / elapsed.toDouble(DurationUnit.SECONDS)).toInt()

View File

@@ -22,16 +22,18 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.socket-client") private val Log = Logger.getLogger("btbench.socket-client")
private const val DEFAULT_STARTUP_DELAY = 3000 class SocketClient(
private val viewModel: AppViewModel,
class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) { private val socket: BluetoothSocket,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun run() { fun run(blocking: Boolean = false) {
viewModel.running = true viewModel.running = true
val socketDataSink = SocketDataSink(socket) val socketDataSink = SocketDataSink(socket)
val streamIO = StreamedPacketIO(socketDataSink) val streamIO = StreamedPacketIO(socketDataSink)
val socketDataSource = SocketDataSource(socket, streamIO::onData) val socketDataSource = SocketDataSource(socket, streamIO::onData)
val sender = Sender(viewModel, streamIO) val ioClient = createIoClient(streamIO)
fun cleanup() { fun cleanup() {
socket.close() socket.close()
@@ -39,9 +41,9 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
viewModel.running = false viewModel.running = false
} }
thread(name = "SocketClient") { val clientThread = thread(name = "SocketClient") {
viewModel.aborter = { viewModel.aborter = {
sender.abort() ioClient.abort()
socket.close() socket.close()
} }
Log.info("connecting to remote") Log.info("connecting to remote")
@@ -54,22 +56,26 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
} }
Log.info("connected") Log.info("connected")
thread { val sourceThread = thread {
socketDataSource.receive() socketDataSource.receive()
socket.close() socket.close()
sender.abort() ioClient.abort()
} }
Log.info("Startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
Log.info("Starting to send")
try { try {
sender.run() ioClient.run()
} catch (error: IOException) { } catch (error: IOException) {
Log.info("run ended abruptly") Log.info("run ended abruptly")
} }
Log.info("waiting for source thread to finish")
sourceThread.join()
cleanup() cleanup()
} }
if (blocking) {
clientThread.join()
}
} }
} }

View File

@@ -21,8 +21,12 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.socket-server") private val Log = Logger.getLogger("btbench.socket-server")
class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) { class SocketServer(
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) { private val viewModel: AppViewModel,
private val serverSocket: BluetoothServerSocket,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) {
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit, blocking: Boolean = false) {
var aborted = false var aborted = false
viewModel.running = true viewModel.running = true
@@ -31,7 +35,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
viewModel.running = false viewModel.running = false
} }
thread(name = "SocketServer") { val serverThread = thread(name = "SocketServer") {
while (!aborted) { while (!aborted) {
viewModel.aborter = { viewModel.aborter = {
serverSocket.close() serverSocket.close()
@@ -46,6 +50,8 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
return@thread return@thread
} }
Log.info("got connection from ${socket.remoteDevice.address}") Log.info("got connection from ${socket.remoteDevice.address}")
Log.info("maxReceivePacketSize=${socket.maxReceivePacketSize}")
Log.info("maxTransmitPacketSize=${socket.maxTransmitPacketSize}")
onConnected() onConnected()
viewModel.aborter = { viewModel.aborter = {
@@ -57,11 +63,15 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
val socketDataSink = SocketDataSink(socket) val socketDataSink = SocketDataSink(socket)
val streamIO = StreamedPacketIO(socketDataSink) val streamIO = StreamedPacketIO(socketDataSink)
val socketDataSource = SocketDataSource(socket, streamIO::onData) val socketDataSource = SocketDataSource(socket, streamIO::onData)
val receiver = Receiver(viewModel, streamIO) val ioClient = createIoClient(streamIO)
socketDataSource.receive() socketDataSource.receive()
socket.close() socket.close()
} }
cleanup() cleanup()
} }
if (blocking) {
serverThread.join()
}
} }
} }

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.2.0" agp = "8.4.0"
kotlin = "1.9.0" kotlin = "1.9.0"
core-ktx = "1.12.0" core-ktx = "1.12.0"
junit = "4.13.2" junit = "4.13.2"
@@ -8,6 +8,8 @@ espresso-core = "3.5.1"
lifecycle-runtime-ktx = "2.6.2" lifecycle-runtime-ktx = "2.6.2"
activity-compose = "1.7.2" activity-compose = "1.7.2"
compose-bom = "2023.08.00" compose-bom = "2023.08.00"
mobly-snippet = "1.4.0"
core = "1.6.1"
[libraries] [libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
@@ -24,6 +26,8 @@ ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
material3 = { group = "androidx.compose.material3", name = "material3" } material3 = { group = "androidx.compose.material3", name = "material3" }
mobly-snippet = { group = "com.google.android.mobly", name = "mobly-snippet-lib", version.ref = "mobly.snippet" }
androidx-core = { group = "androidx.test", name = "core", version.ref = "core" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }

View File

@@ -1,6 +1,6 @@
#Wed Oct 25 07:40:52 PDT 2023 #Wed Oct 25 07:40:52 PDT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists