Skip to content

Arrival time fix perhaps - #11274

Merged
thebentern merged 9 commits into
meshtastic:developfrom
NomDeTom:arrival-time-fix-perhaps
Jul 29, 2026
Merged

Arrival time fix perhaps#11274
thebentern merged 9 commits into
meshtastic:developfrom
NomDeTom:arrival-time-fix-perhaps

Conversation

@NomDeTom

@NomDeTom NomDeTom commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces a robust monotonic uptime clock abstraction and refactors how packet reception timestamps (rx_time) are handled throughout the codebase. The main goals are to provide testable, rollover-immune timekeeping and to ensure that packets only claim to have a valid wall-clock timestamp when the device actually has trustworthy time. The changes also ensure that packets received before the wall clock is trustworthy are later reconciled to real epochs once accurate time becomes available.

The most important changes are:

Monotonic uptime clock abstraction:

  • Introduced the Time namespace (src/Time.h, src/Time.cpp) providing getMillis() (32-bit, wraps every ~49.7 days) and getMillis64() (64-bit, rollover-immune) for monotonic uptime tracking, with support for test injection. This is now included project-wide for consistent timekeeping. [1] [2] [3]

rx_time semantics and presence tracking:

  • Refactored all packet timestamping to use an explicit has_rx_time flag: when the wall clock is not trustworthy, rx_time is set to a monotonic millis() placeholder and has_rx_time is false. Only when real time is available does rx_time represent an actual epoch and has_rx_time is true. All code reading rx_time is updated to check has_rx_time instead of assuming nonzero means valid. [1] [2] [3] [4] [5] [6] [7] [8]

Deferred timestamp reconciliation:

  • Added MeshService::reconcilePendingRxTimes(), which scans queued packets with placeholder rx_time values and, once the wall clock becomes trustworthy, backdates them to a real epoch based on elapsed uptime. This ensures that packets received before accurate time was available are not left with bogus or missing timestamps. [1] [2] [3]

Consistent application across the codebase:

  • All places that generate, update, or display rx_time now consistently use the new semantics, including packet allocation, injection, phone API replay, and NodeDB updates. This prevents accidental use of monotonic placeholders as real epochs and ensures correct presence signaling throughout. [1] [2] [3] [4]

Submodule update:

  • Updated the protobufs submodule to a new commit, likely to reflect protocol changes supporting the new has_rx_time semantics.

🤝 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 packet timestamp handling when network time is unavailable or becomes available later.
    • Corrected queued packet timestamps after network time synchronization.
    • Prevented placeholder timestamps from appearing as real packet ages or JSON timestamps.
    • Improved replayed packet metadata for receive times and signal strength.
    • Displays “?” when packet age is unknown in health and power telemetry.
  • Tests
    • Added coverage for timestamp serialization and time reconciliation scenarios.

NomDeTom added 2 commits July 28, 2026 20:34
rx_time is now proto3 optional with a has_rx_time presence bit, matching
the rx_rssi treatment. A node with no GPS and no phone connected yet has
no time source at all, so a bare 0 was indistinguishable from a genuine
1970-01-01 reading; downstream consumers (replay packets, JSON
serialization) now check has_rx_time instead of the value.
Extract the repeated haveTime/rx_time/has_rx_time stamp logic (5 call
sites across Router.cpp, MeshBeaconModule.cpp, MeshService.cpp) into
Router::computeRxTimeStamp()/stampRxTime(). Also shorten the new RTC.cpp
LOG_DEBUG string. Saves 48 bytes of flash on rak4631 (measured), no
behavior change.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab699b60-0524-4103-83aa-1a6a49fa105e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change introduces monotonic uptime timing and explicit has_rx_time semantics. Receive timestamps use placeholders until network-qualified time is available, queued packets are reconciled after RTC synchronization, and storage, replay, serialization, UI, and tests preserve unknown timestamps correctly.

Changes

Receive timestamp contracts and stamping

Layer / File(s) Summary
Time and packet timestamp contracts
protobufs, src/UptimeClock.*, src/mesh/Router.h, src/mesh/NodeDB.h
Adds monotonic time APIs, test-clock support, timestamp validity helpers, and the SINCE_UNKNOWN sentinel.
Trust-aware receive timestamp stamping
src/mesh/Router.cpp, src/mesh/MeshService.cpp, src/modules/MeshBeaconModule.cpp, src/mesh/RadioInterface.cpp
Centralizes receive timestamp assignment and presence flags across routing, injected, phone, beacon, and trace packets.

