Skip to content

feat(node): add local stats noise floor metrics#5782

Merged
jamesarich merged 2 commits into
meshtastic:release/2.8.0from
RCGV1:codex/noise-floor-local-stats
Jun 16, 2026
Merged

feat(node): add local stats noise floor metrics#5782
jamesarich merged 2 commits into
meshtastic:release/2.8.0from
RCGV1:codex/noise-floor-local-stats

Conversation

@RCGV1

@RCGV1 RCGV1 commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

  • Add local stats telemetry to the Signal Quality metrics screen, including noise floor charting and a dashed busy-floor reference line.
  • Show local stats log cards with noise floor, traffic, relays, nodes, and uptime, matching the existing Android metrics screen patterns.
  • Add Clear, Request, and Save actions for local stats logs, including CSV export and focused repository deletion support.
  • Document Local Stats/noise floor behavior and add local-node preview/screenshot coverage for the telemetry actions section.

Screenshot

Noise floor local stats

Testing

  • ./gradlew spotlessApply
  • ./gradlew :screenshot-tests:updateDebugScreenshotTest
  • ./gradlew detekt :screenshot-tests:validateDebugScreenshotTest
  • ./gradlew test

@github-actions github-actions Bot added the enhancement New feature or request label Jun 12, 2026
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

✅ Docs staleness check passed

This PR includes updates to docs/en/ alongside the source changes. Thank you!

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

✅ Preview staleness check passed

Preview and screenshot references are up to date.

@RCGV1 RCGV1 force-pushed the codex/noise-floor-local-stats branch from 4a4559a to e4c6fbe Compare June 12, 2026 21:08
@RCGV1

RCGV1 commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

Addressed the advisory comments by updating docs/en/user/node-metrics.md, adding a local-node TelemetricActionsSection preview, adding screenshot-test coverage for it, and regenerating the affected node screenshot references. Validation now passes with ./gradlew spotlessApply, ./gradlew detekt :screenshot-tests:validateDebugScreenshotTest, and ./gradlew test.

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. RSSI/SNR CSV export is silently dropped and saveSignalMetricsCSV is now orphaned.
  2. saveLocalStatsCSV exports all history, ignoring the selected time frame, despite the description saying 'visible'.
  3. scripts/sort-strings.py wasn't run — new strings are mis-sorted and strings-index.txt is 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) }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@RCGV1 RCGV1 force-pushed the codex/noise-floor-local-stats branch from e4c6fbe to ee7426f Compare June 16, 2026 21:30
@RCGV1

RCGV1 commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Addressed the requested review items in ee7426f:

  • Restored RSSI/SNR CSV export and kept Local Stats export separate.
  • Local Stats CSV export now uses the visible timeframe-filtered rows passed by the screen.
  • Re-ran scripts/sort-strings.py and refreshed strings-index.txt.
  • Removed the dead telemetry request app-bar hook, aligned the noise-floor card threshold with the chart busy floor, and tightened clear to bulk-delete all matching local-stats telemetry logs.
  • Added coverage for Local Stats CSV export, clearLocalStats, and deleteLocalStatsLogs preserving other telemetry.

Validation run locally:

  • ./gradlew spotlessApply
  • ./gradlew detekt
  • ./gradlew :core:data:jvmTest
  • ./gradlew :feature:node:jvmTest --rerun-tasks
  • ./gradlew test

Note: ./gradlew test completed successfully but printed existing Robolectric temp-directory cleanup stack traces after androidApp unit tests; Gradle still reported BUILD SUCCESSFUL.

RCGV1 and others added 2 commits June 16, 2026 17:49
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>
@jamesarich jamesarich force-pushed the codex/noise-floor-local-stats branch from ee7426f to 62d6f27 Compare June 16, 2026 22:54
@github-actions github-actions Bot added build Build system changes repo Repository maintenance labels Jun 16, 2026
@jamesarich jamesarich changed the base branch from main to release/2.8.0 June 16, 2026 22:55
@jamesarich jamesarich merged commit 5ac26be into meshtastic:release/2.8.0 Jun 16, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build system changes enhancement New feature or request repo Repository maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants