#ifdef USE_ESP32 #include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include #include #include #include namespace esphome::esp32 { static const char *const TAG = "esp32.preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; 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); return true; } bool ESP32PreferenceBackend::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.data.size() != len) { // size mismatch return false; } memcpy(data, obj.data.data(), len); return true; } } char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); size_t actual_len; esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return false; } if (actual_len != len) { ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } err = nvs_get_blob(this->nvs_handle, key_str, data, &len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return false; } else { ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); } return true; } void ESP32Preferences::open() { nvs_flash_init(); esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); if (err == 0) return; ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); nvs_flash_deinit(); nvs_flash_erase(); nvs_flash_init(); err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); if (err != 0) { this->nvs_handle = 0; } } ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = this->nvs_handle; pref->key = type; return ESPPreferenceObject(pref); } bool ESP32Preferences::sync() { if (s_pending_save.empty()) return true; ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; uint32_t last_key = 0; for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); if (err != 0) { ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; continue; } written++; } else { ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } s_pending_save.clear(); ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), last_key); } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes esp_err_t err = nvs_commit(this->nvs_handle); if (err != 0) { ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); return false; } return failed == 0; } bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { size_t actual_len; esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return true; } // Check size first before allocating memory if (actual_len != to_save.data.size()) { return true; } // Most preferences are small, use stack buffer with heap fallback for large ones SmallBufferWithHeapFallback<256> stored_data(actual_len); err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool ESP32Preferences::reset() { ESP_LOGD(TAG, "Erasing storage"); s_pending_save.clear(); nvs_flash_deinit(); nvs_flash_erase(); // Make the handle invalid to prevent any saves until restart this->nvs_handle = 0; return true; } static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32Preferences *get_preferences() { return &s_preferences; } void setup_preferences() { s_preferences.open(); global_preferences = &s_preferences; } } // namespace esphome::esp32 namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome #endif // USE_ESP32