/* * 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 . */ 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 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 ) : 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) : this( to = to, bytes = text.encodeToByteArray(), dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE, channel = channel ) /** * 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 } // 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, ) 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 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() 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) } 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 } 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" // 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 { return arrayOfNulls(size) } } }