diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 89ce13ba..64a251de 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -1,11 +1,9 @@ #include #include -#include void HalGPIO::begin() { inputMgr.begin(); SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); - pinMode(BAT_GPIO0, INPUT); pinMode(UART0_RXD, INPUT); } @@ -23,23 +21,6 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } -void HalGPIO::startDeepSleep() { - // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - while (inputMgr.isPressed(BTN_POWER)) { - delay(50); - inputMgr.update(); - } - // Arm the wakeup trigger *after* the button is released - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - // Enter Deep Sleep - esp_deep_sleep_start(); -} - -int HalGPIO::getBatteryPercentage() const { - static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); - return battery.readPercentage(); -} - bool HalGPIO::isUsbConnected() const { // U0RXD/GPIO20 reads HIGH when USB is connected return digitalRead(UART0_RXD) == HIGH; diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 615a8d63..45ca50a5 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -38,12 +38,6 @@ class HalGPIO { bool wasAnyReleased() const; unsigned long getHeldTime() const; - // Setup wake up GPIO and enter deep sleep - void startDeepSleep(); - - // Get battery percentage (range 0-100) - int getBatteryPercentage() const; - // Check if USB is connected bool isUsbConnected() const; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp new file mode 100644 index 00000000..b4d8d05c --- /dev/null +++ b/lib/hal/HalPowerManager.cpp @@ -0,0 +1,95 @@ +#include "HalPowerManager.h" + +#include +#include +#include + +#include + +#include "HalGPIO.h" + +HalPowerManager powerManager; // Singleton instance + +void HalPowerManager::begin() { + pinMode(BAT_GPIO0, INPUT); + normalFreq = getCpuFrequencyMhz(); + modeMutex = xSemaphoreCreateMutex(); + assert(modeMutex != nullptr); +} + +void HalPowerManager::setPowerSaving(bool enabled) { + if (normalFreq <= 0) { + return; // invalid state + } + + auto wifiMode = WiFi.getMode(); + if (wifiMode != WIFI_MODE_NULL) { + // Wifi is active, force disabling power saving + enabled = false; + } + + // Note: We don't use mutex here to avoid too much overhead, + // it's not very important if we read a slightly stale value for currentLockMode + const LockMode mode = currentLockMode; + + if (mode == None && enabled && !isLowPower) { + LOG_DBG("PWR", "Going to low-power mode"); + if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) { + LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ); + return; + } + isLowPower = true; + + } else if ((!enabled || mode != None) && isLowPower) { + LOG_DBG("PWR", "Restoring normal CPU frequency"); + if (!setCpuFrequencyMhz(normalFreq)) { + LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq); + return; + } + isLowPower = false; + } + + // Otherwise, no change needed +} + +void HalPowerManager::startDeepSleep(HalGPIO& gpio) 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(); + } + // Arm the wakeup trigger *after* the button is released + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); + // Enter Deep Sleep + esp_deep_sleep_start(); +} + +int HalPowerManager::getBatteryPercentage() const { + static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); + return battery.readPercentage(); +} + +HalPowerManager::Lock::Lock() { + xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY); + // Current limitation: only one lock at a time + if (powerManager.currentLockMode != None) { + LOG_ERR("PWR", "Lock already held, ignore"); + valid = false; + } else { + powerManager.currentLockMode = NormalSpeed; + valid = true; + } + xSemaphoreGive(powerManager.modeMutex); + if (valid) { + // Immediately restore normal CPU frequency if currently in low-power mode + powerManager.setPowerSaving(false); + } +} + +HalPowerManager::Lock::~Lock() { + xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY); + if (valid) { + powerManager.currentLockMode = None; + } + xSemaphoreGive(powerManager.modeMutex); +} diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h new file mode 100644 index 00000000..80f667d2 --- /dev/null +++ b/lib/hal/HalPowerManager.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "HalGPIO.h" + +class HalPowerManager; +extern HalPowerManager powerManager; // Singleton + +class HalPowerManager { + int normalFreq = 0; // MHz + bool isLowPower = false; + + enum LockMode { None, NormalSpeed }; + LockMode currentLockMode = None; + SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode + + public: + static constexpr int LOW_POWER_FREQ = 10; // MHz + static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms + + void begin(); + + // 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; + + // Get battery percentage (range 0-100) + int getBatteryPercentage() const; + + // RAII helper class to manage power saving locks + // Usage: create an instance of Lock in a scope to disable power saving, for example when running a task that needs + // full performance. When the Lock instance is destroyed (goes out of scope), power saving will be re-enabled. + class Lock { + friend class HalPowerManager; + bool valid = false; + + public: + explicit Lock(); + ~Lock(); + + // Non-copyable and non-movable + Lock(const Lock&) = delete; + Lock& operator=(const Lock&) = delete; + Lock(Lock&&) = delete; + Lock& operator=(Lock&&) = delete; + }; +}; diff --git a/src/activities/Activity.cpp b/src/activities/Activity.cpp index 6cd8493e..c725c2d5 100644 --- a/src/activities/Activity.cpp +++ b/src/activities/Activity.cpp @@ -1,5 +1,7 @@ #include "Activity.h" +#include + void Activity::renderTaskTrampoline(void* param) { auto* self = static_cast(param); self->renderTaskLoop(); @@ -9,6 +11,7 @@ void Activity::renderTaskLoop() { while (true) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); { + HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering RenderLock lock(*this); render(std::move(lock)); } diff --git a/src/activities/ActivityWithSubactivity.cpp b/src/activities/ActivityWithSubactivity.cpp index 40da93f1..2af75d73 100644 --- a/src/activities/ActivityWithSubactivity.cpp +++ b/src/activities/ActivityWithSubactivity.cpp @@ -1,9 +1,12 @@ #include "ActivityWithSubactivity.h" +#include + void ActivityWithSubactivity::renderTaskLoop() { while (true) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); { + HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering RenderLock lock(*this); if (!subActivity) { render(std::move(lock)); diff --git a/src/activities/settings/ClearCacheActivity.h b/src/activities/settings/ClearCacheActivity.h index 8bfbe1f3..5976e212 100644 --- a/src/activities/settings/ClearCacheActivity.h +++ b/src/activities/settings/ClearCacheActivity.h @@ -13,6 +13,7 @@ class ClearCacheActivity final : public ActivityWithSubactivity { void onEnter() override; void onExit() override; void loop() override; + bool skipLoopDelay() override { return true; } // Prevent power-saving mode void render(Activity::RenderLock&&) override; private: diff --git a/src/activities/settings/OtaUpdateActivity.h b/src/activities/settings/OtaUpdateActivity.h index 7978acf4..76711c1b 100644 --- a/src/activities/settings/OtaUpdateActivity.h +++ b/src/activities/settings/OtaUpdateActivity.h @@ -34,4 +34,5 @@ class OtaUpdateActivity : public ActivityWithSubactivity { void loop() override; void render(Activity::RenderLock&&) override; bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } + bool skipLoopDelay() override { return true; } // Prevent power-saving mode }; diff --git a/src/main.cpp b/src/main.cpp index 1d9d01ae..d0a422ab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -183,7 +184,7 @@ void verifyPowerButtonDuration() { if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again - gpio.startDeepSleep(); + powerManager.startDeepSleep(gpio); } } @@ -206,7 +207,7 @@ void enterDeepSleep() { LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); LOG_DBG("MAIN", "Entering deep sleep"); - gpio.startDeepSleep(); + powerManager.startDeepSleep(gpio); } void onGoHome(); @@ -283,6 +284,7 @@ void setup() { t1 = millis(); gpio.begin(); + powerManager.begin(); // Only start serial if USB connected if (gpio.isUsbConnected()) { @@ -319,7 +321,7 @@ void setup() { case HalGPIO::WakeupReason::AfterUSBPower: // If USB power caused a cold boot, go back to sleep LOG_DBG("MAIN", "Wakeup reason: After USB Power"); - gpio.startDeepSleep(); + powerManager.startDeepSleep(gpio); break; case HalGPIO::WakeupReason::AfterFlash: // After flashing, just proceed to boot @@ -391,7 +393,8 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { - lastActivityTime = millis(); // Reset inactivity timer + lastActivityTime = millis(); // Reset inactivity timer + powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); @@ -426,11 +429,12 @@ void loop() { // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // Otherwise, use longer delay to save power if (currentActivity && currentActivity->skipLoopDelay()) { - yield(); // Give FreeRTOS a chance to run tasks, but return immediately + powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested + yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { - static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // 3 seconds - if (millis() - lastActivityTime >= IDLE_POWER_SAVING_MS) { + if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) { // If we've been inactive for a while, increase the delay to save power + powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity delay(50); } else { // Short delay to prevent tight loop while still being responsive