feat: run BLE ops one at a time

This commit is contained in:
Fabian Christoffel
2023-06-19 16:52:55 +02:00
parent cf1e82132a
commit 5700bc8645
2 changed files with 272 additions and 98 deletions

View File

@@ -0,0 +1,267 @@
/*
* Copyright 2019 Punch Through Design 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
*
* http://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.punchthrough.blestarterappandroid.ble
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.util.Log
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
private const val GATT_MIN_MTU_SIZE = 23
/** Maximum BLE MTU size as defined in gatt_api.h. */
private const val GATT_MAX_MTU_SIZE = 517
/** Abstract sealed class representing a type of BLE operation */
sealed class BleOperationType {
abstract val device: BluetoothDevice
}
/** Connect to [device] and perform service discovery */
data class Connect(override val device: BluetoothDevice, val context: Context) : BleOperationType()
/** Disconnect from [device] and release all connection resources */
data class Disconnect(override val device: BluetoothDevice) : BleOperationType()
/** Request for an MTU of [mtu] */
data class MtuRequest(
override val device: BluetoothDevice,
val mtu: Int
) : BleOperationType()
data class DiscoverServicesRequest(
override val device: BluetoothDevice,
) : BleOperationType()
private fun BluetoothGatt.printGattTable() {
if (services.isEmpty()) {
Log.i(
"printGattTable",
"No service and characteristic available, call discoverServices() first?"
)
return
}
services.forEach { service ->
val characteristicsTable = service.characteristics.joinToString(
separator = "\n|--",
prefix = "|--"
) { it.uuid.toString() }
Log.i(
"printGattTable", "\nService ${service.uuid}\nCharacteristics:\n$characteristicsTable"
)
}
}
@SuppressLint("MissingPermission")
object ConnectionManager {
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
private var pendingOperation: BleOperationType? = null
fun connect(device: BluetoothDevice, context: Context) {
if (device.isConnected()) {
Log.e("ConnectionManager", "Already connected to ${device.address}!")
} else {
enqueueOperation(Connect(device, context.applicationContext))
}
}
fun teardownConnection(device: BluetoothDevice) {
if (device.isConnected()) {
enqueueOperation(Disconnect(device))
} else {
Log.e(
"ConnectionManager",
"Not connected to ${device.address}, cannot teardown connection!"
)
}
}
fun requestMtu(device: BluetoothDevice, mtu: Int) {
enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE)))
}
fun discoverServices(device: BluetoothDevice) {
enqueueOperation(DiscoverServicesRequest(device))
}
// - Beginning of PRIVATE functions
@Synchronized
private fun enqueueOperation(operation: BleOperationType) {
operationQueue.add(operation)
if (pendingOperation == null) {
doNextOperation()
}
}
@Synchronized
private fun signalEndOfOperation() {
Log.d("ConnectionManager", "End of $pendingOperation")
pendingOperation = null
if (operationQueue.isNotEmpty()) {
doNextOperation()
}
}
/**
* Perform a given [BleOperationType]. All permission checks are performed before an operation
* can be enqueued by [enqueueOperation].
*/
@Synchronized
private fun doNextOperation() {
if (pendingOperation != null) {
Log.e(
"ConnectionManager",
"doNextOperation() called when an operation is pending! Aborting."
)
return
}
val operation = operationQueue.poll() ?: run {
Log.d("ConnectionManager", "Operation queue empty, returning")
return
}
pendingOperation = operation
// Handle Connect separately from other operations that require device to be connected
if (operation is Connect) {
with(operation) {
Log.w("ConnectionManager", "Connecting to ${device.address}")
device.connectGatt(
context, false, callback, BluetoothDevice.TRANSPORT_LE
)
}
return
}
// Check BluetoothGatt availability for other operations
val gatt = deviceGattMap[operation.device]
?: this@ConnectionManager.run {
Log.e(
"ConnectionManager",
"Not connected to ${operation.device.address}! Aborting $operation operation."
)
signalEndOfOperation()
return
}
when (operation) {
is Disconnect -> with(operation) {
Log.w("ConnectionManager", "Disconnecting from ${device.address}")
gatt.close()
deviceGattMap.remove(device)
signalEndOfOperation()
}
is MtuRequest -> with(operation) {
gatt.requestMtu(mtu)
}
is DiscoverServicesRequest -> with(operation) {
gatt.discoverServices()
}
is Connect -> {
Log.e("ConnectionManager", "Shouldn't get here")
}
}
}
private val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val deviceAddress = gatt.device.address
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.w(
"ConnectionManager",
"onConnectionStateChange: connected to $deviceAddress"
)
deviceGattMap[gatt.device] = gatt
if (pendingOperation is Connect) {
signalEndOfOperation()
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.e(
"ConnectionManager",
"onConnectionStateChange: disconnected from $deviceAddress"
)
teardownConnection(gatt.device)
}
} else {
Log.e(
"ConnectionManager",
"onConnectionStateChange: status $status encountered for $deviceAddress!"
)
teardownConnection(gatt.device)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
with(gatt) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.w(
"ConnectionManager",
"Discovered ${services.size} services for ${device.address}."
)
printGattTable()
} else {
Log.e("ConnectionManager", "Service discovery failed due to status $status")
teardownConnection(gatt.device)
}
}
if (pendingOperation is DiscoverServicesRequest) {
signalEndOfOperation()
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
Log.w(
"ConnectionManager",
"ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}"
)
if (pendingOperation is MtuRequest) {
signalEndOfOperation()
}
}
}
private fun BluetoothDevice.isConnected() = deviceGattMap.containsKey(this)
}

View File

@@ -5,11 +5,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
@@ -30,6 +26,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.example.sensortestingapp.databinding.ActivityMainBinding
import com.punchthrough.blestarterappandroid.ble.ConnectionManager
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
@@ -56,24 +53,6 @@ fun Context.hasRequiredRuntimePermissions(): Boolean {
}
}
private fun BluetoothGatt.printGattTable() {
if (services.isEmpty()) {
Log.i(
"printGattTable",
"No service and characteristic available, call discoverServices() first?"
)
return
}
services.forEach { service ->
val characteristicsTable = service.characteristics.joinToString(
separator = "\n|--",
prefix = "|--"
) { it.uuid.toString() }
Log.i(
"printGattTable", "\nService ${service.uuid}\nCharacteristics:\n$characteristicsTable"
)
}
}
@SuppressLint("MissingPermission")
class MainActivity : AppCompatActivity() {
@@ -103,77 +82,9 @@ class MainActivity : AppCompatActivity() {
private val kirbyScanResults = ArrayList<ScanResult>();
private var mtuSizeInBytes = 23
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val deviceAddress = gatt.device.address
val deviceName = gatt.device.name
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(
"BluetoothGattCallback",
"Successfully connected to $deviceAddress (${deviceName})"
)
if (!gatt.requestMtu(GATT_REQUESTED_MTU_SIZE)) {
Log.e(
"BluetoothGattCallback",
"Requesting MTU size failed"
)
}
if (!gatt.discoverServices()) {
Log.e(
"BluetoothGattCallback",
"Service discovery did not start"
)
}
// TODO: Store a reference to BluetoothGatt
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.w(
"BluetoothGattCallback",
"Successfully disconnected from $deviceAddress (${deviceName})"
)
gatt.close()
}
} else {
Log.w(
"BluetoothGattCallback",
"Error $status encountered for $deviceAddress (${deviceName})! Disconnecting..."
)
gatt.close()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
with(gatt) {
Log.i(
"BluetoothGattCallback",
"Discovered ${services.size} services for ${device.address} (${device.name})"
)
printGattTable()
}
}
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(
"BluetoothGattCallback",
"mtuSizeInBytes set to ${mtu}"
)
mtuSizeInBytes = mtu
} else {
Log.w(
"BluetoothGattCallback",
"Unsuccessful MTU change. Leaving mtuSizeInBytes unchanged ${mtuSizeInBytes}"
)
}
}
}
private val scanResultAdapter: ScanResultAdapter by lazy {
ScanResultAdapter(kirbyScanResults) { scanResult ->
@@ -187,13 +98,9 @@ class MainActivity : AppCompatActivity() {
"Start connecting to ${scanResult.device} (${scanResult.device.name})"
)
scanResult.device.connectGatt(
applicationContext,
false,
gattCallback,
BluetoothDevice.TRANSPORT_LE
)
ConnectionManager.connect(scanResult.device, applicationContext)
ConnectionManager.requestMtu(scanResult.device, Int.MAX_VALUE)
ConnectionManager.discoverServices(scanResult.device)
}
}