sforkowany z mirror/meshtastic-android
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
rodzic
a3db3eca06
commit
3e89510f52
|
|
@ -0,0 +1,5 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
open class BLEException(msg: String) : IOException(msg)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue