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.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
@@ -30,6 +26,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.example.sensortestingapp.databinding.ActivityMainBinding import com.example.sensortestingapp.databinding.ActivityMainBinding
import com.punchthrough.blestarterappandroid.ble.ConnectionManager
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1 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") @SuppressLint("MissingPermission")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -103,77 +82,9 @@ class MainActivity : AppCompatActivity() {
private val kirbyScanResults = ArrayList<ScanResult>(); private val kirbyScanResults = ArrayList<ScanResult>();
private var mtuSizeInBytes = 23 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 { private val scanResultAdapter: ScanResultAdapter by lazy {
ScanResultAdapter(kirbyScanResults) { scanResult -> ScanResultAdapter(kirbyScanResults) { scanResult ->
@@ -187,13 +98,9 @@ class MainActivity : AppCompatActivity() {
"Start connecting to ${scanResult.device} (${scanResult.device.name})" "Start connecting to ${scanResult.device} (${scanResult.device.name})"
) )
scanResult.device.connectGatt( ConnectionManager.connect(scanResult.device, applicationContext)
applicationContext, ConnectionManager.requestMtu(scanResult.device, Int.MAX_VALUE)
false, ConnectionManager.discoverServices(scanResult.device)
gattCallback,
BluetoothDevice.TRANSPORT_LE
)
} }
} }