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

updates on message status
pull/40/head
geeksville 2020-05-30 15:48:50 -07:00
rodzic 67d95039bf
commit 6031b5eaa0
6 zmienionych plików z 175 dodań i 76 usunięć

Wyświetl plik

@ -26,16 +26,16 @@ interface IMeshService {
String getMyId();
/*
Send an opaque packet to a specified node name
Send a packet to a specified node name
typ is defined in mesh.proto Data.Type. For now juse use 0 to mean opaque bytes.
destId can be null to indicate "broadcast message"
Returns true if the packet has been sent into the mesh, or false if it was merely queued
inside the service - and will be delivered to mesh the next time we hear from our radio.
messageStatus and id of the provided message will be updated by this routine to indicate
message send status and the ID that can be used to locate the message in the future
*/
boolean sendData(String destId, in byte[] payload, int typ);
void send(inout DataPacket packet);
/**
Get the IDs of everyone on the mesh. You should also subscribe for NODE_CHANGE broadcasts.
@ -46,7 +46,8 @@ 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)
/// Return an list of MeshPacket protobuf (byte arrays) which were received while your client app was offline (recent messages only).
/// Also includes any messages we have sent recently (useful for finding current message status)
List<DataPacket> getOldMessages();
/// This method is only intended for use in our GUI, so the user can set radio options
@ -75,9 +76,9 @@ interface IMeshService {
int getUpdateStatus();
// see com.geeksville.com.geeksville.mesh broadcast intents
// RECEIVED_OPAQUE for data received from other nodes. payload will contain a DataPacket
// RECEIVED_DATA 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
// MESSAGE_STATUS_CHANGED for any message status changes (for sent messages only, other messages come via RECEIVED_DATA. payload will contain a message ID and a MessageStatus)
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

@ -1 +1 @@
Subproject commit bfae47bdc0da23bb1e53fed054d3de2d161389bc
Subproject commit adf4127fe3e4140bfd97b48a2d11b53ee34a16c8