feat(wire): migrate from protobuf -> wire (#4401)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-03 18:01:12 -06:00 committed by GitHub
parent 9dbc8b7fbf
commit 25657e8f8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
239 changed files with 7149 additions and 6144 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2026 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
@ -16,66 +16,39 @@
*/
package com.geeksville.mesh.repository.radio
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertArrayEquals
import com.geeksville.mesh.service.Fakes
import io.mockk.every
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Assert.assertEquals
import org.junit.Test
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
class TCPInterfaceTest {
private val service: RadioInterfaceService = mockk(relaxed = true)
private val dispatchers: CoroutineDispatchers = mockk(relaxed = true)
@Test
fun `keepAlive generates correct heartbeat bytes`() = runTest {
val address = "192.168.1.1:4403"
// We need a subclass to capture handleSendToRadio or sendBytes
val tcpInterface =
object : TCPInterface(service, dispatchers, address) {
var capturedBytes: ByteArray? = null
fun testKeepAlive() {
val fakes = Fakes()
val testDispatcher = UnconfinedTestDispatcher()
val testScope = CoroutineScope(testDispatcher + Job())
every { fakes.service.serviceScope } returns testScope
val dispatchers = CoroutineDispatchers(io = testDispatcher, main = testDispatcher, default = testDispatcher)
val tcpIf =
object : TCPInterface(fakes.service, dispatchers, "127.0.0.1") {
var lastSent: ByteArray? = null
override fun handleSendToRadio(p: ByteArray) {
capturedBytes = p
lastSent = p
}
// Override connect to prevent it from starting automatically in init
override fun connect() {}
}
tcpInterface.keepAlive()
tcpIf.keepAlive()
val expectedHeartbeat =
MeshProtos.ToRadio.newBuilder()
.setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance())
.build()
.toByteArray()
assertArrayEquals("Heartbeat bytes should match", expectedHeartbeat, tcpInterface.capturedBytes)
}
@Test
fun `sendBytes does not crash when outStream is null`() = runTest {
val address = "192.168.1.1:4403"
val tcpInterface =
object : TCPInterface(service, dispatchers, address) {
override fun connect() {}
}
// This should not throw UninitializedPropertyAccessException
tcpInterface.sendBytes(byteArrayOf(1, 2, 3))
}
@Test
fun `flushBytes does not crash when outStream is null`() = runTest {
val address = "192.168.1.1:4403"
val tcpInterface =
object : TCPInterface(service, dispatchers, address) {
override fun connect() {}
}
// This should not throw UninitializedPropertyAccessException
tcpInterface.flushBytes()
val expected = ToRadio(heartbeat = Heartbeat()).encode()
assertEquals(expected.toList(), tcpIf.lastSent?.toList())
}
}

View file

@ -17,60 +17,15 @@
package com.geeksville.mesh.service
import android.app.Notification
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import io.mockk.mockk
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.entity.NodeWithRelations
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.TelemetryProtos
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Telemetry
class FakeNodeInfoReadDataSource : NodeInfoReadDataSource {
val myNodeInfo = MutableStateFlow<MyNodeEntity?>(null)
val nodes = MutableStateFlow<Map<Int, NodeWithRelations>>(emptyMap())
override fun myNodeInfoFlow(): Flow<MyNodeEntity?> = myNodeInfo
override fun nodeDBbyNumFlow(): Flow<Map<Int, NodeWithRelations>> = nodes
override fun getNodesFlow(
sort: String,
filter: String,
includeUnknown: Boolean,
hopsAwayMax: Int,
lastHeardMin: Int,
): Flow<List<NodeWithRelations>> = flowOf(emptyList())
override suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity> = emptyList()
override suspend fun getUnknownNodes(): List<NodeEntity> = emptyList()
}
class FakeNodeInfoWriteDataSource : NodeInfoWriteDataSource {
override suspend fun upsert(node: NodeEntity) {}
override suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) {}
override suspend fun clearMyNodeInfo() {}
override suspend fun clearNodeDB(preserveFavorites: Boolean) {}
override suspend fun deleteNode(num: Int) {}
override suspend fun deleteNodes(nodeNums: List<Int>) {}
override suspend fun deleteMetadata(num: Int) {}
override suspend fun upsert(metadata: MetadataEntity) {}
override suspend fun setNodeNotes(num: Int, notes: String) {}
override suspend fun backfillDenormalizedNames() {}
class Fakes {
val service: RadioInterfaceService = mockk(relaxed = true)
}
class FakeMeshServiceNotifications : MeshServiceNotifications {
@ -78,10 +33,8 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
override fun initChannels() {}
override fun updateServiceStateNotification(
summaryString: String?,
telemetry: TelemetryProtos.Telemetry?,
): Notification = null as Notification
override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification =
mockk(relaxed = true)
override suspend fun updateMessageNotification(
contactKey: String,
@ -115,11 +68,11 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {}
override fun showClientNotification(clientNotification: MeshProtos.ClientNotification) {}
override fun showClientNotification(clientNotification: ClientNotification) {}
override fun cancelMessageNotification(contactKey: String) {}
override fun cancelLowBatteryNotification(node: NodeEntity) {}
override fun clearClientNotification(notification: MeshProtos.ClientNotification) {}
override fun clearClientNotification(notification: ClientNotification) {}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2026 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
@ -16,71 +16,79 @@
*/
package com.geeksville.mesh.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.fromRadio
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.NodeInfo
import org.meshtastic.proto.QueueStatus
class FromRadioPacketHandlerTest {
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val router: MeshRouter = mockk(relaxed = true)
private val mqttManager: MeshMqttManager = mockk(relaxed = true)
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true)
private val configHandler: MeshConfigHandler = mockk(relaxed = true)
private lateinit var handler: FromRadioPacketHandler
@Before
fun setUp() {
every { router.configFlowManager } returns configFlowManager
every { router.configHandler } returns configHandler
fun setup() {
handler = FromRadioPacketHandler(serviceRepository, router, mqttManager, packetHandler, serviceNotifications)
}
@Test
fun `handleFromRadio routes MY_INFO to configFlowManager`() {
val myInfo = MeshProtos.MyNodeInfo.newBuilder().setMyNodeNum(1234).build()
val proto = fromRadio { this.myInfo = myInfo }
val myInfo = MyNodeInfo(my_node_num = 1234)
val proto = FromRadio(my_info = myInfo)
handler.handleFromRadio(proto)
verify { configFlowManager.handleMyInfo(myInfo) }
verify { router.configFlowManager.handleMyInfo(myInfo) }
}
@Test
fun `handleFromRadio routes METADATA to configFlowManager`() {
val metadata = MeshProtos.DeviceMetadata.newBuilder().setFirmwareVersion("v1.0").build()
val proto = fromRadio { this.metadata = metadata }
val metadata = DeviceMetadata(firmware_version = "v1.0")
val proto = FromRadio(metadata = metadata)
handler.handleFromRadio(proto)
verify { configFlowManager.handleLocalMetadata(metadata) }
verify { router.configFlowManager.handleLocalMetadata(metadata) }
}
@Test
fun `handleFromRadio routes NODE_INFO to configFlowManager`() {
val nodeInfo = MeshProtos.NodeInfo.newBuilder().setNum(1234).build()
val proto = fromRadio { this.nodeInfo = nodeInfo }
fun `handleFromRadio routes NODE_INFO to configFlowManager and updates status`() {
val nodeInfo = NodeInfo(num = 1234)
val proto = FromRadio(node_info = nodeInfo)
handler.handleFromRadio(proto)
verify { configFlowManager.handleNodeInfo(nodeInfo) }
verify { router.configFlowManager.handleNodeInfo(nodeInfo) }
verify { serviceRepository.setStatusMessage(any()) }
}
@Test
fun `handleFromRadio routes CONFIG_COMPLETE_ID to configFlowManager`() {
val nonce = 69420
val proto = FromRadio(config_complete_id = nonce)
handler.handleFromRadio(proto)
verify { router.configFlowManager.handleConfigComplete(nonce) }
}
@Test
fun `handleFromRadio routes QUEUESTATUS to packetHandler`() {
val queueStatus = MeshProtos.QueueStatus.newBuilder().setFree(5).build()
val proto = fromRadio { this.queueStatus = queueStatus }
val queueStatus = QueueStatus(free = 10)
val proto = FromRadio(queueStatus = queueStatus)
handler.handleFromRadio(proto)
@ -89,23 +97,23 @@ class FromRadioPacketHandlerTest {
@Test
fun `handleFromRadio routes CONFIG to configHandler`() {
val config = ConfigProtos.Config.newBuilder().build()
val proto = fromRadio { this.config = config }
val config = Config(lora = Config.LoRaConfig(use_preset = true))
val proto = FromRadio(config = config)
handler.handleFromRadio(proto)
verify { configHandler.handleDeviceConfig(config) }
verify { router.configHandler.handleDeviceConfig(config) }
}
@Test
fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository and notifications`() {
val notification = MeshProtos.ClientNotification.newBuilder().setReplyId(42).build()
val proto = fromRadio { this.clientNotification = notification }
val notification = ClientNotification(message = "test")
val proto = FromRadio(clientNotification = notification)
handler.handleFromRadio(proto)
verify { serviceRepository.setClientNotification(notification) }
verify { serviceNotifications.showClientNotification(notification) }
verify { packetHandler.removeResponse(42, false) }
verify { packetHandler.removeResponse(0, complete = false) }
}
}

View file

@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@ -31,9 +32,9 @@ import org.junit.Test
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.MeshProtos.MeshPacket
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.MeshPacket
class MeshCommandSenderHopLimitTest {
@ -42,7 +43,7 @@ class MeshCommandSenderHopLimitTest {
private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true)
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
private val localConfigFlow = MutableStateFlow(LocalConfig.getDefaultInstance())
private val localConfigFlow = MutableStateFlow(LocalConfig())
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = CoroutineScope(testDispatcher)
@ -64,42 +65,41 @@ class MeshCommandSenderHopLimitTest {
val packet =
DataPacket(
to = DataPacket.ID_BROADCAST,
bytes = byteArrayOf(1, 2, 3),
bytes = byteArrayOf(1, 2, 3).toByteString(),
dataType = 1, // PortNum.TEXT_MESSAGE_APP
)
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
// Ensure localConfig has lora.hopLimit = 0
localConfigFlow.value =
LocalConfig.newBuilder().setLora(Config.LoRaConfig.newBuilder().setHopLimit(0)).build()
// Ensure localConfig has lora.hop_limit = 0
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 0))
commandSender.sendData(packet)
verify(exactly = 1) { packetHandler.sendToRadio(any<MeshPacket>()) }
val capturedHopLimit = meshPacketSlot.captured.hopLimit
val capturedHopLimit = meshPacketSlot.captured.hop_limit ?: 0
assertTrue("Hop limit should be greater than 0, but was $capturedHopLimit", capturedHopLimit > 0)
assertEquals(3, capturedHopLimit)
assertEquals(3, meshPacketSlot.captured.hopStart)
assertEquals(3, meshPacketSlot.captured.hop_start)
}
@Test
fun `sendData respects non-zero hop limit from config`() = runTest(testDispatcher) {
val packet = DataPacket(to = DataPacket.ID_BROADCAST, bytes = byteArrayOf(1, 2, 3), dataType = 1)
val packet =
DataPacket(to = DataPacket.ID_BROADCAST, bytes = byteArrayOf(1, 2, 3).toByteString(), dataType = 1)
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
localConfigFlow.value =
LocalConfig.newBuilder().setLora(Config.LoRaConfig.newBuilder().setHopLimit(7)).build()
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 7))
commandSender.sendData(packet)
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
assertEquals(7, meshPacketSlot.captured.hopLimit)
assertEquals(7, meshPacketSlot.captured.hopStart)
assertEquals(7, meshPacketSlot.captured.hop_limit)
assertEquals(7, meshPacketSlot.captured.hop_start)
}
@Test
@ -108,8 +108,7 @@ class MeshCommandSenderHopLimitTest {
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
localConfigFlow.value =
LocalConfig.newBuilder().setLora(Config.LoRaConfig.newBuilder().setHopLimit(6)).build()
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6))
// Mock node manager interactions
nodeManager.nodeDBbyNodeNum.remove(destNum)
@ -117,7 +116,7 @@ class MeshCommandSenderHopLimitTest {
commandSender.requestUserInfo(destNum)
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
assertEquals("Hop Limit should be 6", 6, meshPacketSlot.captured.hopLimit)
assertEquals("Hop Start should be 6", 6, meshPacketSlot.captured.hopStart)
assertEquals("Hop Limit should be 6", 6, meshPacketSlot.captured.hop_limit)
assertEquals("Hop Start should be 6", 6, meshPacketSlot.captured.hop_start)
}
}

View file

@ -22,7 +22,7 @@ import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.user
import org.meshtastic.proto.User
class MeshCommandSenderTest {
@ -60,7 +60,7 @@ class MeshCommandSenderTest {
fun `resolveNodeNum handles custom node ID from database`() {
val nodeNum = 456
val userId = "custom_id"
val entity = NodeEntity(num = nodeNum, user = user { id = userId })
val entity = NodeEntity(num = nodeNum, user = User(id = userId))
nodeManager.nodeDBbyNodeNum[nodeNum] = entity
nodeManager.nodeDBbyID[userId] = entity

View file

@ -39,9 +39,9 @@ import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.MeshProtos.ToRadio
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.ToRadio
class MeshConnectionManagerTest {
@ -60,7 +60,7 @@ class MeshConnectionManagerTest {
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
private val analytics: PlatformAnalytics = mockk(relaxed = true)
private val radioConnectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
private val localConfigFlow = MutableStateFlow(LocalConfig.getDefaultInstance())
private val localConfigFlow = MutableStateFlow(LocalConfig())
private val testDispatcher = UnconfinedTestDispatcher()
@ -112,7 +112,7 @@ class MeshConnectionManagerTest {
connectionStateHolder.connectionState.value,
)
verify { serviceBroadcasts.broadcastConnection() }
verify { packetHandler.sendToRadio(any<ToRadio.Builder>()) }
verify { packetHandler.sendToRadio(any<ToRadio>()) }
}
@Test
@ -139,12 +139,10 @@ class MeshConnectionManagerTest {
fun `DeviceSleep behavior when power saving is off maps to Disconnected`() = runTest(testDispatcher) {
// Power saving disabled + Role CLIENT
val config =
LocalConfig.newBuilder()
.apply {
powerBuilder.setIsPowerSaving(false)
deviceBuilder.setRole(Config.DeviceConfig.Role.CLIENT)
}
.build()
LocalConfig(
power = Config.PowerConfig(is_power_saving = false),
device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT),
)
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
manager.start(backgroundScope)
@ -163,7 +161,7 @@ class MeshConnectionManagerTest {
@Test
fun `DeviceSleep behavior when power saving is on stays in DeviceSleep`() = runTest(testDispatcher) {
// Power saving enabled
val config = LocalConfig.newBuilder().apply { powerBuilder.setIsPowerSaving(true) }.build()
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true))
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
manager.start(backgroundScope)

View file

@ -16,9 +16,9 @@
*/
package com.geeksville.mesh.service
import com.google.protobuf.ByteString
import io.mockk.every
import io.mockk.mockk
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@ -26,7 +26,9 @@ import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
class MeshDataMapperTest {
@ -64,7 +66,7 @@ class MeshDataMapperTest {
@Test
fun `toDataPacket returns null when no decoded data`() {
val packet = MeshProtos.MeshPacket.newBuilder().build()
val packet = MeshPacket()
assertNull(mapper.toDataPacket(packet))
}
@ -77,26 +79,22 @@ class MeshDataMapperTest {
every { nodeManager.nodeDBbyNodeNum[any()] } returns nodeEntity
val proto =
MeshProtos.MeshPacket.newBuilder()
.apply {
id = 42
from = nodeNum
to = DataPacket.NODENUM_BROADCAST
rxTime = 1600000000
rxSnr = 5.5f
rxRssi = -100
hopLimit = 3
hopStart = 3
decoded =
MeshProtos.Data.newBuilder()
.apply {
portnumValue = 1 // TEXT_MESSAGE_APP
payload = ByteString.copyFrom("hello".toByteArray())
replyId = 123
}
.build()
}
.build()
MeshPacket(
id = 42,
from = nodeNum,
to = DataPacket.NODENUM_BROADCAST,
rx_time = 1600000000,
rx_snr = 5.5f,
rx_rssi = -100,
hop_limit = 3,
hop_start = 3,
decoded =
Data(
portnum = PortNum.TEXT_MESSAGE_APP,
payload = "hello".encodeToByteArray().toByteString(),
reply_id = 123,
),
)
val result = mapper.toDataPacket(proto)
assertNotNull(result)
@ -106,21 +104,14 @@ class MeshDataMapperTest {
assertEquals(1600000000000L, result.time)
assertEquals(5.5f, result.snr)
assertEquals(-100, result.rssi)
assertEquals(1, result.dataType)
assertEquals("hello", result.bytes?.decodeToString())
assertEquals(PortNum.TEXT_MESSAGE_APP.value, result.dataType)
assertEquals("hello", result.bytes?.utf8())
assertEquals(123, result.replyId)
}
@Test
fun `toDataPacket maps PKC channel correctly for encrypted packets`() {
val proto =
MeshProtos.MeshPacket.newBuilder()
.apply {
pkiEncrypted = true
channel = 1
decoded = MeshProtos.Data.getDefaultInstance()
}
.build()
val proto = MeshPacket(pki_encrypted = true, channel = 1, decoded = Data())
every { nodeManager.nodeDBbyNodeNum[any()] } returns null

View file

@ -27,7 +27,9 @@ import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
class MeshMessageProcessorTest {
@ -56,13 +58,7 @@ class MeshMessageProcessorTest {
@Test
fun `early packets are buffered and flushed when DB is ready`() = runTest(testDispatcher) {
val packet =
MeshProtos.MeshPacket.newBuilder()
.apply {
id = 123
decoded = MeshProtos.Data.newBuilder().setPortnumValue(1).build()
}
.build()
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
// 1. Database is NOT ready
isNodeDbReady.value = false
@ -83,13 +79,7 @@ class MeshMessageProcessorTest {
@Test
fun `packets are processed immediately if DB is already ready`() = runTest(testDispatcher) {
val packet =
MeshProtos.MeshPacket.newBuilder()
.apply {
id = 456
decoded = MeshProtos.Data.newBuilder().setPortnumValue(1).build()
}
.build()
val packet = MeshPacket(id = 456, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
isNodeDbReady.value = true
testScheduler.runCurrent()

View file

@ -26,8 +26,9 @@ import org.junit.Test
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.user
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Position
import org.meshtastic.proto.User
class MeshNodeManagerTest {
@ -49,73 +50,51 @@ class MeshNodeManagerTest {
assertNotNull(result)
assertEquals(nodeNum, result.num)
assertTrue(result.user.longName.startsWith("Meshtastic"))
assertTrue(result.user.long_name?.startsWith("Meshtastic") == true)
assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id)
}
@Test
fun `handleReceivedUser preserves existing user if incoming is default`() {
val nodeNum = 1234
val existingUser = user {
id = "!12345678"
longName = "My Custom Name"
shortName = "MCN"
hwModel = MeshProtos.HardwareModel.TLORA_V2
}
val existingUser =
User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2)
// Setup existing node
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
val incomingDefaultUser = user {
id = "!12345678"
longName = "Meshtastic 5678"
shortName = "5678"
hwModel = MeshProtos.HardwareModel.UNSET
}
val incomingDefaultUser =
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
nodeManager.handleReceivedUser(nodeNum, incomingDefaultUser)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals("My Custom Name", result!!.user.longName)
assertEquals(MeshProtos.HardwareModel.TLORA_V2, result.user.hwModel)
assertEquals("My Custom Name", result!!.user.long_name)
assertEquals(HardwareModel.TLORA_V2, result.user.hw_model)
}
@Test
fun `handleReceivedUser updates user if incoming is higher detail`() {
val nodeNum = 1234
val existingUser = user {
id = "!12345678"
longName = "Meshtastic 5678"
shortName = "5678"
hwModel = MeshProtos.HardwareModel.UNSET
}
val existingUser =
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
val incomingDetailedUser = user {
id = "!12345678"
longName = "Real User"
shortName = "RU"
hwModel = MeshProtos.HardwareModel.TLORA_V1
}
val incomingDetailedUser =
User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1)
nodeManager.handleReceivedUser(nodeNum, incomingDetailedUser)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals("Real User", result!!.user.longName)
assertEquals(MeshProtos.HardwareModel.TLORA_V1, result.user.hwModel)
assertEquals("Real User", result!!.user.long_name)
assertEquals(HardwareModel.TLORA_V1, result.user.hw_model)
}
@Test
fun `handleReceivedPosition updates node position`() {
val nodeNum = 1234
val position =
MeshProtos.Position.newBuilder()
.apply {
latitudeI = 450000000
longitudeI = 900000000
}
.build()
val position = Position(latitude_i = 450000000, longitude_i = 900000000)
nodeManager.handleReceivedPosition(nodeNum, 9999, position)

View file

@ -29,7 +29,9 @@ import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.QueueStatus
import org.meshtastic.proto.ToRadio
class PacketHandlerTest {
@ -58,20 +60,17 @@ class PacketHandlerTest {
}
@Test
fun `sendToRadio with ToRadio Builder sends immediately`() {
val builder =
MeshProtos.ToRadio.newBuilder().apply { packet = MeshProtos.MeshPacket.newBuilder().setId(123).build() }
fun `sendToRadio with ToRadio sends immediately`() {
val toRadio = ToRadio(packet = MeshPacket(id = 123))
handler.sendToRadio(builder)
handler.sendToRadio(toRadio)
verify { radioInterfaceService.sendToRadio(any()) }
// Verify broadcast status ENROUTE (via status mapping) is not directly testable easily without more mocks,
// but we verify the call to radio service occurred.
}
@Test
fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) {
val packet = MeshProtos.MeshPacket.newBuilder().setId(456).build()
val packet = MeshPacket(id = 456)
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
handler.sendToRadio(packet)
@ -82,25 +81,20 @@ class PacketHandlerTest {
@Test
fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) {
val packet = MeshProtos.MeshPacket.newBuilder().setId(789).build()
val packet = MeshPacket(id = 789)
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
handler.sendToRadio(packet)
testScheduler.runCurrent()
val status =
MeshProtos.QueueStatus.newBuilder()
.apply {
meshPacketId = 789
res = 0 // Success
free = 1
}
.build()
QueueStatus(
mesh_packet_id = 789,
res = 0, // Success
free = 1,
)
handler.handleQueueStatus(status)
testScheduler.runCurrent()
// If it completed, the queue job should move to the next packet or finish.
// We can't easily check the deferred inside, but we can check if it cleared the internal wait.
}
}

View file

@ -18,7 +18,7 @@ package com.geeksville.mesh.service
import org.junit.Assert.assertEquals
import org.junit.Test
import org.meshtastic.proto.StoreAndForwardProtos
import org.meshtastic.proto.StoreAndForward
class StoreForwardHistoryRequestTest {
@ -31,14 +31,14 @@ class StoreForwardHistoryRequestTest {
historyReturnMax = 25,
)
assertEquals(StoreAndForwardProtos.StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(42, request.history.lastRequest)
assertEquals(15, request.history.window)
assertEquals(25, request.history.historyMessages)
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(42, request.history?.last_request)
assertEquals(15, request.history?.window)
assertEquals(25, request.history?.history_messages)
}
@Test
fun `buildStoreForwardHistoryRequest omits non-positive parameters`() {
fun `buildStoreForwardHistoryRequest clamps non-positive parameters`() {
val request =
MeshHistoryManager.buildStoreForwardHistoryRequest(
lastRequest = 0,
@ -46,10 +46,10 @@ class StoreForwardHistoryRequestTest {
historyReturnMax = 0,
)
assertEquals(StoreAndForwardProtos.StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(0, request.history.lastRequest)
assertEquals(0, request.history.window)
assertEquals(0, request.history.historyMessages)
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(0, request.history?.last_request)
assertEquals(0, request.history?.window)
assertEquals(0, request.history?.history_messages)
}
@Test

View file

@ -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 com.geeksville.mesh.ui.metrics
import org.junit.Assert.assertEquals
import org.junit.Test
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.proto.TelemetryProtos
import org.meshtastic.proto.copy
import org.meshtastic.proto.EnvironmentMetrics
import org.meshtastic.proto.Telemetry
class EnvironmentMetricsTest {
@ -33,15 +32,14 @@ class EnvironmentMetricsTest {
val expectedSoilTemperatureFahrenheit = celsiusToFahrenheit(initialSoilTemperatureCelsius)
val telemetry =
TelemetryProtos.Telemetry.newBuilder()
.setEnvironmentMetrics(
TelemetryProtos.EnvironmentMetrics.newBuilder()
.setTemperature(initialTemperatureCelsius)
.setSoilTemperature(initialSoilTemperatureCelsius)
.build(),
)
.setTime(1000)
.build()
Telemetry(
environment_metrics =
EnvironmentMetrics(
temperature = initialTemperatureCelsius,
soil_temperature = initialSoilTemperatureCelsius,
),
time = 1000,
)
val data = listOf(telemetry)
@ -50,15 +48,16 @@ class EnvironmentMetricsTest {
val processedTelemetries =
if (isFahrenheit) {
data.map { tel ->
val temperatureFahrenheit = celsiusToFahrenheit(tel.environmentMetrics.temperature)
val soilTemperatureFahrenheit = celsiusToFahrenheit(tel.environmentMetrics.soilTemperature)
tel.copy {
environmentMetrics =
tel.environmentMetrics.copy {
temperature = temperatureFahrenheit
soilTemperature = soilTemperatureFahrenheit
}
}
val metrics = tel.environment_metrics!!
val temperatureFahrenheit = celsiusToFahrenheit(metrics.temperature ?: 0f)
val soilTemperatureFahrenheit = celsiusToFahrenheit(metrics.soil_temperature ?: 0f)
tel.copy(
environment_metrics =
metrics.copy(
temperature = temperatureFahrenheit,
soil_temperature = soilTemperatureFahrenheit,
),
)
}
} else {
data
@ -66,7 +65,11 @@ class EnvironmentMetricsTest {
val resultTelemetry = processedTelemetries.first()
assertEquals(expectedTemperatureFahrenheit, resultTelemetry.environmentMetrics.temperature, 0.01f)
assertEquals(expectedSoilTemperatureFahrenheit, resultTelemetry.environmentMetrics.soilTemperature, 0.01f)
assertEquals(expectedTemperatureFahrenheit, resultTelemetry.environment_metrics?.temperature ?: 0f, 0.01f)
assertEquals(
expectedSoilTemperatureFahrenheit,
resultTelemetry.environment_metrics?.soil_temperature ?: 0f,
0.01f,
)
}
}