ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
preferences.cpp
Go to the documentation of this file.
1#ifdef USE_LIBRETINY
2
3#include "preferences.h"
5#include "esphome/core/log.h"
6#include <cstring>
7#include <span>
8#include <vector>
9
10namespace esphome::libretiny {
11
12ESPHOME_LOG_TAG(TAG, "preferences");
13
14struct NVSData {
15 uint32_t key;
16 SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
17#ifdef USE_LIBRETINY_RTC_PREFERENCES
18 bool rtc;
19 bool is_rtc() const { return this->rtc; }
20#else
21 static constexpr bool is_rtc() { return false; }
22#endif
23};
24
25static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
26
27// The FlashDB name of a key: its decimal digits, with a prefix for storage: rtc so one pass over the
28// database finds every rtc entry. Without rtc storage is_rtc() is a constant false and the branch folds away.
29static constexpr size_t KEY_NAME_SIZE = UINT32_MAX_STR_SIZE + 1;
30static constexpr char RTC_KEY_PREFIX = 'r';
31static void key_name(char (&buf)[KEY_NAME_SIZE], uint32_t key, bool rtc) {
32 char *digits = buf;
33 if (rtc)
34 *digits++ = RTC_KEY_PREFIX;
35 uint32_to_str(std::span<char, UINT32_MAX_STR_SIZE>(digits, UINT32_MAX_STR_SIZE), key);
36}
37
38#ifdef USE_LIBRETINY_RTC_PREFERENCES
39// Whether this boot followed a reset that RTC memory survives on ESP32 and ESP8266. LibreTiny chips
40// have no such memory, so storage: rtc keeps preferences in flash and uses this to forget them.
41static bool boot_kept_rtc_preferences() {
42 switch (lt_get_reboot_reason()) {
43 case REBOOT_REASON_SOFTWARE:
44 case REBOOT_REASON_WATCHDOG:
45 case REBOOT_REASON_CRASH:
46 case REBOOT_REASON_SLEEP_GPIO:
47 case REBOOT_REASON_SLEEP_RTC:
48 case REBOOT_REASON_SLEEP_USB:
49 return true;
50 default: // power-on, brown-out, reset pin, debugger, unknown
51 return false;
52 }
53}
54#endif // USE_LIBRETINY_RTC_PREFERENCES
55
56bool LibreTinyPreferenceBackend::save(const uint8_t *data, size_t len) {
57 // try find in pending saves and update that; a flash and an rtc entry may share a key
58 for (auto &obj : s_pending_save) {
59 if (obj.key == this->key && obj.is_rtc() == this->is_rtc()) {
60 obj.data.set(data, len);
61 return true;
62 }
63 }
64 NVSData save{};
65 save.key = this->key;
66#ifdef USE_LIBRETINY_RTC_PREFERENCES
67 save.rtc = this->rtc;
68#endif
69 save.data.set(data, len);
70 s_pending_save.push_back(std::move(save));
71 ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
72 return true;
73}
74
75bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) {
76 // try find in pending saves and load from that
77 for (auto &obj : s_pending_save) {
78 if (obj.key == this->key && obj.is_rtc() == this->is_rtc()) {
79 if (obj.data.size() != len) {
80 // size mismatch
81 return false;
82 }
83 memcpy(data, obj.data.data(), len);
84 return true;
85 }
86 }
87
88 char key_str[KEY_NAME_SIZE];
89 key_name(key_str, this->key, this->is_rtc());
90 fdb_blob_make(this->blob, data, len);
91 size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
92 if (actual_len != len) {
93 ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
94 return false;
95 } else {
96 ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len);
97 }
98 return true;
99}
100
102 //
103 fdb_err_t err = fdb_kvdb_init(&this->db, "esphome", "kvs", NULL, NULL);
104 if (err != FDB_NO_ERR) {
105 LT_E("fdb_kvdb_init(...) failed: %d", err);
106 return;
107 }
108 LT_I("Preferences initialized");
109#ifdef USE_LIBRETINY_RTC_PREFERENCES
110 if (!boot_kept_rtc_preferences())
111 this->discard_rtc_entries_();
112#endif
113}
114
115#ifdef USE_LIBRETINY_RTC_PREFERENCES
117 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
118 return ESPPreferenceObject(new LibreTinyPreferenceBackend(this->make_backend_(type, !in_flash)));
119}
120
121// One pass over the database deletes every rtc entry. FlashDB only marks a deleted entry's status in place
122// and its iterator steps by address, so deleting the current entry is safe. This runs before the logger
123// exists, so it logs through LibreTiny; a failed delete is a flash fault the next write will report as well.
125 uint16_t discarded = 0;
126 fdb_kv_iterator iterator;
127 fdb_kv_iterator_init(&iterator);
128 while (fdb_kv_iterate(&this->db, &iterator)) {
129 const char *name = iterator.curr_kv.name;
130 if (name[0] != RTC_KEY_PREFIX)
131 continue;
132 if (const fdb_err_t err = fdb_kv_del(&this->db, name); err != FDB_NO_ERR) {
133 LT_E("Failed to discard rtc preference %s: %d", name, err);
134 continue;
135 }
136 discarded++;
137 }
138 if (discarded != 0) {
139 LT_D("Discarded %u rtc preferences after a hard reset", discarded);
140 }
141}
142#endif
143
145 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
147}
148
151 backend.key = type;
152 backend.db = &this->db;
153 backend.blob = &this->blob;
154#ifdef USE_LIBRETINY_RTC_PREFERENCES
155 backend.rtc = rtc;
156#endif
157 return backend;
158}
159
161 LibreTinyPreferenceBackend backend = this->make_backend_(type);
162 return backend.load(data, len);
163}
164
166 if (s_pending_save.empty())
167 return true;
168
169 ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
170 int cached = 0, written = 0, failed = 0;
171 fdb_err_t last_err = FDB_NO_ERR;
172 uint32_t last_key = 0;
173
174 for (const auto &save : s_pending_save) {
175 char key_str[KEY_NAME_SIZE];
176 key_name(key_str, save.key, save.is_rtc());
177 ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
178 if (this->is_changed_(&this->db, save, key_str)) {
179 ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
180 fdb_blob_make(&this->blob, save.data.data(), save.data.size());
181 fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
182 if (err != FDB_NO_ERR) {
183 ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
184 failed++;
185 last_err = err;
186 last_key = save.key;
187 continue;
188 }
189 written++;
190 } else {
191 ESP_LOGV(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
192 cached++;
193 }
194 }
195 s_pending_save.clear();
196
197 if (failed > 0) {
198 ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%d for key=%" PRIu32,
199 cached + written + failed, cached, written, failed, last_err, last_key);
200 } else if (written > 0) {
201 ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
202 failed);
203 } else {
204 ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
205 failed);
206 }
207
208 return failed == 0;
209}
210
211bool LibreTinyPreferences::is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) {
212 struct fdb_kv kv;
213 fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv);
214 if (kvp == nullptr) {
215 ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str);
216 return true;
217 }
218
219 // Check size first - if different, data has changed
220 if (kv.value_len != to_save.data.size()) {
221 return true;
222 }
223
224 // Most preferences are small, use stack buffer with heap fallback for large ones
225 SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
226 fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
227 size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
228 if (actual_len != kv.value_len) {
229 ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %zu != %zu", key_str, actual_len, (size_t) kv.value_len);
230 return true;
231 }
232
233 // Compare the actual data. fdb_kv_get_blob filled stored_data through this->blob, which the analyzer cannot see.
234 // NOLINTNEXTLINE(clang-analyzer-unix.cstring.UninitializedRead)
235 return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
236}
237
239 ESP_LOGD(TAG, "Erasing storage");
240 s_pending_save.clear();
241
242 fdb_kv_set_default(&this->db);
243 fdb_kvdb_deinit(&this->db);
244 return true;
245}
246
247static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
248
249LibreTinyPreferences *get_preferences() { return &s_preferences; }
250
252 s_preferences.open();
253 global_preferences = &s_preferences;
254}
255
256} // namespace esphome::libretiny
257
258namespace esphome {
259ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
260} // namespace esphome
261
262#endif // USE_LIBRETINY
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)
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.
bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str)
LibreTinyPreferenceBackend make_backend_(uint32_t type, bool rtc=false)
uint8_t type
const char * key
LibreTinyPreferences * get_preferences()
ESPHOME_LOG_TAG(TAG, "libretiny.gpio")
Preferences ESPPreferences
Definition preferences.h:43
const void size_t len
Definition hal.h:68
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
int written
Definition helpers.h:1262
static void uint32_t
uint16_t length
Definition tt21100.cpp:0