Add basic clock support
This commit is contained in:
@@ -82,6 +82,15 @@ STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
|
||||
STR_HYPHENATION: "Hyphenation"
|
||||
STR_TIME_TO_SLEEP: "Time to Sleep"
|
||||
STR_SHOW_HIDDEN_FILES: "Show Hidden Files"
|
||||
STR_KEEP_CLOCK_ALIVE: "Keep Clock Alive"
|
||||
STR_CLOCK: "Clock"
|
||||
STR_CLOCK_FORMAT: "Clock Format"
|
||||
STR_24H: "24h"
|
||||
STR_12H: "12h"
|
||||
STR_SYNC_TIME: "Sync Time"
|
||||
STR_SYNCING_CLOCK: "Syncing clock..."
|
||||
STR_TIME_SYNCED: "Time synced"
|
||||
STR_TIME_SYNC_FAILED: "Time sync failed"
|
||||
STR_REFRESH_FREQ: "Refresh Frequency"
|
||||
STR_KOREADER_SYNC: "KOReader Sync"
|
||||
STR_CHECK_UPDATES: "Check for updates"
|
||||
|
||||
+10
-1
@@ -1,5 +1,7 @@
|
||||
#include "Logging.h"
|
||||
|
||||
#include <HalClock.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#define MAX_ENTRY_LEN 256
|
||||
@@ -41,7 +43,14 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
||||
// add the timestamp
|
||||
{
|
||||
unsigned long ms = millis();
|
||||
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||
char wallClock[12];
|
||||
HalClock::formatLogTime(wallClock, sizeof(wallClock));
|
||||
int len;
|
||||
if (wallClock[0] != '\0') {
|
||||
len = snprintf(c, sizeof(buf), "[%lu %s] ", ms, wallClock);
|
||||
} else {
|
||||
len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||
}
|
||||
if (len < 0) {
|
||||
return; // encoding error, skip logging
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "HalClock.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
#include <Preferences.h>
|
||||
#include <esp_private/esp_clk.h>
|
||||
#include <esp_sntp.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
// ---- RTC-memory state (survives deep sleep, not cold boot) ----------------
|
||||
|
||||
static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D;
|
||||
|
||||
RTC_NOINIT_ATTR static uint32_t rtcClockMagic;
|
||||
RTC_NOINIT_ATTR static time_t rtcEpoch; // last-known unix epoch
|
||||
RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() at capture
|
||||
|
||||
static bool clockApproximate = true;
|
||||
|
||||
// ---- NVS helpers ----------------------------------------------------------
|
||||
|
||||
static constexpr char NVS_NAMESPACE[] = "halclock";
|
||||
static constexpr char NVS_KEY[] = "epoch";
|
||||
|
||||
static void nvsWrite(time_t epoch) {
|
||||
Preferences prefs;
|
||||
if (prefs.begin(NVS_NAMESPACE, false)) {
|
||||
prefs.putLong64(NVS_KEY, (int64_t)epoch);
|
||||
prefs.end();
|
||||
}
|
||||
}
|
||||
|
||||
static time_t nvsRead() {
|
||||
Preferences prefs;
|
||||
time_t epoch = 0;
|
||||
if (prefs.begin(NVS_NAMESPACE, true)) {
|
||||
epoch = (time_t)prefs.getLong64(NVS_KEY, 0);
|
||||
prefs.end();
|
||||
}
|
||||
return epoch;
|
||||
}
|
||||
|
||||
// ---- internal helpers -----------------------------------------------------
|
||||
|
||||
static void setSystemClock(time_t epoch) {
|
||||
struct timeval tv = {};
|
||||
tv.tv_sec = epoch;
|
||||
settimeofday(&tv, nullptr);
|
||||
}
|
||||
|
||||
static bool rtcValid() {
|
||||
return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0;
|
||||
}
|
||||
|
||||
/// Capture current time + LP timer into RTC memory, and epoch into NVS.
|
||||
static void capture() {
|
||||
rtcEpoch = time(nullptr);
|
||||
rtcLpTimeUs = esp_clk_rtc_time();
|
||||
rtcClockMagic = CLOCK_RTC_MAGIC;
|
||||
nvsWrite(rtcEpoch);
|
||||
}
|
||||
|
||||
// ---- public API -----------------------------------------------------------
|
||||
|
||||
namespace HalClock {
|
||||
|
||||
bool syncNtp() {
|
||||
if (esp_sntp_enabled()) {
|
||||
esp_sntp_stop();
|
||||
}
|
||||
|
||||
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
||||
esp_sntp_setservername(0, "pool.ntp.org");
|
||||
esp_sntp_init();
|
||||
|
||||
int retry = 0;
|
||||
constexpr int maxRetries = 50; // 5 seconds
|
||||
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
retry++;
|
||||
}
|
||||
|
||||
if (retry >= maxRetries) {
|
||||
LOG_ERR("CLK", "NTP sync timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
capture();
|
||||
clockApproximate = false;
|
||||
LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch);
|
||||
return true;
|
||||
}
|
||||
|
||||
void saveBeforeSleep() {
|
||||
if (!isSynced()) {
|
||||
return;
|
||||
}
|
||||
capture();
|
||||
LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch);
|
||||
}
|
||||
|
||||
void restore() {
|
||||
if (rtcValid()) {
|
||||
// RTC memory survived — we woke from deep sleep.
|
||||
// Use the LP timer to compute how much time elapsed during sleep.
|
||||
uint64_t lpNow = esp_clk_rtc_time();
|
||||
time_t estimated = rtcEpoch;
|
||||
if (lpNow > rtcLpTimeUs) {
|
||||
estimated += (time_t)((lpNow - rtcLpTimeUs) / 1000000LL);
|
||||
}
|
||||
setSystemClock(estimated);
|
||||
// Re-capture with current LP baseline
|
||||
rtcEpoch = estimated;
|
||||
rtcLpTimeUs = lpNow;
|
||||
clockApproximate = true;
|
||||
LOG_INF("CLK", "Restored from RTC + LP timer, epoch %lld", (long long)estimated);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cold boot — try NVS. No elapsed correction possible.
|
||||
time_t epoch = nvsRead();
|
||||
if (epoch > 0) {
|
||||
setSystemClock(epoch);
|
||||
rtcEpoch = epoch;
|
||||
rtcLpTimeUs = esp_clk_rtc_time();
|
||||
rtcClockMagic = CLOCK_RTC_MAGIC;
|
||||
clockApproximate = true;
|
||||
LOG_INF("CLK", "Restored from NVS, epoch %lld (no elapsed correction)", (long long)epoch);
|
||||
}
|
||||
}
|
||||
|
||||
time_t now() {
|
||||
if (!isSynced()) {
|
||||
return 0;
|
||||
}
|
||||
return time(nullptr);
|
||||
}
|
||||
|
||||
bool isSynced() {
|
||||
return time(nullptr) > 1577836800; // > 2020-01-01
|
||||
}
|
||||
|
||||
bool isApproximate() {
|
||||
return clockApproximate;
|
||||
}
|
||||
|
||||
void formatTime(char* buf, size_t bufSize, bool use24h) {
|
||||
if (!isSynced()) {
|
||||
snprintf(buf, bufSize, "--:--");
|
||||
return;
|
||||
}
|
||||
|
||||
time_t t = time(nullptr);
|
||||
struct tm timeinfo;
|
||||
localtime_r(&t, &timeinfo);
|
||||
|
||||
const char* prefix = isApproximate() ? "~" : "";
|
||||
|
||||
if (use24h) {
|
||||
snprintf(buf, bufSize, "%s%02d:%02d", prefix, timeinfo.tm_hour, timeinfo.tm_min);
|
||||
} else {
|
||||
int hour = timeinfo.tm_hour % 12;
|
||||
if (hour == 0) hour = 12;
|
||||
const char* ampm = timeinfo.tm_hour < 12 ? "am" : "pm";
|
||||
snprintf(buf, bufSize, "%s%d:%02d%s", prefix, hour, timeinfo.tm_min, ampm);
|
||||
}
|
||||
}
|
||||
|
||||
void formatLogTime(char* buf, size_t bufSize) {
|
||||
if (!isSynced()) {
|
||||
buf[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
time_t t = time(nullptr);
|
||||
struct tm timeinfo;
|
||||
localtime_r(&t, &timeinfo);
|
||||
snprintf(buf, bufSize, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||||
}
|
||||
|
||||
} // namespace HalClock
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
/// Lightweight wall-clock facade.
|
||||
///
|
||||
/// The ESP32-C3 has no battery-backed RTC, so wall-clock time is lost on every
|
||||
/// deep-sleep / power cycle. HalClock bridges this gap using three layers:
|
||||
///
|
||||
/// - **LP timer** (`esp_clk_rtc_time()`) — keeps running during deep sleep
|
||||
/// when `keepClockAlive` is enabled (GPIO13 stays HIGH). Used to compute
|
||||
/// elapsed time and correct the stored epoch on wake.
|
||||
/// - **RTC memory** (`RTC_NOINIT_ATTR`) — survives deep sleep, lost on cold
|
||||
/// boot. Stores the epoch + LP timer value captured before sleep.
|
||||
/// - **NVS** (flash key-value store) — survives power cycles. Fallback when
|
||||
/// RTC memory is unavailable (cold boot).
|
||||
///
|
||||
/// Usage:
|
||||
/// 1. On boot, call `restore()` to seed the system clock from the best
|
||||
/// available source (RTC memory + LP correction > NVS).
|
||||
/// 2. After a successful NTP sync, call `onNtpSynced()`.
|
||||
/// 3. Before entering deep sleep, call `saveBeforeSleep()`.
|
||||
///
|
||||
/// `now()` returns the best-effort epoch (0 if never synced).
|
||||
namespace HalClock {
|
||||
|
||||
/// Perform an NTP sync (requires WiFi to be connected). Starts SNTP,
|
||||
/// waits up to 5 seconds for completion, then captures the result.
|
||||
/// Returns true if the sync succeeded.
|
||||
bool syncNtp();
|
||||
|
||||
/// Call just before deep sleep. Snapshots the current system time to RTC
|
||||
/// memory and NVS so it can be restored on wake / cold boot.
|
||||
void saveBeforeSleep();
|
||||
|
||||
/// Call on boot to seed the system clock from the best available stored
|
||||
/// value. When RTC memory is valid (deep-sleep wake) and the LP timer was
|
||||
/// running, the restored time includes elapsed-time correction. Falls back
|
||||
/// to NVS for cold boot (stale, but better than nothing).
|
||||
void restore();
|
||||
|
||||
/// Returns the current best-effort wall-clock epoch, or 0 if the clock was
|
||||
/// never set.
|
||||
time_t now();
|
||||
|
||||
/// True if the clock has been set at least once (NTP or restore).
|
||||
bool isSynced();
|
||||
|
||||
/// True if the last restore was from a backup (not NTP) — i.e. the clock
|
||||
/// may have drifted. Cleared on NTP sync.
|
||||
bool isApproximate();
|
||||
|
||||
/// 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".
|
||||
/// Output is written to `buf` (must be at least 16 bytes).
|
||||
void formatTime(char* buf, size_t bufSize, bool use24h);
|
||||
|
||||
/// Format the current time for log timestamps. Returns "HH:MM:SS" if
|
||||
/// synced, or an empty string if not.
|
||||
void formatLogTime(char* buf, size_t bufSize);
|
||||
|
||||
} // namespace HalClock
|
||||
@@ -52,26 +52,30 @@ void HalPowerManager::setPowerSaving(bool enabled) {
|
||||
// Otherwise, no change needed
|
||||
}
|
||||
|
||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
delay(50);
|
||||
gpio.update();
|
||||
}
|
||||
// Pre-sleep routines from the original firmware
|
||||
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
|
||||
// Note that this means the MCU will be completely powered off during sleep, including RTC
|
||||
// GPIO13 is connected to the battery latch MOSFET.
|
||||
// When keepClockAlive is false (default): GPIO13 goes LOW, the MCU is
|
||||
// completely powered off during sleep (including the LP timer / RTC memory).
|
||||
// When keepClockAlive is true: GPIO13 stays HIGH, the MCU remains powered
|
||||
// at ~3-4 mA so the LP timer keeps running and RTC memory is preserved.
|
||||
// This allows HalClock to accurately compute elapsed sleep time on wake.
|
||||
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(GPIO_SPIWP, 0);
|
||||
gpio_set_level(GPIO_SPIWP, keepClockAlive ? 1 : 0);
|
||||
esp_sleep_config_gpio_isolate();
|
||||
gpio_deep_sleep_hold_en();
|
||||
gpio_hold_en(GPIO_SPIWP);
|
||||
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
|
||||
// power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source
|
||||
// configuration
|
||||
// Note: when keepClockAlive is false, this is only useful for waking up on USB power. On battery, the MCU will be
|
||||
// completely powered off, so the power button is hard-wired to briefly provide power to the MCU, waking it up
|
||||
// regardless of the wakeup source configuration.
|
||||
// When keepClockAlive is true, this is the actual wakeup mechanism since the MCU stays powered.
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
|
||||
@@ -29,9 +29,11 @@ class HalPowerManager {
|
||||
// Control CPU frequency for power saving
|
||||
void setPowerSaving(bool enabled);
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
// Should be called inside main loop() to handle the currentLockMode
|
||||
void startDeepSleep(HalGPIO& gpio) const;
|
||||
// Setup wake up GPIO and enter deep sleep.
|
||||
// When keepClockAlive is true, GPIO13 stays HIGH so the LP timer keeps
|
||||
// running during sleep (~3-4 mA extra). This allows HalClock to compute
|
||||
// elapsed sleep time and restore the wall clock accurately on wake.
|
||||
void startDeepSleep(HalGPIO& gpio, bool keepClockAlive = false) const;
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
uint16_t getBatteryPercentage() const;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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) +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user