Add basic clock support

This commit is contained in:
jpirnay
2026-03-26 16:46:23 +01:00
parent 5b43639f98
commit dd9c9e6def
18 changed files with 484 additions and 43 deletions
+7
View File
@@ -199,6 +199,13 @@ class CrossPointSettings {
uint8_t showHiddenFiles = 0;
// Image rendering mode in EPUB reader
uint8_t imageRendering = IMAGES_DISPLAY;
// Show clock in the reader status bar
uint8_t statusBarClock = 0;
// Clock format: 0 = 24h (14:00), 1 = 12h (2:00pm)
uint8_t clockFormat12h = 0;
// Keep the LP timer running during deep sleep (GPIO13 HIGH) so the clock
// can be accurately restored on wake. Increases sleep current by ~3-4 mA.
uint8_t keepClockAlive = 0;
~CrossPointSettings() = default;
+6
View File
@@ -80,6 +80,10 @@ inline const std::vector<SettingInfo>& getSettingsList() {
"sleepTimeout", StrId::STR_CAT_SYSTEM),
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
StrId::STR_CAT_SYSTEM),
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H},
"clockFormat12h", StrId::STR_CAT_SYSTEM),
SettingInfo::Toggle(StrId::STR_KEEP_CLOCK_ALIVE, &CrossPointSettings::keepClockAlive, "keepClockAlive",
StrId::STR_CAT_SYSTEM),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString(
@@ -136,6 +140,8 @@ inline const std::vector<SettingInfo>& getSettingsList() {
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery",
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock",
StrId::STR_CUSTOMISE_STATUS_BAR),
};
return list;
}
+2 -26
View File
@@ -1,6 +1,7 @@
#include "KOReaderSyncActivity.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
@@ -14,31 +15,6 @@
#include "fontIds.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");
}
}
void wifiOff() {
if (esp_sntp_enabled()) {
esp_sntp_stop();
@@ -70,7 +46,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
requestUpdate(true);
// Sync time with NTP before making API requests
syncTimeWithNTP();
HalClock::syncNtp();
{
RenderLock lock(*this);
@@ -13,6 +13,7 @@
#include "OtaUpdateActivity.h"
#include "SettingsList.h"
#include "StatusBarSettingsActivity.h"
#include "SyncTimeActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -46,6 +47,7 @@ void SettingsActivity::onEnter() {
// Append device-only ACTION items
controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
systemSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_TIME, SettingAction::SyncTime));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser));
@@ -192,6 +194,9 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::SyncTime:
startActivityForResult(std::make_unique<SyncTimeActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::None:
// Do nothing
break;
@@ -21,6 +21,7 @@ enum class SettingAction {
ClearCache,
CheckForUpdates,
Language,
SyncTime,
};
struct SettingInfo {
@@ -11,13 +11,14 @@
#include "fontIds.h"
namespace {
constexpr int MENU_ITEMS = 6;
constexpr int MENU_ITEMS = 7;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_CHAPTER_PAGE_COUNT,
StrId::STR_BOOK_PROGRESS_PERCENTAGE,
StrId::STR_PROGRESS_BAR,
StrId::STR_PROGRESS_BAR_THICKNESS,
StrId::STR_TITLE,
StrId::STR_BATTERY};
StrId::STR_BATTERY,
StrId::STR_CLOCK};
constexpr int PROGRESS_BAR_ITEMS = 3;
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
@@ -110,6 +111,9 @@ void StatusBarSettingsActivity::handleSelection() {
} else if (selectedIndex == 5) {
// Show Battery
SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2;
} else if (selectedIndex == 6) {
// Show Clock
SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % 2;
}
SETTINGS.saveToFile();
}
@@ -143,6 +147,8 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
return I18N.get(titleNames[SETTINGS.statusBarTitle]);
} else if (index == 5) {
return SETTINGS.statusBarBattery ? tr(STR_SHOW) : tr(STR_HIDE);
} else if (index == 6) {
return SETTINGS.statusBarClock ? tr(STR_SHOW) : tr(STR_HIDE);
} else {
return tr(STR_HIDE);
}
@@ -0,0 +1,119 @@
#include "SyncTimeActivity.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include "CrossPointSettings.h"
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <esp_sntp.h>
#include "MappedInputManager.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
void wifiOff() {
if (esp_sntp_enabled()) {
esp_sntp_stop();
}
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
delay(100);
}
} // namespace
void SyncTimeActivity::onEnter() {
Activity::onEnter();
if (WiFi.status() == WL_CONNECTED) {
onWifiSelectionComplete(true);
return;
}
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
}
void SyncTimeActivity::onExit() {
Activity::onExit();
wifiOff();
}
void SyncTimeActivity::onWifiSelectionComplete(bool success) {
if (!success) {
state = FAILED;
requestUpdate();
return;
}
{
RenderLock lock(*this);
state = SYNCING;
}
requestUpdateAndWait();
performSync();
}
void SyncTimeActivity::performSync() {
bool ok = HalClock::syncNtp();
wifiOff();
state = ok ? SUCCESS : FAILED;
requestUpdate();
}
void SyncTimeActivity::render(RenderLock&&) {
const 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_TIME));
if (state == SYNCING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_SYNCING_CLOCK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD);
time_t now = HalClock::now();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char timePart[16];
HalClock::formatTime(timePart, sizeof(timePart), !SETTINGS.clockFormat12h);
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);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIME_SYNC_FAILED), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
}
void SyncTimeActivity::loop() {
if (state == SUCCESS || state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
}
}
}
@@ -0,0 +1,21 @@
#pragma once
#include "activities/Activity.h"
class SyncTimeActivity final : public Activity {
public:
explicit SyncTimeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("SyncTime", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
enum State { CONNECTING, SYNCING, SUCCESS, FAILED };
State state = CONNECTING;
void onWifiSelectionComplete(bool success);
void performSync();
};
+1 -1
View File
@@ -96,7 +96,7 @@ int UITheme::getStatusBarHeight() {
// Add status bar margin
const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE ||
SETTINGS.statusBarBattery;
SETTINGS.statusBarBattery || SETTINGS.statusBarClock;
const bool showProgressBar =
SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) +
+21 -1
View File
@@ -1,6 +1,7 @@
#include "BaseTheme.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <Logging.h>
@@ -294,6 +295,13 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
showBatteryPercentage);
// Draw clock in header
{
char clockStr[16];
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, clockStr);
}
if (title) {
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
@@ -715,6 +723,17 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
showBatteryPercentage);
}
// Draw Clock
int clockTextWidth = 0;
if (SETTINGS.statusBarClock) {
char clockStr[16];
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, clockStr);
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
renderer.drawText(SMALL_FONT_ID, metrics.statusBarHorizontalMargin + orientedMarginLeft + batterySize + 8, textY,
clockStr);
}
// Draw Title
if (!title.empty()) {
textY -= textYOffset;
@@ -724,7 +743,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
const int titleMarginLeft = batterySize + 30;
const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0;
const int titleMarginLeft = batterySize + clockSize + 30;
const int titleMarginRight = progressTextWidth + 30;
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
+8
View File
@@ -1,6 +1,7 @@
#include "LyraTheme.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include <HalGPIO.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
@@ -167,6 +168,13 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
showBatteryPercentage);
// Draw clock in header
{
char clockStr[16];
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, rect.y + 5, clockStr);
}
int maxTitleWidth =
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
+4 -1
View File
@@ -3,6 +3,7 @@
#include <FontCacheManager.h>
#include <FontDecompressor.h>
#include <GfxRenderer.h>
#include <HalClock.h>
#include <HalDisplay.h>
#include <HalGPIO.h>
#include <HalPowerManager.h>
@@ -184,6 +185,7 @@ void waitForPowerRelease() {
void enterDeepSleep() {
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
APP_STATE.lastSleepFromReader = activityManager.isReaderActivity();
HalClock::saveBeforeSleep();
APP_STATE.saveToFile();
activityManager.goToSleep();
@@ -192,7 +194,7 @@ void enterDeepSleep() {
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
LOG_DBG("MAIN", "Entering deep sleep");
powerManager.startDeepSleep(gpio);
powerManager.startDeepSleep(gpio, SETTINGS.keepClockAlive);
}
void setupDisplayAndFonts() {
@@ -289,6 +291,7 @@ void setup() {
activityManager.goToBoot();
APP_STATE.loadFromFile();
HalClock::restore();
RECENT_BOOKS.loadFromFile();
// Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity