mirror of
https://github.com/google/bumble.git
synced 2026-07-14 14:10:50 +00:00
wip
This commit is contained in:
@@ -1607,6 +1607,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
'--att-mtu',
|
'--att-mtu',
|
||||||
metavar='MTU',
|
metavar='MTU',
|
||||||
type=click.IntRange(23, 517),
|
type=click.IntRange(23, 517),
|
||||||
|
default=517,
|
||||||
help='GATT MTU (gatt-client mode)',
|
help='GATT MTU (gatt-client mode)',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ class OneDeviceBenchTest(base_test.BaseTestClass):
|
|||||||
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
print("### Final status:", final_status)
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
def test_gatt_client_send(self):
|
||||||
|
runner = self.dut.bench.runGattClient(
|
||||||
|
"send", "F1:F1:F1:F1:F1:F1", 128, True, 100, 970, 100, "HIGH"
|
||||||
|
)
|
||||||
|
print("### Initial status:", runner)
|
||||||
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
def test_gatt_server_receive(self):
|
||||||
|
runner = self.dut.bench.runGattServer("receive")
|
||||||
|
print("### Initial status:", runner)
|
||||||
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_runner.main()
|
test_runner.main()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ TestBeds:
|
|||||||
- Name: BenchTestBed
|
- Name: BenchTestBed
|
||||||
Controllers:
|
Controllers:
|
||||||
AndroidDevice:
|
AndroidDevice:
|
||||||
- serial: 37211FDJG000DJ
|
- serial: emulator-5554
|
||||||
local_bt_address: 94:45:60:5E:03:B0
|
local_bt_address: 94:45:60:5E:03:B0
|
||||||
|
|
||||||
- serial: 23071FDEE001F7
|
#- serial: 23071FDEE001F7
|
||||||
local_bt_address: DC:E5:5B:E5:51:2C
|
# local_bt_address: DC:E5:5B:E5:51:2C
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.github.google.bumble.btbench">
|
package="com.github.google.bumble.btbench">
|
||||||
<uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" />
|
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="34" />
|
||||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.le.AdvertiseCallback
|
||||||
|
import android.bluetooth.le.AdvertiseData
|
||||||
|
import android.bluetooth.le.AdvertiseSettings
|
||||||
|
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
||||||
|
import android.os.Build
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.advertiser")
|
||||||
|
|
||||||
|
class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCallback() {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun start() {
|
||||||
|
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
||||||
|
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
||||||
|
.setConnectable(true)
|
||||||
|
advertiseSettingsBuilder.setDiscoverable(true)
|
||||||
|
val advertiseSettings = advertiseSettingsBuilder.build()
|
||||||
|
val advertiseData = AdvertiseData.Builder().build()
|
||||||
|
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
||||||
|
bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising(advertiseSettings, advertiseData, scanData, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun stop() {
|
||||||
|
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartFailure(errorCode: Int) {
|
||||||
|
Log.warning("failed to start advertising: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
||||||
|
Log.info("advertising started: $settingsInEffect")
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -112,6 +112,18 @@ public class AutomationSnippet implements Snippet {
|
|||||||
packetIO));
|
packetIO));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "gatt-client":
|
||||||
|
runnable = new GattClient(model, mBluetoothAdapter, mContext,
|
||||||
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
|
packetIO));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gatt-server":
|
||||||
|
runnable = new GattServer(model, mBluetoothAdapter, mContext,
|
||||||
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
|
packetIO));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -277,6 +289,53 @@ public class AutomationSnippet implements Snippet {
|
|||||||
return runner.toJson();
|
return runner.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Rpc(description = "Run a scenario in GATT Client mode")
|
||||||
|
public JSONObject runGattClient(String scenario, String peerBluetoothAddress,
|
||||||
|
boolean use_2m_phy, int packetCount, int packetSize,
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppViewModel model = new AppViewModel();
|
||||||
|
model.setPeerBluetoothAddress(peerBluetoothAddress);
|
||||||
|
model.setUse2mPhy(use_2m_phy);
|
||||||
|
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, "gatt-client", scenario);
|
||||||
|
assert runner != null;
|
||||||
|
return runner.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rpc(description = "Run a scenario in GATT Server mode")
|
||||||
|
public JSONObject runGattServer(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppViewModel model = new AppViewModel();
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
Runner runner = runScenario(model, "gatt-server", scenario);
|
||||||
|
assert runner != null;
|
||||||
|
return runner.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
@Rpc(description = "Stop a Runner")
|
@Rpc(description = "Stop a Runner")
|
||||||
public JSONObject stopRunner(String runnerId) throws JSONException {
|
public JSONObject stopRunner(String runnerId) throws JSONException {
|
||||||
Runner runner = findRunner(runnerId);
|
Runner runner = findRunner(runnerId);
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ open class Connection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun disconnect() {
|
open fun disconnect() {
|
||||||
gatt?.disconnect()
|
gatt?.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// 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 java.util.UUID
|
||||||
|
|
||||||
|
var CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
|
||||||
|
|
||||||
|
val BENCH_SERVICE_UUID = UUID.fromString("50DB505C-8AC4-4738-8448-3B1D9CC09CC5")
|
||||||
|
val BENCH_TX_UUID = UUID.fromString("E789C754-41A1-45F4-A948-A0A1A90DBA53")
|
||||||
|
val BENCH_RX_UUID = UUID.fromString("016A2CC7-E14B-4819-935F-1F56EAE4098D")
|
||||||
+16
-11
@@ -30,12 +30,6 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.gatt-client")
|
private val Log = Logger.getLogger("btbench.gatt-client")
|
||||||
|
|
||||||
private var CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
|
|
||||||
|
|
||||||
private val SPEED_SERVICE_UUID = UUID.fromString("50DB505C-8AC4-4738-8448-3B1D9CC09CC5")
|
|
||||||
private val SPEED_TX_UUID = UUID.fromString("E789C754-41A1-45F4-A948-A0A1A90DBA53")
|
|
||||||
private val SPEED_RX_UUID = UUID.fromString("016A2CC7-E14B-4819-935F-1F56EAE4098D")
|
|
||||||
|
|
||||||
|
|
||||||
class GattClientConnection(
|
class GattClientConnection(
|
||||||
viewModel: AppViewModel,
|
viewModel: AppViewModel,
|
||||||
@@ -52,7 +46,8 @@ class GattClientConnection(
|
|||||||
super.connect()
|
super.connect()
|
||||||
|
|
||||||
// Check if we're already connected and have discovered the services
|
// Check if we're already connected and have discovered the services
|
||||||
if (gatt?.getService(SPEED_SERVICE_UUID) != null) {
|
if (gatt?.getService(BENCH_SERVICE_UUID) != null) {
|
||||||
|
Log.fine("already connected")
|
||||||
onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS)
|
onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +58,7 @@ class GattClientConnection(
|
|||||||
) {
|
) {
|
||||||
super.onConnectionStateChange(gatt, status, newState)
|
super.onConnectionStateChange(gatt, status, newState)
|
||||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.warning("onConnectionStateChange status=$status")
|
||||||
discoveryDone.countDown()
|
discoveryDone.countDown()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -76,6 +72,8 @@ class GattClientConnection(
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||||
|
Log.fine("onServicesDiscovered")
|
||||||
|
|
||||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.warning("failed to discover services: ${status}")
|
Log.warning("failed to discover services: ${status}")
|
||||||
discoveryDone.countDown()
|
discoveryDone.countDown()
|
||||||
@@ -83,7 +81,7 @@ class GattClientConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the service
|
// Find the service
|
||||||
val service = gatt!!.getService(SPEED_SERVICE_UUID)
|
val service = gatt!!.getService(BENCH_SERVICE_UUID)
|
||||||
if (service == null) {
|
if (service == null) {
|
||||||
Log.warning("GATT Service not found")
|
Log.warning("GATT Service not found")
|
||||||
discoveryDone.countDown()
|
discoveryDone.countDown()
|
||||||
@@ -91,13 +89,13 @@ class GattClientConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the RX and TX characteristics
|
// Find the RX and TX characteristics
|
||||||
rxCharacteristic = service.getCharacteristic(SPEED_RX_UUID)
|
rxCharacteristic = service.getCharacteristic(BENCH_RX_UUID)
|
||||||
if (rxCharacteristic == null) {
|
if (rxCharacteristic == null) {
|
||||||
Log.warning("GATT RX Characteristics not found")
|
Log.warning("GATT RX Characteristics not found")
|
||||||
discoveryDone.countDown()
|
discoveryDone.countDown()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
txCharacteristic = service.getCharacteristic(SPEED_TX_UUID)
|
txCharacteristic = service.getCharacteristic(BENCH_TX_UUID)
|
||||||
if (txCharacteristic == null) {
|
if (txCharacteristic == null) {
|
||||||
Log.warning("GATT TX Characteristics not found")
|
Log.warning("GATT TX Characteristics not found")
|
||||||
discoveryDone.countDown()
|
discoveryDone.countDown()
|
||||||
@@ -105,6 +103,7 @@ class GattClientConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to the RX characteristic
|
// Subscribe to the RX characteristic
|
||||||
|
Log.fine("subscribing to RX")
|
||||||
gatt.setCharacteristicNotification(rxCharacteristic, true)
|
gatt.setCharacteristicNotification(rxCharacteristic, true)
|
||||||
val cccdDescriptor = rxCharacteristic!!.getDescriptor(CCCD_UUID)
|
val cccdDescriptor = rxCharacteristic!!.getDescriptor(CCCD_UUID)
|
||||||
gatt.writeDescriptor(cccdDescriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
gatt.writeDescriptor(cccdDescriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||||
@@ -132,7 +131,7 @@ class GattClientConnection(
|
|||||||
characteristic: BluetoothGattCharacteristic,
|
characteristic: BluetoothGattCharacteristic,
|
||||||
value: ByteArray
|
value: ByteArray
|
||||||
) {
|
) {
|
||||||
if (characteristic.uuid == SPEED_RX_UUID && packetSink != null) {
|
if (characteristic.uuid == BENCH_RX_UUID && packetSink != null) {
|
||||||
val packet = Packet.from(value)
|
val packet = Packet.from(value)
|
||||||
packetSink!!.onPacket(packet)
|
packetSink!!.onPacket(packet)
|
||||||
}
|
}
|
||||||
@@ -163,6 +162,12 @@ class GattClientConnection(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override
|
||||||
|
fun disconnect() {
|
||||||
|
super.disconnect()
|
||||||
|
discoveryDone.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
fun waitForDiscoveryCompletion() {
|
fun waitForDiscoveryCompletion() {
|
||||||
discoveryDone.await()
|
discoveryDone.await()
|
||||||
}
|
}
|
||||||
|
|||||||
+243
@@ -0,0 +1,243 @@
|
|||||||
|
// 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.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
|
import android.bluetooth.BluetoothGattServer
|
||||||
|
import android.bluetooth.BluetoothGattServerCallback
|
||||||
|
import android.bluetooth.BluetoothGattService
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothStatusCodes
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.Semaphore
|
||||||
|
import java.util.logging.Logger
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.gatt-server")
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
class GattServer(
|
||||||
|
private val viewModel: AppViewModel,
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter,
|
||||||
|
context: Context,
|
||||||
|
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||||
|
) : Mode, PacketIO, BluetoothGattServerCallback() {
|
||||||
|
override var packetSink: PacketSink? = null
|
||||||
|
private val gattServer: BluetoothGattServer
|
||||||
|
private val rxCharacteristic: BluetoothGattCharacteristic?
|
||||||
|
private val txCharacteristic: BluetoothGattCharacteristic?
|
||||||
|
private val notifySemaphore: Semaphore = Semaphore(1)
|
||||||
|
private val ready: CountDownLatch = CountDownLatch(1)
|
||||||
|
private var peerDevice: BluetoothDevice? = null
|
||||||
|
private var clientThread: Thread? = null
|
||||||
|
private var sinkQueue: LinkedBlockingQueue<Packet>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
val bluetoothManager = ContextCompat.getSystemService(context, BluetoothManager::class.java)
|
||||||
|
gattServer = bluetoothManager!!.openGattServer(context, this)
|
||||||
|
val benchService = gattServer.getService(BENCH_SERVICE_UUID)
|
||||||
|
if (benchService == null) {
|
||||||
|
rxCharacteristic = BluetoothGattCharacteristic(
|
||||||
|
BENCH_RX_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
txCharacteristic = BluetoothGattCharacteristic(
|
||||||
|
BENCH_TX_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
val rxCCCD = BluetoothGattDescriptor(
|
||||||
|
CCCD_UUID,
|
||||||
|
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
rxCharacteristic.addDescriptor(rxCCCD)
|
||||||
|
|
||||||
|
val service =
|
||||||
|
BluetoothGattService(BENCH_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
||||||
|
service.addCharacteristic(rxCharacteristic)
|
||||||
|
service.addCharacteristic(txCharacteristic)
|
||||||
|
|
||||||
|
gattServer.addService(service)
|
||||||
|
} else {
|
||||||
|
rxCharacteristic = benchService.getCharacteristic(BENCH_RX_UUID)
|
||||||
|
txCharacteristic = benchService.getCharacteristic(BENCH_TX_UUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWriteRequest(
|
||||||
|
device: BluetoothDevice?,
|
||||||
|
requestId: Int,
|
||||||
|
characteristic: BluetoothGattCharacteristic?,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray?
|
||||||
|
) {
|
||||||
|
Log.info("onCharacteristicWriteRequest")
|
||||||
|
if (characteristic != null && characteristic.uuid == BENCH_TX_UUID) {
|
||||||
|
if (packetSink == null) {
|
||||||
|
Log.warning("no sink, dropping")
|
||||||
|
} else if (offset != 0) {
|
||||||
|
Log.warning("offset != 0")
|
||||||
|
} else if (value == null) {
|
||||||
|
Log.warning("no value")
|
||||||
|
} else {
|
||||||
|
// Deliver the packet in a separate thread so that we don't block this
|
||||||
|
// callback.
|
||||||
|
sinkQueue?.put(Packet.from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationSent(device: BluetoothDevice?, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
notifySemaphore.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDescriptorWriteRequest(
|
||||||
|
device: BluetoothDevice?,
|
||||||
|
requestId: Int,
|
||||||
|
descriptor: BluetoothGattDescriptor?,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray?
|
||||||
|
) {
|
||||||
|
if (descriptor?.uuid == CCCD_UUID && descriptor?.characteristic?.uuid == BENCH_RX_UUID) {
|
||||||
|
if (offset == 0 && value?.size == 2) {
|
||||||
|
if (value[0].and(1).toInt() != 0) {
|
||||||
|
// Subscription
|
||||||
|
Log.fine("peer subscribed to RX")
|
||||||
|
peerDevice = device
|
||||||
|
ready.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun sendPacket(packet: Packet) {
|
||||||
|
if (peerDevice == null) {
|
||||||
|
Log.warning("no peer device, cannot send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rxCharacteristic == null) {
|
||||||
|
Log.warning("no RX characteristic, cannot send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we can notify
|
||||||
|
notifySemaphore.acquire()
|
||||||
|
|
||||||
|
// Send the packet via a notification
|
||||||
|
val result = gattServer.notifyCharacteristicChanged(
|
||||||
|
peerDevice!!,
|
||||||
|
rxCharacteristic,
|
||||||
|
false,
|
||||||
|
packet.toBytes()
|
||||||
|
)
|
||||||
|
if (result != BluetoothStatusCodes.SUCCESS) {
|
||||||
|
Log.warning("notifyCharacteristicChanged failed: $result")
|
||||||
|
notifySemaphore.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
viewModel.running = true
|
||||||
|
|
||||||
|
// Start advertising
|
||||||
|
Log.fine("starting advertiser")
|
||||||
|
val advertiser = Advertiser(bluetoothAdapter)
|
||||||
|
advertiser.start()
|
||||||
|
|
||||||
|
clientThread = thread(name = "GattServer") {
|
||||||
|
// Wait for a subscriber
|
||||||
|
Log.info("waiting for RX subscriber")
|
||||||
|
viewModel.aborter = {
|
||||||
|
ready.countDown()
|
||||||
|
}
|
||||||
|
ready.await()
|
||||||
|
if (peerDevice == null) {
|
||||||
|
Log.warning("server interrupted")
|
||||||
|
viewModel.running = false
|
||||||
|
gattServer.close()
|
||||||
|
return@thread
|
||||||
|
}
|
||||||
|
Log.info("RX subscriber accepted")
|
||||||
|
|
||||||
|
// Stop advertising
|
||||||
|
Log.info("stopping advertiser")
|
||||||
|
advertiser.stop()
|
||||||
|
|
||||||
|
sinkQueue = LinkedBlockingQueue()
|
||||||
|
val sinkWriterThread = thread(name = "SinkWriter") {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
val packet = sinkQueue!!.take()
|
||||||
|
if (packetSink == null) {
|
||||||
|
Log.warning("no sink, dropping packet")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packetSink!!.onPacket(packet)
|
||||||
|
} catch (error: InterruptedException) {
|
||||||
|
Log.warning("sink writer interrupted")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ioClient = createIoClient(this)
|
||||||
|
|
||||||
|
try {
|
||||||
|
ioClient.run()
|
||||||
|
viewModel.status = "OK"
|
||||||
|
} catch (error: IOException) {
|
||||||
|
Log.info("run ended abruptly")
|
||||||
|
viewModel.status = "ABORTED"
|
||||||
|
viewModel.lastError = "IO_ERROR"
|
||||||
|
} finally {
|
||||||
|
sinkWriterThread.interrupt()
|
||||||
|
sinkWriterThread.join()
|
||||||
|
gattServer.close()
|
||||||
|
viewModel.running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitForCompletion() {
|
||||||
|
clientThread?.join()
|
||||||
|
Log.info("server thread completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-22
@@ -37,34 +37,15 @@ class L2capServer(
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun run() {
|
override fun run() {
|
||||||
// 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 advertiser = Advertiser(bluetoothAdapter)
|
||||||
override fun onStartFailure(errorCode: Int) {
|
|
||||||
Log.warning("failed to start advertising: $errorCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
||||||
Log.info("advertising started: $settingsInEffect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
|
||||||
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
|
||||||
.setConnectable(true)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
advertiseSettingsBuilder.setDiscoverable(true)
|
|
||||||
}
|
|
||||||
val advertiseSettings = advertiseSettingsBuilder.build()
|
|
||||||
val advertiseData = AdvertiseData.Builder().build()
|
|
||||||
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
|
||||||
val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
|
||||||
|
|
||||||
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
||||||
viewModel.l2capPsm = serverSocket.psm
|
viewModel.l2capPsm = serverSocket.psm
|
||||||
Log.info("psm = $serverSocket.psm")
|
Log.info("psm = $serverSocket.psm")
|
||||||
|
|
||||||
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
|
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
|
||||||
socketServer!!.run(
|
socketServer!!.run(
|
||||||
{ advertiser.stopAdvertising(callback) },
|
{ advertiser.stop() },
|
||||||
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) }
|
{ advertiser.start() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -211,6 +211,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
GATT_CLIENT_MODE -> GattClient(
|
GATT_CLIENT_MODE -> GattClient(
|
||||||
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
||||||
)
|
)
|
||||||
|
GATT_SERVER_MODE -> GattServer(
|
||||||
|
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.github.google.bumble.btbench
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
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 expectedSequenceNumber: Int = 0
|
private var expectedSequenceNumber: Int = 0
|
||||||
|
private val done = CountDownLatch(1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
packetIO.packetSink = this
|
packetIO.packetSink = this
|
||||||
@@ -30,6 +32,7 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
done.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
@@ -58,5 +61,10 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
||||||
viewModel.packetsSent += 1
|
viewModel.packetsSent += 1
|
||||||
|
|
||||||
|
if (packet.flags and Packet.LAST_FLAG != 0) {
|
||||||
|
Log.info("received last packet")
|
||||||
|
done.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.github.google.bumble.btbench
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
@@ -24,6 +25,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
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
|
||||||
|
private val done = CountDownLatch(1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
packetIO.packetSink = this
|
packetIO.packetSink = this
|
||||||
@@ -31,6 +33,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
done.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
@@ -62,6 +65,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
Log.info("throughput: $throughput")
|
Log.info("throughput: $throughput")
|
||||||
viewModel.throughput = throughput
|
viewModel.throughput = throughput
|
||||||
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
||||||
|
done.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user