[libretiny] Emulate rtc preference storage on the Beken BK7231T, BK7231N and BK7238 with flash and the reset reason (#21007)

This commit is contained in:
Michele Porelli
2026-10-17 14:59:40 -10:00
committed by GitHub
parent 89afb111ad
commit 928cf7d696
6 changed files with 192 additions and 20 deletions
@@ -1,6 +1,8 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/defines.h"
#include <cstddef>
#include <cstdint>
@@ -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;
+85 -10
View File
@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstring>
#include <span>
#include <vector>
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<NVSData> 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<char, UINT32_MAX_STR_SIZE>(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());
+10 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/defines.h"
#include "esphome/core/preference_backend.h"
#include <flashdb.h>
@@ -12,9 +13,14 @@ class LibreTinyPreferences final : public PreferencesMixin<LibreTinyPreferences>
public:
using PreferencesMixin<LibreTinyPreferences>::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<LibreTinyPreferences>
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();
+1
View File
@@ -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
+33 -9
View File
@@ -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):
+53
View File
@@ -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()