mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Add SFPP confirmed status to Messages and Reactions (#4139)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: Mac DeCourcy <github.znq26@slmail.me>
This commit is contained in:
parent
78bd1ad6dd
commit
782c068ead
21 changed files with 2699 additions and 61 deletions
|
|
@ -39,6 +39,8 @@ enum class MessageStatus : Parcelable {
|
|||
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
|
||||
SFPP_ROUTING, // Message is being routed/buffered in the SFPP system
|
||||
SFPP_CONFIRMED, // Message is confirmed on the SFPP chain
|
||||
ERROR, // We received back a nak, message not delivered
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +67,7 @@ data class DataPacket(
|
|||
var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path
|
||||
var retryCount: Int = 0, // Number of automatic retry attempts
|
||||
var emoji: Int = 0,
|
||||
var sfppHash: ByteArray? = null,
|
||||
) : Parcelable {
|
||||
|
||||
/** If there was an error with this message, this string describes what was wrong. */
|
||||
|
|
@ -142,6 +145,7 @@ data class DataPacket(
|
|||
parcel.readInt() == 1, // viaMqtt
|
||||
parcel.readInt(), // retryCount
|
||||
parcel.readInt(), // emoji
|
||||
parcel.createByteArray(), // sfppHash
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
@ -168,6 +172,7 @@ data class DataPacket(
|
|||
if (relayNode != other.relayNode) return false
|
||||
if (retryCount != other.retryCount) return false
|
||||
if (emoji != other.emoji) return false
|
||||
if (!sfppHash.contentEquals(other.sfppHash)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -190,6 +195,7 @@ data class DataPacket(
|
|||
result = 31 * result + relayNode.hashCode()
|
||||
result = 31 * result + retryCount
|
||||
result = 31 * result + emoji
|
||||
result = 31 * result + (sfppHash?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -213,6 +219,7 @@ data class DataPacket(
|
|||
parcel.writeInt(if (viaMqtt) 1 else 0)
|
||||
parcel.writeInt(retryCount)
|
||||
parcel.writeInt(emoji)
|
||||
parcel.writeByteArray(sfppHash)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
|
@ -238,6 +245,7 @@ data class DataPacket(
|
|||
viaMqtt = parcel.readInt() == 1
|
||||
retryCount = parcel.readInt()
|
||||
emoji = parcel.readInt()
|
||||
sfppHash = parcel.createByteArray()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.core.model.util
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.MessageDigest
|
||||
|
||||
object SfppHasher {
|
||||
private const val HASH_SIZE = 16
|
||||
private const val INT_BYTES = 4
|
||||
|
||||
fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(encryptedPayload)
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(to).array())
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(from).array())
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(id).array())
|
||||
return digest.digest().copyOf(HASH_SIZE)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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 org.meshtastic.core.model
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DataPacketTest {
|
||||
@Test
|
||||
fun `DataPacket sfppHash is nullable and correctly set`() {
|
||||
val hash = byteArrayOf(1, 2, 3, 4)
|
||||
val packet = DataPacket(to = "to", channel = 0, text = "hello").copy(sfppHash = hash)
|
||||
assertArrayEquals(hash, packet.sfppHash)
|
||||
|
||||
val packetNoHash = DataPacket(to = "to", channel = 0, text = "hello")
|
||||
assertEquals(null, packetNoHash.sfppHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MessageStatus SFPP_CONFIRMED exists`() {
|
||||
val status = MessageStatus.SFPP_CONFIRMED
|
||||
assertEquals("SFPP_CONFIRMED", status.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket serialization preserves sfppHash`() {
|
||||
val hash = byteArrayOf(5, 6, 7, 8)
|
||||
val packet =
|
||||
DataPacket(to = "to", channel = 0, text = "test")
|
||||
.copy(sfppHash = hash, status = MessageStatus.SFPP_CONFIRMED)
|
||||
|
||||
val json = Json { isLenient = true }
|
||||
val encoded = json.encodeToString(DataPacket.serializer(), packet)
|
||||
val decoded = json.decodeFromString(DataPacket.serializer(), encoded)
|
||||
|
||||
assertEquals(packet.status, decoded.status)
|
||||
assertArrayEquals(hash, decoded.sfppHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket equals and hashCode include sfppHash`() {
|
||||
val hash1 = byteArrayOf(1, 2, 3)
|
||||
val hash2 = byteArrayOf(4, 5, 6)
|
||||
val p1 = DataPacket(to = "to", channel = 0, text = "text").copy(sfppHash = hash1)
|
||||
val p2 = DataPacket(to = "to", channel = 0, text = "text").copy(sfppHash = hash1)
|
||||
val p3 = DataPacket(to = "to", channel = 0, text = "text").copy(sfppHash = hash2)
|
||||
val p4 = DataPacket(to = "to", channel = 0, text = "text").copy(sfppHash = null)
|
||||
|
||||
assertEquals(p1, p2)
|
||||
assertEquals(p1.hashCode(), p2.hashCode())
|
||||
|
||||
assertNotEquals(p1, p3)
|
||||
assertNotEquals(p1, p4)
|
||||
assertNotEquals(p1.hashCode(), p3.hashCode())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.core.model.util
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
class SfppHasherTest {
|
||||
|
||||
@Test
|
||||
fun `computeMessageHash produces consistent results`() {
|
||||
val payload = "Hello World".toByteArray()
|
||||
val to = 1234
|
||||
val from = 5678
|
||||
val id = 999
|
||||
|
||||
val hash1 = SfppHasher.computeMessageHash(payload, to, from, id)
|
||||
val hash2 = SfppHasher.computeMessageHash(payload, to, from, id)
|
||||
|
||||
assertArrayEquals(hash1, hash2)
|
||||
assertEquals(16, hash1.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `computeMessageHash produces different results for different inputs`() {
|
||||
val payload = "Hello World".toByteArray()
|
||||
val to = 1234
|
||||
val from = 5678
|
||||
val id = 999
|
||||
|
||||
val hashBase = SfppHasher.computeMessageHash(payload, to, from, id)
|
||||
|
||||
// Different payload
|
||||
val hashDiffPayload = SfppHasher.computeMessageHash("Hello Work".toByteArray(), to, from, id)
|
||||
assertNotEquals(hashBase.toList(), hashDiffPayload.toList())
|
||||
|
||||
// Different to
|
||||
val hashDiffTo = SfppHasher.computeMessageHash(payload, 1235, from, id)
|
||||
assertNotEquals(hashBase.toList(), hashDiffTo.toList())
|
||||
|
||||
// Different from
|
||||
val hashDiffFrom = SfppHasher.computeMessageHash(payload, to, 5679, id)
|
||||
assertNotEquals(hashBase.toList(), hashDiffFrom.toList())
|
||||
|
||||
// Different id
|
||||
val hashDiffId = SfppHasher.computeMessageHash(payload, to, from, 1000)
|
||||
assertNotEquals(hashBase.toList(), hashDiffId.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `computeMessageHash handles large values`() {
|
||||
val payload = byteArrayOf(1, 2, 3)
|
||||
// Testing that large unsigned-like values don't cause issues
|
||||
val to = -1 // 0xFFFFFFFF
|
||||
val from = 0x7FFFFFFF
|
||||
val id = Int.MIN_VALUE
|
||||
|
||||
val hash = SfppHasher.computeMessageHash(payload, to, from, id)
|
||||
assertEquals(16, hash.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `computeMessageHash follows little endian for integers`() {
|
||||
// This test ensures that the hash is computed consistently with the firmware
|
||||
// which uses little-endian byte order for these fields.
|
||||
val payload = byteArrayOf()
|
||||
val to = 0x01020304
|
||||
val from = 0x05060708
|
||||
val id = 0x090A0B0C
|
||||
|
||||
val hash = SfppHasher.computeMessageHash(payload, to, from, id)
|
||||
assertNotNull(hash)
|
||||
assertEquals(16, hash.size)
|
||||
}
|
||||
|
||||
private fun assertNotNull(any: Any?) {
|
||||
if (any == null) throw AssertionError("Should not be null")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue