2020-02-10 15:31:56 -08:00
package com.geeksville.mesh.service
2020-01-24 17:05:55 -08:00
2020-01-27 14:54:35 -08:00
import android.bluetooth.BluetoothAdapter
2020-01-27 16:00:00 -08:00
import android.bluetooth.BluetoothGattCharacteristic
2020-04-19 19:23:20 -07:00
import android.bluetooth.BluetoothGattService
2020-01-27 14:54:35 -08:00
import android.bluetooth.BluetoothManager
2020-01-24 17:05:55 -08:00
import android.content.Context
import com.geeksville.android.Logging
2020-05-14 11:47:24 -07:00
import com.geeksville.concurrent.handledLaunch
2020-05-30 14:38:16 -07:00
import com.geeksville.util.anonymize
2020-03-30 16:44:48 -07:00
import com.geeksville.util.exceptionReporter
2020-04-19 18:12:11 -07:00
import com.geeksville.util.ignoreException
2020-07-01 16:31:23 -07:00
import kotlinx.coroutines.CancellationException
2020-06-11 17:34:22 -07:00
import kotlinx.coroutines.Job
2020-04-13 16:28:32 -07:00
import kotlinx.coroutines.delay
2020-03-30 16:44:48 -07:00
import java.lang.reflect.Method
2020-01-27 14:54:35 -08:00
import java.util.*
2020-02-09 03:40:13 -08:00
2020-01-27 14:54:35 -08:00
/ * Info for the esp32 device side code . See that source for the ' gold ' standard docs on this interface .
MeshBluetoothService UUID 6 ba1b218 - 15 a8 - 461f - 9f a8 - 5 dcae273eafd
FIXME - notify vs indication for fromradio output . Using notify for now , not sure if that is best
FIXME - in the esp32 mesh management code , occasionally mirror the current net db to flash , so that if we reboot we still have a good guess of users who are out there .
FIXME - make sure this protocol is guaranteed robust and won ' t drop packets
" According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)).
In BLE 4.1 the ATT _MTU is 23 bytes ( 20 bytes for payload ) , but in BLE 4.2 the ATT _MTU can be negotiated up to 247 bytes . "
MAXPACKET is 256 ? look into what the lora lib uses . FIXME
Characteristics :
UUID
properties
description
8 ba2bcc2 - ee02 - 4 a55 - a531 - c525c5e454d5
read
fromradio - contains a newly received packet destined towards the phone ( up to MAXPACKET bytes ? per packet ) .
After reading the esp32 will put the next packet in this mailbox . If the FIFO is empty it will put an empty packet in this
mailbox .
f75c76d2 - 129e-4 dad - a1dd - 7866124401 e7
write
toradio - write ToRadio protobufs to this charstic to send them ( up to MAXPACKET len )
ed9da18c - a800 - 4f 66 - a670 - aa7547e34453
read | notify | write
fromnum - the current packet # in the message waiting inside fromradio , if the phone sees this notify it should read messages
until it catches up with this number .
The phone can write to this register to go backwards up to FIXME packets , to handle the rare case of a fromradio packet was dropped after the esp32
callback was called , but before it arrives at the phone . If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio .
When the esp32 advances fromnum , it will delay doing the notify by 100 ms , in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio .
Note : that if the phone ever sees this number decrease , it means the esp32 has rebooted .
Re : queue management
Not all messages are kept in the fromradio queue ( filtered based on SubPacket ) :
* only the most recent Position and User messages for a particular node are kept
* all Data SubPackets are kept
* No WantNodeNum / DenyNodeNum messages are kept
A variable keepAllPackets , if set to true will suppress this behavior and instead keep everything for forwarding to the phone ( for debugging )
* /
2020-01-24 17:05:55 -08:00
2020-04-24 15:22:54 -07:00
2020-01-24 17:05:55 -08:00
/ * *
* Handles the bluetooth link with a mesh radio device . Does not cache any device state ,
* just does bluetooth comms etc .. .
*
* This service is not exposed outside of this process .
*
* Note - this class intentionally dumb . It doesn ' t understand protobuf framing etc .. .
* It is designed to be simple so it can be stubbed out with a simulated version as needed .
* /
2020-06-07 17:11:30 -07:00
class BluetoothInterface ( val service : RadioInterfaceService , val address : String ) : IRadioInterface ,
Logging {
2020-01-24 17:05:55 -08:00
2020-02-13 19:54:05 -08:00
companion object : Logging {
2020-01-25 11:40:13 -08:00
2020-02-13 09:25:39 -08:00
/// this service UUID is publically visible for scanning
val BTM _SERVICE _UUID = UUID . fromString ( " 6ba1b218-15a8-461f-9fa8-5dcae273eafd " )
2020-06-05 21:22:56 -07:00
val BTM _FROMRADIO _CHARACTER =
UUID . fromString ( " 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 " )
val BTM _TORADIO _CHARACTER =
UUID . fromString ( " f75c76d2-129e-4dad-a1dd-7866124401e7 " )
val BTM _FROMNUM _CHARACTER =
UUID . fromString ( " ed9da18c-a800-4f66-a670-aa7547e34453 " )
2020-03-02 06:25:17 -08:00
2020-02-13 19:54:05 -08:00
/// Get our bluetooth adapter (should always succeed except on emulator
private fun getBluetoothAdapter ( context : Context ) : BluetoothAdapter ? {
val bluetoothManager =
context . getSystemService ( Context . BLUETOOTH _SERVICE ) as BluetoothManager
return bluetoothManager . adapter
}
2021-02-01 10:31:39 +08:00
fun toInterfaceName ( deviceName : String ) = " x $deviceName "
2020-06-07 17:11:30 -07:00
/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid ( context : Context , address : String ) : Boolean {
val allPaired =
getBluetoothAdapter ( context ) ?. bondedDevices . orEmpty ( ) . map { it . address } . toSet ( )
return if ( ! allPaired . contains ( address ) ) {
warn ( " Ignoring stale bond to ${address.anonymize} " )
false
} else
true
}
2020-02-13 09:25:39 -08:00
/// Return the device we are configured to use, or null for none
2020-06-07 17:11:30 -07:00
/ *
2020-04-18 18:45:50 -07:00
@SuppressLint ( " NewApi " )
fun getBondedDeviceAddress ( context : Context ) : String ? =
if ( hasCompanionDeviceApi ( context ) ) {
// Use new companion API
val deviceManager = context . getSystemService ( CompanionDeviceManager :: class . java )
val associations = deviceManager . associations
val result = associations . firstOrNull ( )
debug ( " reading bonded devices: $result " )
result
} else {
// Use classic API and a preferences string
2020-02-13 09:25:39 -08:00
2020-04-18 18:45:50 -07:00
val allPaired =
getBluetoothAdapter ( context ) ?. bondedDevices . orEmpty ( ) . map { it . address } . toSet ( )
// If the user has unpaired our device, treat things as if we don't have one
2020-06-07 17:11:30 -07:00
val address = InterfaceService . getPrefs ( context ) . getString ( DEVADDR _KEY , null )
2020-04-18 18:45:50 -07:00
if ( address != null && ! allPaired . contains ( address ) ) {
2020-05-30 14:38:16 -07:00
warn ( " Ignoring stale bond to ${address.anonymize} " )
2020-04-18 18:45:50 -07:00
null
} else
address
2020-02-13 09:25:39 -08:00
}
2020-06-07 17:11:30 -07:00
* /
2020-04-18 18:45:50 -07:00
2020-04-18 16:30:30 -07:00
/// Can we use the modern BLE scan API?
2020-04-18 18:45:50 -07:00
fun hasCompanionDeviceApi ( context : Context ) : Boolean = false / * ALAS - not ready for production yet
2020-04-18 16:30:30 -07:00
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . O ) {
val res =
context . packageManager . hasSystemFeature ( PackageManager . FEATURE _COMPANION _DEVICE _SETUP )
debug ( " CompanionDevice API available= $res " )
res
} else {
warn ( " CompanionDevice API not available, falling back to classic scan " )
false
2020-04-18 18:45:50 -07:00
} * /
2020-01-24 17:05:55 -08:00
2020-06-07 17:11:30 -07:00
/ * * FIXME - when adding companion device support back in , use this code to set companion device from setBondedDevice
* if ( BluetoothInterface . hasCompanionDeviceApi ( this ) ) {
// We only keep an association to one device at a time...
if ( addr != null ) {
val deviceManager = getSystemService ( CompanionDeviceManager :: class . java )
deviceManager . associations . forEach { old ->
if ( addr != old ) {
BluetoothInterface . debug ( " Forgetting old BLE association $old " )
deviceManager . disassociate ( old )
}
}
}
* /
2020-05-13 14:47:55 -07:00
/ * *
* this is created in onCreate ( )
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
* /
2020-06-30 12:02:12 -07:00
@Volatile
2020-05-13 14:47:55 -07:00
var safe : SafeBluetooth ? = null
}
2020-01-27 14:54:35 -08:00
2020-02-25 08:23:26 -08:00
/// Our BLE device
2020-08-15 11:19:23 -07:00
val device
get ( ) = ( safe ?: throw RadioNotConnectedException ( " No SafeBluetooth " ) ) . gatt
?: throw RadioNotConnectedException ( " No GATT " )
2020-02-25 08:23:26 -08:00
2020-04-19 19:23:20 -07:00
/// Our service - note - it is possible to get back a null response for getService if the device services haven't yet been found
2020-06-07 17:11:30 -07:00
val bservice
2020-04-19 19:23:20 -07:00
get ( ) : BluetoothGattService = device . getService ( BTM _SERVICE _UUID )
?: throw RadioNotConnectedException ( " BLE service not found " )
2020-01-27 16:00:00 -08:00
private lateinit var fromNum : BluetoothGattCharacteristic
2020-04-23 11:02:44 -07:00
/ * *
* With the new rev2 api , our first send is to start the configure readbacks . In that case ,
* rather than waiting for FromNum notifies - we try to just aggressively read all of the responses .
* /
private var isFirstSend = true
2020-07-17 11:55:36 -07:00
// NRF52 targets do not need the nasty force refresh hack that ESP32 needs (because they keep their
// BLE handles stable. So turn the hack off for these devices. FIXME - find a better way to know that the board is NRF52 based
2020-07-17 14:12:46 -07:00
// and Amazon fire devices seem to not need this hack either
// Build.MANUFACTURER != "Amazon" &&
2020-07-17 11:55:36 -07:00
private var needForceRefresh = ! address . startsWith ( " FD:10:04 " )
2020-06-07 17:11:30 -07:00
init {
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
val device = getBluetoothAdapter ( service ) ?. getRemoteDevice ( address )
if ( device != null ) {
info ( " Creating radio interface service. device= ${address.anonymize} " )
// Note this constructor also does no comm
val s = SafeBluetooth ( service , device )
safe = s
startConnect ( )
} else {
errormsg ( " Bluetooth adapter not found, assuming running on the emulator! " )
}
}
2020-06-11 16:22:20 -07:00
2020-01-24 17:05:55 -08:00
/// Send a packet/command out the radio link
2020-08-18 11:25:16 -07:00
override fun handleSendToRadio ( p : ByteArray ) {
2020-06-11 16:32:33 -07:00
try {
safe ?. let { s ->
val uuid = BTM _TORADIO _CHARACTER
2020-08-18 11:25:16 -07:00
debug ( " queuing ${p.size} bytes to $uuid " )
2020-06-11 16:32:33 -07:00
// 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 = getCharacteristic ( uuid )
2020-08-18 11:25:16 -07:00
s . asyncWriteCharacteristic ( toRadio , p ) { r ->
2020-06-11 16:32:33 -07:00
try {
r . getOrThrow ( )
2020-08-18 11:25:16 -07:00
debug ( " write of ${p.size} bytes to $uuid completed " )
2020-06-11 16:32:33 -07:00
if ( isFirstSend ) {
isFirstSend = false
doReadFromRadio ( false )
}
} catch ( ex : Exception ) {
2020-06-11 17:34:22 -07:00
scheduleReconnect ( " error during asyncWriteCharacteristic - disconnecting, ${ex.message} " )
2020-06-11 16:22:20 -07:00
}
}
2020-06-05 11:53:50 -07:00
}
2020-06-11 16:32:33 -07:00
} catch ( ex : BLEException ) {
2020-06-11 17:34:22 -07:00
scheduleReconnect ( " error during handleSendToRadio ${ex.message} " )
2020-02-04 17:27:10 -08:00
}
2020-01-24 17:05:55 -08:00
}
2020-06-11 17:34:22 -07:00
@Volatile
private var reconnectJob : Job ? = null
/ * *
* We had some problem , schedule a reconnection attempt ( if one isn ' t already queued )
* /
fun scheduleReconnect ( reason : String ) {
if ( reconnectJob == null ) {
warn ( " Scheduling reconnect because $reason " )
reconnectJob = service . serviceScope . handledLaunch { retryDueToException ( ) }
} else {
warn ( " Skipping reconnect for $reason " )
}
}
2020-01-24 17:05:55 -08:00
2020-01-27 16:00:00 -08:00
/// Attempt to read from the fromRadio mailbox, if data is found broadcast it to android apps
2020-04-21 07:57:07 -07:00
private fun doReadFromRadio ( firstRead : Boolean ) {
2020-06-07 17:11:30 -07:00
safe ?. let { s ->
2020-04-24 15:22:54 -07:00
val fromRadio = getCharacteristic ( BTM _FROMRADIO _CHARACTER )
2020-06-07 17:11:30 -07:00
s . asyncReadCharacteristic ( fromRadio ) {
2020-04-26 13:03:35 -07:00
try {
val b = it . getOrThrow ( )
. value . clone ( ) // We clone the array just in case, I'm not sure if they keep reusing the array
if ( b . isNotEmpty ( ) ) {
debug ( " Received ${b.size} bytes from radio " )
2020-06-07 17:11:30 -07:00
service . handleFromRadio ( b )
2020-04-26 13:03:35 -07:00
// Queue up another read, until we run out of packets
doReadFromRadio ( firstRead )
} else {
debug ( " Done reading from radio, fromradio is empty " )
if ( firstRead ) // If we just finished our initial download, now we want to start listening for notifies
startWatchingFromNum ( )
}
} catch ( ex : BLEException ) {
2020-06-11 17:34:22 -07:00
scheduleReconnect ( " error during doReadFromRadio - disconnecting, ${ex.message} " )
2020-02-04 12:12:29 -08:00
}
2020-01-27 19:08:12 -08:00
}
2020-02-09 03:40:13 -08:00
}
2020-01-27 16:00:00 -08:00
}
2020-03-30 16:44:48 -07:00
/ * *
* Android caches old services . But our service is still changing often , so force it to reread the service definitions every
* time
* /
private fun forceServiceRefresh ( ) {
exceptionReporter {
2020-06-28 16:09:37 -07:00
// If the gatt has been destroyed, skip the refresh attempt
safe ?. gatt ?. let { gatt ->
2020-07-24 13:00:27 -07:00
debug ( " DOING FORCE REFRESH " )
2020-06-28 16:09:37 -07:00
val refresh : Method = gatt . javaClass . getMethod ( " refresh " )
refresh . invoke ( gatt )
}
2020-03-30 16:44:48 -07:00
}
}
2020-04-04 14:37:13 -07:00
/// We only force service refresh the _first_ time we connect to the device. Thereafter it is assumed the firmware didn't change
private var hasForcedRefresh = false
2020-06-11 16:22:20 -07:00
@Volatile
var fromNumChanged = false
2020-04-21 07:57:07 -07:00
private fun startWatchingFromNum ( ) {
safe !! . setNotify ( fromNum , true ) {
2020-06-11 16:22:20 -07:00
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
fromNumChanged = true
debug ( " fromNum changed " )
service . serviceScope . handledLaunch {
2020-08-15 11:19:23 -07:00
try {
if ( fromNumChanged ) {
fromNumChanged = false
debug ( " fromNum changed, so we are reading new messages " )
doReadFromRadio ( false )
}
} catch ( e : RadioNotConnectedException ) {
// Don't report autobugs for this, getting an exception here is expected behavior
errormsg ( " Ending FromNum read, radio not connected " , e )
2020-06-11 16:22:20 -07:00
}
}
2020-04-21 07:57:07 -07:00
}
}
2020-04-24 15:49:34 -07:00
/ * *
* Some buggy BLE stacks can fail on initial connect , with either missing services or missing characteristics . If that happens we
* disconnect and try again when the device reenumerates .
* /
2020-07-01 16:31:23 -07:00
private suspend fun retryDueToException ( ) = try {
2020-06-05 12:11:35 -07:00
/// We gracefully handle safe being null because this can occur if someone has unpaired from our device - just abandon the reconnect attempt
val s = safe
if ( s != null ) {
warn ( " Forcing disconnect and hopefully device will comeback (disabling forced refresh) " )
2020-07-04 17:37:52 -07:00
// The following optimization is not currently correct - because the device might be sleeping and come back with different BLE handles
// hasForcedRefresh = true // We've already tossed any old service caches, no need to do it again
// Make sure the old connection was killed
2020-06-05 12:11:35 -07:00
ignoreException {
s . closeConnection ( )
}
2020-07-04 17:37:52 -07:00
2020-06-14 16:43:36 -07:00
service . onDisconnect ( false ) // assume we will fail
2020-09-02 20:16:41 -04:00
delay ( 1500 ) // Give some nasty time for buggy BLE stacks to shutdown (500ms was not enough)
2020-06-11 17:34:22 -07:00
reconnectJob = null // Any new reconnect requests after this will be allowed to run
2020-06-05 12:11:35 -07:00
warn ( " Attempting reconnect " )
2020-06-30 12:02:12 -07:00
if ( safe != null ) // check again, because we just slept for 1sec, and someone might have closed our interface
startConnect ( )
else
warn ( " Not connecting, because safe==null, someone must have closed us " )
2020-06-05 12:11:35 -07:00
} else {
warn ( " Abandoning reconnect because safe==null, someone must have closed the device " )
2020-04-24 15:49:34 -07:00
}
2020-07-01 16:31:23 -07:00
} catch ( ex : CancellationException ) {
warn ( " retryDueToException was cancelled " )
2020-07-18 14:23:58 -07:00
} finally {
reconnectJob = null
2020-04-24 15:49:34 -07:00
}
/// We only try to set MTU once, because some buggy implementations fail
2020-06-11 16:22:20 -07:00
@Volatile
2020-04-24 15:49:34 -07:00
private var shouldSetMtu = true
2020-05-10 21:39:23 -07:00
/// For testing
2020-06-11 16:22:20 -07:00
@Volatile
2020-05-10 21:39:23 -07:00
private var isFirstTime = true
2020-04-24 15:49:34 -07:00
private fun doDiscoverServicesAndInit ( ) {
2020-07-04 11:02:18 -07:00
val s = safe
if ( s == null )
warn ( " Interface is shutting down, so skipping discover " )
else
s . asyncDiscoverServices { discRes ->
try {
discRes . getOrThrow ( )
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
service . serviceScope . handledLaunch {
try {
debug ( " Discovered services! " )
delay ( 1000 ) // android BLE is buggy and needs a 500ms sleep before calling getChracteristic, or you might get back null
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
/ * if ( isFirstTime ) {
isFirstTime = false
throw BLEException ( " Faking a BLE failure " )
} * /
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
fromNum = getCharacteristic ( BTM _FROMNUM _CHARACTER )
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
// We treat the first send by a client as special
isFirstSend = true
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
// Now tell clients they can (finally use the api)
service . onConnect ( )
2020-04-24 15:49:34 -07:00
2020-07-04 11:02:18 -07:00
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio ( true )
} catch ( ex : BLEException ) {
scheduleReconnect (
" Unexpected error in initial device enumeration, forcing disconnect $ex "
)
}
2020-07-02 09:38:08 -07:00
}
2020-07-04 11:02:18 -07:00
} catch ( ex : BLEException ) {
2020-08-30 12:01:38 -07:00
if ( s . gatt == null )
warn ( " GATT was closed while discovering, assume we are shutting down " )
else
scheduleReconnect (
" Unexpected error discovering services, forcing disconnect $ex "
)
2020-04-24 15:49:34 -07:00
}
}
}
2020-02-04 20:11:05 -08:00
private fun onConnect ( connRes : Result < Unit > ) {
// This callback is invoked after we are connected
2020-04-20 10:37:46 -07:00
connRes . getOrThrow ( )
2020-02-04 20:11:05 -08:00
2020-06-25 15:53:17 -07:00
service . serviceScope . handledLaunch {
info ( " Connected to radio! " )
2020-06-25 17:56:31 -07:00
2020-06-25 15:53:17 -07:00
if ( needForceRefresh ) { // Our ESP32 code doesn't properly generate "service changed" indications. Therefore we need to force a refresh on initial start
//needForceRefresh = false // In fact, because of tearing down BLE in sleep on the ESP32, our handle # assignments are not stable across sleep - so we much refetch every time
forceServiceRefresh ( ) // this article says android should not be caching, but it does on some phones: https://punchthrough.com/attribute-caching-in-ble-advantages-and-pitfalls/
2020-07-18 14:23:58 -07:00
2020-07-17 14:12:46 -07:00
delay ( 500 ) // From looking at the android C code it seems that we need to give some time for the refresh message to reach that worked _before_ we try to set mtu/get services
// 200ms was not enough on an Amazon Fire
2020-06-25 15:53:17 -07:00
}
2020-04-22 08:20:57 -07:00
2020-06-25 15:53:17 -07:00
// we begin by setting our MTU size as high as it can go (if we can)
if ( shouldSetMtu )
2020-07-01 15:43:01 -07:00
safe ?. asyncRequestMtu ( 512 ) { mtuRes ->
2020-06-25 15:53:17 -07:00
try {
2020-07-04 17:37:52 -07:00
mtuRes . getOrThrow ( )
2020-06-25 15:53:17 -07:00
debug ( " MTU change attempted " )
2020-04-24 15:49:34 -07:00
2020-06-25 15:53:17 -07:00
// throw BLEException("Test MTU set failed")
2020-04-24 15:49:34 -07:00
2020-06-25 15:53:17 -07:00
doDiscoverServicesAndInit ( )
} catch ( ex : BLEException ) {
shouldSetMtu = false
scheduleReconnect (
" Giving up on setting MTUs, forcing disconnect $ex "
)
}
2020-04-13 16:28:32 -07:00
}
2020-06-25 15:53:17 -07:00
else
doDiscoverServicesAndInit ( )
}
2020-02-04 20:11:05 -08:00
}
2020-04-20 09:56:38 -07:00
2020-06-07 17:11:30 -07:00
override fun close ( ) {
2020-07-01 16:31:23 -07:00
reconnectJob ?. cancel ( ) // Cancel any queued reconnect attempts
2020-06-07 17:11:30 -07:00
if ( safe != null ) {
2020-06-30 12:18:49 -07:00
info ( " Closing BluetoothInterface " )
2020-06-07 17:11:30 -07:00
val s = safe
safe =
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
2020-01-24 20:35:42 -08:00
2020-06-07 17:11:30 -07:00
s ?. close ( )
} else {
debug ( " Radio was not connected, skipping disable " )
}
2020-01-24 17:47:32 -08:00
}
2020-04-24 15:22:54 -07:00
/// 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
2020-07-04 17:37:52 -07:00
safe !! . asyncConnect ( true ,
2020-04-24 15:22:54 -07:00
cb = :: onConnect ,
2020-07-04 17:37:52 -07:00
lostConnectCb = { scheduleReconnect ( " connection dropped " ) } )
2020-02-24 18:10:25 -08:00
}
2020-02-04 09:41:38 -08:00
2020-04-24 15:22:54 -07:00
/ * *
* Get a chracteristic , but in a safe manner because some buggy BLE implementations might return null
* /
private fun getCharacteristic ( uuid : UUID ) =
2020-11-16 15:57:40 +08:00
bservice . getCharacteristic ( uuid ) ?: throw BLECharacteristicNotFoundException ( uuid )
2020-04-24 15:22:54 -07:00
2020-02-13 20:11:00 -08:00
}