ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
preferences.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include "preferences.h"
5#include "esphome/core/log.h"
7#include <esp_attr.h>
8#include <nvs_flash.h>
9#include <soc/soc_caps.h>
10#include <cstring>
11#include <vector>
12
13namespace esphome::esp32 {
14
15ESPHOME_LOG_TAG(TAG, "preferences");
16
17struct NVSData {
18 uint32_t key;
19 SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
20};
21
22static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
23
24// RTC memory backend for preferences requested with in_flash=false. Survives deep sleep and
25// software/CPU resets, but not power loss; integrity is guarded by a per-record checksum so
26// power-on garbage is detected on load. Keep this small: RTC memory is scarce and shared.
27//
28// Only compiled in when USE_ESP32_RTC_PREFERENCES_STORAGE is set (see preferences.h): the storage
29// buffer reserves RTC memory, so it exists only when some config option actually selected RTC
30// storage AND the variant has RTC memory (the ESP32-C2 and -C61 have none, so RTC_NOINIT_ATTR would
31// have no section to land in and fail to link). Otherwise in_flash=false transparently falls back
32// to NVS (see make_preference below).
33//
34// On variants with only RTC fast memory (C3/C6/H2/P4/C5/...) RTC_NOINIT_ATTR lands in RTC fast memory.
35// This is still safe: the linker reserves .rtc_noinit ahead of any RTC-fast-as-heap pool
36// (CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP), and IDF keeps the RTC fast power domain on in deep
37// sleep (forced on whether or not it is used as heap), so the data is retained across both resets and
38// deep sleep -- only power loss clears it.
39#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
40static constexpr size_t RTC_PREF_SIZE_WORDS = 64; // 256 bytes
41static constexpr size_t RTC_PREF_MAX_WORDS = 255; // length_words field is a uint8_t
42
43// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
44static RTC_NOINIT_ATTR uint32_t s_rtc_storage[RTC_PREF_SIZE_WORDS];
45
46static bool save_to_rtc(uint16_t offset, uint32_t key, uint8_t length_words, const uint8_t *data, size_t len) {
47 if (rtc_pref_bytes_to_words(len) != length_words)
48 return false;
49 const size_t buffer_size = static_cast<size_t>(length_words) + 1;
50 if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
51 return false;
52 rtc_pref_encode(&s_rtc_storage[offset], key, length_words, data, len);
53 return true;
54}
55
56static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, uint8_t *data, size_t len) {
57 if (rtc_pref_bytes_to_words(len) != length_words)
58 return false;
59 const size_t buffer_size = static_cast<size_t>(length_words) + 1;
60 if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
61 return false;
62 return rtc_pref_decode(&s_rtc_storage[offset], key, length_words, data, len);
63}
64#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
65
66// open() runs from app_main() before the logger is initialized, so any failure
67// must be deferred until after global_logger is set. This is emitted from the
68// first make_preference() call, which runs from the generated setup() after
69// log->pre_setup() has run at EARLY_INIT priority.
70static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
71
72bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
73#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
74 if (!this->in_flash)
75 return save_to_rtc(this->rtc_offset, this->key, this->length_words, data, len);
76#endif
77 // try find in pending saves and update that
78 for (auto &obj : s_pending_save) {
79 if (obj.key == this->key) {
80 obj.data.set(data, len);
81 return true;
82 }
83 }
84 NVSData save{};
85 save.key = this->key;
86 save.data.set(data, len);
87 s_pending_save.push_back(std::move(save));
88 ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
89 return true;
90}
91
92bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
93#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
94 if (!this->in_flash)
95 return load_from_rtc(this->rtc_offset, this->key, this->length_words, data, len);
96#endif
97 // try find in pending saves and load from that
98 for (auto &obj : s_pending_save) {
99 if (obj.key == this->key) {
100 if (obj.data.size() != len) {
101 // size mismatch
102 return false;
103 }
104 memcpy(data, obj.data.data(), len);
105 return true;
106 }
107 }
108
109 char key_str[UINT32_MAX_STR_SIZE];
110 uint32_to_str(key_str, this->key);
111 size_t actual_len;
112 esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
113 if (err != 0) {
114 ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
115 return false;
116 }
117 if (actual_len != len) {
118 ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
119 return false;
120 }
121 err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
122 if (err != 0) {
123 ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
124 return false;
125 } else {
126 ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
127 }
128 return true;
129}
130
132 // Runs from app_main() before the logger is initialized; any logging here
133 // must be deferred. See s_open_err and make_preference() below.
134 // Components that need NVS check nvs_handle instead of initializing it again.
135 nvs_flash_init();
136 esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
137 if (err == 0)
138 return;
139
140 s_open_err = err;
141 nvs_flash_deinit();
142 nvs_flash_erase();
143 nvs_flash_init();
144
145 err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
146 if (err != 0) {
147 this->nvs_handle = 0;
148 }
149}
150
152#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
153 if (!in_flash)
154 return this->make_rtc_preference_(length, type);
155#else
156 if (!in_flash) {
157 // RTC storage is not compiled in (no config option selected it), so this request
158 // falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly
159 // asking for RTC storage can discover the fallback.
160 static bool warned = false;
161 if (!warned) {
162 ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')");
163 warned = true;
164 }
165 }
166#endif
167 // in_flash, or RTC storage not compiled in: fall back to NVS.
168 return this->make_preference(length, type);
169}
170
172 if (s_open_err != ESP_OK) {
173 if (this->nvs_handle == 0) {
174 ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
175 } else {
176 ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
177 }
178 s_open_err = ESP_OK;
179 }
180 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
182}
183
185 // in_flash keeps its default of true, selecting the NVS path
187 backend.nvs_handle = this->nvs_handle;
188 backend.key = type;
189 return backend;
190}
191
192bool ESP32Preferences::load_from_key(uint32_t type, uint8_t *data, size_t len) {
193 ESP32PreferenceBackend backend = this->make_backend_(type);
194 return backend.load(data, len);
195}
196
197#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
199 const uint32_t length_words = rtc_pref_bytes_to_words(length);
200 if (length_words > RTC_PREF_MAX_WORDS) {
201 ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words);
202 return {};
203 }
204 const uint32_t total_words = length_words + 1; // +1 for checksum
205 if (static_cast<size_t>(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) {
206 ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words);
207 return {};
208 }
209 auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
210 pref->key = type;
211 pref->in_flash = false;
212 pref->rtc_offset = this->current_rtc_offset_;
213 pref->length_words = static_cast<uint8_t>(length_words);
214 this->current_rtc_offset_ += static_cast<uint16_t>(total_words);
215
216 return ESPPreferenceObject(pref);
217}
218#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
219
221 if (s_pending_save.empty())
222 return true;
223
224 ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
225 int cached = 0, written = 0, failed = 0;
226 esp_err_t last_err = ESP_OK;
227 uint32_t last_key = 0;
228
229 for (const auto &save : s_pending_save) {
230 char key_str[UINT32_MAX_STR_SIZE];
231 uint32_to_str(key_str, save.key);
232 ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
233 if (this->is_changed_(this->nvs_handle, save, key_str)) {
234 esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
235 ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
236 if (err != 0) {
237 ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
238 failed++;
239 last_err = err;
240 last_key = save.key;
241 continue;
242 }
243 written++;
244 } else {
245 ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
246 cached++;
247 }
248 }
249 s_pending_save.clear();
250
251 if (failed > 0) {
252 ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%s for key=%" PRIu32,
253 cached + written + failed, cached, written, failed, esp_err_to_name(last_err), last_key);
254 } else if (written > 0) {
255 ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
256 failed);
257 } else {
258 ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
259 failed);
260 }
261
262 // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
263 esp_err_t err = nvs_commit(this->nvs_handle);
264 if (err != 0) {
265 ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
266 return false;
267 }
268
269 return failed == 0;
270}
271
272bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
273 size_t actual_len;
274 esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
275 if (err != 0) {
276 ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
277 return true;
278 }
279 // Check size first before allocating memory
280 if (actual_len != to_save.data.size()) {
281 return true;
282 }
283 // Most preferences are small, use stack buffer with heap fallback for large ones
284 SmallBufferWithHeapFallback<256> stored_data(actual_len);
285 err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
286 if (err != 0) {
287 ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
288 return true;
289 }
290 return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
291}
292
294 ESP_LOGD(TAG, "Erasing storage");
295 s_pending_save.clear();
296#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
297 // Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is
298 // deliberately left alone: existing backends keep pointing at their allocated slots, and reset()
299 // is always followed by a restart (same reason nvs_handle is zeroed below).
300 memset(s_rtc_storage, 0, sizeof(s_rtc_storage));
301#endif
302
303 nvs_flash_deinit();
304 nvs_flash_erase();
305 // Make the handle invalid to prevent any saves until restart
306 this->nvs_handle = 0;
307 return true;
308}
309
310static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
311
312ESP32Preferences *get_preferences() { return &s_preferences; }
313
315 s_preferences.open();
316 global_preferences = &s_preferences;
317}
318
319} // namespace esphome::esp32
320
321namespace esphome {
322ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
323} // namespace esphome
324
325#endif // USE_ESP32
Helper class for efficient buffer allocation - uses stack for small sizes, heap for large This is use...
Definition helpers.h:827
Small buffer optimization - stores data inline when small, heap-allocates for large data This avoids ...
Definition helpers.h:206
size_t size() const
Definition helpers.h:273
bool load(uint8_t *data, size_t len)
bool save(const uint8_t *data, size_t len)
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str)
ESP32PreferenceBackend make_backend_(uint32_t type) const
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)
bool load_from_key(uint32_t type, uint8_t *data, size_t len)
One-shot read of a stored preference by key, without allocating a backend.
ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type)
uint8_t type
const char * key
ESP32Preferences * get_preferences()
void setup_preferences()
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG)
size_t rtc_pref_bytes_to_words(size_t bytes)
Round a byte count up to whole 32-bit words.
const void size_t len
Definition hal.h:68
bool rtc_pref_decode(const uint32_t *buffer, uint32_t type, uint8_t length_words, uint8_t *data, size_t len)
Verify the checksum of a record held in buffer (length_words data words + 1 checksum word) and,...
size_t uint32_to_str(std::span< char, UINT32_MAX_STR_SIZE > buf, uint32_t val)
Write unsigned 32-bit integer to buffer with compile-time size check.
Definition helpers.h:1536
ESPPreferences * global_preferences
void rtc_pref_encode(uint32_t *buffer, uint32_t type, uint8_t length_words, const uint8_t *data, size_t len)
Encode len data bytes into buffer (length_words data words + 1 trailing checksum word).
int written
Definition helpers.h:1262
static void uint32_t
uint16_t length
Definition tt21100.cpp:0