mirror of
https://github.com/google/bumble.git
synced 2026-04-18 00:45:32 +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 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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_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() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user