Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions lib/hal/HalGPIO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion lib/hal/HalGPIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 6 additions & 1 deletion lib/hal/HalPowerManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Comment on lines +81 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "HalPowerManager.cpp" -type f

Repository: jpirnay/crosspoint-reader

Length of output: 98


🏁 Script executed:

# Search for the specific code pattern to verify line numbers and context
rg -n "esp_sleep_enable_timer_wakeup" lib/hal/HalPowerManager.cpp -B 2 -A 3

Repository: jpirnay/crosspoint-reader

Length of output: 356


🏁 Script executed:

# Check if there are other usages of esp_sleep_enable_timer_wakeup in the codebase
rg "esp_sleep_enable_timer_wakeup" --type cpp

Repository: jpirnay/crosspoint-reader

Length of output: 146


🏁 Script executed:

# Look for error handling patterns with ESP APIs in the codebase
rg "esp_err_t|ESP_OK" lib/hal/ --type cpp -A 2 | head -50

Repository: jpirnay/crosspoint-reader

Length of output: 51


🏁 Script executed:

# Read the entire HalPowerManager.cpp file to understand context and error handling patterns
wc -l lib/hal/HalPowerManager.cpp

Repository: jpirnay/crosspoint-reader

Length of output: 100


🏁 Script executed:

# View the file to understand error handling patterns and context
cat -n lib/hal/HalPowerManager.cpp

Repository: jpirnay/crosspoint-reader

Length of output: 5027


🏁 Script executed:

# Search for other ESP API calls in the file that might handle errors
rg "esp_" lib/hal/HalPowerManager.cpp -n

Repository: jpirnay/crosspoint-reader

Length of output: 319


🏁 Script executed:

# Look for error handling patterns with LOG_ERR or similar in lib/hal/
rg "LOG_ERR|return.*ESP_" lib/hal/ --type cpp -B 1

Repository: jpirnay/crosspoint-reader

Length of output: 854


🏁 Script executed:

# Check if esp_sleep.h is in the repository or if it comes from ESP-IDF
find . -name "esp_sleep.h" -type f

Repository: jpirnay/crosspoint-reader

Length of output: 51


🏁 Script executed:

# Search for any documentation or comments about ESP API return values in the codebase
rg "esp_err_t|esp_sleep_enable_timer_wakeup" --type cpp -B 2 -A 2

Repository: jpirnay/crosspoint-reader

Length of output: 4238


🏁 Script executed:

# Check if there are any error checks for similar ESP APIs elsewhere in the codebase
rg "esp_deep_sleep|esp_sleep" --type cpp -B 1 -A 1

Repository: jpirnay/crosspoint-reader

Length of output: 1676


Check return value from esp_sleep_enable_timer_wakeup() on line 82.

The return value is being ignored. If the timer wakeup fails to arm, the code still logs success, resulting in silent failure of scheduled wakeup functionality.

🔧 Proposed fix
   // 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);
