Fix #11: we now keep a record of past messages in the persistent service state

pull/28/head
geeksville 2020-04-19 11:47:34 -07:00
rodzic a10e02ecdf
commit 5784138c96
5 zmienionych plików z 171 dodań i 75 usunięć

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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<DataPacket>(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<DataPacket>).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())

Wyświetl plik

@ -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(

Wyświetl plik

@ -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

Wyświetl plik

@ -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<DataPacket>()
/// 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<DataPacket> {
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 ->