2020-02-10 15:31:56 -08:00
package com.geeksville.mesh.service
2020-01-24 17:05:55 -08:00
2020-04-18 18:45:50 -07:00
import android.annotation.SuppressLint
2020-01-27 16:00:00 -08:00
import android.app.Service
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-04-18 18:45:50 -07:00
import android.companion.CompanionDeviceManager
2020-01-24 17:05:55 -08:00
import android.content.Context
import android.content.Intent
2020-01-27 16:00:00 -08:00
import android.os.IBinder
2020-03-30 17:36:09 -07:00
import android.os.RemoteException
2020-02-12 15:47:06 -08:00
import androidx.core.content.edit
2020-02-04 16:06:33 -08:00
import com.geeksville.android.BinaryLogFile
2020-03-11 14:46:02 -07:00
import com.geeksville.android.GeeksvilleApplication
2020-01-24 17:05:55 -08:00
import com.geeksville.android.Logging
2020-01-27 19:08:12 -08:00
import com.geeksville.concurrent.DeferredExecution
2020-02-10 15:31:56 -08:00
import com.geeksville.mesh.IRadioInterfaceService
2020-04-21 08:18:46 -07:00
import com.geeksville.mesh.anonymized
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-02-04 12:12:29 -08:00
import com.geeksville.util.toRemoteExceptions
2020-04-13 16:28:32 -07:00
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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 .
2020-02-04 09:41:38 -08:00
meshMyNodeCharacteristic ( " ea9f3f82-8dc4-4733-9452-1f6da28892a2 " , BLECharacteristic :: PROPERTY _READ )
mynode - read / write this to access a MyNodeInfo protobuf
meshNodeInfoCharacteristic ( " d31e02e0-c8ab-4d3f-9cc9-0b8466bdabe8 " , BLECharacteristic :: PROPERTY _WRITE | BLECharacteristic :: PROPERTY _READ ) ,
nodeinfo - read this to get a series of node infos ( ending with a null empty record ) , write to this to restart the read statemachine that returns all the node infos
meshRadioCharacteristic ( " b56786c8-839a-44a1-b98e-a1724c4a0262 " , BLECharacteristic :: PROPERTY _WRITE | BLECharacteristic :: PROPERTY _READ ) ,
radio - read / write this to access a RadioConfig protobuf
meshOwnerCharacteristic ( " 6ff1d8b6-e2de-41e3-8c0b-8fa384f64eb6 " , BLECharacteristic :: PROPERTY _WRITE | BLECharacteristic :: PROPERTY _READ )
owner - read / write this to access a User protobuf
2020-01-27 14:54:35 -08:00
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
/ * *
* 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-01-27 16:00:00 -08:00
class RadioInterfaceService : Service ( ) , Logging {
2020-01-24 17:05:55 -08:00
2020-02-13 19:54:05 -08:00
companion object : Logging {
2020-01-24 17:05:55 -08:00
/ * *
* The RECEIVED _FROMRADIO
* Payload will be the raw bytes which were contained within a MeshProtos . FromRadio protobuf
* /
const val RECEIVE _FROMRADIO _ACTION = " $prefix .RECEIVE_FROMRADIO "
2020-02-09 05:52:17 -08:00
/ * *
2020-02-10 15:31:56 -08:00
* This is broadcast when connection state changed
2020-02-09 05:52:17 -08:00
* /
2020-02-10 15:31:56 -08:00
const val RADIO _CONNECTED _ACTION = " $prefix .CONNECT_CHANGED "
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-01-27 14:54:35 -08:00
private val BTM _FROMRADIO _CHARACTER =
UUID . fromString ( " 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 " )
private val BTM _TORADIO _CHARACTER =
UUID . fromString ( " f75c76d2-129e-4dad-a1dd-7866124401e7 " )
private val BTM _FROMNUM _CHARACTER =
UUID . fromString ( " ed9da18c-a800-4f66-a670-aa7547e34453 " )
2020-02-04 09:41:38 -08:00
/// mynode - read/write this to access a MyNodeInfo protobuf
private val BTM _MYNODE _CHARACTER =
UUID . fromString ( " ea9f3f82-8dc4-4733-9452-1f6da28892a2 " )
/// nodeinfo - read this to get a series of node infos (ending with a null empty record), write to this to restart the read statemachine that returns all the node infos
private val BTM _NODEINFO _CHARACTER =
UUID . fromString ( " d31e02e0-c8ab-4d3f-9cc9-0b8466bdabe8 " )
/// radio - read/write this to access a RadioConfig protobuf
private val BTM _RADIO _CHARACTER =
UUID . fromString ( " b56786c8-839a-44a1-b98e-a1724c4a0262 " )
/// owner - read/write this to access a User protobuf
private val BTM _OWNER _CHARACTER =
UUID . fromString ( " 6ff1d8b6-e2de-41e3-8c0b-8fa384f64eb6 " )
2020-03-02 06:25:17 -08:00
private const val DEVADDR _KEY = " devAddr "
/// If our service is currently running, this pointer can be used to reach it (in case setBondedDeviceAddress is called)
private var runningService : RadioInterfaceService ? = null
2020-04-23 11:54:47 -07:00
/ * *
* Temp hack ( until old API deprecated ) , we probe for old API at connected
* /
var isOldApi : Boolean ? = null
2020-01-25 12:24:53 -08:00
/// This is public only so that SimRadio can bootstrap our message flow
fun broadcastReceivedFromRadio ( context : Context , payload : ByteArray ) {
val intent = Intent ( RECEIVE _FROMRADIO _ACTION )
intent . putExtra ( EXTRA _PAYLOAD , payload )
context . sendBroadcast ( intent )
}
2020-02-12 15:47:06 -08:00
private fun getPrefs ( context : Context ) =
context . getSharedPreferences ( " radio-prefs " , Context . MODE _PRIVATE )
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
}
2020-02-13 09:25:39 -08:00
/// Return the device we are configured to use, or null for none
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
val address = getPrefs ( context ) . getString ( DEVADDR _KEY , null )
if ( address != null && ! allPaired . contains ( address ) ) {
2020-04-21 08:18:46 -07:00
warn ( " Ignoring stale bond to ${address.anonymized} " )
2020-04-18 18:45:50 -07:00
null
} else
address
2020-02-13 09:25:39 -08:00
}
2020-03-02 06:25:17 -08: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-01-27 14:54:35 -08:00
// Both of these are created in onCreate()
2020-02-09 03:58:13 -08:00
private var safe : SafeBluetooth ? = null
2020-01-27 14:54:35 -08:00
2020-02-25 08:23:26 -08:00
/// Our BLE device
val device get ( ) = safe !! . gatt !!
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
val service
get ( ) : BluetoothGattService = device . getService ( BTM _SERVICE _UUID )
?: throw RadioNotConnectedException ( " BLE service not found " )
2020-02-25 08:23:26 -08:00
//.services.find { it.uuid == BTM_SERVICE_UUID }!!
2020-01-27 16:24:38 -08:00
2020-01-27 16:00:00 -08:00
private lateinit var fromNum : BluetoothGattCharacteristic
2020-02-04 17:27:10 -08:00
private val logSends = false
2020-03-02 06:31:15 -08:00
private val logReceives = false
2020-02-04 16:06:33 -08:00
lateinit var sentPacketsLog : BinaryLogFile // inited in onCreate
2020-03-02 06:31:15 -08:00
lateinit var receivedPacketsLog : BinaryLogFile
2020-01-27 16:00:00 -08:00
2020-02-04 12:12:29 -08:00
private var isConnected = false
2020-01-27 19:08:12 -08:00
2020-04-13 16:28:32 -07:00
private val serviceJob = Job ( )
private val serviceScope = CoroutineScope ( Dispatchers . IO + serviceJob )
2020-01-27 19:08:12 -08:00
/// Work that users of our service want done, which might get deferred until after
/// we have completed our initial connection
private val clientOperations = DeferredExecution ( )
2020-04-20 08:27:08 -07:00
private fun broadcastConnectionChanged ( isConnected : Boolean , isPermanent : Boolean ) {
2020-02-04 13:24:04 -08:00
debug ( " Broadcasting connection= $isConnected " )
2020-02-10 15:31:56 -08:00
val intent = Intent ( RADIO _CONNECTED _ACTION )
2020-01-24 17:05:55 -08:00
intent . putExtra ( EXTRA _CONNECTED , isConnected )
2020-04-20 08:27:08 -07:00
intent . putExtra ( EXTRA _PERMANENT , isPermanent )
2020-01-24 17:05:55 -08:00
sendBroadcast ( intent )
}
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-01-24 17:05:55 -08:00
/// Send a packet/command out the radio link
2020-01-25 10:00:57 -08:00
private fun handleSendToRadio ( p : ByteArray ) {
2020-04-23 11:02:44 -07:00
// Do this in the IO thread because it might take a while
serviceScope . handledLaunch {
debug ( " sending to radio " )
doWrite (
BTM _TORADIO _CHARACTER ,
p
) // Do a synchronous write, so that we can then do our reads if needed
if ( logSends ) {
sentPacketsLog . write ( p )
sentPacketsLog . flush ( )
}
2020-01-24 17:47:32 -08:00
2020-04-23 11:02:44 -07:00
if ( isFirstSend ) {
isFirstSend = false
doReadFromRadio ( false )
}
2020-02-04 17:27:10 -08:00
}
2020-01-24 17:05:55 -08:00
}
// Handle an incoming packet from the radio, broadcasts it as an android intent
private fun handleFromRadio ( p : ByteArray ) {
2020-03-04 13:59:22 -08:00
if ( logReceives ) {
2020-03-02 06:31:15 -08:00
receivedPacketsLog . write ( p )
receivedPacketsLog . flush ( )
}
2020-03-02 07:46:03 -08:00
2020-02-10 15:31:56 -08:00
broadcastReceivedFromRadio (
this ,
p
)
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-02-04 12:12:29 -08:00
if ( !is Connected )
warn ( " Abandoning fromradio read because we are not connected " )
2020-02-09 03:40:13 -08:00
else {
val fromRadio = service . getCharacteristic ( BTM _FROMRADIO _CHARACTER )
2020-02-09 03:58:13 -08:00
safe !! . asyncReadCharacteristic ( fromRadio ) {
2020-02-25 09:28:47 -08:00
val b = it . getOrThrow ( )
. value . clone ( ) // We clone the array just in case, I'm not sure if they keep reusing the array
2020-02-04 12:12:29 -08:00
if ( b . isNotEmpty ( ) ) {
debug ( " Received ${b.size} bytes from radio " )
handleFromRadio ( b )
// Queue up another read, until we run out of packets
2020-04-21 07:57:07 -07:00
doReadFromRadio ( firstRead )
2020-02-04 12:12:29 -08:00
} else {
debug ( " Done reading from radio, fromradio is empty " )
2020-04-21 07:57:07 -07:00
if ( firstRead ) // If we just finished our initial download, now we want to start listening for notifies
startWatchingFromNum ( )
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-01-27 19:08:12 -08:00
2020-04-19 19:23:20 -07:00
@SuppressLint ( " NewApi " )
fun setBondedDeviceAddress ( addr : String ? ) {
// Record that this use has configured a radio
GeeksvilleApplication . analytics . track (
" mesh_bond "
)
// Ignore any errors that happen while closing old device
ignoreException {
Companion . info ( " shutting down old service " )
setEnabled ( false ) // nasty, needed to force the next setEnabled call to reconnect
}
debug ( " Setting bonded device to $addr " )
if ( 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 ) {
Companion . debug ( " Forgetting old BLE association $old " )
deviceManager . disassociate ( old )
}
}
}
} else {
getPrefs ( this ) . edit ( commit = true ) {
if ( addr == null )
this . remove ( DEVADDR _KEY )
else
putString ( DEVADDR _KEY , addr )
}
}
// Force the service to reconnect
if ( addr != null ) {
info ( " Setting enable on the running radio service " )
setEnabled ( true )
}
}
2020-04-20 08:27:08 -07:00
private fun onDisconnect ( isPermanent : Boolean ) {
broadcastConnectionChanged ( false , isPermanent )
2020-02-04 20:11:05 -08:00
isConnected = false
}
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 {
// BluetoothGatt gatt
val gatt = safe !! . gatt !!
val refresh : Method = gatt . javaClass . getMethod ( " refresh " )
refresh . invoke ( gatt )
}
}
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-04-21 07:57:07 -07:00
private fun startWatchingFromNum ( ) {
safe !! . setNotify ( fromNum , true ) {
debug ( " fromNum changed, so we are reading new messages " )
doReadFromRadio ( false )
}
}
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
info ( " Connected to radio! " )
2020-04-04 14:37:13 -07:00
if ( ! hasForcedRefresh ) {
2020-04-04 15:29:16 -07:00
// FIXME - for some reason we need to refresh _everytime_. It is almost as if we've cached wrong descriptor fieldnums forever
// hasForcedRefresh = true
2020-04-04 14:37:13 -07:00
forceServiceRefresh ( )
}
2020-04-22 08:20:57 -07:00
2020-04-22 08:10:23 -07:00
// we begin by setting our MTU size as high as it can go
safe !! . asyncRequestMtu ( 512 ) { mtuRes ->
mtuRes . getOrThrow ( ) // FIXME - why sometimes is the result Unit!?!
2020-04-22 08:20:57 -07:00
debug ( " MTU change attempted " )
2020-04-22 08:10:23 -07:00
// FIXME - no need to discover services more than once - instead use lazy() to use them in future attempts
safe !! . asyncDiscoverServices { discRes ->
discRes . getOrThrow ( ) // FIXME, instead just try to reconnect?
2020-04-22 08:20:57 -07:00
2020-04-13 16:28:32 -07:00
serviceScope . handledLaunch {
2020-04-22 08:20:57 -07:00
debug ( " Discovered services! " )
2020-04-13 16:28:32 -07:00
delay ( 500 ) // android BLE is buggy and needs a 500ms sleep before calling getChracteristic, or you might get back null
2020-02-04 20:11:05 -08:00
2020-04-23 11:54:47 -07:00
isOldApi = service . getCharacteristic ( BTM _RADIO _CHARACTER ) != null
warn ( " Use oldAPI = $isOldApi " )
2020-04-13 16:28:32 -07:00
fromNum = service . getCharacteristic ( BTM _FROMNUM _CHARACTER ) !!
2020-02-04 20:11:05 -08:00
2020-04-13 16:28:32 -07:00
// We must set this to true before broadcasting connectionChanged
isConnected = true
2020-03-04 13:59:22 -08:00
2020-04-23 11:02:44 -07:00
// We treat the first send by a client as special
isFirstSend = true
2020-04-13 16:28:32 -07:00
// Now tell clients they can (finally use the api)
2020-04-20 08:27:08 -07:00
broadcastConnectionChanged ( true , isPermanent = false )
2020-02-04 20:11:05 -08:00
2020-04-13 16:28:32 -07:00
// Immediately broadcast any queued packets sitting on the device
2020-04-21 07:57:07 -07:00
doReadFromRadio ( true )
2020-04-13 16:28:32 -07:00
}
2020-02-04 20:11:05 -08:00
}
}
}
2020-04-20 09:56:38 -07:00
/ * *
* If the user turns on bluetooth after we start , make sure to try and reconnected then
* /
private val bluetoothStateReceiver = BluetoothStateReceiver { enabled ->
if ( enabled )
setEnabled ( true )
}
2020-01-24 20:35:42 -08:00
override fun onCreate ( ) {
2020-03-02 06:25:17 -08:00
runningService = this
2020-01-24 20:35:42 -08:00
super . onCreate ( )
2020-02-24 18:10:25 -08:00
setEnabled ( true )
2020-04-20 09:56:38 -07:00
registerReceiver ( bluetoothStateReceiver , bluetoothStateReceiver . intent )
2020-01-24 20:35:42 -08:00
}
2020-01-24 17:47:32 -08:00
override fun onDestroy ( ) {
2020-04-20 09:56:38 -07:00
unregisterReceiver ( bluetoothStateReceiver )
2020-02-24 18:10:25 -08:00
setEnabled ( false )
2020-04-13 16:28:32 -07:00
serviceJob . cancel ( )
2020-03-02 06:25:17 -08:00
runningService = null
2020-01-24 17:47:32 -08:00
super . onDestroy ( )
}
2020-01-27 16:00:00 -08:00
override fun onBind ( intent : Intent ? ) : IBinder ? {
return binder ;
2020-01-24 17:05:55 -08:00
}
2020-02-24 18:10:25 -08:00
/// Open or close a bluetooth connection to our device
private fun setEnabled ( on : Boolean ) {
if ( on ) {
2020-03-02 06:25:17 -08:00
if ( safe != null ) {
info ( " Skipping radio enable, it is already on " )
} else {
val address = getBondedDeviceAddress ( this )
if ( address == null )
2020-04-19 19:23:20 -07:00
errormsg ( " No bonded mesh radio, can't start service " )
2020-03-02 06:25:17 -08:00
else {
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
val device = getBluetoothAdapter ( this ) ?. getRemoteDevice ( address )
if ( device != null ) {
2020-04-21 08:18:46 -07:00
info ( " Creating radio interface service. device= ${address.anonymized} " )
2020-03-02 06:25:17 -08:00
// Note this constructor also does no comm
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
2020-04-20 08:27:08 -07:00
s . asyncConnect ( true ,
cb = :: onConnect ,
lostConnectCb = { onDisconnect ( isPermanent = false ) } )
2020-03-02 06:25:17 -08:00
} else {
errormsg ( " Bluetooth adapter not found, assuming running on the emulator! " )
}
if ( logSends )
sentPacketsLog = BinaryLogFile ( this , " sent_log.pb " )
2020-03-02 06:31:15 -08:00
if ( logReceives )
receivedPacketsLog = BinaryLogFile ( this , " receive_log.pb " )
2020-02-24 18:10:25 -08:00
}
}
} else {
2020-04-20 08:27:08 -07:00
if ( safe != null ) {
info ( " Closing radio interface service " )
if ( logSends )
sentPacketsLog . close ( )
if ( logReceives )
receivedPacketsLog . close ( )
safe ?. close ( )
safe = null
onDisconnect ( isPermanent = true ) // Tell any clients we are now offline
} else {
debug ( " Radio was not connected, skipping disable " )
}
2020-02-24 18:10:25 -08:00
}
}
2020-02-04 09:41:38 -08:00
/ * *
2020-02-04 12:12:29 -08:00
* do a synchronous write operation
2020-02-04 09:41:38 -08:00
* /
2020-02-04 12:12:29 -08:00
private fun doWrite ( uuid : UUID , a : ByteArray ) = toRemoteExceptions {
if ( !is Connected )
throw RadioNotConnectedException ( )
else {
debug ( " queuing ${a.size} bytes to $uuid " )
2020-02-04 09:41:38 -08: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 = service . getCharacteristic ( uuid )
toRadio . value = a
2020-02-09 03:58:13 -08:00
safe !! . writeCharacteristic ( toRadio )
2020-02-04 12:12:29 -08:00
debug ( " write of ${a.size} bytes completed " )
2020-02-04 09:41:38 -08:00
}
2020-02-04 12:12:29 -08:00
}
2020-02-04 09:41:38 -08:00
2020-02-16 13:33:29 -08:00
/ * *
* do an asynchronous write operation
* Any error responses will be ignored ( other than log messages )
* /
private fun doAsyncWrite ( uuid : UUID , a : ByteArray ) = toRemoteExceptions {
if ( !is Connected )
throw RadioNotConnectedException ( )
else {
debug ( " queuing ${a.size} bytes to $uuid " )
// 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 )
toRadio . value = a
safe !! . asyncWriteCharacteristic ( toRadio ) {
debug ( " asyncwrite of ${a.size} bytes completed " )
}
}
}
2020-02-04 12:12:29 -08:00
/ * *
* do a synchronous read operation
* /
private fun doRead ( uuid : UUID ) : ByteArray ? = toRemoteExceptions {
if ( !is Connected )
throw RadioNotConnectedException ( )
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 )
2020-02-25 09:28:47 -08:00
var a = safe !! . readCharacteristic ( toRadio )
. value . clone ( ) // we copy the bluetooth array because it might still be in use
2020-02-04 12:12:29 -08:00
debug ( " Read of $uuid got ${a.size} bytes " )
if ( a . isEmpty ( ) ) // An empty bluetooth response is converted to a null response for our clients
2020-02-25 09:28:47 -08:00
null
else
a
2020-02-04 12:12:29 -08:00
}
2020-02-04 09:41:38 -08:00
}
private val binder = object : IRadioInterfaceService . Stub ( ) {
2020-04-19 19:23:20 -07:00
override fun setDeviceAddress ( deviceAddr : String ? ) = toRemoteExceptions {
setBondedDeviceAddress ( deviceAddr )
2020-02-24 18:10:25 -08:00
}
2020-02-04 09:41:38 -08:00
// A write of any size to nodeinfo means restart reading
2020-02-04 12:12:29 -08:00
override fun restartNodeInfo ( ) = doWrite ( BTM _NODEINFO _CHARACTER , ByteArray ( 0 ) )
2020-02-04 09:41:38 -08:00
2020-03-30 16:44:48 -07:00
override fun readMyNode ( ) =
2020-03-30 17:36:09 -07:00
doRead ( BTM _MYNODE _CHARACTER )
?: throw RemoteException ( " Device returned empty MyNodeInfo " )
2020-02-04 09:41:38 -08:00
2020-02-04 16:06:33 -08:00
override fun sendToRadio ( a : ByteArray ) = handleSendToRadio ( a )
2020-02-04 09:41:38 -08:00
2020-03-30 16:44:48 -07:00
override fun readRadioConfig ( ) =
2020-03-30 17:36:09 -07:00
doRead ( BTM _RADIO _CHARACTER )
?: throw RemoteException ( " Device returned empty RadioConfig " )
2020-02-04 09:41:38 -08:00
2020-03-30 16:44:48 -07:00
override fun readOwner ( ) =
2020-03-30 17:36:09 -07:00
doRead ( BTM _OWNER _CHARACTER ) ?: throw RemoteException ( " Device returned empty Owner " )
2020-02-04 09:41:38 -08:00
2020-02-04 12:12:29 -08:00
override fun writeOwner ( owner : ByteArray ) = doWrite ( BTM _OWNER _CHARACTER , owner )
2020-02-04 09:41:38 -08:00
2020-02-04 12:12:29 -08:00
override fun writeRadioConfig ( config : ByteArray ) = doWrite ( BTM _RADIO _CHARACTER , config )
2020-01-27 19:08:12 -08:00
2020-02-04 12:12:29 -08:00
override fun readNodeInfo ( ) = doRead ( BTM _NODEINFO _CHARACTER )
2020-01-27 16:00:00 -08:00
}
2020-02-13 20:11:00 -08:00
}