diff --git a/esphome/components/libretiny/preference_backend.h b/esphome/components/libretiny/preference_backend.h index f7f8279ac08..00d76d7d862 100644 --- a/esphome/components/libretiny/preference_backend.h +++ b/esphome/components/libretiny/preference_backend.h @@ -1,6 +1,8 @@ #pragma once #ifdef USE_LIBRETINY +#include "esphome/core/defines.h" + #include #include @@ -20,6 +22,14 @@ class LibreTinyPreferenceBackend final { uint32_t key; struct fdb_kvdb *db; struct fdb_blob *blob; +#ifdef USE_LIBRETINY_RTC_PREFERENCES + // storage: rtc lives under its own FlashDB name prefix, so the database can drop every rtc entry + // at once after a boot that RTC memory would not have survived + bool rtc{false}; + bool is_rtc() const { return this->rtc; } +#else + static constexpr bool is_rtc() { return false; } +#endif }; class LibreTinyPreferences; diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index a868e5042e2..9d21e88b14b 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include +#include #include namespace esphome::libretiny { @@ -13,20 +14,58 @@ ESPHOME_LOG_TAG(TAG, "preferences"); struct NVSData { uint32_t key; SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) +#ifdef USE_LIBRETINY_RTC_PREFERENCES + bool rtc; + bool is_rtc() const { return this->rtc; } +#else + static constexpr bool is_rtc() { return false; } +#endif }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// The FlashDB name of a key: its decimal digits, with a prefix for storage: rtc so one pass over the +// database finds every rtc entry. Without rtc storage is_rtc() is a constant false and the branch folds away. +static constexpr size_t KEY_NAME_SIZE = UINT32_MAX_STR_SIZE + 1; +static constexpr char RTC_KEY_PREFIX = 'r'; +static void key_name(char (&buf)[KEY_NAME_SIZE], uint32_t key, bool rtc) { + char *digits = buf; + if (rtc) + *digits++ = RTC_KEY_PREFIX; + uint32_to_str(std::span(digits, UINT32_MAX_STR_SIZE), key); +} + +#ifdef USE_LIBRETINY_RTC_PREFERENCES +// Whether this boot followed a reset that RTC memory survives on ESP32 and ESP8266. LibreTiny chips +// have no such memory, so storage: rtc keeps preferences in flash and uses this to forget them. +static bool boot_kept_rtc_preferences() { + switch (lt_get_reboot_reason()) { + case REBOOT_REASON_SOFTWARE: + case REBOOT_REASON_WATCHDOG: + case REBOOT_REASON_CRASH: + case REBOOT_REASON_SLEEP_GPIO: + case REBOOT_REASON_SLEEP_RTC: + case REBOOT_REASON_SLEEP_USB: + return true; + default: // power-on, brown-out, reset pin, debugger, unknown + return false; + } +} +#endif // USE_LIBRETINY_RTC_PREFERENCES + bool LibreTinyPreferenceBackend::save(const uint8_t *data, size_t len) { - // try find in pending saves and update that + // try find in pending saves and update that; a flash and an rtc entry may share a key for (auto &obj : s_pending_save) { - if (obj.key == this->key) { + if (obj.key == this->key && obj.is_rtc() == this->is_rtc()) { obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; +#ifdef USE_LIBRETINY_RTC_PREFERENCES + save.rtc = this->rtc; +#endif save.data.set(data, len); s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); @@ -36,7 +75,7 @@ bool LibreTinyPreferenceBackend::save(const uint8_t *data, size_t len) { bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) { // try find in pending saves and load from that for (auto &obj : s_pending_save) { - if (obj.key == this->key) { + if (obj.key == this->key && obj.is_rtc() == this->is_rtc()) { if (obj.data.size() != len) { // size mismatch return false; @@ -46,8 +85,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) { } } - char key_str[UINT32_MAX_STR_SIZE]; - uint32_to_str(key_str, this->key); + char key_str[KEY_NAME_SIZE]; + key_name(key_str, this->key, this->is_rtc()); fdb_blob_make(this->blob, data, len); size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); if (actual_len != len) { @@ -64,21 +103,57 @@ void LibreTinyPreferences::open() { fdb_err_t err = fdb_kvdb_init(&this->db, "esphome", "kvs", NULL, NULL); if (err != FDB_NO_ERR) { LT_E("fdb_kvdb_init(...) failed: %d", err); - } else { - LT_I("Preferences initialized"); + return; + } + LT_I("Preferences initialized"); +#ifdef USE_LIBRETINY_RTC_PREFERENCES + if (!boot_kept_rtc_preferences()) + this->discard_rtc_entries_(); +#endif +} + +#ifdef USE_LIBRETINY_RTC_PREFERENCES +ESPPreferenceObject LibreTinyPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + return ESPPreferenceObject(new LibreTinyPreferenceBackend(this->make_backend_(type, !in_flash))); +} + +// One pass over the database deletes every rtc entry. FlashDB only marks a deleted entry's status in place +// and its iterator steps by address, so deleting the current entry is safe. This runs before the logger +// exists, so it logs through LibreTiny; a failed delete is a flash fault the next write will report as well. +void LibreTinyPreferences::discard_rtc_entries_() { + uint16_t discarded = 0; + fdb_kv_iterator iterator; + fdb_kv_iterator_init(&iterator); + while (fdb_kv_iterate(&this->db, &iterator)) { + const char *name = iterator.curr_kv.name; + if (name[0] != RTC_KEY_PREFIX) + continue; + if (const fdb_err_t err = fdb_kv_del(&this->db, name); err != FDB_NO_ERR) { + LT_E("Failed to discard rtc preference %s: %d", name, err); + continue; + } + discarded++; + } + if (discarded != 0) { + LT_D("Discarded %u rtc preferences after a hard reset", discarded); } } +#endif ESPPreferenceObject LibreTinyPreferences::make_preference(size_t length, uint32_t type) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return ESPPreferenceObject(new LibreTinyPreferenceBackend(this->make_backend_(type))); } -LibreTinyPreferenceBackend LibreTinyPreferences::make_backend_(uint32_t type) { +LibreTinyPreferenceBackend LibreTinyPreferences::make_backend_(uint32_t type, bool rtc) { LibreTinyPreferenceBackend backend; backend.key = type; backend.db = &this->db; backend.blob = &this->blob; +#ifdef USE_LIBRETINY_RTC_PREFERENCES + backend.rtc = rtc; +#endif return backend; } @@ -97,8 +172,8 @@ bool LibreTinyPreferences::sync() { uint32_t last_key = 0; for (const auto &save : s_pending_save) { - char key_str[UINT32_MAX_STR_SIZE]; - uint32_to_str(key_str, save.key); + char key_str[KEY_NAME_SIZE]; + key_name(key_str, save.key, save.is_rtc()); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); diff --git a/esphome/components/libretiny/preferences.h b/esphome/components/libretiny/preferences.h index fd86c48b20d..4b3737f07fd 100644 --- a/esphome/components/libretiny/preferences.h +++ b/esphome/components/libretiny/preferences.h @@ -1,6 +1,7 @@ #pragma once #ifdef USE_LIBRETINY +#include "esphome/core/defines.h" #include "esphome/core/preference_backend.h" #include @@ -12,9 +13,14 @@ class LibreTinyPreferences final : public PreferencesMixin public: using PreferencesMixin::make_preference; void open(); +#ifdef USE_LIBRETINY_RTC_PREFERENCES + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); +#else + // Without rtc storage every preference lives in flash; inline so builds that don't use rtc pay nothing ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { return this->make_preference(length, type); } +#endif ESPPreferenceObject make_preference(size_t length, uint32_t type); /// One-shot read of a stored preference by key, without allocating a backend bool load_from_key(uint32_t type, uint8_t *data, size_t len); @@ -25,8 +31,11 @@ class LibreTinyPreferences final : public PreferencesMixin struct fdb_blob blob; protected: - LibreTinyPreferenceBackend make_backend_(uint32_t type); + LibreTinyPreferenceBackend make_backend_(uint32_t type, bool rtc = false); bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str); +#ifdef USE_LIBRETINY_RTC_PREFERENCES + void discard_rtc_entries_(); +#endif }; void setup_preferences(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 935952d3b31..852ea3439a9 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -717,6 +717,7 @@ #else #define USE_BK72XX_BLE_TRACKER #endif +#define USE_LIBRETINY_RTC_PREFERENCES #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define ESPHOME_SUSPEND_LOOP #define USE_BLE_SCAN_RESPONSE_MERGER diff --git a/esphome/preferences.py b/esphome/preferences.py index c9a465d6252..b31dcecf874 100644 --- a/esphome/preferences.py +++ b/esphome/preferences.py @@ -3,8 +3,11 @@ Preferences can be stored either in flash (durable across power loss) or in RTC memory (fast, survives deep sleep and soft resets but not power loss). The flash-vs-RTC choice is only meaningful on platforms whose preferences backend -honors the ``in_flash`` flag — currently ESP32 and ESP8266. On other platforms -the value is accepted only as ``flash`` (the sole supported backend). +honors the ``in_flash`` flag — currently ESP32, ESP8266 and the Beken BK7231T, +BK7231N and BK7238. LibreTiny chips have no RTC memory: there ``rtc`` keeps the +preference in flash and discards it after a reset that RTC memory would not +survive. On other platforms the value is accepted only as ``flash`` (the sole +supported backend). Components include :func:`storage_schema` in their config and convert the chosen value with :func:`is_in_flash` when calling ``make_preference``. @@ -20,7 +23,7 @@ STORAGE_RTC = "rtc" def rtc_supported() -> bool: - """Whether the target has RTC-backed preferences (not ESP32-C2/C61); call at validation time.""" + """Whether the target has RTC-backed preferences (ESP8266, ESP32 except C2/C61, some Beken chips); call at validation time.""" if CORE.is_esp8266: return True if CORE.is_esp32: @@ -29,7 +32,26 @@ def rtc_supported() -> bool: # Mirrors the C++ SOC_RTC_MEM_SUPPORTED guard in the ESP32 backend return get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32C61) - return False + return _libretiny_rtc_supported() + + +def _libretiny_rtc_supported() -> bool: + """Whether this LibreTiny family accepts ``rtc``, emulated with flash plus the reset reason. + + The emulation is only as good as the family's ``lt_get_reboot_reason()``, so families are + listed once that reporting has been checked on hardware: a chip-enable reset and a power cut + both report a power-on, a software, watchdog or crash reset does not. + """ + if not CORE.is_libretiny: + return False + from esphome.components.libretiny import get_libretiny_family + from esphome.components.libretiny.const import ( + FAMILY_BK7231N, + FAMILY_BK7231T, + FAMILY_BK7238, + ) + + return get_libretiny_family() in (FAMILY_BK7231T, FAMILY_BK7231N, FAMILY_BK7238) def _default_storage() -> str: @@ -58,15 +80,17 @@ def storage_schema(): def request_rtc_storage() -> None: - """Compile the RTC-backed storage into the ESP32 preferences backend. + """Compile the RTC-backed storage into the ESP32 or LibreTiny preferences backend. - The RTC storage region is left out of ESP32 builds unless something asks for - it, so unused builds don't reserve RTC memory. Call this from ``to_code`` - when a config option selects RTC storage. No-op on other platforms (ESP8266 - always has its RTC backend). + It is left out of ESP32 and LibreTiny builds unless something asks for it, so + unused builds don't reserve RTC memory or check the reset reason. Call this + from ``to_code`` when a config option selects RTC storage. No-op on other + platforms (ESP8266 always has its RTC backend). """ if CORE.is_esp32: cg.add_define("USE_ESP32_RTC_PREFERENCES") + elif _libretiny_rtc_supported(): + cg.add_define("USE_LIBRETINY_RTC_PREFERENCES") def validate_rtc_storage(value): diff --git a/tests/unit_tests/test_preferences.py b/tests/unit_tests/test_preferences.py index a1ab78bfbff..da56aa8d6fa 100644 --- a/tests/unit_tests/test_preferences.py +++ b/tests/unit_tests/test_preferences.py @@ -10,15 +10,26 @@ from esphome.components.esp32.const import ( VARIANT_ESP32C3, VARIANT_ESP32C61, ) +from esphome.components.libretiny.const import ( + FAMILY_BK7231N, + FAMILY_BK7231T, + FAMILY_BK7238, + FAMILY_BK7251, + FAMILY_RTL8710B, + KEY_FAMILY, + KEY_LIBRETINY, +) import esphome.config_validation as cv from esphome.const import ( CONF_STORAGE, KEY_CORE, KEY_TARGET_PLATFORM, KEY_VARIANT, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PLATFORM_RTL87XX, ) from esphome.core import CORE @@ -191,3 +202,45 @@ def test_validate_storage_rejects_unknown_value() -> None: _set_esp32(VARIANT_ESP32) with pytest.raises(cv.Invalid): preferences.validate_storage("nvs") + + +def _set_libretiny(platform: str, family: str) -> None: + _set_platform(platform) + CORE.data[KEY_LIBRETINY] = {KEY_FAMILY: family} + + +@pytest.mark.parametrize("family", [FAMILY_BK7231T, FAMILY_BK7231N, FAMILY_BK7238]) +def test_rtc_allowed_on_tested_beken_families(family: str) -> None: + # Emulated with flash plus the reset reason, which was checked on hardware. + _set_libretiny(PLATFORM_BK72XX, family) + assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC + + +@pytest.mark.parametrize( + ("platform", "family"), + [ + (PLATFORM_BK72XX, FAMILY_BK7251), + (PLATFORM_RTL87XX, FAMILY_RTL8710B), + ], +) +def test_rtc_rejected_on_other_libretiny_families(platform: str, family: str) -> None: + _set_libretiny(platform, family) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + _validate({CONF_STORAGE: "rtc"}) + + +def test_default_storage_bk7231t_is_flash() -> None: + _set_libretiny(PLATFORM_BK72XX, FAMILY_BK7231T) + assert _validate({})[CONF_STORAGE] == preferences.STORAGE_FLASH + + +def test_request_rtc_storage_bk7231t_emits_define() -> None: + _set_libretiny(PLATFORM_BK72XX, FAMILY_BK7231T) + preferences.request_rtc_storage() + assert "USE_LIBRETINY_RTC_PREFERENCES" in _define_names() + + +def test_request_rtc_storage_other_libretiny_family_is_a_no_op() -> None: + _set_libretiny(PLATFORM_BK72XX, FAMILY_BK7251) + preferences.request_rtc_storage() + assert "USE_LIBRETINY_RTC_PREFERENCES" not in _define_names()