From 40448b99766ad49ccdd688d94fa133a0e20f302a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 29 Mar 2026 17:12:34 +0200 Subject: [PATCH] Add autowake actions --- lib/I18n/translations/english.yaml | 6 ++ lib/hal/HalGPIO.cpp | 4 + lib/hal/HalGPIO.h | 2 +- lib/hal/HalPowerManager.cpp | 7 +- lib/hal/HalPowerManager.h | 2 +- src/CrossPointSettings.h | 8 ++ src/SettingsList.h | 12 +++ src/main.cpp | 59 ++++++++++++-- src/util/ScheduledTaskRunner.cpp | 125 +++++++++++++++++++++++++++++ src/util/ScheduledTaskRunner.h | 7 ++ 10 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 src/util/ScheduledTaskRunner.cpp create mode 100644 src/util/ScheduledTaskRunner.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 774c0ad7c3..9af260189f 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -119,6 +119,12 @@ STR_TIMEZONE_DETECT_FAILED: "Timezone detect failed" STR_DST_ACTIVE: "DST: active" STR_DST_INACTIVE: "DST: inactive" STR_DST_UNKNOWN: "DST: unknown" +STR_SCHEDULED_WAKE: "Scheduled Wakeup" +STR_SCHEDULED_WAKE_HOUR: "Wakeup Hour" +STR_SCHEDULED_WAKE_MINUTE: "Wakeup Minute" +STR_SCHED_TASK_NTP: "Sync Time on Wake" +STR_SCHED_TASK_IMG: "Download Sleep Image" +STR_SCHEDULED_WAKE_IMAGE_URL: "Sleep Image URL" STR_REFRESH_FREQ: "Refresh Frequency" STR_KOREADER_SYNC: "KOReader Sync" STR_CHECK_UPDATES: "Check for updates" diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index ec0176a219..67e3089c1d 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -38,6 +38,10 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); + // Timer wakeup from deep sleep (scheduled wake feature) + if (wakeupCause == ESP_SLEEP_WAKEUP_TIMER && resetReason == ESP_RST_DEEPSLEEP) { + return WakeupReason::TimerWake; + } if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) || (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) { return WakeupReason::PowerButton; diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index a283ed608d..543024c93b 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -47,7 +47,7 @@ class HalGPIO { // Returns true once per edge (plug or unplug) since the last update() bool wasUsbStateChanged() const; - enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; + enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, TimerWake, Other }; WakeupReason getWakeupReason() const; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index 989376ab19..18638b7a84 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -52,7 +52,7 @@ void HalPowerManager::setPowerSaving(bool enabled) { // Otherwise, no change needed } -void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const { +void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive, uint64_t timerWakeupUs) 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); @@ -77,6 +77,11 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const { // 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); + // Optionally arm a timer wakeup (for scheduled tasks) + if (timerWakeupUs > 0) { + esp_sleep_enable_timer_wakeup(timerWakeupUs); + LOG_DBG("PWR", "Timer wakeup armed for %llu us (~%llu min)", timerWakeupUs, timerWakeupUs / 60000000ULL); + } // Enter Deep Sleep esp_deep_sleep_start(); } diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h index 4fdc1652e2..2c77c7e283 100644 --- a/lib/hal/HalPowerManager.h +++ b/lib/hal/HalPowerManager.h @@ -33,7 +33,7 @@ class HalPowerManager { // 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; + void startDeepSleep(HalGPIO& gpio, bool keepClockAlive = false, uint64_t timerWakeupUs = 0) const; // Get battery percentage (range 0-100) uint16_t getBatteryPercentage() const; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 3280b06376..4dbe82b341 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -230,6 +230,14 @@ class CrossPointSettings { // so time can be accurately restored on wake. Increases sleep current by ~3-4 mA. uint8_t useClock = 0; + // Scheduled wakeup settings + uint8_t scheduledWakeEnabled = 0; + uint8_t scheduledWakeHour = 3; // 0-23, default 3 AM + uint8_t scheduledWakeMinute = 0; // 0-59 + uint8_t scheduledWakeTaskNtp = 1; // NTP sync task enabled by default + uint8_t scheduledWakeTaskImg = 0; // Download sleep image task + char scheduledWakeImageUrl[128] = ""; + ~CrossPointSettings() = default; // Get singleton instance diff --git a/src/SettingsList.h b/src/SettingsList.h index 4bcbc75430..d09718ea41 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -89,6 +89,18 @@ inline const std::vector& getSettingsList() { StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, "timeZone", StrId::STR_CAT_SYSTEM), SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SCHEDULED_WAKE, &CrossPointSettings::scheduledWakeEnabled, "scheduledWakeEnabled", + StrId::STR_CAT_SYSTEM), + SettingInfo::Value(StrId::STR_SCHEDULED_WAKE_HOUR, &CrossPointSettings::scheduledWakeHour, {0, 23, 1}, + "scheduledWakeHour", StrId::STR_CAT_SYSTEM), + SettingInfo::Value(StrId::STR_SCHEDULED_WAKE_MINUTE, &CrossPointSettings::scheduledWakeMinute, {0, 59, 5}, + "scheduledWakeMinute", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SCHED_TASK_NTP, &CrossPointSettings::scheduledWakeTaskNtp, "scheduledWakeTaskNtp", + StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SCHED_TASK_IMG, &CrossPointSettings::scheduledWakeTaskImg, "scheduledWakeTaskImg", + StrId::STR_CAT_SYSTEM), + SettingInfo::String(StrId::STR_SCHEDULED_WAKE_IMAGE_URL, SETTINGS.scheduledWakeImageUrl, + sizeof(SETTINGS.scheduledWakeImageUrl), "scheduledWakeImageUrl", StrId::STR_CAT_SYSTEM), // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- SettingInfo::DynamicString( diff --git a/src/main.cpp b/src/main.cpp index 4cc63aaad4..32b969684d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ #include "components/UITheme.h" #include "fontIds.h" #include "util/ButtonNavigator.h" +#include "util/ScheduledTaskRunner.h" #include "util/ScreenshotUtil.h" HalDisplay display; @@ -129,6 +130,8 @@ EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); unsigned long t1 = 0; unsigned long t2 = 0; +uint64_t calculateTimerWakeupUs(); // forward declaration + // Verify power button press duration on wake-up from deep sleep // Pre-condition: isWakeupByPowerButton() == true void verifyPowerButtonDuration() { @@ -168,8 +171,9 @@ void verifyPowerButtonDuration() { if (abort) { // Button released too early. Returning to sleep. - // IMPORTANT: Re-arm the wakeup trigger before sleeping again - powerManager.startDeepSleep(gpio); + // IMPORTANT: Re-arm the wakeup trigger (and scheduled timer) before sleeping again + uint64_t timerUs = calculateTimerWakeupUs(); + powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs); } } @@ -181,6 +185,37 @@ void waitForPowerRelease() { } } +// Returns microseconds until the next scheduled wakeup time, or 0 if timer should not be armed. +uint64_t calculateTimerWakeupUs() { + if (!SETTINGS.scheduledWakeEnabled || !SETTINGS.useClock) { + return 0; + } + if (!HalClock::isSynced()) { + return 0; + } + if (!SETTINGS.scheduledWakeTaskNtp && !SETTINGS.scheduledWakeTaskImg) { + return 0; + } + + time_t now = time(nullptr); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + + struct tm target = timeinfo; + target.tm_hour = SETTINGS.scheduledWakeHour; + target.tm_min = SETTINGS.scheduledWakeMinute; + target.tm_sec = 0; + time_t targetEpoch = mktime(&target); + + // If target is in the past or within 60s, schedule for tomorrow + if (targetEpoch <= now + 60) { + targetEpoch += 24 * 3600; + } + + int64_t diffSeconds = static_cast(targetEpoch - now); + return static_cast(diffSeconds) * 1000000ULL; +} + // Enter deep sleep mode void enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation @@ -194,7 +229,8 @@ void enterDeepSleep() { LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); LOG_DBG("MAIN", "Entering deep sleep"); - powerManager.startDeepSleep(gpio, SETTINGS.useClock); + uint64_t timerUs = calculateTimerWakeupUs(); + powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs); } void setupDisplayAndFonts() { @@ -267,16 +303,29 @@ void setup() { ButtonNavigator::setMappedInputManager(mappedInputManager); switch (gpio.getWakeupReason()) { + case HalGPIO::WakeupReason::TimerWake: { + // Scheduled timer wakeup — run background tasks headlessly, then re-sleep + LOG_DBG("MAIN", "Timer wakeup - running scheduled tasks"); + HalClock::restore(); + ScheduledTaskRunner::run(); + HalClock::saveBeforeSleep(true); // useClock must be true for timer wake + uint64_t timerUs = calculateTimerWakeupUs(); + powerManager.startDeepSleep(gpio, true, timerUs); + break; // never reached + } case HalGPIO::WakeupReason::PowerButton: // For normal wakeups, verify power button press duration LOG_DBG("MAIN", "Verifying power button press duration"); verifyPowerButtonDuration(); break; - case HalGPIO::WakeupReason::AfterUSBPower: + case HalGPIO::WakeupReason::AfterUSBPower: { // If USB power caused a cold boot, go back to sleep LOG_DBG("MAIN", "Wakeup reason: After USB Power"); - powerManager.startDeepSleep(gpio); + HalClock::restore(); + uint64_t timerUs = calculateTimerWakeupUs(); + powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs); break; + } case HalGPIO::WakeupReason::AfterFlash: // After flashing, just proceed to boot case HalGPIO::WakeupReason::Other: diff --git a/src/util/ScheduledTaskRunner.cpp b/src/util/ScheduledTaskRunner.cpp new file mode 100644 index 0000000000..83b0f57711 --- /dev/null +++ b/src/util/ScheduledTaskRunner.cpp @@ -0,0 +1,125 @@ +#include "ScheduledTaskRunner.h" + +#include +#include +#include +#include +#include + +#include "CrossPointSettings.h" +#include "WifiCredentialStore.h" +#include "network/HttpDownloader.h" + +namespace ScheduledTaskRunner { + +static constexpr uint16_t WIFI_TIMEOUT_MS = 15000; +static constexpr uint16_t LOW_BATTERY_THRESHOLD = 10; + +static bool connectWifi() { + WIFI_STORE.loadFromFile(); + const std::string& lastSsid = WIFI_STORE.getLastConnectedSsid(); + if (lastSsid.empty()) { + LOG_ERR("SCHED", "No saved WiFi network"); + return false; + } + const auto* cred = WIFI_STORE.findCredential(lastSsid); + if (!cred) { + LOG_ERR("SCHED", "No credentials for %s", lastSsid.c_str()); + return false; + } + + WiFi.mode(WIFI_STA); + if (!cred->password.empty()) { + WiFi.begin(cred->ssid.c_str(), cred->password.c_str()); + } else { + WiFi.begin(cred->ssid.c_str()); + } + + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_TIMEOUT_MS) { + delay(100); + } + if (WiFi.status() != WL_CONNECTED) { + LOG_ERR("SCHED", "WiFi connect timeout"); + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + return false; + } + LOG_INF("SCHED", "WiFi connected to %s", lastSsid.c_str()); + return true; +} + +static void taskNtpSync() { + LOG_INF("SCHED", "Running NTP sync task"); + if (HalClock::syncNtp()) { + LOG_INF("SCHED", "NTP sync successful"); + } else { + LOG_ERR("SCHED", "NTP sync failed"); + } +} + +static void taskDownloadSleepImage() { + if (SETTINGS.scheduledWakeImageUrl[0] == '\0') { + LOG_DBG("SCHED", "No sleep image URL configured, skipping"); + return; + } + LOG_INF("SCHED", "Downloading sleep image from %s", SETTINGS.scheduledWakeImageUrl); + + const std::string tempPath = "/.crosspoint/sleep_download.tmp"; + const std::string destPath = "/sleep.bmp"; + + auto result = HttpDownloader::downloadToFile(SETTINGS.scheduledWakeImageUrl, tempPath); + + if (result == HttpDownloader::OK) { + if (Storage.exists(destPath.c_str())) { + Storage.remove(destPath.c_str()); + } + if (Storage.rename(tempPath.c_str(), destPath.c_str())) { + LOG_INF("SCHED", "Sleep image saved to %s", destPath.c_str()); + } else { + LOG_ERR("SCHED", "Failed to rename temp file to %s", destPath.c_str()); + Storage.remove(tempPath.c_str()); + } + } else { + LOG_ERR("SCHED", "Sleep image download failed (error %d)", result); + Storage.remove(tempPath.c_str()); + } +} + +void run() { + HalPowerManager::Lock powerLock; + + // Skip tasks if battery is critically low + uint16_t battery = powerManager.getBatteryPercentage(); + if (battery > 0 && battery < LOW_BATTERY_THRESHOLD) { + LOG_INF("SCHED", "Battery critically low (%u%%), skipping scheduled tasks", battery); + return; + } + + bool needWifi = SETTINGS.scheduledWakeTaskNtp || SETTINGS.scheduledWakeTaskImg; + bool wifiConnected = false; + + if (needWifi) { + wifiConnected = connectWifi(); + if (!wifiConnected) { + LOG_ERR("SCHED", "WiFi required but failed to connect, aborting tasks"); + return; + } + } + + if (SETTINGS.scheduledWakeTaskNtp) { + taskNtpSync(); + } + + if (SETTINGS.scheduledWakeTaskImg) { + taskDownloadSleepImage(); + } + + if (wifiConnected) { + HalClock::wifiOff(true); // skip opportunistic NTP since we already synced + } + + LOG_INF("SCHED", "Scheduled tasks completed"); +} + +} // namespace ScheduledTaskRunner diff --git a/src/util/ScheduledTaskRunner.h b/src/util/ScheduledTaskRunner.h new file mode 100644 index 0000000000..0c84e88738 --- /dev/null +++ b/src/util/ScheduledTaskRunner.h @@ -0,0 +1,7 @@ +#pragma once + +namespace ScheduledTaskRunner { +// Runs all enabled scheduled tasks headlessly (no display). +// Connects to WiFi if needed, executes tasks, disconnects. +void run(); +} // namespace ScheduledTaskRunner