Skip to content

feat: Dynamic light sleep support for ESP32 platform#7121

Open
m1nl wants to merge 31 commits into
meshtastic:developfrom
m1nl:esp_light_sleep_release
Open

feat: Dynamic light sleep support for ESP32 platform#7121
m1nl wants to merge 31 commits into
meshtastic:developfrom
m1nl:esp_light_sleep_release

Conversation

@m1nl
Copy link
Copy Markdown
Contributor

@m1nl m1nl commented Jun 24, 2025

fixes #6660

I've finally implemented the Feature Request I created a few months ago. Since then, I've been testing the dynamic light-sleep functionality and haven't encountered any issues.

In the initial version, legacy light-sleep support with the default Arduino framework could lead to problems after my changes. To address this, I had to rewrite and refactor parts of the code to ensure compatibility with builds compiled using the default PlatformIO Arduino framework.

I tested the updated implementation on a Heltec V3 board in the following scenarios:

  • default Arduino, bluetooth, light-sleep disabled
  • default Arduino, bluetooth, light-sleep enabled
  • default Arduino, bluetooth, light-sleep enabled, force disable support for EXT wake-up
  • customized Arduino, bluetooth, light-sleep disabled
  • customized Arduino, bluetooth, light-sleep enabled
  • customized Arduino, bluetooth, light-sleep enabled, force disable support for EXT wake-up
  • customized Arduino, bluetooth disabled, light-sleep enabled
  • customized Arduino, WiFi enabled, MQTT enabled, light-sleep enabled

There are a few topics that need discussion:

  1. Power Management (PM) Support for ESP32 requires a custom variant of the Arduino framework. I’ve forked it from Espressif’s GitHub repository and am currently hosting it under my personal GitHub account. If these changes are accepted, I’d like to move the repository under the meshtastic organization.
  2. In my opinion, re-enabling Bluetooth on the ESP32 without restarting is not possible, so NB PowerFSM state seems redundant. I'm open to restoring it if necessary, but I’d appreciate guidance on the intended logic behind this state.
  3. I updated the platformio.ini files for the hardware variants I was able to test. I shared customized builds with others, who confirmed they were working as expected.
  4. I updated the variant.h files for the hardware variants which do have 32kHz oscillator, according to Espressif docs, it helps with Bluetooth stability when dynamic light-sleep is enabled

Links to repositiories mentioned in 1) :

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631
    • Seeed Studio T-1000E tracker card
    • Other (please specify below)

@m1nl m1nl force-pushed the esp_light_sleep_release branch 4 times, most recently from 9526383 to d8f2fcd Compare June 29, 2025 20:12
@fifieldt
Copy link
Copy Markdown
Member

fifieldt commented Jul 1, 2025

This is a ton of work, thank you very much

@fifieldt
Copy link
Copy Markdown
Member

fifieldt commented Jul 1, 2025

If you have time .... I "maintain" the GPS code - just wondering what the impact might be there or whether we might need further extensions?

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Jul 1, 2025

AFAIK dynamic light-sleep in ESP32 platform makes the chip light-sleep when FreeRTOS waits for an event or there is an explicit vTaskDelay call (=~ sleep in Arduino). So most of the functionality shouldn't be impacted as Arduino runs all the code in one loop, which waits different intervals depending on the type of thread (using vTaskDelay under the hood). However, for critical modules or core functionality I added an explicit call to PowerFSM.trigger to exit LS state and allow for at least 300ms of uninterrupted processing time; another approach is to use "preflightSleepObserver" which prevents entering LS state if there is some work pending (like TX queue is not empty). I took a look at GPS code and I think we should exit LS state when we change state to GPS_ACTIVE and prevent entering LS as long as the state doesn't change. I'll take a look at the code and propose some changes soon.

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Jul 1, 2025

If there is a chance to have this feature merged, I'd appreciate getting some feedback on the additional repos to be hosted under meshtastic Github organization. As mentioned above, default Arduino framework provided by Espressif doesn't enable PM functionality at all (which is really bad IMO).

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Jul 2, 2025

@fifieldt changes committed; device should not go into light-sleep when GPS position is requested (=module is in GPS_ACTIVE state). I'm unable to test as I'm on vacation right now.

@m1nl m1nl force-pushed the esp_light_sleep_release branch from 151def2 to a42c8f6 Compare July 3, 2025 17:41
@m1nl m1nl force-pushed the esp_light_sleep_release branch from a42c8f6 to 892f77b Compare July 12, 2025 11:44
@vidplace7 vidplace7 added the enhancement New feature or request label Jul 18, 2025
@m1nl m1nl force-pushed the esp_light_sleep_release branch from 892f77b to 4223701 Compare July 20, 2025 19:55
@m1nl m1nl force-pushed the esp_light_sleep_release branch from 4223701 to f58f5e5 Compare July 31, 2025 13:17
@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Aug 7, 2025

hi,

  1. Bumping my last question
    I'd appreciate getting some feedback on hosting additional repos under meshtastic Github organization. Default Arduino framework provided by Espressif doesn't enable PM functionality at all. I'm happy to move content of my repos to new ones. There are two repos to be created - arduino-esp32 and esp32-arduino-lib-builder

  2. I took a look at the code and it reminded me I did change the WiFi code a bit - I'm enabling power saving by default (regardless of powersaving setting in module options); I think I should revoke that change to ensure compatibility with older versions; let me know what you think

  3. I'd appreciate getting feedback on removing NB sleep state - as mentioned in the description for this PR 1) it doesn't work for ESP32 (no way to reenable BT after disabling it without reboot) 2) it doesn't make much sense for NRF platform. However I see it's used in Router so Bluetooth is disabled on NRF platform when device goes into this state - is this expected? IMO this creates some confusion as ESP32 FW behaves differently than NRF one.

@m1nl m1nl force-pushed the esp_light_sleep_release branch from 058e904 to 50151fc Compare August 9, 2025 21:06
Comment thread src/sleep.cpp Outdated
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT

#if defined(ARCH_ESP32) && !HAS_TFT
#ifdef HAS_WIFI
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

#if HAS_WIFI

Copy link
Copy Markdown

@metala metala Aug 23, 2025

Choose a reason for hiding this comment

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

Would it also make sense to have the HAS_ESP32_DYNAMIC_LIGHT_SLEEP (and HAS_ESP32_PM_SUPPORT, HAS_32768HZ) as a bool as well, instead of just checking #ifdef ?

It's more broad discussion about convention. This has to be decided on project level I guess.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure, I'll check these definitions and I can update them to bool. I see there is inconsistency about how we use HAS_XYZ flags in the project. I will create a separate PR for this.

@metala
Copy link
Copy Markdown

metala commented Aug 23, 2025

General critique about having a lot of assert(res == ESP_OK);:

While I've already make a boot loop with those asserts because of a wrong setting, the more concerning part is that assert() signifies an unrecoverable error and this should never happen, unless the system is in unstable/undefined state.

Surely some of the calls can fail in runtime or due to a wrong setting. Should we then crash the device? There is a known issue here that some ESP32 devices corrupt their device configuration when running on solar power. This not only increases the risk, but also cause the device to become irrecoverable, because of a wrong setting.

Example:
esp_sleep_enable_timer_wakeup() can return ESP_ERR_INVALID_ARG if value is out of range. Therefore if the sleep time (sleepLeft = config.power.ls_secs * 1000LL - sleepTime;) gets somehow messed due to a wrong ls_secs setting, the device might be hard to restore through the radio.

I'd recommend an approach that would fail in a recoverable way.

@metala
Copy link
Copy Markdown

metala commented Aug 23, 2025

/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/heltec-v2_1/firmware.elf section `.iram0.text' will not fit in region `iram0_0_seg'
/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: IRAM0 segment data does not fit.
/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: region `iram0_0_seg' overflowed by 776 bytes
collect2: error: ld returned 1 exit status

The linker fails on ESP32 (not S3).

Comment thread variants/esp32s3/heltec_v3/platformio.ini Outdated
@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Aug 24, 2025

/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/heltec-v2_1/firmware.elf section `.iram0.text' will not fit in region `iram0_0_seg'
/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: IRAM0 segment data does not fit.
/work/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: region `iram0_0_seg' overflowed by 776 bytes
collect2: error: ld returned 1 exit status

The linker fails on ESP32 (not S3).

This fails because the ESP32 platform has very limited IRAM available, and enabling PM capabilities pushes it beyond the limit. I’ll explore making the ESP-IDF components lighter to reduce memory usage without affecting Meshtastic functionality, but that may not be feasible. In the end, we may have to accept that dynamic light-sleep isn’t compatible with the ESP32.

In newer ESP-IDF versions (5.x compatible with Arduino 3.2.x), there’s a new sdkconfig option CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM, which makes more RAM available as IRAM, allowing the build to succeed. However, adopting this would be a breaking change for the project, since it requires moving from Arduino 2.x to 3.x. I’ve already created and tested such a build with the current Meshtastic version, but this shift is too significant to include in this PR.

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Aug 24, 2025

General critique about having a lot of assert(res == ESP_OK);:

While I've already make a boot loop with those asserts because of a wrong setting, the more concerning part is that assert() signifies an unrecoverable error and this should never happen, unless the system is in unstable/undefined state.

Surely some of the calls can fail in runtime or due to a wrong setting. Should we then crash the device? There is a known issue here that some ESP32 devices corrupt their device configuration when running on solar power. This not only increases the risk, but also cause the device to become irrecoverable, because of a wrong setting.

Example: esp_sleep_enable_timer_wakeup() can return ESP_ERR_INVALID_ARG if value is out of range. Therefore if the sleep time (sleepLeft = config.power.ls_secs * 1000LL - sleepTime;) gets somehow messed due to a wrong ls_secs setting, the device might be hard to restore through the radio.

I'd recommend an approach that would fail in a recoverable way.

Understood, I'll check if there is any assertion, which relies on user settings and make them fail safe. However I'd recommend to keep assertions for all calls, which use predefined values (board configuration, etc.). IMO this is the only way to validate the changes with all devices during alpha testing phase. Let me know what you think.

@m1nl m1nl marked this pull request as draft August 26, 2025 15:30
@m1nl m1nl force-pushed the esp_light_sleep_release branch from 2815ae5 to 89b296d Compare August 26, 2025 16:17
@m1nl m1nl changed the base branch from master to develop August 26, 2025 16:18
@m1nl m1nl force-pushed the esp_light_sleep_release branch from 89b296d to 4b22d1d Compare August 26, 2025 21:30
@m1nl m1nl requested a review from metala August 26, 2025 21:32
@m1nl m1nl force-pushed the esp_light_sleep_release branch from 4b22d1d to 2e860a8 Compare August 26, 2025 21:49
@akohlsmith
Copy link
Copy Markdown

I'm testing this on ESP32C3 and so far the testing is looking very good.

My only nitpick is that I think that there should be a comment in variants/esp32c3/heltec_esp32c3/variant.h (and others) which mentions the HAS_32768HZ macro and that it should be uncommented if your board has a 32kHz crystal. One of the big issues with supporting so many platforms is that these definitions tend to get buried in code.

e.g.

// uncomment this is your board has a 32kHz crystal
//#define HAS_32768HZ 1

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 22, 2026

@m1nl would your recommendation be to switch to 2048 for now?

I can't provide any specific reference for that particular value, but my tests show that anything less than 2048 breaks Bluetooth when dynamic light-sleep is enabled; I couldn't find any Espressif materials explaining how to optimize this value.

This is my initial implementation of keeping NodeDB in PSRAM, which significantly reduces heap usage:
m1nl@41eb876 ; I can work on that, but I'd like to finish this feature first.

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 22, 2026

@thebentern I spent several days debugging why this PR made SPIRAM-capable devices extremely unstable when using latest develop branch. I was able to narrow the issue down to commit 5910cc2, which changes the SPIRAM allocation threshold to a very aggressive value of 256 bytes.
We need to keep in mind that SPIRAM is significantly slower than internal RAM, and moving small allocations there can easily cause subtle and hard-to-debug failures. In my case, enabling dynamic light sleep made the device completely unresponsive. With a 256-byte threshold, even very small buffers are pushed to SPIRAM, and this was causing Bluetooth to hang after sleep.

Wow, this is amazing debugging. I have run into similar issues on other projects but wasn't paying close enough attention here.

Does this PR allow I2C to come up early enough to enable the use of INAxxx devices to detect battery voltage for deep sleep? This was the biggest source of frustration in my earlier attempts at enabling light sleep for ESP32 and why my PRs were so difficult to manage. The devices I design do not use an ADC channel for battery voltage at all, and I had significant hacks to do be able to read the current/voltage sensor early enough.

Hey :) I think it does - when dynamic light sleep is enabled, all scheduled tasks will take place as requested. ESP-IDF will wake-up the device when next OSThread is to be run, so routines in Power.c should run as expected.

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 22, 2026

One other question -- my own version of the espressif-arduino enabled PM for ESP32 (ESP32C3 only) also enables DFS and enables the use of an optional external 32.768kHz crystal. I took a brief look at your branch and didn't see either of these enabled. Is there any reason not to?

  1. I think my code does enable DFS - take a look here https://github.com/m1nl/meshtastic-firmware/blob/1df8c24c02f1f13b7a0554d02461d653b09591d1/src/sleep.cpp#L502 .
  2. 32.768kHz crystal is enabled for supported devices; there is a flag in variant.h, which enables that feature:
...
#define HAS_32768HZ 1

I believe it may be missing from several variants as defined in repo (i.e. T-Lora Pager, no schematic is available, but it can be enabled successfully).

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 25, 2026

@thebentern is there a chance you could help with the item below?

- my changes require ESP-IDF / Arduino with custom sdkconfig (https://github.com/m1nl/esp32-arduino-lib-builder) - would it be possible to move my repo under the meshtastic organization? I’ve added automated CI builds using GitHub Actions - everything is transparent now and no binary blobs are hosted in the repository.

I can set up the CI pipeline and migrate the repository contents. After that, I’ll update the PR so all links to the custom Arduino framework point to releases under the Meshtastic GitHub organization.

@akohlsmith
Copy link
Copy Markdown

akohlsmith commented Feb 25, 2026

I can confirm that both 32k crystal switchover (as well as failure to switch over) and DFS are active and working with this PR:

DEBUG | ??:??:?? 1 32k XTAL OSC has not started up
WARN  | ??:??:?? 1 Failed to switch 32K XTAL RTC source to 32.768kHz !!!

...

DEBUG | ??:??:?? 0 Switch RTC Source to 32.768kHz succeeded, using 32k XTAL

...

INFO  | ??:??:?? 2 initLightSleep() dfsSupported=1, ret=0, set/get: min_freq: 20/20, max_freq 80/80, ls_ena: 1/1
INFO  | ??:??:?? 2 PM config enabled - min_freq_mhz=20, max_freq_mhz=80, light_sleep_enable=1
INFO  | ??:??:?? 2 PowerFSM init, has_power=1, is_power_saving=1, wake_time_ms=10000

I still need to test whether the INA219 will work EARLY in the boot (it is detected very early) to drop into deep sleep if the battery voltage is below the threshold, but this is excellent work!

@akohlsmith
Copy link
Copy Markdown

I do not see light sleep getting enabled, only periodic AGC reset attempts and messages about gpio_hold_en being attempted on output-capable IOs:

[2026-02-25 16:00:41] DEBUG | ??:??:?? 104 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
[2026-02-25 16:00:42] E (106235) gpio: gpio_hold_en(641): Only output-capable GPIO support this function
[2026-02-25 16:00:42] E (106238) gpio: gpio_hold_en(641): Only output-capable GPIO support this function
[2026-02-25 16:00:42] E (106245) gpio: gpio_hold_en(641): Only output-capable GPIO support this function
[2026-02-25 16:00:42] E (106252) gpio: gpio_hold_en(641): Only output-capable GPIO support this function
[2026-02-25 16:00:42] E (106260) gpio: gpio_hold_en(641): Only output-capable GPIO support this function
[2026-02-25 16:01:41] DEBUG | ??:??:?? 164 attempting AGC reset
[2026-02-25 16:01:41] DEBUG | ??:??:?? 224 attempting AGC reset
[2026-02-25 16:02:41] DEBUG | ??:??:?? 284 attempting AGC reset
[2026-02-25 16:03:41] DEBUG | ??:??:?? 344 attempting AGC reset

I do have light sleep enabled in the configuration:

$ meshtastic --port /dev/cu.usbserial-14101 --get power
Connected to radio
power.is_power_saving: True
power.wait_bluetooth_secs: 60
power.sds_secs: 3600
power.ls_secs: 86400
power.min_wake_secs: 60
power.device_battery_ina_address: 64
Completed getting preferences

I also do not see the "Using INA on I2C addr 0x%x for charging detection" message on bootup, although I don't see why I shouldn't: I have an INA219, I have telemetry enabled (I see those messages in the log and being transmitted over LoRa), I'm not excluding environmental sensing and DISABLE_INA_CHARGING_DETECTION isn't defined anywhere.

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

  1. GPIO hold errors happen because sb decided to use 255 as placeholder value for TB_* defines instead of just keeping them undefined. This is not critical.

  2. You won't see device explicitly entering light sleep as it's only releasing a PM lock in light sleep PowerFSM state so FreeRTOS can put device into sleep when idle. This also allows device to keep Bluetooth connection active with very small impact on overall power consumption.

@akohlsmith
Copy link
Copy Markdown

  1. GPIO hold errors happen because sb decided to use 255 as placeholder value for TB_* defines instead of just keeping them undefined. This is not critical.
  2. You won't see device explicitly entering light sleep as it's only releasing a PM lock in light sleep PowerFSM state so FreeRTOS can put device into sleep when idle. This also allows device to keep Bluetooth connection active with very small impact on overall power consumption.

yep I just realized that after adding some explicit debugging. I've got trivial patch to fix that since it's cluttering up the debug logs.

Any idea why the INA219 is correctly detected and the configuration does reference it correctly (power.device_battery_ina_address = 64) yet it's not being used for charge or battery voltage? That's the last thing I'm running against (but it was also the biggest pain in my ass when I was doing my hack-job of ESP32 light sleep)

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

Does INA219 get properly detected with stock firmware ? It may be out of scope of my changes but I can take a look later during the day.

@akohlsmith
Copy link
Copy Markdown

Does INA219 get properly detected with stock firmware ? It may be out of scope of my changes but I can take a look later during the day.

yep it's absolutely detected by your branch (and stock) without any changes whatsoever. The power telemetry is also working great:

[2026-02-25 16:26:58] INFO  | ??:??:?? 102 [PowerTelemetry] Send: ch1_voltage=0.000000, ch1_current=0.000000, ch2_voltage=0.000000, ch2_current=0.000000, ch3_voltage=3.668000, ch3_current=427.899994

@akohlsmith
Copy link
Copy Markdown

it looks like the AGC reset seems to never re-enter light sleep -- every minute it attempts an AGC reset, but I never see doLightSleep() get called until after a packet is received:

[2026-02-25 16:27:00] INFO  | ??:??:?? 104 [PowerFSM] ABK doLightSleep(39146 ms)
[2026-02-25 16:27:00] DEBUG | ??:??:?? 104 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
[2026-02-25 16:27:00] DEBUG | ??:??:?? 164 attempting AGC reset
[2026-02-25 16:28:00] DEBUG | ??:??:?? 224 attempting AGC reset
[2026-02-25 16:29:00] DEBUG | ??:??:?? 284 attempting AGC reset
[2026-02-25 16:30:00] DEBUG | ??:??:?? 344 attempting AGC reset
[2026-02-25 16:31:00] DEBUG | ??:??:?? 404 attempting AGC reset

I'm digging into this a little more but wanted to mention it in case you might already know what's going on

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

it looks like the AGC reset seems to never re-enter light sleep -- every minute it attempts an AGC reset, but I never see doLightSleep() get called until after a packet is received:


[2026-02-25 16:27:00] INFO  | ??:??:?? 104 [PowerFSM] ABK doLightSleep(39146 ms)

[2026-02-25 16:27:00] DEBUG | ??:??:?? 104 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt

[2026-02-25 16:27:00] DEBUG | ??:??:?? 164 attempting AGC reset

[2026-02-25 16:28:00] DEBUG | ??:??:?? 224 attempting AGC reset

[2026-02-25 16:29:00] DEBUG | ??:??:?? 284 attempting AGC reset

[2026-02-25 16:30:00] DEBUG | ??:??:?? 344 attempting AGC reset

[2026-02-25 16:31:00] DEBUG | ??:??:?? 404 attempting AGC reset

I'm digging into this a little more but wanted to mention it in case you might already know what's going on

Please see my comment above (the one about PM lock and FreeRTOS, AGC resets do not matter here).

@akohlsmith
Copy link
Copy Markdown

nevermind, I think I just got the point of what you said. 🤦

If HAS_ESP32_DYNAMIC_LIGHT_SLEEP is defined, we aren't actually setting up any kind of light sleep, only acquiring the light sleep lock so FreeRTOS does its thing -- we still enable the wakeup sources but the actual call to esp_light_sleep_start() never happens since it's unnecessary.

Now the logs make more sense:

[2026-02-25 16:44:49] INFO  | ??:??:?? 473 [PowerFSM] ABK doLightSleep(-1 ms): enter
[2026-02-25 16:44:50] DEBUG | ??:??:?? 473 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
[2026-02-25 16:44:50] INFO  | ??:??:?? 473 [PowerFSM] ABK doLightSleep(-1 ms): exit, wake cause 4 (ESP_SLEEP_WAKEUP_TIMER).
[2026-02-25 16:44:50] DEBUG | ??:??:?? 533 attempting AGC reset
[2026-02-25 16:45:49] DEBUG | ??:??:?? 593 attempting AGC reset

What's interesting is that receiving a packet also wakes up with the same wakeCause -- I would have expected it to wake from GPIO.

@akohlsmith
Copy link
Copy Markdown

I've also figured out the INA219 not being used for battery level -- it's the same issue as I had when I was doing this last year: the battery level stuff is VERY strongly tied to AnalogBatteryLevel, which isn't at all what I'm doing on my own hardware.

I'll rely on the brownout tripping deep sleep instead for the time being. I'm excited to see your PR merged, this is very excellent work. Thank you!

@cpatulea
Copy link
Copy Markdown
Contributor

@tdovera
Copy link
Copy Markdown

tdovera commented Feb 26, 2026

I’ve been testing the Meshtastic develop branch firmware for several days, compiled with the only modification of using the following in the heltec v3 platformio.ini:

platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/m1nl/arduino-esp32/archive/refs/tags/2.0.17+5ae9873e.tar.gz ; disable WiFi IRAM optimizations in ESP-IDF

With this build on the Heltec V3, the LiPo battery current consumption drops from about 100 mA to around 50 mA, and I haven’t noticed any side effects with Bluetooth or WiFi enabled. In my case, with a 1200 mAh LiPo battery, I can finally exceed 20 hours of operation while keeping Bluetooth continuously connected to the phone app.

@m1nl, you’ve done a great job reducing power consumption by enabling some PM features for the ESP32-S3.

By porting the additional dynamic light sleep patches, it’s probably to achieve even better results.

@akohlsmith
Copy link
Copy Markdown

akohlsmith commented Feb 26, 2026

I developed a tiny node (Heltec HT-CT62 based) and with my earlier changes it would basically stay at ~11mA which is roughly the limit (SX1262 with LNA on and in RX mode is ~10mA) -- with this PR I'm measuring ~16-18mA but have to verify that it's not because of the environmental sensor, which can draw a few mA due to the heater.

This patch does enable both DFS and DLS -- I tried briefly just #undef HAS_ESP32_DYNAMIC_LIGHT_SLEEP in variant.h and rebuilding, but it seems to still be building with dynamic light sleep enabled -- will try blowing away the build and building from a clean slate next just to compare, but it should not be a big difference in overall power consumption.

@akohlsmith
Copy link
Copy Markdown

I am noticing WAY more frequent wakeups from light sleep than I expected:

INFO  | 09:56:25 1665 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:25 1666 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:27 1667 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:27 1668 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:31 1671 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:31 1672 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:35 1675 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:35 1676 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:37 1677 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:37 1678 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:39 1679 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:39 1680 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:41 1681 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:41 1682 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt

I thought it might be related to being connected via bluetooth but after disconnecting and rebooting, I am still seeing the same.

Why is power status waking us up every 2-3 seconds?

@akohlsmith
Copy link
Copy Markdown

I also noticed if I disable bluetooth in the settings the node tends to hang after about a minute or two of booting. I have not been able to debug this further yet. Re-enabling bluetooth seems to correct the behaviour (and also seems to have reset the waking up every two seconds which makes no sense at all)

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

I’ve been testing the Meshtastic develop branch firmware for several days, compiled with the only modification of using the following in the heltec v3 platformio.ini:

platform_packages = platformio/framework-arduinoespressif32 @ https://github.com/m1nl/arduino-esp32/archive/refs/tags/2.0.17+5ae9873e.tar.gz ; disable WiFi IRAM optimizations in ESP-IDF

With this build on the Heltec V3, the LiPo battery current consumption drops from about 100 mA to around 50 mA, and I haven’t noticed any side effects with Bluetooth or WiFi enabled. In my case, with a 1200 mAh LiPo battery, I can finally exceed 20 hours of operation while keeping Bluetooth continuously connected to the phone app.

@m1nl, you’ve done a great job reducing power consumption by enabling some PM features for the ESP32-S3.

By porting the additional dynamic light sleep patches, it’s probably to achieve even better results.

Interesting, thanks for feedback! These are the sdkconfig options I've enabled (pretty standard):
https://github.com/m1nl/esp32-arduino-lib-builder/blob/release/v4.4/configs/defconfig.common#L97

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

I am noticing WAY more frequent wakeups from light sleep than I expected:

INFO  | 09:56:25 1665 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:25 1666 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:27 1667 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:27 1668 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:31 1671 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:31 1672 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:35 1675 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:35 1676 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:37 1677 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:37 1678 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:39 1679 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4188 pcnt 99
DEBUG | 09:56:39 1680 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt
INFO  | 09:56:41 1681 [Power] ABK Power::readPowerStatus() hasbattery 1, powered 0, charging 0, mv 4184 pcnt 99
DEBUG | 09:56:41 1682 [PowerFSM] Setup radio interrupt (GPIO03) with wakeup by GPIO interrupt

I thought it might be related to being connected via bluetooth but after disconnecting and rebooting, I am still seeing the same.

Why is power status waking us up every 2-3 seconds?

It does not happen with my devices. Are you using any of these interrupts below? Power thread interval should 20 seconds at minimum.

...
#ifdef EXT_PWR_DETECT
    attachInterrupt(
        EXT_PWR_DETECT,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
        },
        CHANGE);
#endif
#ifdef BATTERY_CHARGING_INV
    attachInterrupt(
        BATTERY_CHARGING_INV,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
        },
        CHANGE);
#endif
#ifdef EXT_CHRG_DETECT
    attachInterrupt(
        EXT_CHRG_DETECT,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
        },
        CHANGE);
#endif
...

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented Feb 26, 2026

I also noticed if I disable bluetooth in the settings the node tends to hang after about a minute or two of booting. I have not been able to debug this further yet. Re-enabling bluetooth seems to correct the behaviour (and also seems to have reset the waking up every two seconds which makes no sense at all)

Actually it makes sense - without Bluetooth enabled, sleep intervals are much longer and interrupts can be missed unless GPIOs are set to wake up device from sleep. I didn't notice any hang when Bluetooth is disabled - when does it happen? Are you able to provide fragment of logs from device around the time when it hangs?

@akohlsmith
Copy link
Copy Markdown

Actually it makes sense - without Bluetooth enabled, sleep intervals are much longer and interrupts can be missed unless GPIOs are set to wake up device from sleep. I didn't notice any hang when Bluetooth is disabled - when does it happen? Are you able to provide fragment of logs from device around the time when it hangs?

It will hang in the middle of emitting a log message to the serial port -- I haven't made any changes around enabling GPIO for wake from interrupt, but perhaps dynamic light sleep is skipping this part (I see log messages saying otherwise, but every wakeup I've documented has been from a timer, not a GPIO, even when receiving a packet).

I'll re-test of course but was surprised at this. I was relying on VERY long sleeps with my hacked up version of power saving and that was working reliably since I think the sleep interval was something on the order of 10 or even 30 minutes for housekeeping/transmitting telemetry (with GPIO interrupts waking it earlier of course).

@iuvi7
Copy link
Copy Markdown

iuvi7 commented Apr 10, 2026

@m1nl Hi! Heltec make revision of their v4 schematic, and released v.4.3 board version

There was huge fix improving RX sensitivity (merge #9906) so on today 2.7.21 (this one) is the only one firmware that works fine on 4.3 boards and allow them TX\RX normally

If it possible to ask m1nl version with powersaving for 2.7.21 ? 🙏

Thank you

@dblaber
Copy link
Copy Markdown

dblaber commented Apr 10, 2026

I have been using this PR branches revision on my daily carry for around a month now without issue (and extremely decent battery consumption). Wondering what is remaining to have this merged, as believe this to be extremely beneficial.

@vidplace7
Copy link
Copy Markdown
Member

This needs to be reworked now that the Arduino 3.x work has been merge (to develop branch).

@iuvi7
Copy link
Copy Markdown

iuvi7 commented May 20, 2026

@dblaber Totally agree, seems what @vidplace7 mentioned moves this PR even further from merging 😢

@m1nl
Copy link
Copy Markdown
Contributor Author

m1nl commented May 21, 2026

Hey, I will try to migrate the changes. Actually I did test them with pioarduino before. But timing is bad for me right now, need at least 2 weeks.

@h3lix1
Copy link
Copy Markdown
Contributor

h3lix1 commented May 30, 2026

In an attempt to make a 0.5w solar node for esp32 (I'm glutten for punishment) I've tweaked a few existing features to save power.

The expectation is that this will only work if wifi AND bluetooth is disabled, meaning the only way to manage the node is through remote admin. Even serial will require a reboot of the node. Using a Debug Mate with custom firmware to count mAh and mWh, I can get a Xiao Wio ESP32 down to a 1 watt hour per day with very busy mesh traffic.

This might be good for very specific solar routers. I have this running on a Heltec V4.3 with a estimated 2Wh/day.

The changes are #10570 #10571 #10572 and #10582 ..

These are the settings I'm working with on with the above changes.

network.wifi_enabled = false
bluetooth.enabled = false
display.screen_on_secs = 2147483647 # translates to 0
power.min_wake_secs = 2147483647 # translates to 0
power.wait_bluetooth_secs = 2147483647  # optional if bluetooth.enabled=false, translates to 0
power.ls_secs = 300 # Wake every once in a while for admin tasks

# Maybe if you want to disable LNA for more power savings.
lora.fem_lna_mode disable # Depends on if node has LNA

At the very least, #10582 should be interesting since light sleep does not advance ticks, causing incorrect (inflated) chutil.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request triaged Reviewed by the team, has enough information and ready to work on now.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Dynamic light sleep support for ESP32 platform