[esp32] Add RTC-backed preferences (honor in_flash flag) (#17073)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chris Boot
2026-07-05 11:48:30 +10:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2c24e82ba3
commit 8ab19c0242
20 changed files with 520 additions and 37 deletions
@@ -11,8 +11,11 @@ class ESP32PreferenceBackend final {
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key;
uint32_t nvs_handle;
uint32_t key{0};
uint32_t nvs_handle{0}; // NVS (flash) path
uint16_t rtc_offset{0}; // RTC path: word offset into the RTC storage region
uint8_t length_words{0}; // RTC path: data length in 32-bit words
bool in_flash{true}; // true: store in NVS (flash); false: store in RTC memory
};
class ESP32Preferences;
+103
View File
@@ -3,7 +3,10 @@
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <esp_attr.h>
#include <nvs_flash.h>
#include <soc/soc_caps.h>
#include <cstring>
#include <vector>
@@ -18,6 +21,48 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// RTC memory backend for preferences requested with in_flash=false. Survives deep sleep and
// software/CPU resets, but not power loss; integrity is guarded by a per-record checksum so
// power-on garbage is detected on load. Keep this small: RTC memory is scarce and shared.
//
// Only compiled in when USE_ESP32_RTC_PREFERENCES_STORAGE is set (see preferences.h): the storage
// buffer reserves RTC memory, so it exists only when some config option actually selected RTC
// storage AND the variant has RTC memory (the ESP32-C2 and -C61 have none, so RTC_NOINIT_ATTR would
// have no section to land in and fail to link). Otherwise in_flash=false transparently falls back
// to NVS (see make_preference below).
//
// On variants with only RTC fast memory (C3/C6/H2/P4/C5/...) RTC_NOINIT_ATTR lands in RTC fast memory.
// This is still safe: the linker reserves .rtc_noinit ahead of any RTC-fast-as-heap pool
// (CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP), and IDF keeps the RTC fast power domain on in deep
// sleep (forced on whether or not it is used as heap), so the data is retained across both resets and
// deep sleep -- only power loss clears it.
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
static constexpr size_t RTC_PREF_SIZE_WORDS = 64; // 256 bytes
static constexpr size_t RTC_PREF_MAX_WORDS = 255; // length_words field is a uint8_t
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static RTC_NOINIT_ATTR uint32_t s_rtc_storage[RTC_PREF_SIZE_WORDS];
static bool save_to_rtc(uint16_t offset, uint32_t key, uint8_t length_words, const uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
rtc_pref_encode(&s_rtc_storage[offset], key, length_words, data, len);
return true;
}
static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
return rtc_pref_decode(&s_rtc_storage[offset], key, length_words, data, len);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
@@ -25,6 +70,10 @@ static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-n
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return save_to_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -41,6 +90,10 @@ bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
}
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return load_from_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -94,6 +147,26 @@ void ESP32Preferences::open() {
}
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!in_flash)
return this->make_rtc_preference_(length, type);
#else
if (!in_flash) {
// RTC storage is not compiled in (no config option selected it), so this request
// falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly
// asking for RTC storage can discover the fallback.
static bool warned = false;
if (!warned) {
ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')");
warned = true;
}
}
#endif
// in_flash, or RTC storage not compiled in: fall back to NVS.
return this->make_preference(length, type);
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
@@ -106,10 +179,34 @@ ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t ty
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
pref->in_flash = true;
return ESPPreferenceObject(pref);
}
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
ESPPreferenceObject ESP32Preferences::make_rtc_preference_(size_t length, uint32_t type) {
const uint32_t length_words = rtc_pref_bytes_to_words(length);
if (length_words > RTC_PREF_MAX_WORDS) {
ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words);
return {};
}
const uint32_t total_words = length_words + 1; // +1 for checksum
if (static_cast<size_t>(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) {
ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words);
return {};
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->key = type;
pref->in_flash = false;
pref->rtc_offset = this->current_rtc_offset_;
pref->length_words = static_cast<uint8_t>(length_words);
this->current_rtc_offset_ += static_cast<uint16_t>(total_words);
return ESPPreferenceObject(pref);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
bool ESP32Preferences::sync() {
if (s_pending_save.empty())
return true;
@@ -186,6 +283,12 @@ bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save,
bool ESP32Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is
// deliberately left alone: existing backends keep pointing at their allocated slots, and reset()
// is always followed by a restart (same reason nvs_handle is zeroed below).
memset(s_rtc_storage, 0, sizeof(s_rtc_storage));
#endif
nvs_flash_deinit();
nvs_flash_erase();
+18 -3
View File
@@ -2,6 +2,15 @@
#ifdef USE_ESP32
#include "esphome/core/preference_backend.h"
#include <soc/soc_caps.h>
// RTC-backed preference storage is compiled in only when a config option actually selects it
// (USE_ESP32_RTC_PREFERENCES, emitted during code generation) and the variant has RTC memory
// (SOC_RTC_MEM_SUPPORTED; the ESP32-C2 and -C61 have none). Otherwise in_flash=false falls
// back to NVS and no RTC memory is reserved.
#if defined(USE_ESP32_RTC_PREFERENCES) && SOC_RTC_MEM_SUPPORTED
#define USE_ESP32_RTC_PREFERENCES_STORAGE
#endif
namespace esphome::esp32 {
@@ -11,9 +20,8 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
public:
using PreferencesMixin<ESP32Preferences>::make_preference;
void open();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
// Two-argument form defaults to NVS (flash) storage, preserving historic ESP32 behavior.
ESPPreferenceObject make_preference(size_t length, uint32_t type);
bool sync();
bool reset();
@@ -22,6 +30,13 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// RTC-backed storage (in_flash=false).
ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type);
// Next free word offset in the RTC storage region (bump allocated in make_preference order).
uint16_t current_rtc_offset_{0};
#endif
};
void setup_preferences();
+7 -21
View File
@@ -8,6 +8,7 @@ extern "C" {
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <cstring>
@@ -80,16 +81,6 @@ static uint32_t get_esp8266_flash_sector() {
}
static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; }
template<class It> uint32_t calculate_crc(It first, It last, uint32_t type) {
uint32_t crc = type;
while (first != last) {
crc ^= (*first++ * 2654435769UL) >> 1;
}
return crc;
}
static bool save_to_flash(size_t offset, const uint32_t *data, size_t len) {
for (uint32_t i = 0; i < len; i++) {
uint32_t j = offset + i;
@@ -137,21 +128,19 @@ static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
if (rtc_pref_bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
rtc_pref_encode(buffer, this->type, this->length_words, data, len);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
if (rtc_pref_bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
@@ -161,10 +150,7 @@ bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
return rtc_pref_decode(buffer, this->type, this->length_words, data, len);
}
void ESP8266Preferences::setup() {
@@ -177,13 +163,13 @@ void ESP8266Preferences::setup() {
}
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
const uint32_t length_words = bytes_to_words(length);
const uint32_t length_words = rtc_pref_bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
return {};
}
const uint32_t total_words = length_words + 1; // +1 for CRC
const uint32_t total_words = length_words + 1; // +1 for checksum
uint16_t offset;
if (in_flash) {
@@ -1,3 +1,4 @@
from esphome import preferences
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
@@ -10,10 +11,17 @@ preferences_ns = cg.esphome_ns.namespace("preferences")
IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.Component)
CONF_FLASH_WRITE_INTERVAL = "flash_write_interval"
CONF_RTC_STORAGE = "rtc_storage"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(IntervalSyncer),
cv.Optional(CONF_FLASH_WRITE_INTERVAL, default="60s"): cv.update_interval,
# Compile the RTC-backed storage into the ESP32 preferences backend even
# when no other option selects it, so components (including external
# ones) requesting in_flash=false are honoured instead of falling back
# to NVS. No default: absence means "no request" (see
# preferences.validate_rtc_storage for the per-platform rules).
cv.Optional(CONF_RTC_STORAGE): preferences.validate_rtc_storage,
}
).extend(cv.COMPONENT_SCHEMA)
@@ -26,4 +34,6 @@ async def to_code(config):
cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP")
else:
cg.add(var.set_write_interval(write_interval))
if config.get(CONF_RTC_STORAGE):
preferences.request_rtc_storage()
await cg.register_component(var, config)
+4 -1
View File
@@ -1,4 +1,4 @@
from esphome import automation
from esphome import automation, preferences
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -7,6 +7,7 @@ from esphome.const import (
CONF_NUM_ATTEMPTS,
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_STORAGE,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
CONF_REBOOT_TIMEOUT, default="5min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}),
**preferences.storage_schema(),
}
).extend(cv.COMPONENT_SCHEMA),
_remove_id_if_disabled,
@@ -87,6 +89,7 @@ async def to_code(config):
config[CONF_NUM_ATTEMPTS],
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
preferences.is_in_flash(config[CONF_STORAGE]),
)
cg.add(RawExpression(f"if ({condition}) return"))
+3 -3
View File
@@ -162,13 +162,13 @@ bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
}
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time,
uint32_t boot_is_good_after) {
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after,
bool in_flash) {
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_boot_is_good_after_ = boot_is_good_after;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(RTC_KEY, false);
this->rtc_ = global_preferences->make_preference<uint32_t>(RTC_KEY, in_flash);
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Check partition state to detect if bootloader supports rollback
+1 -1
View File
@@ -17,7 +17,7 @@ constexpr uint32_t RTC_KEY = 233825507UL;
/// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent final : public Component {
public:
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after);
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, bool in_flash);
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
+27 -4
View File
@@ -1,10 +1,10 @@
import logging
import math
from esphome import automation
from esphome import automation, preferences
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.const import CONF_ENABLED, CONF_USE_PSRAM
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
const,
@@ -50,6 +50,7 @@ from esphome.const import (
CONF_REBOOT_TIMEOUT,
CONF_SSID,
CONF_STATIC_IP,
CONF_STORAGE,
CONF_SUBNET,
CONF_TIMEOUT,
CONF_TTLS_PHASE_2,
@@ -434,6 +435,22 @@ def _validate(config):
CONF_PASSIVE_SCAN = "passive_scan"
FAST_CONNECT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
**preferences.storage_schema(),
}
)
def _fast_connect_schema(value):
"""Accept the historic plain boolean or a dict with enabled/storage keys."""
if isinstance(value, bool):
value = {CONF_ENABLED: value}
return FAST_CONNECT_SCHEMA(value)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -459,7 +476,7 @@ CONFIG_SCHEMA = cv.All(
rtl87xx="none",
ln882x="light",
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_FAST_CONNECT, default=False): _fast_connect_schema,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
VALIDATE_WIFI_MIN_AUTH_MODE,
@@ -619,8 +636,14 @@ async def to_code(config):
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
if CONF_MIN_AUTH_MODE in config:
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
if config[CONF_FAST_CONNECT]:
fast_connect = config[CONF_FAST_CONNECT]
if fast_connect[CONF_ENABLED]:
cg.add_define("USE_WIFI_FAST_CONNECT")
# The storage default preserves this preference's historic location:
# ESP8266 has always used RTC memory; every other platform effectively
# used flash (the in_flash flag was previously ignored outside ESP8266).
if preferences.is_in_flash(fast_connect[CONF_STORAGE]):
cg.add_define("USE_WIFI_FAST_CONNECT_IN_FLASH")
# passive_scan defaults to false in C++ - only set if true
if config[CONF_PASSIVE_SCAN]:
cg.add(var.set_passive_scan(True))
+7 -1
View File
@@ -649,7 +649,13 @@ void WiFiComponent::start() {
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
#ifdef USE_WIFI_FAST_CONNECT
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
#ifdef USE_WIFI_FAST_CONNECT_IN_FLASH
const bool fast_connect_in_flash = true;
#else
const bool fast_connect_in_flash = false;
#endif
this->fast_connect_pref_ =
global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, fast_connect_in_flash);
#endif
SavedWifiSettings save{};
+1
View File
@@ -976,6 +976,7 @@ CONF_STEP_PIN = "step_pin"
CONF_STILL_THRESHOLD = "still_threshold"
CONF_STOP = "stop"
CONF_STOP_ACTION = "stop_action"
CONF_STORAGE = "storage"
CONF_STORE_BASELINE = "store_baseline"
CONF_SUBNET = "subnet"
CONF_SUBSCRIBE_QOS = "subscribe_qos"
+2
View File
@@ -240,6 +240,7 @@
#define USE_OTA_ROLLBACK
#define USE_OTA_SIGNED_VERIFICATION
#define USE_ESP32_MIN_CHIP_REVISION_SET
#define USE_ESP32_RTC_PREFERENCES
#define USE_ESP32_SRAM1_AS_IRAM
#define USE_BLUETOOTH_PROXY
@@ -300,6 +301,7 @@
#define USE_CAPTIVE_PORTAL_GZIP
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
#define USE_WIFI_PHY_MODE
#define USE_WIFI_IP_STATE_LISTENERS
#define USE_WIFI_SCAN_RESULTS_LISTENERS
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace esphome {
// Shared storage format for word-addressable preference backends.
//
// Several platforms persist preferences as a buffer of 32-bit words followed by a
// single checksum word, seeded with the preference's `type` (its hashed key). This
// format is used for RTC user memory (ESP8266, ESP32) and for the ESP8266
// flash-emulation buffer. The helpers here are platform independent; each backend
// supplies its own word read/write primitives and offset allocation.
/// Round a byte count up to whole 32-bit words.
inline size_t rtc_pref_bytes_to_words(size_t bytes) { return (bytes + 3) / 4; }
/// Compute the integrity checksum over [first, last), seeded with `type`.
/// Iterates over 32-bit words; the result is stored as the trailing word of a record.
/// (Not a true CRC -- it XORs each word after a Fibonacci-hash multiply -- but the
/// algorithm is kept as-is for compatibility with records written by old firmware.)
template<class It> uint32_t rtc_pref_calculate_checksum(It first, It last, uint32_t type) {
uint32_t checksum = type;
while (first != last) {
// UINT32_C keeps the multiply wrapping at 32 bits regardless of the width of
// unsigned long, so 64-bit host builds compute the same value as the devices.
checksum ^= (*first++ * UINT32_C(2654435769)) >> 1;
}
return checksum;
}
/// Encode `len` data bytes into `buffer` (length_words data words + 1 trailing checksum word).
/// `buffer` must have capacity for at least `length_words + 1` words. Trailing padding in
/// the final data word is zeroed so the checksum is deterministic.
inline void rtc_pref_encode(uint32_t *buffer, uint32_t type, uint8_t length_words, const uint8_t *data, size_t len) {
memset(buffer, 0, (static_cast<size_t>(length_words) + 1) * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[length_words] = rtc_pref_calculate_checksum(buffer, buffer + length_words, type);
}
/// Verify the checksum of a record held in `buffer` (length_words data words + 1 checksum
/// word) and, on success, copy `len` bytes out to `data`. Returns false on checksum mismatch
/// (e.g. the record was never written or RTC memory holds power-on garbage).
inline bool rtc_pref_decode(const uint32_t *buffer, uint32_t type, uint8_t length_words, uint8_t *data, size_t len) {
if (buffer[length_words] != rtc_pref_calculate_checksum(buffer, buffer + length_words, type)) {
return false;
}
memcpy(data, buffer, len);
return true;
}
} // namespace esphome
+106
View File
@@ -0,0 +1,106 @@
"""Helpers for letting a component choose where a preference is persisted.
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).
Components include :func:`storage_schema` in their config and convert the chosen
value with :func:`is_in_flash` when calling ``make_preference``.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_STORAGE
from esphome.core import CORE
STORAGE_FLASH = "flash"
STORAGE_RTC = "rtc"
def _rtc_supported() -> bool:
"""Whether the active platform has an RTC-backed preferences backend.
Mirrors the C++ ``SOC_RTC_MEM_SUPPORTED`` guard in the ESP32 backend: the ESP32-C2
and -C61 have no RTC memory at all, so RTC storage is unavailable there.
"""
if CORE.is_esp8266:
return True
if CORE.is_esp32:
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2, VARIANT_ESP32C61
return get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32C61)
return False
def _default_storage() -> str:
"""Default that preserves each platform's historic behavior.
ESP8266 has always stored these preferences in RTC memory; every other
platform effectively used flash. Evaluated at validation time.
"""
return STORAGE_RTC if CORE.is_esp8266 else STORAGE_FLASH
def _validate_storage(value):
value = cv.one_of(STORAGE_FLASH, STORAGE_RTC, lower=True)(value)
if value == STORAGE_RTC and not _rtc_supported():
raise cv.Invalid(
f"'{STORAGE_RTC}' storage is not supported on this platform; only "
f"'{STORAGE_FLASH}' is available"
)
return value
def storage_schema():
"""Return an Optional(CONF_STORAGE) entry for merging into a component schema."""
return {cv.Optional(CONF_STORAGE, default=_default_storage): _validate_storage}
def request_rtc_storage() -> None:
"""Compile the RTC-backed storage into the ESP32 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).
"""
if CORE.is_esp32:
cg.add_define("USE_ESP32_RTC_PREFERENCES")
def validate_rtc_storage(value):
"""Validate a boolean option that requests RTC-backed preference storage.
``false`` means "no request", not "disable": it never turns RTC storage off
(another option selecting ``storage: rtc`` still compiles it in). On ESP8266
the backend is integral and always enabled, so ``false`` is rejected rather
than silently ignored; ``true`` is a tolerated no-op there so shared config
packages work across mixed fleets.
"""
value = cv.boolean(value)
if not value:
if CORE.is_esp8266:
raise cv.Invalid(
"RTC preference storage is always enabled on ESP8266 and cannot "
"be disabled"
)
return value
if not _rtc_supported():
raise cv.Invalid("RTC preference storage is not supported on this platform")
return value
def is_in_flash(value: str) -> bool:
"""Map a CONF_STORAGE value to the ``in_flash`` argument of make_preference.
Call this from ``to_code``: when RTC storage is selected on ESP32 it also emits
the define that compiles the RTC storage buffer into the ESP32 backend (see
:func:`request_rtc_storage`).
"""
in_flash = value == STORAGE_FLASH
if not in_flash:
request_rtc_storage()
return in_flash
+1 -1
View File
@@ -555,7 +555,7 @@ def lint_constants_usage():
# Maximum allowed CONF_ constants in esphome/const.py.
# This file is frozen — new constants go in esphome/components/const/__init__.py.
# Decrease this number when constants are moved out of const.py.
CONST_PY_MAX_CONF = 1014
CONST_PY_MAX_CONF = 1015
@lint_content_check(include=["esphome/const.py"])
@@ -0,0 +1,4 @@
# Exercises the opt-in that compiles the RTC-backed preference storage into
# the ESP32 backend without any other option selecting it.
preferences:
rtc_storage: true
@@ -0,0 +1,4 @@
# Exercises the ESP32 RTC-backed preferences path (storage: rtc) for safe_mode.
safe_mode:
num_attempts: 3
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect with RTC-backed preference storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
enabled: true
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect overriding the ESP8266 default (rtc)
# back to flash storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
storage: flash
+149
View File
@@ -0,0 +1,149 @@
"""Tests for esphome.preferences storage backend selection."""
import pytest
from esphome import preferences
from esphome.components.esp32 import KEY_ESP32
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C61,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_STORAGE,
KEY_CORE,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.core import CORE
def _set_platform(platform: str) -> None:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
def _set_esp32(variant: str) -> None:
_set_platform(PLATFORM_ESP32)
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
def _validate(value: dict):
return cv.Schema(preferences.storage_schema())(value)
def _define_names() -> set[str]:
return {define.name for define in CORE.defines}
def test_is_in_flash() -> None:
_set_platform(PLATFORM_ESP8266)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
# The RTC storage define is ESP32-specific.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_is_in_flash_esp32_rtc_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
def test_request_rtc_storage_esp32_only() -> None:
_set_platform(PLATFORM_ESP8266)
preferences.request_rtc_storage()
# ESP8266 always has its RTC backend; no define is needed or emitted.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_request_rtc_storage_esp32_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
preferences.request_rtc_storage()
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_validate_rtc_storage_accepted(variant: str) -> None:
_set_esp32(variant)
assert preferences.validate_rtc_storage(True) is True
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
# Tolerated no-op: the ESP8266 backend always has RTC storage.
assert preferences.validate_rtc_storage(True) is True
# But it cannot be disabled, so an explicit false is an error.
with pytest.raises(cv.Invalid, match="always enabled on ESP8266"):
preferences.validate_rtc_storage(False)
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_validate_rtc_storage_rejected_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
# Disabling it is always fine.
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
@pytest.mark.parametrize(
("platform", "expected"),
[
# Defaults preserve each platform's historic behavior.
(PLATFORM_ESP8266, preferences.STORAGE_RTC),
(PLATFORM_RP2040, preferences.STORAGE_FLASH),
],
)
def test_default_storage_per_platform(platform: str, expected: str) -> None:
_set_platform(platform)
assert _validate({})[CONF_STORAGE] == expected
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C2])
def test_default_storage_esp32_is_flash(variant: str) -> None:
# ESP32 defaults to flash on every variant, including those without RTC memory.
_set_esp32(variant)
assert _validate({})[CONF_STORAGE] == preferences.STORAGE_FLASH
def test_rtc_allowed_on_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_rtc_allowed_on_esp32_with_rtc_memory(variant: str) -> None:
_set_esp32(variant)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_rtc_rejected_on_esp32_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_rtc_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_flash_allowed_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
assert _validate({CONF_STORAGE: "flash"})[CONF_STORAGE] == preferences.STORAGE_FLASH