Fix 27: add timeouts for BLE operations, to protect against buggy drivers

pull/40/head
geeksville 2020-05-24 09:27:43 -07:00
rodzic bba9def20e
commit 28023b8f42
1 zmienionych plików z 53 dodań i 6 usunięć

Wyświetl plik

@ -13,6 +13,7 @@ import com.geeksville.concurrent.CallbackContinuation
import com.geeksville.concurrent.Continuation import com.geeksville.concurrent.Continuation
import com.geeksville.concurrent.SyncContinuation import com.geeksville.concurrent.SyncContinuation
import com.geeksville.util.exceptionReporter import com.geeksville.util.exceptionReporter
import kotlinx.coroutines.*
import java.io.Closeable import java.io.Closeable
import java.util.* import java.util.*
@ -67,6 +68,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
/// from characteristic UUIDs to the handler function for notfies /// from characteristic UUIDs to the handler function for notfies
private val notifyHandlers = mutableMapOf<UUID, (BluetoothGattCharacteristic) -> Unit>() private val notifyHandlers = mutableMapOf<UUID, (BluetoothGattCharacteristic) -> Unit>()
private val serviceScope = CoroutineScope(Dispatchers.IO)
/// When we see the BT stack getting disabled/renabled we handle that as a connect/disconnect event /// When we see the BT stack getting disabled/renabled we handle that as a connect/disconnect event
private val btStateReceiver = BluetoothStateReceiver { enabled -> private val btStateReceiver = BluetoothStateReceiver { enabled ->
if (!enabled) { if (!enabled) {
@ -107,7 +110,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
private class BluetoothContinuation( private class BluetoothContinuation(
val tag: String, val tag: String,
val completion: com.geeksville.concurrent.Continuation<*>, val completion: com.geeksville.concurrent.Continuation<*>,
val startWorkFn: () -> Boolean val timeoutMillis: Long = 0, // If we want to timeout this operation at a certain time, use a non zero value
private val startWorkFn: () -> Boolean
) : Logging { ) : Logging {
/// Start running a queued bit of work, return true for success or false for fatal bluetooth error /// Start running a queued bit of work, return true for success or false for fatal bluetooth error
@ -146,6 +150,10 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
} }
} }
// Our own custom BLE status codes
private val STATUS_RELIABLE_WRITE_FAILED = 4403
private val STATUS_TIMEOUT = 4403
private val gattCallback = object : BluetoothGattCallback() { private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange( override fun onConnectionStateChange(
@ -240,7 +248,10 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
if (!characteristic.value.contentEquals(reliable)) { if (!characteristic.value.contentEquals(reliable)) {
errormsg("A reliable write failed!") errormsg("A reliable write failed!")
gatt.abortReliableWrite(); gatt.abortReliableWrite();
completeWork(42, characteristic) // skanky code to indicate failure completeWork(
STATUS_RELIABLE_WRITE_FAILED,
characteristic
) // skanky code to indicate failure
} else { } else {
logAssert(gatt.executeReliableWrite()) logAssert(gatt.executeReliableWrite())
// After this execute reliable completes - we can continue with normal operations (see onReliableWriteCompleted) // After this execute reliable completes - we can continue with normal operations (see onReliableWriteCompleted)
@ -310,6 +321,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
} }
private var activeTimeout: Job? = null
/// If we have work we can do, start doing it. /// If we have work we can do, start doing it.
private fun startNewWork() { private fun startNewWork() {
logAssert(currentWork == null) logAssert(currentWork == null)
@ -317,15 +330,35 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
if (workQueue.isNotEmpty()) { if (workQueue.isNotEmpty()) {
val newWork = workQueue.removeAt(0) val newWork = workQueue.removeAt(0)
currentWork = newWork currentWork = newWork
if (newWork.timeoutMillis != 0L) {
activeTimeout = serviceScope.launch {
debug("Starting failsafe timer ${newWork.timeoutMillis}")
delay(newWork.timeoutMillis)
errormsg("Failsafe BLE timer expired!")
completeWork(
STATUS_TIMEOUT,
Unit
) // Throw an exception in that work
}
}
logAssert(newWork.startWork()) logAssert(newWork.startWork())
} }
} }
private fun <T> queueWork(tag: String, cont: Continuation<T>, initFn: () -> Boolean) { private fun <T> queueWork(
tag: String,
cont: Continuation<T>,
timeout: Long = 0,
initFn: () -> Boolean
) {
val btCont = val btCont =
BluetoothContinuation( BluetoothContinuation(
tag, tag,
cont, cont,
timeout,
initFn initFn
) )
@ -339,6 +372,17 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
} }
} }
/**
* Stop any current work
*/
private fun stopCurrentWork() {
activeTimeout?.let {
it.cancel()
activeTimeout = null
}
currentWork = null
}
/** /**
* Called from our big GATT callback, completes the current job and then schedules a new one * Called from our big GATT callback, completes the current job and then schedules a new one
*/ */
@ -352,7 +396,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val w = val w =
currentWork currentWork
?: throw Exception("currentWork was null") // will throw if null, which is helpful (FIXME - throws in the field) ?: throw Exception("currentWork was null") // will throw if null, which is helpful (FIXME - throws in the field)
currentWork = null // We are now no longer working on anything stopCurrentWork() // We are now no longer working on anything
startNewWork() startNewWork()
w w
@ -380,7 +424,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
it.completion.resumeWithException(ex) it.completion.resumeWithException(ex)
} }
workQueue.clear() workQueue.clear()
currentWork = null stopCurrentWork()
} }
} }
@ -481,10 +525,13 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
fun discoverServices() = makeSync<Unit> { queueDiscoverServices(it) } fun discoverServices() = makeSync<Unit> { queueDiscoverServices(it) }
/**
* mtu operations seem to hang sometimes. To cope with this we have a 5 second timeout before throwing an exception and cancelling the work
*/
private fun queueRequestMtu( private fun queueRequestMtu(
len: Int, len: Int,
cont: Continuation<Unit> cont: Continuation<Unit>
) = queueWork("reqMtu", cont) { gatt!!.requestMtu(len) } ) = queueWork("reqMtu", cont, 5 * 1000) { gatt!!.requestMtu(len) }
fun asyncRequestMtu( fun asyncRequestMtu(
len: Int, len: Int,