diff --git a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt new file mode 100644 index 000000000..657aaf963 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt @@ -0,0 +1,5 @@ +package com.geeksville.mesh.service + +import java.io.IOException + +open class BLEException(msg: String) : IOException(msg) \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index ab9084852..5302a63cb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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") } diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index fedf8e873..d0866c224 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -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) { // 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") diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index 529c0f938..dba53be2f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -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) } @@ -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) {