Arrival time fix perhaps (#11274)

* Add explicit presence for MeshPacket.rx_time (arrival time)

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.

* Dedupe rx_time stamping into a shared helper; trim a debug log string

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.

* Fix has_rx_rssi presence carried unconditionally through StoreForward 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 #11271 (same root cause the has_rx_time explicit
presence work fixes) but never addressed before that PR merged.

* Trim comment blocks to the repo's 1-2 line guideline

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

* Add rx_time explicit-presence test coverage

- 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().

* Fix three correctness issues flagged in review

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

* tryfix CI

* post review fixes

* more test fixes
This commit is contained in:
Tom authored and GitHub committed 2026-07-29 14:03:25 +00:00
1 parent 0fef83d434
commit 2024bb8384
26 files changed
+436 -32

No files matched your search

+19
View File
@@ -1238,6 +1238,17 @@ void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLit
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
}
/// 2020-01-01: a boot-relative counter needs ~50 years of uptime to reach this, so it cannot be
/// confused with a real epoch.
constexpr uint32_t MIN_PLAUSIBLE_EPOCH = 1577836800u;
/// Not every last_heard writer gates on RTC quality - NodeDB::addFromContact stamps it with a bare
/// getTime(), which is boot-relative seconds on a node that has never had a clock.
bool lastHeardIsWallClock(const meshtastic_NodeInfoLite *header)
{
return header && header->last_heard >= MIN_PLAUSIBLE_EPOCH;
}
} // namespace
// Replayed packets deliberately leave rx_rssi absent. NodeInfoLite stores no RSSI, and
@@ -1258,6 +1269,8 @@ meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const mesh
// fix time (which is often 0 and, when present, already round-trips inside the payload
// via ConvertToPosition).
pkt.rx_time = header ? header->last_heard : 0;
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
pkt.has_rx_time = lastHeardIsWallClock(header);
// Stable per-node/per-fix id: replaying the same unchanged history on every
// reconnect must not look like a brand new packet to the phone's history/dedup.
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
@@ -1285,6 +1298,8 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
// No native timestamp on telemetry packets here; use last_heard.
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
pkt.has_rx_time = lastHeardIsWallClock(header);
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
pkt.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
@@ -1391,6 +1406,8 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
pkt.to = NODENUM_BROADCAST;
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
pkt.has_rx_time = lastHeardIsWallClock(header);
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
pkt.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
@@ -1456,6 +1473,8 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
// StatusMessage has no native timestamp; use last_heard.
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
pkt.has_rx_time = lastHeardIsWallClock(header);
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
pkt.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;