Some phones have buggy race conditions wrt finding services or characteristics

This workaround is: If we fail in that way during initial device connection
we disconnect() and try again 500 ms later.
1.2-legacy
geeksville 2020-04-24 15:22:54 -07:00
rodzic a3db3eca06
commit 3e89510f52
4 zmienionych plików z 94 dodań i 34 usunięć

Wyświetl plik

@ -0,0 +1,5 @@
package com.geeksville.mesh.service
import java.io.IOException
open class BLEException(msg: String) : IOException(msg)

Wyświetl plik

@ -34,9 +34,6 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
class RadioNotConnectedException(message: String = "Not connected to radio") : Exception(message)
private val errorHandler = CoroutineExceptionHandler { _, exception ->
Exceptions.report(exception, "MeshService-coroutine", "coroutine-exception")
}

Wyświetl plik

@ -12,6 +12,7 @@ import android.content.Intent
import android.os.IBinder
import android.os.RemoteException
import androidx.core.content.edit
import com.geeksville.analytics.DataPair
import com.geeksville.android.BinaryLogFile
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
@ -87,6 +88,11 @@ A variable keepAllPackets, if set to true will suppress this behavior and instea
*/
class RadioNotConnectedException(message: String = "Not connected to radio") :
BLEException(message)
/**
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
* just does bluetooth comms etc...
@ -287,7 +293,7 @@ class RadioInterfaceService : Service(), Logging {
if (!isConnected)
warn("Abandoning fromradio read because we are not connected")
else {
val fromRadio = service.getCharacteristic(BTM_FROMRADIO_CHARACTER)
val fromRadio = getCharacteristic(BTM_FROMRADIO_CHARACTER)
safe!!.asyncReadCharacteristic(fromRadio) {
val b = it.getOrThrow()
.value.clone() // We clone the array just in case, I'm not sure if they keep reusing the array
@ -379,6 +385,8 @@ class RadioInterfaceService : Service(), Logging {
}
}
// private var isFirstTime = true
private fun onConnect(connRes: Result<Unit>) {
// This callback is invoked after we are connected
@ -401,25 +409,52 @@ class RadioInterfaceService : Service(), Logging {
discRes.getOrThrow() // FIXME, instead just try to reconnect?
serviceScope.handledLaunch {
debug("Discovered services!")
delay(500) // android BLE is buggy and needs a 500ms sleep before calling getChracteristic, or you might get back null
try {
debug("Discovered services!")
delay(500) // android BLE is buggy and needs a 500ms sleep before calling getChracteristic, or you might get back null
isOldApi = service.getCharacteristic(BTM_RADIO_CHARACTER) != null
warn("Use oldAPI = $isOldApi")
// service could be null, test this by throwing BLEException and testing it on my machine
isOldApi = service.getCharacteristic(BTM_RADIO_CHARACTER) != null
warn("Use oldAPI = $isOldApi")
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)!!
/* if (isFirstTime) {
isFirstTime = false
throw BLEException("Faking a BLE failure")
} */
// We must set this to true before broadcasting connectionChanged
isConnected = true
fromNum = getCharacteristic(BTM_FROMNUM_CHARACTER)
// We treat the first send by a client as special
isFirstSend = true
// We must set this to true before broadcasting connectionChanged
isConnected = true
// Now tell clients they can (finally use the api)
broadcastConnectionChanged(true, isPermanent = false)
// We treat the first send by a client as special
isFirstSend = true
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio(true)
// Now tell clients they can (finally use the api)
broadcastConnectionChanged(true, isPermanent = false)
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio(true)
} catch (ex: BLEException) {
// Track how often in the field we need this hack
GeeksvilleApplication.analytics.track(
"ble_reconnect_hack",
DataPair(1)
)
errormsg(
"Unexpected error in initial device enumeration, forcing disconnect",
ex
)
warn("Forcing disconnect and hopefully device will comeback (disabling forced refresh)")
hasForcedRefresh = true
ignoreException {
safe!!.closeConnection()
}
delay(500) // Give some nasty time for buggy BLE stacks to shutdown
warn("Attempting reconnect")
startConnect()
}
}
}
}
@ -452,6 +487,17 @@ class RadioInterfaceService : Service(), Logging {
return binder;
}
/// Start a connection attempt
private fun startConnect() {
// we 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
safe!!.asyncConnect(true,
cb = ::onConnect,
lostConnectCb = { onDisconnect(isPermanent = false) })
}
/// Open or close a bluetooth connection to our device
private fun setEnabled(on: Boolean) {
if (on) {
@ -472,13 +518,7 @@ class RadioInterfaceService : Service(), Logging {
val s = SafeBluetooth(this, device)
safe = s
// 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
s.asyncConnect(true,
cb = ::onConnect,
lostConnectCb = { onDisconnect(isPermanent = false) })
startConnect()
} else {
errormsg("Bluetooth adapter not found, assuming running on the emulator!")
}
@ -516,7 +556,7 @@ class RadioInterfaceService : Service(), Logging {
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
val toRadio = service.getCharacteristic(uuid)
val toRadio = getCharacteristic(uuid)
toRadio.value = a
safe!!.writeCharacteristic(toRadio)
@ -524,6 +564,12 @@ class RadioInterfaceService : Service(), Logging {
}
}
/**
* Get a chracteristic, but in a safe manner because some buggy BLE implementations might return null
*/
private fun getCharacteristic(uuid: UUID) =
service.getCharacteristic(uuid) ?: throw BLEException("Can't get characteristic $uuid")
/**
* do an asynchronous write operation
* Any error responses will be ignored (other than log messages)
@ -536,7 +582,7 @@ class RadioInterfaceService : Service(), Logging {
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
val toRadio = service.getCharacteristic(uuid)
val toRadio = getCharacteristic(uuid)
toRadio.value = a
safe!!.asyncWriteCharacteristic(toRadio) {
@ -554,7 +600,7 @@ class RadioInterfaceService : Service(), Logging {
else {
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
val toRadio = service.getCharacteristic(uuid)
val toRadio = getCharacteristic(uuid)
var a = safe!!.readCharacteristic(toRadio)
.value.clone() // we copy the bluetooth array because it might still be in use
debug("Read of $uuid got ${a.size} bytes")

Wyświetl plik

@ -14,7 +14,6 @@ import com.geeksville.concurrent.Continuation
import com.geeksville.concurrent.SyncContinuation
import com.geeksville.util.exceptionReporter
import java.io.Closeable
import java.io.IOException
import java.util.*
@ -51,8 +50,6 @@ class BluetoothStateReceiver(val onChanged: (Boolean) -> Unit) : BroadcastReceiv
class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) :
Logging, Closeable {
class BLEException(msg: String) : IOException(msg)
/// Timeout before we declare a bluetooth operation failed
var timeoutMsec = 30 * 1000L
@ -331,7 +328,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val work =
synchronized(workQueue) {
val w =
currentWork!! // will throw if null, which is helpful (FIXME - throws in the field)
currentWork
?: 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
startNewWork()
@ -340,7 +338,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
if (status != 0)
work.completion.resumeWithException(BLEException("Bluetooth status=$status while doing ${work.tag}"))
work.completion.resumeWithException(
BLEException(
"Bluetooth status=$status while doing ${work.tag}"
)
)
else
work.completion.resume(Result.success(res) as Result<Nothing>)
}
@ -493,7 +495,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
) = queueWriteDescriptor(c, CallbackContinuation(cb))
private fun closeConnection() {
/**
* Close down any existing connection, any existing calls (including async connects will be
* cancelled and you'll need to recall connect to use this againt
*/
fun closeConnection() {
failAllWork(BLEException("Connection closing"))
if (gatt != null) {
@ -501,9 +507,14 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
gatt!!.disconnect()
gatt!!.close()
gatt = null
lostConnectCallback = null
connectionCallback = null
}
}
/**
* Close and destroy this SafeBluetooth instance. You'll need to make a new instance before using it again
*/
override fun close() {
closeConnection()
@ -546,7 +557,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
at android.os.Binder.execTransact(Binder.java:994)
*/
// per https://stackoverflow.com/questions/27068673/subscribe-to-a-ble-gatt-notification-android
val descriptor: BluetoothGattDescriptor = c.getDescriptor(configurationDescriptorUUID)!!
val descriptor: BluetoothGattDescriptor = c.getDescriptor(configurationDescriptorUUID)
?: throw BLEException("Notify descriptor not found for ${c.uuid}") // This can happen on buggy BLE implementations
descriptor.value =
if (enable) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
asyncWriteDescriptor(descriptor) {