From 31f792c71e88c2e25dcf5295716c09b612db96a2 Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 5 May 2026 17:22:54 -0500 Subject: [PATCH] fix: remove SFPP vestige, resource-back badge strings, add bridge tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove handleStoreForwardPlusPlus() from interface + impl (dead code post-SDK cutover) - Move StoreForwardBadge/CongestionBadge hardcoded strings to string resources (i18n) - Add SdkStateBridge tests: congestion warning → ServiceRepository, S&F server propagation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../manager/StoreForwardPacketHandlerImpl.kt | 6 +- .../StoreForwardPacketHandlerImplTest.kt | 24 ------- .../core/data/radio/SdkStateBridgeTest.kt | 63 ++++++++++++++++++- .../repository/StoreForwardPacketHandler.kt | 9 +-- .../composeResources/values/strings.xml | 2 + .../feature/node/component/NodeStatusIcons.kt | 12 ++-- 6 files changed, 74 insertions(+), 42 deletions(-) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt index 7fef493f8..503a045ad 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt @@ -34,7 +34,7 @@ import kotlin.time.Duration.Companion.milliseconds /** * Implementation of [StoreForwardPacketHandler] that keeps legacy S&F parsing for backward compatibility. * - * SF++ parsing/status updates are now delegated to the SDK and consumed via [org.meshtastic.core.data.radio.SdkStateBridge]. + * SF++ parsing/status updates are delegated to the SDK and consumed via [org.meshtastic.core.data.radio.SdkStateBridge]. */ @Single class StoreForwardPacketHandlerImpl( @@ -50,10 +50,6 @@ class StoreForwardPacketHandlerImpl( handleReceivedStoreAndForward(dataPacket, u, myNodeNum) } - override fun handleStoreForwardPlusPlus(packet: MeshPacket) { - Logger.d { "SFPP packet received from=${packet.from} (handled by SDK)" } - } - private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForward, myNodeNum: Int) { val lastRequest = s.history?.last_request ?: 0 Logger.d { "StoreAndForward from=${dataPacket.from} lastRequest=$lastRequest" } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt index 5bbce6fc9..215ac6c3f 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt @@ -185,28 +185,4 @@ class StoreForwardPacketHandlerImplTest { advanceUntilIdle() // No crash — falls through to else branch } - - // ---------- SF++: delegated to SDK ---------- - - @Test - fun `handleStoreForwardPlusPlus logs only and leaves repository untouched`() = testScope.runTest { - val packet = - MeshPacket( - from = 999, - decoded = Data( - portnum = PortNum.STORE_FORWARD_APP, - payload = "ignored".encodeToByteArray().toByteString(), - ), - ) - - handler.handleStoreForwardPlusPlus(packet) - advanceUntilIdle() - - verifySuspend(mode = VerifyMode.exactly(0)) { - packetRepository.updateSFPPStatus(any(), any(), any(), any(), any(), any(), any()) - } - verifySuspend(mode = VerifyMode.exactly(0)) { - packetRepository.updateSFPPStatusByHash(any(), any(), any()) - } - } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/radio/SdkStateBridgeTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/radio/SdkStateBridgeTest.kt index edea8eb0d..2b9dc9871 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/radio/SdkStateBridgeTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/radio/SdkStateBridgeTest.kt @@ -51,6 +51,7 @@ import org.meshtastic.sdk.TransportIdentity import org.meshtastic.sdk.testing.FakeRadioTransport import org.meshtastic.sdk.testing.InMemoryStorage import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.time.Clock @@ -196,6 +197,65 @@ class SdkStateBridgeTest { client.disconnect() } + @Test + fun `congestion warning updates service repository congestion level`() = runTest { + val serviceRepo = FakeServiceRepository() + val (transport, client) = connectedClient(SeededHeartbeatStorageProvider(emptyMap())) + buildBridge(client, FakeNodeRepository(), serviceRepository = serviceRepo) + + client.connect() + runCurrent() + + // Inject a telemetry packet with high air utilization to trigger CongestionWarning + transport.injectPacket( + MeshPacket( + from = 0x11111111, // "own node" — triggers congestion from local metrics + to = 0, + decoded = Data( + portnum = PortNum.TELEMETRY_APP, + payload = org.meshtastic.proto.Telemetry( + device_metrics = org.meshtastic.proto.DeviceMetrics( + air_util_tx = 80f, + channel_utilization = 85f, + ), + ).let { org.meshtastic.proto.Telemetry.ADAPTER.encode(it).toByteString() }, + ), + ), + ) + runCurrent() + + assertEquals(org.meshtastic.sdk.CongestionLevel.CRITICAL, serviceRepo.congestionLevel.value) + + client.disconnect() + } + + @Test + fun `store forward server list propagates to service repository`() = runTest { + val serviceRepo = FakeServiceRepository() + val (transport, client) = connectedClient(SeededHeartbeatStorageProvider(emptyMap())) + buildBridge(client, FakeNodeRepository(), serviceRepository = serviceRepo) + + client.connect() + runCurrent() + + // Inject a StoreAndForward heartbeat from a server node to trigger server discovery + transport.injectStoreForwardResponse( + requestId = 0, + message = org.meshtastic.proto.StoreAndForward( + rr = org.meshtastic.proto.StoreAndForward.RequestResponse.ROUTER_HEARTBEAT, + heartbeat = org.meshtastic.proto.StoreAndForward.Heartbeat(period = 900, secondary = 0), + ), + fromNode = 0xABCD1234.toInt(), + ) + runCurrent() + advanceTimeBy(1.seconds) + runCurrent() + + assertTrue(serviceRepo.storeForwardServers.value.contains(0xABCD1234.toInt())) + + client.disconnect() + } + private fun TestScope.connectedClient( storage: StorageProvider, myNodeNum: Int = 0x11111111, @@ -217,10 +277,11 @@ class SdkStateBridgeTest { client: RadioClient, nodeRepository: FakeNodeRepository, packetRepository: PacketRepository = mock(MockMode.autofill), + serviceRepository: FakeServiceRepository = FakeServiceRepository(), ): SdkStateBridge = SdkStateBridge( accessor = TestRadioClientAccessor(client), - serviceRepository = FakeServiceRepository(), + serviceRepository = serviceRepository, nodeRepository = nodeRepository, packetRepository = lazyOf(packetRepository), locationManager = NoOpLocationManager, diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt index e884a8d3c..b7fefddae 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt @@ -19,7 +19,7 @@ package org.meshtastic.core.repository import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.MeshPacket -/** Interface for handling Store & Forward (legacy) and SF++ packets. */ +/** Interface for handling Store & Forward (legacy) packets. */ interface StoreForwardPacketHandler { /** * Handles a legacy Store & Forward packet. @@ -29,11 +29,4 @@ interface StoreForwardPacketHandler { * @param myNodeNum The local node number. */ fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) - - /** - * Handles a Store Forward++ packet. - * - * @param packet The received mesh packet. - */ - fun handleStoreForwardPlusPlus(packet: MeshPacket) } diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 0f3cf4e28..a48e9c57a 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -1144,6 +1144,8 @@ Store & Forward Store & Forward Config Store & Forward enabled + Store & Forward server + Channel: %1$s Subred Super deep sleep duration Supported diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt index 40ca70da8..a5b6faf5a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt @@ -39,12 +39,14 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.congestion_level import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected import org.meshtastic.core.resources.favorite import org.meshtastic.core.resources.mute_always +import org.meshtastic.core.resources.store_forward_server import org.meshtastic.core.resources.unmessageable import org.meshtastic.core.resources.unmonitored_or_infrastructure import org.meshtastic.core.ui.component.ConnectionsNavIcon @@ -142,14 +144,15 @@ private fun ThisNodeStatusBadge(connectionState: ConnectionState, deviceType: De @OptIn(ExperimentalMaterial3Api::class) @Composable private fun StoreForwardBadge() { + val text = stringResource(Res.string.store_forward_server) TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), - tooltip = { PlainTooltip { Text("Store & Forward server") } }, + tooltip = { PlainTooltip { Text(text) } }, state = rememberTooltipState(), ) { Icon( imageVector = MeshtasticIcons.CloudDownload, - contentDescription = "Store & Forward server", + contentDescription = text, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.StatusBlue, ) @@ -166,14 +169,15 @@ private fun CongestionBadge(level: CongestionLevel) { CongestionLevel.CRITICAL -> MaterialTheme.colorScheme.StatusRed else -> return } + val tooltipText = stringResource(Res.string.congestion_level, level.name) TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), - tooltip = { PlainTooltip { Text("Channel: ${level.name}") } }, + tooltip = { PlainTooltip { Text(tooltipText) } }, state = rememberTooltipState(), ) { Icon( imageVector = MeshtasticIcons.Warning, - contentDescription = "Congestion: ${level.name}", + contentDescription = tooltipText, modifier = Modifier.size(24.dp), tint = color, )