diff --git a/examples/mobly/bench/one_device_bench_test.py b/examples/mobly/bench/one_device_bench_test.py index 6fbc101..adec626 100644 --- a/examples/mobly/bench/one_device_bench_test.py +++ b/examples/mobly/bench/one_device_bench_test.py @@ -28,7 +28,7 @@ class OneDeviceBenchTest(base_test.BaseTestClass): def test_l2cap_client_ping(self): runner = self.dut.bench.runL2capClient( - "ping", "4B:2A:67:76:2B:E3", 128, True, 100, 970, 100 + "ping", "4B:2A:67:76:2B:E3", 128, True, 100, 970, 100, "HIGH" ) print("### Initial status:", runner) final_status = self.dut.bench.waitForRunnerCompletion(runner["id"]) @@ -36,7 +36,15 @@ class OneDeviceBenchTest(base_test.BaseTestClass): def test_l2cap_client_send(self): runner = self.dut.bench.runL2capClient( - "send", "7E:90:D0:F2:7A:11", 131, True, 100, 970, 0 + "send", + "F1:F1:F1:F1:F1:F1", + 128, + True, + 100, + 970, + 0, + "HIGH", + 10000, ) print("### Initial status:", runner) final_status = self.dut.bench.waitForRunnerCompletion(runner["id"]) diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/AutomationSnippet.java b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/AutomationSnippet.java index 3a7afa4..6cdbc5f 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/AutomationSnippet.java +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/AutomationSnippet.java @@ -22,11 +22,13 @@ import androidx.test.core.app.ApplicationProvider; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcOptional; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.UUID; @@ -71,12 +73,15 @@ public class AutomationSnippet implements Snippet { private final Context mContext; private final ArrayList mRunners = new ArrayList<>(); - public AutomationSnippet() { + public AutomationSnippet() throws IOException { mContext = ApplicationProvider.getApplicationContext(); BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class); mBluetoothAdapter = bluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { - throw new RuntimeException("bluetooth not supported"); + throw new IOException("bluetooth not supported"); + } + if (!mBluetoothAdapter.isEnabled()) { + throw new IOException("bluetooth not enabled"); } } @@ -85,32 +90,34 @@ public class AutomationSnippet implements Snippet { switch (mode) { case "rfcomm-client": runnable = new RfcommClient(model, mBluetoothAdapter, - (PacketIO packetIO) -> createIoClient(model, scenario, - packetIO)); + (PacketIO packetIO) -> createIoClient(model, scenario, + packetIO)); break; case "rfcomm-server": runnable = new RfcommServer(model, mBluetoothAdapter, - (PacketIO packetIO) -> createIoClient(model, scenario, - packetIO)); + (PacketIO packetIO) -> createIoClient(model, scenario, + packetIO)); break; case "l2cap-client": runnable = new L2capClient(model, mBluetoothAdapter, mContext, - (PacketIO packetIO) -> createIoClient(model, scenario, - packetIO)); + (PacketIO packetIO) -> createIoClient(model, scenario, + packetIO)); break; case "l2cap-server": runnable = new L2capServer(model, mBluetoothAdapter, - (PacketIO packetIO) -> createIoClient(model, scenario, - packetIO)); + (PacketIO packetIO) -> createIoClient(model, scenario, + packetIO)); break; default: return null; } + model.setMode(mode); + model.setScenario(scenario); runnable.run(); Runner runner = new Runner(runnable, mode, scenario, model); mRunners.add(runner); @@ -140,7 +147,21 @@ public class AutomationSnippet implements Snippet { JSONObject result = new JSONObject(); result.put("status", model.getStatus()); result.put("running", model.getRunning()); + result.put("peer_bluetooth_address", model.getPeerBluetoothAddress()); + result.put("mode", model.getMode()); + result.put("scenario", model.getScenario()); + result.put("sender_packet_size", model.getSenderPacketSize()); + result.put("sender_packet_count", model.getSenderPacketCount()); + result.put("sender_packet_interval", model.getSenderPacketInterval()); + result.put("packets_sent", model.getPacketsSent()); + result.put("packets_received", model.getPacketsReceived()); result.put("l2cap_psm", model.getL2capPsm()); + result.put("use_2m_phy", model.getUse2mPhy()); + result.put("connection_priority", model.getConnectionPriority()); + result.put("mtu", model.getMtu()); + result.put("rx_phy", model.getRxPhy()); + result.put("tx_phy", model.getTxPhy()); + result.put("startup_delay", model.getStartupDelay()); if (model.getStatus().equals("OK")) { JSONObject stats = new JSONObject(); result.put("stats", stats); @@ -167,12 +188,12 @@ public class AutomationSnippet implements Snippet { @Rpc(description = "Run a scenario in RFComm Client mode") public JSONObject runRfcommClient(String scenario, String peerBluetoothAddress, int packetCount, - int packetSize, int packetInterval) throws JSONException { - assert (mBluetoothAdapter != null); - + int packetSize, int packetInterval, + @RpcOptional Integer startupDelay) throws JSONException { // 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"); + throw new InvalidParameterException( + "only 'send' and 'ping' are supported for this mode"); } AppViewModel model = new AppViewModel(); @@ -180,6 +201,9 @@ public class AutomationSnippet implements Snippet { model.setSenderPacketCount(packetCount); model.setSenderPacketSize(packetSize); model.setSenderPacketInterval(packetInterval); + if (startupDelay != null) { + model.setStartupDelay(startupDelay); + } Runner runner = runScenario(model, "rfcomm-client", scenario); assert runner != null; @@ -187,15 +211,18 @@ public class AutomationSnippet implements Snippet { } @Rpc(description = "Run a scenario in RFComm Server mode") - public JSONObject runRfcommServer(String scenario) throws JSONException { - assert (mBluetoothAdapter != null); - + public JSONObject runRfcommServer(String scenario, + @RpcOptional Integer startupDelay) throws JSONException { // 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"); + throw new InvalidParameterException( + "only 'receive' and 'pong' are supported for this mode"); } AppViewModel model = new AppViewModel(); + if (startupDelay != null) { + model.setStartupDelay(startupDelay); + } Runner runner = runScenario(model, "rfcomm-server", scenario); assert runner != null; @@ -205,12 +232,12 @@ public class AutomationSnippet implements Snippet { @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 { - assert (mBluetoothAdapter != null); - + int packetInterval, @RpcOptional String connectionPriority, + @RpcOptional Integer startupDelay) throws JSONException { // 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"); + throw new InvalidParameterException( + "only 'send' and 'ping' are supported for this mode"); } AppViewModel model = new AppViewModel(); @@ -220,22 +247,30 @@ public class AutomationSnippet implements Snippet { model.setSenderPacketCount(packetCount); model.setSenderPacketSize(packetSize); model.setSenderPacketInterval(packetInterval); - + if (connectionPriority != null) { + model.setConnectionPriority(connectionPriority); + } + if (startupDelay != null) { + model.setStartupDelay(startupDelay); + } 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); - + public JSONObject runL2capServer(String scenario, + @RpcOptional Integer startupDelay) throws JSONException { // 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"); + throw new InvalidParameterException( + "only 'receive' and 'pong' are supported for this mode"); } AppViewModel model = new AppViewModel(); + if (startupDelay != null) { + model.setStartupDelay(startupDelay); + } Runner runner = runScenario(model, "l2cap-server", scenario); assert runner != null; @@ -276,7 +311,7 @@ public class AutomationSnippet implements Snippet { JSONObject result = new JSONObject(); JSONArray runners = new JSONArray(); result.put("runners", runners); - for (Runner runner: mRunners) { + for (Runner runner : mRunners) { runners.put(runner.toJson()); } diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt index a6567a6..5a4cc3c 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt @@ -90,6 +90,18 @@ class L2capClient( // Request an MTU update, even though we don't use GATT, because Android // won't request a larger link layer maximum data length otherwise. gatt.requestMtu(517) + + // Request a specific connection priority + val connectionPriority = when (viewModel.connectionPriority) { + "BALANCED" -> BluetoothGatt.CONNECTION_PRIORITY_BALANCED + "LOW_POWER" -> BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER + "HIGH" -> BluetoothGatt.CONNECTION_PRIORITY_HIGH + "DCK" -> BluetoothGatt.CONNECTION_PRIORITY_DCK + else -> 0 + } + if (!gatt.requestConnectionPriority(connectionPriority)) { + Log.warning("requestConnectionPriority failed") + } } } }, diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt index 02729eb..df5c53c 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/MainActivity.kt @@ -66,6 +66,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import com.github.google.bumble.btbench.ui.theme.BTBenchTheme +import java.io.IOException import java.util.logging.Logger private val Log = Logger.getLogger("bumble.main-activity") @@ -76,6 +77,7 @@ 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" +const val CONNECTION_PRIORITY_PREF_KEY = "connection_priority" class MainActivity : ComponentActivity() { private val appViewModel = AppViewModel() @@ -195,7 +197,7 @@ class MainActivity : ComponentActivity() { private fun runScenario() { if (bluetoothAdapter == null) { - return + throw IOException("bluetooth not enabled") } val runner = when (appViewModel.mode) { @@ -366,7 +368,35 @@ fun MainView( checked = appViewModel.use2mPhy, onCheckedChange = { appViewModel.use2mPhy = it } ) - + Column(Modifier.selectableGroup()) { + listOf( + "BALANCED", + "LOW", + "HIGH", + "DCK" + ).forEach { text -> + Row( + Modifier + .selectable( + selected = (text == appViewModel.connectionPriority), + onClick = { appViewModel.updateConnectionPriority(text) }, + role = Role.RadioButton + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == appViewModel.connectionPriority), + onClick = null + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } } Row { Column(Modifier.selectableGroup()) { diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt index a58e344..b15c4fe 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt @@ -25,6 +25,7 @@ import java.util.UUID val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF6D3AE") const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF" +const val DEFAULT_STARTUP_DELAY = 3000 const val DEFAULT_SENDER_PACKET_COUNT = 100 const val DEFAULT_SENDER_PACKET_SIZE = 1024 const val DEFAULT_SENDER_PACKET_INTERVAL = 100 @@ -47,8 +48,10 @@ class AppViewModel : ViewModel() { var mode by mutableStateOf(RFCOMM_SERVER_MODE) var scenario by mutableStateOf(RECEIVE_SCENARIO) var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS) + var startupDelay by mutableIntStateOf(DEFAULT_STARTUP_DELAY) var l2capPsm by mutableIntStateOf(DEFAULT_PSM) var use2mPhy by mutableStateOf(true) + var connectionPriority by mutableStateOf("BALANCED") var mtu by mutableIntStateOf(0) var rxPhy by mutableIntStateOf(0) var txPhy by mutableIntStateOf(0) @@ -98,6 +101,11 @@ class AppViewModel : ViewModel() { if (savedScenario != null) { scenario = savedScenario } + + val savedConnectionPriority = preferences.getString(CONNECTION_PRIORITY_PREF_KEY, null) + if (savedConnectionPriority != null) { + connectionPriority = savedConnectionPriority + } } fun updatePeerBluetoothAddress(peerBluetoothAddress: String) { @@ -220,6 +228,14 @@ class AppViewModel : ViewModel() { } } + fun updateConnectionPriority(connectionPriority: String) { + this.connectionPriority = connectionPriority + with(preferences!!.edit()) { + putString(CONNECTION_PRIORITY_PREF_KEY, connectionPriority) + apply() + } + } + fun clear() { status = "" lastError = "" diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Pinger.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Pinger.kt index d5d375e..2b66092 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Pinger.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Pinger.kt @@ -19,8 +19,6 @@ 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, @@ -36,8 +34,8 @@ class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO override fun run() { viewModel.clear() - Log.info("startup delay: $DEFAULT_STARTUP_DELAY") - Thread.sleep(DEFAULT_STARTUP_DELAY.toLong()); + Log.info("startup delay: ${viewModel.startupDelay}") + Thread.sleep(viewModel.startupDelay.toLong()); Log.info("running") Log.info("sending reset") diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Sender.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Sender.kt index ed45cdd..d248f3b 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Sender.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Sender.kt @@ -19,8 +19,6 @@ 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) : IoClient, @@ -36,8 +34,8 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO override fun run() { viewModel.clear() - Log.info("startup delay: $DEFAULT_STARTUP_DELAY") - Thread.sleep(DEFAULT_STARTUP_DELAY.toLong()); + Log.info("startup delay: ${viewModel.startupDelay}") + Thread.sleep(viewModel.startupDelay.toLong()); Log.info("running") Log.info("sending reset")