RTC reconciliation and time consumers

Layer / File(s) Summary
RTC transition and queued packet reconciliation
src/gps/RTC.cpp, src/mesh/MeshService.*, src/mesh/NodeDB.cpp, src/modules/NodeInfoModule.cpp, src/modules/Telemetry/*
Reconciles queued millisecond placeholders when network time becomes valid and reports unknown ages without trustworthy receive timestamps.

Replay, storage, and validation

Layer / File(s) Summary
Replay, storage, serialization, and validation
src/modules/StoreForwardModule.*, src/mesh/PhoneAPI.cpp, src/serialization/MeshPacketSerializer.cpp, test/*, variants/native/portduino/platformio.ini
Preserves receive-time and RSSI presence through store-and-forward and satellite replay, serializes absent timestamps as zero, and adds regression coverage and wasm build wiring.

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

Sequence Diagram(s)

sequenceDiagram
  participant RTC
  participant MeshService
  participant toPhoneQueue
  RTC->>MeshService: onTimeSourceQualityChanged()
  MeshService->>toPhoneQueue: inspect packets with has_rx_time=false
  MeshService->>MeshService: convert millis placeholder to epoch rx_time
  MeshService->>toPhoneQueue: set has_rx_time=true and re-enqueue
Loading

Possibly related PRs

Suggested labels: bugfix, requires-protos

Suggested reviewers: thebentern, caveman99, jp-bennett, ndoo

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.50% 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 related to the main change but too vague and tentative to clearly describe the PR. Use a concise, specific title like 'Refactor packet arrival timestamps with monotonic uptime clock'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the main changes and includes the required attestations, with only minor gaps in detail.
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-28. Updated for 2cc646e.

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 introduces a monotonic uptime clock abstraction (Time::getMillis() / Time::getMillis64()) and refactors meshtastic_MeshPacket::rx_time to have explicit presence via has_rx_time, ensuring packets only claim a valid wall-clock timestamp when RTC quality is trustworthy. It also adds a reconciliation step to retroactively convert queued “unknown-time” packets (carrying a millis placeholder) into real epochs once net-quality time becomes available.

Changes:

  • Added src/Time.{h,cpp} as a single test-injectable seam for monotonic uptime timekeeping.
  • Implemented explicit rx_time presence (has_rx_time) across packet creation/receive, serialization, NodeDB updates, and replay paths.
  • Added MeshService::reconcilePendingRxTimes() and hooked it into RTC quality transitions to backdate queued packets once time becomes trustworthy.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/test_traffic_management/test_main.cpp Updates synthetic test packet to set has_rx_time=true.
test/test_meshpacket_serializer/test_helpers.h Updates synthetic test helper packet to set has_rx_time=true.
src/Time.h New monotonic uptime API + unit-test injection seam.
src/Time.cpp Implements Time::getMillis() and Time::getMillis64().
src/serialization/MeshPacketSerializer.cpp Emits JSON timestamp=0 when has_rx_time=false to avoid leaking placeholder values.
src/modules/StoreForwardModule.cpp Sets has_rx_time for stored history packets (needs adjustment; see PR comments).
src/modules/NodeInfoModule.cpp Switches NodeInfo reply-suppression timing to gate on has_rx_time.
src/modules/MeshBeaconModule.cpp Uses stampRxTime() so beacons don’t incorrectly appear to have epoch time when clock is untrusted.
src/mesh/Router.h Introduces RxTimeStamp, computeRxTimeStamp(), and stampRxTime() helpers.
src/mesh/Router.cpp Centralizes rx-time stamping and applies placeholder semantics in dispatchReceived() and send allocation.
src/mesh/RadioInterface.cpp Prints rx time only when has_rx_time is set.
src/mesh/PhoneAPI.cpp Sets has_rx_time when replaying packets based on whether last_heard exists.
src/mesh/NodeDB.cpp Ensures last_heard and age calculations are gated on has_rx_time.
src/mesh/MeshService.h Declares reconcilePendingRxTimes().
src/mesh/MeshService.cpp Implements rx-time reconciliation for queued-to-phone packets once RTC quality improves.
src/mesh/generated/meshtastic/mesh.pb.h Updates generated nanopb bindings: makes rx_time optional and adds has_rx_time.
src/gps/RTC.cpp Triggers rx-time reconciliation when RTC quality crosses RTCQualityFromNet.
src/configuration.h Includes Time.h globally for consistent access to the uptime seam.

Comment thread src/modules/StoreForwardModule.cpp Outdated
Comment thread src/Time.h Outdated

@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: 3

🧹 Nitpick comments (3)
src/Time.h (1)

7-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the clock documentation within the repository’s comment-size rule.

  • src/Time.h#L7-L25: shorten the multi-paragraph API rationale to a compact contract comment.
  • src/Time.cpp#L1-L10: shorten the implementation header comment and move extended rationale to external documentation.

As per coding guidelines, C++ comments should be one or two lines maximum and avoid multi-paragraph explanations.

🤖 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/Time.h` around lines 7 - 25, Shorten the clock documentation in
src/Time.h lines 7-25 to a compact one- or two-line contract covering monotonic
uptime, getMillis64() rollover behavior, and the ISR-safety distinction; remove
the extended rationale. Also shorten the implementation header comment in
src/Time.cpp lines 1-10 to the same concise style, moving no additional
rationale into code comments.

Source: Coding guidelines

test/test_meshpacket_serializer/test_helpers.h (1)

40-40: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for absent receive timestamps.

This shared fixture now always exercises the present case. Add a parameter or companion fixture with has_rx_time = false and a nonzero placeholder, then assert serializers emit zero rather than the placeholder.

🤖 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 `@test/test_meshpacket_serializer/test_helpers.h` at line 40, Add coverage
alongside the shared packet fixture for an absent receive timestamp by setting
has_rx_time to false while retaining a nonzero placeholder rx_time. Update the
relevant serializer tests to use this fixture and assert they emit zero instead
of the placeholder, while preserving the existing present-timestamp coverage.
src/mesh/Router.h (1)

13-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the new timestamp comments concise. These blocks repeat one cross-layer contract and exceed the repository’s one-to-two-line comment limit.

  • src/mesh/Router.h#L13-L15: reduce the RxTimeStamp contract to its essential invariant.
  • src/mesh/Router.cpp#L296-L299: shorten the allocation-time placeholder explanation.
  • src/mesh/Router.cpp#L1325-L1332: shorten the dispatch-time timestamp explanation.
  • src/mesh/Router.cpp#L1472-L1476: shorten the trace-path explanation.
  • src/mesh/MeshService.cpp#L183-L188: shorten the reconciliation rationale.
  • src/mesh/MeshService.cpp#L253-L257: shorten the injection-path explanation.
  • src/mesh/MeshService.cpp#L296-L299: shorten the phone-originated packet explanation.
  • src/mesh/MeshService.cpp#L637-L640: shorten the age-calculation explanation.
  • src/modules/MeshBeaconModule.cpp#L345-L349: shorten the beacon stamping explanation.
  • src/modules/StoreForwardModule.cpp#L260-L267: shorten the replay timestamp explanation.
  • src/gps/RTC.cpp#L21-L24: shorten the threshold-crossing explanation.
  • src/mesh/MeshService.h#L140-L143: shorten the method declaration comment.
  • src/mesh/NodeDB.cpp#L3236-L3239: shorten the sinceReceived() explanation.
  • src/mesh/NodeDB.cpp#L3639-L3642: shorten the last_heard explanation.
🤖 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/Router.h` around lines 13 - 15, The timestamp comments are overly
verbose; reduce each listed comment to one or two concise lines while preserving
only the essential RxTimeStamp invariant and the specific behavior relevant to
its nearby code. Update the comments at src/mesh/Router.h:13-15,
src/mesh/Router.cpp:296-299, 1325-1332, and 1472-1476;
src/mesh/MeshService.cpp:183-188, 253-257, 296-299, and 637-640;
src/modules/MeshBeaconModule.cpp:345-349;
src/modules/StoreForwardModule.cpp:260-267; src/gps/RTC.cpp:21-24;
src/mesh/MeshService.h:140-143; and src/mesh/NodeDB.cpp:3236-3239 and 3639-3642.

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/modules/NodeInfoModule.cpp`:
- Around line 36-38: Update the NodeInfo suppression-window logic around
lastNodeInfoSeen to use a single monotonic Time value for both the current
timestamp and stored comparisons, rather than selecting between mp.rx_time and
getTime(). Preserve the existing has_rx_time handling for packet metadata, but
ensure an unset or adjusted RTC cannot cause repeated requests to remain
suppressed or extend the throttle window.

In `@src/modules/StoreForwardModule.cpp`:
- Line 268: Update the packet history record and its write path to persist the
original timestamp validity separately from the history-age field and retain the
captured timestamp value. In the restore logic around has_rx_time in
StoreForwardModule, assign the stored timestamp and presence state instead of
deriving validity from the current getRTCQuality() result, preserving correct
handling by reconcilePendingRxTimes().

In `@src/Time.cpp`:
- Around line 22-31: The 64-bit time accumulator must be rebased when the clock
source changes or resets. In src/Time.cpp lines 22-31, update Time::getMillis64
and its static state so lastLow/highWord are reset or rebased after
injected-clock resets; in src/Time.h lines 45-50, make useRealClock() invoke
that rebase instead of only clearing test-clock variables.

---

Nitpick comments:
In `@src/mesh/Router.h`:
- Around line 13-15: The timestamp comments are overly verbose; reduce each
listed comment to one or two concise lines while preserving only the essential
RxTimeStamp invariant and the specific behavior relevant to its nearby code.
Update the comments at src/mesh/Router.h:13-15, src/mesh/Router.cpp:296-299,
1325-1332, and 1472-1476; src/mesh/MeshService.cpp:183-188, 253-257, 296-299,
and 637-640; src/modules/MeshBeaconModule.cpp:345-349;
src/modules/StoreForwardModule.cpp:260-267; src/gps/RTC.cpp:21-24;
src/mesh/MeshService.h:140-143; and src/mesh/NodeDB.cpp:3236-3239 and 3639-3642.

In `@src/Time.h`:
- Around line 7-25: Shorten the clock documentation in src/Time.h lines 7-25 to
a compact one- or two-line contract covering monotonic uptime, getMillis64()
rollover behavior, and the ISR-safety distinction; remove the extended
rationale. Also shorten the implementation header comment in src/Time.cpp lines
1-10 to the same concise style, moving no additional rationale into code
comments.

In `@test/test_meshpacket_serializer/test_helpers.h`:
- Line 40: Add coverage alongside the shared packet fixture for an absent
receive timestamp by setting has_rx_time to false while retaining a nonzero
placeholder rx_time. Update the relevant serializer tests to use this fixture
and assert they emit zero instead of the placeholder, while preserving the
existing present-timestamp coverage.
🪄 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: e23d641a-1439-4b1e-8e40-23fc5e27eda2

📥 Commits

Reviewing files that changed from the base of the PR and between d87cbec and 50742ed.

⛔ Files ignored due to path filters (1)
  • src/mesh/generated/meshtastic/mesh.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
📒 Files selected for processing (18)
  • protobufs
  • src/Time.cpp
  • src/Time.h
  • src/configuration.h
  • src/gps/RTC.cpp
  • src/mesh/MeshService.cpp
  • src/mesh/MeshService.h
  • src/mesh/NodeDB.cpp
  • src/mesh/PhoneAPI.cpp
  • src/mesh/RadioInterface.cpp
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/MeshBeaconModule.cpp
  • src/modules/NodeInfoModule.cpp
  • src/modules/StoreForwardModule.cpp
  • src/serialization/MeshPacketSerializer.cpp
  • test/test_meshpacket_serializer/test_helpers.h
  • test/test_traffic_management/test_main.cpp

Comment thread src/modules/NodeInfoModule.cpp Outdated
Comment thread src/modules/StoreForwardModule.cpp Outdated
Comment thread src/UptimeClock.cpp
NomDeTom added 4 commits July 28, 2026 23:08
… 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.
.github/copilot-instructions.md:338 caps code comments at 1-2 lines; several
blocks added across the rx_time explicit-presence work ran well past that.
Also consolidates Time.cpp's file-level doc comment into Time.h, where the
rest of the Time:: API contract already lives.

No behavior change.
- test_meshpacket_serializer: has_rx_time=false fixture plus tests asserting
  JsonSerialize/JsonSerializeEncrypted emit 0 rather than leaking the
  millis() placeholder, alongside the has_rx_time=true baseline.
- test_stream_api: two tests driving a real PhoneAPI handshake (want_config_id
  through STATE_SEND_PACKETS) that simulate a phone time-giving transaction
  arriving before vs. after a queued packet is drained - covering both the
  reconciled and the ships-with-placeholder-absent paths of
  MeshService::reconcilePendingRxTimes().
- Time.h: drop the reserved-identifier include guard (_MT_TIME_H); pragma
  once already covers it, matching convention elsewhere (e.g. RTC.h).
- Time.cpp: rebase getMillis64()'s wrap accumulator when the test seam
  swaps clock sources, so a real<->injected clock jump isn't miscounted
  as a genuine 32-bit wrap.
- NodeInfoModule: the 12h reply-suppression window is a local dedup
  duration, not a wall-clock reading - switch it to Time::getMillis64()
  so RTC-quality jumps and replayed packets' stale rx_time can't perturb
  it.
- StoreForwardModule: has_rx_time was derived from *current* RTC quality
  at replay time rather than stored at capture time, so a history entry
  saved while time-blind could be misreported as a valid epoch once the
  clock later improved. Persist the presence bit in PacketHistoryStruct
  instead.
@NomDeTom

Copy link
Copy Markdown
Collaborator Author

Addressed all actionable review feedback:

Correctness fixes:

  • StoreForwardModule: has_rx_time (and, while in there, has_rx_rssi) were derived from current RTC quality/state at replay time instead of what was actually true when the packet was stored - fixed by persisting both presence bits in PacketHistoryStruct at store time. The has_rx_rssi half was flagged by Copilot back on Phantom node fix perhaps #11271 but never landed before that PR merged.
  • NodeInfoModule: the 12h reply-suppression window was riding a mix of mp.rx_time/getTime() (wall clock) instead of monotonic uptime - switched to Time::getMillis64(), since it's a purely local dedup duration that shouldn't be affected by RTC-quality jumps or a replayed packet's stale timestamp.
  • Time::getMillis64(): the wrap accumulator wasn't rebased across an injected-clock reset in tests, so a real↔test clock swap could be miscounted as a genuine 32-bit wrap.
  • Time.h: dropped the reserved-identifier include guard (_MT_TIME_H); #pragma once already covers it.

Style: trimmed every flagged comment block to the repo's 1-2 line guideline (.github/copilot-instructions.md:338), and consolidated Time.cpp's file-level doc comment into Time.h.

Test coverage:

  • test_meshpacket_serializer: new has_rx_time=false fixture + tests asserting JsonSerialize/JsonSerializeEncrypted emit 0 rather than leaking the placeholder.
  • test_stream_api: two new tests drive a real PhoneAPI handshake (want_config_id through STATE_SEND_PACKETS) simulating a phone time-giving transaction arriving before vs. after a queued packet is drained - covering both "still queued, gets reconciled" and "already delivered, keeps its placeholder absent" (the latter also confirmed that rx_time is proto3 optional, so an absent value never crosses the wire at all).

Verification: native suite 793/793 passing; rak4631 hardware build succeeds.

Commits are split by concern: bd44b4e8c (has_rx_rssi correction from #11271), 54597e9f0 (docs-only), cc02d5781 (tests), c81f7110e (the three fixes above).

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

🧹 Nitpick comments (1)
test/test_stream_api/test_main.cpp (1)

571-643: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assertion failure mid-test leaks global nodeDB/Time/RTC state to later tests.

Both new tests restore nodeDB, Time::useRealClock(), and RTC state only at the very end of the function. Unity's TEST_ASSERT* aborts via longjmp on failure, which skips both the remaining restore statements and the destructors of testNodeDB/ScopedMeshService/PhoneAPITestShim. If any assertion before the restore lines fails (e.g. drainHandshakeForPacketFrom returns false, or the has_rx_time/rx_time checks fail), nodeDB is left pointing at a destroyed stack object and Time stays on the injected test clock for every subsequent test in the binary — turning one genuine failure into a cascade of unrelated ones.

Consider an RAII guard (mirroring the existing ScopedMeshService pattern) that swaps nodeDB in its constructor and restores nodeDB/Time/RTC state in its destructor, so cleanup runs even when an assertion aborts the test.

🤖 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 `@test/test_stream_api/test_main.cpp` around lines 571 - 643, Make both
handshake tests exception/longjmp-safe by introducing an RAII cleanup guard,
modeled on ScopedMeshService, that restores nodeDB, switches back to the real
clock, and resets RTC state in its destructor. Initialize it before replacing
nodeDB and remove the manual end-of-test restoration so cleanup also occurs when
any TEST_ASSERT aborts execution.
🤖 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.

Nitpick comments:
In `@test/test_stream_api/test_main.cpp`:
- Around line 571-643: Make both handshake tests exception/longjmp-safe by
introducing an RAII cleanup guard, modeled on ScopedMeshService, that restores
nodeDB, switches back to the real clock, and resets RTC state in its destructor.
Initialize it before replacing nodeDB and remove the manual end-of-test
restoration so cleanup also occurs when any TEST_ASSERT aborts execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 407e7451-5235-426a-91a3-316f50cf76af

📥 Commits

Reviewing files that changed from the base of the PR and between 50742ed and c81f711.

📒 Files selected for processing (18)
  • src/Time.cpp
  • src/Time.h
  • src/gps/RTC.cpp
  • src/mesh/MeshService.cpp
  • src/mesh/MeshService.h
  • src/mesh/NodeDB.cpp
  • src/mesh/PhoneAPI.cpp
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/MeshBeaconModule.cpp
  • src/modules/NodeInfoModule.cpp
  • src/modules/StoreForwardModule.cpp
  • src/modules/StoreForwardModule.h
  • src/serialization/MeshPacketSerializer.cpp
  • test/test_meshpacket_serializer/ports/test_timestamp.cpp
  • test/test_meshpacket_serializer/test_helpers.h
  • test/test_meshpacket_serializer/test_serializer.cpp
  • test/test_stream_api/test_main.cpp
💤 Files with no reviewable changes (1)
  • src/modules/MeshBeaconModule.cpp
🚧 Files skipped from review as they are similar to previous changes (11)
  • test/test_meshpacket_serializer/test_helpers.h
  • src/Time.cpp
  • src/serialization/MeshPacketSerializer.cpp
  • src/mesh/MeshService.h
  • src/Time.h
  • src/mesh/NodeDB.cpp
  • src/mesh/Router.h
  • src/mesh/MeshService.cpp
  • src/gps/RTC.cpp
  • src/mesh/PhoneAPI.cpp
  • src/mesh/Router.cpp

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

Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

src/mesh/PhoneAPI.cpp:1293

  • pkt.has_rx_time is currently derived from header->last_heard != 0, but NodeDB::addFromContact can stamp last_heard via getTime() (boot-relative seconds) even when RTCQualityNone, making last_heard nonzero but not a real epoch. That will cause telemetry replay packets (and their embedded Telemetry.time field) to claim a valid timestamp and leak a bogus “epoch” to the phone.

Consider only treating last_heard as an epoch when it is outside the boot-relative wrap range (getTime() when RTCQualityNone stays < ~4,294,967).

    const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
    pkt.rx_time = header ? header->last_heard : 0;
    // last_heard is only ever written from a has_rx_time-gated reception (NodeDB::updateFrom),
    // so a legacy 0 unambiguously means "never".
    pkt.has_rx_time = (header && header->last_heard != 0);

src/mesh/PhoneAPI.cpp:1470

  • pkt.has_rx_time is currently derived from header->last_heard != 0, but NodeDB::addFromContact can stamp last_heard via getTime() (boot-relative seconds) even when RTCQualityNone, making last_heard nonzero but not a real epoch. That will cause status replay packets to claim a valid timestamp and leak a bogus “epoch” to the phone.

Consider only treating last_heard as an epoch when it is outside the boot-relative wrap range (getTime() when RTCQualityNone stays < ~4,294,967).

    const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
    pkt.rx_time = header ? header->last_heard : 0;
    // last_heard is only ever written from a has_rx_time-gated reception (NodeDB::updateFrom),
    // so a legacy 0 unambiguously means "never".
    pkt.has_rx_time = (header && header->last_heard != 0);

src/mesh/PhoneAPI.cpp:1402

  • pkt.has_rx_time is currently derived from header->last_heard != 0, but NodeDB::addFromContact can stamp last_heard via getTime() (boot-relative seconds) even when RTCQualityNone, making last_heard nonzero but not a real epoch. That will cause environment replay packets (and their embedded Telemetry.time field) to claim a valid timestamp and leak a bogus “epoch” to the phone.

Consider only treating last_heard as an epoch when it is outside the boot-relative wrap range (getTime() when RTCQualityNone stays < ~4,294,967).

    const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
    pkt.rx_time = header ? header->last_heard : 0;
    // last_heard is only ever written from a has_rx_time-gated reception (NodeDB::updateFrom),
    // so a legacy 0 unambiguously means "never".
    pkt.has_rx_time = (header && header->last_heard != 0);

src/modules/StoreForwardModule.cpp:264

  • StoreForwardModule local-to-phone replay sets p->has_rx_time based on when the packet was stored (good), but when has_rx_time is false it still copies time = getTime() into p->rx_time. When RTCQualityNone, getTime() is seconds since boot, but the new reconciliation logic (MeshService::reconcilePendingRxTimes) assumes rx_time placeholders are milliseconds since boot (Time::getMillis()). If the clock becomes trustworthy while these S&F packets are still queued, they can be reconciled to the wrong epoch.

To keep placeholder units consistent, convert the stored boot-seconds value into a millis()-style placeholder when has_rx_time is false.

                p->rx_time = this->packetHistory[i].time;
                p->has_rx_time = this->packetHistory[i].has_rx_time; // presence captured at store time, not replay time

src/mesh/PhoneAPI.cpp:1263

  • pkt.has_rx_time is currently derived from header->last_heard != 0, but NodeDB::addFromContact can stamp last_heard via getTime() (boot-relative seconds) even when RTCQualityNone (see NodeDB.cpp around addFromContact), making last_heard nonzero but not a real epoch. That will cause replay packets to claim a valid timestamp and leak a bogus “epoch” to the phone.

Consider only treating last_heard as an epoch when it is outside the boot-relative wrap range (getTime() when RTCQualityNone stays < ~4,294,967).

This issue also appears in the following locations of the same file:

  • line 1289
  • line 1398
  • line 1466
    pkt.rx_time = header ? header->last_heard : 0;
    // last_heard is only ever written from a has_rx_time-gated reception (NodeDB::updateFrom),
    // so a legacy 0 unambiguously means "never".
    pkt.has_rx_time = (header && header->last_heard != 0);

src/mesh/MeshService.cpp:194

  • reconcilePendingRxTimes() currently rewrites all queued packets with has_rx_time==false, including packets where rx_time==0 is being used as an explicit “unknown/never” sentinel (e.g. replay packets that intentionally have no timestamp). In that case, the unsigned subtraction makes elapsedMs huge and the code will fabricate a bogus epoch timestamp.

Consider skipping reconciliation when rx_time==0 so “unknown” stays unknown; reconciliation should only run on real millis() placeholders captured at receive time.

        if (!p->has_rx_time) {

@NomDeTom
NomDeTom marked this pull request as ready for review July 29, 2026 00:48
@NomDeTom
NomDeTom requested a review from Copilot July 29, 2026 13:44
@NomDeTom

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review, please

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@NomDeTom I’ll review the latest changes and the follow-up fixes.

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

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

Copilot reviewed 25 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

test/test_stream_api/test_main.cpp:643

  • This test depends on time(NULL), so its input varies with the system clock. Prefer a fixed epoch value to keep the unit test deterministic and avoid flakes if CI time changes.
    struct timeval networkTime;
    networkTime.tv_sec = time(NULL) + SEC_PER_DAY;
    networkTime.tv_usec = 0;

test/test_stream_api/test_main.cpp:607

  • This test depends on the host wall clock via time(NULL), which makes it non-deterministic and can introduce flaky behavior (e.g., if system time changes during CI runs). Use a fixed epoch value instead so the test outcome is stable.

This issue also appears on line 641 of the same file.

    struct timeval networkTime;
    networkTime.tv_sec = time(NULL) + SEC_PER_DAY;
    networkTime.tv_usec = 0;

@thebentern
thebentern added this pull request to the merge queue Jul 29, 2026
Merged via the queue into meshtastic:develop with commit 2024bb8 Jul 29, 2026
101 checks passed
@NomDeTom
NomDeTom deleted the arrival-time-fix-perhaps branch August 6, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants