diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 278a3f7..7b087be 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1662,6 +1662,27 @@ void loop() { // CPU frequency auto-timeout back to idle cpuPower.loop(); + // Low-power mode — drop CPU to 40 MHz and throttle loop when lock screen + // is active. The mesh radio has its own FIFO so packets are buffered; + // 50 ms yield means the loop still runs 20×/sec which is more than enough + // to drain the radio FIFO before overflow. +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) + { + static bool wasLocked = false; + bool nowLocked = ui_task.isLocked(); + if (nowLocked && !wasLocked) { + cpuPower.setLowPower(); + Serial.printf("[Power] Low-power mode: CPU %d MHz, loop throttled\n", + cpuPower.getFrequencyMHz()); + } else if (!nowLocked && wasLocked) { + cpuPower.clearLowPower(); + Serial.printf("[Power] Normal mode: CPU %d MHz\n", + cpuPower.getFrequencyMHz()); + } + wasLocked = nowLocked; + } +#endif + // Audiobook: service audio decode regardless of which screen is active #if defined(LilyGo_TDeck_Pro) && !defined(HAS_4G_MODEM) { @@ -2182,6 +2203,16 @@ void loop() { } } #endif + + // Low-power loop throttle — yield CPU when lock screen is active. + // The RTOS idle task executes WFI (wait-for-interrupt) during delay(), + // dramatically reducing CPU power draw. 50 ms gives 20 loop cycles/sec + // which is ample for LoRa packet reception (radio has hardware FIFO). +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) + if (ui_task.isLocked()) { + delay(50); + } +#endif } // ============================================================================ diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 54cf96b..5536bb1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1844,7 +1844,7 @@ void UITask::lockScreen() { _next_refresh = 0; // Draw lock screen immediately _auto_off = millis() + 60000; // 60s before display off while locked _lastLockRefresh = millis(); // Start 15-min clock refresh cycle - Serial.println("[UI] Screen locked"); + Serial.println("[UI] Screen locked — entering low-power mode"); } void UITask::unlockScreen() { @@ -1863,7 +1863,7 @@ void UITask::unlockScreen() { _auto_off = millis() + AUTO_OFF_MILLIS; _lastInputMillis = millis(); // Reset auto-lock idle timer _next_refresh = 0; - Serial.println("[UI] Screen unlocked"); + Serial.println("[UI] Screen unlocked — exiting low-power mode"); } #endif // LilyGo_T5S3_EPaper_Pro || LilyGo_TDeck_Pro diff --git a/merge_firmware.py b/merge_firmware.py index 752995f..01cab2a 100644 --- a/merge_firmware.py +++ b/merge_firmware.py @@ -21,7 +21,7 @@ def merge_bin(source, target, env): bootloader = os.path.join(build_dir, "bootloader.bin") partitions = os.path.join(build_dir, "partitions.bin") firmware = os.path.join(build_dir, "firmware.bin") - output = os.path.join(build_dir, "firmware_merged.bin") + output = os.path.join(build_dir, "firmware-merged.bin") # Verify all inputs exist for f in [bootloader, partitions, firmware]: diff --git a/readback.bin b/readback.bin new file mode 100644 index 0000000..dd5a273 Binary files /dev/null and b/readback.bin differ diff --git a/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h b/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h index d1f33a5..4099db8 100644 --- a/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h +++ b/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h @@ -8,9 +8,10 @@ // 240 MHz ~70-80 mA // 160 MHz ~50-60 mA // 80 MHz ~30-40 mA +// 40 MHz ~15-20 mA (low-power / lock screen mode) // // SPI peripherals and UART use their own clock dividers from the APB clock, -// so LoRa, e-ink, and GPS serial all work fine at 80MHz. +// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz. #ifdef ESP32 @@ -22,23 +23,36 @@ #define CPU_FREQ_BOOST 240 // MHz — heavy processing #endif +#ifndef CPU_FREQ_LOW_POWER +#define CPU_FREQ_LOW_POWER 40 // MHz — lock screen / idle standby +#endif + #ifndef CPU_BOOST_TIMEOUT_MS #define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds #endif class CPUPowerManager { public: - CPUPowerManager() : _boosted(false), _boost_started(0) {} + CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {} void begin() { setCpuFrequencyMhz(CPU_FREQ_IDLE); _boosted = false; + _lowPower = false; MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); } void loop() { if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { - setIdle(); + // Return to low-power if locked, otherwise normal idle + if (_lowPower) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER); + } else { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + _boosted = false; } } @@ -57,13 +71,42 @@ public: _boosted = false; MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); } + if (_lowPower) { + _lowPower = false; + } + } + + // Low-power mode — drops CPU to 40 MHz for lock screen standby. + // If currently boosted, the boost timeout will return to 40 MHz + // instead of 80 MHz. + void setLowPower() { + _lowPower = true; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER); + } + // If boosted, the loop() timeout will drop to low-power instead of idle + } + + // Exit low-power mode — returns to normal idle (80 MHz). + // If currently boosted, the boost timeout will return to idle + // instead of low-power. + void clearLowPower() { + _lowPower = false; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE); + } + // If boosted, the loop() timeout will drop to idle as normal } bool isBoosted() const { return _boosted; } + bool isLowPower() const { return _lowPower; } uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } private: bool _boosted; + bool _lowPower; unsigned long _boost_started; }; diff --git a/variants/lilygo_tdeck_pro/CPUPowerManager.h b/variants/lilygo_tdeck_pro/CPUPowerManager.h index d1f33a5..4099db8 100644 --- a/variants/lilygo_tdeck_pro/CPUPowerManager.h +++ b/variants/lilygo_tdeck_pro/CPUPowerManager.h @@ -8,9 +8,10 @@ // 240 MHz ~70-80 mA // 160 MHz ~50-60 mA // 80 MHz ~30-40 mA +// 40 MHz ~15-20 mA (low-power / lock screen mode) // // SPI peripherals and UART use their own clock dividers from the APB clock, -// so LoRa, e-ink, and GPS serial all work fine at 80MHz. +// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz. #ifdef ESP32 @@ -22,23 +23,36 @@ #define CPU_FREQ_BOOST 240 // MHz — heavy processing #endif +#ifndef CPU_FREQ_LOW_POWER +#define CPU_FREQ_LOW_POWER 40 // MHz — lock screen / idle standby +#endif + #ifndef CPU_BOOST_TIMEOUT_MS #define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds #endif class CPUPowerManager { public: - CPUPowerManager() : _boosted(false), _boost_started(0) {} + CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {} void begin() { setCpuFrequencyMhz(CPU_FREQ_IDLE); _boosted = false; + _lowPower = false; MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); } void loop() { if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { - setIdle(); + // Return to low-power if locked, otherwise normal idle + if (_lowPower) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER); + } else { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + _boosted = false; } } @@ -57,13 +71,42 @@ public: _boosted = false; MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); } + if (_lowPower) { + _lowPower = false; + } + } + + // Low-power mode — drops CPU to 40 MHz for lock screen standby. + // If currently boosted, the boost timeout will return to 40 MHz + // instead of 80 MHz. + void setLowPower() { + _lowPower = true; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER); + } + // If boosted, the loop() timeout will drop to low-power instead of idle + } + + // Exit low-power mode — returns to normal idle (80 MHz). + // If currently boosted, the boost timeout will return to idle + // instead of low-power. + void clearLowPower() { + _lowPower = false; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE); + } + // If boosted, the loop() timeout will drop to idle as normal } bool isBoosted() const { return _boosted; } + bool isLowPower() const { return _lowPower; } uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } private: bool _boosted; + bool _lowPower; unsigned long _boost_started; };