Compare commits

...

2 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod d35643524e allow specifying the address type 2023-12-08 18:46:25 -08:00
Gilles Boccon-Gibod 085f163c92 add support for 2M phy 2023-12-08 10:14:38 -08:00
9 changed files with 160 additions and 43 deletions
@@ -16,17 +16,77 @@ package com.github.google.bumble.btbench
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import java.io.IOException
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.os.Build
import java.util.logging.Logger
import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.l2cap-client")
class L2capClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) {
class L2capClient(
private val viewModel: AppViewModel,
val bluetoothAdapter: BluetoothAdapter,
val context: Context
) {
@SuppressLint("MissingPermission")
fun run() {
viewModel.running = true
val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress)
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
val address = viewModel.peerBluetoothAddress.take(17)
val remoteDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
bluetoothAdapter.getRemoteLeDevice(
address,
if (addressIsPublic) {
BluetoothDevice.ADDRESS_TYPE_PUBLIC
} else {
BluetoothDevice.ADDRESS_TYPE_RANDOM
}
)
} else {
bluetoothAdapter.getRemoteDevice(address)
}
val gatt = remoteDevice.connectGatt(
context,
false,
object : BluetoothGattCallback() {
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
Log.info("MTU update: mtu=$mtu status=$status")
viewModel.mtu = mtu
}
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
Log.info("PHY update: tx=$txPhy, rx=$rxPhy, status=$status")
viewModel.txPhy = txPhy
viewModel.rxPhy = rxPhy
}
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
Log.info("PHY: tx=$txPhy, rx=$rxPhy, status=$status")
viewModel.txPhy = txPhy
viewModel.rxPhy = rxPhy
}
override fun onConnectionStateChange(
gatt: BluetoothGatt?, status: Int, newState: Int
) {
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
gatt.setPreferredPhy(
BluetoothDevice.PHY_LE_2M_MASK,
BluetoothDevice.PHY_LE_2M_MASK,
BluetoothDevice.PHY_OPTION_NO_PREFERRED
)
gatt.readPhy()
}
}
},
BluetoothDevice.TRANSPORT_LE,
if (viewModel.use2mPhy) BluetoothDevice.PHY_LE_2M_MASK else BluetoothDevice.PHY_LE_1M_MASK
)
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
val client = SocketClient(viewModel, socket)
@@ -30,7 +30,7 @@ private val Log = Logger.getLogger("btbench.l2cap-server")
class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdapter: BluetoothAdapter) {
@SuppressLint("MissingPermission")
fun run() {
// Advertise to that the peer can find us and connect.
// Advertise so that the peer can find us and connect.
val callback = object: AdvertiseCallback() {
override fun onStartFailure(errorCode: Int) {
Log.warning("failed to start advertising: $errorCode")
@@ -50,13 +50,12 @@ class L2capServer(private val viewModel: AppViewModel, private val bluetoothAdap
val advertiseData = AdvertiseData.Builder().build()
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback)
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
viewModel.l2capPsm = serverSocket.psm
Log.info("psm = $serverSocket.psm")
val server = SocketServer(viewModel, serverSocket)
server.run({ advertiser.stopAdvertising(callback) })
server.run({ advertiser.stopAdvertising(callback) }, { advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) })
}
}
@@ -26,23 +26,33 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@@ -171,7 +181,7 @@ class MainActivity : ComponentActivity() {
}
private fun runL2capClient() {
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it) }
val l2capClient = bluetoothAdapter?.let { L2capClient(appViewModel, it, baseContext) }
l2capClient?.run()
}
@@ -199,9 +209,12 @@ fun MainView(
runL2capServer: () -> Unit
) {
BTBenchTheme {
// A surface container using the 'background' color from the theme
val scrollState = rememberScrollState()
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
color = MaterialTheme.colorScheme.background
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
@@ -212,28 +225,33 @@ fun MainView(
)
Divider()
val keyboardController = LocalSoftwareKeyboardController.current
TextField(label = {
Text(text = "Peer Bluetooth Address")
},
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
TextField(
label = {
Text(text = "Peer Bluetooth Address")
},
value = appViewModel.peerBluetoothAddress,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done
),
onValueChange = {
appViewModel.updatePeerBluetoothAddress(it)
},
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus()
})
)
Divider()
TextField(label = {
Text(text = "L2CAP PSM")
},
value = appViewModel.l2capPsm.toString(),
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
onValueChange = {
if (it.isNotEmpty()) {
@@ -243,7 +261,11 @@ fun MainView(
}
}
},
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }))
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus()
})
)
Divider()
Slider(
value = appViewModel.senderPacketCountSlider, onValueChange = {
@@ -264,7 +286,19 @@ fun MainView(
ActionButton(
text = "Become Discoverable", onClick = becomeDiscoverable, true
)
Row() {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "2M PHY")
Spacer(modifier = Modifier.padding(start = 8.dp))
Switch(
checked = appViewModel.use2mPhy,
onCheckedChange = { appViewModel.use2mPhy = it }
)
}
Row {
ActionButton(
text = "RFCOMM Client", onClick = runRfcommClient, !appViewModel.running
)
@@ -272,7 +306,7 @@ fun MainView(
text = "RFCOMM Server", onClick = runRfcommServer, !appViewModel.running
)
}
Row() {
Row {
ActionButton(
text = "L2CAP Client", onClick = runL2capClient, !appViewModel.running
)
@@ -281,6 +315,12 @@ fun MainView(
)
}
Divider()
Text(
text = if (appViewModel.mtu != 0) "MTU: ${appViewModel.mtu}" else ""
)
Text(
text = if (appViewModel.rxPhy != 0 || appViewModel.txPhy != 0) "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}" else ""
)
Text(
text = "Packets Sent: ${appViewModel.packetsSent}"
)
@@ -32,6 +32,10 @@ class AppViewModel : ViewModel() {
private var preferences: SharedPreferences? = null
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
var l2capPsm by mutableStateOf(0)
var use2mPhy by mutableStateOf(true)
var mtu by mutableStateOf(0)
var rxPhy by mutableStateOf(0)
var txPhy by mutableStateOf(0)
var senderPacketCountSlider by mutableFloatStateOf(0.0F)
var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
@@ -64,11 +68,12 @@ class AppViewModel : ViewModel() {
}
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
this.peerBluetoothAddress = peerBluetoothAddress
val address = peerBluetoothAddress.uppercase()
this.peerBluetoothAddress = address
// Save the address to the preferences
with(preferences!!.edit()) {
putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, peerBluetoothAddress)
putString(PEER_BLUETOOTH_ADDRESS_PREF_KEY, address)
apply()
}
}
@@ -116,7 +121,7 @@ class AppViewModel : ViewModel() {
}
fun updateSenderPacketSizeSlider() {
if (senderPacketSize <= 1) {
if (senderPacketSize <= 16) {
senderPacketSizeSlider = 0.0F
} else if (senderPacketSize <= 256) {
senderPacketSizeSlider = 0.02F
@@ -138,7 +143,7 @@ class AppViewModel : ViewModel() {
fun updateSenderPacketSize() {
if (senderPacketSizeSlider < 0.1F) {
senderPacketSize = 1
senderPacketSize = 16
} else if (senderPacketSizeSlider < 0.3F) {
senderPacketSize = 256
} else if (senderPacketSizeSlider < 0.5F) {
@@ -25,7 +25,8 @@ private val Log = Logger.getLogger("btbench.rfcomm-client")
class RfcommClient(private val viewModel: AppViewModel, val bluetoothAdapter: BluetoothAdapter) {
@SuppressLint("MissingPermission")
fun run() {
val remoteDevice = bluetoothAdapter.getRemoteDevice(viewModel.peerBluetoothAddress)
val address = viewModel.peerBluetoothAddress.take(17)
val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
val socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(
DEFAULT_RFCOMM_UUID
)
@@ -30,6 +30,6 @@ class RfcommServer(private val viewModel: AppViewModel, val bluetoothAdapter: Bl
)
val server = SocketServer(viewModel, serverSocket)
server.run({})
server.run({}, {})
}
}
@@ -22,6 +22,8 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.socket-client")
private const val DEFAULT_STARTUP_DELAY = 3000
class SocketClient(private val viewModel: AppViewModel, private val socket: BluetoothSocket) {
@SuppressLint("MissingPermission")
fun run() {
@@ -56,6 +58,10 @@ class SocketClient(private val viewModel: AppViewModel, private val socket: Blue
socketDataSource.receive()
}
Log.info("Startup delay: $DEFAULT_STARTUP_DELAY")
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
Log.info("Starting to send")
sender.run()
cleanup()
}
@@ -22,14 +22,13 @@ import kotlin.concurrent.thread
private val Log = Logger.getLogger("btbench.socket-server")
class SocketServer(private val viewModel: AppViewModel, private val serverSocket: BluetoothServerSocket) {
fun run(onTerminate: () -> Unit) {
fun run(onConnected: () -> Unit, onDisconnected: () -> Unit) {
var aborted = false
viewModel.running = true
fun cleanup() {
serverSocket.close()
viewModel.running = false
onTerminate()
}
thread(name = "SocketServer") {
@@ -38,6 +37,7 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
serverSocket.close()
}
Log.info("waiting for connection...")
onDisconnected()
val socket = try {
serverSocket.accept()
} catch (error: IOException) {
@@ -45,7 +45,8 @@ class SocketServer(private val viewModel: AppViewModel, private val serverSocket
cleanup()
return@thread
}
Log.info("got connection")
Log.info("got connection from ${socket.remoteDevice.address}")
onConnected()
viewModel.aborter = {
aborted = true
@@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -71,7 +73,7 @@ class AppViewModel : ViewModel(), HciProxy.Listener {
this.tcpPort = tcpPort
// Save the port to the preferences
with (preferences!!.edit()) {
with(preferences!!.edit()) {
putString(TCP_PORT_PREF_KEY, tcpPort.toString())
apply()
}
@@ -138,7 +140,8 @@ class MainActivity : ComponentActivity() {
log.warning("Exception while running HCI Server: $error")
} catch (error: HalException) {
log.warning("HAL exception: ${error.message}")
appViewModel.message = "Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell."
appViewModel.message =
"Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell."
}
log.info("HCI Proxy thread ended")
appViewModel.canStart = true
@@ -157,9 +160,12 @@ fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) {
@Composable
fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
RemoteHCITheme {
// A surface container using the 'background' color from the theme
val scrollState = rememberScrollState()
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
color = MaterialTheme.colorScheme.background
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
@@ -174,13 +180,15 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
)
Divider()
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
label = {
Text(text = "TCP Port")
},
TextField(label = {
Text(text = "TCP Port")
},
value = appViewModel.tcpPort.toString(),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
onValueChange = {
if (it.isNotEmpty()) {
val tcpPort = it.toIntOrNull()
@@ -189,10 +197,7 @@ fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
}
}
},
keyboardActions = KeyboardActions(
onDone = {keyboardController?.hide()}
)
)
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }))
Divider()
val connectState = if (appViewModel.hostConnected) "CONNECTED" else "DISCONNECTED"
Text(