diff --git a/TODO.md b/TODO.md index 45f62f7e5..13d599b1f 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ the channel is encrypted, you can share the the channel key with others by qr co * take video * make a working currently vs not working list +* show offline nodes as greyed out * make node list view not look like ass * record analytics events when radio connects/disconnects, include # of nodes in mesh * make channel button look like a button diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index 23c548fd8..f6705d83d 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -37,11 +37,17 @@ data class MeshUser(val id: String, val longName: String, val shortName: String) } } -data class Position(val latitude: Double, val longitude: Double, val altitude: Int) : +data class Position( + val latitude: Double, + val longitude: Double, + val altitude: Int, + val time: Int = (System.currentTimeMillis() / 1000).toInt() // default to current time in secs +) : Parcelable { constructor(parcel: Parcel) : this( parcel.readDouble(), parcel.readDouble(), + parcel.readInt(), parcel.readInt() ) { } @@ -56,6 +62,7 @@ data class Position(val latitude: Double, val longitude: Double, val altitude: I parcel.writeDouble(latitude) parcel.writeDouble(longitude) parcel.writeInt(altitude) + parcel.writeInt(time) } override fun describeContents(): Int { @@ -77,17 +84,32 @@ data class Position(val latitude: Double, val longitude: Double, val altitude: I data class NodeInfo( val num: Int, // This is immutable, and used as a key var user: MeshUser? = null, - var position: Position? = null, - var lastSeen: Int? = null + var position: Position? = null ) : Parcelable { constructor(parcel: Parcel) : this( parcel.readInt(), parcel.readParcelable(MeshUser::class.java.classLoader), - parcel.readParcelable(Position::class.java.classLoader), - parcel.readValue(Int::class.java.classLoader) as? Int + parcel.readParcelable(Position::class.java.classLoader) ) { } + /// Return the last time we've seen this node in secs since 1970 + val lastSeen get() = position?.time ?: 0 + + /** + * true if the device was heard from recently + * + * Note: if a node has never had its time set, it will have a time of zero. In that + * case assume it is online - so that we will start sending GPS updates + */ + val isOnline: Boolean + get() { + val now = System.currentTimeMillis() / 1000 + // FIXME - use correct timeout from the device settings + val timeout = 5 * 60 + return (now - lastSeen <= timeout) || lastSeen == 0 + } + /// @return distance in meters to some other node (or null if unknown) fun distance(o: NodeInfo?): Int? { val p = position @@ -111,7 +133,6 @@ data class NodeInfo( parcel.writeInt(num) parcel.writeParcelable(user, flags) parcel.writeParcelable(position, flags) - parcel.writeValue(lastSeen) } override fun describeContents(): Int { diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index 97ff770a6..0315f8b20 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -23,8 +23,7 @@ object NodeDB { "Kevin MesterNoLoc", "KLO" ), - null, - 12345 + null ) val testNodes = testPositions.mapIndexed { index, it -> @@ -35,8 +34,7 @@ object NodeDB { "Kevin Mester$index", "KM$index" ), - it, - 12345 + it ) } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index f8ac34b2d..4c3192b7d 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -132,57 +132,62 @@ class MeshService : Service(), Logging { private var fusedLocationClient: FusedLocationProviderClient? = null /** - * start our location requests + * start our location requests (if they weren't already running) * * 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 + if (fusedLocationClient == null) { + val request = LocationRequest.create().apply { + interval = + 20 * 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 + 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 } - 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 + if (fusedLocationClient != null) { + debug("Stopping location requests") + fusedLocationClient?.removeLocationUpdates(locationCallback) + fusedLocationClient = null + } } /** @@ -386,7 +391,11 @@ class MeshService : Service(), Logging { id ) - // ?: getOrCreateNodeInfo(10) // FIXME hack for now - throw IdNotFoundException(id) + + /** + * How many nodes are currently online (including our local node) + */ + private val numOnlineNodes get() = nodeDBbyNodeNum.values.count { it.isOnline } private fun toNodeNum(id: String) = toNodeInfo(id).num @@ -497,7 +506,9 @@ class MeshService : Service(), Logging { it.position = Position( p.latitude, p.longitude, - p.altitude + p.altitude, + if (p.time != 0) p.time else it.position?.time + ?: 0 // if this position didn't include time, just keep our old one ) } } @@ -512,11 +523,11 @@ class MeshService : Service(), Logging { val p = packet.payload - // Update our last seen based on any valid timestamps - if (packet.rxTime != 0) { - updateNodeInfo(fromNum) { - it.lastSeen = packet.rxTime - } + // Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one + val lastSeen = + if (packet.rxTime != 0) packet.rxTime else (System.currentTimeMillis() / 1000).toInt() + updateNodeInfo(fromNum) { + it.position = it.position?.copy(time = lastSeen) } when (p.variantCase.number) { @@ -530,6 +541,8 @@ class MeshService : Service(), Logging { handleReceivedUser(fromNum, p.user) else -> TODO("Unexpected SubPacket variant") } + + onNodeDBChanged() } /// We are reconnecting to a radio, redownload the full state. This operation might take hundreds of milliseconds @@ -566,17 +579,32 @@ class MeshService : Service(), Logging { it.position = Position( info.position.latitude, info.position.longitude, - info.position.altitude + info.position.altitude, + info.position.time ) - it.lastSeen = info.lastSeen } // advance to next infoBytes = connectedRadio.readNodeInfo() } + + onNodeDBChanged() } + /// If we just changed our nodedb, we might want to do somethings + private fun onNodeDBChanged() { + // we don't ask for GPS locations from android if our device has a built in GPS + if (!myNodeInfo!!.hasGPS) { + // If we have at least one other person in the mesh, send our GPS position otherwise stop listening to GPS + + if (numOnlineNodes >= 2) + startLocationRequests() + else + stopLocationRequests() + } else + debug("Our radio has a built in GPS, so not reading GPS in phone") + } /// Called when we gain/lose connection to our radio private fun onConnectionChanged(c: Boolean) { @@ -586,12 +614,6 @@ class MeshService : Service(), Logging { // Do our startup init try { reinitFromRadio() - - // 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 @@ -662,6 +684,7 @@ class MeshService : Service(), Logging { it.latitude = lat it.longitude = lon it.altitude = alt + it.time = (System.currentTimeMillis() / 1000).toInt() // Include our current timestamp }.build() // encapsulate our payload in the proper protobufs and fire it off diff --git a/app/src/main/proto/mesh.proto b/app/src/main/proto/mesh.proto index 826a8bb0f..cc4055d06 100644 --- a/app/src/main/proto/mesh.proto +++ b/app/src/main/proto/mesh.proto @@ -62,6 +62,9 @@ message Position { /// true if this position came from the GPS inside the esp32 board, false if it was from a helper app on the phone bool from_hardware = 5; + + /// This is usually not sent over the mesh (to save space), but it is sent from the phone so that the local device can set its RTC + uint32 time = 6; // seconds since 1970 } // a data message to forward to an external app (or possibly also be consumed internally in the case of CLEAR_TEXT and CLEAR_READACK @@ -192,6 +195,9 @@ message RadioConfig { // Send our owner info at least this often (also we always send once at boot - to rejoin the mesh) uint32 send_owner_secs = 2; + /// If we miss this many owner messages from a node, we declare the node offline (defaults to 3 - to allow for some lost packets) + uint32 num_missed_to_fail = 3; + // If true, radio should not try to be smart about what packets to queue to the phone bool keep_all_packets = 100; @@ -229,11 +235,9 @@ SET_CONFIG (switches device to a new set of radio params and preshared key, drop message NodeInfo { int32 num = 1; // the node number User user = 2; - Position position = 3; - // Times are typically not sent over the mesh, but they will be added to any Packet (chain of SubPacket) - // sent to the phone (so the phone can know exact time of reception) - uint32 last_seen = 4; // seconds since 1970 + /// This position data will also contain a time last seen + Position position = 3; /// Returns the Signal-to-noise ratio (SNR) of the last received message, as measured /// by the receiver. @@ -295,10 +299,10 @@ message DeviceState { /// We bump up the integer values in this enum to indicate minimum levels of encodings for saved files /// if your file is below the Minimum you should discard it. - Minimum = 13; + Minimum = 15; /// The current value we are using for saved files - Current = 13; + Current = 15; }; /// A version integer used to invalidate old save files when we make incompatible changes