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-28 13:53:16 -08:00
import android.app.*
2020-02-25 08:10:23 -08:00
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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-05-30 19:58:36 -07:00
import android.os.Parcelable
2020-02-17 18:46:20 -08:00
import android.os.RemoteException
2020-04-25 07:33:51 -07:00
import android.widget.Toast
2020-02-04 13:24:04 -08:00
import androidx.annotation.RequiresApi
2020-04-04 14:37:44 -07:00
import androidx.annotation.UiThread
2020-02-04 13:24:04 -08:00
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MIN
2020-04-19 17:25:20 -07:00
import androidx.core.content.edit
2020-05-11 13:12:44 -07:00
import androidx.work.*
2020-02-25 09:28:47 -08:00
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
2020-01-22 22:16:30 -08:00
import com.geeksville.android.Logging
2020-02-25 08:10:23 -08:00
import com.geeksville.android.ServiceClient
2020-05-14 11:47:24 -07:00
import com.geeksville.concurrent.handledLaunch
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-04-25 07:33:51 -07:00
import com.geeksville.mesh.R
2020-04-20 20:44:21 -07:00
import com.geeksville.util.*
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-05-11 11:44:24 -07:00
import com.google.protobuf.InvalidProtocolBufferException
2020-04-04 14:37:44 -07:00
import kotlinx.coroutines.*
2020-04-19 17:25:20 -07:00
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
2020-05-31 11:23:25 -07:00
import java.util.*
2020-05-11 13:12:44 -07:00
import java.util.concurrent.TimeUnit
2020-05-31 11:23:25 -07:00
import kotlin.math.absoluteValue
2020-01-24 17:05:55 -08:00
2020-02-04 13:24:04 -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
2020-06-07 17:11:30 -07:00
/// special broadcast address
const val NODENUM _BROADCAST = ( 0xffffffff ) . toInt ( )
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-05-30 19:58:36 -07:00
const val ACTION _MESSAGE _STATUS = " $prefix .MESSAGE_STATUS "
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-05-14 11:47:24 -07:00
// class NotInMeshException() : Exception("We are not yet in a mesh")
2020-01-25 12:24:53 -08:00
2020-05-11 13:12:44 -07:00
/ * * A little helper that just calls startService
* /
class ServiceStarter ( appContext : Context , workerParams : WorkerParameters ) :
Worker ( appContext , workerParams ) {
override fun doWork ( ) : Result = try {
startService ( this . applicationContext )
// Indicate whether the task finished successfully with the Result
Result . success ( )
} catch ( ex : Exception ) {
errormsg ( " failure starting service, will retry " , ex )
Result . retry ( )
}
}
2020-06-07 20:17:47 -07:00
/ * *
* Talk to our running service and try to set a new device address . And then immediately
* call start on the service to possibly promote our service to be a foreground service .
* /
fun changeDeviceAddress ( context : Context , service : IMeshService , address : String ? ) {
service . setDeviceAddress ( address )
startService ( context )
}
2020-05-11 13:12:44 -07:00
/ * *
* Just after boot the android OS is super busy , so if we call startForegroundService then , our
* thread might be stalled long enough to expose this google / samsung bug :
* https : //issuetracker.google.com/issues/76112072#comment56
* /
fun startLater ( context : Context ) {
2020-06-07 17:27:19 -07:00
// No point in even starting the service if the user doesn't have a device bonded
2020-06-07 20:17:47 -07:00
info ( " Received boot complete announcement, starting mesh service in one minute " )
val delayRequest = OneTimeWorkRequestBuilder < ServiceStarter > ( )
. setInitialDelay ( 1 , TimeUnit . MINUTES )
. setBackoffCriteria ( BackoffPolicy . EXPONENTIAL , 1 , TimeUnit . MINUTES )
. addTag ( " startLater " )
. build ( )
WorkManager . getInstance ( context ) . enqueue ( delayRequest )
2020-05-11 13:12:44 -07:00
}
val intent = Intent ( ) . apply {
setClassName (
" com.geeksville.mesh " ,
" com.geeksville.mesh.service.MeshService "
)
}
/// Helper function to start running our service
fun startService ( context : Context ) {
2020-04-19 19:23:20 -07:00
// 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
2020-05-11 13:12:44 -07:00
2020-02-09 05:52:17 -08:00
2020-04-19 19:23:20 -07: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.
2020-05-11 13:12:44 -07:00
info ( " Trying to start service " )
val compName =
2020-04-19 19:23:20 -07:00
( if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . O ) {
context . startForegroundService ( intent )
} else {
context . startService ( intent )
2020-05-11 13:12:44 -07:00
} )
2020-02-12 15:47:06 -08:00
2020-05-11 13:12:44 -07:00
if ( compName == null )
throw Exception ( " Failed to start service " )
2020-02-12 15:47:06 -08:00
}
2020-01-25 10:00:57 -08:00
}
2020-04-19 17:25:20 -07:00
enum class ConnectionState {
2020-04-04 15:29:16 -07:00
DISCONNECTED ,
CONNECTED ,
DEVICE _SLEEP // device is in LS sleep state, it will reconnected to us over bluetooth once it has data
}
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-02-25 08:10:23 -08:00
val radio = ServiceClient {
2020-06-11 11:20:51 -07:00
val stub = IRadioInterfaceService . Stub . asInterface ( it )
// Now that we are connected to the radio service, tell it to connect to the radio
stub . connect ( )
stub
2020-02-25 08:10:23 -08:00
}
2020-01-27 16:00:00 -08:00
2020-04-04 14:37:44 -07:00
private val serviceJob = Job ( )
private val serviceScope = CoroutineScope ( Dispatchers . IO + serviceJob )
2020-04-04 15:29:16 -07:00
/// The current state of our connection
private var connectionState = ConnectionState . DISCONNECTED
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 ( ) {
2020-02-19 18:51:59 -08:00
private var lastSendMsec = 0L
2020-02-16 14:22:24 -08:00
override fun onLocationResult ( locationResult : LocationResult ) {
2020-04-04 14:37:44 -07:00
serviceScope . handledLaunch {
2020-02-24 20:08:18 -08:00
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 ) {
2020-04-13 15:23:46 -07:00
info ( " got phone location " )
2020-02-24 20:08:18 -08:00
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 {
val now = System . currentTimeMillis ( )
// we limit our sends onto the lora net to a max one once every FIXME
val sendLora = ( now - lastSendMsec >= 30 * 1000 )
if ( sendLora )
lastSendMsec = now
try {
sendPosition (
l . latitude , l . longitude , l . altitude . toInt ( ) ,
destNum = if ( sendLora ) NODENUM _BROADCAST else myNodeNum ,
wantResponse = sendLora
)
2020-04-20 10:15:43 -07:00
} catch ( ex : RemoteException ) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
2020-02-24 20:08:18 -08:00
warn ( " Lost connection to radio, stopping location requests " )
2020-04-04 15:29:16 -07:00
onConnectionChanged ( ConnectionState . DEVICE _SLEEP )
2020-05-24 09:39:27 -07:00
} catch ( ex : BLEException ) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
warn ( " BLE exception, stopping location requests $ex " )
onConnectionChanged ( ConnectionState . DEVICE _SLEEP )
2020-02-24 20:08:18 -08:00
}
}
2020-02-16 14:22:24 -08:00
}
}
}
}
private var fusedLocationClient : FusedLocationProviderClient ? = null
2020-04-25 07:33:51 -07:00
private fun warnUserAboutLocation ( ) {
Toast . makeText (
this ,
getString ( R . string . location _disabled ) ,
Toast . LENGTH _LONG
) . show ( )
}
2020-02-16 14:22:24 -08:00
/ * *
2020-02-19 10:53:36 -08:00
* start our location requests ( if they weren ' t already running )
2020-02-16 14:22:24 -08:00
*
* per https : //developer.android.com/training/location/change-location-settings
* /
@SuppressLint ( " MissingPermission " )
2020-04-04 14:37:44 -07:00
@UiThread
2020-02-16 14:22:24 -08:00
private fun startLocationRequests ( ) {
2020-02-19 10:53:36 -08:00
if ( fusedLocationClient == null ) {
2020-02-25 09:28:47 -08:00
GeeksvilleApplication . analytics . track ( " location_start " ) // Figure out how many users needed to use the phone GPS
2020-02-19 10:53:36 -08:00
val request = LocationRequest . create ( ) . apply {
interval =
2020-02-24 18:10:25 -08:00
5 * 60 * 1000 // FIXME, do more like once every 5 mins while we are connected to our radio _and_ someone else is in the mesh
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
priority = LocationRequest . PRIORITY _HIGH _ACCURACY
}
val builder = LocationSettingsRequest . Builder ( ) . addLocationRequest ( request )
val locationClient = LocationServices . getSettingsClient ( this )
val locationSettingsResponse = locationClient . checkLocationSettings ( builder . build ( ) )
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
locationSettingsResponse . addOnSuccessListener {
debug ( " We are now successfully listening to the GPS " )
}
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
locationSettingsResponse . addOnFailureListener { exception ->
2020-02-29 13:21:05 -08:00
errormsg ( " Failed to listen to GPS " )
2020-02-19 10:53:36 -08:00
if ( exception is ResolvableApiException ) {
exceptionReporter {
// Location settings are not satisfied, but this can be fixed
// by showing the user a dialog.
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
2020-04-25 07:33:51 -07:00
// exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS)
// For now just punt and show a dialog
warnUserAboutLocation ( )
2020-02-19 10:53:36 -08:00
}
} else
2020-02-24 15:33:35 -08:00
Exceptions . report ( exception )
2020-02-19 10:53:36 -08:00
}
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
val client = LocationServices . getFusedLocationProviderClient ( this )
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
// 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 )
2020-02-16 14:22:24 -08:00
2020-02-19 10:53:36 -08:00
fusedLocationClient = client
}
2020-02-16 14:22:24 -08:00
}
private fun stopLocationRequests ( ) {
2020-02-19 10:53:36 -08:00
if ( fusedLocationClient != null ) {
debug ( " Stopping location requests " )
2020-02-25 09:28:47 -08:00
GeeksvilleApplication . analytics . track ( " location_stop " )
2020-02-19 10:53:36 -08:00
fusedLocationClient ?. removeLocationUpdates ( locationCallback )
fusedLocationClient = null
}
2020-02-16 14:22:24 -08:00
}
2020-02-10 15:31:56 -08:00
2020-01-24 17:05:55 -08:00
/ * *
* The RECEIVED _OPAQUE :
2020-04-19 11:47:34 -07:00
* Payload will be a DataPacket
2020-01-24 17:05:55 -08:00
* /
2020-04-19 11:47:34 -07:00
private fun broadcastReceivedData ( payload : DataPacket ) {
2020-02-09 05:52:17 -08:00
val intent = Intent ( ACTION _RECEIVED _DATA )
2020-01-24 17:05:55 -08:00
intent . putExtra ( EXTRA _PAYLOAD , payload )
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-25 09:28:47 -08:00
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-05-30 19:58:36 -07:00
private fun broadcastMessageStatus ( p : DataPacket ) {
if ( p . id == 0 ) {
debug ( " Ignoring anonymous packet status " )
} else {
debug ( " Broadcasting message status $p " )
val intent = Intent ( ACTION _MESSAGE _STATUS )
intent . putExtra ( EXTRA _PACKET _ID , p . id )
intent . putExtra ( EXTRA _STATUS , p . status as Parcelable )
explicitBroadcast ( intent )
}
}
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
2020-04-04 15:29:16 -07:00
get ( ) = ( if ( connectionState == ConnectionState . CONNECTED ) radio . serviceP else null )
?: throw RadioNotConnectedException ( )
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-04-22 07:59:07 -07:00
/ * *
* Send a mesh packet to the radio , if the radio is not currently connected this function will throw NotConnectedException
* /
2020-04-23 11:02:44 -07:00
private fun sendToRadio ( packet : MeshPacket ) {
2020-04-22 07:59:07 -07:00
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . packet = packet
} )
}
2020-01-22 21:25:31 -08:00
2020-02-04 13:24:04 -08:00
@RequiresApi ( Build . VERSION_CODES . O )
private fun createNotificationChannel ( ) : String {
val channelId = " my_service "
2020-06-16 10:09:22 -07:00
val channelName = getString ( R . string . meshtastic _service _notifications )
2020-02-04 13:24:04 -08:00
val chan = NotificationChannel (
channelId ,
channelName , NotificationManager . IMPORTANCE _HIGH
)
chan . lightColor = Color . BLUE
chan . importance = NotificationManager . IMPORTANCE _NONE
chan . lockscreenVisibility = Notification . VISIBILITY _PRIVATE
2020-02-28 20:09:00 -08:00
notificationManager . createNotificationChannel ( chan )
2020-02-04 13:24:04 -08:00
return channelId
}
2020-02-28 13:53:16 -08:00
private val notifyId = 101
val notificationManager : NotificationManager by lazy ( ) {
getSystemService ( Context . NOTIFICATION _SERVICE ) as NotificationManager
}
2020-02-04 13:24:04 -08:00
2020-02-28 20:09:00 -08:00
/// This must be lazy because we use Context
private val channelId : String by lazy ( ) {
2020-02-28 13:53:16 -08:00
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)
" "
}
2020-02-28 20:09:00 -08:00
}
2020-02-28 13:53:16 -08:00
2020-02-28 20:09:00 -08:00
private val openAppIntent : PendingIntent by lazy ( ) {
PendingIntent . getActivity ( this , 0 , Intent ( this , MainActivity :: class . java ) , 0 )
}
2020-02-28 14:07:04 -08:00
/// A text message that has a arrived since the last notification update
2020-04-19 11:47:34 -07:00
private var recentReceivedText : DataPacket ? = null
2020-02-28 13:53:16 -08:00
2020-04-04 15:29:16 -07:00
private val summaryString
get ( ) = when ( connectionState ) {
2020-05-01 13:43:57 -07:00
ConnectionState . CONNECTED -> getString ( R . string . connected _count ) . format (
numOnlineNodes ,
numNodes
)
ConnectionState . DISCONNECTED -> getString ( R . string . disconnected )
ConnectionState . DEVICE _SLEEP -> getString ( R . string . device _sleeping )
2020-04-04 15:29:16 -07:00
}
2020-02-28 13:53:16 -08:00
2020-06-30 11:13:18 -07:00
// Note: do not override toString, it causes infinite recursion on some androids (because contextWrapper.getResources calls to string)
// override fun toString() = summaryString
2020-02-04 13:24:04 -08:00
2020-02-28 13:53:16 -08:00
/ * *
* Generate a new version of our notification - reflecting current app state
* /
private fun createNotification ( ) : Notification {
2020-02-28 20:09:00 -08:00
val notificationBuilder = NotificationCompat . Builder ( this , channelId )
2020-02-28 13:53:16 -08:00
val builder = notificationBuilder . setOngoing ( true )
2020-02-04 13:24:04 -08:00
. setPriority ( PRIORITY _MIN )
2020-02-28 14:07:04 -08:00
. setCategory ( if ( recentReceivedText != null ) Notification . CATEGORY _SERVICE else Notification . CATEGORY _MESSAGE )
2020-02-04 13:24:04 -08:00
. setSmallIcon ( android . R . drawable . stat _sys _data _bluetooth )
2020-02-28 20:09:00 -08:00
. setContentTitle ( summaryString ) // leave this off for now so our notification looks smaller
2020-02-28 13:53:16 -08:00
. setVisibility ( NotificationCompat . VISIBILITY _PUBLIC )
. setContentIntent ( openAppIntent )
2020-02-04 13:24:04 -08:00
2020-02-28 14:11:42 -08:00
// FIXME, show information about the nearest node
// if(shortContent != null) builder.setContentText(shortContent)
2020-01-24 17:05:55 -08:00
2020-02-28 14:07:04 -08:00
// If a text message arrived include it with our notification
2020-04-19 11:47:34 -07:00
recentReceivedText ?. let { packet ->
2020-04-19 09:33:41 -07:00
// Try to show the human name of the sender if possible
2020-04-19 11:47:34 -07:00
val sender = nodeDBbyID [ packet . from ] ?. user ?. longName ?: packet . from
2020-04-19 09:33:41 -07:00
builder . setContentText ( " Message from $sender " )
2020-02-28 14:07:04 -08:00
2020-02-28 13:53:16 -08:00
builder . setStyle (
NotificationCompat . BigTextStyle ( )
2020-05-30 15:48:50 -07:00
. bigText ( packet . bytes !! . toString ( utf8 ) )
2020-02-28 13:53:16 -08:00
)
2020-02-28 14:07:04 -08:00
}
2020-01-24 20:35:42 -08:00
2020-02-28 13:53:16 -08:00
return builder . build ( )
}
2020-02-04 13:24:04 -08:00
2020-02-28 13:53:16 -08:00
/ * *
* Update our notification with latest data
* /
private fun updateNotification ( ) {
notificationManager . notify ( notifyId , createNotification ( ) )
}
/ * *
* tell android not to kill us
* /
private fun startForeground ( ) {
2020-06-07 17:51:51 -07:00
val wantForeground = RadioInterfaceService . getBondedDeviceAddress ( this ) != null
info ( " Requesting foreground service= $wantForeground " )
2020-06-08 14:19:49 -07:00
// We always start foreground because that's how our service is always started (if we didn't then android would kill us)
// but if we don't really need forground we immediately stop it.
startForeground ( notifyId , createNotification ( ) )
if ( ! wantForeground )
2020-06-07 17:51:51 -07:00
stopForeground ( true )
2020-02-28 13:53:16 -08:00
}
2020-02-04 13:24:04 -08:00
2020-02-28 13:53:16 -08:00
override fun onCreate ( ) {
super . onCreate ( )
2020-02-04 13:24:04 -08:00
2020-02-28 13:53:16 -08:00
info ( " Creating mesh service " )
2020-02-04 13:24:04 -08:00
2020-04-19 17:25:20 -07:00
// Switch to the IO thread
serviceScope . handledLaunch {
loadSettings ( ) // Load our last known node DB
2020-02-04 12:12:29 -08:00
2020-04-19 17:25:20 -07:00
// we listen for messages from the radio receiver _before_ trying to create the service
val filter = IntentFilter ( )
2020-06-07 17:11:30 -07:00
filter . addAction ( RadioInterfaceService . RECEIVE _FROMRADIO _ACTION )
filter . addAction ( RadioInterfaceService . RADIO _CONNECTED _ACTION )
2020-04-19 17:25:20 -07:00
registerReceiver ( radioInterfaceReceiver , filter )
2020-01-24 20:35:42 -08:00
2020-04-19 17:25:20 -07:00
// We in turn need to use the radiointerface service
2020-06-07 17:11:30 -07:00
val intent = Intent ( this @MeshService , RadioInterfaceService :: class . java )
2020-04-19 17:25:20 -07:00
// intent.action = IMeshService::class.java.name
radio . connect ( this @MeshService , intent , Context . BIND _AUTO _CREATE )
// the rest of our init will happen once we are in radioConnection.onServiceConnected
}
2020-01-24 17:05:55 -08:00
}
2020-06-07 17:51:51 -07:00
/ * *
* If someone binds to us , this will be called after on create
* /
override fun onBind ( intent : Intent ? ) : IBinder ? {
startForeground ( )
return binder
}
/ * *
* If someone starts us ( or restarts us ) this will be called after onCreate )
* /
override fun onStartCommand ( intent : Intent ? , flags : Int , startId : Int ) : Int {
startForeground ( )
return super . onStartCommand ( intent , flags , startId )
}
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-04-20 20:44:21 -07:00
// This might fail if we get destroyed before the handledLaunch completes
ignoreException {
unregisterReceiver ( radioInterfaceReceiver )
}
2020-04-21 14:46:52 -07:00
2020-02-25 08:10:23 -08:00
radio . close ( )
2020-04-19 17:25:20 -07:00
saveSettings ( )
2020-01-27 16:00:00 -08:00
2020-01-24 17:05:55 -08:00
super . onDestroy ( )
2020-04-04 14:37:44 -07:00
serviceJob . cancel ( )
2020-01-24 17:05:55 -08:00
}
2020-02-25 08:10:23 -08:00
2020-01-24 22:22:30 -08:00
///
/// BEGINNING OF MODEL - FIXME, move elsewhere
///
2020-04-19 17:25:20 -07:00
/// Our saved preferences as stored on disk
@Serializable
private data class SavedSettings (
val nodeDB : Array < NodeInfo > ,
val myInfo : MyNodeInfo ,
val messages : Array < DataPacket >
) {
override fun equals ( other : Any ? ) : Boolean {
if ( this === other ) return true
if ( javaClass != other ?. javaClass ) return false
other as SavedSettings
if ( ! nodeDB . contentEquals ( other . nodeDB ) ) return false
if ( myInfo != other . myInfo ) return false
if ( ! messages . contentEquals ( other . messages ) ) return false
return true
}
override fun hashCode ( ) : Int {
var result = nodeDB . contentHashCode ( )
result = 31 * result + myInfo . hashCode ( )
result = 31 * result + messages . contentHashCode ( )
return result
}
}
private fun getPrefs ( ) = getSharedPreferences ( " service-prefs " , Context . MODE _PRIVATE )
/// Save information about our mesh to disk, so we will have it when we next start the service (even before we hear from our device)
private fun saveSettings ( ) {
myNodeInfo ?. let { myInfo ->
val settings = SavedSettings (
myInfo = myInfo ,
nodeDB = nodeDBbyNodeNum . values . toTypedArray ( ) ,
messages = recentDataPackets . toTypedArray ( )
)
val json = Json ( JsonConfiguration . Default )
val asString = json . stringify ( SavedSettings . serializer ( ) , settings )
2020-06-09 10:21:54 -07:00
debug ( " Saving settings " )
2020-04-19 17:25:20 -07:00
getPrefs ( ) . edit ( commit = true ) {
// FIXME, not really ideal to store this bigish blob in preferences
putString ( " json " , asString )
}
}
}
2020-04-22 18:34:22 -07:00
/ * *
* Install a new node DB
* /
private fun installNewNodeDB ( newMyNodeInfo : MyNodeInfo , nodes : Array < NodeInfo > ) {
discardNodeDB ( ) // Get rid of any old state
myNodeInfo = newMyNodeInfo
// put our node array into our two different map representations
nodeDBbyNodeNum . putAll ( nodes . map { Pair ( it . num , it ) } )
nodeDBbyID . putAll ( nodes . mapNotNull {
it . user ?. let { user -> // ignore records that don't have a valid user
Pair (
user . id ,
it
)
}
} )
}
2020-04-19 17:25:20 -07:00
/// Load our saved DB state
private fun loadSettings ( ) {
try {
getPrefs ( ) . getString ( " json " , null ) ?. let { asString ->
2020-04-20 08:27:08 -07:00
2020-04-19 17:25:20 -07:00
val json = Json ( JsonConfiguration . Default )
val settings = json . parse ( SavedSettings . serializer ( ) , asString )
2020-04-22 18:34:22 -07:00
installNewNodeDB ( settings . myInfo , settings . nodeDB )
2020-04-19 17:25:20 -07:00
// Note: we do not haveNodeDB = true because that means we've got a valid db from a real device (rather than this possibly stale hint)
recentDataPackets . addAll ( settings . messages )
}
} catch ( ex : Exception ) {
errormsg ( " Ignoring error loading saved state for service: ${ex.message} " )
}
}
2020-04-20 07:46:06 -07:00
/ * *
2020-07-01 17:47:53 -07:00
* discard entire node db & message state - used when downloading a new db from the device
2020-04-20 07:46:06 -07:00
* /
private fun discardNodeDB ( ) {
myNodeInfo = null
nodeDBbyNodeNum . clear ( )
nodeDBbyID . clear ( )
2020-07-01 17:47:53 -07:00
// recentDataPackets.clear() We do NOT want to clear this, because it is the record of old messages the GUI still might want to show
2020-04-20 07:46:06 -07:00
haveNodeDB = false
}
2020-02-16 14:22:24 -08:00
var myNodeInfo : MyNodeInfo ? = null
2020-04-04 15:29:16 -07:00
private var radioConfig : MeshProtos . RadioConfig ? = null
2020-01-24 22:22:30 -08:00
2020-03-30 17:35:33 -07:00
/// True after we've done our initial node db init
2020-06-12 17:02:21 -07:00
@Volatile
2020-03-30 17:35:33 -07:00
private var haveNodeDB = 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
2020-04-19 11:47:34 -07:00
/// Map a nodenum to the nodeid string, or return null if not present or no id found
2020-05-30 15:48:50 -07:00
private fun toNodeID ( n : Int ) =
if ( n == NODENUM _BROADCAST ) DataPacket . ID _BROADCAST else nodeDBbyNodeNum [ n ] ?. user ?. id
2020-01-24 22:22:30 -08:00
/// 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
2020-02-19 10:53:36 -08:00
2020-02-25 09:28:47 -08:00
private val numNodes get ( ) = nodeDBbyNodeNum . size
2020-02-19 10:53:36 -08:00
/ * *
* How many nodes are currently online ( including our local node )
* /
private val numOnlineNodes get ( ) = nodeDBbyNodeNum . values . count { it . isOnline }
2020-01-24 22:22:30 -08:00
2020-05-30 15:48:50 -07:00
private fun toNodeNum ( id : String ) =
when ( id ) {
DataPacket . ID _BROADCAST -> NODENUM _BROADCAST
DataPacket . ID _LOCAL -> myNodeNum
else -> toNodeInfo ( id ) . num
}
2020-01-24 22:22:30 -08:00
/// 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
2020-04-22 18:34:22 -07:00
private val myNodeNum
get ( ) = myNodeInfo ?. myNodeNum
?: throw RadioNotConnectedException ( " We don't yet have our myNodeInfo " )
2020-02-17 13:14:53 -08:00
/// 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-06-07 22:15:47 -07:00
val useShortAddresses = ( myNodeInfo ?. nodeNumBits ?: 8 ) != 32
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-06-07 22:15:47 -07:00
// We might need to change broadcast addresses to work with old device loads
to = if ( useShortAddresses && idNum == NODENUM _BROADCAST ) 255 else idNum
2020-01-24 20:35:42 -08:00
}
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
* /
2020-05-30 15:48:50 -07:00
private fun newMeshPacketTo ( id : String ) =
newMeshPacketTo ( toNodeNum ( id ) )
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-05-30 15:48:50 -07:00
destId : String ,
2020-05-21 17:25:04 -07:00
wantAck : Boolean = false ,
2020-05-30 17:28:00 -07:00
id : Int = 0 ,
2020-01-25 10:00:57 -08:00
initFn : MeshProtos . SubPacket . Builder . ( ) -> Unit
) : MeshPacket = newMeshPacketTo ( destId ) . apply {
2020-05-21 17:25:04 -07:00
this . wantAck = wantAck
2020-05-30 17:28:00 -07:00
this . id = id
2020-05-09 21:20:17 -07:00
decoded = MeshProtos . SubPacket . newBuilder ( ) . also {
2020-02-02 18:38:01 -08:00
initFn ( it )
2020-01-25 10:00:57 -08:00
} . build ( )
} . build ( )
2020-06-04 08:53:37 -07:00
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
// then adding items are affecting that shared list rather than a copy. This was causing aliasing of
// recentDataPackets with messages.value in the GUI. So if the current list is empty we are careful to make a new list
private var recentDataPackets = mutableListOf < DataPacket > ( )
2020-04-19 11:47:34 -07:00
/// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so
private fun toDataPacket ( packet : MeshPacket ) : DataPacket ? {
2020-05-09 21:20:17 -07:00
return if ( ! packet . hasDecoded ( ) || ! packet . decoded . hasData ( ) ) {
2020-04-19 11:47:34 -07:00
// We never convert packets that are not DataPackets
null
} else {
2020-05-09 21:20:17 -07:00
val data = packet . decoded . data
2020-04-19 11:47:34 -07:00
val bytes = data . payload . toByteArray ( )
val fromId = toNodeID ( packet . from )
val toId = toNodeID ( packet . to )
// If the rxTime was not set by the device (because device software was old), guess at a time
val rxTime = if ( packet . rxTime == 0 ) packet . rxTime else currentSecond ( )
2020-05-30 15:48:50 -07:00
when {
fromId == null -> {
errormsg ( " Ignoring data from ${packet.from} because we don't yet know its ID " )
null
}
toId == null -> {
errormsg ( " Ignoring data to ${packet.to} because we don't yet know its ID " )
null
}
else -> {
DataPacket (
from = fromId ,
to = toId ,
2020-05-30 19:58:36 -07:00
time = rxTime * 1000L ,
2020-05-30 15:48:50 -07:00
id = packet . id ,
dataType = data . typValue ,
bytes = bytes
)
}
2020-04-19 11:47:34 -07:00
}
}
}
2020-05-30 15:48:50 -07:00
private fun toMeshPacket ( p : DataPacket ) : MeshPacket {
2020-05-30 17:28:00 -07:00
return buildMeshPacket ( p . to !! , id = p . id , wantAck = true ) {
2020-05-30 15:48:50 -07:00
data = MeshProtos . Data . newBuilder ( ) . also {
it . typ = MeshProtos . Data . Type . forNumber ( p . dataType )
it . payload = ByteString . copyFrom ( p . bytes )
} . build ( )
}
}
2020-04-19 11:47:34 -07:00
private fun rememberDataPacket ( dataPacket : DataPacket ) {
// discard old messages if needed then add the new one
2020-06-04 08:53:37 -07:00
while ( recentDataPackets . size > 50 )
2020-04-19 11:47:34 -07:00
recentDataPackets . removeAt ( 0 )
2020-06-04 08:53:37 -07:00
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
// then adding items are affecting that shared list rather than a copy. This was causing aliasing of
// recentDataPackets with messages.value in the GUI. So if the current list is empty we are careful to make a new list
if ( recentDataPackets . isEmpty ( ) )
recentDataPackets = mutableListOf ( dataPacket )
else
recentDataPackets . add ( dataPacket )
2020-04-19 11:47:34 -07:00
}
2020-01-25 06:16:10 -08:00
/// Update our model and resend as needed for a MeshPacket we just received from the radio
2020-04-19 11:47:34 -07:00
private fun handleReceivedData ( packet : MeshPacket ) {
2020-05-31 11:23:25 -07:00
myNodeInfo ?. let { myInfo ->
val data = packet . decoded . data
val bytes = data . payload . toByteArray ( )
val fromId = toNodeID ( packet . from )
val dataPacket = toDataPacket ( packet )
2020-01-25 06:16:10 -08:00
2020-05-31 11:23:25 -07:00
if ( dataPacket != null ) {
2020-01-25 06:16:10 -08:00
2020-05-31 11:23:25 -07:00
if ( myInfo . myNodeNum == packet . from )
debug ( " Ignoring retransmission of our packet ${bytes.size} " )
else {
debug ( " Received data from $fromId ${bytes.size} " )
2020-02-09 05:52:17 -08:00
2020-05-31 11:23:25 -07:00
dataPacket . status = MessageStatus . RECEIVED
rememberDataPacket ( dataPacket )
2020-02-28 14:07:04 -08:00
2020-05-31 11:23:25 -07:00
when ( data . typValue ) {
MeshProtos . Data . Type . CLEAR _TEXT _VALUE -> {
debug ( " Received CLEAR_TEXT from $fromId " )
2020-01-25 06:16:10 -08:00
2020-05-31 11:23:25 -07:00
recentReceivedText = dataPacket
updateNotification ( )
broadcastReceivedData ( dataPacket )
}
2020-01-25 06:16:10 -08:00
2020-05-31 11:23:25 -07:00
MeshProtos . Data . Type . CLEAR _READACK _VALUE ->
warn (
" TODO ignoring CLEAR_READACK from $fromId "
)
2020-02-09 05:52:17 -08:00
2020-05-31 11:23:25 -07:00
MeshProtos . Data . Type . OPAQUE _VALUE ->
broadcastReceivedData ( dataPacket )
2020-03-04 11:16:43 -08:00
2020-05-31 11:23:25 -07:00
else -> TODO ( )
}
2020-04-22 07:59:07 -07:00
2020-05-31 11:23:25 -07:00
GeeksvilleApplication . analytics . track (
" num_data_receive " ,
DataPair ( 1 )
)
GeeksvilleApplication . analytics . track (
" data_receive " ,
DataPair ( " num_bytes " , bytes . size ) ,
DataPair ( " type " , data . typValue )
)
}
}
2020-04-19 11:47:34 -07:00
}
2020-01-25 06:16:10 -08:00
}
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-25 10:30:10 -08:00
val oldId = it . user ?. id . orEmpty ( )
2020-02-10 15:31:56 -08:00
it . user = MeshUser (
2020-02-25 10:30:10 -08:00
if ( p . id . isNotEmpty ( ) ) p . id else oldId , // If the new update doesn't contain an ID keep our old value
2020-02-10 15:31:56 -08:00
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 ) {
2020-05-04 08:05:59 -07:00
it . position = Position ( p , it . position ?. time ?: 0 )
2020-02-16 14:22:24 -08:00
}
}
2020-03-30 17:35:33 -07:00
/// If packets arrive before we have our node DB, we delay parsing them until the DB is ready
2020-04-04 15:29:16 -07:00
private val earlyReceivedPackets = mutableListOf < MeshPacket > ( )
/// If apps try to send packets when our radio is sleeping, we queue them here instead
2020-05-30 15:48:50 -07:00
private val offlineSentPackets = mutableListOf < DataPacket > ( )
2020-03-30 17:35:33 -07:00
2020-05-30 19:58:36 -07:00
/** Keep a record of recently sent packets, so we can properly handle ack/nak */
private val sentPackets = mutableMapOf < Int , DataPacket > ( )
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 ) {
2020-03-30 17:35:33 -07:00
if ( haveNodeDB ) {
processReceivedMeshPacket ( packet )
onNodeDBChanged ( )
} else {
2020-04-04 15:29:16 -07:00
earlyReceivedPackets . add ( packet )
logAssert ( earlyReceivedPackets . size < 128 ) // The max should normally be about 32, but if the device is messed up it might try to send forever
2020-03-30 17:35:33 -07:00
}
}
2020-06-11 14:03:10 -07:00
private fun sendNow ( p : DataPacket ) {
val packet = toMeshPacket ( p )
p . status = MessageStatus . ENROUTE
p . time = System . currentTimeMillis ( ) // update time to the actual time we started sending
2020-06-13 17:02:32 -07:00
// debug("SENDING TO RADIO: $packet")
2020-06-11 14:03:10 -07:00
sendToRadio ( packet )
}
2020-03-30 17:35:33 -07:00
/// Process any packets that showed up too early
private fun processEarlyPackets ( ) {
2020-04-04 15:29:16 -07:00
earlyReceivedPackets . forEach { processReceivedMeshPacket ( it ) }
earlyReceivedPackets . clear ( )
2020-05-30 15:48:50 -07:00
offlineSentPackets . forEach { p ->
// encapsulate our payload in the proper protobufs and fire it off
2020-06-11 14:03:10 -07:00
sendNow ( p )
2020-05-30 19:58:36 -07:00
broadcastMessageStatus ( p )
2020-05-30 15:48:50 -07:00
}
2020-04-04 15:29:16 -07:00
offlineSentPackets . clear ( )
2020-03-30 17:35:33 -07:00
}
2020-06-11 16:22:44 -07:00
/ * *
* Change the status on a data packet and update watchers
* /
private fun changeStatus ( p : DataPacket , m : MessageStatus ) {
p . status = m
broadcastMessageStatus ( p )
}
2020-04-04 15:29:16 -07:00
2020-05-30 19:58:36 -07:00
/ * *
* Handle an ack / nak packet by updating sent message status
* /
private fun handleAckNak ( isAck : Boolean , id : Int ) {
sentPackets . remove ( id ) ?. let { p ->
2020-06-11 16:22:44 -07:00
changeStatus ( p , if ( isAck ) MessageStatus . DELIVERED else MessageStatus . ERROR )
2020-05-30 19:58:36 -07:00
}
}
2020-03-30 17:35:33 -07:00
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun processReceivedMeshPacket ( packet : MeshPacket ) {
2020-01-24 22:22:30 -08:00
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)
2020-02-29 13:42:15 -08:00
//val toNum = packet.to
2020-01-24 22:22:30 -08:00
2020-06-13 16:02:57 -07:00
// debug("Recieved: $packet")
2020-05-09 21:20:17 -07:00
val p = packet . decoded
2020-02-09 05:52:17 -08:00
2020-04-19 11:47:34 -07:00
// If the rxTime was not set by the device (because device software was old), guess at a time
val rxTime = if ( packet . rxTime == 0 ) packet . rxTime else currentSecond ( )
2020-02-19 11:35:16 -08:00
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
// through our node on the way to the phone that means that local node is also alive in the mesh
2020-02-19 10:53:36 -08:00
updateNodeInfo ( fromNum ) {
2020-02-19 11:35:16 -08:00
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
2020-04-19 11:47:34 -07:00
val lastSeen = rxTime
2020-02-19 11:35:16 -08:00
2020-02-19 10:53:36 -08:00
it . position = it . position ?. copy ( time = lastSeen )
2020-02-09 05:52:17 -08:00
}
2020-02-19 11:35:16 -08:00
updateNodeInfo ( myNodeNum ) {
it . position = it . position ?. copy ( time = currentSecond ( ) )
}
2020-02-09 05:52:17 -08:00
2020-04-19 09:23:57 -07:00
if ( p . hasPosition ( ) )
handleReceivedPosition ( fromNum , p . position )
2020-02-16 14:22:24 -08:00
2020-04-19 09:23:57 -07:00
if ( p . hasData ( ) )
2020-04-19 11:47:34 -07:00
handleReceivedData ( packet )
2020-02-02 18:38:01 -08:00
2020-04-19 09:23:57 -07:00
if ( p . hasUser ( ) )
handleReceivedUser ( fromNum , p . user )
2020-05-30 19:58:36 -07:00
if ( p . successId != 0 )
handleAckNak ( true , p . successId )
if ( p . failId != 0 )
handleAckNak ( false , p . failId )
2020-01-24 22:22:30 -08:00
}
2020-01-24 20:35:42 -08:00
2020-02-19 11:35:16 -08:00
private fun currentSecond ( ) = ( System . currentTimeMillis ( ) / 1000 ) . toInt ( )
2020-04-04 15:29:16 -07:00
2020-02-19 10:53:36 -08:00
/// If we just changed our nodedb, we might want to do somethings
private fun onNodeDBChanged ( ) {
2020-02-28 14:11:42 -08:00
updateNotification ( )
2020-02-28 20:09:00 -08:00
2020-02-19 10:53:36 -08:00
// we don't ask for GPS locations from android if our device has a built in GPS
2020-05-24 10:46:50 -07:00
// Note: myNodeInfo can go away if we lose connections, so it might be null
if ( myNodeInfo ?. hasGPS != true ) {
2020-02-19 10:53:36 -08:00
// If we have at least one other person in the mesh, send our GPS position otherwise stop listening to GPS
2020-04-04 14:37:44 -07:00
serviceScope . handledLaunch ( Dispatchers . Main ) {
if ( numOnlineNodes >= 2 )
startLocationRequests ( )
else
stopLocationRequests ( )
}
2020-02-19 10:53:36 -08:00
} else
debug ( " Our radio has a built in GPS, so not reading GPS in phone " )
}
2020-02-04 12:12:29 -08:00
2020-04-04 14:37:44 -07:00
2020-04-22 18:34:22 -07:00
/ * *
* Send in analytics about mesh connection
* /
private fun reportConnection ( ) {
val radioModel = DataPair ( " radio_model " , myNodeInfo ?. model ?: " unknown " )
GeeksvilleApplication . analytics . track (
" mesh_connect " ,
DataPair ( " num_nodes " , numNodes ) ,
DataPair ( " num_online " , numOnlineNodes ) ,
radioModel
)
// Once someone connects to hardware start tracking the approximate number of nodes in their mesh
// this allows us to collect stats on what typical mesh size is and to tell difference between users who just
// downloaded the app, vs has connected it to some hardware.
GeeksvilleApplication . analytics . setUserInfo (
DataPair ( " num_nodes " , numNodes ) ,
radioModel
)
}
2020-04-04 15:29:16 -07:00
private var sleepTimeout : Job ? = null
2020-04-21 14:46:52 -07:00
/// msecs since 1970 we started this connection
private var connectTimeMsec = 0L
2020-02-04 12:12:29 -08:00
/// Called when we gain/lose connection to our radio
2020-04-04 15:29:16 -07:00
private fun onConnectionChanged ( c : ConnectionState ) {
debug ( " onConnectionChanged= $c " )
/// Perform all the steps needed once we start waiting for device sleep to complete
fun startDeviceSleep ( ) {
2020-07-01 17:47:53 -07:00
// Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy)
saveSettings ( )
2020-04-04 15:29:16 -07:00
// lost radio connection, therefore no need to keep listening to GPS
stopLocationRequests ( )
2020-04-21 14:46:52 -07:00
if ( connectTimeMsec != 0L ) {
val now = System . currentTimeMillis ( )
connectTimeMsec = 0L
GeeksvilleApplication . analytics . track (
" connected_seconds " ,
DataPair ( ( now - connectTimeMsec ) / 1000.0 )
)
}
2020-04-04 15:29:16 -07:00
// Have our timeout fire in the approprate number of seconds
sleepTimeout = serviceScope . handledLaunch {
try {
// If we have a valid timeout, wait that long (+30 seconds) otherwise, just wait 30 seconds
val timeout = ( radioConfig ?. preferences ?. lsSecs ?: 0 ) + 30
debug ( " Waiting for sleeping device, timeout= $timeout secs " )
delay ( timeout * 1000L )
warn ( " Device timeout out, setting disconnected " )
onConnectionChanged ( ConnectionState . DISCONNECTED )
} catch ( ex : CancellationException ) {
debug ( " device sleep timeout cancelled " )
}
}
2020-05-31 11:23:25 -07:00
// broadcast an intent with our new connection state
broadcastConnection ( )
2020-04-04 15:29:16 -07:00
}
fun startDisconnect ( ) {
2020-04-19 17:25:20 -07:00
// Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy)
saveSettings ( )
2020-04-04 15:29:16 -07:00
GeeksvilleApplication . analytics . track (
" mesh_disconnect " ,
DataPair ( " num_nodes " , numNodes ) ,
DataPair ( " num_online " , numOnlineNodes )
)
2020-04-21 14:46:52 -07:00
GeeksvilleApplication . analytics . track ( " num_nodes " , DataPair ( numNodes ) )
2020-05-31 11:23:25 -07:00
// broadcast an intent with our new connection state
broadcastConnection ( )
2020-04-04 15:29:16 -07:00
}
fun startConnect ( ) {
2020-02-04 12:12:29 -08:00
// Do our startup init
2020-02-17 18:46:20 -08:00
try {
2020-04-21 14:46:52 -07:00
connectTimeMsec = System . currentTimeMillis ( )
2020-06-05 21:12:15 -07:00
startConfig ( )
2020-02-25 09:28:47 -08:00
2020-05-11 11:44:24 -07:00
} catch ( ex : InvalidProtocolBufferException ) {
errormsg (
" Invalid protocol buffer sent by device - update device software and try again " ,
ex
)
2020-04-24 15:59:01 -07:00
} catch ( ex : RadioNotConnectedException ) {
// note: no need to call startDeviceSleep(), because this exception could only have reached us if it was already called
2020-05-11 11:44:24 -07:00
errormsg ( " Lost connection to radio during init - waiting for reconnect " )
2020-02-17 18:46:20 -08:00
} 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
2020-04-04 15:29:16 -07:00
connectionState = ConnectionState . DEVICE _SLEEP
startDeviceSleep ( )
2020-02-17 18:46:20 -08:00
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-04-04 15:29:16 -07:00
}
2020-02-25 09:28:47 -08:00
2020-04-04 15:29:16 -07:00
// Cancel any existing timeouts
sleepTimeout ?. let {
it . cancel ( )
sleepTimeout = null
}
connectionState = c
when ( c ) {
ConnectionState . CONNECTED ->
startConnect ( )
ConnectionState . DEVICE _SLEEP ->
startDeviceSleep ( )
ConnectionState . DISCONNECTED ->
startDisconnect ( )
2020-02-04 12:12:29 -08:00
}
2020-02-28 13:53:16 -08:00
2020-05-31 11:23:25 -07:00
// Update the android notification in the status bar
updateNotification ( )
}
/ * *
* broadcast our current connection status
* /
private fun broadcastConnection ( ) {
2020-04-04 15:29:16 -07:00
val intent = Intent ( ACTION _MESH _CONNECTED )
intent . putExtra (
EXTRA _CONNECTED ,
connectionState . toString ( )
)
explicitBroadcast ( intent )
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-06-09 09:53:32 -07:00
// NOTE: Do not call handledLaunch here, because it can cause out of order message processing - because each routine is scheduled independently
// serviceScope.handledLaunch {
debug ( " Received broadcast ${intent.action} " )
when ( intent . action ) {
RadioInterfaceService . RADIO _CONNECTED _ACTION -> {
try {
val connected = intent . getBooleanExtra ( EXTRA _CONNECTED , false )
val permanent = intent . getBooleanExtra ( EXTRA _PERMANENT , false )
onConnectionChanged (
when {
connected -> ConnectionState . CONNECTED
permanent -> ConnectionState . DISCONNECTED
else -> ConnectionState . DEVICE _SLEEP
}
)
} 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-17 20:00:11 -08:00
}
2020-06-09 09:53:32 -07:00
}
2020-02-04 12:12:29 -08:00
2020-06-09 09:53:32 -07:00
RadioInterfaceService . RECEIVE _FROMRADIO _ACTION -> {
2020-06-12 17:02:21 -07:00
val bytes = intent . getByteArrayExtra ( EXTRA _PAYLOAD ) !!
try {
val proto =
MeshProtos . FromRadio . parseFrom ( bytes )
2020-06-13 17:02:32 -07:00
// info("Received from radio service: ${proto.toOneLineString()}")
2020-06-12 17:02:21 -07:00
when ( proto . variantCase . number ) {
MeshProtos . FromRadio . PACKET _FIELD _NUMBER -> handleReceivedMeshPacket (
proto . packet
)
2020-02-04 12:12:29 -08:00
2020-06-12 17:02:21 -07:00
MeshProtos . FromRadio . CONFIG _COMPLETE _ID _FIELD _NUMBER -> handleConfigComplete (
proto . configCompleteId
)
2020-04-22 18:34:22 -07:00
2020-06-12 17:02:21 -07:00
MeshProtos . FromRadio . MY _INFO _FIELD _NUMBER -> handleMyInfo ( proto . myInfo )
2020-04-22 18:34:22 -07:00
2020-06-12 17:02:21 -07:00
MeshProtos . FromRadio . NODE _INFO _FIELD _NUMBER -> handleNodeInfo ( proto . nodeInfo )
2020-04-22 18:34:22 -07:00
2020-06-12 17:02:21 -07:00
MeshProtos . FromRadio . RADIO _FIELD _NUMBER -> handleRadioConfig ( proto . radio )
2020-04-22 18:34:22 -07:00
2020-06-12 17:02:21 -07:00
else -> errormsg ( " Unexpected FromRadio variant " )
}
} catch ( ex : InvalidProtocolBufferException ) {
2020-07-01 15:47:58 -07:00
errormsg ( " Invalid Protobuf from radio, len= ${bytes.size} " , ex )
2020-02-04 12:12:29 -08:00
}
2020-04-04 14:37:44 -07:00
}
2020-06-09 09:53:32 -07:00
else -> errormsg ( " Unexpected radio interface broadcast " )
2020-01-24 22:22:30 -08:00
}
2020-01-24 17:05:55 -08:00
}
}
2020-04-22 18:34:22 -07:00
/// A provisional MyNodeInfo that we will install if all of our node config downloads go okay
private var newMyNodeInfo : MyNodeInfo ? = null
/// provisional NodeInfos we will install if all goes well
private val newNodes = mutableListOf < MeshProtos . NodeInfo > ( )
/// Used to make sure we never get foold by old BLE packets
private var configNonce = 1
private fun handleRadioConfig ( radio : MeshProtos . RadioConfig ) {
radioConfig = radio
}
/ * *
* Convert a protobuf NodeInfo into our model objects and update our node DB
* /
private fun installNodeInfo ( info : MeshProtos . NodeInfo ) {
// 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 ( ) ) {
// For the local node, it might not be able to update its times because it doesn't have a valid GPS reading yet
// so if the info is for _our_ node we always assume time is current
2020-05-04 08:05:59 -07:00
it . position = Position ( info . position )
2020-04-22 18:34:22 -07:00
}
}
}
private fun handleNodeInfo ( info : MeshProtos . NodeInfo ) {
debug ( " Received nodeinfo num= ${info.num} , hasUser= ${info.hasUser()} , hasPosition= ${info.hasPosition()} " )
logAssert ( newNodes . size <= 256 ) // Sanity check to make sure a device bug can't fill this list forever
newNodes . add ( info )
}
2020-05-30 17:28:00 -07:00
/ * *
* Update the nodeinfo ( called from either new API version or the old one )
* /
2020-04-22 18:34:22 -07:00
private fun handleMyInfo ( myInfo : MeshProtos . MyNodeInfo ) {
2020-05-13 17:00:23 -07:00
setFirmwareUpdateFilename ( myInfo )
2020-04-22 18:34:22 -07:00
val mi = with ( myInfo ) {
2020-05-13 14:47:55 -07:00
MyNodeInfo (
myNodeNum ,
hasGps ,
region ,
hwModel ,
firmwareVersion ,
2020-05-13 17:00:23 -07:00
firmwareUpdateFilename != null ,
2020-05-30 17:28:00 -07:00
SoftwareUpdateService . shouldUpdate ( this @MeshService , firmwareVersion ) ,
currentPacketId . toLong ( ) and 0xffffffffL ,
2020-05-30 19:58:36 -07:00
if ( nodeNumBits == 0 ) 8 else nodeNumBits ,
if ( packetIdBits == 0 ) 8 else packetIdBits ,
2020-06-03 16:16:51 -07:00
if ( messageTimeoutMsec == 0 ) 5 * 60 * 1000 else messageTimeoutMsec , // constants from current device code
minAppVersion
2020-05-13 14:47:55 -07:00
)
2020-04-22 18:34:22 -07:00
}
newMyNodeInfo = mi
/// Track types of devices and firmware versions in use
GeeksvilleApplication . analytics . setUserInfo (
DataPair ( " region " , mi . region ) ,
DataPair ( " firmware " , mi . firmwareVersion ) ,
DataPair ( " has_gps " , mi . hasGPS ) ,
DataPair ( " hw_model " , mi . model ) ,
DataPair ( " dev_error_count " , myInfo . errorCount )
)
if ( myInfo . errorCode != 0 ) {
GeeksvilleApplication . analytics . track (
" dev_error " ,
DataPair ( " code " , myInfo . errorCode ) ,
DataPair ( " address " , myInfo . errorAddress ) ,
// We also include this info, because it is required to correctly decode address from the map file
DataPair ( " firmware " , mi . firmwareVersion ) ,
DataPair ( " hw_model " , mi . model ) ,
DataPair ( " region " , mi . region )
)
}
}
private fun handleConfigComplete ( configCompleteId : Int ) {
if ( configCompleteId == configNonce ) {
// This was our config request
if ( newMyNodeInfo == null || newNodes . isEmpty ( ) )
2020-07-01 15:47:58 -07:00
errormsg ( " Did not receive a valid config " )
2020-04-22 18:34:22 -07:00
else {
debug ( " Installing new node DB " )
discardNodeDB ( )
myNodeInfo = newMyNodeInfo
newNodes . forEach ( :: installNodeInfo )
newNodes . clear ( ) // Just to save RAM ;-)
haveNodeDB = true // we now have nodes from real hardware
processEarlyPackets ( ) // send receive any packets that were queued up
2020-05-31 11:23:25 -07:00
// broadcast an intent with our new connection state
broadcastConnection ( )
2020-04-22 18:34:22 -07:00
onNodeDBChanged ( )
reportConnection ( )
}
} else
warn ( " Ignoring stale config complete " )
}
/ * *
* Start the modern ( REV2 ) API configuration flow
* /
private fun startConfig ( ) {
configNonce += 1
newNodes . clear ( )
newMyNodeInfo = null
2020-06-09 09:53:32 -07:00
debug ( " Starting config nonce= $configNonce " )
2020-04-22 18:34:22 -07:00
2020-04-23 11:02:44 -07:00
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . wantConfigId = configNonce
} )
2020-04-22 18:34:22 -07:00
}
2020-02-16 14:22:24 -08:00
/// Send a position (typically from our built in GPS) into the mesh
2020-02-19 18:51:59 -08:00
private fun sendPosition (
lat : Double ,
lon : Double ,
alt : Int ,
destNum : Int = NODENUM _BROADCAST ,
wantResponse : Boolean = false
) {
debug ( " Sending our position to= $destNum lat= $lat , lon= $lon , alt= $alt " )
2020-02-16 14:22:24 -08:00
val position = MeshProtos . Position . newBuilder ( ) . also {
2020-05-04 08:05:59 -07:00
it . longitudeI = Position . degI ( lon )
it . latitudeI = Position . degI ( lat )
2020-02-16 14:22:24 -08:00
it . altitude = alt
2020-02-19 11:35:16 -08:00
it . time = currentSecond ( ) // Include our current timestamp
2020-02-16 14:22:24 -08:00
} . build ( )
// encapsulate our payload in the proper protobufs and fire it off
val packet = newMeshPacketTo ( destNum )
2020-05-09 21:20:17 -07:00
packet . decoded = MeshProtos . SubPacket . newBuilder ( ) . also {
2020-02-16 14:22:24 -08:00
it . position = position
2020-02-19 18:51:59 -08:00
it . wantResponse = wantResponse
2020-02-16 14:22:24 -08:00
} . 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
2020-04-23 11:02:44 -07:00
sendToRadio ( packet . build ( ) )
2020-04-04 15:29:16 -07:00
}
2020-04-23 11:18:48 -07:00
/ * * Set our radio config either with the new or old API
* /
private fun setRadioConfig ( payload : ByteArray ) {
val parsed = MeshProtos . RadioConfig . parseFrom ( payload )
// Update our device
2020-06-05 21:12:15 -07:00
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . setRadio = parsed
} )
2020-04-23 11:18:48 -07:00
// Update our cached copy
this @MeshService . radioConfig = parsed
}
/ * *
* Set our owner with either the new or old API
* /
fun setOwner ( myId : String ? , longName : String , shortName : String ) {
2020-06-13 16:02:57 -07:00
val myNode = myNodeInfo
if ( myNode != null ) {
2020-04-23 11:18:48 -07:00
2020-06-13 16:02:57 -07:00
val myInfo = toNodeInfo ( myNode . myNodeNum )
if ( longName == myInfo . user ?. longName && shortName == myInfo . user ?. shortName )
debug ( " Ignoring nop owner change " )
else {
debug ( " SetOwner $myId : ${longName.anonymize} : $shortName " )
2020-04-23 11:18:48 -07:00
2020-06-13 16:02:57 -07:00
val user = MeshProtos . User . newBuilder ( ) . also {
if ( myId != null ) // Only set the id if it was provided
it . id = myId
it . longName = longName
it . shortName = shortName
} . build ( )
// Also update our own map for our nodenum, by handling the packet just like packets from other users
handleReceivedUser ( myNode . myNodeNum , user )
// set my owner info
sendToRadio ( ToRadio . newBuilder ( ) . apply {
this . setOwner = user
} )
}
} else
throw Exception ( " Can't set user without a node info " ) // this shouldn't happen
2020-04-23 11:18:48 -07:00
}
2020-05-30 17:28:00 -07:00
/// Do not use directly, instead call generatePacketId()
private var currentPacketId = 0L
/ * *
* Generate a unique packet ID ( if we know enough to do so - otherwise return 0 so the device will do it )
* /
private fun generatePacketId ( ) : Int {
myNodeInfo ?. let {
val numPacketIds =
( ( 1L shl it . packetIdBits ) - 1 ) . toLong ( ) // A mask for only the valid packet ID bits, either 255 or maxint
if ( currentPacketId == 0L ) {
logAssert ( it . packetIdBits == 8 || it . packetIdBits == 32 ) // Only values I'm expecting (though we don't require this)
2020-05-31 11:23:25 -07:00
val devicePacketId = if ( it . currentPacketId == 0L ) {
// Old devices don't send their current packet ID, in that case just pick something random and it will probably be fine ;-)
val random = Random ( System . currentTimeMillis ( ) )
random . nextLong ( ) . absoluteValue
} else
it . currentPacketId
2020-05-30 17:28:00 -07:00
// Not inited - pick a number on the opposite side of what the device is using
2020-05-31 11:23:25 -07:00
currentPacketId = devicePacketId + numPacketIds / 2
2020-05-30 17:28:00 -07:00
} else {
currentPacketId ++
}
currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits
// Use modulus and +1 to ensure we skip 0 on any values we return
return ( ( currentPacketId % numPacketIds ) + 1L ) . toInt ( )
}
return 0 // We don't have mynodeinfo yet, so just let the radio eventually assign an ID
}
2020-05-13 17:00:23 -07:00
var firmwareUpdateFilename : String ? = null
2020-05-13 14:47:55 -07:00
/ * * *
* Return the filename we will install on the device
* /
2020-06-28 14:55:02 -07:00
private fun setFirmwareUpdateFilename ( info : MeshProtos . MyNodeInfo ) {
2020-05-13 17:00:23 -07:00
firmwareUpdateFilename = try {
if ( info . region != null && info . firmwareVersion != null && info . hwModel != null )
SoftwareUpdateService . getUpdateFilename (
this ,
info . region ,
info . firmwareVersion ,
info . hwModel
)
else
2020-05-13 14:47:55 -07:00
null
2020-05-13 17:00:23 -07:00
} catch ( ex : Exception ) {
errormsg ( " Unable to update " , ex )
null
}
2020-07-02 10:21:14 -07:00
debug ( " setFirmwareUpdateFilename $firmwareUpdateFilename " )
2020-05-13 17:00:23 -07:00
}
2020-05-13 14:47:55 -07:00
private fun doFirmwareUpdate ( ) {
// Run in the IO thread
val filename = firmwareUpdateFilename ?: throw Exception ( " No update filename " )
val safe =
2020-06-07 17:11:30 -07:00
BluetoothInterface . safe
2020-06-04 12:34:34 -07:00
?: throw Exception ( " Can't update - no bluetooth connected " )
2020-05-13 14:47:55 -07:00
serviceScope . handledLaunch {
SoftwareUpdateService . doUpdate ( this @MeshService , safe , filename )
}
}
2020-05-30 19:58:36 -07:00
/ * *
* Remove any sent packets that have been sitting around too long
*
* Note : we give each message what the timeout the device code is using , though in the normal
* case the device will fail after 3 retries much sooner than that ( and it will provide a nak to us )
* /
private fun deleteOldPackets ( ) {
myNodeInfo ?. apply {
val now = System . currentTimeMillis ( )
2020-06-11 16:22:44 -07:00
val old = sentPackets . values . filter { p ->
( p . status == MessageStatus . ENROUTE && p . time + messageTimeoutMsec < now )
}
// Do this using a separate list to prevent concurrent modification exceptions
old . forEach { p ->
handleAckNak ( false , p . id )
2020-05-30 19:58:36 -07:00
}
}
}
2020-06-11 14:03:10 -07:00
private fun enqueueForSending ( p : DataPacket ) {
p . status = MessageStatus . QUEUED
offlineSentPackets . add ( p )
}
2020-05-13 14:47:55 -07:00
val binder = object : IMeshService . Stub ( ) {
2020-04-19 19:23:20 -07:00
2020-04-20 15:38:53 -07:00
override fun setDeviceAddress ( deviceAddr : String ? ) = toRemoteExceptions {
2020-04-19 19:23:20 -07:00
debug ( " Passing through device change to radio service: $deviceAddr " )
2020-04-20 07:46:06 -07:00
discardNodeDB ( )
2020-04-20 15:38:53 -07:00
radio . service . setDeviceAddress ( deviceAddr )
2020-04-19 19:23:20 -07:00
}
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-04-19 11:47:34 -07:00
override fun getOldMessages ( ) : MutableList < DataPacket > {
return recentDataPackets
}
2020-05-13 14:47:55 -07:00
override fun getUpdateStatus ( ) : Int = SoftwareUpdateService . progress
override fun startFirmwareUpdate ( ) = toRemoteExceptions {
doFirmwareUpdate ( )
}
2020-06-09 18:56:34 -07:00
override fun getMyNodeInfo ( ) : MyNodeInfo = toRemoteExceptions {
this @MeshService . myNodeInfo ?: throw RadioNotConnectedException ( " No MyNodeInfo " )
}
2020-05-13 14:47:55 -07: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-04-23 11:18:48 -07:00
this @MeshService . setOwner ( myId , longName , shortName )
2020-01-25 10:00:57 -08:00
}
2020-05-30 15:48:50 -07:00
override fun send (
p : DataPacket
) {
2020-01-26 10:44:42 -08:00
toRemoteExceptions {
2020-05-30 17:28:00 -07:00
// Init from and id
2020-05-30 15:48:50 -07:00
myNodeID ?. let { myId ->
if ( p . from == DataPacket . ID _LOCAL )
p . from = myId
2020-05-30 17:28:00 -07:00
if ( p . id == 0 )
p . id = generatePacketId ( )
2020-01-25 10:00:57 -08:00
}
2020-05-30 15:48:50 -07:00
2020-05-30 19:58:36 -07:00
info ( " sendData dest= ${p.to} , id= ${p.id} <- ${p.bytes!!.size} bytes (connectionState= $connectionState ) " )
2020-05-30 15:48:50 -07:00
2020-04-19 11:47:34 -07:00
// Keep a record of datapackets, so GUIs can show proper chat history
2020-05-30 15:48:50 -07:00
rememberDataPacket ( p )
2020-04-19 11:47:34 -07:00
2020-05-30 19:58:36 -07:00
if ( p . id != 0 ) { // If we have an ID we can wait for an ack or nak
deleteOldPackets ( )
sentPackets [ p . id ] = p
}
2020-06-11 14:03:10 -07:00
// If radio is sleeping or disconnected, queue the packet
2020-04-04 15:29:16 -07:00
when ( connectionState ) {
2020-06-11 14:03:10 -07:00
ConnectionState . CONNECTED ->
try {
sendNow ( p )
} catch ( ex : Exception ) {
// This can happen if a user is unlucky and the device goes to sleep after the GUI starts a send, but before we update connectionState
errormsg ( " Error sending message, so enqueueing " , ex )
enqueueForSending ( p )
}
else -> // sleeping or disconnected
enqueueForSending ( p )
2020-04-04 15:29:16 -07:00
}
2020-03-04 11:16:43 -08:00
2020-03-08 15:22:31 -07:00
GeeksvilleApplication . analytics . track (
" data_send " ,
2020-05-30 17:28:00 -07:00
DataPair ( " num_bytes " , p . bytes . size ) ,
2020-05-30 15:48:50 -07:00
DataPair ( " type " , p . dataType )
2020-03-08 15:22:31 -07:00
)
2020-04-22 07:59:07 -07:00
GeeksvilleApplication . analytics . track (
" num_data_sent " ,
DataPair ( 1 )
)
2020-01-25 10:00:57 -08:00
}
2020-05-30 15:48:50 -07:00
}
2020-01-22 21:25:31 -08:00
2020-02-11 19:19:56 -08:00
override fun getRadioConfig ( ) : ByteArray = toRemoteExceptions {
2020-05-13 14:47:55 -07:00
this @MeshService . radioConfig ?. toByteArray ( )
?: throw RadioNotConnectedException ( )
2020-02-11 19:19:56 -08:00
}
override fun setRadioConfig ( payload : ByteArray ) = toRemoteExceptions {
2020-04-23 11:18:48 -07:00
this @MeshService . setRadioConfig ( payload )
2020-02-11 19:19:56 -08:00
}
2020-04-19 11:56:06 -07:00
override fun getNodes ( ) : MutableList < NodeInfo > = toRemoteExceptions {
val r = nodeDBbyID . values . toMutableList ( )
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-04-04 15:29:16 -07:00
override fun connectionState ( ) : String = toRemoteExceptions {
val r = this @MeshService . connectionState
info ( " in connectionState= $r " )
r . toString ( )
2020-01-22 21:25:31 -08:00
}
}
2020-05-30 15:48:50 -07:00
}