4 Commits

Author SHA1 Message Date
Zach Nelson
d02e21a48f fix: Added missing up/down button labels (#935)
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
## Summary

**What is the goal of this PR?**

In some places, button labels are omitted intentionally because the
button has no purpose in the activity. I noticed a few obvious cases,
like Home > File Transfer and Settings > System > Language, where the up
and down button labels were missing. This change fixes those and all
similar instances I could find.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-18 21:54:02 +03:00
Xuan-Son Nguyen
6ec5fc5603 feat: lower CPU freq on idle, add HalPowerManager (#852)
## Summary

Continue my experiment from
https://github.com/crosspoint-reader/crosspoint-reader/pull/801

This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)

Update: more info in
https://github.com/crosspoint-reader/crosspoint-reader/pull/852#issuecomment-3904562827

## Testing

Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is **used** for a given duration:

| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
https://github.com/crosspoint-reader/crosspoint-reader/pull/801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |

(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.

**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**

The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
2026-02-18 17:12:29 +03:00
Zach Nelson
9125a7ce68 perf: Avoid redundant font map lookups (#933)
## Summary

**What is the goal of this PR?**

Several methods in GfxRenderer were doing a `count()` followed by `at()`
on the fonts map, effectively doing the same map lookup unnecessarily.
This can be avoided by doing a single `find()` and reusing the iterator.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-18 11:55:41 +01:00
Uri Tauber
dc6562a51c fix: Fix a dangling pointer (#939)
## Summary

* **What is the goal of this PR?** Fix a dangling pointer issue caused
by using `.c_str()` on a temporary `std::string`.

`basepath.substr()` creates a temporary `std::string`, and calling
`.c_str()` on it returns a pointer to its internal buffer (not a copy).
Since the temporary string is destroyed at the end of the full
expression, `folderName` ends up holding a dangling pointer, leading to
undefined behavior.

To solve this, we stores the result in a persistent `std::string`
object, ensuring the underlying buffer remains valid for the duration of
its use.

A similar pattern caused the behavior reported in
https://github.com/crosspoint-reader/crosspoint-reader/pull/728#issuecomment-3902529697

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-18 11:55:23 +01:00
17 changed files with 205 additions and 57 deletions

View File

@@ -74,13 +74,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
} }
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const { int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
int w = 0, h = 0; int w = 0, h = 0;
fontMap.at(fontId).getTextDimensions(text, &w, &h, style); fontIt->second.getTextDimensions(text, &w, &h, style);
return w; return w;
} }
@@ -100,11 +101,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
return; return;
} }
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return; return;
} }
const auto font = fontMap.at(fontId); const auto& font = fontIt->second;
// no printable characters // no printable characters
if (!font.hasPrintableChars(text, style)) { if (!font.hasPrintableChars(text, style)) {
@@ -709,52 +711,58 @@ int GfxRenderer::getScreenHeight() const {
} }
int GfxRenderer::getSpaceWidth(const int fontId) const { int GfxRenderer::getSpaceWidth(const int fontId) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX; return fontIt->second.getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
} }
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const { int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
uint32_t cp; uint32_t cp;
int width = 0; int width = 0;
const auto& font = fontIt->second;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
width += fontMap.at(fontId).getGlyph(cp, EpdFontFamily::REGULAR)->advanceX; width += font.getGlyph(cp, EpdFontFamily::REGULAR)->advanceX;
} }
return width; return width;
} }
int GfxRenderer::getFontAscenderSize(const int fontId) const { int GfxRenderer::getFontAscenderSize(const int fontId) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender; return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
} }
int GfxRenderer::getLineHeight(const int fontId) const { int GfxRenderer::getLineHeight(const int fontId) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY; return fontIt->second.getData(EpdFontFamily::REGULAR)->advanceY;
} }
int GfxRenderer::getTextHeight(const int fontId) const { int GfxRenderer::getTextHeight(const int fontId) const {
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return 0; return 0;
} }
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender; return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
} }
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black, void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
@@ -764,11 +772,13 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
return; return;
} }
if (fontMap.count(fontId) == 0) { const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
return; return;
} }
const auto font = fontMap.at(fontId);
const auto& font = fontIt->second;
// No printable characters // No printable characters
if (!font.hasPrintableChars(text, style)) { if (!font.hasPrintableChars(text, style)) {

View File

@@ -1,11 +1,9 @@
#include <HalGPIO.h> #include <HalGPIO.h>
#include <SPI.h> #include <SPI.h>
#include <esp_sleep.h>
void HalGPIO::begin() { void HalGPIO::begin() {
inputMgr.begin(); inputMgr.begin();
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
pinMode(BAT_GPIO0, INPUT);
pinMode(UART0_RXD, INPUT); pinMode(UART0_RXD, INPUT);
} }
@@ -23,23 +21,6 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } 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 { bool HalGPIO::isUsbConnected() const {
// U0RXD/GPIO20 reads HIGH when USB is connected // U0RXD/GPIO20 reads HIGH when USB is connected
return digitalRead(UART0_RXD) == HIGH; return digitalRead(UART0_RXD) == HIGH;

View File

@@ -38,12 +38,6 @@ class HalGPIO {
bool wasAnyReleased() const; bool wasAnyReleased() const;
unsigned long getHeldTime() 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 // Check if USB is connected
bool isUsbConnected() const; bool isUsbConnected() const;

View File

@@ -0,0 +1,95 @@
#include "HalPowerManager.h"
#include <Logging.h>
#include <WiFi.h>
#include <esp_sleep.h>
#include <cassert>
#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);
}

56
lib/hal/HalPowerManager.h Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <Arduino.h>
#include <InputManager.h>
#include <Logging.h>
#include <freertos/semphr.h>
#include <cassert>
#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;
};
};

View File

@@ -1,5 +1,7 @@
#include "Activity.h" #include "Activity.h"
#include <HalPowerManager.h>
void Activity::renderTaskTrampoline(void* param) { void Activity::renderTaskTrampoline(void* param) {
auto* self = static_cast<Activity*>(param); auto* self = static_cast<Activity*>(param);
self->renderTaskLoop(); self->renderTaskLoop();
@@ -9,6 +11,7 @@ void Activity::renderTaskLoop() {
while (true) { while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{ {
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
RenderLock lock(*this); RenderLock lock(*this);
render(std::move(lock)); render(std::move(lock));
} }

View File

@@ -1,9 +1,12 @@
#include "ActivityWithSubactivity.h" #include "ActivityWithSubactivity.h"
#include <HalPowerManager.h>
void ActivityWithSubactivity::renderTaskLoop() { void ActivityWithSubactivity::renderTaskLoop() {
while (true) { while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{ {
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
RenderLock lock(*this); RenderLock lock(*this);
if (!subActivity) { if (!subActivity) {
render(std::move(lock)); render(std::move(lock));

View File

@@ -189,7 +189,7 @@ void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) { if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
confirmLabel = tr(STR_DOWNLOAD); confirmLabel = tr(STR_DOWNLOAD);
} }
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (entries.empty()) { if (entries.empty()) {

View File

@@ -196,8 +196,8 @@ void MyLibraryActivity::render(Activity::RenderLock&&) {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
auto folderName = basepath == "/" ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1).c_str(); std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;

View File

@@ -92,7 +92,7 @@ void NetworkModeSelectionActivity::render(Activity::RenderLock&&) {
} }
// Draw help text at bottom // Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -310,8 +310,8 @@ void KOReaderSyncActivity::render(Activity::RenderLock&&) {
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1); renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1);
// Bottom button hints: show Back and Select // Bottom button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;

View File

@@ -149,7 +149,7 @@ void CalibreSettingsActivity::render(Activity::RenderLock&&) {
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -13,6 +13,7 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
void render(Activity::RenderLock&&) override; void render(Activity::RenderLock&&) override;
private: private:

View File

@@ -173,7 +173,7 @@ void KOReaderSettingsActivity::render(Activity::RenderLock&&) {
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -87,7 +87,7 @@ void LanguageSelectActivity::render(Activity::RenderLock&&) {
} }
// Button hints // Button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -34,4 +34,5 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override; void render(Activity::RenderLock&&) override;
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
}; };

View File

@@ -3,6 +3,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalDisplay.h> #include <HalDisplay.h>
#include <HalGPIO.h> #include <HalGPIO.h>
#include <HalPowerManager.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <Logging.h> #include <Logging.h>
@@ -183,7 +184,7 @@ void verifyPowerButtonDuration() {
if (abort) { if (abort) {
// Button released too early. Returning to sleep. // Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again // 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", "Power button press calibration value: %lu ms", t2 - t1);
LOG_DBG("MAIN", "Entering deep sleep"); LOG_DBG("MAIN", "Entering deep sleep");
gpio.startDeepSleep(); powerManager.startDeepSleep(gpio);
} }
void onGoHome(); void onGoHome();
@@ -283,6 +284,7 @@ void setup() {
t1 = millis(); t1 = millis();
gpio.begin(); gpio.begin();
powerManager.begin();
// Only start serial if USB connected // Only start serial if USB connected
if (gpio.isUsbConnected()) { if (gpio.isUsbConnected()) {
@@ -319,7 +321,7 @@ void setup() {
case HalGPIO::WakeupReason::AfterUSBPower: case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep // If USB power caused a cold boot, go back to sleep
LOG_DBG("MAIN", "Wakeup reason: After USB Power"); LOG_DBG("MAIN", "Wakeup reason: After USB Power");
gpio.startDeepSleep(); powerManager.startDeepSleep(gpio);
break; break;
case HalGPIO::WakeupReason::AfterFlash: case HalGPIO::WakeupReason::AfterFlash:
// After flashing, just proceed to boot // After flashing, just proceed to boot
@@ -392,6 +394,7 @@ void loop() {
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { 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(); 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 // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
// Otherwise, use longer delay to save power // Otherwise, use longer delay to save power
if (currentActivity && currentActivity->skipLoopDelay()) { if (currentActivity && currentActivity->skipLoopDelay()) {
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 yield(); // Give FreeRTOS a chance to run tasks, but return immediately
} else { } else {
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // 3 seconds if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
if (millis() - lastActivityTime >= IDLE_POWER_SAVING_MS) {
// If we've been inactive for a while, increase the delay to save power // 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); delay(50);
} else { } else {
// Short delay to prevent tight loop while still being responsive // Short delay to prevent tight loop while still being responsive