From 2f6d7ce3d37a5cfec3e2fb4b7fa5b4f3fdfe89fa Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Fri, 10 Mar 2023 01:14:42 +0300 Subject: [PATCH] system: light and deep sleep api see #2578 * deep sleep api without ESP class dependency * rf + cpu light sleep for cases when deep sleep cannot be used * generic time-based light sleep; nonos does not halt cpu, unlike the wakeup variant. timers also keep going, so this has to be used with extra care pending some 'deferred' variant and before and after sleep actions --- code/espurna/system.cpp | 352 +++++++++++++++++++++++++++++++++++++++- code/espurna/system.h | 32 ++++ code/espurna/wifi.cpp | 5 + code/espurna/ws.cpp | 5 +- 4 files changed, 385 insertions(+), 9 deletions(-) diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index b7b88782ba..158027da30 100644 --- a/code/espurna/system.cpp +++ b/code/espurna/system.cpp @@ -50,12 +50,23 @@ PROGMEM_STRING(None, "none"); PROGMEM_STRING(Once, "once"); PROGMEM_STRING(Repeat, "repeat"); -static constexpr espurna::settings::options::Enumeration HeartbeatModeOptions[] PROGMEM { +template +using Enumeration = espurna::settings::options::Enumeration; + +static constexpr Enumeration HeartbeatModeOptions[] PROGMEM { {heartbeat::Mode::None, None}, {heartbeat::Mode::Once, Once}, {heartbeat::Mode::Repeat, Repeat}, }; +PROGMEM_STRING(Low, "low"); +PROGMEM_STRING(High, "high"); + +static constexpr Enumeration SleepInterruptOptions[] PROGMEM { + {sleep::Interrupt::Low, Low}, + {sleep::Interrupt::High, High}, +}; + } // namespace options namespace keys { @@ -73,6 +84,15 @@ PROGMEM_STRING(Password, "adminPass"); namespace settings { namespace internal { +template <> +espurna::sleep::Interrupt convert(const String& value) { + return convert(system::settings::options::SleepInterruptOptions, value, sleep::Interrupt::Low); +} + +String serialize(espurna::sleep::Interrupt value) { + return serialize(system::settings::options::SleepInterruptOptions, value); +} + template <> espurna::heartbeat::Mode convert(const String& value) { return convert(system::settings::options::HeartbeatModeOptions, value, heartbeat::Mode::Repeat); @@ -96,9 +116,14 @@ std::chrono::duration convert(const String& value) { return std::chrono::duration(convert(value)); } +template +T duration_convert(const String& value) { + return T{ convert(value) }; +} + template <> duration::Milliseconds convert(const String& value) { - return duration::Milliseconds(convert(value)); + return duration_convert(value); } String serialize(espurna::duration::Milliseconds value) { @@ -112,6 +137,280 @@ String serialize(espurna::duration::ClockCycles value) { } // namespace internal } // namespace settings +// TODO: implement 'before' and 'after' callbacks executed outside of normal loop +// TODO: flush HW UART before light sleep? + +namespace sleep { +namespace { + +constexpr auto DeepSleepWakeupPin = uint8_t{ 16 }; + +namespace build { + +static constexpr auto DefaultInterrupt = espurna::sleep::Interrupt::Low; +static constexpr auto DefaultPin = uint8_t{ GPIO_NONE }; + +} // namespace build + +namespace settings { +namespace keys { + +PROGMEM_STRING(Pin, "sleepPin"); +PROGMEM_STRING(Interrupt, "sleepIntr"); + +} // namespace keys + +uint8_t pin() { + return getSetting(keys::Pin, build::DefaultPin); +} + +espurna::sleep::Interrupt interrupt() { + return getSetting(keys::Interrupt, build::DefaultInterrupt); +} + +} // namespace settings + +// 0xFFFFFFF is a magic number per the NONOS API reference, 3.7.5 wifi_fpm_do_sleep: +// > If sleep_time_in_us is 0xFFFFFFF, the ESP8266 will sleep till be woke up as below: +// > • If wifi_fpm_set_sleep_type is set to be LIGHT_SLEEP_T, ESP8266 can wake up by GPIO. +// > • If wifi_fpm_set_sleep_type is set to be MODEM_SLEEP_T, ESP8266 can wake up by wifi_fpm_do_wakeup. +// +// In our case, both sleep modes are indefinite when OFF action is executed. +// Light sleep timed mode *can* be enabled, but effectively forces all SDK timers to be expunged. +// +// Null mode turns off radio, no need for extra code to enable MODEM sleep after switching to NULL mode. + +extern "C" bool fpm_is_open(void); +extern "C" bool fpm_rf_is_closed(void); + +// We have to wait for certain {F,}PM changes that happen in SDK system idle task +template +bool wait_for_fpm(duration::Milliseconds timeout, T&& condition) { + time::blockingDelay( + timeout, duration::Milliseconds{ 1 }, condition); + return condition(); +} + +template +bool wait_for_fpm(duration::Milliseconds timeout, Condition&& condition, Action&& action) { + if (condition()) { + action(); + return wait_for_fpm(timeout, std::forward(condition)); + } + + return false; +} + +bool forced_wakeup() { + // Per API spec, we have to disable current forced PM mode to switch + // it to something else. Wait for a bit until both conditions are true + // and no idle task is attached to the system loop. + constexpr auto Timeout = duration::Seconds{ 1 }; + const auto rf_closed = wait_for_fpm( + Timeout, fpm_rf_is_closed, wifi_fpm_do_wakeup); + if (rf_closed) { + return false; + } + + const auto fpm_opened = wait_for_fpm( + Timeout, fpm_is_open, wifi_fpm_close); + if (fpm_opened) { + return false; + } + + return true; +} + +// Generic PM mode that disables RF peripheral. +// We can still execute user code while it is active. +bool forced_modem_sleep() { + if (!wifiDisabled()) { + return false; + } + + if (!forced_wakeup()) { + return false; + } + + wifi_fpm_set_sleep_type(MODEM_SLEEP_T); + wifi_fpm_open(); + + const auto result = wifi_fpm_do_sleep(FpmSleepIndefinite.count()); + if (result != 0) { + wifi_fpm_close(); + return false; + } + + // Change *would* occur regardless, but we still notify that expected t/o happened + return wait_for_fpm( + duration::Milliseconds{ 1000 }, + []() { + return !fpm_rf_is_closed(); + }); +} + +// CPU + RF power saving mode. +// SDK enters IDLE task with minimal amount of operations. +// User code would not be executed while the device is asleep. +// On wakeup, execution resumes from this point. +struct FpmLightSleep { + FpmLightSleep(); + ~FpmLightSleep(); + + explicit operator bool() const { + return _ok; + } + +private: + bool _ok { false }; +}; + +FpmLightSleep::~FpmLightSleep() { + if (_ok) { + wifi_fpm_close(); + } + + wifi_fpm_auto_sleep_set_in_null_mode(1); + espurnaReload(); + forced_modem_sleep(); +} + +FpmLightSleep::FpmLightSleep() { + // Unlike deep sleep option, we have to manually go over everything related + // to WiFi state management; both for our internals and SDK ones + wifi_fpm_auto_sleep_set_in_null_mode(0); + + // Since both sleep variants are going to block user + // task, WiFi actions would be delayed until woken up + wifiDisconnect(); + wifiDisable(); + + if (!forced_wakeup()) { + return; + } + + // ...before FPM type can be changed yet again + wifi_fpm_set_sleep_type(LIGHT_SLEEP_T); + wifi_fpm_open(); + + _ok = true; +} + +bool forced_light_sleep(sleep::Microseconds time) { + // Can't really do what deep sleep does without EXT wakeup source + if ((time <= FpmSleepMin) || (time >= FpmSleepIndefinite)) { + return false; + } + + // Common before and after actions + FpmLightSleep sleep; + if (!sleep) { + return false; + } + + // NONOS has a quirky implementation - while RF peripheral is stopped, + // neither system or sdk tasks are. Timers are still processed, + // interrupts are happening, background tasks may still execute. + // + // Instead of doing a (fairly common) workaround that temporarily hides + // every available timer from SDK, just pretend this works as intended and + // block with software timer loop. + // + // Also ref. `ets_set_idle_cb` processing at 0x3fffdab0 (func) and 0x3fffdab4 (arg) + // (in case RF + CPU sleep and RTC peripheral communication can be replicated here) + static bool block; + block = true; + + wifi_fpm_set_wakeup_cb([]() { + block = false; + }); + + const auto result = wifi_fpm_do_sleep(time.count()); + if (result == 0) { + const auto Wait = std::chrono::duration_cast(time); + espurna::time::blockingDelay( + Wait, + Wait, + []() { + return block; + }); + } + + wifi_fpm_close(); + + return result == 0; +} + +bool forced_light_sleep(uint8_t pin, Interrupt interrupt) { + if (pin == DeepSleepWakeupPin) { + return false; + } + + const auto& hardware = hardwareGpio(); + if (!hardware.valid(pin)) { + return false; + } + + // Common before and after actions + FpmLightSleep sleep; + if (!sleep) { + return false; + } + + // TODO: does pin mode apply interrupt mask for wakeup properly? + // TODO: check whether pin is already in use? + const auto pin_mode = espurna::gpio::pin_mode(pin); + pinMode(pin, + (interrupt == Interrupt::Low) + ? INPUT + : INPUT_PULLUP); + + wifi_enable_gpio_wakeup(pin, + (interrupt == Interrupt::Low) + ? GPIO_PIN_INTR_LOLEVEL + : GPIO_PIN_INTR_HILEVEL); + + // User task is suspended for the duration of the sleep, + // delay is just a context switch so idle task does its job + const auto result = wifi_fpm_do_sleep(FpmSleepIndefinite.count()); + delay(10); + + // Restore everything back as it was before + wifi_disable_gpio_wakeup(); + pinMode(pin, pin_mode.value); + + return result == 0; +} + +bool forced_light_sleep() { + return forced_light_sleep(settings::pin(), settings::interrupt()); +} + +// Extended sleep variant that disables everything but RTC memory. +// Unlike LIGHT sleep, device would be reset via RST pin to wake up. +bool deep_sleep(sleep::Microseconds time) { + const auto& hardware = hardwareGpio(); + if (hardware.lock(DeepSleepWakeupPin)) { + return false; + } + + system_deep_sleep_set_option(RF_DEFAULT); + if (system_deep_sleep(time.count())) { + yield(); + return true; + } + + return false; +} + +// Force WiFi RF peripheral to power down when NULL opmode is selected +void init() { + wifi_fpm_auto_sleep_set_in_null_mode(1); +} + +} // namespace +} // namespace sleep + // ----------------------------------------------------------------------------- namespace system { @@ -272,10 +571,23 @@ bool password_equals(StringView other) { namespace settings { namespace query { -static constexpr std::array Settings PROGMEM {{ +#define EXACT_VALUE(NAME, FUNC)\ +String NAME () {\ + return espurna::settings::internal::serialize(FUNC());\ +} + +EXACT_VALUE(sleepPin, sleep::settings::pin); +EXACT_VALUE(sleepInterrupt, sleep::settings::interrupt); + +#undef EXACT_VALUE + +static constexpr std::array Settings PROGMEM {{ {keys::Description, system::description}, {keys::Hostname, system::hostname}, {keys::Password, system::password}, + + {sleep::settings::keys::Pin, query::sleepPin}, + {sleep::settings::keys::Interrupt, query::sleepInterrupt}, }}; bool checkExact(StringView key) { @@ -1106,8 +1418,12 @@ void onConnected(JsonObject& root) { } bool onKeyCheck(StringView key, const JsonVariant&) { - return espurna::settings::query::samePrefix(key, STRING_VIEW("sys")) - || espurna::settings::query::samePrefix(key, STRING_VIEW("hb")); + return (key == system::settings::keys::Description) + || (key == system::settings::keys::Hostname) + || (key == system::settings::keys::Password) + || key.startsWith(STRING_VIEW("hb")) + || key.startsWith(STRING_VIEW("sleep")) + || key.startsWith(STRING_VIEW("sys")); } void init() { @@ -1197,6 +1513,8 @@ void setup() { boot::hardware(); boot::customReason(); + sleep::init(); + #if SYSTEM_CHECK_ENABLED boot::stability::init(); #endif @@ -1301,6 +1619,30 @@ String customResetReasonToPayload(CustomResetReason reason) { return espurna::boot::serialize(reason); } +bool prepareModemForcedSleep() { + return espurna::sleep::forced_modem_sleep(); +} + +bool wakeupModemForcedSleep() { + return espurna::sleep::forced_wakeup(); +} + +bool instantLightSleep() { + return espurna::sleep::forced_light_sleep(); +} + +bool instantLightSleep(espurna::sleep::Microseconds time) { + return espurna::sleep::forced_light_sleep(time); +} + +bool instantLightSleep(uint8_t pin, espurna::sleep::Interrupt interrupt) { + return espurna::sleep::forced_light_sleep(pin, interrupt); +} + +bool instantDeepSleep(espurna::sleep::Microseconds time) { + return espurna::sleep::deep_sleep(time); +} + #if SYSTEM_CHECK_ENABLED uint8_t systemStabilityCounter() { return espurna::boot::internal::persistent_data.counter(); diff --git a/code/espurna/system.h b/code/espurna/system.h index dde800e31b..95ad13f99c 100644 --- a/code/espurna/system.h +++ b/code/espurna/system.h @@ -39,6 +39,17 @@ enum class CustomResetReason : uint8_t { }; namespace espurna { +namespace sleep { + +// Both LIGHT and DEEP sleep accept microseconds as input +// Effective limit is ~31bit - 1 in size +using Microseconds = std::chrono::duration; + +constexpr auto FpmSleepMin = Microseconds{ 1000 }; +constexpr auto FpmSleepIndefinite = Microseconds{ 0xFFFFFFF }; + +} // namespace sleep + namespace system { struct RandomDevice { @@ -335,6 +346,15 @@ Mode currentMode(); } // namespace heartbeat +namespace sleep { + +enum class Interrupt { + Low, + High, +}; + +} // namespace sleep + namespace settings { namespace internal { @@ -344,6 +364,9 @@ heartbeat::Mode convert(const String&); template <> duration::Milliseconds convert(const String&); +template <> +duration::Microseconds convert(const String&); + template <> std::chrono::duration convert(const String&); @@ -386,6 +409,15 @@ void deferredReset(espurna::duration::Milliseconds, CustomResetReason); void prepareReset(CustomResetReason); bool pendingDeferredReset(); +bool wakeupModemForcedSleep(); +bool prepareModemForcedSleep(); + +bool instantLightSleep(); +bool instantLightSleep(espurna::sleep::Microseconds); +bool instantLightSleep(uint8_t pin, espurna::sleep::Interrupt); + +bool instantDeepSleep(espurna::sleep::Microseconds); + unsigned long systemLoadAverage(); espurna::duration::Seconds systemHeartbeatInterval(); diff --git a/code/espurna/wifi.cpp b/code/espurna/wifi.cpp index 3a13691ea7..a42d17f938 100644 --- a/code/espurna/wifi.cpp +++ b/code/espurna/wifi.cpp @@ -275,6 +275,7 @@ void ensure_opmode(uint8_t mode) { // since we should enforce mode changes to happen *only* through the configuration loop if (!is_set()) { + const auto current = wifi_get_opmode(); wifi_set_opmode_current(mode); espurna::time::blockingDelay( @@ -287,6 +288,10 @@ void ensure_opmode(uint8_t mode) { if (!is_set()) { abort(); } + + if (current == OpmodeNull) { + wakeupModemForcedSleep(); + } } } diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index 7503f553f8..9135975a3b 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -621,10 +621,7 @@ void _wsParse(AsyncWebSocketClient* client, uint8_t* payload, size_t length) { } bool _wsOnKeyCheck(espurna::StringView key, const JsonVariant&) { - return (key == STRING_VIEW("adminPass")) - || (key == STRING_VIEW("hostname")) - || (key == STRING_VIEW("desc")) - || (key == STRING_VIEW("webPort")) + return (key == STRING_VIEW("webPort")) || key.startsWith(STRING_VIEW("ws")); }