mod: fix clock bugs, add NTP auto-sync, show clock in all headers
- Fix SetTimeActivity immediately dismissing by changing wasReleased to wasPressed for all button inputs (matching other subactivities) - Extract NTP sync into shared TimeSync utility (startNtpSync, waitForNtpSync, stopNtpSync) and trigger non-blocking NTP sync on every WiFi connection - Move clock rendering into drawHeader (BaseTheme + LyraTheme) so it appears on all screens with a header, positioned symmetrically with the battery icon (12px margin, same Y offset, SMALL_FONT_ID) - Add per-minute auto-refresh on home screen so clock updates without button press - Add RTC time debug log on boot to verify time persistence across deep sleep Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -192,6 +192,19 @@ void HomeActivity::freeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loop() {
|
void HomeActivity::loop() {
|
||||||
|
// Refresh the screen when the displayed minute changes (clock update)
|
||||||
|
if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
if (t != nullptr && t->tm_year > 100) {
|
||||||
|
const int currentMinute = t->tm_hour * 60 + t->tm_min;
|
||||||
|
if (lastRenderedMinute >= 0 && currentMinute != lastRenderedMinute) {
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
lastRenderedMinute = currentMinute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
buttonNavigator.onNext([this, menuCount] {
|
buttonNavigator.onNext([this, menuCount] {
|
||||||
@@ -240,23 +253,6 @@ void HomeActivity::render(Activity::RenderLock&&) {
|
|||||||
|
|
||||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr);
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr);
|
||||||
|
|
||||||
// Draw clock in the header area (left side) if enabled
|
|
||||||
if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) {
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm* t = localtime(&now);
|
|
||||||
if (t != nullptr && t->tm_year > 100) {
|
|
||||||
char timeBuf[16];
|
|
||||||
if (SETTINGS.homeScreenClock == 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.drawText(SMALL_FONT_ID, metrics.contentSidePadding, metrics.topPadding, timeBuf, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
|
GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
|
||||||
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
|
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
|
||||||
std::bind(&HomeActivity::storeCoverBuffer, this));
|
std::bind(&HomeActivity::storeCoverBuffer, this));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class HomeActivity final : public Activity {
|
|||||||
bool recentsLoaded = false;
|
bool recentsLoaded = false;
|
||||||
bool firstRenderDone = false;
|
bool firstRenderDone = false;
|
||||||
bool hasOpdsUrl = false;
|
bool hasOpdsUrl = false;
|
||||||
|
int lastRenderedMinute = -1; // Track displayed minute for clock auto-update
|
||||||
bool coverRendered = false; // Track if cover has been rendered once
|
bool coverRendered = false; // Track if cover has been rendered once
|
||||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "activities/util/KeyboardEntryActivity.h"
|
#include "activities/util/KeyboardEntryActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/TimeSync.h"
|
||||||
|
|
||||||
void WifiSelectionActivity::onEnter() {
|
void WifiSelectionActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
@@ -243,6 +244,9 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
autoConnecting = false;
|
autoConnecting = false;
|
||||||
|
|
||||||
|
// Start NTP time sync in the background (non-blocking)
|
||||||
|
TimeSync::startNtpSync();
|
||||||
|
|
||||||
// Save this as the last connected network - SD card operations need lock as
|
// Save this as the last connected network - SD card operations need lock as
|
||||||
// we use SPI for both
|
// we use SPI for both
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_sntp.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
#include "KOReaderDocumentId.h"
|
#include "KOReaderDocumentId.h"
|
||||||
@@ -12,34 +11,7 @@
|
|||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/TimeSync.h"
|
||||||
namespace {
|
|
||||||
void syncTimeWithNTP() {
|
|
||||||
// Stop SNTP if already running (can't reconfigure while running)
|
|
||||||
if (esp_sntp_enabled()) {
|
|
||||||
esp_sntp_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure SNTP
|
|
||||||
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
|
||||||
esp_sntp_setservername(0, "pool.ntp.org");
|
|
||||||
esp_sntp_init();
|
|
||||||
|
|
||||||
// Wait for time to sync (with timeout)
|
|
||||||
int retry = 0;
|
|
||||||
const int maxRetries = 50; // 5 seconds max
|
|
||||||
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
retry++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry < maxRetries) {
|
|
||||||
LOG_DBG("KOSync", "NTP time synced");
|
|
||||||
} else {
|
|
||||||
LOG_DBG("KOSync", "NTP sync timeout, using fallback");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
@@ -59,8 +31,8 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
}
|
}
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|
||||||
// Sync time with NTP before making API requests
|
// Wait for NTP sync before making API requests (blocks up to 5s)
|
||||||
syncTimeWithNTP();
|
TimeSync::waitForNtpSync();
|
||||||
|
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
@@ -199,8 +171,8 @@ void KOReaderSyncActivity::onEnter() {
|
|||||||
xTaskCreate(
|
xTaskCreate(
|
||||||
[](void* param) {
|
[](void* param) {
|
||||||
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
||||||
// Sync time first
|
// Wait for NTP sync before making API requests
|
||||||
syncTimeWithNTP();
|
TimeSync::waitForNtpSync();
|
||||||
{
|
{
|
||||||
RenderLock lock(*self);
|
RenderLock lock(*self);
|
||||||
self->statusMessage = tr(STR_CALC_HASH);
|
self->statusMessage = tr(STR_CALC_HASH);
|
||||||
|
|||||||
@@ -34,25 +34,25 @@ void SetTimeActivity::onExit() { Activity::onExit(); }
|
|||||||
|
|
||||||
void SetTimeActivity::loop() {
|
void SetTimeActivity::loop() {
|
||||||
// Back button: discard and exit
|
// Back button: discard and exit
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onBack();
|
onBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm button: apply time and exit
|
// Confirm button: apply time and exit
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
applyTime();
|
applyTime();
|
||||||
onBack();
|
onBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left/Right: switch between hour and minute fields
|
// Left/Right: switch between hour and minute fields
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
selectedField = 0;
|
selectedField = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
selectedField = 1;
|
selectedField = 1;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "I18n.h"
|
#include "I18n.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@@ -260,6 +262,23 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
|
// Draw clock on the left side (symmetric with battery on the right)
|
||||||
|
if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
if (t != nullptr && t->tm_year > 100) {
|
||||||
|
char timeBuf[16];
|
||||||
|
if (SETTINGS.homeScreenClock == 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.drawText(SMALL_FONT_ID, rect.x + 12, rect.y + 5, timeBuf, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
||||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -113,6 +115,23 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
|
// Draw clock on the left side (symmetric with battery on the right)
|
||||||
|
if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
if (t != nullptr && t->tm_year > 100) {
|
||||||
|
char timeBuf[16];
|
||||||
|
if (SETTINGS.homeScreenClock == 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.drawText(SMALL_FONT_ID, rect.x + 12, rect.y + 5, timeBuf, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
auto truncatedTitle = renderer.truncatedText(
|
auto truncatedTitle = renderer.truncatedText(
|
||||||
UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
|
UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
|
||||||
|
|||||||
13
src/main.cpp
13
src/main.cpp
@@ -11,6 +11,7 @@
|
|||||||
#include <builtinFonts/all.h>
|
#include <builtinFonts/all.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@@ -350,6 +351,18 @@ void setup() {
|
|||||||
// First serial output only here to avoid timing inconsistencies for power button press duration verification
|
// First serial output only here to avoid timing inconsistencies for power button press duration verification
|
||||||
LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
|
LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
|
||||||
|
|
||||||
|
// Log RTC time to verify persistence across deep sleep
|
||||||
|
{
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
if (t != nullptr && t->tm_year > 100) {
|
||||||
|
LOG_DBG("MAIN", "RTC time: %04d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||||
|
t->tm_hour, t->tm_min, t->tm_sec);
|
||||||
|
} else {
|
||||||
|
LOG_DBG("MAIN", "RTC time not set (epoch)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupDisplayAndFonts();
|
setupDisplayAndFonts();
|
||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|||||||
50
src/util/TimeSync.cpp
Normal file
50
src/util/TimeSync.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include "TimeSync.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <esp_sntp.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
namespace TimeSync {
|
||||||
|
|
||||||
|
void startNtpSync() {
|
||||||
|
if (esp_sntp_enabled()) {
|
||||||
|
esp_sntp_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
||||||
|
esp_sntp_setservername(0, "pool.ntp.org");
|
||||||
|
esp_sntp_init();
|
||||||
|
|
||||||
|
LOG_DBG("NTP", "SNTP service started");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool waitForNtpSync(int timeoutMs) {
|
||||||
|
startNtpSync();
|
||||||
|
|
||||||
|
const int intervalMs = 100;
|
||||||
|
const int maxRetries = timeoutMs / intervalMs;
|
||||||
|
int retry = 0;
|
||||||
|
|
||||||
|
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
||||||
|
vTaskDelay(intervalMs / portTICK_PERIOD_MS);
|
||||||
|
retry++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry < maxRetries) {
|
||||||
|
LOG_DBG("NTP", "Time synced after %d ms", retry * intervalMs);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("NTP", "Sync timeout after %d ms", timeoutMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopNtpSync() {
|
||||||
|
if (esp_sntp_enabled()) {
|
||||||
|
esp_sntp_stop();
|
||||||
|
LOG_DBG("NTP", "SNTP service stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TimeSync
|
||||||
17
src/util/TimeSync.h
Normal file
17
src/util/TimeSync.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace TimeSync {
|
||||||
|
|
||||||
|
// Start NTP time synchronization (non-blocking).
|
||||||
|
// Configures and starts the SNTP service; time will be updated
|
||||||
|
// automatically when the NTP response arrives.
|
||||||
|
void startNtpSync();
|
||||||
|
|
||||||
|
// Start NTP sync and block until complete or timeout.
|
||||||
|
// Returns true if time was synced, false on timeout.
|
||||||
|
bool waitForNtpSync(int timeoutMs = 5000);
|
||||||
|
|
||||||
|
// Stop the SNTP service. Call before disconnecting WiFi.
|
||||||
|
void stopNtpSync();
|
||||||
|
|
||||||
|
} // namespace TimeSync
|
||||||
Reference in New Issue
Block a user