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) 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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue