diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index b7b88782..158027da 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 dde800e3..95ad13f9 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 3a13691e..a42d17f9 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 7503f553..9135975a 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")); }