mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[libretiny] Emulate rtc preference storage on the Beken BK7231T, BK7231N and BK7238 with flash and the reset reason (#21007)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user