2020-02-10 15:31:56 -08:00
package com.geeksville.mesh.service
2020-01-22 21:25:31 -08:00
2020-02-16 14:22:24 -08:00
import android.annotation.SuppressLint
2020-02-04 13:24:04 -08:00
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
2020-01-22 21:25:31 -08:00
import android.app.Service
2020-01-27 16:00:00 -08:00
import android.content.*
2020-02-04 13:24:04 -08:00
import android.graphics.Color
import android.os.Build
2020-01-22 21:25:31 -08:00
import android.os.IBinder
2020-02-17 18:46:20 -08:00
import android.os.RemoteException
2020-02-04 13:24:04 -08:00
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MIN
2020-01-22 22:16:30 -08:00
import com.geeksville.android.Logging
2020-02-10 17:05:51 -08:00
import com.geeksville.mesh.*
2020-01-24 20:35:42 -08:00
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
2020-02-04 13:24:04 -08:00
import com.geeksville.util.exceptionReporter
2020-02-16 14:22:24 -08:00
import com.geeksville.util.reportException
2020-01-25 11:40:13 -08:00
import com.geeksville.util.toOneLineString
2020-01-26 10:44:42 -08:00
import com.geeksville.util.toRemoteExceptions
2020-02-16 14:22:24 -08:00
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.*
2020-01-24 20:35:42 -08:00
import com.google.protobuf.ByteString
2020-01-25 06:16:10 -08:00
import java.nio.charset.Charset
2020-01-24 17:05:55 -08:00
2020-02-04 13:24:04 -08:00
2020-02-17 15:56:04 -08:00
class RadioNotConnectedException ( ) : Exception ( " Not connected to radio " )
2020-01-26 11:33:51 -08:00
2020-02-09 05:52:17 -08:00
2020-01-23 08:09:50 -08:00
/ * *
2020-01-24 17:05:55 -08:00
* Handles all the communication with android apps . Also keeps an internal model
* of the network state .
*
2020-01-23 08:09:50 -08:00
* Note : this service will go away once all clients are unbound from it .
* /
2020-01-22 22:16:30 -08:00
class MeshService : Service ( ) , Logging {
2020-02-12 15:47:06 -08:00
companion object : Logging {
2020-02-09 05:52:17 -08:00
/// Intents broadcast by MeshService
const val ACTION _RECEIVED _DATA = " $prefix .RECEIVED_DATA "
const val ACTION _NODE _CHANGE = " $prefix .NODE_CHANGE "
2020-02-10 15:31:56 -08:00
const val ACTION _MESH _CONNECTED = " $prefix .MESH_CONNECTED "
2020-02-09 05:52:17 -08:00
2020-01-25 10:00:57 -08:00
class IdNotFoundException ( id : String ) : Exception ( " ID not found $id " )
class NodeNumNotFoundException ( id : Int ) : Exception ( " NodeNum not found $id " )
2020-01-25 12:24:53 -08:00
class NotInMeshException ( ) : Exception ( " We are not yet in a mesh " )
2020-02-12 15:47:06 -08:00
/// Helper function to start running our service, returns the intent used to reach it
/// or null if the service could not be started (no bluetooth or no bonded device set)
fun startService ( context : Context ) : Intent ? {
if ( RadioInterfaceService . getBondedDeviceAddress ( context ) == null ) {
warn ( " No mesh radio is bonded, not starting service " )
return null
} else {
// bind to our service using the same mechanism an external client would use (for testing coverage)
// The following would work for us, but not external users
//val intent = Intent(this, MeshService::class.java)
//intent.action = IMeshService::class.java.name
val intent = Intent ( )
intent . setClassName (
" com.geeksville.mesh " ,
" com.geeksville.mesh.service.MeshService "
)
2020-02-09 05:52:17 -08:00
2020-02-12 15:47:06 -08:00
// Before binding we want to explicitly create - so the service stays alive forever (so it can keep
// listening for the bluetooth packets arriving from the radio. And when they arrive forward them
// to Signal or whatever.
logAssert (
( if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . O ) {
context . startForegroundService ( intent )
} else {
context . startService ( intent )
} ) != null
)
return intent
}
}
2020-01-25 10:00:57 -08:00
}
2020-01-26 11:33:51 -08:00
/// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf < String , String > ( )
2020-01-27 16:00:00 -08:00
private var radioService : IRadioInterfaceService ? = null
2020-01-22 22:16:30 -08:00
/ *
2020-01-23 06:34:15 -08:00
see com . geeksville . mesh broadcast intents
2020-01-22 22:16:30 -08:00
// RECEIVED_OPAQUE for data received from other nodes
// NODE_CHANGE for new IDs appearing or disappearing
2020-02-10 15:31:56 -08:00
// ACTION_MESH_CONNECTED for losing/gaining connection to the packet radio (note, this is not
the same as RadioInterfaceService . RADIO _CONNECTED _ACTION , because it implies we have assembled a valid
node db .
2020-01-22 22:16:30 -08:00
* /
2020-01-24 17:05:55 -08:00
2020-01-26 11:33:51 -08:00
private fun explicitBroadcast ( intent : Intent ) {
2020-02-09 05:52:17 -08:00
sendBroadcast ( intent ) // We also do a regular (not explicit broadcast) so any context-registered rceivers will work
2020-01-26 11:33:51 -08:00
clientPackages . forEach {
intent . setClassName ( it . value , it . key )
sendBroadcast ( intent )
}
}
2020-02-16 14:22:24 -08:00
private val locationCallback = object : LocationCallback ( ) {
override fun onLocationResult ( locationResult : LocationResult ) {
super . onLocationResult ( locationResult )
var l = locationResult . lastLocation
// Docs say lastLocation should always be !null if there are any locations, but that's not the case
if ( l == null ) {
// try to only look at the accurate locations
val locs =
locationResult . locations . filter { ! it . hasAccuracy ( ) || it . accuracy < 200 }
l = locs . lastOrNull ( )
}
if ( l != null ) {
info ( " got location $l " )
if ( l . hasAccuracy ( ) && l . accuracy >= 200 ) // if more than 200 meters off we won't use it
warn ( " accuracy ${l.accuracy} is too poor to use " )
else {
sendPosition ( l . latitude , l . longitude , l . altitude . toInt ( ) )
}
}
}
}
private var fusedLocationClient : FusedLocationProviderClient ? = null
/ * *
* start our location requests
*
* per https : //developer.android.com/training/location/change-location-settings
* /
@SuppressLint ( " MissingPermission " )
private fun startLocationRequests ( ) {
val request = LocationRequest . create ( ) . apply {
interval =
60 * 1000 // FIXME, do more like once every 5 mins while we are connected to our radio _and_ someone else is in the mesh
priority = LocationRequest . PRIORITY _HIGH _ACCURACY
}
val builder = LocationSettingsRequest . Builder ( ) . addLocationRequest ( request )
val locationClient = LocationServices . getSettingsClient ( this )
val locationSettingsResponse = locationClient . checkLocationSettings ( builder . build ( ) )
locationSettingsResponse . addOnSuccessListener {
debug ( " We are now successfully listening to the GPS " )
}
locationSettingsResponse . addOnFailureListener { exception ->
error ( " Failed to listen to GPS " )
if ( exception is ResolvableApiException ) {
exceptionReporter {
// Location settings are not satisfied, but this can be fixed
// by showing the user a dialog.
// FIXME
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
/ * exception . startResolutionForResult (
this @MainActivity ,
REQUEST _CHECK _SETTINGS
) * /
}
} else
reportException ( exception )
}
val client = LocationServices . getFusedLocationProviderClient ( this )
// FIXME - should we use Looper.myLooper() in the third param per https://github.com/android/location-samples/blob/432d3b72b8c058f220416958b444274ddd186abd/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/LocationUpdatesService.java
client . requestLocationUpdates ( request , locationCallback , null )
fusedLocationClient = client
}
private fun stopLocationRequests ( ) {
fusedLocationClient ?. removeLocationUpdates ( locationCallback )
fusedLocationClient = null
}
2020-02-10 15:31:56 -08:00
2020-01-24 17:05:55 -08:00
/ * *
* The RECEIVED _OPAQUE :
* Payload will be the raw bytes which were contained within a MeshPacket . Opaque field
* Sender will be a user ID string
2020-01-25 06:33:30 -08:00
* Type will be the Data . Type enum code for this payload
2020-01-24 17:05:55 -08:00
* /
2020-02-09 05:52:17 -08:00
private fun broadcastReceivedData ( senderId : String , payload : ByteArray , typ : Int ) {
val intent = Intent ( ACTION _RECEIVED _DATA )
2020-01-24 17:05:55 -08:00
intent . putExtra ( EXTRA _SENDER , senderId )
intent . putExtra ( EXTRA _PAYLOAD , payload )
2020-01-25 06:33:30 -08:00
intent . putExtra ( EXTRA _TYP , typ )
2020-01-26 11:33:51 -08:00
explicitBroadcast ( intent )
2020-01-22 22:16:30 -08:00
}
2020-02-09 05:52:17 -08:00
private fun broadcastNodeChange ( info : NodeInfo ) {
debug ( " Broadcasting node change $info " )
val intent = Intent ( ACTION _NODE _CHANGE )
2020-02-10 16:34:01 -08:00
/ *
if ( info . user == null )
info . user = MeshUser ( " x " , " y " , " z " )
if ( info . position == null )
info . position = Position ( 1.5 , 1.6 , 3 )
* /
2020-02-09 05:52:17 -08:00
intent . putExtra ( EXTRA _NODEINFO , info )
2020-01-26 11:33:51 -08:00
explicitBroadcast ( intent )
2020-01-22 22:16:30 -08:00
}
2020-01-22 21:25:31 -08:00
2020-02-04 12:12:29 -08:00
/// Safely access the radio service, if not connected an exception will be thrown
private val connectedRadio : IRadioInterfaceService
get ( ) {
val s = radioService
if ( s == null || !is Connected )
throw RadioNotConnectedException ( )
return s
}
2020-01-27 19:23:34 -08:00
/// Send a command/packet to our radio. But cope with the possiblity that we might start up
/// before we are fully bound to the RadioInterfaceService
2020-01-24 20:35:42 -08:00
private fun sendToRadio ( p : ToRadio . Builder ) {
2020-01-27 19:23:34 -08:00
val b = p . build ( ) . toByteArray ( )
2020-02-04 12:12:29 -08:00
connectedRadio . sendToRadio ( b )
2020-01-24 20:35:42 -08:00
}
2020-01-26 11:33:51 -08:00
override fun onBind ( intent : Intent ? ) : IBinder ? {
2020-01-22 21:25:31 -08:00
return binder
}
2020-01-27 16:00:00 -08:00
private val radioConnection = object : ServiceConnection {
override fun onServiceConnected ( name : ComponentName ? , service : IBinder ? ) {
2020-02-10 15:31:56 -08:00
val m = IRadioInterfaceService . Stub . asInterface (
service
)
2020-01-27 16:00:00 -08:00
radioService = m
}
override fun onServiceDisconnected ( name : ComponentName ? ) {
radioService = null
}
}
2020-02-04 13:24:04 -08:00
@RequiresApi ( Build . VERSION_CODES . O )
private fun createNotificationChannel ( ) : String {
val channelId = " my_service "
val channelName = " My Background Service "
val chan = NotificationChannel (
channelId ,
channelName , NotificationManager . IMPORTANCE _HIGH
)
chan . lightColor = Color . BLUE
chan . importance = NotificationManager . IMPORTANCE _NONE
chan . lockscreenVisibility = Notification . VISIBILITY _PRIVATE
val service = getSystemService ( Context . NOTIFICATION _SERVICE ) as NotificationManager
service . createNotificationChannel ( chan )
return channelId
}
private fun startForeground ( ) {
2020-02-09 05:52:17 -08:00
// val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2020-02-04 13:24:04 -08:00
val channelId =
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . O ) {
createNotificationChannel ( )
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
" "
}
val notificationBuilder = NotificationCompat . Builder ( this , channelId )
val notification = notificationBuilder . setOngoing ( true )
. setPriority ( PRIORITY _MIN )
. setCategory ( Notification . CATEGORY _SERVICE )
. setSmallIcon ( android . R . drawable . stat _sys _data _bluetooth )
//.setContentTitle("Meshtastic") // leave this off for now so our notification looks smaller
//.setContentText("Listening for mesh...")
. build ( )
startForeground ( 101 , notification )
}
2020-01-24 17:05:55 -08:00
override fun onCreate ( ) {
super . onCreate ( )
2020-01-25 10:00:57 -08:00
info ( " Creating mesh service " )
2020-01-24 20:35:42 -08:00
2020-02-04 13:24:04 -08:00
/ *
// This intent will be used if the user clicks on the item in the status bar
val notificationIntent = Intent ( this , MainActivity :: class . java )
val pendingIntent = PendingIntent . getActivity (
this , 0 ,
notificationIntent , 0
)
val notification : Notification = NotificationCompat . Builder ( this )
. setSmallIcon ( android . R . drawable . stat _sys _data _bluetooth )
. setContentTitle ( " Meshtastic " )
. setContentText ( " Listening for mesh... " )
. setContentIntent ( pendingIntent ) . build ( )
// We are required to call this within a few seconds of create
startForeground ( 1337 , notification )
* /
startForeground ( )
2020-02-04 12:12:29 -08:00
// we listen for messages from the radio receiver _before_ trying to create the service
2020-02-04 13:24:04 -08:00
val filter = IntentFilter ( )
filter . addAction ( RadioInterfaceService . RECEIVE _FROMRADIO _ACTION )
2020-02-10 15:31:56 -08:00
filter . addAction ( RadioInterfaceService . RADIO _CONNECTED _ACTION )
2020-02-04 12:12:29 -08:00
registerReceiver ( radioInterfaceReceiver , filter )
2020-01-27 16:00:00 -08:00
// We in turn need to use the radiointerface service
val intent = Intent ( this , RadioInterfaceService :: class . java )
// intent.action = IMeshService::class.java.name
logAssert ( bindService ( intent , radioConnection , Context . BIND _AUTO _CREATE ) )
2020-01-24 20:35:42 -08:00
2020-01-27 16:00:00 -08:00
// the rest of our init will happen once we are in radioConnection.onServiceConnected
2020-01-24 17:05:55 -08:00
}
2020-01-27 16:00:00 -08:00
2020-01-24 17:05:55 -08:00
override fun onDestroy ( ) {
2020-01-25 10:00:57 -08:00
info ( " Destroying mesh service " )
2020-02-04 12:12:29 -08:00
unregisterReceiver ( radioInterfaceReceiver )
2020-01-27 16:00:00 -08:00
unbindService ( radioConnection )
radioService = null
2020-01-24 17:05:55 -08:00
super . onDestroy ( )
}
2020-01-24 22:22:30 -08:00
///
/// BEGINNING OF MODEL - FIXME, move elsewhere
///
2020-02-16 14:22:24 -08:00
/// special broadcast address
val NODENUM _BROADCAST = 255
// MyNodeInfo sent via special protobuf from radio
data class MyNodeInfo ( val myNodeNum : Int , val hasGPS : Boolean )
var myNodeInfo : MyNodeInfo ? = null
2020-01-24 22:22:30 -08:00
/// Is our radio connected to the phone?
private var isConnected = false
2020-01-24 20:35:42 -08:00
// The database of active nodes, index is the node number
private val nodeDBbyNodeNum = mutableMapOf < Int , NodeInfo > ( )
/// The database of active nodes, index is the node user ID string
2020-01-24 22:22:30 -08:00
/// NOTE: some NodeInfos might be in only nodeDBbyNodeNum (because we don't yet know
/// an ID). But if a NodeInfo is in both maps, it must be one instance shared by
/// both datastructures.
2020-01-24 20:35:42 -08:00
private val nodeDBbyID = mutableMapOf < String , NodeInfo > ( )
2020-01-24 22:22:30 -08:00
///
/// END OF MODEL
///
2020-01-24 20:35:42 -08:00
2020-01-24 22:22:30 -08:00
/// Map a nodenum to a node, or throw an exception if not found
2020-02-10 15:31:56 -08:00
private fun toNodeInfo ( n : Int ) = nodeDBbyNodeNum [ n ] ?: throw NodeNumNotFoundException (
n
)
2020-01-24 22:22:30 -08:00
/// Map a nodenum to the nodeid string, or throw an exception if not present
private fun toNodeID ( n : Int ) = toNodeInfo ( n ) . user ?. id
/// given a nodenum, return a db entry - creating if necessary
2020-01-25 06:16:10 -08:00
private fun getOrCreateNodeInfo ( n : Int ) =
nodeDBbyNodeNum . getOrPut ( n ) { -> NodeInfo ( n ) }
2020-01-24 22:22:30 -08:00
/// Map a userid to a node/ node num, or throw an exception if not found
2020-01-25 11:40:13 -08:00
private fun toNodeInfo ( id : String ) =
nodeDBbyID [ id ]
2020-02-10 15:31:56 -08:00
?: throw IdNotFoundException (
id
)
2020-01-26 15:01:59 -08:00
// ?: getOrCreateNodeInfo(10) // FIXME hack for now - throw IdNotFoundException(id)
2020-01-24 22:22:30 -08:00
private fun toNodeNum ( id : String ) = toNodeInfo ( id ) . num
/// A helper function that makes it easy to update node info objects
private fun updateNodeInfo ( nodeNum : Int , updatefn : ( NodeInfo ) -> Unit ) {
val info = getOrCreateNodeInfo ( nodeNum )
updatefn ( info )
2020-02-04 12:12:29 -08:00
// This might have been the first time we know an ID for this node, so also update the by ID map
2020-02-14 04:41:20 -08:00
val userId = info . user ?. id . orEmpty ( )
if ( userId . isNotEmpty ( ) )
2020-02-04 12:12:29 -08:00
nodeDBbyID [ userId ] = info
2020-02-09 05:52:17 -08:00
2020-02-09 10:18:26 -08:00
// parcelable is busted
2020-02-10 16:34:01 -08:00
broadcastNodeChange ( info )
2020-01-24 22:22:30 -08:00
}
2020-02-17 13:14:53 -08:00
/// My node num
private val myNodeNum get ( ) = myNodeInfo !! . myNodeNum
/// My node ID string
private val myNodeID get ( ) = toNodeID ( myNodeNum )
2020-01-24 22:22:30 -08:00
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
private fun newMeshPacketTo ( idNum : Int ) = MeshPacket . newBuilder ( ) . apply {
2020-02-16 14:22:24 -08:00
if ( myNodeInfo == null )
2020-01-25 12:24:53 -08:00
throw RadioNotConnectedException ( )
2020-02-17 13:14:53 -08:00
from = myNodeNum
2020-01-24 20:35:42 -08:00
to = idNum
}
2020-02-17 15:39:49 -08:00
/ * *
* Generate a new mesh packet builder with our node as the sender , and the specified recipient
*
* If id is null we assume a broadcast message
* /
private fun newMeshPacketTo ( id : String ? ) =
newMeshPacketTo ( if ( id != null ) toNodeNum ( id ) else NODENUM _BROADCAST )
2020-01-24 22:22:30 -08:00
2020-02-17 15:39:49 -08:00
/ * *
* Helper to make it easy to build a subpacket in the proper protobufs
*
* If destId is null we assume a broadcast message
* /
2020-01-25 10:00:57 -08:00
private fun buildMeshPacket (
2020-02-17 15:39:49 -08:00
destId : String ? ,
2020-01-25 10:00:57 -08:00
initFn : MeshProtos . SubPacket . Builder . ( ) -> Unit
) : MeshPacket = newMeshPacketTo ( destId ) . apply {
2020-02-02 18:38:01 -08:00
payload = MeshProtos . SubPacket . newBuilder ( ) . also {
initFn ( it )
2020-01-25 10:00:57 -08:00
} . build ( )
} . build ( )
2020-01-25 06:16:10 -08:00
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedData ( fromNum : Int , data : MeshProtos . Data ) {
val bytes = data . payload . toByteArray ( )
val fromId = toNodeID ( fromNum )
/// the sending node ID if possible, else just its number
val fromString = fromId ?: fromId . toString ( )
2020-02-09 05:52:17 -08:00
fun forwardData ( ) {
if ( fromId == null )
warn ( " Ignoring data from $fromNum because we don't yet know its ID " )
else {
debug ( " Received data from $fromId ${bytes.size} " )
broadcastReceivedData ( fromId , bytes , data . typValue )
}
}
2020-01-25 06:16:10 -08:00
when ( data . typValue ) {
2020-02-09 05:52:17 -08:00
MeshProtos . Data . Type . CLEAR _TEXT _VALUE -> {
debug (
" FIXME - don't long this: Received CLEAR_TEXT from $fromString : $ {bytes.toString(
2020-01-25 06:16:10 -08:00
Charset . forName ( " UTF-8 " )
) } "
)
2020-02-09 05:52:17 -08:00
forwardData ( )
}
2020-01-25 06:16:10 -08:00
MeshProtos . Data . Type . CLEAR _READACK _VALUE ->
warn (
" TODO ignoring CLEAR_READACK from $fromString "
)
MeshProtos . Data . Type . SIGNAL _OPAQUE _VALUE ->
2020-02-09 05:52:17 -08:00
forwardData ( )
2020-01-25 06:16:10 -08:00
else -> TODO ( )
}
}
2020-01-25 10:00:57 -08:00
/// Update our DB of users based on someone sending out a User subpacket
private fun handleReceivedUser ( fromNum : Int , p : MeshProtos . User ) {
updateNodeInfo ( fromNum ) {
2020-02-10 15:31:56 -08:00
it . user = MeshUser (
p . id ,
p . longName ,
p . shortName
)
2020-01-25 10:00:57 -08:00
}
}
2020-02-16 14:22:24 -08:00
/// Update our DB of users based on someone sending out a Position subpacket
private fun handleReceivedPosition ( fromNum : Int , p : MeshProtos . Position ) {
updateNodeInfo ( fromNum ) {
it . position = Position (
p . latitude ,
p . longitude ,
p . altitude
)
}
}
2020-01-24 22:22:30 -08:00
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedMeshPacket ( packet : MeshPacket ) {
val fromNum = packet . from
// FIXME, perhaps we could learn our node ID by looking at any to packets the radio
// decided to pass through to us (except for broadcast packets)
val toNum = packet . to
2020-02-02 18:38:01 -08:00
val p = packet . payload
2020-02-09 05:52:17 -08:00
// Update our last seen based on any valid timestamps
2020-02-12 14:15:59 -08:00
if ( packet . rxTime != 0 ) {
2020-02-09 05:52:17 -08:00
updateNodeInfo ( fromNum ) {
it . lastSeen = packet . rxTime
}
}
2020-02-02 18:38:01 -08:00
when ( p . variantCase . number ) {
MeshProtos . SubPacket . POSITION _FIELD _NUMBER ->
2020-02-16 14:22:24 -08:00
handleReceivedPosition ( fromNum , p . position )
2020-02-02 18:38:01 -08:00
MeshProtos . SubPacket . DATA _FIELD _NUMBER ->
handleReceivedData ( fromNum , p . data )
MeshProtos . SubPacket . USER _FIELD _NUMBER ->
handleReceivedUser ( fromNum , p . user )
else -> TODO ( " Unexpected SubPacket variant " )
2020-01-24 22:22:30 -08:00
}
}
2020-01-24 20:35:42 -08:00
2020-02-04 12:12:29 -08:00
/// Called when we gain/lose connection to our radio
private fun onConnectionChanged ( c : Boolean ) {
2020-02-04 13:24:04 -08:00
debug ( " onConnectionChanged connected= $c " )
2020-02-04 12:12:29 -08:00
isConnected = c
if ( c ) {
// Do our startup init
2020-02-17 18:46:20 -08:00
try {
// FIXME - don't do this until after we see that the radio is connected to the phone
//val sim = SimRadio(this@MeshService)
//sim.start() // Fake up our node id info and some past packets from other nodes
2020-02-04 12:12:29 -08:00
2020-02-17 18:46:20 -08:00
val myInfo = MeshProtos . MyNodeInfo . parseFrom (
connectedRadio . readMyNode ( )
)
2020-02-04 12:12:29 -08:00
2020-02-17 18:46:20 -08:00
val mynodeinfo = MyNodeInfo ( myInfo . myNodeNum , myInfo . hasGps )
myNodeInfo = mynodeinfo
// Ask for the current node DB
connectedRadio . restartNodeInfo ( )
// read all the infos until we get back null
var infoBytes = connectedRadio . readNodeInfo ( )
while ( infoBytes != null ) {
val info =
MeshProtos . NodeInfo . parseFrom ( infoBytes )
debug ( " Received initial nodeinfo $info " )
// Just replace/add any entry
updateNodeInfo ( info . num ) {
if ( info . hasUser ( ) )
it . user =
MeshUser (
info . user . id ,
info . user . longName ,
info . user . shortName
)
if ( info . hasPosition ( ) )
it . position = Position (
info . position . latitude ,
info . position . longitude ,
info . position . altitude
2020-02-10 15:31:56 -08:00
)
2020-02-04 12:12:29 -08:00
2020-02-17 18:46:20 -08:00
it . lastSeen = info . lastSeen
}
2020-02-04 12:12:29 -08:00
2020-02-17 18:46:20 -08:00
// advance to next
infoBytes = connectedRadio . readNodeInfo ( )
2020-02-04 12:12:29 -08:00
}
2020-02-17 18:46:20 -08:00
// we don't ask for GPS locations from android if our device has a built in GPS
if ( ! mynodeinfo . hasGPS )
startLocationRequests ( )
else
debug ( " Our radio has a built in GPS, so not reading GPS in phone " )
} catch ( ex : RemoteException ) {
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which
// causes the phone to try and reconnect. If we fail downloading our initial radio state we don't want to
// claim we have a valid connection still
isConnected = false ;
throw ex ; // Important to rethrow so that we don't tell the app all is well
2020-02-04 12:12:29 -08:00
}
2020-02-16 14:22:24 -08:00
} else {
// lost radio connection, therefore no need to keep listening to GPS
stopLocationRequests ( )
2020-02-04 12:12:29 -08:00
}
2020-01-25 06:16:10 -08:00
}
2020-01-24 17:05:55 -08:00
/ * *
* Receives messages from our BT radio service and processes them to update our model
* and send to clients as needed .
* /
private val radioInterfaceReceiver = object : BroadcastReceiver ( ) {
2020-02-04 13:24:04 -08:00
// Important to never throw exceptions out of onReceive
override fun onReceive ( context : Context , intent : Intent ) = exceptionReporter {
2020-02-04 12:12:29 -08:00
2020-02-04 13:24:04 -08:00
debug ( " Received broadcast ${intent.action} " )
2020-02-04 12:12:29 -08:00
when ( intent . action ) {
2020-02-10 15:31:56 -08:00
RadioInterfaceService . RADIO _CONNECTED _ACTION -> {
2020-02-17 20:00:11 -08:00
try {
onConnectionChanged ( intent . getBooleanExtra ( EXTRA _CONNECTED , false ) )
// forward the connection change message to anyone who is listening to us. but change the action
// to prevent an infinite loop from us receiving our own broadcast. ;-)
intent . action = ACTION _MESH _CONNECTED
explicitBroadcast ( intent )
} catch ( ex : RemoteException ) {
// This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics
warn ( " Abandoning reconnect attempt, due to errors during init: ${ex.message} " )
}
2020-02-04 12:12:29 -08:00
}
RadioInterfaceService . RECEIVE _FROMRADIO _ACTION -> {
val proto =
2020-02-10 15:31:56 -08:00
MeshProtos . FromRadio . parseFrom (
intent . getByteArrayExtra (
EXTRA _PAYLOAD
) !!
)
2020-02-04 12:12:29 -08:00
info ( " Received from radio service: ${proto.toOneLineString()} " )
when ( proto . variantCase . number ) {
2020-02-09 05:52:17 -08:00
MeshProtos . FromRadio . PACKET _FIELD _NUMBER -> handleReceivedMeshPacket (
proto . packet
)
2020-02-04 12:12:29 -08:00
else -> TODO ( " Unexpected FromRadio variant " )
}
}
else -> TODO ( " Unexpected radio interface broadcast " )
2020-01-24 22:22:30 -08:00
}
2020-01-24 17:05:55 -08:00
}
}
2020-02-16 14:22:24 -08:00
/// Send a position (typically from our built in GPS) into the mesh
private fun sendPosition ( lat : Double , lon : Double , alt : Int ) {
debug ( " Sending our position into mesh lat= $lat , lon= $lon , alt= $alt " )
val destNum = NODENUM _BROADCAST
val position = MeshProtos . Position . newBuilder ( ) . also {
it . latitude = lat
it . longitude = lon
it . altitude = alt
} . build ( )
// encapsulate our payload in the proper protobufs and fire it off
val packet = newMeshPacketTo ( destNum )
packet . payload = MeshProtos . SubPacket . newBuilder ( ) . also {
it . position = position
} . build ( )
// Also update our own map for our nodenum, by handling the packet just like packets from other users
handleReceivedPosition ( myNodeInfo !! . myNodeNum , position )
// send the packet into the mesh
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . packet = packet . build ( )
} )
}
2020-01-22 21:25:31 -08:00
private val binder = object : IMeshService . Stub ( ) {
2020-01-25 10:00:57 -08:00
// Note: bound methods don't get properly exception caught/logged, so do that with a wrapper
// per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63
2020-01-26 11:33:51 -08:00
override fun subscribeReceiver ( packageName : String , receiverName : String ) =
toRemoteExceptions {
clientPackages [ receiverName ] = packageName
}
2020-01-22 21:25:31 -08:00
2020-02-17 13:14:53 -08:00
override fun getMyId ( ) = toRemoteExceptions { myNodeID }
2020-02-14 04:41:20 -08:00
override fun setOwner ( myId : String ? , longName : String , shortName : String ) =
2020-01-26 10:44:42 -08:00
toRemoteExceptions {
2020-02-14 04:41:20 -08:00
debug ( " SetOwner $myId : $longName : $shortName " )
2020-01-25 10:00:57 -08:00
val user = MeshProtos . User . newBuilder ( ) . also {
2020-02-14 04:41:20 -08:00
if ( myId != null ) // Only set the id if it was provided
it . id = myId
2020-01-25 10:00:57 -08:00
it . longName = longName
it . shortName = shortName
2020-01-24 20:35:42 -08:00
} . build ( )
2020-01-25 10:00:57 -08:00
// Also update our own map for our nodenum, by handling the packet just like packets from other users
2020-02-16 14:22:24 -08:00
if ( myNodeInfo != null ) {
handleReceivedUser ( myNodeInfo !! . myNodeNum , user )
2020-01-25 10:00:57 -08:00
}
2020-02-04 12:12:29 -08:00
// set my owner info
connectedRadio . writeOwner ( user . toByteArray ( ) )
2020-01-25 10:00:57 -08:00
}
2020-02-17 15:39:49 -08:00
override fun sendData ( destId : String ? , payloadIn : ByteArray , typ : Int ) =
2020-01-26 10:44:42 -08:00
toRemoteExceptions {
2020-02-17 15:39:49 -08:00
info ( " sendData dest= $destId <- ${payloadIn.size} bytes " )
2020-01-25 10:00:57 -08:00
// encapsulate our payload in the proper protobufs and fire it off
val packet = buildMeshPacket ( destId ) {
data = MeshProtos . Data . newBuilder ( ) . also {
2020-02-17 18:46:20 -08:00
it . typ = MeshProtos . Data . Type . forNumber ( typ )
2020-01-25 10:00:57 -08:00
it . payload = ByteString . copyFrom ( payloadIn )
} . build ( )
}
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . packet = packet
} )
}
2020-01-22 21:25:31 -08:00
2020-02-11 19:19:56 -08:00
override fun getRadioConfig ( ) : ByteArray = toRemoteExceptions {
2020-02-12 14:15:59 -08:00
connectedRadio . readRadioConfig ( )
2020-02-11 19:19:56 -08:00
}
override fun setRadioConfig ( payload : ByteArray ) = toRemoteExceptions {
connectedRadio . writeRadioConfig ( payload )
}
2020-02-10 17:05:51 -08:00
override fun getNodes ( ) : Array < NodeInfo > = toRemoteExceptions {
val r = nodeDBbyID . values . toTypedArray ( )
2020-01-24 20:46:29 -08:00
info ( " in getOnline, count= ${r.size} " )
2020-01-24 20:35:42 -08:00
// return arrayOf("+16508675309")
2020-01-25 10:00:57 -08:00
r
2020-01-22 21:25:31 -08:00
}
2020-01-26 10:44:42 -08:00
override fun isConnected ( ) : Boolean = toRemoteExceptions {
2020-01-24 20:46:29 -08:00
val r = this @MeshService . isConnected
2020-02-04 13:24:04 -08:00
info ( " in isConnected= $r " )
2020-01-25 10:00:57 -08:00
r
2020-01-22 21:25:31 -08:00
}
}
}