fix(model): rssi explicit presence for protobufs 2.7.26.138 - #6498
Conversation
Upstream protobufs commit 3fec03bb made `MeshPacket.rx_rssi` an `optional int32`, so Wire now generates `Int?` and :core:model no longer compiles against the pinned snapshot. 0 dBm is a legitimate reading on some radios (SX126x reports exactly 0; SX127x can go positive), which is why the field gained explicit presence upstream. Rather than coerce the absent case back to 0, thread nullability through the domain layer so "no reading" and "0 dBm" stay distinct: - DataPacket, Message, Reaction and MeshBeaconOffer carry `rssi: Int?`. - packet.rssi and reactions.rssi become nullable columns (schema 51, auto-migrated; Room recreates both tables and rebuilds the FTS sync triggers via onPre/onPostMigrate, preserving uuid rowids). - MetricFormatter.rssi renders an absent reading as "—"; the Rssi composable renders nothing at all. - Locally-sent reactions now store a null rssi instead of 0. - Node.rssi keeps its Int.MAX_VALUE sentinel; an absent reading no longer clobbers a node's last real one. Two existing zero-guards were hiding real data and are now presence checks: DiscoveryScanEngine dropped a reported 0 dBm, and the signal metrics chart filtered those packets out entirely. Pre-schema-51 rows stored 0 for both absent and 0 dBm, so history stays ambiguous; the reaction dialog keeps its legacy `== 0` check for that reason. Supersedes the bare version bump in #6496. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughRSSI handling now preserves the distinction between an absent reading and a valid 0 dBm value across models, database schema, packet processing, discovery, UI rendering, maps, charts, and exports. ChangesNullable RSSI propagation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt (1)
176-179: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueMake
SnrAndRssihandle nullable RSSI.
Rssialready acceptsInt?, butSnrAndRssistill takesrssi: Intand has no callers, so it just adds an unnecessary coercion surface for nullable RSSI sources. Give it the same nullable signature and pass it through toRssi(rssi).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt` around lines 176 - 179, Update SnrAndRssi to accept rssi: Int? instead of Int, and pass the nullable value directly to Rssi(rssi). Preserve the existing SNR rendering behavior and avoid coercing nullable RSSI values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt`:
- Around line 270-271: Make CollectedNodeData.rssi nullable with a null default
so absent RSSI remains distinct from a reported 0 dBm; update
DiscoveryScanEngine presence checks, including the logic around the
direct-signal handling near the existing rssi == 0 check, to use node.rssi ==
null. Preserve the nullable value through persistence and add a regression test
alongside the zero-value RSSI test for packets without RSSI.
---
Nitpick comments:
In
`@core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt`:
- Around line 176-179: Update SnrAndRssi to accept rssi: Int? instead of Int,
and pass the nullable value directly to Rssi(rssi). Preserve the existing SNR
rendering behavior and avoid coercing nullable RSSI values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a83524d-98f2-47eb-bac1-3fc7505a5d31
📒 Files selected for processing (22)
core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.ktcore/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.ktcore/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/51.jsoncore/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshBeaconOffer.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.ktcore/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.ktfeature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.ktfeature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/MeshBeaconInvitationCard.ktfeature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.ktgradle/libs.versions.toml
Both are public wrappers that forward straight to Rssi, which already takes Int?. Keeping them on Int just forced callers with a nullable source to coerce at the boundary — the coercion this change set exists to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review catch: making the packet field nullable only fixed half of the discovery path. CollectedNodeData.rssi still defaulted to 0, so an absent reading was persisted as 0 dBm and the neighbor-type heuristic treated `rssi == 0` as "never seen directly". - CollectedNodeData.rssi and discovered_node.rssi are nullable; the column folds into the same schema 51 migration rather than adding 52. - The neighbor-type check uses `rssi == null` for presence. - Ranking excludes nodes with no reading from the median instead of dragging it toward 0 dBm. - The exported report and both map snippets format via MetricFormatter, so an absent reading shows "—" rather than "0 dBm". Moved the packet rssi round-trip test out of CommonPacketDaoTest, which is abstract with no subclass and therefore never runs, into PacketDaoAtomicTransactionTest. It caught a real ordering bug in my own assertion: getMessagesFrom returns newest-first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt`:
- Line 194: Add a dedicated 50→51 migration test alongside
discoveredNodeEntity_defaultValues() that builds or loads a v50 database
containing a discovery row with the legacy nullable-column defaults, including
rssi = 0, then runs the Room migration to v51 and verifies the row is preserved
with the expected v51 SQL NULL behavior. Keep the existing current-schema
default-value test separate and reuse the migration/database fixtures already
defined in DiscoveryMigrationTest.
In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt`:
- Around line 96-97: Update the RSSI median calculation in the discovery ranking
flow to remain nullable when rssiValues is empty instead of defaulting to 0 dBm.
Adjust the comparator to rank nodes with missing RSSI after nodes with measured
values, while preserving the existing median-based ordering for non-empty RSSI
data.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f069a00-8537-48f2-ba83-134dd1cea9e3
📒 Files selected for processing (12)
androidApp/src/fdroid/kotlin/org/meshtastic/app/map/discovery/DiscoveryOsmMap.ktandroidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryGoogleMap.ktcore/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/51.jsoncore/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/PacketDaoAtomicTransactionTest.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.ktfeature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.ktfeature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.ktfeature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.ktfeature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt
…rvation Two review findings. An all-absent rssi set produced a median of 0 via medianInt's empty-list sentinel, and 0 dBm outranks every real negative reading — so a preset where nobody reported rssi won criterion 4b over one with genuinely good signal. medianRssi is now Int?, absent sorts after any measured value. MeshtasticDatabaseMigrationTest.migrateAll only walks v3→latest on an empty database, so nothing proved rows survive the 50→51 table recreate (DROP + RENAME across packet, reactions and discovered_node). Added a data-preservation case: seeds v50 rows, migrates, and asserts a legacy rssi = 0 stays 0 rather than becoming NULL, the cascading FK target is intact, and NULL is now storable where the column was NOT NULL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three inconsistencies in the previous commit, two of them self-inflicted by describing #6498's end state as if it were already on main. - The "Sibling call sites" pre-merge check told reviewers to flag SNR zero defaults, while the ast-grep rule deliberately excludes rx_snr because it has no proto presence and cannot be fixed app-side. The check now carves out rx_snr while keeping app-level nullable SNR in scope. - The empty-aggregate warning claimed a 0 fallback "outranks every real negative RSSI", which is only true under a higher-is-better comparator; a plain min would pick 0 as the smallest instead. Reworded as a comparator-dependent bias, keeping the concrete ranking case. - The signal rule's message pointed at MetricFormatter.rssi(null), which does not exist on main -- the only overload is rssi(value: Int); the nullable one arrives with #6498. Message now states the requirement without naming an API that isn't there. Declined the suggestion to add Float RSSI patterns (?: 0f, takeIf { it != 0f }) to the signal rule. RSSI is an integer dBm value throughout -- proto rx_rssi: Int, Node.rssi: Int, MetricFormatter.rssi(value: Int) -- and no Float-typed rssi declaration exists, so those patterns would be dead. The rule now documents that scope and why, instead of over-claiming Int?/Float?. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Supersedes #6496, which cannot go green on its own.
Upstream protobufs commit 3fec03bb made
MeshPacket.rx_rssianoptional int32, so Wire now generatesInt?and:core:modelstops compiling against the pinned snapshot. Every check on #6496 fails on that one error, because:core:modelsits at the bottom of the module graph.The reason upstream added explicit presence is the interesting part: 0 dBm is a legitimate reading. An SX126x can report exactly 0 dBm and an SX127x's formula can go positive, so
has_rx_rssiis what distinguishes a real 0 from "the radio told us nothing" — and a replayed packet rebuilt from history is supposed to leave the field absent rather than emit 0.So rather than coerce the absent case back to
0and throw away what the upstream change exists to provide, this threads nullability through the domain layer.🛠️ Changes
DataPacket,Message,ReactionandMeshBeaconOffercarryrssi: Int?.packet.rssiandreactions.rssibecome nullable columns at schema 51, auto-migrated.MetricFormatter.rssirenders an absent reading as—; theRssicomposable renders nothing at all, so no row ever claims a fabricated0 dBm.0— they were never received over the air, so they have no reading.Node.rssikeeps itsInt.MAX_VALUEsentinel; absence was already representable there andSignalInfoalready checks it. What changed is that an absent reading no longer clobbers a node's last real one.🐛 Two zero-guards that were hiding real data
Making absence explicit exposed two places that were already losing genuine readings:
DiscoveryScanEnginedidif (rx_rssi != 0) node.rssi = rx_rssi, discarding a reported 0 dBm outright.Both are now presence checks, so a real 0 dBm is recorded and plotted.
🧹 Migration notes
Room generated the 50→51 auto-migration; I verified the emitted SQL rather than assuming:
uuidis copied — which matters becausepacket_ftsis@Fts5(contentEntity = Packet::class)and keyed by rowid.onPreMigrateand rebuilt inonPostMigrate, so the search index survives the table rebuild.Known limitation: rows written before schema 51 stored
0for both absent and 0 dBm, so existing history stays ambiguous — unrecoverable by construction. The reaction dialog therefore keeps its legacy== 0check alongside the new null check, and says so in a comment.🌟 SNR audit
rx_snris unchanged upstream — still a barefloat rx_snr = 8at both the pinned commit and HEAD. No compile impact.It does carry the identical latent bug, but the app cannot fix it: a proto3 bare
floathas no presence, so0fon the wire is genuinely ambiguous. The inconsistency is already visible across three call sites:DiscoveryScanEngineandExportDataUseCaseboth guard onrx_snr != 0f, discarding real 0 dB readings (the export silently omits those rows from the CSV).SignalMetricsinstead guards on!isNaN(), so it does treat0fas valid.The real fix is an upstream
optional float rx_snrmirroring what 3fec03bb did for rssi. Left alone here deliberately — papering over it in the app would just add a fourth convention.Also worth flagging: 3fec03bb additionally added a
NodeInfoLite.rssistorage field, and94cdec45removed it hours later the same day. The pinned commit26db1b5already includes the removal, so we're fine — but treat this snapshot range as unsettled.Testing Performed
Full baseline green:
spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompile.New coverage:
MeshDataMapperTest— absent rssi stays null; a reported0stays0.MetricFormatterTest—rssi(null)renders—(alongside the existingrssi(0)→0 dBm).MeshBeaconOfferTest— encode/decode round-trips an absent rssi without collapsing it to zero.CommonPacketDaoTest—nulland0persist distinctly through the nullable column.DiscoveryScanEngineTest— a reported 0 dBm is recorded rather than discarded.Not verified on device: the migration was validated by reading the generated SQL and by the schema diff, not by an on-device upgrade from 50 → 51.
🤖 Generated with Claude Code
Summary by CodeRabbit