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,
)