diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index 75f0780be..7602cfb90 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -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 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) } diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt index 5996309c6..409e85a7f 100644 --- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt +++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt @@ -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 { + // 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 { + return arrayOfNulls(size) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index f34113c20..688d91d61 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 + ) ) } } 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 197192577..338385196 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt @@ -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}" 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 65a75d113..dbc80571c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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() /// If apps try to send packets when our radio is sleeping, we queue them here instead - private val offlineSentPackets = mutableListOf() + private val offlineSentPackets = mutableListOf() /// 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() } } -} \ No newline at end of file +} diff --git a/app/src/main/proto b/app/src/main/proto index bfae47bdc..adf4127fe 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit bfae47bdc0da23bb1e53fed054d3de2d161389bc +Subproject commit adf4127fe3e4140bfd97b48a2d11b53ee34a16c8