touch: fix the P4 clock — mirror the mesh RTC into the ESP32 system clock

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 <noreply@anthropic.com>
This commit is contained in:
Kaj Schittecat
2026-07-30 13:39:08 +02:00
co-authored by Claude Opus 4.8
parent d6c0321472
commit fe87ff02c5
3 changed files with 57 additions and 7 deletions
+35
View File
@@ -1,6 +1,7 @@
#pragma once
#include <helpers/AutoDiscoverRTCClock.h>
#include <sys/time.h>
// 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.
+20 -6
View File
@@ -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
}
+2 -1
View File
@@ -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. */