ESPHome 2026.8.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
15static const char *const 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 nvs_flash_init();
135 esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
136 if (err == 0)
137 return;
138
139 s_open_err = err;
140 nvs_flash_deinit();
141 nvs_flash_erase();
142 nvs_flash_init();
143
144 err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
145 if (err != 0) {
146 this->nvs_handle = 0;
147 }
148}
149
151#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
152 if (!in_flash)
153 return this->make_rtc_preference_(length, type);
154#else
155 if (!in_flash) {
156 // RTC storage is not compiled in (no config option selected it), so this request
157 // falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly
158 // asking for RTC storage can discover the fallback.
159 static bool warned = false;
160 if (!warned) {
161 ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')");
162 warned = true;
163 }
164 }
165#endif
166 // in_flash, or RTC storage not compiled in: fall back to NVS.
167 return this->make_preference(length, type);
168}
169
171 if (s_open_err != ESP_OK) {
172 if (this->nvs_handle == 0) {
173 ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
174 } else {
175 ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
176 }
177 s_open_err = ESP_OK;
178 }
179 auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
180 pref->nvs_handle = this->nvs_handle;
181 pref->key = type;
182 pref->in_flash = true;
183
184 return ESPPreferenceObject(pref);
185}
186
187#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
189 const uint32_t length_words = rtc_pref_bytes_to_words(length);
190 if (length_words > RTC_PREF_MAX_WORDS) {
191 ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words);
192 return {};
193 }
194 const uint32_t total_words = length_words + 1; // +1 for checksum
195 if (static_cast<size_t>(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) {
196 ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words);
197 return {};
198 }
199 auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
200 pref->key = type;
201 pref->in_flash = false;
202 pref->rtc_offset = this->current_rtc_offset_;
203 pref->length_words = static_cast<uint8_t>(length_words);
204 this->current_rtc_offset_ += static_cast<uint16_t>(total_words);
205
206 return ESPPreferenceObject(pref);
207}
208#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
209
211 if (s_pending_save.empty())
212 return true;
213
214 ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
215 int cached = 0, written = 0, failed = 0;
216 esp_err_t last_err = ESP_OK;
217 uint32_t last_key = 0;
218
219 for (const auto &save : s_pending_save) {
220 char key_str[UINT32_MAX_STR_SIZE];
221 uint32_to_str(key_str, save.key);
222 ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
223 if (this->is_changed_(this->nvs_handle, save, key_str)) {
224 esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
225 ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
226 if (err != 0) {
227 ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
228 failed++;
229 last_err = err;
230 last_key = save.key;
231 continue;
232 }
233 written++;
234 } else {
235 ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
236 cached++;
237 }
238 }
239 s_pending_save.clear();
240
241 if (failed > 0) {
242 ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%s for key=%" PRIu32,
243 cached + written + failed, cached, written, failed, esp_err_to_name(last_err), last_key);
244 } else if (written > 0) {
245 ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
246 failed);
247 } else {
248 ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
249 failed);
250 }
251
252 // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
253 esp_err_t err = nvs_commit(this->nvs_handle);
254 if (err != 0) {
255 ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
256 return false;
257 }
258
259 return failed == 0;
260}
261
262bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
263 size_t actual_len;
264 esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
265 if (err != 0) {
266 ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
267 return true;
268 }
269 // Check size first before allocating memory
270 if (actual_len != to_save.data.size()) {
271 return true;
272 }
273 // Most preferences are small, use stack buffer with heap fallback for large ones
274 SmallBufferWithHeapFallback<256> stored_data(actual_len);
275 err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
276 if (err != 0) {
277 ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
278 return true;
279 }
280 return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
281}
282
284 ESP_LOGD(TAG, "Erasing storage");
285 s_pending_save.clear();
286#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
287 // Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is
288 // deliberately left alone: existing backends keep pointing at their allocated slots, and reset()
289 // is always followed by a restart (same reason nvs_handle is zeroed below).
290 memset(s_rtc_storage, 0, sizeof(s_rtc_storage));
291#endif
292
293 nvs_flash_deinit();
294 nvs_flash_erase();
295 // Make the handle invalid to prevent any saves until restart
296 this->nvs_handle = 0;
297 return true;
298}
299
300static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
301
302ESP32Preferences *get_preferences() { return &s_preferences; }
303
305 s_preferences.open();
306 global_preferences = &s_preferences;
307}
308
309} // namespace esphome::esp32
310
311namespace esphome {
312ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
313} // namespace esphome
314
315#endif // USE_ESP32
Helper class for efficient buffer allocation - uses stack for small sizes, heap for large This is use...
Definition helpers.h:727
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)
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)
ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type)
uint16_t type
ESP32Preferences * get_preferences()
void setup_preferences()
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:64
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:1327
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:1059
static void uint32_t
uint16_t length
Definition tt21100.cpp:0