Skip to content

Phantom node fix perhaps - #11271

Merged
thebentern merged 3 commits into
meshtastic:developfrom
NomDeTom:phantom-node-fix-perhaps
Jul 28, 2026
Merged

Phantom node fix perhaps#11271
thebentern merged 3 commits into
meshtastic:developfrom
NomDeTom:phantom-node-fix-perhaps

Conversation

@NomDeTom

@NomDeTom NomDeTom commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

After a phone reconnects, the client synthesizes history packets from the stored NodeDB
(PhoneAPI::makeReplay{Position,Telemetry,Environment,Status}Packet) so the app doesn't have
to wait to re-hear every node live. Users report these replayed nodes sometimes appearing as
phantom direct (0-hop) connections with 0 SNR / 0 RSSI, indistinguishable from a real
zero-signal neighbor — originally reported in #11208.

Captured a full NodeDB dump plus a live BLE/serial replay trace from a real device and traced
this to several independent defects in the replay path and in how snr/rssi are stored on
NodeInfoLite, not one bug. All four replay builders start from a blank MeshPacket and copy
only a handful of fields from the stored node header, so several stored fields were silently
dropped — and two of the numeric fields involved (rx_snr, rx_rssi) had no wire-level way to
represent "never measured" at all, so a dropped value came back as a plausible 0 instead of
nothing.

Fixes

  1. rx_rssi had no "unset" state on the wire. proto3 implicit presence makes an absent
    value byte-identical to a genuine 0 dBm reading — and 0 dBm is a real reading on
    SX126x/LR11x0/LR20x0, sometimes even positive on SX127x. Made MeshPacket.rx_rssi
    optional (has_rx_rssi). Every genuine-reception site now sets the bit explicitly:
    SX126xInterface, SX128xInterface, LR11x0Interface, LR20x0Interface, RF95Interface,
    StoreForwardModule's history replay, and the fabricated link metadata in
    MeshService::injectAsReceived. UdpMulticastHandler clears it when it deliberately zeroes
    SNR/RSSI for a UDP-relayed packet. MeshPacketSerializer's JSON/serial dump switched from
    rx_rssi != 0 to has_rx_rssi.

  2. Replay fabricated a direct-neighbor reading instead of leaving hop count unknown.
    setReplayHopFields defaulted an unknown hop count to 0, producing
    hop_start == hop_limit — indistinguishable from a genuine 0-hop neighbor. It now leaves
    both at 0 (documented as the "unknown" encoding on hop_start in mesh.proto), matching
    how firmware's own getHopsAway() already treats an un-bitfielded hop_start == 0.

  3. Persisted SNR (snr_q4) used truncation and a bare-zero sentinel. Truncating instead of
    rounding collapsed any reading within ±0.25 dB of zero to exactly 0, indistinguishable from
    "never stored." Encoding now uses lroundf, and an explicit NODEINFO_BITFIELD_HAS_SNR
    presence bit (bit 10, following the existing HAS_USER/HAS_IS_UNMESSAGABLE/
    HAS_XEDDSA_SIGNED precedent) disambiguates a genuine 0 dB reading from "unmeasured."
    Legacy on-disk records (bit unset) still decode correctly, since the old write gate never
    stored a real 0 dB value before this bit existed.

  4. The live write gate for SNR was if (mp.rx_snr) — truthiness, not presence — so a
    genuine 0 dB reading was silently discarded before it could even reach fix 3. Replaced with
    a check on mp.transport_mechanism == TRANSPORT_LORA, which is set only on the real
    over-the-air receive path (excludes locally-injected and MQTT-only packets, but still covers
    an MQTT-origin packet a gateway rebroadcasts onto LoRa, since that hop was genuinely
    measured).

  5. Position replay used the GPS fix time, not reception time, for rx_time.
    makeReplayPositionPacket set pkt.rx_time = pos.time — the position's own, often-zero,
    GPS timestamp — so a position last heard 11 days ago replayed as "Last heard: Now" on every
    reconnect. Now uses header->last_heard, matching the other three replay builders; the GPS
    fix time is unaffected since it already round-trips inside the payload via
    ConvertToPosition.

  6. Replay silently dropped channel and via_mqtt. All four builders hardcoded
    channel = 0 and never set via_mqtt, so an MQTT-sourced or non-default-channel node
    replayed as a directly-received, default-channel LoRa packet. Both are now copied from the
    stored header.

  7. Incidental: TraceRouteModule::appendMyIDandSNR's SNR→int8 cast could produce exactly
    -128 for a very low but real reading, colliding with the INT8_MIN "unknown" sentinel that
    RouteDiscovery.snr_towards/snr_back already use elsewhere in this file. Clamped to -127
    so a real reading can no longer alias the sentinel.

Deliberately out of scope

  • RSSI still cannot survive a reboot. NodeInfoLite has no RSSI field, so a replayed
    packet now leaves rx_rssi absent rather than fabricating 0 — honest, but the client shows
    no RSSI for any node until it's heard live again. Adding storage is a NodeInfoLite schema
    change with flash-budget impact across the 3-tier node sizing tiers, and is being kept out of
    this PR.
  • The handful of nodes observed with stored hops_away == 0 that may or may not be genuine
    direct neighbors isn't addressed — needs a live capture of the actual hop_start/hop_limit
    header from one of them, not a heuristic guess.
  • hopScalingModule's hop histogram deliberately buckets an unproven-but-real hop count as 0
    for its own conservative sizing purpose. That's an intentional disagreement with the stricter
    hops_away storage gate, documented in place, not changed here.

Testing

  • ./bin/run-tests.sh -e native — full 41-suite native run GREEN on the branch prior to the
    final RSSI-field-removal + comment cleanup pass.
  • test_type_conversions re-verified GREEN (28/28) after removing the NodeInfoLite.rssi
    storage field and regenerating the nanopb header.
  • test_fuzz_packets exercises both the presence and absence of has_rx_rssi.
  • Hardware compile check: nrf52_promicro_diy_tcxo, heltec-mesh-pocket-5000-inkhud.

Acknowledgements

Hat tip to @Folex-Fire for #11208 - it wasn't right, but it did spur me to make a run at it.

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631
    • Seeed Studio T-1000E tracker card
    • Other (please specify below)

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of received RSSI values, including legitimate readings of 0.
    • Preserved RSSI presence through packet encoding, replay, storage, and serialization.
    • Prevented packets received over UDP from reporting stale radio measurements.
    • Corrected SNR storage and restoration, including accurate 0 dB readings.
    • Improved replayed packet metadata, including channel, SNR, route, and receive time.
  • Tests
    • Expanded packet and RSSI presence coverage in fuzz and serialization tests.

@NomDeTom NomDeTom added requires-protos Requires changes to protobufs to work bugfix Pull request that fixes bugs labels 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: b5b661ed-dce7-47ae-9d4b-7e6c015a48f5

📥 Commits

Reviewing files that changed from the base of the PR and between 5f45073 and a9673e7.

📒 Files selected for processing (2)
  • src/mesh/NodeDB.cpp
  • src/mesh/RadioInterface.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mesh/NodeDB.cpp

📝 Walkthrough

Walkthrough

Changes

Mesh metadata semantics

