diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c9a18ab0..774c0ad7 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -112,6 +112,8 @@ STR_SYNCING_CLOCK: "Syncing clock..." STR_DETECTING_TIMEZONE: "Detecting timezone..." STR_TIME_SYNCED: "Time synced" STR_TIME_SYNC_FAILED: "Time sync failed" +STR_CLOCK_DRIFT: "Drift: %s" +STR_LAST_NTP_SYNC: "Last sync: %s" STR_TIMEZONE_DETECTED: "Timezone detected" STR_TIMEZONE_DETECT_FAILED: "Timezone detect failed" STR_DST_ACTIVE: "DST: active" diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index f7fec425..43806689 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -227,6 +227,8 @@ bool isSynced() { bool isApproximate() { return clockApproximate; } +time_t lastSyncTime() { return nvsReadSyncTime(); } + void formatTime(char* buf, size_t bufSize, bool use24h) { if (!isSynced()) { snprintf(buf, bufSize, "--:--"); diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index cdb49604..a96d230f 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -55,6 +55,10 @@ bool isSynced(); /// may have drifted. Cleared on NTP sync. bool isApproximate(); +/// Returns the epoch of the last successful NTP sync (from NVS), or 0 if +/// no sync has ever been recorded. +time_t lastSyncTime(); + /// Format the current time for display. Returns "--:--" if the clock was /// never synced, prefixes with "~" if approximate. /// When use24h is false, formats as "2:05pm" / "12:30am". diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp index fc81f3aa..53ae4f28 100644 --- a/src/activities/settings/SyncTimeActivity.cpp +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -6,12 +6,48 @@ #include #include +#include + #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" +static void formatDuration(char* buf, size_t bufSize, int32_t totalSeconds) { + const char* sign = totalSeconds < 0 ? "-" : "+"; + int32_t abs = totalSeconds < 0 ? -totalSeconds : totalSeconds; + + int32_t days = abs / 86400; + int32_t hours = (abs % 86400) / 3600; + int32_t mins = (abs % 3600) / 60; + int32_t secs = abs % 60; + + if (days > 0) { + snprintf(buf, bufSize, "%s%ldd %ldh %ldm", sign, (long)days, (long)hours, (long)mins); + } else if (hours > 0) { + snprintf(buf, bufSize, "%s%ldh %ldm %lds", sign, (long)hours, (long)mins, (long)secs); + } else if (mins > 0) { + snprintf(buf, bufSize, "%s%ldm %lds", sign, (long)mins, (long)secs); + } else { + snprintf(buf, bufSize, "%s%lds", sign, (long)secs); + } +} + +static void formatElapsed(char* buf, size_t bufSize, int32_t totalSeconds) { + int32_t days = totalSeconds / 86400; + int32_t hours = (totalSeconds % 86400) / 3600; + int32_t mins = (totalSeconds % 3600) / 60; + + if (days > 0) { + snprintf(buf, bufSize, "%ldd %ldh ago", (long)days, (long)hours); + } else if (hours > 0) { + snprintf(buf, bufSize, "%ldh %ldm ago", (long)hours, (long)mins); + } else { + snprintf(buf, bufSize, "%ldm ago", (long)mins); + } +} + void SyncTimeActivity::onEnter() { Activity::onEnter(); @@ -54,7 +90,16 @@ void SyncTimeActivity::onWifiSelectionComplete(bool success) { void SyncTimeActivity::onWifiSelectionCancelled() { finish(); } void SyncTimeActivity::performSync() { + hadTimeBeforeSync = HalClock::isSynced(); + preSyncTime = hadTimeBeforeSync ? time(nullptr) : 0; + prevSyncTime = HalClock::lastSyncTime(); + bool ok = HalClock::syncNtp(); + + if (ok && hadTimeBeforeSync) { + driftSeconds = (int32_t)(time(nullptr) - preSyncTime); + } + HalClock::wifiOff(true); state = ok ? SUCCESS : FAILED; @@ -76,7 +121,8 @@ void SyncTimeActivity::render(RenderLock&&) { } if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); + int y = pageHeight / 2 - 40; + renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); time_t now = HalClock::now(); struct tm timeinfo; @@ -86,7 +132,45 @@ void SyncTimeActivity::render(RenderLock&&) { char timeStr[32]; snprintf(timeStr, sizeof(timeStr), "%s %04d-%02d-%02d", timePart, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, timeStr); + y += 30; + renderer.drawCenteredText(UI_10_FONT_ID, y, timeStr); + + int32_t elapsedSinceSync = -1; + if (prevSyncTime > 0) { + elapsedSinceSync = (int32_t)(now - prevSyncTime); + } + + char driftStr[80]; + if (hadTimeBeforeSync) { + char driftFmt[24]; + formatDuration(driftFmt, sizeof(driftFmt), driftSeconds); + + if (elapsedSinceSync > 0) { + double hours = (double)elapsedSinceSync / 3600.0; + double rate = (double)driftSeconds / hours; + char rateFmt[16]; + snprintf(rateFmt, sizeof(rateFmt), "%+.2f", rate); + + char driftWithRate[48]; + snprintf(driftWithRate, sizeof(driftWithRate), "%s (%s s/hr)", driftFmt, rateFmt); + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftWithRate); + } else { + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftFmt); + } + } else { + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), "N/A"); + } + y += 30; + renderer.drawCenteredText(UI_10_FONT_ID, y, driftStr); + + if (elapsedSinceSync > 0) { + char elapsedFmt[24]; + formatElapsed(elapsedFmt, sizeof(elapsedFmt), elapsedSinceSync); + char lastSyncStr[48]; + snprintf(lastSyncStr, sizeof(lastSyncStr), tr(STR_LAST_NTP_SYNC), elapsedFmt); + y += 25; + renderer.drawCenteredText(UI_10_FONT_ID, y, lastSyncStr); + } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/SyncTimeActivity.h b/src/activities/settings/SyncTimeActivity.h index 43a419fb..08f2eecd 100644 --- a/src/activities/settings/SyncTimeActivity.h +++ b/src/activities/settings/SyncTimeActivity.h @@ -15,6 +15,10 @@ class SyncTimeActivity final : public Activity { private: enum State { CONNECTING, SYNCING, SUCCESS, FAILED }; State state = CONNECTING; + time_t preSyncTime = 0; + time_t prevSyncTime = 0; + int32_t driftSeconds = 0; + bool hadTimeBeforeSync = false; void onWifiSelectionComplete(bool success); void onWifiSelectionCancelled(); void performSync();