+    const esp_err_t err = esp_sleep_enable_timer_wakeup(timerWakeupUs);
+    if (err != ESP_OK) {
+      LOG_ERR("PWR", "Failed to arm timer wakeup: %d", static_cast<int>(err));
+    } else {
+      LOG_DBG("PWR", "Timer wakeup armed for %llu us (~%llu min)", timerWakeupUs, timerWakeupUs / 60000000ULL);
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (timerWakeupUs > 0) {
esp_sleep_enable_timer_wakeup(timerWakeupUs);
LOG_DBG("PWR", "Timer wakeup armed for %llu us (~%llu min)", timerWakeupUs, timerWakeupUs / 60000000ULL);
}
if (timerWakeupUs > 0) {
const esp_err_t err = esp_sleep_enable_timer_wakeup(timerWakeupUs);
if (err != ESP_OK) {
LOG_ERR("PWR", "Failed to arm timer wakeup: %d", static_cast<int>(err));
} else {
LOG_DBG("PWR", "Timer wakeup armed for %llu us (~%llu min)", timerWakeupUs, timerWakeupUs / 60000000ULL);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/hal/HalPowerManager.cpp` around lines 81 - 84, The call to
esp_sleep_enable_timer_wakeup(timerWakeupUs) is ignoring its return value;
update the HalPowerManager code around the esp_sleep_enable_timer_wakeup
invocation to check the returned esp_err_t, and if it indicates failure log an
error (e.g., via LOG_ERR or similar) including the error code and timerWakeupUs,
and avoid treating the wakeup as armed (do not emit the current LOG_DBG success
message on failure); ensure the success path still logs the existing debug
message and consider returning/setting an error flag so callers know the timer
wakeup was not armed.

// Enter Deep Sleep
esp_deep_sleep_start();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/hal/HalPowerManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/CrossPointSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/SettingsList.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ inline const std::vector<SettingInfo>& 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(
Expand Down
59 changes: 54 additions & 5 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Comment on lines +174 to +176
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore clock before re-arming timer in abort path.

At Line 175, calculateTimerWakeupUs() can run before clock restore, so HalClock::isSynced() may be false and the timer may not be re-armed after an aborted wake.

🔧 Proposed fix
   if (abort) {
     // Button released too early. Returning to sleep.
     // IMPORTANT: Re-arm the wakeup trigger (and scheduled timer) before sleeping again
+    HalClock::restore();
     uint64_t timerUs = calculateTimerWakeupUs();
+    HalClock::saveBeforeSleep(SETTINGS.useClock);
     powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// IMPORTANT: Re-arm the wakeup trigger (and scheduled timer) before sleeping again
uint64_t timerUs = calculateTimerWakeupUs();
powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs);
// IMPORTANT: Re-arm the wakeup trigger (and scheduled timer) before sleeping again
HalClock::restore();
uint64_t timerUs = calculateTimerWakeupUs();
HalClock::saveBeforeSleep(SETTINGS.useClock);
powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.cpp` around lines 174 - 176, The abort path is re-arming the timer
before the RTC/clock is restored, so calculateTimerWakeupUs() may see
HalClock::isSynced() == false and skip re-arming; move the clock-restore step to
run before calculateTimerWakeupUs() (i.e., restore the hardware clock/RTC first
so HalClock::isSynced() is true), then call calculateTimerWakeupUs() and finally
powerManager.startDeepSleep(gpio, SETTINGS.useClock, timerUs).

}
}

Expand All @@ -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<int64_t>(targetEpoch - now);
return static_cast<uint64_t>(diffSeconds) * 1000000ULL;
}

// Enter deep sleep mode
void enterDeepSleep() {
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
Expand All @@ -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() {
Expand Down Expand Up @@ -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:
Expand Down
125 changes: 125 additions & 0 deletions src/util/ScheduledTaskRunner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#include "ScheduledTaskRunner.h"

#include <HalClock.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <Logging.h>
#include <WiFi.h>

#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);

Comment on lines +66 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging full download URLs (possible secret leakage).

On Line 66, logging the raw URL can expose embedded credentials or signed query tokens in device logs.

🔒 Proposed fix
-  LOG_INF("SCHED", "Downloading sleep image from %s", SETTINGS.scheduledWakeImageUrl);
+  LOG_INF("SCHED", "Downloading configured sleep image");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LOG_INF("SCHED", "Downloading sleep image from %s", SETTINGS.scheduledWakeImageUrl);
LOG_INF("SCHED", "Downloading configured sleep image");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/ScheduledTaskRunner.cpp` around lines 66 - 67, Replace the direct
logging of SETTINGS.scheduledWakeImageUrl in the LOG_INF call so secrets in the
URL aren't emitted; instead redact or canonicalize the URL (e.g., remove query
string and userinfo) before logging, or log only the hostname/path or a generic
message like "Downloading sleep image" plus a safe identifier. Update the place
where LOG_INF("SCHED", "Downloading sleep image from %s",
SETTINGS.scheduledWakeImageUrl) is called in ScheduledTaskRunner.cpp to compute
a redactedUrl (strip ?query and any user@host credentials) and pass that to
LOG_INF, or remove the URL from the log entirely and log a non-sensitive status
message.

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
7 changes: 7 additions & 0 deletions src/util/ScheduledTaskRunner.h
Original file line number Diff line number Diff line change
@@ -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
Loading