Meshtastic-Android/app/src/main/java/com/geeksville/mesh/DataPacket.kt

246 lines
8.3 KiB
Kotlin

/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Generic [Parcel.readParcelable] Android 13 compatibility extension.
*/
private inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? {
return if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
readParcelable(loader)
} else {
readParcelable(loader, T::class.java)
}
}
@Parcelize
enum class MessageStatus : Parcelable {
UNKNOWN, // Not set for this message
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
ERROR // We received back a nak, message not delivered
}
/**
* A parcelable version of the protobuf MeshPacket + Data subpacket.
*/
@Serializable
data class DataPacket(
var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
val bytes: ByteArray?,
val dataType: Int, // A port number for this packet (formerly called DataType, see portnums.proto for new usage instructions)
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
var time: Long = System.currentTimeMillis(), // msecs since 1970
var id: Int = 0, // 0 means unassigned
var status: MessageStatus? = MessageStatus.UNKNOWN,
var hopLimit: Int = 0,
var channel: Int = 0, // channel index
var wantAck: Boolean = true, // If true, the receiver should send an ack back
var hopStart: Int = 0,
var snr: Float = 0f,
var rssi: Int = 0,
var replyId: Int? = null // If this is a reply to a previous message, this is the ID of that message
) : Parcelable {
/**
* If there was an error with this message, this string describes what was wrong.
*/
var errorMessage: String? = null
/**
* Syntactic sugar to make it easy to create text messages
*/
constructor(to: String?, channel: Int, text: String, replyId: Int? = null) : this(
to = to,
bytes = text.encodeToByteArray(),
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
channel = channel,
replyId = replyId ?: 0
)
/**
* If this is a text message, return the string, otherwise null
*/
val text: String?
get() = if (dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
bytes?.decodeToString()
} else {
null
}
val alert: String?
get() = if (dataType == Portnums.PortNum.ALERT_APP_VALUE) {
bytes?.decodeToString()
} else {
null
}
constructor(to: String?, channel: Int, waypoint: MeshProtos.Waypoint) : this(
to = to,
bytes = waypoint.toByteArray(),
dataType = Portnums.PortNum.WAYPOINT_APP_VALUE,
channel = channel,
)
val waypoint: MeshProtos.Waypoint?
get() = if (dataType == Portnums.PortNum.WAYPOINT_APP_VALUE) {
MeshProtos.Waypoint.parseFrom(bytes)
} else {
null
}
val hopsAway: Int
get() = if (hopStart == 0 || hopLimit > hopStart) -1 else hopStart - hopLimit
// 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.readParcelableCompat(MessageStatus::class.java.classLoader),
parcel.readInt(),
parcel.readInt(),
parcel.readInt() == 1,
parcel.readInt(),
parcel.readFloat(),
parcel.readInt(),
parcel.readInt().let { if (it == 0) null else it }
)
@Suppress("CyclomaticComplexMethod")
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 (channel != other.channel) return false
if (time != other.time) return false
if (id != other.id) return false
if (dataType != other.dataType) return false
if (!bytes!!.contentEquals(other.bytes!!)) return false
if (status != other.status) return false
if (hopLimit != other.hopLimit) return false
if (wantAck != other.wantAck) return false
if (hopStart != other.hopStart) return false
if (snr != other.snr) return false
if (rssi != other.rssi) return false
if (replyId != other.replyId) return false
return true
}
override fun hashCode(): Int {
var result = from.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + time.hashCode()
result = 31 * result + id
result = 31 * result + dataType
result = 31 * result + bytes!!.contentHashCode()
result = 31 * result + status.hashCode()
result = 31 * result + hopLimit
result = 31 * result + channel
result = 31 * result + wantAck.hashCode()
result = 31 * result + hopStart
result = 31 * result + snr.hashCode()
result = 31 * result + rssi
result = 31 * result + replyId.hashCode()
return result
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(to)
parcel.writeByteArray(bytes)
parcel.writeInt(dataType)
parcel.writeString(from)
parcel.writeLong(time)
parcel.writeInt(id)
parcel.writeParcelable(status, flags)
parcel.writeInt(hopLimit)
parcel.writeInt(channel)
parcel.writeInt(if (wantAck) 1 else 0)
parcel.writeInt(hopStart)
parcel.writeFloat(snr)
parcel.writeInt(rssi)
parcel.writeInt(replyId ?: 0)
}
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()
time = parcel.readLong()
id = parcel.readInt()
status = parcel.readParcelableCompat(MessageStatus::class.java.classLoader)
hopLimit = parcel.readInt()
channel = parcel.readInt()
wantAck = parcel.readInt() == 1
hopStart = parcel.readInt()
snr = parcel.readFloat()
rssi = parcel.readInt()
replyId = parcel.readInt().let { if (it == 0) null else it }
}
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"
// special broadcast address
const val NODENUM_BROADCAST = (0xffffffff).toInt()
// Public-key cryptography (PKC) channel index
const val PKC_CHANNEL_INDEX = 8
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
fun idToDefaultNodeNum(id: String?): Int? =
runCatching { id?.toLong(16)?.toInt() }.getOrNull()
override fun createFromParcel(parcel: Parcel): DataPacket {
return DataPacket(parcel)
}
override fun newArray(size: Int): Array<DataPacket?> {
return arrayOfNulls(size)
}
}
}