Skip to content

fix(model): rssi explicit presence for protobufs 2.7.26.138 - #6498

Merged
jamesarich merged 4 commits into
mainfrom
fix/proto-2726138-rssi-explicit-presence
Jul 28, 2026
Merged

fix(model): rssi explicit presence for protobufs 2.7.26.138#6498
jamesarich merged 4 commits into
mainfrom
fix/proto-2726138-rssi-explicit-presence

Conversation

@jamesarich

@jamesarich jamesarich commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #6496, which cannot go green on its own.

Upstream protobufs commit 3fec03bb made MeshPacket.rx_rssi an optional int32, so Wire now generates Int? and :core:model stops compiling against the pinned snapshot. Every check on #6496 fails on that one error, because :core:model sits 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_rssi is 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 0 and throw away what the upstream change exists to provide, this threads nullability through the domain layer.

🛠️ Changes

  • DataPacket, Message, Reaction and MeshBeaconOffer carry rssi: Int?.
  • packet.rssi and reactions.rssi become nullable columns at schema 51, auto-migrated.
  • MetricFormatter.rssi renders an absent reading as ; the Rssi composable renders nothing at all, so no row ever claims a fabricated 0 dBm.
  • Locally-sent reactions store a null rssi instead of 0 — they were never received over the air, so they have no reading.
  • Node.rssi keeps its Int.MAX_VALUE sentinel; absence was already representable there and SignalInfo already 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:

  • DiscoveryScanEngine did if (rx_rssi != 0) node.rssi = rx_rssi, discarding a reported 0 dBm outright.
  • The signal-metrics chart filtered those packets out of the series entirely.

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:

  • Both tables are recreated with every column, row and index preserved, and uuid is copied — which matters because packet_fts is @Fts5(contentEntity = Packet::class) and keyed by rowid.
  • FTS sync triggers are dropped in onPreMigrate and rebuilt in onPostMigrate, so the search index survives the table rebuild.
  • The 50→51 schema diff is exactly those two columns and nothing else.

Known limitation: rows written before schema 51 stored 0 for both absent and 0 dBm, so existing history stays ambiguous — unrecoverable by construction. The reaction dialog therefore keeps its legacy == 0 check alongside the new null check, and says so in a comment.

🌟 SNR audit

rx_snr is unchanged upstream — still a bare float rx_snr = 8 at 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 float has no presence, so 0f on the wire is genuinely ambiguous. The inconsistency is already visible across three call sites:

  • DiscoveryScanEngine and ExportDataUseCase both guard on rx_snr != 0f, discarding real 0 dB readings (the export silently omits those rows from the CSV).
  • SignalMetrics instead guards on !isNaN(), so it does treat 0f as valid.

The real fix is an upstream optional float rx_snr mirroring 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.rssi storage field, and 94cdec45 removed it hours later the same day. The pinned commit 26db1b5 already 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 reported 0 stays 0.
  • MetricFormatterTestrssi(null) renders (alongside the existing rssi(0)0 dBm).
  • MeshBeaconOfferTest — encode/decode round-trips an absent rssi without collapsing it to zero.
  • CommonPacketDaoTestnull and 0 persist 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

  • Bug Fixes
    • RSSI readings now distinguish between an unavailable value and a valid 0 dBm measurement (including “unknown” display).
    • Missing RSSI values no longer overwrite previously recorded readings.
    • Signal charts, discovery outputs, maps, reports, and reaction dialogs consistently handle unavailable RSSI.
    • Discovery ranking now ignores nodes without RSSI when computing signal-strength scores.
    • CSV exports leave unavailable RSSI fields blank.
  • Database
    • Existing data is migrated automatically to support nullable/unavailable RSSI values.

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>
@github-actions github-actions Bot added the bugfix PR tag label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b13dbdd-ec01-4182-8b15-5556607c5338

📥 Commits

Reviewing files that changed from the base of the PR and between 6db03e6 and b0283fa.

📒 Files selected for processing (3)
  • core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryRankingEngineTest.kt

📝 Walkthrough

Walkthrough

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

Changes

Nullable RSSI propagation

Layer / File(s) Summary
Nullable model contracts
core/model/..., core/common/...
RSSI fields and beacon decoding now use nullable values, while explicit 0 dBm readings remain valid.
Database migration and persistence
core/database/...
Room schema version 51 stores nullable RSSI values and validates migration, defaults, and distinct null/zero round trips.
Packet and discovery processing
core/data/..., core/service/..., feature/discovery/..., gradle/libs.versions.toml
Packet, reaction, and discovery flows preserve missing RSSI, record reported zero values, exclude absent values from ranking, and update the protobuf snapshot.
Presentation and exports
core/ui/..., feature/discovery/..., feature/messaging/..., feature/node/..., androidApp/...
RSSI formatting, signal indicators, discovery views, maps, reaction dialogs, charts, and CSV output now handle nullable readings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: testing

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the main theme: making RSSI presence explicit for the protobuf update, even though the change spans more than model code.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 value

Make SnrAndRssi handle nullable RSSI.

Rssi already accepts Int?, but SnrAndRssi still takes rssi: Int and 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 to Rssi(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

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3196c and 18bbbc4.

📒 Files selected for processing (22)
  • core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt
  • core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
  • core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/51.json
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshBeaconOffer.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt
  • core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt
  • core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/MeshBeaconInvitationCard.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt
  • feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
  • gradle/libs.versions.toml

jamesarich and others added 2 commits July 28, 2026 13:07
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>
@jamesarich

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 18bbbc4 and 6db03e6.

📒 Files selected for processing (12)
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/discovery/DiscoveryOsmMap.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/discovery/DiscoveryGoogleMap.kt
  • core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/51.json
  • core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/DiscoveryMigrationTest.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/PacketDaoAtomicTransactionTest.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/DiscoveryMapNode.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/export/DiscoveryReportFormatter.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/scan/DiscoveryRankingEngine.kt
  • feature/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>
@jamesarich
jamesarich added this pull request to the merge queue Jul 28, 2026
Merged via the queue into main with commit 3913939 Jul 28, 2026
15 checks passed
@jamesarich
jamesarich deleted the fix/proto-2726138-rssi-explicit-presence branch July 28, 2026 21:23
jamesarich added a commit that referenced this pull request Jul 28, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant