feat: Add NTP clock sync to Clock settings
Adds a "Sync Clock" action in Settings > Clock that connects to WiFi (auto-connecting to saved networks or prompting for selection) and performs a blocking NTP time sync. Shows the synced time on success with an auto-dismiss countdown, or an error on failure. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -398,6 +398,8 @@ enum class StrId : uint16_t {
|
|||||||
STR_INDEXING_POPUP,
|
STR_INDEXING_POPUP,
|
||||||
STR_INDEXING_STATUS_TEXT,
|
STR_INDEXING_STATUS_TEXT,
|
||||||
STR_INDEXING_STATUS_ICON,
|
STR_INDEXING_STATUS_ICON,
|
||||||
|
STR_SYNC_CLOCK,
|
||||||
|
STR_TIME_SYNCED,
|
||||||
// Sentinel - must be last
|
// Sentinel - must be last
|
||||||
_COUNT
|
_COUNT
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Zobrazení indexování"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
||||||
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -362,3 +362,5 @@ STR_INDEXING_DISPLAY: "Indexing Display"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
||||||
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Affichage indexation"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
||||||
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexierungsanzeige"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
||||||
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Exibição de indexação"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
||||||
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -316,3 +316,5 @@ STR_UPLOAD: "Încărcare"
|
|||||||
STR_BOOK_S_STYLE: "Stilul cărţii"
|
STR_BOOK_S_STYLE: "Stilul cărţii"
|
||||||
STR_EMBEDDED_STYLE: "Stil încorporat"
|
STR_EMBEDDED_STYLE: "Stil încorporat"
|
||||||
STR_OPDS_SERVER_URL: "URL server OPDS"
|
STR_OPDS_SERVER_URL: "URL server OPDS"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Отображение индексации"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
||||||
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Mostrar indexación"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
||||||
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexeringsvisning"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
||||||
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#include "NtpSyncActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#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<int>((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<int>(elapsed / 1000);
|
||||||
|
if (currentSecond != lastCountdownSecond) {
|
||||||
|
lastCountdownSecond = currentSecond;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/activities/settings/NtpSyncActivity.h
Normal file
24
src/activities/settings/NtpSyncActivity.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
class NtpSyncActivity : public ActivityWithSubactivity {
|
||||||
|
enum State { WIFI_SELECTION, SYNCING, SUCCESS, FAILED };
|
||||||
|
|
||||||
|
const std::function<void()> 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<void()>& 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; }
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "LanguageSelectActivity.h"
|
#include "LanguageSelectActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "NtpSyncActivity.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "SetTimeActivity.h"
|
#include "SetTimeActivity.h"
|
||||||
#include "SetTimezoneOffsetActivity.h"
|
#include "SetTimezoneOffsetActivity.h"
|
||||||
@@ -221,6 +222,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::SetTimezoneOffset:
|
case SettingAction::SetTimezoneOffset:
|
||||||
enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete));
|
enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete));
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::SyncClock:
|
||||||
|
enterSubActivity(new NtpSyncActivity(renderer, mappedInput, onComplete));
|
||||||
|
break;
|
||||||
case SettingAction::None:
|
case SettingAction::None:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
break;
|
break;
|
||||||
@@ -245,7 +249,8 @@ void SettingsActivity::rebuildClockActions() {
|
|||||||
[](const SettingInfo& s) { return s.type == SettingType::ACTION; }),
|
[](const SettingInfo& s) { return s.type == SettingType::ACTION; }),
|
||||||
clockSettings.end());
|
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));
|
clockSettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime));
|
||||||
|
|
||||||
// Only add Set UTC Offset when timezone is set to Custom
|
// Only add Set UTC Offset when timezone is set to Custom
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ enum class SettingAction {
|
|||||||
Language,
|
Language,
|
||||||
SetTime,
|
SetTime,
|
||||||
SetTimezoneOffset,
|
SetTimezoneOffset,
|
||||||
|
SyncClock,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user