From 02498609dbed3d32542264dbd9ac0d180be0fc09 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 9 Jan 2026 01:38:07 +0100 Subject: [PATCH 1/2] Allow setting time to the future Especially with powersaving on the RTC can run hours ahead after only a few days. --- examples/companion_radio/MyMesh.cpp | 9 ++------- examples/simple_secure_chat/main.cpp | 9 ++------- src/helpers/CommonCLI.cpp | 26 ++++++++------------------ 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7689708c0..82e0106ae 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1003,13 +1003,8 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { uint32_t secs; memcpy(&secs, &cmd_frame[1], 4); - uint32_t curr = getRTCClock()->getCurrentTime(); - if (secs >= curr) { - getRTCClock()->setCurrentTime(secs); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } + getRTCClock()->setCurrentTime(secs); + writeOKFrame(); } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { mesh::Packet* pkt; if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da1bac5b3..02ecf69ce 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -158,13 +158,8 @@ class MyMesh : public BaseChatMesh, ContactVisitor { } void setClock(uint32_t timestamp) { - uint32_t curr = getRTCClock()->getCurrentTime(); - if (timestamp > curr) { - getRTCClock()->setCurrentTime(timestamp); - Serial.println(" (OK - clock set!)"); - } else { - Serial.println(" (ERR: clock cannot go backwards)"); - } + getRTCClock()->setCurrentTime(timestamp); + Serial.println(" (OK - clock set!)"); } void importCard(const char* command) { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 78e1b5e0b..ea804f853 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -190,15 +190,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _callbacks->sendSelfAdvertisement(1500); // longer delay, give CLI response time to be sent first strcpy(reply, "OK - Advert sent"); } else if (memcmp(command, "clock sync", 10) == 0) { - uint32_t curr = getRTCClock()->getCurrentTime(); - if (sender_timestamp > curr) { - getRTCClock()->setCurrentTime(sender_timestamp + 1); - uint32_t now = getRTCClock()->getCurrentTime(); - DateTime dt = DateTime(now); - sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); - } else { - strcpy(reply, "ERR: clock cannot go backwards"); - } + getRTCClock()->setCurrentTime(sender_timestamp + 1); + uint32_t now = getRTCClock()->getCurrentTime(); + DateTime dt = DateTime(now); + sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "start ota", 9) == 0) { if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); @@ -209,15 +204,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); - uint32_t curr = getRTCClock()->getCurrentTime(); - if (secs > curr) { - getRTCClock()->setCurrentTime(secs); - uint32_t now = getRTCClock()->getCurrentTime(); - DateTime dt = DateTime(now); - sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); - } else { - strcpy(reply, "(ERR: clock cannot go backwards)"); - } + getRTCClock()->setCurrentTime(secs); + uint32_t now = getRTCClock()->getCurrentTime(); + DateTime dt = DateTime(now); + sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "neighbors", 9) == 0) { _callbacks->formatNeighborsReply(reply); } else if (memcmp(command, "neighbor.remove ", 16) == 0) { From af368648e278e7828bc8692cf8ca71f9cba110e6 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 9 Jan 2026 19:11:54 +0100 Subject: [PATCH 2/2] Fix timestamp calculations when RTC clock is corrected backwards When the RTC drifts ahead and is corrected via clock sync, stored timestamps can appear to be in the future, causing underflow in "time ago" calculations (wrapping to ~4 billion seconds). Changes: - Add safeElapsedSecs() helper that clamps to 0 if timestamp > now - Apply to neighbor "heard X ago" displays in simple_repeater - Apply to UI time displays in companion_radio - Apply to TimeSeriesData calculations in simple_sensor - Switch BaseChatMesh connection expiry from RTC to millis() The connection expiry change is the most important: using monotonic time (millis) makes it immune to RTC adjustments from GPS, NTP, or manual sync. --- examples/companion_radio/ui-new/UITask.cpp | 4 ++-- examples/simple_repeater/MyMesh.cpp | 4 ++-- examples/simple_sensor/TimeSeriesData.cpp | 3 ++- src/helpers/ArduinoHelpers.h | 9 +++++++++ src/helpers/BaseChatMesh.cpp | 16 ++++++++++------ 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8077627f8..cc26320cf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -219,7 +219,7 @@ class HomeScreen : public UIScreen { for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; if (a->name[0] == 0) continue; // empty slot - int secs = _rtc->getCurrentTime() - a->recv_timestamp; + uint32_t secs = safeElapsedSecs(_rtc->getCurrentTime(), a->recv_timestamp); if (secs < 60) { sprintf(tmp, "%ds", secs); } else if (secs < 60*60) { @@ -480,7 +480,7 @@ class MsgPreviewScreen : public UIScreen { auto p = &unread[0]; - int secs = _rtc->getCurrentTime() - p->timestamp; + uint32_t secs = safeElapsedSecs(_rtc->getCurrentTime(), p->timestamp); if (secs < 60) { sprintf(tmp, "%ds", secs); } else if (secs < 60*60) { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 33e32a68a..f147d711c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -280,7 +280,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t // add next neighbour to results auto neighbour = sorted_neighbours[index + offset]; - uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + uint32_t heard_seconds_ago = safeElapsedSecs(getRTCClock()->getCurrentTime(), neighbour->heard_timestamp); memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length); results_offset += pubkey_prefix_length; memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4; memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1; @@ -852,7 +852,7 @@ void MyMesh::formatNeighborsReply(char *reply) { mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); // add next neighbour - uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + uint32_t secs_ago = safeElapsedSecs(getRTCClock()->getCurrentTime(), neighbour->heard_timestamp); sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr); while (*dp) dp++; // find end of string diff --git a/examples/simple_sensor/TimeSeriesData.cpp b/examples/simple_sensor/TimeSeriesData.cpp index f6157f9af..c780c7de2 100644 --- a/examples/simple_sensor/TimeSeriesData.cpp +++ b/examples/simple_sensor/TimeSeriesData.cpp @@ -1,4 +1,5 @@ #include "TimeSeriesData.h" +#include void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) { uint32_t now = clock->getCurrentTime(); @@ -12,7 +13,7 @@ void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) { void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const { int i = next, n = num_slots; - uint32_t ago = clock->getCurrentTime() - last_timestamp; + uint32_t ago = safeElapsedSecs(clock->getCurrentTime(), last_timestamp); int num_values = 0; float total = 0.0f; diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 97596daa3..5f12e5b01 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -3,6 +3,15 @@ #include #include +// Safe elapsed time calculation that handles clock corrections (when RTC is set backwards). +// Returns 0 if recorded_timestamp is in the "future" relative to current_time. +inline uint32_t safeElapsedSecs(uint32_t current_time, uint32_t recorded_timestamp) { + if (recorded_timestamp > current_time) { + return 0; // Clock was corrected backwards; treat as "just now" + } + return current_time - recorded_timestamp; +} + class VolatileRTCClock : public mesh::RTCClock { uint32_t base_time; uint64_t accumulator; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 597444fa7..7207e95c7 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -548,7 +548,7 @@ bool BaseChatMesh::startConnection(const ContactInfo& contact, uint16_t keep_ali uint32_t interval = connections[use_idx].keep_alive_millis = ((uint32_t)keep_alive_secs)*1000; connections[use_idx].next_ping = futureMillis(interval); connections[use_idx].expected_ack = 0; - connections[use_idx].last_activity = getRTCClock()->getCurrentTime(); + connections[use_idx].last_activity = _ms->getMillis(); // use monotonic time for connection expiry return true; // success } @@ -574,7 +574,7 @@ bool BaseChatMesh::hasConnectionTo(const uint8_t* pub_key) { void BaseChatMesh::markConnectionActive(const ContactInfo& contact) { for (int i = 0; i < MAX_CONNECTIONS; i++) { if (connections[i].keep_alive_millis > 0 && connections[i].server_id.matches(contact.id)) { - connections[i].last_activity = getRTCClock()->getCurrentTime(); + connections[i].last_activity = _ms->getMillis(); // use monotonic time for connection expiry // re-schedule next KEEP_ALIVE, now that we have heard from server connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); @@ -588,7 +588,7 @@ ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) { if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) { // yes, got an ack for our keep_alive request! connections[i].expected_ack = 0; - connections[i].last_activity = getRTCClock()->getCurrentTime(); + connections[i].last_activity = _ms->getMillis(); // use monotonic time for connection expiry // re-schedule next KEEP_ALIVE, now that we have heard from server connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); @@ -605,9 +605,13 @@ void BaseChatMesh::checkConnections() { for (int i = 0; i < MAX_CONNECTIONS; i++) { if (connections[i].keep_alive_millis == 0) continue; // unused slot - uint32_t now = getRTCClock()->getCurrentTime(); - uint32_t expire_secs = (connections[i].keep_alive_millis / 1000) * 5 / 2; // 2.5 x keep_alive interval - if (now >= connections[i].last_activity + expire_secs) { + // Use monotonic time (millis) for connection expiry - immune to RTC clock changes. + // Note: This assumes light sleep mode where millis() continues to increment. + // Deep sleep would reset millis(), but BaseChatMesh is only used by companion_radio + // which uses light sleep. + unsigned long now = _ms->getMillis(); + uint32_t expire_millis = (connections[i].keep_alive_millis * 5) / 2; // 2.5 x keep_alive interval + if ((now - connections[i].last_activity) >= expire_millis) { // connection now lost connections[i].keep_alive_millis = 0; connections[i].next_ping = 0;