From 035dfaf60244b580d7d5abc56ec016549e0e6bdb Mon Sep 17 00:00:00 2001 From: m1nl Date: Thu, 24 Apr 2025 00:33:23 +0200 Subject: [PATCH] refactor sleep and PowerFSM to allow dynamic light sleep --- src/PowerFSM.cpp | 247 ++++++++-------- src/PowerFSM.h | 12 +- src/main.cpp | 8 +- src/platform/esp32/main-esp32.cpp | 2 - src/sleep.cpp | 461 ++++++++++++++++-------------- src/sleep.h | 45 ++- 6 files changed, 406 insertions(+), 369 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 3b3f8080d..afd4beaf1 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -19,13 +19,19 @@ #include "sleep.h" #include "target_specific.h" -#if HAS_WIFI && !defined(ARCH_PORTDUINO) || defined(MESHTASTIC_EXCLUDE_WIFI) +#ifdef ARCH_ESP32 +#include "esp32/pm.h" +#include "esp_pm.h" +#endif + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif #ifndef SLEEP_TIME #define SLEEP_TIME 30 #endif + #if MESHTASTIC_EXCLUDE_POWER_FSM FakeFsm powerFSM; void PowerFSM_setup(){}; @@ -77,85 +83,104 @@ static void shutdownEnter() #include "error.h" -static uint32_t secsSlept; +uint32_t sleepStart; +uint32_t sleepTime; static void lsEnter() { - LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); + LOG_DEBUG("State: LS"); if (screen) screen->setOn(false); - secsSlept = 0; // How long have we been sleeping this time + ledBlink.set(false); - // LOG_INFO("lsEnter end"); + if (!doPreflightSleep()) { + LOG_DEBUG("State change to LS aborted"); + sleepStart = -1; + powerFSM.trigger(EVENT_WAKE_TIMER); + return; + } + + sleepStart = millis(); + sleepTime = 0; + + powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); + +#ifdef ARCH_ESP32 + doLightSleep(SLEEP_TIME * 1000LL); +#endif } static void lsIdle() { - // LOG_INFO("lsIdle begin ls_secs=%u", getPref_ls_secs()); + if (!doPreflightSleep()) { + powerFSM.trigger(EVENT_WAKE_TIMER); + return; + } + + sleepTime = millis() - sleepStart; #ifdef ARCH_ESP32 + esp_sleep_source_t cause = esp_sleep_get_wakeup_cause(); - // Do we have more sleeping to do? - if (secsSlept < config.power.ls_secs) { - // If some other service would stall sleep, don't let sleep happen yet - if (doPreflightSleep()) { - // Briefly come out of sleep long enough to blink the led once every few seconds - uint32_t sleepTime = SLEEP_TIME; + switch (cause) { + case ESP_SLEEP_WAKEUP_UART: + LOG_DEBUG("Wake cause ESP_SLEEP_WAKEUP_UART"); + powerFSM.trigger(EVENT_INPUT); + return; - powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); - ledBlink.set(false); // Never leave led on while in light sleep - esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); - powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); + case ESP_SLEEP_WAKEUP_EXT0: + LOG_DEBUG("Wake cause ESP_SLEEP_WAKEUP_EXT0"); + powerFSM.trigger(EVENT_LORA_INTERRUPT); + return; - switch (wakeCause2) { - case ESP_SLEEP_WAKEUP_TIMER: - // Normal case: timer expired, we should just go back to sleep ASAP + case ESP_SLEEP_WAKEUP_EXT1: + LOG_DEBUG("Wake cause ESP_SLEEP_WAKEUP_EXT1"); + powerFSM.trigger(EVENT_PRESS); + return; - ledBlink.set(true); // briefly turn on led - wakeCause2 = doLightSleep(100); // leave led on for 1ms - - secsSlept += sleepTime; - // LOG_INFO("Sleep, flash led!"); - break; - - case ESP_SLEEP_WAKEUP_UART: - // Not currently used (because uart triggers in hw have problems) - powerFSM.trigger(EVENT_SERIAL_CONNECTED); - break; - - default: - // We woke for some other reason (button press, device IRQ interrupt) - -#ifdef BUTTON_PIN - bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); -#else - bool pressed = false; -#endif - if (pressed) { // If we woke because of press, instead generate a PRESS event. - powerFSM.trigger(EVENT_PRESS); - } else { - // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) - // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code - powerFSM.trigger(EVENT_WAKE_TIMER); - } - break; - } - } else { - // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so - delay(100); - } - } else { - // Time to stop sleeping! - ledBlink.set(false); - LOG_INFO("Reached ls_secs, service loop()"); + case ESP_SLEEP_WAKEUP_GPIO: + LOG_DEBUG("Wake cause ESP_SLEEP_WAKEUP_GPIO"); powerFSM.trigger(EVENT_WAKE_TIMER); + return; + + case ESP_SLEEP_WAKEUP_UNDEFINED: + LOG_DEBUG("Wake cause ESP_SLEEP_WAKEUP_UNDEFINED"); + powerFSM.trigger(EVENT_WAKE_TIMER); + return; + + default: + if (sleepTime > config.power.ls_secs * 1000LL) { + powerFSM.trigger(EVENT_WAKE_TIMER); + return; + } + break; } + + uint32_t sleepLeft; + + sleepLeft = config.power.ls_secs * 1000LL - sleepTime; + if (sleepLeft > SLEEP_TIME * 1000LL) { + sleepLeft = SLEEP_TIME * 1000LL; + } + + doLightSleep(sleepLeft); #endif } static void lsExit() { - LOG_INFO("Exit state: LS"); +#ifdef ARCH_ESP32 + doLightSleep(LIGHT_SLEEP_ABORT); +#endif + + if (sleepStart != -1) { + sleepTime = millis() - sleepStart; + sleepStart = 0; + + powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); + + LOG_DEBUG("Exit state: LS, slept %d ms", sleepTime); + } } static void nbEnter() @@ -167,12 +192,11 @@ static void nbEnter() // Only ESP32 should turn off bluetooth setBluetoothEnable(false); #endif - - // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE } static void darkEnter() { + // LOG_DEBUG("State: DARK"); setBluetoothEnable(true); if (screen) screen->setOn(false); @@ -195,11 +219,12 @@ static void serialExit() static void powerEnter() { - // LOG_DEBUG("State: POWER"); + LOG_DEBUG("State: POWER"); if (!isPowered()) { // If we got here, we are in the wrong state - we should be in powered, let that state handle things LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } else { if (screen) screen->setOn(true); @@ -261,23 +286,16 @@ void PowerFSM_setup() { bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); bool hasPower = isPowered(); + State *stateIDLE; - LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); - powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); - - // wake timer expired or a packet arrived - // if we are a router node, we go to NB (no need for bluetooth) otherwise we go to DARK (so we can send message to phone) #ifdef ARCH_ESP32 - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); -#else // Don't go into a no-bluetooth state on low power platforms - powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); + stateIDLE = isRouter ? &stateNB : &stateDARK; +#else + stateIDLE = &stateDARK; #endif + LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); - // We need this transition, because we might not transition if we were waiting to enter light-sleep, because when we wake from - // light sleep we _always_ transition to NB or dark and - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, - "Received packet, exiting light sleep"); - powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); + powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); // Handle press events - note: we ignore button presses when in API mode powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); @@ -314,12 +332,17 @@ void PowerFSM_setup() powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); - // if we are a router we don't turn the screen on for these things + // stay in dark state as long as we continue talking with phone + powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); + powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); + if (!isRouter) { - // if any packet destined for phone arrives, turn on bluetooth at least - powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WEB_REQUEST, NULL, "Web request"); // Removed 2.7: we don't show the nodes individually for every node on the screen anymore + // powerFSM.add_transition(&stateLS, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); @@ -329,29 +352,41 @@ void PowerFSM_setup() powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); powerFSM.add_transition(&stateON, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); // restarts the sleep timer + + } else { + // if we are a router we don't turn the screen on for these things + powerFSM.add_timed_transition( + &stateDARK, &stateNB, + Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, + "Bluetooth timeout"); } // If we are not in statePOWER but get a serial connection, suppress sleep (and keep the screen on) while connected - powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "Serial API"); + powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "Serial API"); + powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "Serial API"); + powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "Serial API"); + powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "Serial API"); // If we get power connected, go to the power connect state - powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "Power connect"); + powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "Power connect"); + powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "Power connect"); + powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "Power connect"); - powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); - // powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "Power disconnected"); + powerFSM.add_transition(&stateLS, &stateON, EVENT_POWER_DISCONNECTED, NULL, "Power disconnected"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_POWER_DISCONNECTED, NULL, "Power disconnected"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_POWER_DISCONNECTED, NULL, "Power disconnected"); // the only way to leave state serial is for the client to disconnect (or we timeout and force disconnect them) // when we leave, go to ON (which might not be the correct state if we have power connected, we will fix that in onEnter) - powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); + powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "Serial disconnect"); - powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); + powerFSM.add_transition(&stateLS, stateIDLE, EVENT_LORA_INTERRUPT, NULL, "LoRa interrupt"); + powerFSM.add_transition(&stateLS, stateIDLE, EVENT_WAKE_TIMER, NULL, "Wake timer"); + + powerFSM.add_transition(stateIDLE, stateIDLE, EVENT_WAKE_TIMER, NULL, "Wake timer"); #ifdef USE_EINK // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 @@ -368,39 +403,9 @@ void PowerFSM_setup() // We never enter light-sleep or NB states on NRF52 (because the CPU uses so little power normally) #ifdef ARCH_ESP32 - // See: https://github.com/meshtastic/firmware/issues/1071 - // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated - // through the modules - -#if HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) - bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; - - if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { - powerFSM.add_timed_transition(&stateNB, &stateLS, - Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, - "Min wake timeout"); - - // If ESP32 and using power-saving, timer mover from DARK to light-sleep - // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 - powerFSM.add_timed_transition( - &stateDARK, &stateLS, - Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, - "Bluetooth timeout"); - } else { - // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); + if (config.power.is_power_saving) { + powerFSM.add_timed_transition(stateIDLE, &stateLS, WAKE_TIME_MS, NULL, "Min wake timeout"); } -#endif // HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) - -#else // (not) ARCH_ESP32 - // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, - "Screen-on timeout"); #endif powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 6330a5fc6..0bfef470e 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -21,6 +21,16 @@ #define EVENT_FIRMWARE_UPDATE 15 // We just received a new firmware update packet from the phone #define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep) #define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen +#define EVENT_LORA_INTERRUPT 18 +#define EVENT_WEB_REQUEST 19 + +#if defined(ARCH_ESP32) && !defined(WAKE_TIME_MS) +#ifdef CONFIG_FREERTOS_USE_TICKLESS_IDLE +#define WAKE_TIME_MS 500 +#else +#define WAKE_TIME_MS Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs) +#endif +#endif #if MESHTASTIC_EXCLUDE_POWER_FSM class FakeFsm @@ -45,7 +55,7 @@ void PowerFSM_setup(); #else #include extern Fsm powerFSM; -extern State stateON, statePOWER, stateSERIAL, stateDARK; +extern State stateON, statePOWER, stateSERIAL, stateDARK, stateNB, stateLS; void PowerFSM_setup(); #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c37001307..76f08cefb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -254,7 +254,7 @@ static int32_t ledBlinker() ledBlink.set(ledOn); // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); + return (powerStatus->getIsCharging() && powerFSM.getState() != &stateLS) ? 1000 : (ledOn ? 1 : 1000); } uint32_t timeLastPowered = 0; @@ -1410,6 +1410,12 @@ void setup() 1000); } +#ifdef ARCH_ESP32 + if (config.power.is_power_saving) { + initLightSleep(); + } +#endif + // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS powerFSMthread = new PowerFSMThread(); diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index cdea53c9a..c13865500 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -162,8 +162,6 @@ void esp32Setup() WiFiOTA::initialize(); #endif - // enableModemSleep(); - // Since we are turning on watchdogs rather late in the release schedule, we really don't want to catch any // false positives. The wait-to-sleep timeout for shutting down radios is 30 secs, so pick 45 for now. // #define APP_WATCHDOG_SECS 45 diff --git a/src/sleep.cpp b/src/sleep.cpp index 09484f46e..313ce0ecf 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -10,6 +10,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" +#include "concurrency/Lock.h" #include "detect/LoRaRadioType.h" #include "error.h" #include "main.h" @@ -17,18 +18,16 @@ #include "target_specific.h" #ifdef ARCH_ESP32 -// "esp_pm_config_esp32_t is deprecated, please include esp_pm.h and use esp_pm_config_t instead" +#ifdef CONFIG_PM_ENABLE #include "esp32/pm.h" #include "esp_pm.h" +#endif #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif #include "rom/rtc.h" -#include #include #include - -esp_sleep_source_t wakeCause; // the reason we booted this time #endif #include "Throttle.h" @@ -46,11 +45,25 @@ Observable notifyDeepSleep; Observable notifyReboot; #ifdef ARCH_ESP32 +// Wake cause when returning from a deep sleep +esp_sleep_source_t wakeCause; + /// Called to tell observers that light sleep is about to begin Observable notifyLightSleep; /// Called to tell observers that light sleep has just ended, and why it ended Observable notifyLightSleepEnd; + +#ifdef CONFIG_PM_ENABLE +esp_pm_lock_handle_t pmHandle; +#endif + +// internal helper functions +void gpioResetHold(void); +void enableButtonInterrupt(void); + +void enableLoraInterrupt(void); +bool shouldLoraWake(uint32_t msecToWake); #endif // deep sleep support @@ -68,30 +81,21 @@ RTC_DATA_ATTR int bootCount = 0; */ void setCPUFast(bool on) { -#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT - +#if defined(ARCH_ESP32) && !HAS_TFT +#ifdef HAS_WIFI if (isWifiAvailable()) { - /* - * - * There's a newly introduced bug in the espressif framework where WiFi is - * unstable when the frequency is less than 240MHz. - * - * This mostly impacts WiFi AP mode but we'll bump the frequency for - * all WiFi use cases. - * (Added: Dec 23, 2021 by Jm Casler) - */ -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(WIFI_MAX_PERFORMANCE) LOG_DEBUG("Set CPU to 240MHz because WiFi is in use"); setCpuFrequencyMhz(240); -#endif return; +#endif } +#endif // The Heltec LORA32 V1 runs at 26 MHz base frequency and doesn't react well to switching to 80 MHz... #if !defined(ARDUINO_HELTEC_WIFI_LORA_32) && !defined(CONFIG_IDF_TARGET_ESP32C3) setCpuFrequencyMhz(on ? 240 : 80); #endif - #endif } @@ -100,6 +104,7 @@ void initDeepSleep() { #ifdef ARCH_ESP32 bootCount++; + const char *reason; wakeCause = esp_sleep_get_wakeup_cause(); @@ -147,30 +152,17 @@ void initDeepSleep() LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason); #endif -#if SOC_RTCIO_HOLD_SUPPORTED - // If waking from sleep, release any and all RTC GPIOs +#ifdef ARCH_ESP32 if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { - LOG_DEBUG("Disable any holds on RTC IO pads"); - for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { - if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) - rtc_gpio_hold_dis((gpio_num_t)i); - - // ESP32 (original) - else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) - gpio_hold_dis((gpio_num_t)i); - } + gpioResetHold(); } #endif - #endif } bool doPreflightSleep() { - if (preflightSleep.notifyObservers(NULL) != 0) - return false; // vetoed - else - return true; + return preflightSleep.notifyObservers(NULL) == 0; } /// Tell devices we are going to sleep and wait for them to handle things @@ -199,6 +191,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN { if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { LOG_INFO("Enter deep sleep forever"); + } else { LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); } @@ -269,32 +262,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN if (shouldLoraWake(msecToWake)) { enableLoraInterrupt(); } -#ifdef BUTTON_PIN - // Avoid leakage through button pin - if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { -#ifdef BUTTON_NEED_PULLUP - pinMode(BUTTON_PIN, INPUT_PULLUP); -#else - pinMode(BUTTON_PIN, INPUT); -#endif - gpio_hold_en((gpio_num_t)BUTTON_PIN); - } -#endif -#ifdef SENSECAP_INDICATOR - // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - gpio_hold_en((gpio_num_t)LORA_CS); -#elif defined(ELECROW_PANEL) - // Elecrow panels do not use LORA_CS, do nothing -#else - if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { - // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - gpio_hold_en((gpio_num_t)LORA_CS); - } -#endif + enableButtonInterrupt(); #endif #ifdef HAS_PMU @@ -340,166 +308,256 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN pinMode(I2C_SCL, ANALOG); #endif +#if defined(ARCH_ESP32) && defined(I2C_SDA1) + // Added by https://github.com/meshtastic/firmware/pull/4418 + // Possibly to support Heltec Capsule Sensor? + Wire1.end(); + pinMode(I2C_SDA1, ANALOG); + pinMode(I2C_SCL1, ANALOG); +#endif + console->flush(); cpuDeepSleep(msecToWake); } #ifdef ARCH_ESP32 +bool pmLockAcquired; +concurrency::Lock *pmLightSleepLock; + /** * enter light sleep (preserves ram but stops everything about CPU). * * Returns (after restoring hw state) when the user presses a button or we get a LoRa interrupt */ -esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default +void doLightSleep(uint32_t sleepMsec) { - // LOG_DEBUG("Enter light sleep"); + esp_err_t res; - // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering - // LightSleep. -#if defined(SENSECAP_INDICATOR) - return ESP_SLEEP_WAKEUP_TIMER; + assert(pmLightSleepLock); + pmLightSleepLock->lock(); + + if (sleepMsec == LIGHT_SLEEP_ABORT) { + if (pmLockAcquired) { + pmLightSleepLock->unlock(); + return; // nothing to do + } + +#ifdef CONFIG_PM_ENABLE + res = esp_pm_lock_acquire(pmHandle); + assert(res == ESP_OK); +#endif + pmLockAcquired = true; + + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + gpioResetHold(); + + notifyLightSleepEnd.notifyObservers(esp_sleep_get_wakeup_cause()); + + pmLightSleepLock->unlock(); + return; + } + + if (!pmLockAcquired) { + console->flush(); + +#ifndef CONFIG_FREERTOS_USE_TICKLESS_IDLE + esp_light_sleep_start(); #endif - waitEnterSleep(false); - notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here + pmLightSleepLock->unlock(); + return; + } - uint64_t sleepUsec = sleepMsec * 1000LL; - - // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep - - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - -#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP) - gpio_pullup_en((gpio_num_t)BUTTON_PIN); -#endif - -#ifdef SERIAL0_RX_GPIO - // We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means - // someone started to send something - - // gpio 3 is RXD for serialport 0 on ESP32 - // Send a few Z characters to wake the port - - // this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts) - // So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it - // never tries to go to sleep if the user is using the API - // gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL); - - // doesn't help - I think the USB-UART chip losing power is pulling the signal low - // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); - - // alas - can only work if using the refclock, which is limited to about 9600 bps - // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); - // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); -#endif -#ifdef BUTTON_PIN - // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup - gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); - - gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); - esp_sleep_enable_gpio_wakeup(); -#endif -#ifdef INPUTDRIVER_ENCODER_BTN - gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); -#endif -#if defined(WAKE_ON_TOUCH) - gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); -#endif enableLoraInterrupt(); + enableButtonInterrupt(); + +#ifndef CONFIG_FREERTOS_USE_TICKLESS_IDLE + res = esp_sleep_enable_timer_wakeup(sleepMsec * 1000LL); + assert(res == ESP_OK); +#endif + + res = uart_set_wakeup_threshold(UART_NUM_0, 3); + assert(res == ESP_OK); + + res = esp_sleep_enable_uart_wakeup(UART_NUM_0); + assert(res == ESP_OK); + #ifdef PMU_IRQ // wake due to PMU can happen repeatedly if there is no battery installed or the battery fills - if (pmu_found) - gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq + if (pmu_found) { + res = gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq + assert(res == ESP_OK); + } #endif - auto res = esp_sleep_enable_gpio_wakeup(); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); - } - assert(res == ESP_OK); - res = esp_sleep_enable_timer_wakeup(sleepUsec); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res); - } - assert(res == ESP_OK); - console->flush(); - res = esp_light_sleep_start(); - if (res != ESP_OK) { - LOG_ERROR("esp_light_sleep_start result %d", res); - } - // commented out because it's not that crucial; - // if it sporadically happens the node will go into light sleep during the next round - // assert(res == ESP_OK); - -#ifdef BUTTON_PIN - // Disable wake-on-button interrupt. Re-attach normal button-interrupts - gpio_wakeup_disable(pin); +#if defined(VEXT_ENABLE) + gpio_hold_en((gpio_num_t)VEXT_ENABLE); #endif + +#if defined(RESET_OLED) + gpio_hold_en((gpio_num_t)RESET_OLED); +#endif + #if defined(INPUTDRIVER_ENCODER_BTN) - gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); + res = gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); + assert(res == ESP_OK); #endif #if defined(WAKE_ON_TOUCH) - gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); -#endif -#if !defined(SOC_PM_SUPPORT_EXT_WAKEUP) && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)LORA_DIO1); - } -#endif -#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)RF95_IRQ); - } + res = gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); + assert(res == ESP_OK); #endif - esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); - notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here + res = esp_sleep_enable_gpio_wakeup(); + assert(res == ESP_OK); -#ifdef BUTTON_PIN - if (cause == ESP_SLEEP_WAKEUP_GPIO) { - LOG_INFO("Exit light sleep gpio: btn=%d", - !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - } else + notifyLightSleep.notifyObservers(NULL); + + console->flush(); + +#ifdef CONFIG_PM_ENABLE + res = esp_pm_lock_release(pmHandle); + assert(res == ESP_OK); #endif - { - LOG_INFO("Exit light sleep cause: %d", cause); - } + pmLockAcquired = false; - return cause; +#ifndef CONFIG_FREERTOS_USE_TICKLESS_IDLE + esp_light_sleep_start(); +#endif + + pmLightSleepLock->unlock(); } -// not legal on the stock android ESP build - -/** - * enable modem sleep mode as needed and available. Should lower our CPU current draw to an average of about 20mA. - * - * per https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html - * - * supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino - */ -void enableModemSleep() +// Initialize power management settings to allow light sleep +void initLightSleep() { -#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - static esp_pm_config_t esp32_config; // filled with zeros because bss + esp_err_t res; + +#ifdef CONFIG_PM_ENABLE + res = esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "meshtastic", &pmHandle); + assert(res == ESP_OK); + + res = esp_pm_lock_acquire(pmHandle); + assert(res == ESP_OK); + + esp_pm_config_esp32_t pm_config; + pm_config.max_freq_mhz = 80; + pm_config.min_freq_mhz = 20; +#ifdef CONFIG_FREERTOS_USE_TICKLESS_IDLE + pm_config.light_sleep_enable = true; #else - static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss + pm_config.light_sleep_enable = false; #endif -#if CONFIG_IDF_TARGET_ESP32S3 - esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ; -#elif CONFIG_IDF_TARGET_ESP32S2 - esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ; -#elif CONFIG_IDF_TARGET_ESP32C6 - esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; -#elif CONFIG_IDF_TARGET_ESP32C3 - esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; + + res = esp_pm_configure(&pm_config); + assert(res == ESP_OK); + + LOG_INFO("PM config enabled - min_freq_mhz=%d, max_freq_mhz=%d, light_sleep_enable=%d", pm_config.min_freq_mhz, + pm_config.max_freq_mhz, pm_config.light_sleep_enable); +#endif + + pmLightSleepLock = new concurrency::Lock(); + pmLockAcquired = true; +} + +void gpioResetHold() +{ + for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { + if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) { + rtc_gpio_hold_dis((gpio_num_t)i); + rtc_gpio_deinit((gpio_num_t)i); + + } else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) + gpio_hold_dis((gpio_num_t)i); + } +} + +void enableButtonInterrupt() +{ + esp_err_t res; + gpio_num_t pin; + +#ifdef BUTTON_PIN + pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); +#ifdef SOC_PM_SUPPORT_EXT_WAKEUP + if (rtc_gpio_is_valid_gpio(pin)) { + LOG_DEBUG("Setup button pin (GPIO%02d) with wakeup by ext2 source", pin); +#ifdef BUTTON_NEED_PULLUP + res = rtc_gpio_pullup_en(pin); + assert(res == ESP_OK); +#endif + res = rtc_gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = esp_sleep_enable_ext1_wakeup(1ULL << pin, ESP_EXT1_WAKEUP_ANY_LOW); + + } else { + LOG_DEBUG("Setup button pin (GPIO%02d) with wakeup by GPIO interrupt", pin); +#ifdef BUTTON_NEED_PULLUP + gpio_pullup_en(pin); + assert(res == ESP_OK); +#endif + res = gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); + } #else - esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ; +#ifdef BUTTON_NEED_PULLUP + gpio_pullup_en(pin); + assert(res == ESP_OK); #endif - esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended - esp32_config.light_sleep_enable = false; - int rv = esp_pm_configure(&esp32_config); - LOG_DEBUG("Sleep request result %x", rv); + res = gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); + LOG_DEBUG("Setup button pin (GPIO%02d) with wakeup by GPIO interrupt", pin); +#endif + assert(res == ESP_OK); +#endif +} + +void enableLoraInterrupt() +{ + esp_err_t res; + gpio_num_t pin; + + pin = GPIO_NUM_NC; + +#if defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) + pin = (gpio_num_t)LORA_DIO1; +#elif defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) + pin = (gpio_num_t)RF95_IRQ; +#endif + + assert(pin != GPIO_NUM_NC); + +#if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) + gpio_hold_en((gpio_num_t)LORA_RESET); +#endif + +#if SOC_PM_SUPPORT_EXT_WAKEUP + if (rtc_gpio_is_valid_gpio(pin)) { + LOG_DEBUG("Setup radio interrupt (GPIO%02d) with wakeup by ext1 source", pin); + res = rtc_gpio_pulldown_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = rtc_gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = esp_sleep_enable_ext0_wakeup(pin, HIGH); + + } else { + LOG_DEBUG("Setup radio interrupt (GPIO%02d) with wakeup by GPIO interrupt", pin); + res = gpio_pulldown_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_wakeup_enable(pin, GPIO_INTR_HIGH_LEVEL); + } +#else + LOG_DEBUG("Setup radio interrupt (GPIO%02d) with wakeup by GPIO interrupt", pin); + res = gpio_pulldown_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_hold_en((gpio_num_t)pin); + assert(res == ESP_OK); + res = gpio_wakeup_enable(pin, GPIO_INTR_HIGH_LEVEL); +#endif + assert(res == ESP_OK); } bool shouldLoraWake(uint32_t msecToWake) @@ -507,39 +565,4 @@ bool shouldLoraWake(uint32_t msecToWake) return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER); } - -void enableLoraInterrupt() -{ - esp_err_t res; -#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); - if (res != ESP_OK) { - LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); - } -#if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) - res = gpio_pullup_en((gpio_num_t)LORA_RESET); - if (res != ESP_OK) { - LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); - } -#endif -#if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL) - gpio_pullup_en((gpio_num_t)LORA_CS); -#endif - - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); - -#elif defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high - } -#endif -#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - LOG_INFO("setup RF95_IRQ (GPIO%02d) with wakeup by gpio interrupt", RF95_IRQ); - gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high - } -#endif -} #endif diff --git a/src/sleep.h b/src/sleep.h index f780fb3c0..9ce97258e 100644 --- a/src/sleep.h +++ b/src/sleep.h @@ -4,52 +4,47 @@ #include "Observer.h" #include "configuration.h" -void doDeepSleep(uint32_t msecToWake, bool skipPreflight, bool skipSaveNodeDb), cpuDeepSleep(uint32_t msecToWake); - -#ifdef ARCH_ESP32 -#include "esp_sleep.h" -esp_sleep_wakeup_cause_t doLightSleep(uint64_t msecToWake); - -extern esp_sleep_source_t wakeCause; -#endif - #ifdef HAS_PMU #include "XPowersLibInterface.hpp" extern XPowersLibInterface *PMU; #endif -// Perform power on init that we do on each wake from deep sleep +#ifdef ARCH_ESP32 +#include "esp_sleep.h" + +#define LIGHT_SLEEP_ABORT 0 + +void initLightSleep(); +void doLightSleep(uint32_t msecToWake); +#endif + +// perform power on init that we do on each wake from deep sleep void initDeepSleep(); +void doDeepSleep(uint32_t msecToWake, bool skipPreflight, bool skipSaveNodeDb), cpuDeepSleep(uint32_t msecToWake); void setCPUFast(bool on); -/** return true if sleep is allowed right now */ +// returns true if sleep is allowed right now bool doPreflightSleep(); extern int bootCount; -// is bluetooth sw currently running? -extern bool bluetoothOn; - -/// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen +// called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen extern Observable preflightSleep; -/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 +// called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 extern Observable notifyDeepSleep; -/// Called to tell observers we are rebooting ASAP. Must return 0 +// called to tell observers we are rebooting ASAP. Must return 0 extern Observable notifyReboot; #ifdef ARCH_ESP32 -/// Called to tell observers that light sleep is about to begin +// wake cause, set when init from deep sleep is called +extern esp_sleep_source_t wakeCause; + +/// called to tell observers that light sleep is about to begin extern Observable notifyLightSleep; -/// Called to tell observers that light sleep has just ended, and why it ended +/// called to tell observers that light sleep has just ended, and why it ended extern Observable notifyLightSleepEnd; #endif - -void enableModemSleep(); -#ifdef ARCH_ESP32 -void enableLoraInterrupt(); -bool shouldLoraWake(uint32_t msecToWake); -#endif \ No newline at end of file