From fe87ff02c519b93a37f81cc52629a84b2559c25d Mon Sep 17 00:00:00 2001 From: Kaj Schittecat Date: Thu, 30 Jul 2026 13:39:08 +0200 Subject: [PATCH] =?UTF-8?q?touch:=20fix=20the=20P4=20clock=20=E2=80=94=20m?= =?UTF-8?q?irror=20the=20mesh=20RTC=20into=20the=20ESP32=20system=20clock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI reads the ESP32 *system* clock for every displayed timestamp (status bar, chat bubbles) while protocol timestamps come from ClockFloorRTC. On a board with no RTC chip those are the same clock, so they cannot disagree. The T-Display P4 (and the ThinkNode M9) carry a PCF8563, and AutoDiscoverRTCClock::setCurrentTime is an if/else chain: with a chip present it writes ONLY the chip and never reaches the ESP32RTCClock fallback. So the system clock kept ESP32RTCClock::begin()'s power-on seed forever. That seed is 1715770351 == exactly ClockFloorRTC::MIN_VALID_EPOCH, which explains both halves of the P4 field report: - the UI showed 15 May 2024 while sent messages carried the correct time - "Sync clock from system" then poisoned the good clock: it fed that seed back in as a real set and cleared the MIN check by being precisely equal to it (the old `t < 100000` guard only caught a clock counting up from 0) Fixed in the one funnel every time source already goes through: - ClockFloorRTC::setCurrentTime mirrors every ACCEPTED value into the system clock, so NTP-via-C6, GPS and the phone app all correct the display too. Validation runs first, so 1902/2043-class garbage never reaches it either. - new seedSystemClock(), called at boot right after seedFloor(): a battery-backed chip already knows the time, so the UI reads correctly with no network and no GPS fix. After seedFloor, so a dead chip yields the persisted floor rather than the seed. - the sync button rejects an untouched seed and now reports honestly instead of always claiming "Clock synced". No-op on the seven chipless boards: the fallback already wrote the same value through settimeofday. GPS re-syncs are rate-capped to ~30 min, so the mirror adds no measurable work. Builds clean on all 8 S3 envs + the P4 IDF target; compile-verified only, the P4 is still off the USB bus pending a power cycle. Co-Authored-By: Claude Opus 4.8 --- src/helpers/ClockFloorRTC.h | 35 +++++++++++++++++++++++++++++++++++ src/ui-touch/UITask.cpp | 26 ++++++++++++++++++++------ src/ui-touch/UITask.h | 3 ++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/helpers/ClockFloorRTC.h b/src/helpers/ClockFloorRTC.h index 7163786..934e985 100644 --- a/src/helpers/ClockFloorRTC.h +++ b/src/helpers/ClockFloorRTC.h @@ -1,6 +1,7 @@ #pragma once #include +#include // Monotonic clock floor for outgoing protocol timestamps (issue #89 follow-up). // @@ -60,9 +61,43 @@ public: void setCurrentTime(uint32_t time) override { if (time < MIN_VALID_EPOCH || time > MAX_PLAUSIBLE_EPOCH) return; // garbage set — ignore, whatever the source AutoDiscoverRTCClock::setCurrentTime(time); + pushSystemClock(time); if (_floor > time + TRUSTED_BACK_CAP) _floor = time; } + // The UI reads the ESP32 *system* clock for every displayed time (status bar, chat + // bubbles, logs — see the note at addMessage()), while protocol timestamps come from + // this class. On a board with NO RTC chip they are the same clock: AutoDiscoverRTCClock + // falls through to ESP32RTCClock, whose set IS settimeofday(), so the two can never + // disagree. With a chip present (the T-Display P4 and the ThinkNode M9 both carry a + // PCF8563 at 0x51 and call rtc_clock.begin(); the T-LoRa Pager has one too but + // deliberately skips begin(), since its PCF85063A is register-incompatible with the + // core's 8563 driver) the base class writes ONLY the chip, so the system clock keeps + // ESP32RTCClock::begin()'s power-on seed forever. That seed is + // exactly MIN_VALID_EPOCH, which is why the P4 showed 15 May 2024 in the UI while sent + // messages carried the correct time — and why "Sync clock from system" then poisoned + // the good clock: it fed that seed back in as a real set, clearing the MIN check by + // being precisely equal to it. + // + // So mirror every ACCEPTED value into the system clock. The validation above has + // already run, so 1902/2043-class garbage never reaches the display either. + static void pushSystemClock(uint32_t t) { + struct timeval tv; + tv.tv_sec = (time_t)t; + tv.tv_usec = 0; + settimeofday(&tv, nullptr); + } + + // Boot seed. An RTC chip is battery-backed, so it already knows the time while the + // system clock is still on the power-on seed — pull it across once so the UI reads + // correctly with no network and no GPS fix. Goes through getCurrentTime(), so the + // garbage-future hatch and the persisted floor both apply. No-op on the chipless + // boards (it reads the system clock and writes the same value back). + void seedSystemClock() { + const uint32_t t = getCurrentTime(); + if (t >= MIN_VALID_EPOCH && t <= MAX_PLAUSIBLE_EPOCH) pushSystemClock(t); + } + // Boot-time seed from the persisted floor (only ever raises), and the getter // the persister reads back. Benign u32 races: both cores may touch _floor. // A poisoned persisted value (saved while the hw clock asserted garbage) is refused. diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index a5b4ac4..759b8a3 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -8596,8 +8596,8 @@ static void saveExperimentalCb(lv_event_t* e) { static void syncClockCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; - g_lv.task->setDeviceTimeFromSystemClock(); - g_lv.task->showAlert(TR("Clock synced"), 900); + const bool ok = g_lv.task->setDeviceTimeFromSystemClock(); + g_lv.task->showAlert(ok ? TR("Clock synced") : TR("No system time yet"), 900); } // Chat-save fallback dropdown (Settings -> General): selection index -> number @@ -43667,6 +43667,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // backwards (server replay guards silently drop those). Seed before the UI // generates traffic; written back rate-capped in loop() + on shutdown(). rtc_clock.seedFloor(touchPrefsGetClockFloor()); + // ...then push the resulting time into the ESP32 system clock, which is what every + // displayed timestamp reads. On a board with a battery-backed RTC chip (P4) the chip + // knows the time but the system clock is still on its power-on seed, so the UI would + // otherwise show 15 May 2024 until a network/GPS sync landed. Order matters: after + // seedFloor(), so a dead chip still yields the persisted floor instead of the seed. + rtc_clock.seedSystemClock(); #endif // GPS resume: initBasicGPS() always leaves the module stopped at boot, so a @@ -45096,13 +45102,21 @@ bool UITask::setWifiRadio(bool on) { return false; } -void UITask::setDeviceTimeFromSystemClock() { +bool UITask::setDeviceTimeFromSystemClock() { #if defined(ESP32) - time_t t = time(nullptr); - if (t < 100000) return; - the_mesh.getRTCClock()->setCurrentTime((uint32_t)t); + const uint32_t t = (uint32_t)time(nullptr); + // ESP32RTCClock::begin() seeds the system clock to exactly MIN_VALID_EPOCH on power-on, + // so "still at the seed" means never synced. Pushing that into the mesh clock is a + // two-year step backwards that setCurrentTime()'s own MIN check cannot catch, because + // the value IS the constant it compares against — that is how a P4 (whose RTC chip left + // the system clock on the seed) could sync 2024 over a perfectly good clock. The old + // `t < 100000` guard only caught a clock counting up from zero, which never happens here. + if (t <= ClockFloorRTC::MIN_VALID_EPOCH) return false; + the_mesh.getRTCClock()->setCurrentTime(t); + return true; #else the_mesh.getRTCClock()->setCurrentTime((uint32_t)(millis() / 1000)); + return true; #endif } diff --git a/src/ui-touch/UITask.h b/src/ui-touch/UITask.h index d52a4f2..a2ec856 100644 --- a/src/ui-touch/UITask.h +++ b/src/ui-touch/UITask.h @@ -464,7 +464,8 @@ public: bool enableBle(); void disableBle() { if (_serial) _serial->disableBle(); } int getWsConnectedCount() const { return _serial ? _serial->getWsConnectedCount() : 0; } - void setDeviceTimeFromSystemClock(); + /** Push the ESP32 system clock into the mesh RTC. false = never synced, mesh clock untouched. */ + bool setDeviceTimeFromSystemClock(); /** Mark recent user input — call when touch / hw button is detected. */ void noteUserInput(); /** Get / set the screen-off-after-idle timeout (0 = never). Persists in NVS. */