Layer / File(s) Summary
RSSI presence propagation and serialization
protobufs, src/mesh/*Interface.cpp, src/mesh/MeshService.cpp, src/mesh/udp/*, src/modules/*, src/serialization/*, src/mesh/RadioInterface.cpp, test/*
RSSI handling now uses explicit has_rx_rssi presence across radio reception, injected and UDP packets, store-and-forward payloads, JSON serialization, logging, and fuzz inputs.
Measured SNR encoding and NodeDB state
src/mesh/NodeDB.h, src/mesh/NodeDB.cpp, src/modules/TraceRouteModule.cpp, src/mesh/TypeConversions.cpp
Measured SNR gains an explicit bitfield flag; Q4 encoding rounds values, decoding preserves measured 0 dB, and NodeDB updates require LoRa reception with RSSI presence.
Satellite database replay metadata
src/mesh/PhoneAPI.cpp
Replayed packets use header-derived channel, SNR, MQTT, and last-heard metadata, while unknown hop information is represented with zeroed hop fields.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RadioInterface
  participant NodeDB
  participant MeshPacketSerializer
  RadioInterface->>NodeDB: provide rx_rssi and rx_snr with presence flags
  NodeDB->>NodeDB: store measured SNR and set its bitfield
  MeshPacketSerializer->>MeshPacketSerializer: serialize rssi when has_rx_rssi is true
Loading

Possibly related PRs

Suggested reviewers: thebentern, jp-bennett

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague and uncertain to clearly summarize the main change. Use a concise, specific title like "Fix phantom direct-node replay metadata".
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is detailed, on-topic, and includes fixes, scope, testing, and attestations.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (31)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Meshnology W12 meshnology_w12 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-27. Updated for a9673e7.

@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)
src/mesh/NodeDB.cpp (1)

219-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the NodeDB bitfield accessor here.

Line 222 reads node.bitfield directly, but the repository convention requires NodeDB bitfield access through helpers. Replace the condition with nodeInfoLiteHasSnr(&node).

🤖 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 `@src/mesh/NodeDB.cpp` around lines 219 - 226, In the SNR restoration logic,
replace the direct node.bitfield check with the repository accessor
nodeInfoLiteHasSnr(&node), while preserving the existing legacy node.snr_q4
fallback and SNR assignment behavior.

Source: Coding guidelines

🤖 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 `@src/mesh/NodeDB.cpp`:
- Around line 3635-3644: Gate the SNR update in updateFrom() on mp.has_rx_rssi
in addition to the existing TRANSPORT_LORA check. Only assign info->snr and set
NODEINFO_BITFIELD_HAS_SNR_MASK when the packet explicitly indicates a local RF
receipt; preserve the current behavior for valid received measurements.

---

Nitpick comments:
In `@src/mesh/NodeDB.cpp`:
- Around line 219-226: In the SNR restoration logic, replace the direct
node.bitfield check with the repository accessor nodeInfoLiteHasSnr(&node),
while preserving the existing legacy node.snr_q4 fallback and SNR assignment
behavior.
🪄 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: 92754fd0-88d6-4571-b2ed-1a39c290648f

📥 Commits

Reviewing files that changed from the base of the PR and between 2a20ee5 and 5f45073.

⛔ Files ignored due to path filters (2)
  • src/mesh/generated/meshtastic/deviceonly.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
  • src/mesh/generated/meshtastic/mesh.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
📒 Files selected for processing (17)
  • protobufs
  • src/mesh/LR11x0Interface.cpp
  • src/mesh/LR20x0Interface.cpp
  • src/mesh/MeshService.cpp
  • src/mesh/NodeDB.cpp
  • src/mesh/NodeDB.h
  • src/mesh/PhoneAPI.cpp
  • src/mesh/RF95Interface.cpp
  • src/mesh/SX126xInterface.cpp
  • src/mesh/SX128xInterface.cpp
  • src/mesh/TypeConversions.cpp
  • src/mesh/udp/UdpMulticastHandler.h
  • src/modules/StoreForwardModule.cpp
  • src/modules/TraceRouteModule.cpp
  • src/serialization/MeshPacketSerializer.cpp
  • test/test_fuzz_packets/test_main.cpp
  • test/test_meshpacket_serializer/test_helpers.h

Comment thread src/mesh/NodeDB.cpp Outdated
@NomDeTom
NomDeTom marked this pull request as ready for review July 28, 2026 16:04
@thebentern
thebentern added this pull request to the merge queue Jul 28, 2026
Merged via the queue into meshtastic:develop with commit 2c57a17 Jul 28, 2026
103 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 28, 2026
8 tasks
@NomDeTom
NomDeTom requested a review from Copilot July 28, 2026 21:35

Copilot AI 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.

Pull request overview

This PR targets “phantom” nodes shown as direct (0-hop) neighbors with 0 SNR / 0 RSSI after a phone reconnect by tightening replay metadata correctness and adding explicit wire presence for MeshPacket.rx_rssi, so “unknown” no longer aliases to a plausible 0 dBm reading.

Changes:

  • Makes MeshPacket.rx_rssi optional (adds has_rx_rssi) and updates radio RX sites + serializers/logging to respect explicit presence.
  • Fixes replay packet construction to avoid fabricating 0-hop/direct-neighbor metadata; preserves channel, via_mqtt, and uses last_heard for rx_time.
  • Improves persisted SNR handling (rounding + new NodeInfoLite presence bit) and clamps traceroute SNR encoding to avoid colliding with the INT8_MIN sentinel.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/test_meshpacket_serializer/test_helpers.h Updates synthetic test packet to set has_rx_rssi when populating rx_rssi.
test/test_fuzz_packets/test_main.cpp Extends fuzzing to exercise both present/absent rx_rssi paths.
src/serialization/MeshPacketSerializer.cpp Switches JSON RSSI emission from rx_rssi != 0 to has_rx_rssi.
src/modules/TraceRouteModule.cpp Clamps q4 SNR to avoid producing -128 and colliding with “unknown” sentinel.
src/modules/StoreForwardModule.cpp Marks S&F replay packets as having measured RSSI (but currently does so unconditionally).
src/mesh/udp/UdpMulticastHandler.h Clears has_rx_rssi when zeroing RSSI/SNR for UDP-relayed packets.
src/mesh/TypeConversions.cpp Notes client-facing NodeInfo SNR still lacks presence, points to NodeInfoLite presence helper.
src/mesh/SX128xInterface.cpp Sets has_rx_rssi=true when capturing real RSSI from radio.
src/mesh/SX126xInterface.cpp Sets has_rx_rssi=true when capturing real RSSI from radio.
src/mesh/RF95Interface.cpp Sets has_rx_rssi=true when capturing real RSSI from radio.
src/mesh/RadioInterface.cpp Updates debug printing to show RSSI only when has_rx_rssi is set.
src/mesh/PhoneAPI.cpp Fixes replay packet hop encoding, rx_time, and preserves channel/via_mqtt; leaves RSSI absent for replay.
src/mesh/NodeDB.h Adds NODEINFO_BITFIELD_HAS_SNR and helper accessor nodeInfoLiteHasSnr().
src/mesh/NodeDB.cpp Uses rounding for SNR q4 persistence and gates SNR write on genuine RF reception (transport + RSSI presence).
src/mesh/MeshService.cpp Fabrics RSSI only when has_rx_rssi is not set (presence-aware injection behavior).
src/mesh/LR20x0Interface.cpp Sets has_rx_rssi=true when capturing real RSSI from radio.
src/mesh/LR11x0Interface.cpp Sets has_rx_rssi=true when capturing real RSSI from radio.
src/mesh/generated/meshtastic/mesh.pb.h Nanopb regen reflecting rx_rssi optional + updated field comments.
src/mesh/generated/meshtastic/deviceonly.pb.h Nanopb regen comment updates for q4 SNR encode/decode semantics.

Comment on lines 261 to +262
p->rx_rssi = this->packetHistory[i].rx_rssi;
p->has_rx_rssi = true; // rx_rssi has explicit presence; the stored value was a genuine measurement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

had to fix it elsehwere

NomDeTom added a commit to NomDeTom/MeshtasticFirmware that referenced this pull request Jul 28, 2026
… replay

preparePayload() set has_rx_rssi = true unconditionally on replay, regardless
of whether the packet's rx_rssi at store time was a genuine measurement (e.g.
MQTT-relayed packets carry no real RSSI). Store the presence bit alongside
rx_rssi in PacketHistoryStruct and restore it on replay instead.

Flagged by Copilot on meshtastic#11271 (same root cause the has_rx_time explicit
presence work fixes) but never addressed before that PR merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs requires-protos Requires changes to protobufs to work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants