fix: remove SFPP vestige, resource-back badge strings, add bridge tests

- 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>
This commit is contained in:
James Rich
2026-05-05 17:22:54 -05:00
parent c712d7ef68
commit 31f792c71e
6 changed files with 74 additions and 42 deletions

View File

@@ -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" }

View File

@@ -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())
}
}
}

View File

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

View File

@@ -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)
}

View File

@@ -1144,6 +1144,8 @@
<string name="store_forward">Store &amp; Forward</string>
<string name="store_forward_config">Store &amp; Forward Config</string>
<string name="store_forward_enabled">Store &amp; Forward enabled</string>
<string name="store_forward_server">Store &amp; Forward server</string>
<string name="congestion_level">Channel: %1$s</string>
<string name="subnet">Subred</string>
<string name="super_deep_sleep_duration_seconds">Super deep sleep duration</string>
<string name="supported">Supported</string>

View File

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