mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
wip
This commit is contained in:
165
apps/bench.py
165
apps/bench.py
@@ -19,6 +19,7 @@ import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import struct
|
||||
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_max = max(stats)
|
||||
stats_avg = sum(stats) / len(stats)
|
||||
stats_avg = statistics.mean(stats)
|
||||
stats_stdev = statistics.stdev(stats)
|
||||
logging.info(
|
||||
color(
|
||||
(
|
||||
f'### {title} stats: '
|
||||
f'min={stats_min:.2f}, '
|
||||
f'max={stats_max:.2f}, '
|
||||
f'average={stats_avg:.2f}'
|
||||
f'min={stats_min:.{precision}f}, '
|
||||
f'max={stats_max:.{precision}f}, '
|
||||
f'average={stats_avg:.{precision}f}, '
|
||||
f'stdev={stats_stdev:.{precision}f}'
|
||||
),
|
||||
'cyan',
|
||||
)
|
||||
@@ -448,9 +451,9 @@ class Ping:
|
||||
self.repeat_delay = repeat_delay
|
||||
self.pace = pace
|
||||
self.done = asyncio.Event()
|
||||
self.current_packet_index = 0
|
||||
self.ping_sent_time = 0.0
|
||||
self.latencies = []
|
||||
self.ping_times = []
|
||||
self.rtts = []
|
||||
self.next_expected_packet_index = 0
|
||||
self.min_stats = []
|
||||
self.max_stats = []
|
||||
self.avg_stats = []
|
||||
@@ -477,60 +480,57 @@ class Ping:
|
||||
logging.info(color('=== Sending RESET', 'magenta'))
|
||||
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
||||
|
||||
self.current_packet_index = 0
|
||||
self.latencies = []
|
||||
await self.send_next_ping()
|
||||
packet_interval = self.pace / 1000
|
||||
start_time = time.time()
|
||||
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()
|
||||
|
||||
min_latency = min(self.latencies)
|
||||
max_latency = max(self.latencies)
|
||||
avg_latency = sum(self.latencies) / len(self.latencies)
|
||||
min_rtt = min(self.rtts)
|
||||
max_rtt = max(self.rtts)
|
||||
avg_rtt = statistics.mean(self.rtts)
|
||||
stdev_rtt = statistics.stdev(self.rtts)
|
||||
logging.info(
|
||||
color(
|
||||
'@@@ Latencies: '
|
||||
f'min={min_latency:.2f}, '
|
||||
f'max={max_latency:.2f}, '
|
||||
f'average={avg_latency:.2f}'
|
||||
'@@@ RTTs: '
|
||||
f'min={min_rtt:.2f}, '
|
||||
f'max={max_rtt:.2f}, '
|
||||
f'average={avg_rtt:.2f}, '
|
||||
f'stdev={stdev_rtt:.2f}'
|
||||
)
|
||||
)
|
||||
|
||||
self.min_stats.append(min_latency)
|
||||
self.max_stats.append(max_latency)
|
||||
self.avg_stats.append(avg_latency)
|
||||
self.min_stats.append(min_rtt)
|
||||
self.max_stats.append(max_rtt)
|
||||
self.avg_stats.append(avg_rtt)
|
||||
|
||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||
logging.info(color(f'=== {run_counter} Done!', 'magenta'))
|
||||
|
||||
if self.repeat:
|
||||
log_stats('Min Latency', self.min_stats)
|
||||
log_stats('Max Latency', self.max_stats)
|
||||
log_stats('Average Latency', self.avg_stats)
|
||||
log_stats('Min RTT', self.min_stats)
|
||||
log_stats('Max RTT', self.max_stats)
|
||||
log_stats('Average RTT', self.avg_stats)
|
||||
|
||||
if self.repeat:
|
||||
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):
|
||||
elapsed = time.time() - self.ping_sent_time
|
||||
|
||||
try:
|
||||
packet_type, packet_data = parse_packet(packet)
|
||||
except ValueError:
|
||||
@@ -542,21 +542,23 @@ class Ping:
|
||||
return
|
||||
|
||||
if packet_type == PacketType.ACK:
|
||||
latency = elapsed * 1000
|
||||
self.latencies.append(latency)
|
||||
elapsed = time.time() - self.ping_times[packet_index]
|
||||
rtt = elapsed * 1000
|
||||
self.rtts.append(rtt)
|
||||
logging.info(
|
||||
color(
|
||||
f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms',
|
||||
f'<<< Received ACK [{packet_index}], RTT={rtt:.2f}ms',
|
||||
'green',
|
||||
)
|
||||
)
|
||||
|
||||
if packet_index == self.current_packet_index:
|
||||
self.current_packet_index += 1
|
||||
if packet_index == self.next_expected_packet_index:
|
||||
self.next_expected_packet_index += 1
|
||||
else:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.current_packet_index} '
|
||||
f'!!! Unexpected packet, '
|
||||
f'expected {self.next_expected_packet_index} '
|
||||
f'but received {packet_index}'
|
||||
)
|
||||
)
|
||||
@@ -565,8 +567,6 @@ class Ping:
|
||||
self.done.set()
|
||||
return
|
||||
|
||||
AsyncRunner.spawn(self.send_next_ping())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pong
|
||||
@@ -583,8 +583,11 @@ class Pong:
|
||||
|
||||
def reset(self):
|
||||
self.expected_packet_index = 0
|
||||
self.receive_times = []
|
||||
|
||||
def on_packet_received(self, packet):
|
||||
self.receive_times.append(time.time())
|
||||
|
||||
try:
|
||||
packet_type, packet_data = parse_packet(packet)
|
||||
except ValueError:
|
||||
@@ -599,10 +602,16 @@ class Pong:
|
||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
||||
except ValueError:
|
||||
return
|
||||
interval = (
|
||||
self.receive_times[-1] - self.receive_times[-2]
|
||||
if len(self.receive_times) >= 2
|
||||
else 0
|
||||
)
|
||||
logging.info(
|
||||
color(
|
||||
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',
|
||||
)
|
||||
)
|
||||
@@ -623,8 +632,35 @@ class Pong:
|
||||
)
|
||||
)
|
||||
|
||||
if packet_flags & PACKET_FLAG_LAST and not self.linger:
|
||||
self.done.set()
|
||||
if packet_flags & PACKET_FLAG_LAST:
|
||||
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):
|
||||
await self.done.wait()
|
||||
@@ -942,9 +978,12 @@ class RfcommClient(StreamedPacketIO):
|
||||
channel = await bumble.rfcomm.find_rfcomm_channel_with_uuid(
|
||||
connection, self.uuid
|
||||
)
|
||||
logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
|
||||
if channel == 0:
|
||||
logging.info(color('!!! No RFComm service with this UUID found', 'red'))
|
||||
if channel:
|
||||
logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
|
||||
else:
|
||||
logging.warning(
|
||||
color('!!! No RFComm service with this UUID found', 'red')
|
||||
)
|
||||
await connection.disconnect()
|
||||
return
|
||||
|
||||
@@ -1054,6 +1093,8 @@ class RfcommServer(StreamedPacketIO):
|
||||
if self.credits_threshold is not None:
|
||||
dlc.rx_credits_threshold = self.credits_threshold
|
||||
|
||||
self.ready.set()
|
||||
|
||||
async def drain(self):
|
||||
assert self.dlc
|
||||
await self.dlc.drain()
|
||||
@@ -1503,7 +1544,7 @@ def create_role_factory(ctx, default_role):
|
||||
'--rfcomm-channel',
|
||||
type=int,
|
||||
default=DEFAULT_RFCOMM_CHANNEL,
|
||||
help='RFComm channel to use',
|
||||
help='RFComm channel to use (specify 0 for channel discovery via SDP)',
|
||||
)
|
||||
@click.option(
|
||||
'--rfcomm-uuid',
|
||||
@@ -1743,7 +1784,11 @@ def peripheral(ctx, transport):
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ class ClientBridge:
|
||||
address: str,
|
||||
tcp_host: str,
|
||||
tcp_port: int,
|
||||
authenticate: bool,
|
||||
encrypt: bool,
|
||||
):
|
||||
self.channel = channel
|
||||
@@ -245,6 +246,7 @@ class ClientBridge:
|
||||
self.address = address
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_port = tcp_port
|
||||
self.authenticate = authenticate
|
||||
self.encrypt = encrypt
|
||||
self.device: Optional[Device] = None
|
||||
self.connection: Optional[Connection] = None
|
||||
@@ -274,6 +276,11 @@ class ClientBridge:
|
||||
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
|
||||
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:
|
||||
print(color("@@@ Encrypting Bluetooth connection", "blue"))
|
||||
await self.connection.encrypt()
|
||||
@@ -491,8 +498,9 @@ def server(context, tcp_host, tcp_port):
|
||||
@click.argument("bluetooth-address")
|
||||
@click.option("--tcp-host", help="TCP host", default="_")
|
||||
@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")
|
||||
def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
|
||||
def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt):
|
||||
bridge = ClientBridge(
|
||||
context.obj["channel"],
|
||||
context.obj["uuid"],
|
||||
@@ -500,6 +508,7 @@ def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
|
||||
bluetooth_address,
|
||||
tcp_host,
|
||||
tcp_port,
|
||||
authenticate,
|
||||
encrypt,
|
||||
)
|
||||
asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge))
|
||||
|
||||
@@ -60,7 +60,8 @@ dependencies {
|
||||
implementation(libs.ui.graphics)
|
||||
implementation(libs.ui.tooling.preview)
|
||||
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)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -29,10 +29,11 @@ private val Log = Logger.getLogger("btbench.l2cap-client")
|
||||
class L2capClient(
|
||||
private val viewModel: AppViewModel,
|
||||
private val bluetoothAdapter: BluetoothAdapter,
|
||||
private val context: Context
|
||||
) {
|
||||
private val context: Context,
|
||||
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||
) : Mode {
|
||||
@SuppressLint("MissingPermission")
|
||||
fun run() {
|
||||
override fun run(blocking: Boolean) {
|
||||
viewModel.running = true
|
||||
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
|
||||
val address = viewModel.peerBluetoothAddress.take(17)
|
||||
@@ -75,6 +76,7 @@ class L2capClient(
|
||||
) {
|
||||
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
if (viewModel.use2mPhy) {
|
||||
Log.info("requesting 2M PHY")
|
||||
gatt.setPreferredPhy(
|
||||
BluetoothDevice.PHY_LE_2M_MASK,
|
||||
BluetoothDevice.PHY_LE_2M_MASK,
|
||||
@@ -95,7 +97,7 @@ class L2capClient(
|
||||
|
||||
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
|
||||
|
||||
val client = SocketClient(viewModel, socket)
|
||||
client.run()
|
||||
val client = SocketClient(viewModel, socket, createIoClient)
|
||||
client.run(blocking)
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,15 @@ import kotlin.concurrent.thread
|
||||
|
||||
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")
|
||||
fun run() {
|
||||
override fun run(blocking: Boolean) {
|
||||
// Advertise so that the peer can find us and connect.
|
||||
val callback = object: AdvertiseCallback() {
|
||||
val callback = object : AdvertiseCallback() {
|
||||
override fun onStartFailure(errorCode: Int) {
|
||||
Log.warning("failed to start advertising: $errorCode")
|
||||
}
|
||||
@@ -55,7 +59,11 @@ class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdap
|
||||
viewModel.l2capPsm = serverSocket.psm
|
||||
Log.info("psm = $serverSocket.psm")
|
||||
|
||||
val server = SocketServer(viewModel, serverSocket)
|
||||
server.run({ advertiser.stopAdvertising(callback) }, { advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) })
|
||||
val server = SocketServer(viewModel, serverSocket, createIoClient)
|
||||
server.run(
|
||||
{ advertiser.stopAdvertising(callback) },
|
||||
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) },
|
||||
blocking
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,15 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
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.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
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 SENDER_PACKET_COUNT_PREF_KEY = "sender_packet_count"
|
||||
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() {
|
||||
private val appViewModel = AppViewModel()
|
||||
@@ -139,10 +146,7 @@ class MainActivity : ComponentActivity() {
|
||||
MainView(
|
||||
appViewModel,
|
||||
::becomeDiscoverable,
|
||||
::runRfcommClient,
|
||||
::runRfcommServer,
|
||||
::runL2capClient,
|
||||
::runL2capServer,
|
||||
::runScenario
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,6 +163,10 @@ class MainActivity : ComponentActivity() {
|
||||
if (packetSize > 0) {
|
||||
appViewModel.senderPacketSize = packetSize
|
||||
}
|
||||
val packetInterval = intent.getIntExtra("packet-interval", 0)
|
||||
if (packetInterval > 0) {
|
||||
appViewModel.senderPacketInterval = packetInterval
|
||||
}
|
||||
appViewModel.updateSenderPacketSizeSlider()
|
||||
intent.getStringExtra("autostart")?.let {
|
||||
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() {
|
||||
val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) }
|
||||
rfcommClient?.run()
|
||||
// val rfcommClient = bluetoothAdapter?.let { RfcommClient(appViewModel, it) }
|
||||
// rfcommClient?.run()
|
||||
}
|
||||
|
||||
private fun runRfcommServer() {
|
||||
val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) }
|
||||
rfcommServer?.run()
|
||||
// val rfcommServer = bluetoothAdapter?.let { RfcommServer(appViewModel, it) }
|
||||
// rfcommServer?.run()
|
||||
}
|
||||
|
||||
private fun runL2capClient() {
|
||||
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) }
|
||||
l2capClient?.run()
|
||||
// val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) }
|
||||
// l2capClient?.run()
|
||||
}
|
||||
|
||||
private fun runL2capServer() {
|
||||
val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) }
|
||||
l2capServer?.run()
|
||||
// val l2capServer = bluetoothAdapter?.let { L2capServer(appViewModel, it) }
|
||||
// l2capServer?.run()
|
||||
}
|
||||
|
||||
private fun runScan(startScan: Boolean) {
|
||||
@@ -210,10 +248,7 @@ class MainActivity : ComponentActivity() {
|
||||
fun MainView(
|
||||
appViewModel: AppViewModel,
|
||||
becomeDiscoverable: () -> Unit,
|
||||
runRfcommClient: () -> Unit,
|
||||
runRfcommServer: () -> Unit,
|
||||
runL2capClient: () -> Unit,
|
||||
runL2capServer: () -> Unit,
|
||||
runScenario: () -> Unit,
|
||||
) {
|
||||
BTBenchTheme {
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -239,7 +274,9 @@ fun MainView(
|
||||
Text(text = "Peer Bluetooth Address")
|
||||
},
|
||||
value = appViewModel.peerBluetoothAddress,
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
|
||||
),
|
||||
@@ -249,14 +286,18 @@ fun MainView(
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
}),
|
||||
enabled = (appViewModel.mode == RFCOMM_CLIENT_MODE) or (appViewModel.mode == L2CAP_CLIENT_MODE)
|
||||
)
|
||||
Divider()
|
||||
TextField(label = {
|
||||
Text(text = "L2CAP PSM")
|
||||
},
|
||||
TextField(
|
||||
label = {
|
||||
Text(text = "L2CAP PSM")
|
||||
},
|
||||
value = appViewModel.l2capPsm.toString(),
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
|
||||
),
|
||||
@@ -271,7 +312,8 @@ fun MainView(
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
}),
|
||||
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE)
|
||||
)
|
||||
Divider()
|
||||
Slider(
|
||||
@@ -290,6 +332,32 @@ fun MainView(
|
||||
)
|
||||
Text(text = "Packet Size: " + appViewModel.senderPacketSize.toString())
|
||||
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(
|
||||
text = "Become Discoverable", onClick = becomeDiscoverable, true
|
||||
)
|
||||
@@ -300,25 +368,78 @@ fun MainView(
|
||||
Text(text = "2M PHY")
|
||||
Spacer(modifier = Modifier.padding(start = 8.dp))
|
||||
Switch(
|
||||
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == L2CAP_SERVER_MODE),
|
||||
checked = appViewModel.use2mPhy,
|
||||
onCheckedChange = { appViewModel.use2mPhy = it }
|
||||
)
|
||||
|
||||
}
|
||||
Row {
|
||||
ActionButton(
|
||||
text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running
|
||||
)
|
||||
ActionButton(
|
||||
text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running
|
||||
)
|
||||
Column(Modifier.selectableGroup()) {
|
||||
listOf(
|
||||
RFCOMM_CLIENT_MODE,
|
||||
RFCOMM_SERVER_MODE,
|
||||
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 {
|
||||
ActionButton(
|
||||
text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running
|
||||
text = "Start", onClick = runScenario, enabled = !appViewModel.running
|
||||
)
|
||||
ActionButton(
|
||||
text = "L2CAP Server", onClick = runL2capServer, !appViewModel.running
|
||||
text = "Stop", onClick = appViewModel::abort, enabled = appViewModel.running
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
@@ -337,9 +458,8 @@ fun MainView(
|
||||
Text(
|
||||
text = "Throughput: ${appViewModel.throughput}"
|
||||
)
|
||||
Divider()
|
||||
ActionButton(
|
||||
text = "Abort", onClick = appViewModel::abort, appViewModel.running
|
||||
Text(
|
||||
text = "Stats: ${appViewModel.stats}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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_SENDER_PACKET_COUNT = 100
|
||||
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
||||
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
|
||||
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() {
|
||||
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 l2capPsm by mutableIntStateOf(DEFAULT_PSM)
|
||||
var use2mPhy by mutableStateOf(true)
|
||||
@@ -41,9 +54,11 @@ class AppViewModel : ViewModel() {
|
||||
var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
|
||||
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
|
||||
var senderPacketSize by mutableIntStateOf(DEFAULT_SENDER_PACKET_SIZE)
|
||||
var senderPacketInterval by mutableIntStateOf(DEFAULT_SENDER_PACKET_INTERVAL)
|
||||
var packetsSent by mutableIntStateOf(0)
|
||||
var packetsReceived by mutableIntStateOf(0)
|
||||
var throughput by mutableIntStateOf(0)
|
||||
var stats by mutableStateOf("")
|
||||
var running by mutableStateOf(false)
|
||||
var aborter: (() -> Unit)? = null
|
||||
|
||||
@@ -66,6 +81,21 @@ class AppViewModel : ViewModel() {
|
||||
senderPacketSize = savedSenderPacketSize
|
||||
}
|
||||
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) {
|
||||
@@ -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() {
|
||||
aborter?.let { it() }
|
||||
}
|
||||
|
||||
@@ -74,13 +74,13 @@ abstract class PacketSink {
|
||||
fun onPacket(packet: Packet) {
|
||||
when (packet) {
|
||||
is ResetPacket -> onResetPacket()
|
||||
is AckPacket -> onAckPacket()
|
||||
is AckPacket -> onAckPacket(packet)
|
||||
is SequencePacket -> onSequencePacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onResetPacket()
|
||||
abstract fun onAckPacket()
|
||||
abstract fun onAckPacket(packet: AckPacket)
|
||||
abstract fun onSequencePacket(packet: SequencePacket)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import kotlin.time.TimeSource
|
||||
|
||||
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 lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
||||
private var bytesReceived = 0
|
||||
@@ -29,6 +29,10 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
||||
packetIO.packetSink = this
|
||||
}
|
||||
|
||||
override fun run() {}
|
||||
|
||||
override fun abort() {}
|
||||
|
||||
override fun onResetPacket() {
|
||||
startTime = TimeSource.Monotonic.markNow()
|
||||
lastPacketTime = startTime
|
||||
@@ -36,9 +40,10 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
||||
viewModel.throughput = 0
|
||||
viewModel.packetsSent = 0
|
||||
viewModel.packetsReceived = 0
|
||||
viewModel.stats = ""
|
||||
}
|
||||
|
||||
override fun onAckPacket() {
|
||||
override fun onAckPacket(packet: AckPacket) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -16,22 +16,24 @@ package com.github.google.bumble.btbench
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import java.io.IOException
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
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")
|
||||
fun run() {
|
||||
override fun run(blocking: Boolean) {
|
||||
val address = viewModel.peerBluetoothAddress.take(17)
|
||||
val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
|
||||
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
|
||||
DEFAULT_RFCOMM_UUID
|
||||
)
|
||||
|
||||
val client = SocketClient(viewModel, socket)
|
||||
client.run()
|
||||
val client = SocketClient(viewModel, socket, createIoClient)
|
||||
client.run(blocking)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,18 @@ import kotlin.concurrent.thread
|
||||
|
||||
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")
|
||||
fun run() {
|
||||
override fun run(blocking: Boolean) {
|
||||
val serverSocket = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
"BumbleBench", DEFAULT_RFCOMM_UUID
|
||||
)
|
||||
|
||||
val server = SocketServer(viewModel, serverSocket)
|
||||
server.run({}, {})
|
||||
val server = SocketServer(viewModel, serverSocket, createIoClient)
|
||||
server.run({}, {}, blocking)
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,12 @@ import java.util.logging.Logger
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
private const val DEFAULT_STARTUP_DELAY = 3000
|
||||
|
||||
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 bytesSent = 0
|
||||
private val done = Semaphore(0)
|
||||
@@ -30,10 +33,15 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
||||
packetIO.packetSink = this
|
||||
}
|
||||
|
||||
fun run() {
|
||||
override fun run() {
|
||||
viewModel.packetsSent = 0
|
||||
viewModel.packetsReceived = 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")
|
||||
packetIO.sendPacket(ResetPacket())
|
||||
@@ -63,14 +71,14 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
||||
Log.info("got ACK")
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
override fun abort() {
|
||||
done.release()
|
||||
}
|
||||
|
||||
override fun onResetPacket() {
|
||||
}
|
||||
|
||||
override fun onAckPacket() {
|
||||
override fun onAckPacket(packet: AckPacket) {
|
||||
Log.info("received ACK")
|
||||
val elapsed = TimeSource.Monotonic.markNow() - startTime
|
||||
val throughput = (bytesSent / elapsed.toDouble(DurationUnit.SECONDS)).toInt()
|
||||
|
||||
@@ -22,16 +22,18 @@ import kotlin.concurrent.thread
|
||||
|
||||
private val Log = Logger.getLogger("btbench.socket-client")
|
||||
|
||||
private const val DEFAULT_STARTUP_DELAY = 3000
|
||||
|
||||
class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) {
|
||||
class SocketClient(
|
||||
private val viewModel: AppViewModel,
|
||||
private val socket: BluetoothSocket,
|
||||
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||
) {
|
||||
@SuppressLint("MissingPermission")
|
||||
fun run() {
|
||||
fun run(blocking: Boolean = false) {
|
||||
viewModel.running = true
|
||||
val socketDataSink = SocketDataSink(socket)
|
||||
val streamIO = StreamedPacketIO(socketDataSink)
|
||||
val socketDataSource = SocketDataSource(socket, streamIO::onData)
|
||||
val sender = Sender(viewModel, streamIO)
|
||||
val ioClient = createIoClient(streamIO)
|
||||
|
||||
fun cleanup() {
|
||||
socket.close()
|
||||
@@ -39,9 +41,9 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
|
||||
viewModel.running = false
|
||||
}
|
||||
|
||||
thread(name = "SocketClient") {
|
||||
val clientThread = thread(name = "SocketClient") {
|
||||
viewModel.aborter = {
|
||||
sender.abort()
|
||||
ioClient.abort()
|
||||
socket.close()
|
||||
}
|
||||
Log.info("connecting to remote")
|
||||
@@ -54,22 +56,26 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
|
||||
}
|
||||
Log.info("connected")
|
||||
|
||||
thread {
|
||||
val sourceThread = thread {
|
||||
socketDataSource.receive()
|
||||
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 {
|
||||
sender.run()
|
||||
ioClient.run()
|
||||
} catch (error: IOException) {
|
||||
Log.info("run ended abruptly")
|
||||
}
|
||||
|
||||
Log.info("waiting for source thread to finish")
|
||||
sourceThread.join()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
if (blocking) {
|
||||
clientThread.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,12 @@ import kotlin.concurrent.thread
|
||||
|
||||
private val Log = Logger.getLogger("btbench.socket-server")
|
||||
|
||||
class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) {
|
||||
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) {
|
||||
class SocketServer(
|
||||
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
|
||||
viewModel.running = true
|
||||
|
||||
@@ -31,7 +35,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
|
||||
viewModel.running = false
|
||||
}
|
||||
|
||||
thread(name = "SocketServer") {
|
||||
val serverThread = thread(name = "SocketServer") {
|
||||
while (!aborted) {
|
||||
viewModel.aborter = {
|
||||
serverSocket.close()
|
||||
@@ -46,6 +50,8 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
|
||||
return@thread
|
||||
}
|
||||
Log.info("got connection from ${socket.remoteDevice.address}")
|
||||
Log.info("maxReceivePacketSize=${socket.maxReceivePacketSize}")
|
||||
Log.info("maxTransmitPacketSize=${socket.maxTransmitPacketSize}")
|
||||
onConnected()
|
||||
|
||||
viewModel.aborter = {
|
||||
@@ -57,11 +63,15 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
|
||||
val socketDataSink = SocketDataSink(socket)
|
||||
val streamIO = StreamedPacketIO(socketDataSink)
|
||||
val socketDataSource = SocketDataSource(socket, streamIO::onData)
|
||||
val receiver = Receiver(viewModel, streamIO)
|
||||
val ioClient = createIoClient(streamIO)
|
||||
socketDataSource.receive()
|
||||
socket.close()
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
if (blocking) {
|
||||
serverThread.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.2.0"
|
||||
agp = "8.4.0"
|
||||
kotlin = "1.9.0"
|
||||
core-ktx = "1.12.0"
|
||||
junit = "4.13.2"
|
||||
@@ -8,6 +8,8 @@ espresso-core = "3.5.1"
|
||||
lifecycle-runtime-ktx = "2.6.2"
|
||||
activity-compose = "1.7.2"
|
||||
compose-bom = "2023.08.00"
|
||||
mobly-snippet = "1.4.0"
|
||||
core = "1.6.1"
|
||||
|
||||
[libraries]
|
||||
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-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
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]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#Wed Oct 25 07:40:52 PDT 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user