clean up message API to/from mesh service, to allow clients to get

updates on message status
This commit is contained in:
geeksville 2020-05-30 15:48:50 -07:00
parent 67d95039bf
commit 6031b5eaa0
6 changed files with 175 additions and 76 deletions

View file

@ -1,5 +1,6 @@
package com.geeksville.mesh
import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.Serializable
@ -10,25 +11,37 @@ enum class MessageStatus : Parcelable {
RECEIVED, // Came in from the mesh
QUEUED, // Waiting to send to the mesh as soon as we connect to the device
ENROUTE, // Delivered to the radio, but no ACK or NAK received
DELIVERED // We received an ack
DELIVERED, // We received an ack
ERROR // We received back a nak, message not delivered
}
/**
* A parcelable version of the protobuf MeshPacket + Data subpacket.
*/
@Serializable
@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,
val status: MessageStatus = MessageStatus.UNKNOWN
var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
val bytes: ByteArray?,
val dataType: Int, // A value such as MeshProtos.Data.Type.OPAQUE_VALUE
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
var rxTime: Long = System.currentTimeMillis(), // msecs since 1970
var id: Int = 0, // 0 means unassigned
var status: MessageStatus? = MessageStatus.UNKNOWN
) : Parcelable {
// Autogenerated comparision, because we have a byte array
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.createByteArray(),
parcel.readInt(),
parcel.readString(),
parcel.readLong(),
parcel.readInt(),
parcel.readParcelable(MessageStatus::class.java.classLoader)
) {
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -40,7 +53,8 @@ data class DataPacket(
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
if (!bytes!!.contentEquals(other.bytes!!)) return false
if (status != other.status) return false
return true
}
@ -51,7 +65,52 @@ data class DataPacket(
result = 31 * result + rxTime.hashCode()
result = 31 * result + id
result = 31 * result + dataType
result = 31 * result + bytes.contentHashCode()
result = 31 * result + bytes!!.contentHashCode()
result = 31 * result + status.hashCode()
return result
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(to)
parcel.writeByteArray(bytes)
parcel.writeInt(dataType)
parcel.writeString(from)
parcel.writeLong(rxTime)
parcel.writeInt(id)
parcel.writeParcelable(status, flags)
}
override fun describeContents(): Int {
return 0
}
/// Update our object from our parcel (used for inout parameters
fun readFromParcel(parcel: Parcel) {
to = parcel.readString()
parcel.createByteArray()
parcel.readInt()
from = parcel.readString()
rxTime = parcel.readLong()
id = parcel.readInt()
status = parcel.readParcelable(MessageStatus::class.java.classLoader)
}
companion object CREATOR : Parcelable.Creator<DataPacket> {
// Special node IDs that can be used for sending messages
/** the Node ID for broadcast destinations */
const val ID_BROADCAST = "^all"
/** The Node ID for the local node - used for from when sender doesn't know our local node ID */
const val ID_LOCAL = "^local"
override fun createFromParcel(parcel: Parcel): DataPacket {
return DataPacket(parcel)
}
override fun newArray(size: Int): Array<DataPacket?> {
return arrayOfNulls(size)
}
}
}

View file

@ -297,15 +297,19 @@ class MainActivity : AppCompatActivity(), Logging,
// Do some test operations
val testPayload = "hello world".toByteArray()
m.sendData(
"+16508675310",
testPayload,
MeshProtos.Data.Type.OPAQUE_VALUE
m.send(
DataPacket(
"+16508675310",
testPayload,
MeshProtos.Data.Type.OPAQUE_VALUE
)
)
m.sendData(
"+16508675310",
testPayload,
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
m.send(
DataPacket(
"+16508675310",
testPayload,
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
)
)
}
}

View file

