From 5784138c968535d5ad9294a812d2aae6d35b5368 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 19 Apr 2020 11:47:34 -0700 Subject: [PATCH] Fix #11: we now keep a record of past messages in the persistent service state --- .../com/geeksville/mesh/IMeshService.aidl | 6 +- .../java/com/geeksville/mesh/MainActivity.kt | 26 ++- .../main/java/com/geeksville/mesh/NodeInfo.kt | 45 ++++++ .../geeksville/mesh/model/MessagesState.kt | 16 +- .../geeksville/mesh/service/MeshService.kt | 153 +++++++++++------- 5 files changed, 171 insertions(+), 75 deletions(-) diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index a905e4478..4dc4b188a 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -2,6 +2,7 @@ package com.geeksville.mesh; // Declare any non-default types here with import statements +// import com.geeksville.mesh.DataPacket; parcelable NodeInfo; @@ -45,6 +46,9 @@ interface IMeshService { /// It returns a RadioConfig protobuf. byte []getRadioConfig(); + /// Return an list of MeshPacket protobuf (byte arrays) which were received while your client app was offline (recent messages only) + List getOldMessages(); + /// This method is only intended for use in our GUI, so the user can set radio options /// It sets a RadioConfig protobuf void setRadioConfig(in byte []payload); @@ -55,7 +59,7 @@ interface IMeshService { String connectionState(); // see com.geeksville.com.geeksville.mesh broadcast intents - // RECEIVED_OPAQUE for data received from other nodes + // RECEIVED_OPAQUE for data received from other nodes. payload will contain a DataPacket // NODE_CHANGE for new IDs appearing or disappearing // CONNECTION_CHANGED for losing/gaining connection to the packet radio } diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index f8cb48d29..0e87fc941 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -557,25 +557,16 @@ class MainActivity : AppCompatActivity(), Logging, } MeshService.ACTION_RECEIVED_DATA -> { - debug("TODO rxdata") - val sender = - intent.getStringExtra(EXTRA_SENDER)!! + debug("received new data from service") val payload = - intent.getByteArrayExtra(EXTRA_PAYLOAD)!! - val typ = intent.getIntExtra(EXTRA_TYP, -1) + intent.getParcelableExtra(EXTRA_PAYLOAD)!! - when (typ) { + when (payload.dataType) { MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> { - // FIXME - use the real time from the packet - // FIXME - don't just slam in a new list each time, it probably causes extra drawing. Figure out how to be Compose smarter... - val msg = TextMessage( - sender, - payload.toString(utf8) - ) - - model.messagesState.addMessage(msg) + model.messagesState.addMessage(payload) } - else -> TODO() + else -> + errormsg("Unhandled dataType ${payload.dataType}") } } MeshService.ACTION_MESH_CONNECTED -> { @@ -604,6 +595,11 @@ class MainActivity : AppCompatActivity(), Logging, // We don't start listening for packets until after we are connected to the service registerMeshReceiver() + // Init our messages table with the service's record of past text messages + model.messagesState.messages.value = (service.oldMessages as List).map { + TextMessage(it) + } + // We won't receive a notify for the initial state of connection, so we force an update here val connectionState = MeshService.ConnectionState.valueOf(service.connectionState()) diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index 336463662..c335a813f 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -4,6 +4,7 @@ import android.os.Parcel import android.os.Parcelable import com.geeksville.mesh.ui.bearing import com.geeksville.mesh.ui.latLongToMeter +import kotlinx.android.parcel.Parcelize /** @@ -16,7 +17,51 @@ import com.geeksville.mesh.ui.latLongToMeter val Any?.anonymized: String get() = if (this != null) ("..." + this.toString().takeLast(3)) else "null" +// // model objects that directly map to the corresponding protobufs +// + +/** + * A parcelable version of the protobuf MeshPacket + Data subpacket. + */ +@Parcelize +data class DataPacket( + val from: String, // a nodeID string + val to: String, // a nodeID string + val rxTime: Long, // msecs since 1970 + val id: Int, + val dataType: Int, + val bytes: ByteArray +) : Parcelable { + + // Autogenerated comparision, because we have a byte array + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DataPacket + + if (from != other.from) return false + if (to != other.to) return false + if (rxTime != other.rxTime) return false + if (id != other.id) return false + if (dataType != other.dataType) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = from.hashCode() + result = 31 * result + to.hashCode() + result = 31 * result + rxTime.hashCode() + result = 31 * result + id + result = 31 * result + dataType + result = 31 * result + bytes.contentHashCode() + return result + } +} + data class MeshUser(val id: String, val longName: String, val shortName: String) : Parcelable { constructor(parcel: Parcel) : this( diff --git a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt index 3b1f1ede8..197192577 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt @@ -4,6 +4,7 @@ import android.os.RemoteException import androidx.lifecycle.MutableLiveData import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging +import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.utf8 import java.util.* @@ -18,7 +19,14 @@ data class TextMessage( val text: String, val date: Date = Date(), val errorMessage: String? = null -) +) { + /// We can auto init from data packets + constructor(payload: DataPacket) : this( + payload.from, + payload.bytes.toString(utf8), + date = Date(payload.rxTime) + ) +} class MessagesState(private val ui: UIViewModel) : Logging { @@ -41,10 +49,14 @@ class MessagesState(private val ui: UIViewModel) : Logging { } /// add a message our GUI list of past msgs - fun addMessage(m: TextMessage) { + private fun addMessage(m: TextMessage) { + // FIXME - don't just slam in a new list each time, it probably causes extra drawing. messages.value = messages.value!! + m } + /// Add a message that was encapsulated in a data packet + fun addMessage(payload: DataPacket) = addMessage(TextMessage(payload)) + /// Send a message and added it to our GUI log fun sendMessage(str: String, dest: String? = null) { var error: String? = null 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 33541dd55..0995fa4cb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -29,7 +29,6 @@ import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.* import com.google.protobuf.ByteString import kotlinx.coroutines.* -import java.nio.charset.Charset import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -99,9 +98,6 @@ class MeshService : Service(), Logging { return intent } } - - /// A model object for a Text message - data class TextMessage(val fromId: String, val text: String) } public enum class ConnectionState { @@ -249,15 +245,11 @@ class MeshService : Service(), Logging { /** * The RECEIVED_OPAQUE: - * Payload will be the raw bytes which were contained within a MeshPacket.Opaque field - * Sender will be a user ID string - * Type will be the Data.Type enum code for this payload + * Payload will be a DataPacket */ - private fun broadcastReceivedData(senderId: String, payload: ByteArray, typ: Int) { + private fun broadcastReceivedData(payload: DataPacket) { val intent = Intent(ACTION_RECEIVED_DATA) - intent.putExtra(EXTRA_SENDER, senderId) intent.putExtra(EXTRA_PAYLOAD, payload) - intent.putExtra(EXTRA_TYP, typ) explicitBroadcast(intent) } @@ -322,7 +314,7 @@ class MeshService : Service(), Logging { } /// A text message that has a arrived since the last notification update - private var recentReceivedText: TextMessage? = null + private var recentReceivedText: DataPacket? = null private val summaryString get() = when (connectionState) { @@ -353,14 +345,14 @@ class MeshService : Service(), Logging { // if(shortContent != null) builder.setContentText(shortContent) // If a text message arrived include it with our notification - recentReceivedText?.let { msg -> + recentReceivedText?.let { packet -> // Try to show the human name of the sender if possible - val sender = nodeDBbyID[msg.fromId]?.user?.longName ?: msg.fromId + val sender = nodeDBbyID[packet.from]?.user?.longName ?: packet.from builder.setContentText("Message from $sender") builder.setStyle( NotificationCompat.BigTextStyle() - .bigText(msg.text) + .bigText(packet.bytes.toString(utf8)) ) } @@ -453,8 +445,8 @@ class MeshService : Service(), Logging { n ) - /// Map a nodenum to the nodeid string, or throw an exception if not present - private fun toNodeID(n: Int) = toNodeInfo(n).user?.id + /// Map a nodenum to the nodeid string, or return null if not present or no id found + private fun toNodeID(n: Int) = nodeDBbyNodeNum[n]?.user?.id /// given a nodenum, return a db entry - creating if necessary private fun getOrCreateNodeInfo(n: Int) = @@ -528,50 +520,86 @@ class MeshService : Service(), Logging { }.build() }.build() - /// 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) + private val recentDataPackets = mutableListOf() - /// the sending node ID if possible, else just its number - val fromString = fromId ?: fromId.toString() + /// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so + private fun toDataPacket(packet: MeshPacket): DataPacket? { + return if (!packet.hasPayload() || !packet.payload.hasData()) { + // We never convert packets that are not DataPackets + null + } else { + val data = packet.payload.data + val bytes = data.payload.toByteArray() + val fromId = toNodeID(packet.from) + val toId = toNodeID(packet.to) + ?: packet.to.toString() // FIXME, we don't currently have IDs specified for the broadcast address - 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) - } - } + // 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() - when (data.typValue) { - MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> { - val text = bytes.toString(Charset.forName("UTF-8")) - - debug("Received CLEAR_TEXT from $fromString") - - recentReceivedText = TextMessage(fromString, text) - updateNotification() - forwardData() - } - - MeshProtos.Data.Type.CLEAR_READACK_VALUE -> - warn( - "TODO ignoring CLEAR_READACK from $fromString" + if (fromId != null) { + DataPacket( + fromId, + toId, + rxTime * 1000L, + packet.id, + data.typValue, + bytes ) - - MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE -> - forwardData() - - else -> TODO() + } else { + warn("Ignoring data from ${packet.from} because we don't yet know its ID") + null + } } + } - GeeksvilleApplication.analytics.track( - "data_receive", - DataPair("num_bytes", bytes.size), - DataPair("type", data.typValue) - ) + private fun rememberDataPacket(dataPacket: DataPacket) { + // discard old messages if needed then add the new one + while (recentDataPackets.size > 20) // FIXME, we should instead serialize this list to flash on shutdown + recentDataPackets.removeAt(0) + recentDataPackets.add(dataPacket) + } + + /// Update our model and resend as needed for a MeshPacket we just received from the radio + private fun handleReceivedData(packet: MeshPacket) { + val data = packet.payload.data + val bytes = data.payload.toByteArray() + val fromId = toNodeID(packet.from) + val dataPacket = toDataPacket(packet) + + if (dataPacket != null) { + debug("Received data from $fromId ${bytes.size}") + + rememberDataPacket(dataPacket) + + when (data.typValue) { + MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> { + val text = bytes.toString(utf8) + + debug("Received CLEAR_TEXT from $fromId") + + recentReceivedText = dataPacket + updateNotification() + broadcastReceivedData(dataPacket) + } + + MeshProtos.Data.Type.CLEAR_READACK_VALUE -> + warn( + "TODO ignoring CLEAR_READACK from $fromId" + ) + + MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE -> + broadcastReceivedData(dataPacket) + + else -> TODO() + } + + GeeksvilleApplication.analytics.track( + "data_receive", + DataPair("num_bytes", bytes.size), + DataPair("type", data.typValue) + ) + } } /// Update our DB of users based on someone sending out a User subpacket @@ -636,12 +664,14 @@ class MeshService : Service(), Logging { val p = packet.payload + // 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() + // 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 updateNodeInfo(fromNum) { // 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 currentSecond() + val lastSeen = rxTime it.position = it.position?.copy(time = lastSeen) } @@ -653,7 +683,7 @@ class MeshService : Service(), Logging { handleReceivedPosition(fromNum, p.position) if (p.hasData()) - handleReceivedData(fromNum, p.data) + handleReceivedData(packet) if (p.hasUser()) handleReceivedUser(fromNum, p.user) @@ -955,6 +985,10 @@ class MeshService : Service(), Logging { clientPackages[receiverName] = packageName } + override fun getOldMessages(): MutableList { + return recentDataPackets + } + override fun getMyId() = toRemoteExceptions { myNodeID } override fun setOwner(myId: String?, longName: String, shortName: String) = @@ -988,6 +1022,11 @@ class MeshService : Service(), Logging { it.payload = ByteString.copyFrom(payloadIn) }.build() } + // Keep a record of datapackets, so GUIs can show proper chat history + toDataPacket(packet)?.let { + rememberDataPacket(it) + } + // If radio is sleeping, queue the packet when (connectionState) { ConnectionState.DEVICE_SLEEP ->