2020-01-27 14:54:35 -08:00
|
|
|
package com.geeksville.mesh
|
|
|
|
|
|
|
|
|
|
import android.bluetooth.*
|
2020-02-04 17:27:10 -08:00
|
|
|
import android.content.BroadcastReceiver
|
2020-01-27 14:54:35 -08:00
|
|
|
import android.content.Context
|
2020-02-04 17:27:10 -08:00
|
|
|
import android.content.Intent
|
|
|
|
|
import android.content.IntentFilter
|
2020-01-27 14:54:35 -08:00
|
|
|
import com.geeksville.android.Logging
|
|
|
|
|
import com.geeksville.concurrent.CallbackContinuation
|
|
|
|
|
import com.geeksville.concurrent.Continuation
|
|
|
|
|
import com.geeksville.concurrent.SyncContinuation
|
2020-02-04 17:27:10 -08:00
|
|
|
import com.geeksville.util.exceptionReporter
|
2020-01-27 14:54:35 -08:00
|
|
|
import java.io.IOException
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Uses coroutines to safely access a bluetooth GATT device with a synchronous API
|
|
|
|
|
*
|
|
|
|
|
* The BTLE API on android is dumb. You can only have one outstanding operation in flight to
|
|
|
|
|
* the device. If you try to do something when something is pending, the operation just returns
|
|
|
|
|
* false. You are expected to chain your operations from the results callbacks.
|
|
|
|
|
*
|
|
|
|
|
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations.
|
|
|
|
|
*/
|
|
|
|
|
class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) :
|
|
|
|
|
Logging {
|
|
|
|
|
|
|
|
|
|
/// Timeout before we declare a bluetooth operation failed
|
2020-02-04 12:12:29 -08:00
|
|
|
var timeoutMsec = 5 * 1000L
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
/// Users can access the GATT directly as needed
|
2020-02-04 17:27:10 -08:00
|
|
|
var gatt: BluetoothGatt? = null
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
var state = BluetoothProfile.STATE_DISCONNECTED
|
|
|
|
|
private var currentWork: BluetoothContinuation? = null
|
|
|
|
|
private val workQueue = mutableListOf<BluetoothContinuation>()
|
|
|
|
|
|
2020-02-04 17:27:10 -08:00
|
|
|
/// When we see the BT stack getting disabled/renabled we handle that as a connect/disconnect event
|
|
|
|
|
private val btStateReceiver = object : BroadcastReceiver() {
|
|
|
|
|
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
|
|
|
|
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
|
|
|
|
val newstate = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
|
|
|
|
|
when (newstate) {
|
|
|
|
|
// Simulate a disconnection if the user disables bluetooth entirely
|
|
|
|
|
BluetoothAdapter.STATE_OFF -> if (gatt != null) gattCallback.onConnectionStateChange(
|
|
|
|
|
gatt!!,
|
|
|
|
|
0,
|
|
|
|
|
BluetoothProfile.STATE_DISCONNECTED
|
|
|
|
|
)
|
|
|
|
|
BluetoothAdapter.STATE_ON -> {
|
|
|
|
|
warn("FIXME - requeue a connect anytime bluetooth is reenabled")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
context.registerReceiver(
|
|
|
|
|
btStateReceiver,
|
|
|
|
|
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-27 14:54:35 -08:00
|
|
|
/**
|
|
|
|
|
* a schedulable bit of bluetooth work, includes both the closure to call to start the operation
|
|
|
|
|
* and the completion (either async or sync) to call when it completes
|
|
|
|
|
*/
|
2020-01-27 18:47:13 -08:00
|
|
|
private class BluetoothContinuation(
|
|
|
|
|
val tag: String,
|
2020-01-27 14:54:35 -08:00
|
|
|
val completion: com.geeksville.concurrent.Continuation<*>,
|
2020-01-27 18:47:13 -08:00
|
|
|
val startWorkFn: () -> Boolean
|
|
|
|
|
) : Logging {
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
/// Start running a queued bit of work, return true for success or false for fatal bluetooth error
|
2020-01-27 18:47:13 -08:00
|
|
|
fun startWork(): Boolean {
|
|
|
|
|
debug("Starting work: $tag")
|
|
|
|
|
return startWorkFn()
|
|
|
|
|
}
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-04 17:27:10 -08:00
|
|
|
|
2020-01-27 14:54:35 -08:00
|
|
|
private val gattCallback = object : BluetoothGattCallback() {
|
|
|
|
|
|
|
|
|
|
override fun onConnectionStateChange(
|
2020-02-04 17:27:10 -08:00
|
|
|
g: BluetoothGatt,
|
2020-01-27 14:54:35 -08:00
|
|
|
status: Int,
|
|
|
|
|
newState: Int
|
|
|
|
|
) {
|
|
|
|
|
info("new bluetooth connection state $newState")
|
|
|
|
|
state = newState
|
|
|
|
|
when (newState) {
|
|
|
|
|
BluetoothProfile.STATE_CONNECTED -> {
|
|
|
|
|
//logAssert(workQueue.isNotEmpty())
|
|
|
|
|
//val work = workQueue.removeAt(0)
|
|
|
|
|
completeWork(status, Unit)
|
|
|
|
|
}
|
|
|
|
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
2020-01-27 16:58:47 -08:00
|
|
|
// cancel any queued ops? for now I think it is best to keep them around
|
|
|
|
|
// failAllWork(IOException("Lost connection"))
|
2020-02-04 17:27:10 -08:00
|
|
|
|
|
|
|
|
gatt = null;
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
|
|
|
completeWork(status, Unit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onCharacteristicRead(
|
|
|
|
|
gatt: BluetoothGatt,
|
|
|
|
|
characteristic: BluetoothGattCharacteristic,
|
|
|
|
|
status: Int
|
|
|
|
|
) {
|
|
|
|
|
completeWork(status, characteristic)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onCharacteristicWrite(
|
|
|
|
|
gatt: BluetoothGatt,
|
|
|
|
|
characteristic: BluetoothGattCharacteristic,
|
|
|
|
|
status: Int
|
|
|
|
|
) {
|
|
|
|
|
completeWork(status, characteristic)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
|
|
|
completeWork(status, mtu)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// If we have work we can do, start doing it.
|
|
|
|
|
private fun startNewWork() {
|
|
|
|
|
logAssert(currentWork == null)
|
|
|
|
|
|
|
|
|
|
if (workQueue.isNotEmpty()) {
|
|
|
|
|
val newWork = workQueue.removeAt(0)
|
|
|
|
|
currentWork = newWork
|
2020-01-27 18:47:13 -08:00
|
|
|
logAssert(newWork.startWork())
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-27 18:47:13 -08:00
|
|
|
private fun <T> queueWork(tag: String, cont: Continuation<T>, initFn: () -> Boolean) {
|
|
|
|
|
val btCont = BluetoothContinuation(tag, cont, initFn)
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
synchronized(workQueue) {
|
2020-01-27 18:47:13 -08:00
|
|
|
debug("Enqueuing work: ${btCont.tag}")
|
2020-01-27 14:54:35 -08:00
|
|
|
workQueue.add(btCont)
|
|
|
|
|
|
|
|
|
|
// if we don't have any outstanding operations, run first item in queue
|
|
|
|
|
if (currentWork == null)
|
|
|
|
|
startNewWork()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called from our big GATT callback, completes the current job and then schedules a new one
|
|
|
|
|
*/
|
|
|
|
|
private fun <T : Any> completeWork(status: Int, res: T) {
|
|
|
|
|
|
|
|
|
|
// startup next job in queue before calling the completion handler
|
|
|
|
|
val work =
|
|
|
|
|
synchronized(workQueue) {
|
|
|
|
|
val w = currentWork!! // will throw if null, which is helpful
|
|
|
|
|
currentWork = null // We are now no longer working on anything
|
|
|
|
|
|
|
|
|
|
startNewWork()
|
|
|
|
|
w
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-04 13:24:04 -08:00
|
|
|
debug("work ${work.tag} is completed, resuming with status $status")
|
2020-01-27 14:54:35 -08:00
|
|
|
if (status != 0)
|
|
|
|
|
work.completion.resumeWithException(IOException("Bluetooth status=$status"))
|
|
|
|
|
else
|
2020-01-27 16:00:00 -08:00
|
|
|
work.completion.resume(Result.success(res) as Result<Nothing>)
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Something went wrong, abort all queued
|
|
|
|
|
*/
|
|
|
|
|
private fun failAllWork(ex: Exception) {
|
|
|
|
|
synchronized(workQueue) {
|
|
|
|
|
workQueue.forEach {
|
|
|
|
|
it.completion.resumeWithException(ex)
|
|
|
|
|
}
|
|
|
|
|
workQueue.clear()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// helper glue to make sync continuations and then wait for the result
|
|
|
|
|
private fun <T> makeSync(wrappedFn: (SyncContinuation<T>) -> Unit): T {
|
|
|
|
|
val cont = SyncContinuation<T>()
|
|
|
|
|
wrappedFn(cont)
|
|
|
|
|
return cont.await(timeoutMsec)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME, pass in true for autoconnect - so we will autoconnect whenever the radio
|
|
|
|
|
// comes in range (even if we made this connect call long ago when we got powered on)
|
|
|
|
|
// see https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble for
|
|
|
|
|
// more info.
|
|
|
|
|
// Otherwise if you pass in false, it will try to connect now and will timeout and fail in 30 seconds.
|
|
|
|
|
private fun queueConnect(autoConnect: Boolean = false, cont: Continuation<Unit>) {
|
2020-02-04 17:27:10 -08:00
|
|
|
assert(gatt == null);
|
2020-01-27 18:47:13 -08:00
|
|
|
queueWork("connect", cont) {
|
2020-01-27 14:54:35 -08:00
|
|
|
val g = device.connectGatt(context, autoConnect, gattCallback)
|
|
|
|
|
if (g != null)
|
|
|
|
|
gatt = g
|
|
|
|
|
g != null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun asyncConnect(autoConnect: Boolean = false, cb: (Result<Unit>) -> Unit) {
|
2020-01-27 18:47:13 -08:00
|
|
|
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
|
2020-01-27 14:54:35 -08:00
|
|
|
queueConnect(autoConnect, CallbackContinuation(cb))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun connect(autoConnect: Boolean = false) = makeSync<Unit> { queueConnect(autoConnect, it) }
|
|
|
|
|
|
|
|
|
|
private fun queueReadCharacteristic(
|
|
|
|
|
c: BluetoothGattCharacteristic,
|
|
|
|
|
cont: Continuation<BluetoothGattCharacteristic>
|
2020-02-04 17:27:10 -08:00
|
|
|
) = queueWork("readc", cont) { gatt!!.readCharacteristic(c) }
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
fun asyncReadCharacteristic(
|
|
|
|
|
c: BluetoothGattCharacteristic,
|
|
|
|
|
cb: (Result<BluetoothGattCharacteristic>) -> Unit
|
|
|
|
|
) = queueReadCharacteristic(c, CallbackContinuation(cb))
|
|
|
|
|
|
|
|
|
|
fun readCharacteristic(c: BluetoothGattCharacteristic): BluetoothGattCharacteristic =
|
|
|
|
|
makeSync { queueReadCharacteristic(c, it) }
|
|
|
|
|
|
|
|
|
|
private fun queueDiscoverServices(cont: Continuation<Unit>) {
|
2020-01-27 18:47:13 -08:00
|
|
|
queueWork("discover", cont) {
|
2020-02-04 17:27:10 -08:00
|
|
|
gatt!!.discoverServices()
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun asyncDiscoverServices(cb: (Result<Unit>) -> Unit) {
|
2020-01-27 18:47:13 -08:00
|
|
|
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
|
2020-01-27 14:54:35 -08:00
|
|
|
queueDiscoverServices(CallbackContinuation(cb))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun discoverServices() = makeSync<Unit> { queueDiscoverServices(it) }
|
|
|
|
|
|
|
|
|
|
private fun queueRequestMtu(
|
|
|
|
|
len: Int,
|
|
|
|
|
cont: Continuation<Int>
|
2020-02-04 17:27:10 -08:00
|
|
|
) = queueWork("reqMtu", cont) { gatt!!.requestMtu(len) }
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
fun asyncRequestMtu(
|
|
|
|
|
len: Int,
|
|
|
|
|
cb: (Result<Int>) -> Unit
|
2020-01-27 18:47:13 -08:00
|
|
|
) {
|
|
|
|
|
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
|
|
|
|
|
queueRequestMtu(len, CallbackContinuation(cb))
|
|
|
|
|
}
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
fun requestMtu(len: Int): Int =
|
|
|
|
|
makeSync { queueRequestMtu(len, it) }
|
|
|
|
|
|
|
|
|
|
private fun queueWriteCharacteristic(
|
|
|
|
|
c: BluetoothGattCharacteristic,
|
|
|
|
|
cont: Continuation<BluetoothGattCharacteristic>
|
2020-02-04 17:27:10 -08:00
|
|
|
) = queueWork("writec", cont) { gatt!!.writeCharacteristic(c) }
|
2020-01-27 14:54:35 -08:00
|
|
|
|
|
|
|
|
fun asyncWriteCharacteristic(
|
|
|
|
|
c: BluetoothGattCharacteristic,
|
|
|
|
|
cb: (Result<BluetoothGattCharacteristic>) -> Unit
|
|
|
|
|
) = queueWriteCharacteristic(c, CallbackContinuation(cb))
|
|
|
|
|
|
|
|
|
|
fun writeCharacteristic(c: BluetoothGattCharacteristic): BluetoothGattCharacteristic =
|
|
|
|
|
makeSync { queueWriteCharacteristic(c, it) }
|
|
|
|
|
|
|
|
|
|
fun disconnect() {
|
2020-02-04 17:27:10 -08:00
|
|
|
if (gatt != null)
|
|
|
|
|
gatt!!.disconnect()
|
|
|
|
|
|
|
|
|
|
context.unregisterReceiver(btStateReceiver)
|
2020-01-27 18:47:13 -08:00
|
|
|
failAllWork(Exception("SafeBluetooth disconnected"))
|
2020-01-27 14:54:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
2020-01-27 18:47:13 -08:00
|
|
|
|