feat: run BLE ops one at a time
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user