@ -22,8 +22,8 @@ data class TextMessage(
) {
/// We can auto init from data packets
constructor(payload: DataPacket) : this(
payload.from,
payload.bytes.toString(utf8),
payload.from!!,
payload.bytes!!.toString(utf8),
date = Date(payload.rxTime)
)
}
@ -58,15 +58,17 @@ class MessagesState(private val ui: UIViewModel) : Logging {
fun addMessage(payload: DataPacket) = addMessage(TextMessage(payload))
/// Send a message and added it to our GUI log
fun sendMessage(str: String, dest: String? = null) {
fun sendMessage(str: String, dest: String = DataPacket.ID_BROADCAST) {
var error: String? = null
val service = ui.meshService
if (service != null)
try {
service.sendData(
dest,
str.toByteArray(utf8),
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
service.send(
DataPacket(
dest,
str.toByteArray(utf8),
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
)
)
} catch (ex: RemoteException) {
error = "Error: ${ex.message}"

View file

@ -395,7 +395,7 @@ class MeshService : Service(), Logging {
builder.setStyle(
NotificationCompat.BigTextStyle()
.bigText(packet.bytes.toString(utf8))
.bigText(packet.bytes!!.toString(utf8))
)
}
@ -588,7 +588,8 @@ class MeshService : Service(), Logging {
)
/// 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
private fun toNodeID(n: Int) =
if (n == NODENUM_BROADCAST) DataPacket.ID_BROADCAST else nodeDBbyNodeNum[n]?.user?.id
/// given a nodenum, return a db entry - creating if necessary
private fun getOrCreateNodeInfo(n: Int) =
@ -609,7 +610,12 @@ class MeshService : Service(), Logging {
*/
private val numOnlineNodes get() = nodeDBbyNodeNum.values.count { it.isOnline }
private fun toNodeNum(id: String) = toNodeInfo(id).num
private fun toNodeNum(id: String) =
when (id) {
DataPacket.ID_BROADCAST -> NODENUM_BROADCAST
DataPacket.ID_LOCAL -> myNodeNum
else -> toNodeInfo(id).num
}
/// A helper function that makes it easy to update node info objects
private fun updateNodeInfo(nodeNum: Int, updatefn: (NodeInfo) -> Unit) {
@ -647,8 +653,8 @@ class MeshService : Service(), Logging {
*
* If id is null we assume a broadcast message
*/
private fun newMeshPacketTo(id: String?) =
newMeshPacketTo(if (id != null) toNodeNum(id) else NODENUM_BROADCAST)
private fun newMeshPacketTo(id: String) =
newMeshPacketTo(toNodeNum(id))
/**
* Helper to make it easy to build a subpacket in the proper protobufs
@ -656,7 +662,7 @@ class MeshService : Service(), Logging {
* If destId is null we assume a broadcast message
*/
private fun buildMeshPacket(
destId: String?,
destId: String,
wantAck: Boolean = false,
initFn: MeshProtos.SubPacket.Builder.() -> Unit
): MeshPacket = newMeshPacketTo(destId).apply {
@ -678,27 +684,44 @@ class MeshService : Service(), Logging {
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
// 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()
if (fromId != null) {
DataPacket(
fromId,
toId,
rxTime * 1000L,
packet.id,
data.typValue,
bytes
)
} else {
warn("Ignoring data from ${packet.from} because we don't yet know its ID")
null
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,
rxTime = rxTime * 1000L,
id = packet.id,
dataType = data.typValue,
bytes = bytes
)
}
}
}
}
private fun toMeshPacket(p: DataPacket): MeshPacket {
val packet = buildMeshPacket(p.to!!, wantAck = true) {
data = MeshProtos.Data.newBuilder().also {
it.typ = MeshProtos.Data.Type.forNumber(p.dataType)
it.payload = ByteString.copyFrom(p.bytes)
}.build()
}
return packet
}
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
@ -716,6 +739,7 @@ class MeshService : Service(), Logging {
if (dataPacket != null) {
debug("Received data from $fromId ${bytes.size}")
dataPacket.status = MessageStatus.RECEIVED
rememberDataPacket(dataPacket)
when (data.typValue) {
@ -774,7 +798,7 @@ class MeshService : Service(), Logging {
private val earlyReceivedPackets = mutableListOf<MeshPacket>()
/// If apps try to send packets when our radio is sleeping, we queue them here instead
private val offlineSentPackets = mutableListOf<MeshPacket>()
private val offlineSentPackets = mutableListOf<DataPacket>()
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedMeshPacket(packet: MeshPacket) {
@ -792,7 +816,12 @@ class MeshService : Service(), Logging {
earlyReceivedPackets.forEach { processReceivedMeshPacket(it) }
earlyReceivedPackets.clear()
offlineSentPackets.forEach { sendToRadio(it) }
offlineSentPackets.forEach { p ->
// encapsulate our payload in the proper protobufs and fire it off
val packet = toMeshPacket(p)
p.status = MessageStatus.ENROUTE
sendToRadio(packet)
}
offlineSentPackets.clear()
}
@ -1347,38 +1376,41 @@ class MeshService : Service(), Logging {
this@MeshService.setOwner(myId, longName, shortName)
}
override fun sendData(
destId: String?,
payloadIn: ByteArray,
typ: Int
): Boolean =
override fun send(
p: DataPacket
) {
toRemoteExceptions {
info("sendData dest=$destId <- ${payloadIn.size} bytes (connectionState=$connectionState)")
info("sendData dest=${p.to} <- ${p.bytes!!.size} bytes (connectionState=$connectionState)")
// encapsulate our payload in the proper protobufs and fire it off
val packet = buildMeshPacket(destId, wantAck = true) {
data = MeshProtos.Data.newBuilder().also {
it.typ = MeshProtos.Data.Type.forNumber(typ)
it.payload = ByteString.copyFrom(payloadIn)
}.build()
// FIXME - init from and id in DataPacket
myNodeID?.let { myId ->
if (p.from == DataPacket.ID_LOCAL)
p.from = myId
}
// Keep a record of datapackets, so GUIs can show proper chat history
toDataPacket(packet)?.let {
rememberDataPacket(it)
}
rememberDataPacket(p)
// If radio is sleeping, queue the packet
when (connectionState) {
ConnectionState.DEVICE_SLEEP ->
offlineSentPackets.add(packet)
else ->
ConnectionState.DEVICE_SLEEP -> {
p.status = MessageStatus.QUEUED
offlineSentPackets.add(p)
}
else -> {
p.status = MessageStatus.ENROUTE
// encapsulate our payload in the proper protobufs and fire it off
val packet = toMeshPacket(p)
sendToRadio(packet)
}
}
GeeksvilleApplication.analytics.track(
"data_send",
DataPair("num_bytes", payloadIn.size),
DataPair("type", typ)
DataPair("num_bytes", p.bytes!!.size),
DataPair("type", p.dataType)
)
GeeksvilleApplication.analytics.track(
@ -1388,6 +1420,7 @@ class MeshService : Service(), Logging {
connectionState == ConnectionState.CONNECTED
}
}
override fun getRadioConfig(): ByteArray = toRemoteExceptions {
this@MeshService.radioConfig?.toByteArray()
@ -1411,4 +1444,4 @@ class MeshService : Service(), Logging {
r.toString()
}
}
}
}