fix(ui): give rx_snr real presence semantics end to end (#6523)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
James RichandClaude Opus 5 authored and GitHub committed 2026-07-30 16:40:41 +00:00
1 parent 4846425ff1
commit 2d20cd8a47
46 files changed
+2194 -148

No files matched your search

@@ -290,20 +290,22 @@ private fun UserAndUptimeRow(node: Node) {
@Composable
private fun SignalRow(node: Node) {
Row(modifier = Modifier.fillMaxWidth()) {
if (node.snr != Float.MAX_VALUE) {
val snr = node.snrOrNull
if (snr != null) {
InfoItem(
label = stringResource(Res.string.snr),
value = MetricFormatter.snr(node.snr),
value = MetricFormatter.snr(snr),
icon = MeshtasticIcons.Snr,
modifier = Modifier.weight(1f),
)
} else {
Spacer(Modifier.weight(1f))
}
if (node.rssi != Int.MAX_VALUE) {
val rssi = node.rssiOrNull
if (rssi != null) {
InfoItem(
label = stringResource(Res.string.rssi),
value = MetricFormatter.rssi(node.rssi),
value = MetricFormatter.rssi(rssi),
icon = MeshtasticIcons.Rssi,
modifier = Modifier.weight(1f),
)
@@ -50,6 +50,7 @@ import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.util.GeoConstants
import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.model.util.rxTimeOrNull
import org.meshtastic.core.model.util.snrOrNull
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
@@ -455,8 +456,9 @@ open class MetricsViewModel(
rows = data,
epochSeconds = { (it.rxTimeOrNull() ?: 0).toLong() },
) { p ->
// An absent rssi exports as an empty field, matching the other optional metrics above.
"\"${p.rx_rssi ?: ""}\",\"${p.rx_snr}\""
// An absent rssi or snr exports as an empty field, matching the other optional metrics above. An empty
// field and "0" must stay distinguishable: 0 dB is a real reading.
"\"${p.rx_rssi ?: ""}\",\"${p.snrOrNull() ?: ""}\""
}
}
@@ -59,6 +59,7 @@ import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.model.util.rxTimeOrNull
import org.meshtastic.core.model.util.snrOrNull
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.busy_noise_floor
import org.meshtastic.core.resources.clear
@@ -164,7 +165,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit, m
val data = remember(signalData, localStatsData) { buildSignalLog(signalData, localStatsData) }
val hasNoiseFloor = remember(localStatsData) { localStatsData.any { it.local_stats?.noise_floor != 0 } }
val hasRssi = remember(signalData) { signalData.any { it.rx_rssi != null } }
val hasSnr = remember(signalData) { signalData.any { !it.rx_snr.isNaN() } }
val hasSnr = remember(signalData) { signalData.any { it.snrOrNull() != null } }
val hasAnyLocalStats = state.localStats.isNotEmpty()
val localStatsExportLauncher = rememberSaveFileLauncher { uri -> viewModel.saveLocalStatsCSV(uri, localStatsData) }
val signalExportLauncher = rememberSaveFileLauncher { uri -> viewModel.saveSignalMetricsCSV(uri, signalData) }
@@ -319,7 +320,7 @@ private fun SignalMetricsChart(
if (noiseFloorData.size > 1) listOf(noiseFloorData.first(), noiseFloorData.last()) else emptyList()
}
val rssiData = remember(meshPackets) { meshPackets.filter { it.rx_rssi != null } }
val snrData = remember(meshPackets) { meshPackets.filter { !it.rx_snr.isNaN() } }
val snrData = remember(meshPackets) { meshPackets.filter { it.snrOrNull() != null } }
val legendData =
remember(noiseFloorData, rssiData, snrData) {
LEGEND_DATA.filter { legend ->
@@ -366,7 +367,9 @@ private fun SignalMetricsChart(
}
if (snrData.isNotEmpty()) {
/* Use a separate lineModel call to associate SNR with the right axis. */
lineModel { series(x = snrData.map { it.rxTimeOrNull() ?: 0 }, y = snrData.map { it.rx_snr }) }
lineModel {
series(x = snrData.map { it.rxTimeOrNull() ?: 0 }, y = snrData.mapNotNull { it.snrOrNull() })
}
}
}
}
@@ -583,14 +586,17 @@ private fun SignalMetricsCard(meshPacket: MeshPacket, isSelected: Boolean, onCli
Row(verticalAlignment = Alignment.CenterVertically) {
MetricValueRow(color = SignalMetric.RSSI.color, text = MetricFormatter.rssi(meshPacket.rx_rssi))
Spacer(Modifier.width(12.dp))
MetricValueRow(color = SignalMetric.SNR.color, text = MetricFormatter.snr(meshPacket.rx_snr))
MetricValueRow(
color = SignalMetric.SNR.color,
text = MetricFormatter.snr(meshPacket.snrOrNull()),
)
}
}
}
/* Signal Indicator */
Box(modifier = Modifier.weight(weight = 3f).height(IntrinsicSize.Max)) {
LoraSignalIndicator(snr = meshPacket.rx_snr)
LoraSignalIndicator(snr = meshPacket.snrOrNull())
}
}
}