Browse Source

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
pull/2584/head
Maxim Prokhorov 1 year ago
parent
commit
2f6d7ce3d3
4 changed files with 385 additions and 9 deletions
  1. +347
    -5
      code/espurna/system.cpp
  2. +32
    -0
      code/espurna/system.h
  3. +5
    -0
      code/espurna/wifi.cpp
  4. +1
    -4
      code/espurna/ws.cpp

+ 347
- 5
code/espurna/system.cpp View File

@ -50,12 +50,23 @@ PROGMEM_STRING(None, "none");
PROGMEM_STRING(Once, "once");
PROGMEM_STRING(Repeat, "repeat");
static constexpr espurna::settings::options::Enumeration<heartbeat::Mode> HeartbeatModeOptions[] PROGMEM {
template <typename T>
using Enumeration = espurna::settings::options::Enumeration<T>;
static constexpr Enumeration<heartbeat::Mode> 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<sleep::Interrupt> 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<float> convert(const String& value) {
return std::chrono::duration<float>(convert<float>(value));
}
template <typename T>
T duration_convert(const String& value) {
return T{ convert<typename T::rep>(value) };
}
template <>
duration::Milliseconds convert(const String& value) {
return duration::Milliseconds(convert<duration::Milliseconds::rep>(value));
return duration_convert<duration::Milliseconds>(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 <typename T>
bool wait_for_fpm(duration::Milliseconds timeout, T&& condition) {
time::blockingDelay(
timeout, duration::Milliseconds{ 1 }, condition);
return condition();
}
template <typename Condition, typename Action>
bool wait_for_fpm(duration::Milliseconds timeout, Condition&& condition, Action&& action) {
if (condition()) {
action();
return wait_for_fpm(timeout, std::forward<Condition>(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<duration::Milliseconds>(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<espurna::settings::query::Setting, 3> 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<espurna::settings::query::Setting, 5> 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();


+ 32
- 0
code/espurna/system.h View File

@ -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<uint32_t, std::micro>;
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<float> 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();


+ 5
- 0
code/espurna/wifi.cpp View File

@ -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();
}
}
}


+ 1
- 4
code/espurna/ws.cpp View File

@ -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"));
}


Loading…
Cancel
Save