mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(wire): migrate from protobuf -> wire (#4401)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9dbc8b7fbf
commit
25657e8f8f
239 changed files with 7149 additions and 6144 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,16 +14,15 @@
|
|||
* 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.database.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
|
@ -37,9 +36,9 @@ import org.meshtastic.core.database.entity.NodeEntity
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.user
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NodeInfoDaoTest {
|
||||
|
|
@ -54,12 +53,12 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 7,
|
||||
user =
|
||||
user {
|
||||
id = "!a1b2c3d4"
|
||||
longName = "Meshtastic c3d4"
|
||||
shortName = "c3d4"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
User(
|
||||
id = "!a1b2c3d4",
|
||||
long_name = "Meshtastic c3d4",
|
||||
short_name = "c3d4",
|
||||
hw_model = HardwareModel.UNSET,
|
||||
),
|
||||
longName = "Meshtastic c3d4",
|
||||
shortName = null, // Dao filter for includeUnknown
|
||||
)
|
||||
|
|
@ -68,13 +67,13 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 8,
|
||||
user =
|
||||
user {
|
||||
id = "+16508765308".format(8)
|
||||
longName = "Kevin Mester"
|
||||
shortName = "KLO"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
isLicensed = false
|
||||
},
|
||||
User(
|
||||
id = "+16508765308".format(8),
|
||||
long_name = "Kevin Mester",
|
||||
short_name = "KLO",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
is_licensed = false,
|
||||
),
|
||||
longName = "Kevin Mester",
|
||||
shortName = "KLO",
|
||||
latitude = 30.267153,
|
||||
|
|
@ -86,12 +85,12 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 9,
|
||||
user =
|
||||
user {
|
||||
id = "!25060801"
|
||||
longName = "Meshtastic 0801"
|
||||
shortName = "0801"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
},
|
||||
User(
|
||||
id = "!25060801",
|
||||
long_name = "Meshtastic 0801",
|
||||
short_name = "0801",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
longName = "Meshtastic 0801",
|
||||
shortName = "0801",
|
||||
hopsAway = 0,
|
||||
|
|
@ -102,12 +101,12 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 10,
|
||||
user =
|
||||
user {
|
||||
id = "!25060802"
|
||||
longName = "Meshtastic 0802"
|
||||
shortName = "0802"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
},
|
||||
User(
|
||||
id = "!25060802",
|
||||
long_name = "Meshtastic 0802",
|
||||
short_name = "0802",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
longName = "Meshtastic 0802",
|
||||
shortName = "0802",
|
||||
hopsAway = 0,
|
||||
|
|
@ -118,12 +117,12 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 11,
|
||||
user =
|
||||
user {
|
||||
id = "!25060803"
|
||||
longName = "Meshtastic 0803"
|
||||
shortName = "0803"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
},
|
||||
User(
|
||||
id = "!25060803",
|
||||
long_name = "Meshtastic 0803",
|
||||
short_name = "0803",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
longName = "Meshtastic 0803",
|
||||
shortName = "0803",
|
||||
hopsAway = 0,
|
||||
|
|
@ -134,12 +133,12 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 12,
|
||||
user =
|
||||
user {
|
||||
id = "!25060804"
|
||||
longName = "Meshtastic 0804"
|
||||
shortName = "0804"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
},
|
||||
User(
|
||||
id = "!25060804",
|
||||
long_name = "Meshtastic 0804",
|
||||
short_name = "0804",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
longName = "Meshtastic 0804",
|
||||
shortName = "0804",
|
||||
hopsAway = 3,
|
||||
|
|
@ -179,14 +178,14 @@ class NodeInfoDaoTest {
|
|||
NodeEntity(
|
||||
num = 1000 + index,
|
||||
user =
|
||||
user {
|
||||
id = "+165087653%02d".format(9 + index)
|
||||
longName = "Kevin Mester$index"
|
||||
shortName = "KM$index"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
isLicensed = false
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { index.toByte() })
|
||||
},
|
||||
User(
|
||||
id = "+165087653%02d".format(9 + index),
|
||||
long_name = "Kevin Mester$index",
|
||||
short_name = "KM$index",
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
is_licensed = false,
|
||||
public_key = ByteArray(32) { index.toByte() }.toByteString(),
|
||||
),
|
||||
longName = "Kevin Mester$index",
|
||||
shortName = "KM$index",
|
||||
latitude = pos.first,
|
||||
|
|
@ -256,14 +255,14 @@ class NodeInfoDaoTest {
|
|||
@Test
|
||||
fun testSortByAlpha() = runBlocking {
|
||||
val nodes = getNodes(sort = NodeSortOption.ALPHABETICAL)
|
||||
val sortedNodes = nodes.sortedBy { it.user.longName.uppercase() }
|
||||
val sortedNodes = nodes.sortedBy { it.user.long_name?.uppercase() ?: "" }
|
||||
assertEquals(sortedNodes, nodes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSortByDistance() = runBlocking {
|
||||
val nodes = getNodes(sort = NodeSortOption.DISTANCE)
|
||||
fun NodeEntity.toNode() = Node(num = num, user = user, position = position)
|
||||
fun NodeEntity.toNode() = Node(num = num, user = user, position = position ?: Position())
|
||||
val sortedNodes =
|
||||
nodes.sortedWith( // nodes with invalid (null) positions at the end
|
||||
compareBy<Node> { it.validPosition == null }.thenBy { it.distance(ourNode.toNode()) },
|
||||
|
|
@ -281,7 +280,7 @@ class NodeInfoDaoTest {
|
|||
@Test
|
||||
fun testSortByViaMqtt() = runBlocking {
|
||||
val nodes = getNodes(sort = NodeSortOption.VIA_MQTT)
|
||||
val sortedNodes = nodes.sortedBy { it.user.longName.contains("(MQTT)") }
|
||||
val sortedNodes = nodes.sortedBy { it.user.long_name?.contains("(MQTT)") == true }
|
||||
assertEquals(sortedNodes, nodes)
|
||||
}
|
||||
|
||||
|
|
@ -339,8 +338,7 @@ class NodeInfoDaoTest {
|
|||
|
||||
@Test
|
||||
fun testPkcMismatch() = runBlocking {
|
||||
val newNode =
|
||||
testNodes[1].copy(user = testNodes[1].user.copy { publicKey = ByteString.copyFrom(ByteArray(32) { 99 }) })
|
||||
val newNode = testNodes[1].copy(user = testNodes[1].user.copy(public_key = ByteArray(32) { 99 }.toByteString()))
|
||||
nodeInfoDao.putAll(listOf(newNode))
|
||||
val nodes = getNodes()
|
||||
val containsMismatchNode = nodes.any { it.mismatchKey }
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
|
@ -36,7 +37,7 @@ import org.meshtastic.core.database.entity.Packet
|
|||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PacketDaoTest {
|
||||
|
|
@ -68,11 +69,11 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = false,
|
||||
DataPacket(DataPacket.ID_BROADCAST, 0, "Message $it!"),
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Message $it!"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +105,7 @@ class PacketDaoTest {
|
|||
|
||||
@Test
|
||||
fun test_getAllPackets() = runBlocking {
|
||||
val packets = packetDao.getAllPackets(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE).first()
|
||||
val packets = packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first()
|
||||
assertEquals(testContactKeys.size * SAMPLE_SIZE, packets.size)
|
||||
|
||||
val onlyMyNodeNum = packets.all { it.myNodeNum == myNodeNum }
|
||||
|
|
@ -177,7 +178,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = true,
|
||||
|
|
@ -195,58 +196,58 @@ class PacketDaoTest {
|
|||
@Test
|
||||
fun test_sfppHashPersistence() = runBlocking {
|
||||
val hash = byteArrayOf(1, 2, 3, 4)
|
||||
val hashByteString = hash.toByteString()
|
||||
val packet =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = true,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = 0, text = "Test"),
|
||||
sfpp_hash = hash,
|
||||
sfpp_hash = hashByteString,
|
||||
)
|
||||
|
||||
packetDao.insert(packet)
|
||||
|
||||
val retrieved =
|
||||
packetDao.getAllPackets(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE).first().find {
|
||||
it.sfpp_hash?.contentEquals(hash) == true
|
||||
}
|
||||
packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first().find { it.sfpp_hash == hashByteString }
|
||||
assertNotNull(retrieved)
|
||||
assertTrue(retrieved?.sfpp_hash?.contentEquals(hash) == true)
|
||||
assertEquals(hashByteString, retrieved?.sfpp_hash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_findPacketBySfppHash() = runBlocking {
|
||||
val hash = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
|
||||
val hashByteString = hash.toByteString()
|
||||
val packet =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = true,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = 0, text = "Test"),
|
||||
sfpp_hash = hash,
|
||||
sfpp_hash = hashByteString,
|
||||
)
|
||||
|
||||
packetDao.insert(packet)
|
||||
|
||||
// Exact match
|
||||
val found = packetDao.findPacketBySfppHash(hash)
|
||||
val found = packetDao.findPacketBySfppHash(hashByteString)
|
||||
assertNotNull(found)
|
||||
assertTrue(found?.sfpp_hash?.contentEquals(hash) == true)
|
||||
assertEquals(hashByteString, found?.sfpp_hash)
|
||||
|
||||
// Substring match (first 8 bytes)
|
||||
val shortHash = hash.copyOf(8)
|
||||
val shortHash = hash.copyOf(8).toByteString()
|
||||
val foundShort = packetDao.findPacketBySfppHash(shortHash)
|
||||
assertNotNull(foundShort)
|
||||
assertTrue(foundShort?.sfpp_hash?.contentEquals(hash) == true)
|
||||
assertEquals(hashByteString, foundShort?.sfpp_hash)
|
||||
|
||||
// No match
|
||||
val wrongHash = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
val wrongHash = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0).toByteString()
|
||||
val notFound = packetDao.findPacketBySfppHash(wrongHash)
|
||||
assertNull(notFound)
|
||||
}
|
||||
|
|
@ -254,6 +255,7 @@ class PacketDaoTest {
|
|||
@Test
|
||||
fun test_findReactionBySfppHash() = runBlocking {
|
||||
val hash = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
|
||||
val hashByteString = hash.toByteString()
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
|
|
@ -261,20 +263,20 @@ class PacketDaoTest {
|
|||
userId = "sender",
|
||||
emoji = "👍",
|
||||
timestamp = System.currentTimeMillis(),
|
||||
sfpp_hash = hash,
|
||||
sfpp_hash = hashByteString,
|
||||
)
|
||||
|
||||
packetDao.insert(reaction)
|
||||
|
||||
val found = packetDao.findReactionBySfppHash(hash)
|
||||
val found = packetDao.findReactionBySfppHash(hashByteString)
|
||||
assertNotNull(found)
|
||||
assertTrue(found?.sfpp_hash?.contentEquals(hash) == true)
|
||||
assertEquals(hashByteString, found?.sfpp_hash)
|
||||
|
||||
val shortHash = hash.copyOf(8)
|
||||
val shortHash = hash.copyOf(8).toByteString()
|
||||
val foundShort = packetDao.findReactionBySfppHash(shortHash)
|
||||
assertNotNull(foundShort)
|
||||
|
||||
val wrongHash = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
val wrongHash = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0).toByteString()
|
||||
assertNull(packetDao.findReactionBySfppHash(wrongHash))
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +288,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = true,
|
||||
|
|
@ -309,7 +311,7 @@ class PacketDaoTest {
|
|||
val packetId = 999
|
||||
val fromNum = 123
|
||||
val toNum = 456
|
||||
val hash = byteArrayOf(9, 8, 7, 6)
|
||||
val hash = byteArrayOf(9, 8, 7, 6).toByteString()
|
||||
|
||||
val fromId = DataPacket.nodeNumToDefaultId(fromNum)
|
||||
val toId = DataPacket.nodeNumToDefaultId(toNum)
|
||||
|
|
@ -318,7 +320,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = true,
|
||||
|
|
@ -339,8 +341,8 @@ class PacketDaoTest {
|
|||
|
||||
val updated = packetDao.findPacketsWithId(packetId)[0]
|
||||
assertEquals(MessageStatus.SFPP_CONFIRMED, updated.data.status)
|
||||
assertTrue(updated.data.sfppHash?.contentEquals(hash) == true)
|
||||
assertTrue(updated.sfpp_hash?.contentEquals(hash) == true)
|
||||
assertEquals(hash, updated.data.sfppHash)
|
||||
assertEquals(hash, updated.sfpp_hash)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -352,7 +354,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = filteredContactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = false,
|
||||
|
|
@ -376,7 +378,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + i,
|
||||
read = false,
|
||||
|
|
@ -424,7 +426,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + index,
|
||||
read = false,
|
||||
|
|
@ -439,7 +441,7 @@ class PacketDaoTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + normalMessages.size + index,
|
||||
read = true, // Filtered messages are marked as read
|
||||
|
|
|
|||
|
|
@ -18,14 +18,18 @@ package org.meshtastic.core.database
|
|||
|
||||
import androidx.room.TypeConverter
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import kotlinx.serialization.json.Json
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class Converters {
|
||||
|
|
@ -40,64 +44,34 @@ class Converters {
|
|||
@TypeConverter fun dataToString(value: DataPacket): String = json.encodeToString(DataPacket.serializer(), value)
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try {
|
||||
MeshProtos.FromRadio.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToFromRadio TypeConverter error" }
|
||||
MeshProtos.FromRadio.getDefaultInstance()
|
||||
}
|
||||
fun bytesToFromRadio(bytes: ByteArray): FromRadio = FromRadio.ADAPTER.decodeOrNull(bytes, Logger) ?: FromRadio()
|
||||
|
||||
@TypeConverter fun fromRadioToBytes(value: MeshProtos.FromRadio): ByteArray? = value.toByteArray()
|
||||
@TypeConverter fun fromRadioToBytes(value: FromRadio): ByteArray = FromRadio.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter fun bytesToUser(bytes: ByteArray): User = User.ADAPTER.decodeOrNull(bytes, Logger) ?: User()
|
||||
|
||||
@TypeConverter fun userToBytes(value: User): ByteArray = User.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToUser(bytes: ByteArray): MeshProtos.User = try {
|
||||
MeshProtos.User.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToUser TypeConverter error" }
|
||||
MeshProtos.User.getDefaultInstance()
|
||||
}
|
||||
fun bytesToPosition(bytes: ByteArray): Position = Position.ADAPTER.decodeOrNull(bytes, Logger) ?: Position()
|
||||
|
||||
@TypeConverter fun userToBytes(value: MeshProtos.User): ByteArray? = value.toByteArray()
|
||||
@TypeConverter fun positionToBytes(value: Position): ByteArray = Position.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToPosition(bytes: ByteArray): MeshProtos.Position = try {
|
||||
MeshProtos.Position.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToPosition TypeConverter error" }
|
||||
MeshProtos.Position.getDefaultInstance()
|
||||
}
|
||||
fun bytesToTelemetry(bytes: ByteArray): Telemetry = Telemetry.ADAPTER.decodeOrNull(bytes, Logger) ?: Telemetry()
|
||||
|
||||
@TypeConverter fun positionToBytes(value: MeshProtos.Position): ByteArray? = value.toByteArray()
|
||||
@TypeConverter fun telemetryToBytes(value: Telemetry): ByteArray = Telemetry.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry = try {
|
||||
TelemetryProtos.Telemetry.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToTelemetry TypeConverter error" }
|
||||
TelemetryProtos.Telemetry.newBuilder().build() // Return an empty Telemetry object
|
||||
}
|
||||
fun bytesToPaxcounter(bytes: ByteArray): Paxcount = Paxcount.ADAPTER.decodeOrNull(bytes, Logger) ?: Paxcount()
|
||||
|
||||
@TypeConverter fun telemetryToBytes(value: TelemetryProtos.Telemetry): ByteArray? = value.toByteArray()
|
||||
@TypeConverter fun paxCounterToBytes(value: Paxcount): ByteArray = Paxcount.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount = try {
|
||||
PaxcountProtos.Paxcount.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToPaxcounter TypeConverter error" }
|
||||
PaxcountProtos.Paxcount.getDefaultInstance()
|
||||
}
|
||||
fun bytesToMetadata(bytes: ByteArray): DeviceMetadata =
|
||||
DeviceMetadata.ADAPTER.decodeOrNull(bytes, Logger) ?: DeviceMetadata()
|
||||
|
||||
@TypeConverter fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata = try {
|
||||
MeshProtos.DeviceMetadata.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Logger.e(ex) { "bytesToMetadata TypeConverter error" }
|
||||
MeshProtos.DeviceMetadata.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? = value.toByteArray()
|
||||
@TypeConverter fun metadataToBytes(value: DeviceMetadata): ByteArray = DeviceMetadata.ADAPTER.encode(value)
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringList(value: String?): List<String>? {
|
||||
|
|
@ -115,8 +89,7 @@ class Converters {
|
|||
return Json.encodeToString(list)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToByteString(bytes: ByteArray?): ByteString? = if (bytes == null) null else ByteString.copyFrom(bytes)
|
||||
@TypeConverter fun bytesToByteString(bytes: ByteArray?): ByteString? = bytes?.toByteString()
|
||||
|
||||
@TypeConverter fun byteStringToBytes(value: ByteString?): ByteArray? = value?.toByteArray()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import androidx.room.OnConflictStrategy
|
|||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeWithRelations
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Dao
|
||||
|
|
@ -47,13 +47,13 @@ interface NodeInfoDao {
|
|||
private suspend fun getVerifiedNodeForUpsert(incomingNode: NodeEntity): NodeEntity {
|
||||
// Populate the NodeEntity.publicKey field from the User.publicKey for consistency
|
||||
// and to support lazy migration.
|
||||
incomingNode.publicKey = incomingNode.user.publicKey
|
||||
incomingNode.publicKey = incomingNode.user.public_key
|
||||
|
||||
// Populate denormalized name columns from the User protobuf for search functionality
|
||||
// Only populate if the user is not a placeholder (hwModel != UNSET); otherwise keep them null
|
||||
if (incomingNode.user.hwModel != MeshProtos.HardwareModel.UNSET) {
|
||||
incomingNode.longName = incomingNode.user.longName
|
||||
incomingNode.shortName = incomingNode.user.shortName
|
||||
if (incomingNode.user.hw_model != HardwareModel.UNSET) {
|
||||
incomingNode.longName = incomingNode.user.long_name
|
||||
incomingNode.shortName = incomingNode.user.short_name
|
||||
} else {
|
||||
incomingNode.longName = null
|
||||
incomingNode.shortName = null
|
||||
|
|
@ -72,7 +72,7 @@ interface NodeInfoDao {
|
|||
private suspend fun handleNewNodeUpsertValidation(newNode: NodeEntity): NodeEntity {
|
||||
// Check if the new node's public key (if present and not empty)
|
||||
// is already claimed by another existing node.
|
||||
if (newNode.publicKey?.isEmpty == false) {
|
||||
if ((newNode.publicKey?.size ?: 0) > 0) {
|
||||
val nodeWithSamePK = findNodeByPublicKey(newNode.publicKey)
|
||||
if (nodeWithSamePK != null && nodeWithSamePK.num != newNode.num) {
|
||||
// This is a potential impersonation attempt.
|
||||
|
|
@ -85,9 +85,9 @@ interface NodeInfoDao {
|
|||
}
|
||||
|
||||
private fun handleExistingNodeUpsertValidation(existingNode: NodeEntity, incomingNode: NodeEntity): NodeEntity {
|
||||
val isPlaceholder = incomingNode.user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
val hasExistingUser = existingNode.user.hwModel != MeshProtos.HardwareModel.UNSET
|
||||
val isDefaultName = incomingNode.user.longName.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
|
||||
val isPlaceholder = incomingNode.user.hw_model == HardwareModel.UNSET
|
||||
val hasExistingUser = existingNode.user.hw_model != HardwareModel.UNSET
|
||||
val isDefaultName = incomingNode.user.long_name?.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$")) == true
|
||||
|
||||
val shouldPreserve = hasExistingUser && isPlaceholder && isDefaultName
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ interface NodeInfoDao {
|
|||
|
||||
// A public key is considered matching if the incoming key equals the existing key,
|
||||
// OR if the existing key is empty (allowing a new key to be set or an update to proceed).
|
||||
val existingResolvedKey = existingNode.publicKey ?: existingNode.user.publicKey
|
||||
val existingResolvedKey = existingNode.publicKey ?: existingNode.user.public_key
|
||||
val isPublicKeyMatchingOrExistingIsEmpty = existingResolvedKey == incomingNode.publicKey || !existingNode.hasPKC
|
||||
|
||||
val resolvedNotes = if (incomingNode.notes.isBlank()) existingNode.notes else incomingNode.notes
|
||||
|
|
@ -129,7 +129,7 @@ interface NodeInfoDao {
|
|||
// We allow the name and user info to update, but we clear the public key
|
||||
// to indicate that this node is no longer "verified" against the previous key.
|
||||
incomingNode.copy(
|
||||
user = incomingNode.user.toBuilder().setPublicKey(NodeEntity.ERROR_BYTE_STRING).build(),
|
||||
user = incomingNode.user.copy(public_key = NodeEntity.ERROR_BYTE_STRING),
|
||||
publicKey = NodeEntity.ERROR_BYTE_STRING,
|
||||
notes = resolvedNotes,
|
||||
)
|
||||
|
|
@ -289,10 +289,9 @@ interface NodeInfoDao {
|
|||
nodes
|
||||
.filter { node ->
|
||||
// Only backfill if columns are NULL AND the user is not a placeholder (hwModel != UNSET)
|
||||
(node.longName == null || node.shortName == null) &&
|
||||
node.user.hwModel != MeshProtos.HardwareModel.UNSET
|
||||
(node.longName == null || node.shortName == null) && node.user.hw_model != HardwareModel.UNSET
|
||||
}
|
||||
.map { node -> node.copy(longName = node.user.longName, shortName = node.user.shortName) }
|
||||
.map { node -> node.copy(longName = node.user.long_name, shortName = node.user.short_name) }
|
||||
if (nodesToUpdate.isNotEmpty()) {
|
||||
putAll(nodesToUpdate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,14 @@ import androidx.room.Transaction
|
|||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.PacketEntity
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Dao
|
||||
|
|
@ -300,7 +301,7 @@ interface PacketDao {
|
|||
AND substr(sfpp_hash, 1, 8) = substr(:hash, 1, 8)
|
||||
""",
|
||||
)
|
||||
suspend fun findPacketBySfppHash(hash: ByteArray): Packet?
|
||||
suspend fun findPacketBySfppHash(hash: ByteString): Packet?
|
||||
|
||||
@Transaction
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = getDataPackets().filter { it.status == MessageStatus.QUEUED }
|
||||
|
|
@ -386,7 +387,7 @@ interface PacketDao {
|
|||
AND substr(sfpp_hash, 1, 8) = substr(:hash, 1, 8)
|
||||
""",
|
||||
)
|
||||
suspend fun findReactionBySfppHash(hash: ByteArray): ReactionEntity?
|
||||
suspend fun findReactionBySfppHash(hash: ByteString): ReactionEntity?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,18 +14,19 @@
|
|||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.google.protobuf.TextFormat
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.FromRadio
|
||||
import org.meshtastic.proto.Portnums
|
||||
import java.io.IOException
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.MyNodeInfo
|
||||
import org.meshtastic.proto.NodeInfo
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
@Suppress("EmptyCatchBlock", "SwallowedException", "ConstructorParameterNaming")
|
||||
@Entity(tableName = "log", indices = [Index(value = ["from_num"]), Index(value = ["port_num"])])
|
||||
|
|
@ -37,52 +38,25 @@ data class MeshLog(
|
|||
@ColumnInfo(name = "from_num", defaultValue = "0") val fromNum: Int = 0,
|
||||
@ColumnInfo(name = "port_num", defaultValue = "0") val portNum: Int = 0,
|
||||
@ColumnInfo(name = "from_radio", typeAffinity = ColumnInfo.BLOB, defaultValue = "x''")
|
||||
val fromRadio: FromRadio = FromRadio.getDefaultInstance(),
|
||||
val fromRadio: FromRadio = FromRadio(),
|
||||
) {
|
||||
|
||||
val meshPacket: MeshProtos.MeshPacket?
|
||||
get() {
|
||||
if (message_type == "Packet") {
|
||||
val builder = MeshProtos.MeshPacket.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
val meshPacket: MeshPacket?
|
||||
get() = fromRadio.packet
|
||||
|
||||
val nodeInfo: MeshProtos.NodeInfo?
|
||||
get() {
|
||||
if (message_type == "NodeInfo") {
|
||||
val builder = MeshProtos.NodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
val nodeInfo: NodeInfo?
|
||||
get() = fromRadio.node_info
|
||||
|
||||
val myNodeInfo: MeshProtos.MyNodeInfo?
|
||||
get() {
|
||||
if (message_type == "MyNodeInfo") {
|
||||
val builder = MeshProtos.MyNodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
val myNodeInfo: MyNodeInfo?
|
||||
get() = fromRadio.my_info
|
||||
|
||||
val position: MeshProtos.Position?
|
||||
get() {
|
||||
return meshPacket?.run {
|
||||
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
||||
return MeshProtos.Position.parseFrom(decoded.payload)
|
||||
val position: Position?
|
||||
get() =
|
||||
fromRadio.packet?.decoded?.payload?.let {
|
||||
if (fromRadio.packet?.decoded?.portnum == org.meshtastic.proto.PortNum.POSITION_APP) {
|
||||
Position.ADAPTER.decodeOrNull(it, Logger)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return null
|
||||
} ?: nodeInfo?.position
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.isNotEmpty
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
|
|
@ -31,10 +31,12 @@ import org.meshtastic.core.model.MeshUser
|
|||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Position as WirePosition
|
||||
|
||||
data class NodeWithRelations(
|
||||
@Embedded val node: NodeEntity,
|
||||
|
|
@ -50,15 +52,15 @@ data class NodeWithRelations(
|
|||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceTelemetry.deviceMetrics,
|
||||
deviceMetrics = deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(),
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
isMuted = isMuted,
|
||||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics(),
|
||||
paxcounter = paxcounter,
|
||||
notes = notes,
|
||||
manuallyVerified = manuallyVerified,
|
||||
|
|
@ -94,7 +96,7 @@ data class NodeWithRelations(
|
|||
@Entity(tableName = "metadata", indices = [Index(value = ["num"])])
|
||||
data class MetadataEntity(
|
||||
@PrimaryKey val num: Int,
|
||||
@ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) val proto: MeshProtos.DeviceMetadata,
|
||||
@ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) val proto: DeviceMetadata,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
|
|
@ -113,18 +115,16 @@ data class MetadataEntity(
|
|||
)
|
||||
data class NodeEntity(
|
||||
@PrimaryKey(autoGenerate = false) val num: Int, // This is immutable, and used as a key
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) var user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) var user: User = User(),
|
||||
@ColumnInfo(name = "long_name") var longName: String? = null,
|
||||
@ColumnInfo(name = "short_name") var shortName: String? = null, // used in includeUnknown filter
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
var position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) var position: WirePosition = WirePosition(),
|
||||
var latitude: Double = 0.0,
|
||||
var longitude: Double = 0.0,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
@ColumnInfo(name = "last_heard") var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
@ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB)
|
||||
var deviceTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
|
||||
@ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB) var deviceTelemetry: Telemetry = Telemetry(),
|
||||
var channel: Int = 0,
|
||||
@ColumnInfo(name = "via_mqtt") var viaMqtt: Boolean = false,
|
||||
@ColumnInfo(name = "hops_away") var hopsAway: Int = -1,
|
||||
|
|
@ -132,33 +132,34 @@ data class NodeEntity(
|
|||
@ColumnInfo(name = "is_ignored", defaultValue = "0") var isIgnored: Boolean = false,
|
||||
@ColumnInfo(name = "is_muted", defaultValue = "0") var isMuted: Boolean = false,
|
||||
@ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB)
|
||||
var environmentTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.newBuilder().build(),
|
||||
@ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB)
|
||||
var powerTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||
var environmentTelemetry: Telemetry = Telemetry(),
|
||||
@ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB) var powerTelemetry: Telemetry = Telemetry(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) var paxcounter: Paxcount = Paxcount(),
|
||||
@ColumnInfo(name = "public_key") var publicKey: ByteString? = null,
|
||||
@ColumnInfo(name = "notes", defaultValue = "") var notes: String = "",
|
||||
@ColumnInfo(name = "manually_verified", defaultValue = "0")
|
||||
var manuallyVerified: Boolean = false, // ONLY set true when scanned/imported manually
|
||||
@ColumnInfo(name = "node_status") var nodeStatus: String? = null,
|
||||
) {
|
||||
val deviceMetrics: TelemetryProtos.DeviceMetrics
|
||||
get() = deviceTelemetry.deviceMetrics
|
||||
val deviceMetrics: org.meshtastic.proto.DeviceMetrics?
|
||||
get() = deviceTelemetry.device_metrics
|
||||
|
||||
val environmentMetrics: TelemetryProtos.EnvironmentMetrics
|
||||
get() = environmentTelemetry.environmentMetrics
|
||||
val environmentMetrics: org.meshtastic.proto.EnvironmentMetrics?
|
||||
get() = environmentTelemetry.environment_metrics
|
||||
|
||||
val powerMetrics: org.meshtastic.proto.PowerMetrics?
|
||||
get() = powerTelemetry.power_metrics
|
||||
|
||||
val isUnknownUser
|
||||
get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
get() = user.hw_model == HardwareModel.UNSET
|
||||
|
||||
val hasPKC
|
||||
get() = (publicKey ?: user.publicKey).isNotEmpty()
|
||||
get() = (publicKey ?: user.public_key)?.size?.let { it > 0 } == true
|
||||
|
||||
fun setPosition(p: MeshProtos.Position, defaultTime: Int = currentTime()) {
|
||||
position = p.copy { time = if (p.time != 0) p.time else defaultTime }
|
||||
latitude = degD(p.latitudeI)
|
||||
longitude = degD(p.longitudeI)
|
||||
fun setPosition(p: WirePosition, defaultTime: Int = currentTime()) {
|
||||
position = p.copy(time = if (p.time != 0) p.time else defaultTime)
|
||||
latitude = degD(p.latitude_i ?: 0)
|
||||
longitude = degD(p.longitude_i ?: 0)
|
||||
}
|
||||
|
||||
/** true if the device was heard from recently */
|
||||
|
|
@ -173,7 +174,7 @@ data class NodeEntity(
|
|||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
val ERROR_BYTE_STRING: ByteString = ByteString.copyFrom(ByteArray(32) { 0 })
|
||||
val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString()
|
||||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
|
|
@ -185,17 +186,17 @@ data class NodeEntity(
|
|||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceTelemetry.deviceMetrics,
|
||||
deviceMetrics = deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(),
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
isMuted = isMuted,
|
||||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics(),
|
||||
paxcounter = paxcounter,
|
||||
publicKey = publicKey ?: user.publicKey,
|
||||
publicKey = publicKey ?: user.public_key,
|
||||
notes = notes,
|
||||
nodeStatus = nodeStatus,
|
||||
)
|
||||
|
|
@ -205,22 +206,22 @@ data class NodeEntity(
|
|||
user =
|
||||
MeshUser(
|
||||
id = user.id,
|
||||
longName = user.longName,
|
||||
shortName = user.shortName,
|
||||
hwModel = user.hwModel,
|
||||
role = user.roleValue,
|
||||
longName = user.long_name ?: "",
|
||||
shortName = user.short_name ?: "",
|
||||
hwModel = user.hw_model,
|
||||
role = user.role.value,
|
||||
)
|
||||
.takeIf { user.id.isNotEmpty() },
|
||||
position =
|
||||
Position(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
altitude = position.altitude,
|
||||
altitude = position.altitude ?: 0,
|
||||
time = position.time,
|
||||
satellitesInView = position.satsInView,
|
||||
groundSpeed = position.groundSpeed,
|
||||
groundTrack = position.groundTrack,
|
||||
precisionBits = position.precisionBits,
|
||||
satellitesInView = position.sats_in_view ?: 0,
|
||||
groundSpeed = position.ground_speed ?: 0,
|
||||
groundTrack = position.ground_track ?: 0,
|
||||
precisionBits = position.precision_bits ?: 0,
|
||||
)
|
||||
.takeIf { it.isValid() },
|
||||
snr = snr,
|
||||
|
|
@ -229,16 +230,16 @@ data class NodeEntity(
|
|||
deviceMetrics =
|
||||
DeviceMetrics(
|
||||
time = deviceTelemetry.time,
|
||||
batteryLevel = deviceMetrics.batteryLevel,
|
||||
voltage = deviceMetrics.voltage,
|
||||
channelUtilization = deviceMetrics.channelUtilization,
|
||||
airUtilTx = deviceMetrics.airUtilTx,
|
||||
uptimeSeconds = deviceMetrics.uptimeSeconds,
|
||||
batteryLevel = deviceMetrics?.battery_level ?: 0,
|
||||
voltage = deviceMetrics?.voltage ?: 0f,
|
||||
channelUtilization = deviceMetrics?.channel_utilization ?: 0f,
|
||||
airUtilTx = deviceMetrics?.air_util_tx ?: 0f,
|
||||
uptimeSeconds = deviceMetrics?.uptime_seconds ?: 0,
|
||||
),
|
||||
channel = channel,
|
||||
environmentMetrics =
|
||||
EnvironmentMetrics.fromTelemetryProto(
|
||||
environmentTelemetry.environmentMetrics,
|
||||
environmentTelemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
environmentTelemetry.time,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
import org.meshtastic.proto.MeshProtos.User
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
data class PacketEntity(
|
||||
@Embedded val packet: Packet,
|
||||
|
|
@ -87,7 +88,7 @@ data class Packet(
|
|||
@ColumnInfo(name = "snr", defaultValue = "0") val snr: Float = 0f,
|
||||
@ColumnInfo(name = "rssi", defaultValue = "0") val rssi: Int = 0,
|
||||
@ColumnInfo(name = "hopsAway", defaultValue = "-1") val hopsAway: Int = -1,
|
||||
@ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteArray? = null,
|
||||
@ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteString? = null,
|
||||
@ColumnInfo(name = "filtered", defaultValue = "0") val filtered: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
|
|
@ -144,7 +145,7 @@ data class Reaction(
|
|||
val relayNode: Int? = null,
|
||||
val to: String? = null,
|
||||
val channel: Int = 0,
|
||||
val sfppHash: ByteArray? = null,
|
||||
val sfppHash: ByteString? = null,
|
||||
)
|
||||
|
||||
@Suppress("ConstructorParameterNaming")
|
||||
|
|
@ -170,7 +171,7 @@ data class ReactionEntity(
|
|||
@ColumnInfo(name = "relay_node") val relayNode: Int? = null,
|
||||
@ColumnInfo(name = "to") val to: String? = null,
|
||||
@ColumnInfo(name = "channel", defaultValue = "0") val channel: Int = 0,
|
||||
@ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteArray? = null,
|
||||
@ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteString? = null,
|
||||
)
|
||||
|
||||
private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node): Reaction {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,14 +14,13 @@
|
|||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
@Entity(
|
||||
tableName = "traceroute_node_position",
|
||||
|
|
@ -41,5 +40,5 @@ data class TracerouteNodePositionEntity(
|
|||
@ColumnInfo(name = "log_uuid") val logUuid: String,
|
||||
@ColumnInfo(name = "request_id") val requestId: Int,
|
||||
@ColumnInfo(name = "node_num") val nodeNum: Int,
|
||||
@ColumnInfo(name = "position", typeAffinity = ColumnInfo.BLOB) val position: MeshProtos.Position,
|
||||
@ColumnInfo(name = "position", typeAffinity = ColumnInfo.BLOB) val position: Position,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,28 +46,28 @@ import org.meshtastic.core.strings.routing_error_rate_limit_exceeded
|
|||
import org.meshtastic.core.strings.routing_error_timeout
|
||||
import org.meshtastic.core.strings.routing_error_too_large
|
||||
import org.meshtastic.core.strings.unrecognized
|
||||
import org.meshtastic.proto.MeshProtos.Routing
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun getStringResFrom(routingError: Int): StringResource = when (routingError) {
|
||||
Routing.Error.NONE_VALUE -> Res.string.routing_error_none
|
||||
Routing.Error.NO_ROUTE_VALUE -> Res.string.routing_error_no_route
|
||||
Routing.Error.GOT_NAK_VALUE -> Res.string.routing_error_got_nak
|
||||
Routing.Error.TIMEOUT_VALUE -> Res.string.routing_error_timeout
|
||||
Routing.Error.NO_INTERFACE_VALUE -> Res.string.routing_error_no_interface
|
||||
Routing.Error.MAX_RETRANSMIT_VALUE -> Res.string.routing_error_max_retransmit
|
||||
Routing.Error.NO_CHANNEL_VALUE -> Res.string.routing_error_no_channel
|
||||
Routing.Error.TOO_LARGE_VALUE -> Res.string.routing_error_too_large
|
||||
Routing.Error.NO_RESPONSE_VALUE -> Res.string.routing_error_no_response
|
||||
Routing.Error.DUTY_CYCLE_LIMIT_VALUE -> Res.string.routing_error_duty_cycle_limit
|
||||
Routing.Error.BAD_REQUEST_VALUE -> Res.string.routing_error_bad_request
|
||||
Routing.Error.NOT_AUTHORIZED_VALUE -> Res.string.routing_error_not_authorized
|
||||
Routing.Error.PKI_FAILED_VALUE -> Res.string.routing_error_pki_failed
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY_VALUE -> Res.string.routing_error_pki_unknown_pubkey
|
||||
Routing.Error.ADMIN_BAD_SESSION_KEY_VALUE -> Res.string.routing_error_admin_bad_session_key
|
||||
Routing.Error.ADMIN_PUBLIC_KEY_UNAUTHORIZED_VALUE -> Res.string.routing_error_admin_public_key_unauthorized
|
||||
Routing.Error.RATE_LIMIT_EXCEEDED_VALUE -> Res.string.routing_error_rate_limit_exceeded
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY_VALUE -> Res.string.routing_error_pki_send_fail_public_key
|
||||
Routing.Error.NONE.value -> Res.string.routing_error_none
|
||||
Routing.Error.NO_ROUTE.value -> Res.string.routing_error_no_route
|
||||
Routing.Error.GOT_NAK.value -> Res.string.routing_error_got_nak
|
||||
Routing.Error.TIMEOUT.value -> Res.string.routing_error_timeout
|
||||
Routing.Error.NO_INTERFACE.value -> Res.string.routing_error_no_interface
|
||||
Routing.Error.MAX_RETRANSMIT.value -> Res.string.routing_error_max_retransmit
|
||||
Routing.Error.NO_CHANNEL.value -> Res.string.routing_error_no_channel
|
||||
Routing.Error.TOO_LARGE.value -> Res.string.routing_error_too_large
|
||||
Routing.Error.NO_RESPONSE.value -> Res.string.routing_error_no_response
|
||||
Routing.Error.DUTY_CYCLE_LIMIT.value -> Res.string.routing_error_duty_cycle_limit
|
||||
Routing.Error.BAD_REQUEST.value -> Res.string.routing_error_bad_request
|
||||
Routing.Error.NOT_AUTHORIZED.value -> Res.string.routing_error_not_authorized
|
||||
Routing.Error.PKI_FAILED.value -> Res.string.routing_error_pki_failed
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value -> Res.string.routing_error_pki_unknown_pubkey
|
||||
Routing.Error.ADMIN_BAD_SESSION_KEY.value -> Res.string.routing_error_admin_bad_session_key
|
||||
Routing.Error.ADMIN_PUBLIC_KEY_UNAUTHORIZED.value -> Res.string.routing_error_admin_public_key_unauthorized
|
||||
Routing.Error.RATE_LIMIT_EXCEEDED.value -> Res.string.routing_error_rate_limit_exceeded
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value -> Res.string.routing_error_pki_send_fail_public_key
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,47 +17,48 @@
|
|||
package org.meshtastic.core.database.model
|
||||
|
||||
import android.graphics.Color
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.isNotEmpty
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.TelemetryProtos.DeviceMetrics
|
||||
import org.meshtastic.proto.TelemetryProtos.EnvironmentMetrics
|
||||
import org.meshtastic.proto.TelemetryProtos.PowerMetrics
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.PowerMetrics
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
data class Node(
|
||||
val num: Int,
|
||||
val metadata: MeshProtos.DeviceMetadata? = null,
|
||||
val user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
val position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
|
||||
val metadata: DeviceMetadata? = null,
|
||||
val user: User = User(),
|
||||
val position: Position = Position(),
|
||||
val snr: Float = Float.MAX_VALUE,
|
||||
val rssi: Int = Int.MAX_VALUE,
|
||||
val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
val deviceMetrics: DeviceMetrics = DeviceMetrics.getDefaultInstance(),
|
||||
val deviceMetrics: DeviceMetrics = DeviceMetrics(),
|
||||
val channel: Int = 0,
|
||||
val viaMqtt: Boolean = false,
|
||||
val hopsAway: Int = -1,
|
||||
val isFavorite: Boolean = false,
|
||||
val isIgnored: Boolean = false,
|
||||
val isMuted: Boolean = false,
|
||||
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics.getDefaultInstance(),
|
||||
val powerMetrics: PowerMetrics = PowerMetrics.getDefaultInstance(),
|
||||
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics(),
|
||||
val powerMetrics: PowerMetrics = PowerMetrics(),
|
||||
val paxcounter: Paxcount = Paxcount(),
|
||||
val publicKey: ByteString? = null,
|
||||
val notes: String = "",
|
||||
val manuallyVerified: Boolean = false,
|
||||
val nodeStatus: String? = null,
|
||||
) {
|
||||
val capabilities: Capabilities by lazy { Capabilities(metadata?.firmwareVersion) }
|
||||
val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) }
|
||||
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
|
|
@ -69,41 +70,41 @@ data class Node(
|
|||
}
|
||||
|
||||
val isUnknownUser
|
||||
get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
get() = user.hw_model == HardwareModel.UNSET
|
||||
|
||||
val hasPKC
|
||||
get() = (publicKey ?: user.publicKey).isNotEmpty()
|
||||
get() = (publicKey ?: user.public_key)?.size?.let { it > 0 } == true
|
||||
|
||||
val mismatchKey
|
||||
get() = (publicKey ?: user.publicKey) == NodeEntity.ERROR_BYTE_STRING
|
||||
get() = (publicKey ?: user.public_key) == NodeEntity.ERROR_BYTE_STRING
|
||||
|
||||
val hasEnvironmentMetrics: Boolean
|
||||
get() = environmentMetrics != EnvironmentMetrics.getDefaultInstance()
|
||||
get() = environmentMetrics != EnvironmentMetrics()
|
||||
|
||||
val hasPowerMetrics: Boolean
|
||||
get() = powerMetrics != PowerMetrics.getDefaultInstance()
|
||||
get() = powerMetrics != PowerMetrics()
|
||||
|
||||
val batteryLevel
|
||||
get() = deviceMetrics.batteryLevel
|
||||
get() = deviceMetrics.battery_level
|
||||
|
||||
val voltage
|
||||
get() = deviceMetrics.voltage
|
||||
|
||||
val batteryStr
|
||||
get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
|
||||
get() = if ((batteryLevel ?: 0) in 1..100) "$batteryLevel%" else ""
|
||||
|
||||
val latitude
|
||||
get() = position.latitudeI * 1e-7
|
||||
get() = (position.latitude_i ?: 0) * 1e-7
|
||||
|
||||
val longitude
|
||||
get() = position.longitudeI * 1e-7
|
||||
get() = (position.longitude_i ?: 0) * 1e-7
|
||||
|
||||
private fun hasValidPosition(): Boolean = latitude != 0.0 &&
|
||||
longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
|
||||
val validPosition: MeshProtos.Position?
|
||||
val validPosition: Position?
|
||||
get() = position.takeIf { hasValidPosition() }
|
||||
|
||||
// @return distance in meters to some other node (or null if unknown)
|
||||
|
|
@ -113,7 +114,7 @@ data class Node(
|
|||
}
|
||||
|
||||
// @return formatted distance string to another node, using the given display units
|
||||
fun distanceStr(o: Node, displayUnits: DisplayConfig.DisplayUnits): String? =
|
||||
fun distanceStr(o: Node, displayUnits: Config.DisplayConfig.DisplayUnits): String? =
|
||||
distance(o)?.toDistanceString(displayUnits)
|
||||
|
||||
// @return bearing to the other position in degrees
|
||||
|
|
@ -126,36 +127,36 @@ data class Node(
|
|||
|
||||
private fun EnvironmentMetrics.getDisplayStrings(isFahrenheit: Boolean): List<String> {
|
||||
val temp =
|
||||
if (temperature != 0f) {
|
||||
if ((temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(temperature))
|
||||
"%.1f°F".format(celsiusToFahrenheit(temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) "%.0f%%".format(relative_humidity) else null
|
||||
val soilTemperatureStr =
|
||||
if (soilTemperature != 0f) {
|
||||
if ((soil_temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(soilTemperature))
|
||||
"%.1f°F".format(celsiusToFahrenheit(soil_temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(soilTemperature)
|
||||
"%.1f°C".format(soil_temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val soilMoistureRange = 0..100
|
||||
val soilMoisture =
|
||||
if (soilMoisture in soilMoistureRange && soilTemperature != 0f) {
|
||||
"%d%%".format(soilMoisture)
|
||||
if ((soil_moisture ?: Int.MIN_VALUE) in soilMoistureRange && (soil_temperature ?: 0f) != 0f) {
|
||||
"%d%%".format(soil_moisture)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if (current != 0f) "%.1fmA".format(current) else null
|
||||
val iaq = if (iaq != 0) "IAQ: $iaq" else null
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if ((current ?: 0f) != 0f) "%.1fmA".format(current) else null
|
||||
val iaq = if ((iaq ?: 0) != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
paxcounter.getDisplayString(),
|
||||
|
|
@ -169,19 +170,19 @@ data class Node(
|
|||
)
|
||||
}
|
||||
|
||||
private fun PaxcountProtos.Paxcount.getDisplayString() =
|
||||
"PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 || wifi != 0 }
|
||||
private fun Paxcount.getDisplayString() =
|
||||
"PAX: ${(ble ?: 0) + (wifi ?: 0)} (B:${ble ?: 0}/W:${wifi ?: 0})".takeIf { (ble ?: 0) != 0 || (wifi ?: 0) != 0 }
|
||||
|
||||
fun getTelemetryStrings(isFahrenheit: Boolean = false): List<String> =
|
||||
environmentMetrics.getDisplayStrings(isFahrenheit)
|
||||
}
|
||||
|
||||
fun ConfigProtos.Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
|
||||
fun Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
|
||||
listOf(
|
||||
ConfigProtos.Config.DeviceConfig.Role.REPEATER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
ConfigProtos.Config.DeviceConfig.Role.SENSOR,
|
||||
ConfigProtos.Config.DeviceConfig.Role.TRACKER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.TAK_TRACKER,
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.SENSOR,
|
||||
Config.DeviceConfig.Role.TRACKER,
|
||||
Config.DeviceConfig.Role.TAK_TRACKER,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ package org.meshtastic.core.database.dao
|
|||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
|
@ -31,8 +31,8 @@ import org.meshtastic.core.database.MeshtasticDatabase
|
|||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.channelSettings
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationTest {
|
||||
|
|
@ -69,32 +69,20 @@ class MigrationTest {
|
|||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_duplicatePSK() = runBlocking {
|
||||
// PSK "AQ==" is base64 for single byte 0x01
|
||||
val pskBytes = ByteString.copyFrom(byteArrayOf(0x01))
|
||||
// PSK \"AQ==\" is base64 for single byte 0x01
|
||||
val pskBytes = byteArrayOf(0x01).toByteString()
|
||||
|
||||
// Create packets for Channel 0
|
||||
insertPacket(channel = 0, text = "Message Ch0")
|
||||
|
||||
// Old settings: Channel 0 has PSK_A
|
||||
val oldSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskBytes
|
||||
name = "LongFast"
|
||||
},
|
||||
)
|
||||
val oldSettings = listOf(ChannelSettings(psk = pskBytes, name = "LongFast"))
|
||||
|
||||
// New settings: Channel 0 has PSK_A, Channel 1 has PSK_A
|
||||
val newSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskBytes
|
||||
name = "LongFast"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskBytes
|
||||
name = "NewChan"
|
||||
},
|
||||
ChannelSettings(psk = pskBytes, name = "LongFast"),
|
||||
ChannelSettings(psk = pskBytes, name = "NewChan"),
|
||||
)
|
||||
|
||||
// Perform migration
|
||||
|
|
@ -107,35 +95,15 @@ class MigrationTest {
|
|||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_reorder() = runBlocking {
|
||||
val pskA = ByteString.copyFrom(byteArrayOf(0x01))
|
||||
val pskB = ByteString.copyFrom(byteArrayOf(0x02))
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
val pskB = byteArrayOf(0x02).toByteString()
|
||||
|
||||
insertPacket(channel = 0, text = "Msg A")
|
||||
insertPacket(channel = 1, text = "Msg B")
|
||||
|
||||
val oldSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskB
|
||||
name = "B"
|
||||
},
|
||||
)
|
||||
val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A"), ChannelSettings(psk = pskB, name = "B"))
|
||||
|
||||
val newSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskB
|
||||
name = "B"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A"
|
||||
},
|
||||
)
|
||||
val newSettings = listOf(ChannelSettings(psk = pskB, name = "B"), ChannelSettings(psk = pskA, name = "A"))
|
||||
|
||||
packetDao.migrateChannelsByPSK(oldSettings, newSettings)
|
||||
|
||||
|
|
@ -146,35 +114,15 @@ class MigrationTest {
|
|||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_disambiguateByName() = runBlocking {
|
||||
val pskA = ByteString.copyFrom(byteArrayOf(0x01))
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
|
||||
insertPacket(channel = 0, text = "Msg A1")
|
||||
insertPacket(channel = 1, text = "Msg A2")
|
||||
|
||||
val oldSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A1"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A2"
|
||||
},
|
||||
)
|
||||
val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A1"), ChannelSettings(psk = pskA, name = "A2"))
|
||||
|
||||
// Swap positions but keep names and PSKs
|
||||
val newSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A2"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A1"
|
||||
},
|
||||
)
|
||||
val newSettings = listOf(ChannelSettings(psk = pskA, name = "A2"), ChannelSettings(psk = pskA, name = "A1"))
|
||||
|
||||
packetDao.migrateChannelsByPSK(oldSettings, newSettings)
|
||||
|
||||
|
|
@ -185,30 +133,14 @@ class MigrationTest {
|
|||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runBlocking {
|
||||
val pskA = ByteString.copyFrom(byteArrayOf(0x01))
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
|
||||
insertPacket(channel = 0, text = "Msg A")
|
||||
|
||||
val oldSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A"
|
||||
},
|
||||
)
|
||||
val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A"))
|
||||
|
||||
// New settings has two identical channels (same PSK, same Name)
|
||||
val newSettings =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A"
|
||||
},
|
||||
channelSettings {
|
||||
psk = pskA
|
||||
name = "A"
|
||||
},
|
||||
)
|
||||
val newSettings = listOf(ChannelSettings(psk = pskA, name = "A"), ChannelSettings(psk = pskA, name = "A"))
|
||||
|
||||
packetDao.migrateChannelsByPSK(oldSettings, newSettings)
|
||||
|
||||
|
|
@ -221,7 +153,7 @@ class MigrationTest {
|
|||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = 42424242,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "$channel!broadcast",
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = false,
|
||||
|
|
@ -230,7 +162,7 @@ class MigrationTest {
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getAllPackets() = packetDao.getAllPackets(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE).first()
|
||||
private suspend fun getAllPackets() = packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first()
|
||||
|
||||
private suspend fun getFirstPacket() = getAllPackets().first()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue