2020-02-10 15:31:56 -08:00
package com.geeksville.mesh.service
2020-01-27 14:54:35 -08:00
import android.bluetooth.*
import android.content.Context
2020-04-10 18:04:39 -07:00
import android.os.Build
2020-06-15 07:09:21 -07:00
import android.os.DeadObjectException
2020-02-25 09:28:47 -08:00
import android.os.Handler
2020-09-25 19:44:04 -06:00
import android.os.Looper
2020-02-25 09:28:47 -08:00
import com.geeksville.android.GeeksvilleApplication
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-09-17 19:49:50 +02:00
import com.geeksville.mesh.android.bluetoothManager
2020-02-04 17:27:10 -08:00
import com.geeksville.util.exceptionReporter
2020-05-24 09:27:43 -07:00
import kotlinx.coroutines.*
2020-02-24 15:34:17 -08:00
import java.io.Closeable
2020-02-09 03:40:13 -08:00
import java.util.*
2020-01-27 14:54:35 -08:00
2020-02-24 15:34:17 -08:00
/// Return a standard BLE 128 bit UUID from the short 16 bit versions
2020-06-18 15:47:36 -07:00
fun longBLEUUID ( hexFour : String ) : UUID = UUID . fromString ( " 0000 $hexFour -0000-1000-8000-00805f9b34fb " )
2020-02-24 15:34:17 -08:00
2020-04-20 09:56:38 -07:00
2020-01-27 14:54:35 -08:00
/ * *
* 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 ) :
2020-02-24 15:34:17 -08:00
Logging , Closeable {
2020-01-27 14:54:35 -08:00
2020-08-30 11:39:26 -07:00
/// Timeout before we declare a bluetooth operation failed (used for synchronous API operations only)
var timeoutMsec = 20 * 1000L
2020-01-27 14:54:35 -08:00
/// Users can access the GATT directly as needed
2020-06-25 17:56:31 -07:00
@Volatile
2020-02-04 17:27:10 -08:00
var gatt : BluetoothGatt ? = null
2020-01-27 14:54:35 -08:00
2020-06-11 16:22:20 -07:00
@Volatile
2020-01-27 14:54:35 -08:00
var state = BluetoothProfile . STATE _DISCONNECTED
2020-06-11 16:22:20 -07:00
@Volatile
2020-01-27 14:54:35 -08:00
private var currentWork : BluetoothContinuation ? = null
private val workQueue = mutableListOf < BluetoothContinuation > ( )
2020-02-04 20:11:05 -08:00
// Called for reconnection attemps
2020-06-11 16:22:20 -07:00
@Volatile
2020-02-04 20:11:05 -08:00
private var connectionCallback : ( ( Result < Unit > ) -> Unit ) ? = null
2020-06-11 16:22:20 -07:00
@Volatile
2020-02-04 20:11:05 -08:00
private var lostConnectCallback : ( ( ) -> Unit ) ? = null
2020-02-09 03:40:13 -08:00
/// from characteristic UUIDs to the handler function for notfies
private val notifyHandlers = mutableMapOf < UUID , ( BluetoothGattCharacteristic ) -> Unit > ( )
2020-05-24 09:27:43 -07:00
private val serviceScope = CoroutineScope ( Dispatchers . IO )
2020-04-24 15:49:34 -07:00
/ * *
* A BLE status code based error
* /
class BLEStatusException ( val status : Int , msg : String ) : BLEException ( msg )
2020-02-09 03:40:13 -08:00
// 0x2902 org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
private val configurationDescriptorUUID =
2020-02-24 15:34:17 -08:00
longBLEUUID ( " 2902 " )
2020-02-09 03:40:13 -08:00
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-05-24 09:27:43 -07:00
val timeoutMillis : Long = 0 , // If we want to timeout this operation at a certain time, use a non zero value
private val startWorkFn : ( ) -> Boolean
2020-01-27 18:47:13 -08:00
) : 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-06-11 16:22:20 -07:00
override fun toString ( ) : String {
2020-07-02 09:38:08 -07:00
return " Work: $tag "
2020-06-11 16:22:20 -07:00
}
2020-07-02 09:38:08 -07:00
/// Connection work items are treated specially
fun isConnect ( ) = tag == " connect " || tag == " reconnect "
2020-01-27 14:54:35 -08:00
}
2020-02-25 09:28:47 -08:00
/ * *
* skanky hack to restart BLE if it says it is hosed
* https : //stackoverflow.com/questions/35103701/ble-android-onconnectionstatechange-not-being-called
* /
2020-09-25 19:44:04 -06:00
private val mHandler : Handler = Handler ( Looper . getMainLooper ( ) )
2020-02-25 09:28:47 -08:00
fun restartBle ( ) {
GeeksvilleApplication . analytics . track ( " ble_restart " ) // record # of times we needed to use this nasty hack
2020-02-29 13:21:05 -08:00
errormsg ( " Doing emergency BLE restart " )
2020-09-17 19:49:50 +02:00
context . bluetoothManager ?. adapter ?. let { adp ->
2020-02-25 09:28:47 -08:00
if ( adp . isEnabled ) {
adp . disable ( )
// TODO: display some kind of UI about restarting BLE
mHandler . postDelayed ( object : Runnable {
override fun run ( ) {
if ( ! adp . isEnabled ) {
adp . enable ( )
} else {
mHandler . postDelayed ( this , 2500 )
}
}
} , 2500 )
}
}
}
2020-05-24 09:27:43 -07:00
// Our own custom BLE status codes
private val STATUS _RELIABLE _WRITE _FAILED = 4403
2020-05-24 10:12:12 -07:00
private val STATUS _TIMEOUT = 4404
2020-06-30 12:35:58 -07:00
private val STATUS _NOSTART = 4405
2020-07-02 09:38:08 -07:00
private val STATUS _SIMFAILURE = 4406
2020-05-24 09:27:43 -07:00
2020-07-04 17:37:52 -07:00
/ * *
* Should we automatically try to reconnect when we lose our connection ?
*
* Originally this was true , but over time ( now that clients are smarter and need to build
* up more state ) I see this was a mistake . Now if the connection drops we just call
* the lostConnection callback and the client of this API is responsible for reconnecting .
* This also prevents nasty races when sometimes both the upperlayer and this layer decide to reconnect
* simultaneously .
* /
private val autoReconnect = false
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
2020-02-04 20:11:05 -08:00
) = exceptionReporter {
2020-06-09 12:18:35 -07:00
info ( " new bluetooth connection state $newState , status $status " )
when ( newState ) {
BluetoothProfile . STATE _CONNECTED -> {
state =
newState // we only care about connected/disconnected - not the transitional states
// If autoconnect is on and this connect attempt failed, hopefully some future attempt will succeed
if ( status != BluetoothGatt . GATT _SUCCESS && autoConnect ) {
errormsg ( " Connect attempt failed $status , not calling connect completion handler... " )
} else
completeWork ( status , Unit )
}
BluetoothProfile . STATE _DISCONNECTED -> {
2020-06-20 14:50:15 -07:00
if ( gatt == null ) {
2020-06-28 17:45:23 -07:00
errormsg ( " No gatt: ignoring connection state $newState , status $status " )
} else if ( isClosing ) {
info ( " Got disconnect because we are shutting down, closing gatt " )
gatt = null
2020-06-20 14:50:15 -07:00
g . close ( ) // Finish closing our gatt here
} else {
2020-05-24 11:38:33 -07:00
// cancel any queued ops if we were already connected
val oldstate = state
state = newState
if ( oldstate == BluetoothProfile . STATE _CONNECTED ) {
2020-06-11 17:34:22 -07:00
info ( " Lost connection - aborting current work: $currentWork " )
2020-05-24 11:38:33 -07:00
2020-07-02 09:38:08 -07:00
// If we get a disconnect, just try again otherwise fail all current operations
2020-07-02 14:08:12 -07:00
// Note: if no work is pending (likely) we also just totally teardown and restart the connection, because we won't be
// throwing a lost connection exception to any worker.
2020-07-04 17:37:52 -07:00
if ( autoReconnect && ( currentWork == null || currentWork ?. isConnect ( ) == true ) )
2020-07-02 09:38:08 -07:00
dropAndReconnect ( )
else
lostConnection ( " lost connection " )
2020-06-04 09:58:06 -07:00
} else if ( status == 133 ) {
// We were not previously connected and we just failed with our non-auto connection attempt. Therefore we now need
// to do an autoconnection attempt. When that attempt succeeds/fails the normal callbacks will be called
// Note: To workaround https://issuetracker.google.com/issues/36995652
// Always call BluetoothDevice#connectGatt() with autoConnect=false
// (the race condition does not affect that case). If that connection times out
// you will get a callback with status=133. Then call BluetoothGatt#connect()
// to initiate a background connection.
if ( autoConnect ) {
warn ( " Failed on non-auto connect, falling back to auto connect attempt " )
2020-06-11 17:34:22 -07:00
closeGatt ( ) // Close the old non-auto connection
2020-06-04 09:58:06 -07:00
lowLevelConnect ( true )
}
2020-05-24 11:38:33 -07:00
}
2020-02-25 09:28:47 -08:00
2020-05-24 11:38:33 -07:00
if ( status == 257 ) { // mystery error code when phone is hung
//throw Exception("Mystery bluetooth failure - debug me")
restartBle ( )
}
2020-02-25 09:28:47 -08:00
}
2020-01-27 14:54:35 -08:00
}
}
}
override fun onServicesDiscovered ( gatt : BluetoothGatt , status : Int ) {
2020-04-20 10:15:22 -07:00
// For testing lie and claim failure
2020-01-27 14:54:35 -08:00
completeWork ( status , Unit )
}
override fun onCharacteristicRead (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic ,
status : Int
) {
completeWork ( status , characteristic )
}
2020-05-15 10:18:15 -07:00
override fun onReliableWriteCompleted ( gatt : BluetoothGatt , status : Int ) {
completeWork ( status , Unit )
}
2020-01-27 14:54:35 -08:00
override fun onCharacteristicWrite (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic ,
status : Int
) {
2020-05-15 10:18:15 -07:00
val reliable = currentReliableWrite
if ( reliable != null )
if ( ! characteristic . value . contentEquals ( reliable ) ) {
errormsg ( " A reliable write failed! " )
gatt . abortReliableWrite ( ) ;
2020-05-24 09:27:43 -07:00
completeWork (
STATUS _RELIABLE _WRITE _FAILED ,
characteristic
) // skanky code to indicate failure
2020-05-15 10:18:15 -07:00
} else {
logAssert ( gatt . executeReliableWrite ( ) )
// After this execute reliable completes - we can continue with normal operations (see onReliableWriteCompleted)
}
else // Just a standard write - do the normal flow
completeWork ( status , characteristic )
2020-01-27 14:54:35 -08:00
}
override fun onMtuChanged ( gatt : BluetoothGatt , mtu : Int , status : Int ) {
2020-02-24 15:47:53 -08:00
// Alas, passing back an Int mtu isn't working and since I don't really care what MTU
// the device was willing to let us have I'm just punting and returning Unit
2020-05-24 11:01:13 -07:00
if ( isSettingMtu )
completeWork ( status , Unit )
else
errormsg ( " Ignoring bogus onMtuChanged " )
2020-01-27 14:54:35 -08:00
}
2020-02-09 03:40:13 -08:00
/ * *
* Callback triggered as a result of a remote characteristic notification .
*
* @param gatt GATT client the characteristic is associated with
* @param characteristic Characteristic that has been updated as a result of a remote
* notification event .
* /
override fun onCharacteristicChanged (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic
) {
val handler = notifyHandlers . get ( characteristic . uuid )
if ( handler == null )
warn ( " Received notification from $characteristic , but no handler registered " )
else {
exceptionReporter {
handler ( characteristic )
}
}
}
/ * *
* Callback indicating the result of a descriptor write operation .
*
* @param gatt GATT client invoked [ BluetoothGatt . writeDescriptor ]
* @param descriptor Descriptor that was writte to the associated remote device .
* @param status The result of the write operation [ BluetoothGatt . GATT _SUCCESS ] if the
* operation succeeds .
* /
override fun onDescriptorWrite (
gatt : BluetoothGatt ,
descriptor : BluetoothGattDescriptor ,
status : Int
) {
completeWork ( status , descriptor )
}
/ * *
* Callback reporting the result of a descriptor read operation .
*
* @param gatt GATT client invoked [ BluetoothGatt . readDescriptor ]
* @param descriptor Descriptor that was read from the associated remote device .
* @param status [ BluetoothGatt . GATT _SUCCESS ] if the read operation was completed
* successfully
* /
override fun onDescriptorRead (
gatt : BluetoothGatt ,
descriptor : BluetoothGattDescriptor ,
status : Int
) {
completeWork ( status , descriptor )
}
2020-01-27 14:54:35 -08:00
}
2020-07-02 09:38:08 -07:00
// To test loss of BLE faults we can randomly fail a certain % of all work items. We
// skip this for "connect" items because the handling for connection failure is special
var simFailures = false
var failPercent =
10 // 15% failure is unusably high because of constant reconnects, 7% somewhat usable, 10% pretty bad
private val failRandom = Random ( )
2020-01-27 14:54:35 -08:00
2020-05-24 09:27:43 -07:00
private var activeTimeout : Job ? = null
2020-01-27 14:54:35 -08:00
/// 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-05-24 09:27:43 -07:00
if ( newWork . timeoutMillis != 0L ) {
activeTimeout = serviceScope . launch {
2021-03-23 13:05:50 +08:00
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
2020-05-24 09:27:43 -07:00
delay ( newWork . timeoutMillis )
errormsg ( " Failsafe BLE timer expired! " )
completeWork (
STATUS _TIMEOUT ,
Unit
) // Throw an exception in that work
}
}
2020-05-24 11:01:13 -07:00
isSettingMtu =
false // Most work is not doing MTU stuff, the work that is will re set this flag
2020-07-02 09:38:08 -07:00
val failThis =
simFailures && ! newWork . isConnect ( ) && failRandom . nextInt ( 100 ) < failPercent
if ( failThis ) {
errormsg ( " Simulating random work failure! " )
completeWork ( STATUS _SIMFAILURE , Unit )
} else {
val started = newWork . startWork ( )
if ( ! started ) {
errormsg ( " Failed to start work, returned error status " )
completeWork (
STATUS _NOSTART ,
Unit
) // abandon the current attempt and try for another
}
2020-06-30 12:35:58 -07:00
}
2020-01-27 14:54:35 -08:00
}
}
2020-05-24 09:27:43 -07:00
private fun < T > queueWork (
tag : String ,
cont : Continuation < T > ,
2020-08-30 11:39:26 -07:00
timeout : Long ,
2020-05-24 09:27:43 -07:00
initFn : ( ) -> Boolean
) {
2020-02-10 15:31:56 -08:00
val btCont =
BluetoothContinuation (
tag ,
cont ,
2020-05-24 09:27:43 -07:00
timeout ,
2020-02-10 15:31:56 -08:00
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 ( )
}
}
2020-05-24 09:27:43 -07:00
/ * *
* Stop any current work
* /
private fun stopCurrentWork ( ) {
activeTimeout ?. let {
it . cancel ( )
activeTimeout = null
}
currentWork = null
}
2020-01-27 14:54:35 -08:00
/ * *
* 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 ) {
2020-02-17 18:46:20 -08:00
exceptionReporter {
// We might unexpectedly fail inside here, but we don't want to pass that exception back up to the bluetooth GATT layer
2020-01-27 14:54:35 -08:00
2020-02-17 18:46:20 -08:00
// startup next job in queue before calling the completion handler
val work =
synchronized ( workQueue ) {
2020-07-04 11:17:11 -07:00
val w = currentWork
2020-01-27 14:54:35 -08:00
2020-07-04 11:17:11 -07:00
if ( w != null ) {
stopCurrentWork ( ) // We are now no longer working on anything
startNewWork ( )
}
2020-02-17 18:46:20 -08:00
w
}
2020-01-27 14:54:35 -08:00
2020-07-04 11:17:11 -07:00
if ( work == null )
warn ( " wor completed, but we already killed it via failsafetimer? status= $status , res= $res " )
else {
2021-03-23 13:05:50 +08:00
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
2020-07-04 11:17:11 -07:00
if ( status != 0 )
work . completion . resumeWithException (
BLEStatusException (
status ,
" Bluetooth status= $status while doing ${work.tag} "
)
2020-04-24 15:22:54 -07:00
)
2020-07-04 11:17:11 -07:00
else
work . completion . resume ( Result . success ( res ) as Result < Nothing > )
}
2020-02-17 18:46:20 -08:00
}
2020-01-27 14:54:35 -08:00
}
/ * *
* Something went wrong , abort all queued
* /
private fun failAllWork ( ex : Exception ) {
synchronized ( workQueue ) {
2020-06-30 12:18:49 -07:00
warn ( " Failing ${workQueue.size} works, because ${ex.message} " )
2020-01-27 14:54:35 -08:00
workQueue . forEach {
2020-07-04 11:33:24 -07:00
try {
it . completion . resumeWithException ( ex )
} catch ( ex : Exception ) {
errormsg (
" Mystery exception, why were we informed about our own exceptions? " ,
ex
)
}
2020-01-27 14:54:35 -08:00
}
workQueue . clear ( )
2020-05-24 09:27:43 -07:00
stopCurrentWork ( )
2020-01-27 14:54:35 -08:00
}
}
/// 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 )
2020-08-30 11:39:26 -07:00
return cont . await ( ) // was timeoutMsec but we now do the timeout at the lower BLE level
2020-01-27 14:54:35 -08:00
}
2020-04-20 11:01:27 -07:00
// Is the gatt trying to repeatedly connect as needed?
private var autoConnect = false
2020-06-07 18:05:18 -07:00
/// True if the current active connection is auto (possible for this to be false but autoConnect to be true
/// if we are in the first non-automated lowLevel connect.
private var currentConnectIsAuto = false
2020-06-04 09:58:06 -07:00
private fun lowLevelConnect ( autoNow : Boolean ) : BluetoothGatt ? {
2020-06-07 18:05:18 -07:00
currentConnectIsAuto = autoNow
2020-06-11 17:34:22 -07:00
logAssert ( gatt == null )
2020-06-07 18:05:18 -07:00
2020-06-04 09:58:06 -07:00
val g = if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . M ) {
device . connectGatt (
context ,
autoNow ,
gattCallback ,
BluetoothDevice . TRANSPORT _LE
)
} else {
device . connectGatt (
context ,
autoNow ,
gattCallback
)
}
gatt = g
return g
}
2020-01-27 14:54:35 -08:00
// 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.
2020-06-04 09:35:25 -07:00
private fun queueConnect (
autoConnect : Boolean = false ,
2020-08-30 11:39:26 -07:00
cont : Continuation < Unit > ,
timeout : Long = 0
2020-06-04 09:35:25 -07:00
) {
2020-04-20 11:01:27 -07:00
this . autoConnect = autoConnect
2020-03-02 08:05:43 -08:00
// assert(gatt == null) this now might be !null with our new reconnect support
2020-08-30 11:39:26 -07:00
queueWork ( " connect " , cont , timeout ) {
2020-06-04 09:58:06 -07:00
2020-06-04 09:35:25 -07:00
// Note: To workaround https://issuetracker.google.com/issues/36995652
// Always call BluetoothDevice#connectGatt() with autoConnect=false
// (the race condition does not affect that case). If that connection times out
// you will get a callback with status=133. Then call BluetoothGatt#connect()
// to initiate a background connection.
2020-06-04 09:58:06 -07:00
val g = lowLevelConnect ( false )
2020-01-27 14:54:35 -08:00
g != null
}
}
2020-02-04 20:11:05 -08:00
/ * *
* start a connection attempt .
*
* Note : if autoConnect is true , the callback you provide will be kept around _even after the connection is complete .
* If we ever lose the connection , this class will immediately requque the attempt ( after canceling
* any outstanding queued operations ) .
*
* So you should expect your callback might be called multiple times , each time to reestablish a new connection .
* /
fun asyncConnect (
autoConnect : Boolean = false ,
cb : ( Result < Unit > ) -> Unit ,
lostConnectCb : ( ) -> Unit
) {
2020-04-26 13:03:35 -07:00
logAssert ( workQueue . isEmpty ( ) )
2020-06-11 16:22:20 -07:00
if ( currentWork != null )
throw AssertionError ( " currentWork was not null: $currentWork " )
2020-05-10 21:39:49 -07:00
2020-02-04 20:11:05 -08:00
lostConnectCallback = lostConnectCb
connectionCallback = if ( autoConnect )
cb
else
null
2020-06-04 09:58:06 -07:00
queueConnect ( autoConnect , CallbackContinuation ( cb ) )
2020-01-27 14:54:35 -08:00
}
2020-06-20 14:50:15 -07:00
/// Restart any previous connect attempts
2020-02-25 09:28:47 -08:00
private fun reconnect ( ) {
2020-06-20 14:50:15 -07:00
// closeGatt() // Get rid of any old gatt
connectionCallback ?. let { cb ->
queueConnect ( true , CallbackContinuation ( cb ) )
}
}
2020-07-02 08:46:25 -07:00
private fun lostConnection ( reason : String ) {
2020-06-18 15:40:14 -07:00
/ *
Supposedly this reconnect attempt happens automatically
" If the connection was established through an auto connect, Android will
automatically try to reconnect to the remote device when it gets disconnected
until you manually call disconnect ( ) or close ( ) . Once a connection established
through direct connect disconnects , no attempt is made to reconnect to the remote device . "
https : //stackoverflow.com/questions/37965337/what-exactly-does-androids-bluetooth-autoconnect-parameter-do?rq=1
closeConnection ( )
* /
2020-07-02 08:46:25 -07:00
failAllWork ( BLEException ( reason ) )
2020-06-18 15:40:14 -07:00
// Cancel any notifications - because when the device comes back it might have forgotten about us
notifyHandlers . clear ( )
lostConnectCallback ?. let {
debug ( " calling lostConnect handler " )
it . invoke ( )
}
2020-07-02 08:46:25 -07:00
}
/// Drop our current connection and then requeue a connect as needed
private fun dropAndReconnect ( ) {
lostConnection ( " lost connection, reconnecting " )
2020-06-18 15:40:14 -07:00
// Queue a new connection attempt
val cb = connectionCallback
if ( cb != null ) {
debug ( " queuing a reconnection callback " )
assert ( currentWork == null )
if ( ! currentConnectIsAuto ) { // we must have been running during that 1-time manual connect, switch to auto-mode from now on
closeGatt ( ) // Close the old non-auto connection
lowLevelConnect ( true )
}
// note - we don't need an init fn (because that would normally redo the connectGatt call - which we don't need)
2020-08-30 11:39:26 -07:00
queueWork ( " reconnect " , CallbackContinuation ( cb ) , 0 ) { -> true }
2020-06-18 15:40:14 -07:00
} else {
debug ( " No connectionCallback registered " )
2020-02-25 09:28:47 -08:00
}
}
2020-06-04 09:35:25 -07:00
fun connect ( autoConnect : Boolean = false ) =
2020-06-04 09:58:06 -07:00
makeSync < Unit > { queueConnect ( autoConnect , it ) }
2020-01-27 14:54:35 -08:00
private fun queueReadCharacteristic (
c : BluetoothGattCharacteristic ,
2020-08-30 11:39:26 -07:00
cont : Continuation < BluetoothGattCharacteristic > , timeout : Long = 0
) = queueWork ( " readC ${c.uuid} " , cont , timeout ) { gatt !! . readCharacteristic ( c ) }
2020-01-27 14:54:35 -08:00
fun asyncReadCharacteristic (
c : BluetoothGattCharacteristic ,
cb : ( Result < BluetoothGattCharacteristic > ) -> Unit
) = queueReadCharacteristic ( c , CallbackContinuation ( cb ) )
2020-08-30 11:39:26 -07:00
fun readCharacteristic (
c : BluetoothGattCharacteristic ,
timeout : Long = timeoutMsec
) : BluetoothGattCharacteristic =
makeSync { queueReadCharacteristic ( c , it , timeout ) }
2020-01-27 14:54:35 -08:00
2020-08-30 11:39:26 -07:00
private fun queueDiscoverServices ( cont : Continuation < Unit > , timeout : Long = 0 ) {
queueWork ( " discover " , cont , timeout ) {
2020-08-30 12:01:38 -07:00
gatt ?. discoverServices ( )
?: false // throw BLEException("GATT is null") - if we return false here it is probably because the device is being torn down
2020-01-27 14:54:35 -08:00
}
}
fun asyncDiscoverServices ( cb : ( Result < Unit > ) -> Unit ) {
queueDiscoverServices ( CallbackContinuation ( cb ) )
}
fun discoverServices ( ) = makeSync < Unit > { queueDiscoverServices ( it ) }
2020-05-24 11:01:13 -07:00
/ * *
* On some phones we receive bogus mtu gatt callbacks , we need to ignore them if we weren ' t setting the mtu
* /
private var isSettingMtu = false
2020-05-24 09:27:43 -07:00
/ * *
* mtu operations seem to hang sometimes . To cope with this we have a 5 second timeout before throwing an exception and cancelling the work
* /
2020-01-27 14:54:35 -08:00
private fun queueRequestMtu (
len : Int ,
2020-02-24 15:47:53 -08:00
cont : Continuation < Unit >
2020-07-04 11:17:11 -07:00
) = queueWork ( " reqMtu " , cont , 10 * 1000 ) {
2020-05-24 11:01:13 -07:00
isSettingMtu = true
2020-06-17 14:02:58 -07:00
gatt ?. requestMtu ( len ) ?: false
2020-05-24 11:01:13 -07:00
}
2020-01-27 14:54:35 -08:00
fun asyncRequestMtu (
len : Int ,
2020-02-24 15:47:53 -08:00
cb : ( Result < Unit > ) -> Unit
2020-01-27 18:47:13 -08:00
) {
queueRequestMtu ( len , CallbackContinuation ( cb ) )
}
2020-01-27 14:54:35 -08:00
2020-02-24 15:47:53 -08:00
fun requestMtu ( len : Int ) : Unit = makeSync { queueRequestMtu ( len , it ) }
2020-01-27 14:54:35 -08:00
2020-05-15 10:18:15 -07:00
private var currentReliableWrite : ByteArray ? = null
2020-01-27 14:54:35 -08:00
private fun queueWriteCharacteristic (
c : BluetoothGattCharacteristic ,
2020-06-13 16:21:26 -07:00
v : ByteArray ,
2020-08-30 11:39:26 -07:00
cont : Continuation < BluetoothGattCharacteristic > , timeout : Long = 0
) = queueWork ( " writeC ${c.uuid} " , cont , timeout ) {
2020-05-15 10:18:15 -07:00
currentReliableWrite = null
2020-06-13 16:21:26 -07:00
c . value = v
2020-06-17 14:02:58 -07:00
gatt ?. writeCharacteristic ( c ) ?: false
2020-05-15 10:18:15 -07:00
}
2020-01-27 14:54:35 -08:00
fun asyncWriteCharacteristic (
c : BluetoothGattCharacteristic ,
2020-06-13 16:21:26 -07:00
v : ByteArray ,
2020-01-27 14:54:35 -08:00
cb : ( Result < BluetoothGattCharacteristic > ) -> Unit
2020-06-13 16:21:26 -07:00
) = queueWriteCharacteristic ( c , v , CallbackContinuation ( cb ) )
2020-01-27 14:54:35 -08:00
2020-06-13 16:21:26 -07:00
fun writeCharacteristic (
c : BluetoothGattCharacteristic ,
2020-08-30 11:39:26 -07:00
v : ByteArray ,
timeout : Long = timeoutMsec
2020-06-13 16:21:26 -07:00
) : BluetoothGattCharacteristic =
2020-08-30 11:39:26 -07:00
makeSync { queueWriteCharacteristic ( c , v , it , timeout ) }
2020-01-27 14:54:35 -08:00
2020-05-15 10:18:15 -07:00
/ * * Like write , but we use the extra reliable flow documented here :
* https : //stackoverflow.com/questions/24485536/what-is-reliable-write-in-ble
* /
private fun queueWriteReliable (
c : BluetoothGattCharacteristic ,
2020-08-30 11:39:26 -07:00
cont : Continuation < Unit > , timeout : Long = 0
) = queueWork ( " rwriteC ${c.uuid} " , cont , timeout ) {
2020-05-15 10:18:15 -07:00
logAssert ( gatt !! . beginReliableWrite ( ) )
currentReliableWrite = c . value . clone ( )
2020-06-17 14:02:58 -07:00
gatt ?. writeCharacteristic ( c ) ?: false
2020-05-15 10:18:15 -07:00
}
/ * fun asyncWriteReliable (
c : BluetoothGattCharacteristic ,
cb : ( Result < Unit > ) -> Unit
) = queueWriteCharacteristic ( c , CallbackContinuation ( cb ) ) * /
fun writeReliable ( c : BluetoothGattCharacteristic ) : Unit =
makeSync { queueWriteReliable ( c , it ) }
2020-02-09 03:40:13 -08:00
private fun queueWriteDescriptor (
c : BluetoothGattDescriptor ,
2020-08-30 11:39:26 -07:00
cont : Continuation < BluetoothGattDescriptor > , timeout : Long = 0
) = queueWork ( " writeD " , cont , timeout ) { gatt ?. writeDescriptor ( c ) ?: false }
2020-02-09 03:40:13 -08:00
fun asyncWriteDescriptor (
c : BluetoothGattDescriptor ,
cb : ( Result < BluetoothGattDescriptor > ) -> Unit
) = queueWriteDescriptor ( c , CallbackContinuation ( cb ) )
2020-06-28 17:45:23 -07:00
/ * *
* Some old androids have a bug where calling disconnect doesn ' t guarantee that the onConnectionStateChange callback gets called
* but the only safe way to call gatt . close is from that callback . So we set a flag once we start closing and then poll
* until we see the callback has set gatt to null ( indicating the CALLBACK has close the gatt ) . If the timeout expires we assume the bug
* has occurred , and we manually close the gatt here .
*
* Log of typical failure
* 06 - 29 08 : 47 : 15.035 29788 - 30155 / com . geeksville . mesh D / BluetoothGatt : cancelOpen ( ) - device : 24 : 62 : AB : F8 : 40 : 9 A
06 - 29 08 : 47 : 15.036 29788 - 30155 / com . geeksville . mesh D / BluetoothGatt : close ( )
06 - 29 08 : 47 : 15.037 29788 - 30155 / com . geeksville . mesh D / BluetoothGatt : unregisterApp ( ) - mClientIf = 5
06 - 29 08 : 47 : 15.037 29788 - 29813 / com . geeksville . mesh D / BluetoothGatt : onClientConnectionState ( ) - status = 0 clientIf = 5 device = 24 : 62 : AB : F8 : 40 : 9 A
06 - 29 08 : 47 : 15.037 29788 - 29813 / com . geeksville . mesh W / BluetoothGatt : Unhandled exception in callback
java . lang . NullPointerException : Attempt to invoke virtual method ' void android . bluetooth . BluetoothGattCallback . onConnectionStateChange ( android . bluetooth . BluetoothGatt , int , int ) ' on a null object reference
at android . bluetooth . BluetoothGatt $ 1. onClientConnectionState ( BluetoothGatt . java : 182 )
at android . bluetooth . IBluetoothGattCallback $ Stub . onTransact ( IBluetoothGattCallback . java : 70 )
at android . os . Binder . execTransact ( Binder . java : 446 )
*
* per https : //github.com/don/cordova-plugin-ble-central/issues/473#issuecomment-367687575
* /
@Volatile
private var isClosing = false
2020-02-09 03:40:13 -08:00
2020-06-11 17:34:22 -07:00
/** Close just the GATT device but keep our pending callbacks active */
fun closeGatt ( ) {
gatt ?. let { g ->
info ( " Closing our GATT connection " )
2020-06-28 17:45:23 -07:00
isClosing = true
2020-06-15 07:09:21 -07:00
try {
g . disconnect ( )
2020-06-28 17:45:23 -07:00
// Wait for our callback to run and handle hte disconnect
var msecsLeft = 1000
while ( gatt != null && msecsLeft >= 0 ) {
Thread . sleep ( 100 )
msecsLeft -= 100
}
2020-06-30 11:39:04 -07:00
gatt ?. let { g2 ->
2020-06-28 17:45:23 -07:00
warn ( " Android onConnectionStateChange did not run, manually closing " )
2020-06-30 11:39:04 -07:00
gatt =
null // clear gat before calling close, bcause close might throw dead object exception
g2 . close ( )
2020-06-28 17:45:23 -07:00
}
2020-07-18 13:18:38 -07:00
} catch ( ex : NullPointerException ) {
// Attempt to invoke virtual method 'com.android.bluetooth.gatt.AdvertiseClient com.android.bluetooth.gatt.AdvertiseManager.getAdvertiseClient(int)' on a null object reference
//com.geeksville.mesh.service.SafeBluetooth.closeGatt
warn ( " Ignoring NPE in close - probably buggy Samsung BLE " )
2020-06-15 07:09:21 -07:00
} catch ( ex : DeadObjectException ) {
2020-06-28 17:45:23 -07:00
warn ( " Ignoring dead object exception, probably bluetooth was just disabled " )
} finally {
isClosing = false
2020-06-15 07:09:21 -07:00
}
2020-06-11 17:34:22 -07:00
}
}
2020-04-24 15:22:54 -07:00
/ * *
* 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 ( ) {
2020-05-10 21:39:49 -07:00
// Set these to null _before_ calling gatt.disconnect(), because we don't want the old lostConnectCallback to get called
lostConnectCallback = null
connectionCallback = null
2020-06-10 12:03:31 -07:00
// Cancel any notifications - because when the device comes back it might have forgotten about us
notifyHandlers . clear ( )
2020-08-30 11:39:26 -07:00
2020-06-11 17:34:22 -07:00
closeGatt ( )
2021-03-23 13:17:36 +08:00
failAllWork ( BLEConnectionClosing ( ) )
2020-02-04 20:11:05 -08:00
}
2020-04-24 15:22:54 -07:00
/ * *
* Close and destroy this SafeBluetooth instance . You ' ll need to make a new instance before using it again
* /
2020-02-24 15:34:17 -08:00
override fun close ( ) {
2020-02-04 20:11:05 -08:00
closeConnection ( )
2020-02-04 17:27:10 -08:00
2020-06-20 14:50:15 -07:00
// context.unregisterReceiver(btStateReceiver)
2020-01-27 14:54:35 -08:00
}
2020-02-09 03:40:13 -08:00
/// asyncronously turn notification on/off for a characteristic
fun setNotify (
c : BluetoothGattCharacteristic ,
enable : Boolean ,
onChanged : ( BluetoothGattCharacteristic ) -> Unit
) {
debug ( " starting setNotify( ${c.uuid} , $enable ) " )
notifyHandlers [ c . uuid ] = onChanged
// c.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt !! . setCharacteristicNotification ( c , enable )
2020-07-20 12:07:55 -07:00
2020-02-09 03:40:13 -08:00
// per https://stackoverflow.com/questions/27068673/subscribe-to-a-ble-gatt-notification-android
2020-04-24 15:22:54 -07:00
val descriptor : BluetoothGattDescriptor = c . getDescriptor ( configurationDescriptorUUID )
?: throw BLEException ( " Notify descriptor not found for ${c.uuid} " ) // This can happen on buggy BLE implementations
2020-02-09 03:40:13 -08:00
descriptor . value =
if ( enable ) BluetoothGattDescriptor . ENABLE _NOTIFICATION _VALUE else BluetoothGattDescriptor . DISABLE _NOTIFICATION _VALUE
asyncWriteDescriptor ( descriptor ) {
debug ( " Notify enable= $enable completed " )
}
}
2020-07-18 13:18:38 -07:00
}