diff --git a/usermods/deep_sleep/deep_sleep.cpp b/usermods/deep_sleep/deep_sleep.cpp index f6f3604fba..08ef61a0c1 100644 --- a/usermods/deep_sleep/deep_sleep.cpp +++ b/usermods/deep_sleep/deep_sleep.cpp @@ -1,6 +1,8 @@ #include "wled.h" #include "driver/rtc_io.h" - +#ifndef CONFIG_IDF_TARGET_ESP32C3 + #include "soc/touch_sensor_periph.h" +#endif #ifdef ESP8266 #error The "Deep Sleep" usermod does not support ESP8266 #endif @@ -21,27 +23,34 @@ #define DEEPSLEEP_DELAY 1 #endif -RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot +#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN +#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1 +#endif +RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot +RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset class DeepSleepUsermod : public Usermod { private: - - bool enabled = true; + bool enabled = false; // do not enable by default bool initDone = false; uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN; uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0 bool noPull = true; // use pullup/pulldown resistor + bool enableTouchWakeup = false; + uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN; int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only + bool presetWake = true; // wakeup timer for preset int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate - int delaycounter = 5; // delay deep sleep at bootup until preset settings are applied + int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup uint32_t lastLoopTime = 0; + // string that are used multiple time (this will save some flash memory) static const char _name[]; static const char _enabled[]; bool pin_is_valid(uint8_t wakePin) { - #ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up + #ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) { return true; } @@ -60,8 +69,56 @@ class DeepSleepUsermod : public Usermod { return false; } - public: + // functions to calculate time difference between now and next scheduled timer event + int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) { + int totalMinutes1 = hour1 * 60 + minute1; + int totalMinutes2 = hour2 * 60 + minute2; + if (totalMinutes2 < totalMinutes1) { + totalMinutes2 += 24 * 60; + } + return totalMinutes2 - totalMinutes1; + } + + int findNextTimerInterval() { + if (localTime < 1672502400) { // time invalid before NTP sync 2023-1-1 + DEBUG_PRINTLN("Skipping timer check: local time not yet synchronized."); + return -1; + } + int currentHour = hour(localTime); + int currentMinute = minute(localTime); + int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday + int minDifference = INT_MAX; + + for (uint8_t i = 0; i < 8; i++) { + // check if timer is enabled and date is in range, also wakes up if no macro is used + if ((timerWeekday[i] & 0x01) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) { + + // if timer is enabled (bit0 of timerWeekday) and date is in range, check all weekdays it is set for + for (int dayOffset = 0; dayOffset < 7; dayOffset++) { + int checkWeekday = ((currentWeekday + dayOffset) % 7); // 1-7, check all weekdays starting from today + if (checkWeekday == 0) { + checkWeekday = 7; // sunday is 7 not 0 + } + int targetHour = timerHours[i]; + int targetMinute = timerMinutes[i]; + if ((timerWeekday[i] >> (checkWeekday)) & 0x01) { + if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute))) + continue; // skip if time has already passed today + + int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute); + if (timeDifference < minDifference) { + minDifference = timeDifference; + wakeupPreset = timerMacro[i]; + } + } + } + } + } + return minDifference; + } + + public: inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state @@ -69,26 +126,39 @@ class DeepSleepUsermod : public Usermod { void setup() { //TODO: if the de-init of RTC pins is required to do it could be done here //rtc_gpio_deinit(wakeupPin); + #ifdef WLED_DEBUG + DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause()); + #endif + if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER) { + wakeupPreset = 0; // not a timed wakeup, don't apply preset + } initDone = true; } void loop() { - if (!enabled || !offMode) { // disabled or LEDs are on + if (!enabled) return; + if (!offMode) { // LEDs are on lastLoopTime = 0; // reset timer + if (delaycounter) delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below) + else if (wakeupPreset) applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time return; } if (sleepDelay > 0) { - if(lastLoopTime == 0) lastLoopTime = millis(); // initialize + powerup = false; // disable powerup sleep if delay is set + if (lastLoopTime == 0) lastLoopTime = millis(); // initialize if (millis() - lastLoopTime < sleepDelay * 1000) { - return; // wait until delay is over + return; // wait until delay is over } + } else if (powerup && delaycounter) { + delaycounter--; // on first boot without sleepDelay set, do not force-turn on + delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI + return; } - - if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case) + if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case) delaycounter--; - if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) - if (briS == 0) bri = 10; // turn on at low brightness + if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) + if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off else bri = briS; strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset) offMode = false; @@ -99,57 +169,82 @@ class DeepSleepUsermod : public Usermod { } return; } - DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep...")); powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot) - if(!pin_is_valid(wakeupPin)) return; + if (!pin_is_valid(wakeupPin)) return; esp_err_t halerror = ESP_OK; pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case) - if(wakeupAfter) - esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds + uint32_t wakeupAfterSec = 0; + if (presetWake) { + int nextInterval = findNextTimerInterval(); + if (nextInterval > 1 && nextInterval < INT_MAX) + wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset + } + if (wakeupAfter > 0) { // user-defined interval + if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) { + wakeupAfterSec = wakeupAfter; + } + } + if (wakeupAfterSec > 0) { + esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6); + DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec); + } #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3 - if(noPull) - gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING); - else { // enable pullup/pulldown resistor - if(wakeWhenHigh) - gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY); + gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin + if (wakeWhenHigh) + halerror = esp_deep_sleep_enable_gpio_wakeup(1<= 0) { + oappend(SET_F("addOption(dd,'")); + oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); + oappend(SET_F("',")); + oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); + oappend(SET_F(");")); + } + } + #endif oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');")); oappend(SET_F("addOption(dd,'Low',0);")); @@ -207,6 +325,7 @@ void addToConfig(JsonObject& root) override oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds (0 = never)');")); + oappend(SET_F("addInfo('DeepSleep:presetWake',1,'(wake up before next preset timer)');")); oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds (0 = sleep at powerup)');")); // first string is suffix, second string is prefix } @@ -217,7 +336,6 @@ void addToConfig(JsonObject& root) override uint16_t getId() { return USERMOD_ID_DEEP_SLEEP; } - }; // add more strings here to reduce flash memory usage diff --git a/usermods/deep_sleep/readme.md b/usermods/deep_sleep/readme.md index 807757f829..b88e3514a1 100644 --- a/usermods/deep_sleep/readme.md +++ b/usermods/deep_sleep/readme.md @@ -1,7 +1,7 @@ # Deep Sleep usermod This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum. -During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** +During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** # A word of warning @@ -28,7 +28,7 @@ For lowest power consumption, remove the Power LED and make sure your board does The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used: -- ESP32: GPIO 0, 2, 4, 12-15, 25-39 +- ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required - ESP32 S3: GPIO 0-21 - ESP32 S2: GPIO 0-21 - ESP32 C3: GPIO 0-5 @@ -61,6 +61,7 @@ To override the default settings, place the `#define` in wled.h or add `-D DEEPS * `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled) * `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2% * `DEEPSLEEP_DELAY` - delay between power-off and sleep +* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup example for env build flags: `-D USERMOD_DEEP_SLEEP` diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2346ee450b..33b1780f50 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -200,6 +200,7 @@ void getTimeString(char* out); bool checkCountdown(); void setCountdown(); byte weekdayMondayFirst(); +bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd); void checkTimers(); void calculateSunriseAndSunset(); void setTimeFromAPI(uint32_t timein); diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index a488d24f70..e1ecc160f8 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -69,7 +69,8 @@ enum struct PinOwner : uint8_t { UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins - UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_DEEP_SLEEP = USERMOD_ID_DEEP_SLEEP // 0x37 // Usermod "usermod_deep_sleep.h" -- Needs pins to control pmos, nmos, other devices }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected");