feat(node): add local stats noise floor metrics#5782
Conversation
✅ Docs staleness check passedThis PR includes updates to |
✅ Preview staleness check passedPreview and screenshot references are up to date. |
4a4559a to
e4c6fbe
Compare
|
Addressed the advisory comments by updating |
jamesarich
left a comment
There was a problem hiding this comment.
Requesting changes. First, credit where due: I checked out this branch and built it locally (JDK 21) — spotlessCheck, detekt, :feature:node:allTests, and :core:data:allTests all pass, imports are clean, and the stringResource format-arg counts all line up. The feature is a nice addition. The requests below are design/cleanup, not breakage.
Should-fix (blocking):
- RSSI/SNR CSV export is silently dropped and
saveSignalMetricsCSVis now orphaned. saveLocalStatsCSVexports all history, ignoring the selected time frame, despite the description saying 'visible'.scripts/sort-strings.pywasn't run — new strings are mis-sorted andstrings-index.txtis stale.
Cleanup (non-blocking): dead onRequestTelemetry arg, card/chart noise-floor threshold mismatch, and the Clear deletion window + row-by-row delete.
Tests: no test covers saveLocalStatsCSV, clearLocalStats, or deleteLocalStatsLogs; the FakeMeshLogRepository override only records the node id, so the 'preserve other telemetry types' filter is unverified.
Inline comments below.
| val hasRssi = remember(signalData) { signalData.any { it.rx_rssi != 0 } } | ||
| val hasSnr = remember(signalData) { signalData.any { !it.rx_snr.isNaN() } } | ||
| val hasAnyLocalStats = state.localStats.isNotEmpty() | ||
| val exportLauncher = rememberSaveFileLauncher { uri -> viewModel.saveLocalStatsCSV(uri) } |
There was a problem hiding this comment.
[Requested change] Dropped RSSI/SNR CSV export + orphaned method. This launcher now calls saveLocalStatsCSV, and the Save button below is gated on if (hasLocalStats). So for a node that only has signal-packet data (RSSI/SNR) and no local stats — historically the common case for this screen — there's no longer any way to export. MetricsViewModel.saveSignalMetricsCSV is left with no production caller (only its own test), so detekt won't flag it.
Please make this intentional: either keep the signal-metrics export alongside the local-stats one, or delete saveSignalMetricsCSV + its test if dropping it is the goal.
| "\"date\",\"time\",\"noise_floor_dbm\",\"uptime_seconds\",\"channel_utilization\",\"air_util_tx\"," + | ||
| "\"packets_tx\",\"packets_rx\",\"bad_rx\",\"rx_dupe\",\"tx_relay\",\"tx_relay_canceled\"," + | ||
| "\"online_nodes\",\"total_nodes\"\n", | ||
| rows = state.value.localStats.filter { it.local_stats != null }, |
There was a problem hiding this comment.
[Requested change] Export ignores the selected time frame. This reads state.value.localStats (all history), but the PR description says Save exports 'the visible Local Stats history', and every sibling export (saveDeviceMetricsCSV, the old signal export) passes the time-filtered list. Please filter by timeFrame.timeThreshold() — or pass the already-filtered localStatsData down from the screen — so the CSV matches what's on screen.
| <string name="shutdown_warning">⚠️ This will SHUTDOWN the node. Physical interaction will be required to turn it back on.</string> | ||
| <string name="signal">Signal</string> | ||
| <string name="signal_quality">Signal Quality</string> | ||
| <string name="noise_floor">Noise Floor</string> |
There was a problem hiding this comment.
[Requested change] Run python3 scripts/sort-strings.py. These strings were inserted next to signal_quality instead of in prefix/alpha order — busy_noise_floor and the noise_floor* keys belong in the b/n groups — and strings-index.txt wasn't regenerated, so the index is now stale. spotlessCheck doesn't cover ordering and there's no CI drift guard, so this won't fail the build, but AGENTS.md requires running the sorter after adding strings. The script reorders and regenerates the index in one shot.
| BaseMetricScreen( | ||
| onNavigateUp = onNavigateUp, | ||
| telemetryType = TelemetryType.LOCAL_STATS, | ||
| telemetryType = null, |
There was a problem hiding this comment.
[Cleanup] Dead onRequestTelemetry. With telemetryType = null, BaseMetricScreen never invokes onRequestTelemetry (the app-bar refresh button is gated on telemetryType != null), so the onRequestTelemetry = { ... } arg a few lines down is dead — the bottom 'Request' button is the real path. Drop the arg to avoid the misleading wiring.
| private fun noiseFloorTextColor(value: Int): Color = when { | ||
| value == 0 -> MaterialTheme.colorScheme.onSurfaceVariant | ||
| value < QUIET_NOISE_FLOOR_DBM -> SignalMetric.SNR.color | ||
| value < MODERATE_NOISE_FLOOR_DBM -> Orange |
There was a problem hiding this comment.
[Cleanup] Card vs. chart threshold mismatch. The card text turns red at >= MODERATE_NOISE_FLOOR_DBM (-90), but the chart's dashed 'busy floor' reference line is at BUSY_FLOOR_DBM (-85). So a -87 dBm reading sits below the busy line on the chart (reads as fine) yet shows red on the card. Consider aligning the card's 'busy' threshold with the -85 reference, or add a comment explaining the intentional difference.
| val logId = if (nodeNum == myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum | ||
| val dao = dbManager.currentDb.value.meshLogDao() | ||
| val localStatsLogs = | ||
| dao.getLogsFrom(logId, PortNum.TELEMETRY_APP.value, DEFAULT_MAX_LOGS) |
There was a problem hiding this comment.
[Cleanup] Clear can miss rows and deletes one-by-one. This loads only the most recent DEFAULT_MAX_LOGS telemetry rows (all types) before filtering to local-stats, so under high telemetry volume older local-stats logs can survive a 'Clear'. It also issues one deleteLog per row. Consider a scoped DELETE (by portNum + a local-stats predicate) or at least documenting the window limit.
| logsFlow.value = logsFlow.value.filterNot { it.fromNum == nodeNum && it.portNum == portNum } | ||
| } | ||
|
|
||
| override suspend fun deleteLocalStatsLogs(nodeNum: Int) { |
There was a problem hiding this comment.
[Tests] New behavior is unverified. This fake override only records the node id, so nothing exercises the real MeshLogRepositoryImpl.deleteLocalStatsLogs filter (the 'preserve other telemetry types' guarantee). There are also no tests for saveLocalStatsCSV or clearLocalStats. A MeshLogRepositoryImpl test that seeds mixed telemetry (local_stats + device/env) and asserts only local-stats rows are deleted would lock in the contract.
e4c6fbe to
ee7426f
Compare
|
Addressed the requested review items in ee7426f:
Validation run locally:
Note: ./gradlew test completed successfully but printed existing Robolectric temp-directory cleanup stack traces after androidApp unit tests; Gradle still reported BUILD SUCCESSFUL. |
deleteLocalStatsLogs fetches without a row cap and deletes via DELETE ... IN (:uuids). For nodes with many local-stats logs the UUID list can exceed SQLite's bind-variable limit and crash "Clear". Chunk the deletes by DELETE_CHUNK_SIZE, re-fetching the DAO per chunk, mirroring PacketRepositoryImpl.deleteMessages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ee7426f to
62d6f27
Compare
Summary
Screenshot
Testing
./gradlew spotlessApply./gradlew :screenshot-tests:updateDebugScreenshotTest./gradlew detekt :screenshot-tests:validateDebugScreenshotTest./gradlew test