diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 76678b5c..51d258a4 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -398,6 +398,8 @@ enum class StrId : uint16_t { STR_INDEXING_POPUP, STR_INDEXING_STATUS_TEXT, STR_INDEXING_STATUS_ICON, + STR_SYNC_CLOCK, + STR_TIME_SYNCED, // Sentinel - must be last _COUNT }; diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index b3ad4281..ca1a6163 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Zobrazení indexování" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Text stavového řádku" STR_INDEXING_STATUS_ICON: "Ikona stavového řádku" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index dd20d71d..d7b34887 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -362,3 +362,5 @@ STR_INDEXING_DISPLAY: "Indexing Display" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Status Bar Text" STR_INDEXING_STATUS_ICON: "Status Bar Icon" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index ea00e876..a2b09f17 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Affichage indexation" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Texte barre d'état" STR_INDEXING_STATUS_ICON: "Icône barre d'état" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 862e3d17..b0a8413b 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexierungsanzeige" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Statusleistentext" STR_INDEXING_STATUS_ICON: "Statusleistensymbol" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index a90f2395..310b282d 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Exibição de indexação" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Texto da barra" STR_INDEXING_STATUS_ICON: "Ícone da barra" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 90acc6a7..fa55025d 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -316,3 +316,5 @@ STR_UPLOAD: "Încărcare" STR_BOOK_S_STYLE: "Stilul cărţii" STR_EMBEDDED_STYLE: "Stil încorporat" STR_OPDS_SERVER_URL: "URL server OPDS" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 2cd37e09..e138f7c1 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Отображение индексации" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Текст в строке" STR_INDEXING_STATUS_ICON: "Иконка в строке" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index d61941a4..21c4c060 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Mostrar indexación" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Texto barra estado" STR_INDEXING_STATUS_ICON: "Icono barra estado" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 57bfb20d..22afe609 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexeringsvisning" STR_INDEXING_POPUP: "Popup" STR_INDEXING_STATUS_TEXT: "Statusfältstext" STR_INDEXING_STATUS_ICON: "Statusfältsikon" +STR_SYNC_CLOCK: "Sync Clock" +STR_TIME_SYNCED: "Time synced!" diff --git a/src/activities/settings/NtpSyncActivity.cpp b/src/activities/settings/NtpSyncActivity.cpp new file mode 100644 index 00000000..f4b2116c --- /dev/null +++ b/src/activities/settings/NtpSyncActivity.cpp @@ -0,0 +1,150 @@ +#include "NtpSyncActivity.h" + +#include +#include +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "activities/network/WifiSelectionActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" +#include "util/TimeSync.h" + +static constexpr unsigned long AUTO_DISMISS_MS = 5000; + +void NtpSyncActivity::onWifiSelectionComplete(const bool success) { + exitActivity(); + + if (!success) { + LOG_ERR("NTP", "WiFi connection failed, exiting"); + goBack(); + return; + } + + LOG_DBG("NTP", "WiFi connected, starting NTP sync"); + + { + RenderLock lock(*this); + state = SYNCING; + } + requestUpdateAndWait(); + + const bool synced = TimeSync::waitForNtpSync(8000); + + { + RenderLock lock(*this); + state = synced ? SUCCESS : FAILED; + if (synced) { + successTimestamp = millis(); + } + } + requestUpdate(); + + if (synced) { + LOG_DBG("NTP", "Time synced successfully"); + } else { + LOG_ERR("NTP", "NTP sync timed out"); + } +} + +void NtpSyncActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + LOG_DBG("NTP", "Turning on WiFi..."); + WiFi.mode(WIFI_STA); + + LOG_DBG("NTP", "Launching WifiSelectionActivity..."); + enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, + [this](const bool connected) { onWifiSelectionComplete(connected); })); +} + +void NtpSyncActivity::onExit() { + ActivityWithSubactivity::onExit(); + + TimeSync::stopNtpSync(); + WiFi.disconnect(false); + delay(100); + WiFi.mode(WIFI_OFF); + delay(100); +} + +void NtpSyncActivity::render(Activity::RenderLock&&) { + if (subActivity) { + return; + } + + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYNC_CLOCK)); + + const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID); + const auto centerY = (pageHeight - lineHeight) / 2; + + if (state == SYNCING) { + renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNCING_TIME)); + } else if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); + + time_t now = time(nullptr); + struct tm* t = localtime(&now); + if (t != nullptr && t->tm_year > 100) { + char timeBuf[32]; + if (SETTINGS.clockFormat == CrossPointSettings::CLOCK_24H) { + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min); + } else { + int hour12 = t->tm_hour % 12; + if (hour12 == 0) hour12 = 12; + snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM"); + } + renderer.drawCenteredText(UI_10_FONT_ID, centerY + lineHeight + metrics.verticalSpacing, timeBuf); + } + + const unsigned long elapsed = millis() - successTimestamp; + const int remaining = static_cast((AUTO_DISMISS_MS - elapsed + 999) / 1000); + char backLabel[32]; + snprintf(backLabel, sizeof(backLabel), "%s (%d)", tr(STR_BACK), remaining > 0 ? remaining : 1); + const auto labels = mappedInput.mapLabels(backLabel, "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } else if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + + renderer.displayBuffer(); +} + +void NtpSyncActivity::loop() { + if (subActivity) { + subActivity->loop(); + return; + } + + if (state == SUCCESS) { + const unsigned long elapsed = millis() - successTimestamp; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || elapsed >= AUTO_DISMISS_MS) { + goBack(); + return; + } + const int currentSecond = static_cast(elapsed / 1000); + if (currentSecond != lastCountdownSecond) { + lastCountdownSecond = currentSecond; + requestUpdate(); + } + return; + } + + if (state == FAILED) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + goBack(); + } + return; + } +} diff --git a/src/activities/settings/NtpSyncActivity.h b/src/activities/settings/NtpSyncActivity.h new file mode 100644 index 00000000..98156e30 --- /dev/null +++ b/src/activities/settings/NtpSyncActivity.h @@ -0,0 +1,24 @@ +#pragma once + +#include "activities/ActivityWithSubactivity.h" + +class NtpSyncActivity : public ActivityWithSubactivity { + enum State { WIFI_SELECTION, SYNCING, SUCCESS, FAILED }; + + const std::function goBack; + State state = WIFI_SELECTION; + unsigned long successTimestamp = 0; + int lastCountdownSecond = -1; + + void onWifiSelectionComplete(bool success); + + public: + explicit NtpSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& goBack) + : ActivityWithSubactivity("NtpSync", renderer, mappedInput), goBack(goBack) {} + void onEnter() override; + void onExit() override; + void loop() override; + void render(Activity::RenderLock&&) override; + bool preventAutoSleep() override { return state == SYNCING; } +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 680896ca..6fb4bc04 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -13,6 +13,7 @@ #include "KOReaderSettingsActivity.h" #include "LanguageSelectActivity.h" #include "MappedInputManager.h" +#include "NtpSyncActivity.h" #include "OtaUpdateActivity.h" #include "SetTimeActivity.h" #include "SetTimezoneOffsetActivity.h" @@ -221,6 +222,9 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::SetTimezoneOffset: enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete)); break; + case SettingAction::SyncClock: + enterSubActivity(new NtpSyncActivity(renderer, mappedInput, onComplete)); + break; case SettingAction::None: // Do nothing break; @@ -245,7 +249,8 @@ void SettingsActivity::rebuildClockActions() { [](const SettingInfo& s) { return s.type == SettingType::ACTION; }), clockSettings.end()); - // Always add Set Time + // Always add Sync Clock and Set Time + clockSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_CLOCK, SettingAction::SyncClock)); clockSettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime)); // Only add Set UTC Offset when timezone is set to Custom diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 8f375ab6..5bebd443 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -23,6 +23,7 @@ enum class SettingAction { Language, SetTime, SetTimezoneOffset, + SyncClock, }; struct SettingInfo {