This commit is contained in:
Gilles Boccon-Gibod
2024-09-27 12:16:03 -07:00
parent c91695c23a
commit fe429cb2eb
14 changed files with 276 additions and 63 deletions

View File

@@ -18,18 +18,58 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.rpc.Rpc;
import androidx.test.core.app.ApplicationProvider;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.UUID;
class Runner {
public UUID mId;
private final Mode mMode;
private final String mModeName;
private final String mScenario;
private final AppViewModel mModel;
Runner(Mode mode, String modeName, String scenario, AppViewModel model) {
this.mId = UUID.randomUUID();
this.mMode = mode;
this.mModeName = modeName;
this.mScenario = scenario;
this.mModel = model;
}
public JSONObject toJson() throws JSONException {
JSONObject result = new JSONObject();
result.put("id", mId.toString());
result.put("mode", mModeName);
result.put("scenario", mScenario);
result.put("model", AutomationSnippet.modelToJson(mModel));
return result;
}
public void stop() {
mModel.abort();
}
public void waitForCompletion() {
mMode.waitForCompletion();
}
}
public class AutomationSnippet implements Snippet {
private static final String TAG = "btbench.snippet";
private final BluetoothAdapter mBluetoothAdapter;
private final Context mContext;
private final ArrayList<Runner> mRunners = new ArrayList<>();
public AutomationSnippet() {
mContext = ApplicationProvider.getApplicationContext();
@@ -40,30 +80,41 @@ public class AutomationSnippet implements Snippet {
}
}
private void runScenario(AppViewModel model, String mode, String scenario) {
Mode runner;
private Runner runScenario(AppViewModel model, String mode, String scenario) {
Mode runnable;
switch (mode) {
case "rfcomm-client":
runner = new RfcommClient(model, mBluetoothAdapter, (PacketIO packetIO) -> createIoClient(model, scenario, packetIO));
runnable = new RfcommClient(model, mBluetoothAdapter,
(PacketIO packetIO) -> createIoClient(model, scenario,
packetIO));
break;
case "rfcomm-server":
runner = new RfcommServer(model, mBluetoothAdapter, (PacketIO packetIO) -> createIoClient(model, scenario, packetIO));
runnable = new RfcommServer(model, mBluetoothAdapter,
(PacketIO packetIO) -> createIoClient(model, scenario,
packetIO));
break;
case "l2cap-client":
runner = new L2capClient(model, mBluetoothAdapter, mContext, (PacketIO packetIO) -> createIoClient(model, scenario, packetIO));
runnable = new L2capClient(model, mBluetoothAdapter, mContext,
(PacketIO packetIO) -> createIoClient(model, scenario,
packetIO));
break;
case "l2cap-server":
runner = new L2capServer(model, mBluetoothAdapter, (PacketIO packetIO) -> createIoClient(model, scenario, packetIO));
runnable = new L2capServer(model, mBluetoothAdapter,
(PacketIO packetIO) -> createIoClient(model, scenario,
packetIO));
break;
default:
return;
return null;
}
runner.run(true);
runnable.run();
Runner runner = new Runner(runnable, mode, scenario, model);
mRunners.add(runner);
return runner;
}
private IoClient createIoClient(AppViewModel model, String scenario, PacketIO packetIO) {
@@ -85,35 +136,83 @@ public class AutomationSnippet implements Snippet {
}
}
private static JSONObject resultFromModel(AppViewModel model) throws JSONException {
public static JSONObject modelToJson(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());
JSONObject rttStats = new JSONObject();
stats.put("rtt", rttStats);
rttStats.put("compound", model.getStats());
result.put("status", model.getStatus());
result.put("running", model.getRunning());
result.put("l2cap_psm", model.getL2capPsm());
if (model.getStatus().equals("OK")) {
JSONObject stats = new JSONObject();
result.put("stats", stats);
stats.put("throughput", model.getThroughput());
JSONObject rttStats = new JSONObject();
stats.put("rtt", rttStats);
rttStats.put("compound", model.getStats());
} else {
result.put("last_error", model.getLastError());
}
return result;
}
private Runner findRunner(String runnerId) {
for (Runner runner : mRunners) {
if (runner.mId.toString().equals(runnerId)) {
return runner;
}
}
return null;
}
@Rpc(description = "Run a scenario in RFComm Client mode")
public JSONObject runRfcommClient(String scenario, String peerBluetoothAddress, int packetCount, int packetSize, int packetInterval) throws JSONException {
public JSONObject runRfcommClient(String scenario, String peerBluetoothAddress, int packetCount,
int packetSize, int packetInterval) throws JSONException {
assert (mBluetoothAdapter != null);
// We only support "send" and "ping" for this mode for now
if (!(scenario.equals("send") || scenario.equals("ping"))) {
throw new InvalidParameterException("only 'send' and 'ping' are supported for this mode");
}
AppViewModel model = new AppViewModel();
model.setPeerBluetoothAddress(peerBluetoothAddress);
model.setSenderPacketCount(packetCount);
model.setSenderPacketSize(packetSize);
model.setSenderPacketInterval(packetInterval);
runScenario(model, "rfcomm-client", scenario);
return resultFromModel(model);
Runner runner = runScenario(model, "rfcomm-client", scenario);
assert runner != null;
return runner.toJson();
}
@Rpc(description = "Run a scenario in RFComm Server mode")
public JSONObject runRfcommServer(String scenario) throws JSONException {
assert (mBluetoothAdapter != null);
// We only support "receive" and "pong" for this mode for now
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
throw new InvalidParameterException("only 'receive' and 'pong' are supported for this mode");
}
AppViewModel model = new AppViewModel();
Runner runner = runScenario(model, "rfcomm-server", scenario);
assert runner != null;
return runner.toJson();
}
@Rpc(description = "Run a scenario in L2CAP Client mode")
public JSONObject runL2capClient(String scenario, String peerBluetoothAddress, int psm, boolean use_2m_phy, int packetCount, int packetSize, int packetInterval) throws JSONException {
public JSONObject runL2capClient(String scenario, String peerBluetoothAddress, int psm,
boolean use_2m_phy, int packetCount, int packetSize,
int packetInterval) throws JSONException {
assert (mBluetoothAdapter != null);
// We only support "send" and "ping" for this mode for now
if (!(scenario.equals("send") || scenario.equals("ping"))) {
throw new InvalidParameterException("only 'send' and 'ping' are supported for this mode");
}
AppViewModel model = new AppViewModel();
model.setPeerBluetoothAddress(peerBluetoothAddress);
model.setL2capPsm(psm);
@@ -122,8 +221,66 @@ public class AutomationSnippet implements Snippet {
model.setSenderPacketSize(packetSize);
model.setSenderPacketInterval(packetInterval);
runScenario(model, "l2cap-client", scenario);
return resultFromModel(model);
Runner runner = runScenario(model, "l2cap-client", scenario);
assert runner != null;
return runner.toJson();
}
@Rpc(description = "Run a scenario in L2CAP Server mode")
public JSONObject runL2capServer(String scenario) throws JSONException {
assert (mBluetoothAdapter != null);
// We only support "receive" and "pong" for this mode for now
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
throw new InvalidParameterException("only 'receive' and 'pong' are supported for this mode");
}
AppViewModel model = new AppViewModel();
Runner runner = runScenario(model, "l2cap-server", scenario);
assert runner != null;
return runner.toJson();
}
@Rpc(description = "Stop a Runner")
public JSONObject stopRunner(String runnerId) throws JSONException {
Runner runner = findRunner(runnerId);
if (runner == null) {
return new JSONObject();
}
runner.stop();
return runner.toJson();
}
@Rpc(description = "Wait for a Runner to complete")
public JSONObject waitForRunnerCompletion(String runnerId) throws JSONException {
Runner runner = findRunner(runnerId);
if (runner == null) {
return new JSONObject();
}
runner.waitForCompletion();
return runner.toJson();
}
@Rpc(description = "Get a Runner by ID")
public JSONObject getRunner(String runnerId) throws JSONException {
Runner runner = findRunner(runnerId);
if (runner == null) {
return new JSONObject();
}
return runner.toJson();
}
@Rpc(description = "Get all Runners")
public JSONObject getRunners() throws JSONException {
JSONObject result = new JSONObject();
JSONArray runners = new JSONArray();
result.put("runners", runners);
for (Runner runner: mRunners) {
runners.put(runner.toJson());
}
return result;
}
@Override

View File

@@ -32,8 +32,10 @@ class L2capClient(
private val context: Context,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
private var socketClient: SocketClient? = null
@SuppressLint("MissingPermission")
override fun run(blocking: Boolean) {
override fun run() {
viewModel.running = true
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
val address = viewModel.peerBluetoothAddress.take(17)
@@ -97,7 +99,11 @@ class L2capClient(
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
val client = SocketClient(viewModel, socket, createIoClient)
client.run(blocking)
socketClient = SocketClient(viewModel, socket, createIoClient)
socketClient!!.run()
}
override fun waitForCompletion() {
socketClient?.waitForCompletion()
}
}

View File

@@ -32,8 +32,10 @@ class L2capServer(
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
private var socketServer: SocketServer? = null
@SuppressLint("MissingPermission")
override fun run(blocking: Boolean) {
override fun run() {
// Advertise so that the peer can find us and connect.
val callback = object : AdvertiseCallback() {
override fun onStartFailure(errorCode: Int) {
@@ -59,11 +61,14 @@ class L2capServer(
viewModel.l2capPsm = serverSocket.psm
Log.info("psm = $serverSocket.psm")
val server = SocketServer(viewModel, serverSocket, createIoClient)
server.run(
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
socketServer!!.run(
{ advertiser.stopAdvertising(callback) },
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) },
blocking
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) }
)
}
override fun waitForCompletion() {
socketServer?.waitForCompletion()
}
}

View File

@@ -210,7 +210,7 @@ class MainActivity : ComponentActivity() {
L2CAP_SERVER_MODE -> L2capServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
else -> throw IllegalStateException()
}
runner.run(false)
runner.run()
}
private fun runScan(startScan: Boolean) {
@@ -443,6 +443,12 @@ fun MainView(
Text(
text = if (appViewModel.rxPhy != 0 || appViewModel.txPhy != 0) "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}" else ""
)
Text(
text = "Status: ${appViewModel.status}"
)
Text(
text = "Last Error: ${appViewModel.lastError}"
)
Text(
text = "Packets Sent: ${appViewModel.packetsSent}"
)

View File

@@ -15,5 +15,6 @@
package com.github.google.bumble.btbench
interface Mode {
fun run(blocking: Boolean)
fun run()
fun waitForCompletion()
}

View File

@@ -42,6 +42,8 @@ const val PONG_SCENARIO = "Pong"
class AppViewModel : ViewModel() {
private var preferences: SharedPreferences? = null
var status by mutableStateOf("")
var lastError by mutableStateOf("")
var mode by mutableStateOf(RFCOMM_SERVER_MODE)
var scenario by mutableStateOf(RECEIVE_SCENARIO)
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
@@ -218,6 +220,18 @@ class AppViewModel : ViewModel() {
}
}
fun clear() {
status = ""
lastError = ""
mtu = 0
rxPhy = 0
txPhy = 0
packetsSent = 0
packetsReceived = 0
throughput = 0
stats = ""
}
fun abort() {
aborter?.let { it() }
}

View File

@@ -34,9 +34,7 @@ class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO
}
override fun run() {
viewModel.packetsSent = 0
viewModel.packetsReceived = 0
viewModel.stats = ""
viewModel.clear()
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());

View File

@@ -28,7 +28,9 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
packetIO.packetSink = this
}
override fun run() {}
override fun run() {
viewModel.clear()
}
override fun abort() {}

View File

@@ -29,7 +29,9 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
packetIO.packetSink = this
}
override fun run() {}
override fun run() {
viewModel.clear()
}
override fun abort() {}

View File

@@ -25,15 +25,21 @@ class RfcommClient(
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
private var socketClient: SocketClient? = null
@SuppressLint("MissingPermission")
override fun run(blocking: Boolean) {
override fun run() {
val address = viewModel.peerBluetoothAddress.take(17)
val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
DEFAULT_RFCOMM_UUID
)
val client = SocketClient(viewModel, socket, createIoClient)
client.run(blocking)
socketClient = SocketClient(viewModel, socket, createIoClient)
socketClient!!.run()
}
override fun waitForCompletion() {
socketClient?.waitForCompletion()
}
}

View File

@@ -16,9 +16,7 @@ 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-server")
@@ -27,13 +25,18 @@ class RfcommServer(
private val bluetoothAdapter: BluetoothAdapter,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) : Mode {
private var socketServer: SocketServer? = null
@SuppressLint("MissingPermission")
override fun run(blocking: Boolean) {
override fun run() {
val serverSocket = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
"BumbleBench", DEFAULT_RFCOMM_UUID
)
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
socketServer!!.run({}, {})
}
val server = SocketServer(viewModel, serverSocket, createIoClient)
server.run({}, {}, blocking)
override fun waitForCompletion() {
socketServer?.waitForCompletion()
}
}

View File

@@ -34,10 +34,7 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
}
override fun run() {
viewModel.packetsSent = 0
viewModel.packetsReceived = 0
viewModel.throughput = 0
viewModel.stats = ""
viewModel.clear()
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());

View File

@@ -27,8 +27,10 @@ class SocketClient(
private val socket: BluetoothSocket,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) {
private var clientThread: Thread? = null
@SuppressLint("MissingPermission")
fun run(blocking: Boolean = false) {
fun run() {
viewModel.running = true
val socketDataSink = SocketDataSink(socket)
val streamIO = StreamedPacketIO(socketDataSink)
@@ -41,7 +43,7 @@ class SocketClient(
viewModel.running = false
}
val clientThread = thread(name = "SocketClient") {
clientThread = thread(name = "SocketClient") {
viewModel.aborter = {
ioClient.abort()
socket.close()
@@ -51,6 +53,8 @@ class SocketClient(
socket.connect()
} catch (error: IOException) {
Log.warning("connection failed")
viewModel.status = "ABORTED"
viewModel.lastError = "CONNECTION_FAILED"
cleanup()
return@thread
}
@@ -65,8 +69,11 @@ class SocketClient(
try {
ioClient.run()
socket.close()
viewModel.status = "OK"
} catch (error: IOException) {
Log.info("run ended abruptly")
viewModel.status = "ABORTED"
viewModel.lastError = "IO_ERROR"
}
Log.info("waiting for source thread to finish")
@@ -74,9 +81,9 @@ class SocketClient(
cleanup()
}
}
if (blocking) {
clientThread.join()
}
fun waitForCompletion() {
clientThread?.join()
}
}

View File

@@ -26,7 +26,9 @@ class SocketServer(
private val serverSocket: BluetoothServerSocket,
private val createIoClient: (packetIo: PacketIO) -> IoClient
) {
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit, blocking: Boolean = false) {
private var serverThread: Thread? = null
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) {
var aborted = false
viewModel.running = true
@@ -35,7 +37,7 @@ class SocketServer(
viewModel.running = false
}
val serverThread = thread(name = "SocketServer") {
serverThread = thread(name = "SocketServer") {
while (!aborted) {
viewModel.aborter = {
serverSocket.close()
@@ -63,15 +65,22 @@ class SocketServer(
val socketDataSink = SocketDataSink(socket)
val streamIO = StreamedPacketIO(socketDataSink)
val socketDataSource = SocketDataSource(socket, streamIO::onData)
val ioClient = createIoClient(streamIO)
val ioThread = thread(name = "IoClient") {
val ioClient = createIoClient(streamIO)
ioClient.run()
}
socketDataSource.receive()
socket.close()
ioThread.join()
}
cleanup()
}
if (blocking) {
serverThread.join()
}
}
fun waitForCompletion() {
serverThread?.join()
}
}