2020-01-24 17:05:55 -08:00
package com.geeksville.mesh
2020-01-27 16:00:00 -08:00
import android.app.Service
2020-01-27 14:54:35 -08:00
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
2020-01-27 16:00:00 -08:00
import android.bluetooth.BluetoothGattCharacteristic
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 android.content.Intent
2020-01-27 16:00:00 -08:00
import android.os.IBinder
2020-02-04 16:06:33 -08:00
import com.geeksville.android.BinaryLogFile
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-04 12:12:29 -08:00
import com.geeksville.util.toRemoteExceptions
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
companion object {
/ * *
* 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-04 12:12:29 -08:00
const val CONNECTCHANGED _ACTION = " $prefix .CONNECT_CHANGED "
2020-01-25 11:40:13 -08:00
2020-01-27 14:54:35 -08:00
private val BTM _SERVICE _UUID = UUID . fromString ( " 6ba1b218-15a8-461f-9fa8-5dcae273eafd " )
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-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-01-24 17:05:55 -08:00
}
2020-01-27 14:54:35 -08:00
private val bluetoothAdapter : BluetoothAdapter by lazy ( LazyThreadSafetyMode . NONE ) {
val bluetoothManager = getSystemService ( Context . BLUETOOTH _SERVICE ) as BluetoothManager
bluetoothManager . adapter !!
}
// Both of these are created in onCreate()
private lateinit var device : BluetoothDevice
private lateinit var safe : SafeBluetooth
2020-02-04 17:27:10 -08:00
val service get ( ) = safe . gatt !! . 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-02-04 16:06:33 -08:00
lateinit var sentPacketsLog : BinaryLogFile // inited in onCreate
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
/// 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-02-04 12:12:29 -08:00
private fun broadcastConnectionChanged ( isConnected : Boolean ) {
2020-02-04 13:24:04 -08:00
debug ( " Broadcasting connection= $isConnected " )
2020-02-04 12:12:29 -08:00
val intent = Intent ( CONNECTCHANGED _ACTION )
2020-01-24 17:05:55 -08:00
intent . putExtra ( EXTRA _CONNECTED , isConnected )
sendBroadcast ( intent )
}
/// Send a packet/command out the radio link
2020-01-25 10:00:57 -08:00
private fun handleSendToRadio ( p : ByteArray ) {
2020-01-24 17:47:32 -08:00
// For debugging/logging purposes ONLY we convert back into a protobuf for readability
2020-02-04 16:06:33 -08:00
// al proto = MeshProtos.ToRadio.parseFrom(p)
2020-01-25 11:40:13 -08:00
2020-02-04 16:06:33 -08:00
debug ( " sending to radio " )
doWrite ( BTM _TORADIO _CHARACTER , p )
2020-02-04 17:27:10 -08:00
if ( logSends ) {
sentPacketsLog . write ( p )
sentPacketsLog . flush ( )
}
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-01-25 12:24:53 -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
private fun doReadFromRadio ( ) {
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-04 12:12:29 -08:00
safe . asyncReadCharacteristic ( fromRadio ) {
val b = it . getOrThrow ( ) . value
if ( b . isNotEmpty ( ) ) {
debug ( " Received ${b.size} bytes from radio " )
handleFromRadio ( b )
// Queue up another read, until we run out of packets
doReadFromRadio ( )
} else {
debug ( " Done reading from radio, fromradio is empty " )
}
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-02-04 20:11:05 -08:00
private fun onDisconnect ( ) {
broadcastConnectionChanged ( false )
isConnected = false
}
private fun onConnect ( connRes : Result < Unit > ) {
// This callback is invoked after we are connected
connRes . getOrThrow ( ) // FIXME, instead just try to reconnect?
info ( " Connected to radio! " )
// 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?
debug ( " Discovered services! " )
// we begin by setting our MTU size as high as it can go
safe . asyncRequestMtu ( 512 ) { mtuRes ->
debug ( " requested MTU result= $mtuRes " )
mtuRes . getOrThrow ( ) // FIXME - why sometimes is the result Unit!?!
fromNum = service . getCharacteristic ( BTM _FROMNUM _CHARACTER )
2020-02-09 03:40:13 -08:00
safe . setNotify ( fromNum , true ) {
debug ( " fromNum changed, so we are reading new messages " )
doReadFromRadio ( )
}
2020-02-04 20:11:05 -08:00
// Now tell clients they can (finally use the api)
broadcastConnectionChanged ( true )
isConnected = true
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio ( )
}
}
}
2020-01-24 20:35:42 -08:00
override fun onCreate ( ) {
super . onCreate ( )
2020-01-27 14:54:35 -08:00
// FIXME, let user GUI select which device we are talking to
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
2020-02-02 19:06:56 -08:00
val usetbeam = false
val address = if ( usetbeam ) " B4:E6:2D:EA:32:B7 " else " 24:6F:28:96:C9:2A "
2020-02-04 13:24:04 -08:00
info ( " Creating radio interface service. device= $address " )
2020-02-02 19:06:56 -08:00
device = bluetoothAdapter . getRemoteDevice ( address )
2020-01-27 14:54:35 -08:00
// Note this constructor also does no comm
safe = SafeBluetooth ( this , device )
// 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-02-04 20:11:05 -08:00
safe . asyncConnect ( true , :: onConnect , :: onDisconnect )
2020-01-27 14:54:35 -08:00
2020-02-04 17:27:10 -08:00
if ( logSends )
sentPacketsLog = BinaryLogFile ( this , " sent_log.pb " )
2020-01-24 20:35:42 -08:00
}
2020-01-24 17:47:32 -08:00
override fun onDestroy ( ) {
2020-01-27 16:58:47 -08:00
info ( " Destroying radio interface service " )
2020-02-04 17:27:10 -08:00
if ( logSends )
sentPacketsLog . close ( )
2020-01-27 18:47:13 -08:00
safe . disconnect ( )
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-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-04 12:12:29 -08:00
safe . writeCharacteristic ( toRadio )
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-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 )
var a = safe . readCharacteristic ( toRadio ) . value
debug ( " Read of $uuid got ${a.size} bytes " )
if ( a . isEmpty ( ) ) // An empty bluetooth response is converted to a null response for our clients
a = null
a
}
2020-02-04 09:41:38 -08:00
}
private val binder = object : IRadioInterfaceService . Stub ( ) {
// 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-02-04 12:12:29 -08:00
override fun readMyNode ( ) = doRead ( BTM _MYNODE _CHARACTER ) !!
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-02-04 12:12:29 -08:00
override fun readRadioConfig ( ) = doRead ( BTM _RADIO _CHARACTER ) !!
2020-02-04 09:41:38 -08:00
2020-02-04 12:12:29 -08:00
override fun readOwner ( ) = doRead ( BTM _OWNER _CHARACTER ) !!
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-01-24 17:05:55 -08:00
}