LCOV - code coverage report
Current view: top level - src - storage_manager.cpp (source / functions) Coverage Total Hit
Test: ESP-NOW Manager Unified Coverage Lines: 100.0 % 149 149
Test Date: 2026-04-21 02:14:32 Functions: 100.0 % 21 21
Branches: 93.0 % 86 80

             Branch data     Line data    Source code
       1                 :             : // #include <algorithm>
       2                 :             : // #include <cinttypes>
       3                 :             : // #include <cstring>
       4                 :             : 
       5                 :             : // #include "esp_attr.h"
       6                 :             : #include "esp_log.h"
       7                 :             : #include "esp_rom_crc.h"
       8                 :             : // #include "nvs.h"
       9                 :             : // #include "nvs_flash.h"
      10                 :             : 
      11                 :             : #include "storage_manager.hpp"
      12                 :             : // #include "persistence_backend.hpp"
      13                 :             : 
      14                 :             : static const char* TAG = "StorageManager";
      15                 :             : 
      16                 :          33 : StorageManager::StorageManager(
      17                 :             :     std::unique_ptr<IPersistenceBackend> rtc_peers,
      18                 :             :     std::unique_ptr<IPersistenceBackend> rtc_channel,
      19                 :             :     std::unique_ptr<IPersistenceBackend> rtc_stats,
      20                 :             :     std::unique_ptr<IPersistenceBackend> nvs_peers,
      21                 :             :     std::unique_ptr<IPersistenceBackend> nvs_channel,
      22                 :          33 :     std::unique_ptr<IPersistenceBackend> nvs_stats)
      23                 :          33 :     : rtc_peers_backend_(std::move(rtc_peers))
      24                 :          33 :     , rtc_channel_backend_(std::move(rtc_channel))
      25                 :          33 :     , rtc_stats_backend_(std::move(rtc_stats))
      26                 :          33 :     , nvs_peers_backend_(std::move(nvs_peers))
      27                 :          33 :     , nvs_channel_backend_(std::move(nvs_channel))
      28                 :          33 :     , nvs_stats_backend_(std::move(nvs_stats))
      29                 :             : {
      30                 :          33 : }
      31                 :             : 
      32                 :          66 : StorageManager::~StorageManager() {}
      33                 :             : 
      34                 :          53 : template <typename T> uint32_t StorageManager::calculate_crc(const T& data)
      35                 :             : {
      36                 :             :     static_assert(std::is_standard_layout_v<T>, "T must be standard layout for offset");
      37                 :             :     static_assert(offsetof(T, crc) != 0, "T must have a crc field");
      38                 :          53 :     return esp_rom_crc32_le(0, reinterpret_cast<const uint8_t*>(&data), offsetof(T, crc));
      39                 :             : }
      40                 :             : 
      41                 :           6 : esp_err_t StorageManager::load_channel(uint8_t& wifi_channel)
      42                 :             : {
      43                 :           6 :     PersistentChannel data = {};
      44                 :             : 
      45                 :           6 :     esp_err_t ret;
      46                 :           6 :     ret = load_raw_channel(data);
      47                 :             : 
      48         [ +  + ]:           6 :     if (ret == ESP_OK) {
      49                 :           3 :         wifi_channel = data.wifi_channel;
      50                 :             :     }
      51                 :           6 :     return ret;
      52                 :             : }
      53                 :             : 
      54                 :           4 : esp_err_t StorageManager::store_channel(uint8_t channel)
      55                 :             : {
      56                 :           4 :     PersistentChannel data = {};
      57                 :           4 :     data.magic = PersistentChannel::MAGIC;
      58                 :           4 :     data.wifi_channel = channel;
      59                 :             : 
      60                 :           4 :     bool is_dirty = is_data_dirty(data);
      61         [ +  + ]:           4 :     if (!is_dirty) {
      62                 :             :         return ESP_OK;
      63                 :             :     }
      64                 :             : 
      65                 :           3 :     data.crc = calculate_crc(data);
      66                 :           3 :     rtc_channel_backend_->save(&data, sizeof(data));
      67                 :             : 
      68                 :           3 :     esp_err_t ret = nvs_channel_backend_->save(&data, sizeof(data));
      69                 :           3 :     if (ret == ESP_OK) {
      70                 :             :         ESP_LOGI(TAG, "Saved channel to Storage");
      71                 :             :     }
      72                 :             :     else {
      73                 :           3 :         ESP_LOGE(TAG, "Failed to save channel to NVS: %s", esp_err_to_name(ret));
      74                 :             :     }
      75                 :           3 :     return ret;
      76                 :             : }
      77                 :             : 
      78                 :           8 : esp_err_t StorageManager::load_peers(etl::ivector<PersistentPeer>& peers)
      79                 :             : {
      80                 :           8 :     PersistentPeers data = {};
      81                 :             : 
      82                 :           8 :     esp_err_t ret;
      83                 :           8 :     ret = load_raw_peers(data);
      84                 :             : 
      85         [ +  + ]:           8 :     if (ret == ESP_OK) {
      86                 :             :         // Ensure vector is empty before populating
      87         [ +  + ]:           4 :         peers.clear();
      88                 :             :         // Populate vector with peers from data
      89         [ +  + ]:           4 :         const size_t peers_to_copy = std::min(data.num_peers, (uint8_t)peers.capacity());
      90         [ +  + ]:          36 :         for (size_t i = 0; i < peers_to_copy; ++i) {
      91                 :          32 :             peers.push_back(data.peers[i]);
      92                 :             :         }
      93                 :             :     }
      94                 :           8 :     return ret;
      95                 :             : }
      96                 :             : 
      97                 :           5 : esp_err_t StorageManager::store_peers(const etl::ivector<PersistentPeer>& peers, bool force_nvs_commit)
      98                 :             : {
      99                 :           5 :     PersistentPeers data = {};
     100                 :           5 :     data.magic = PersistentPeers::MAGIC;
     101                 :           5 :     data.version = PersistentPeers::VERSION;
     102         [ +  + ]:           5 :     data.num_peers = std::min(peers.size(), (size_t)MAX_PEERS);
     103                 :             : 
     104         [ +  + ]:          28 :     for (size_t i = 0; i < data.num_peers; ++i) {
     105                 :          23 :         data.peers[i] = peers[i];
     106                 :             :     }
     107                 :             : 
     108                 :             :     // Check if data is dirty
     109                 :           5 :     bool is_dirty = is_data_dirty(data);
     110                 :             : 
     111                 :             :     // Calculate CRC to save if data is dirty
     112                 :           5 :     data.crc = calculate_crc(data);
     113                 :             : 
     114                 :             :     // If data is not dirty and force_nvs_commit is false, return
     115         [ +  + ]:           5 :     if (!is_dirty && !force_nvs_commit) {
     116                 :             :         return ESP_OK;
     117                 :             :     }
     118                 :             : 
     119                 :             :     // If is dirty, persist data to RTC
     120         [ +  + ]:           4 :     if (is_dirty) {
     121                 :           3 :         rtc_peers_backend_->save(&data, sizeof(data));
     122                 :           3 :         ESP_LOGI(TAG, "Saved data to RTC");
     123                 :             :     }
     124                 :             : 
     125                 :             :     // Persist data to NVS
     126                 :           4 :     esp_err_t err = nvs_peers_backend_->save(&data, sizeof(data));
     127                 :           4 :     if (err == ESP_OK) {
     128                 :             :         ESP_LOGI(TAG, "Saved data to NVS");
     129                 :             :     }
     130                 :             :     else {
     131                 :           4 :         ESP_LOGE(TAG, "Failed to save data to NVS: %s", esp_err_to_name(err));
     132                 :             :     }
     133                 :             : 
     134                 :           4 :     return err;
     135                 :             : }
     136                 :             : 
     137                 :           6 : esp_err_t StorageManager::load_stats(etl::ivector<PeerStatisticsPersist>& stats)
     138                 :             : {
     139         [ +  + ]:         114 :     PersistentStats stats_data = {};
     140                 :           6 :     esp_err_t ret = load_raw_stats(stats_data);
     141         [ +  + ]:           6 :     if (ret != ESP_OK) {
     142                 :             :         return ret;
     143                 :             :     }
     144                 :             : 
     145         [ -  + ]:           3 :     stats.clear();
     146         [ -  + ]:           3 :     const uint8_t count = std::min(stats_data.num_stats, (uint8_t)stats.capacity());
     147         [ +  + ]:          10 :     for (uint8_t i = 0; i < count; ++i) {
     148                 :           7 :         stats.push_back(stats_data.stats[i]);
     149                 :             :     }
     150                 :             :     return ESP_OK;
     151                 :             : }
     152                 :             : 
     153                 :           4 : esp_err_t StorageManager::store_stats(const etl::ivector<PeerStatisticsPersist>& stats)
     154                 :             : {
     155         [ +  + ]:          76 :     PersistentStats data = {};
     156                 :           4 :     data.magic = PersistentStats::MAGIC;
     157                 :           4 :     data.version = PersistentStats::VERSION;
     158         [ +  + ]:           4 :     data.num_stats = std::min(stats.size(), (size_t)MAX_PEERS);
     159                 :             : 
     160         [ +  + ]:          27 :     for (size_t i = 0; i < data.num_stats; ++i) {
     161                 :          23 :         data.stats[i] = stats[i];
     162                 :             :     }
     163                 :             : 
     164                 :             :     // Check if data is dirty
     165                 :           4 :     bool is_dirty = is_data_dirty(data);
     166                 :             : 
     167                 :             :     // Calculate CRC to save if data is dirty
     168                 :           4 :     data.crc = calculate_crc(data);
     169                 :             : 
     170                 :             :     // If data is not dirty, return OK
     171         [ +  + ]:           4 :     if (!is_dirty) {
     172                 :             :         return ESP_OK;
     173                 :             :     }
     174                 :             : 
     175                 :             :     // If is dirty, persist data to RTC and NVS
     176                 :           3 :     rtc_stats_backend_->save(&data, sizeof(data));
     177                 :           3 :     esp_err_t err = nvs_stats_backend_->save(&data, sizeof(data));
     178                 :             : 
     179                 :           3 :     if (err == ESP_OK) {
     180                 :             :         ESP_LOGI(TAG, "Saved statistics to Storage");
     181                 :             :     }
     182                 :             :     else {
     183                 :           3 :         ESP_LOGE(TAG, "Failed to save statistics to NVS: %s", esp_err_to_name(err));
     184                 :             :     }
     185                 :             : 
     186                 :           3 :     return err;
     187                 :             : }
     188                 :             : 
     189                 :             : // ================================================================
     190                 :             : // Private helpers
     191                 :             : // ================================================================
     192                 :             : 
     193                 :           8 : esp_err_t StorageManager::load_raw_peers(PersistentPeers& out)
     194                 :             : {
     195                 :           8 :     esp_err_t ret;
     196                 :             :     // 1. Try RTC first (fast, survives deep-sleep)
     197                 :           8 :     ret = rtc_peers_backend_->load(&out, sizeof(PersistentPeers));
     198         [ +  + ]:           8 :     if (ret == ESP_OK) {
     199                 :           4 :         ret = validate_peers_data(out);
     200         [ +  + ]:           4 :         if (ret == ESP_OK) {
     201                 :             :             ESP_LOGD(TAG, "Loaded data from RTC");
     202                 :             :             return ESP_OK;
     203                 :             :         }
     204                 :             :     }
     205                 :             :     // 2. Fall back to NVS
     206                 :           7 :     ret = nvs_peers_backend_->load(&out, sizeof(PersistentPeers));
     207         [ +  + ]:           7 :     if (ret == ESP_OK) {
     208                 :           3 :         ret = validate_peers_data(out);
     209         [ +  - ]:           3 :         if (ret == ESP_OK) {
     210                 :             :             // Sync RTC with NVS
     211                 :           3 :             rtc_peers_backend_->save(&out, sizeof(PersistentPeers));
     212                 :           3 :             ESP_LOGD(TAG, "Loaded data from NVS");
     213                 :           3 :             return ESP_OK;
     214                 :             :         }
     215                 :             :     }
     216                 :             :     return ret; // nothing valid found
     217                 :             : }
     218                 :             : 
     219                 :           7 : esp_err_t StorageManager::validate_peers_data(const PersistentPeers& data)
     220                 :             : {
     221                 :             :     // Validade MAGIC, VERSION and CRC
     222         [ +  + ]:           7 :     if (data.magic != PersistentPeers::MAGIC) {
     223                 :             :         ESP_LOGW(TAG, "Magic mismatch: 0x%08X", data.magic);
     224                 :             :         return ESP_ERR_INVALID_STATE;
     225                 :             :     }
     226         [ +  + ]:           6 :     if (data.version != PersistentPeers::VERSION) {
     227                 :             :         ESP_LOGW(TAG, "Version mismatch: %d", data.version);
     228                 :             :         return ESP_ERR_INVALID_VERSION;
     229                 :             :     }
     230         [ +  + ]:           5 :     if (data.crc != calculate_crc(data)) {
     231                 :           1 :         ESP_LOGW(TAG, "CRC mismatch");
     232                 :           1 :         return ESP_ERR_INVALID_CRC;
     233                 :             :     }
     234                 :             :     return ESP_OK;
     235                 :             : }
     236                 :             : 
     237                 :           5 : bool StorageManager::is_data_dirty(const PersistentPeers& new_peers)
     238                 :             : {
     239                 :           5 :     PersistentPeers current_rtc;
     240                 :             : 
     241                 :             :     // If we can't load from RTC, assume it's dirty to be safe
     242         [ +  + ]:           5 :     if (rtc_peers_backend_->load(&current_rtc, sizeof(PersistentPeers)) != ESP_OK) {
     243                 :             :         return true;
     244                 :             :     }
     245                 :             : 
     246                 :             :     // Using our safe custom comparison operator. This compares only actual field
     247                 :             :     // values instead of the entire raw memory block, preventing struct padding bytes
     248                 :             :     // or unused array elements from triggering a false "dirty" state, thus
     249                 :             :     // avoiding needless NVS flash writes.
     250                 :           4 :     return (current_rtc != new_peers);
     251                 :             : }
     252                 :             : 
     253                 :           6 : esp_err_t StorageManager::load_raw_channel(PersistentChannel& out)
     254                 :             : {
     255                 :           6 :     esp_err_t ret;
     256                 :             :     // 1. Try RTC first (fast, survives deep-sleep)
     257                 :           6 :     ret = rtc_channel_backend_->load(&out, sizeof(PersistentChannel));
     258         [ +  + ]:           6 :     if (ret == ESP_OK) {
     259                 :           3 :         ret = validate_channel_data(out);
     260         [ +  + ]:           3 :         if (ret == ESP_OK) {
     261                 :             :             ESP_LOGD(TAG, "Loaded data from RTC");
     262                 :             :             return ESP_OK;
     263                 :             :         }
     264                 :             :     }
     265                 :             :     // 2. Fall back to NVS
     266                 :           5 :     ret = nvs_channel_backend_->load(&out, sizeof(PersistentChannel));
     267         [ +  + ]:           5 :     if (ret == ESP_OK) {
     268                 :           2 :         ret = validate_channel_data(out);
     269         [ +  - ]:           2 :         if (ret == ESP_OK) {
     270                 :             :             // Sync RTC with NVS
     271                 :           2 :             rtc_channel_backend_->save(&out, sizeof(PersistentChannel));
     272                 :           2 :             ESP_LOGD(TAG, "Loaded data from NVS");
     273                 :           2 :             return ESP_OK;
     274                 :             :         }
     275                 :             :     }
     276                 :             :     return ret; // nothing valid found
     277                 :             : }
     278                 :             : 
     279                 :           5 : esp_err_t StorageManager::validate_channel_data(const PersistentChannel& channel)
     280                 :             : {
     281                 :             :     // Validade MAGIC and CRC
     282         [ +  + ]:           5 :     if (channel.magic != PersistentChannel::MAGIC) {
     283                 :             :         ESP_LOGW(TAG, "Magic mismatch: 0x%08X", channel.magic);
     284                 :             :         return ESP_ERR_INVALID_STATE;
     285                 :             :     }
     286         [ +  + ]:           4 :     if (channel.crc != calculate_crc(channel)) {
     287                 :           1 :         ESP_LOGW(TAG, "CRC mismatch");
     288                 :           1 :         return ESP_ERR_INVALID_CRC;
     289                 :             :     }
     290                 :             :     return ESP_OK;
     291                 :             : }
     292                 :             : 
     293                 :           4 : bool StorageManager::is_data_dirty(const PersistentChannel& new_channel)
     294                 :             : {
     295                 :           4 :     PersistentChannel current_rtc;
     296                 :             : 
     297                 :             :     // If we can't load from RTC, assume it's dirty to be safe
     298         [ +  + ]:           4 :     if (rtc_channel_backend_->load(&current_rtc, sizeof(PersistentChannel)) != ESP_OK) {
     299                 :             :         return true;
     300                 :             :     }
     301                 :             : 
     302         [ +  + ]:           5 :     return (current_rtc != new_channel);
     303                 :             : }
     304                 :             : 
     305                 :           6 : esp_err_t StorageManager::load_raw_stats(PersistentStats& out)
     306                 :             : {
     307                 :           6 :     esp_err_t ret;
     308                 :             :     // 1. Try RTC first (fast, survives deep-sleep)
     309                 :           6 :     ret = rtc_stats_backend_->load(&out, sizeof(PersistentStats));
     310         [ +  + ]:           6 :     if (ret == ESP_OK) {
     311                 :           3 :         ret = validate_stats_data(out);
     312         [ +  + ]:           3 :         if (ret == ESP_OK) {
     313                 :             :             ESP_LOGD(TAG, "Loaded stats from RTC");
     314                 :             :             return ESP_OK;
     315                 :             :         }
     316                 :             :     }
     317                 :             :     // 2. Fall back to NVS
     318                 :           5 :     ret = nvs_stats_backend_->load(&out, sizeof(PersistentStats));
     319         [ +  + ]:           5 :     if (ret == ESP_OK) {
     320                 :           2 :         ret = validate_stats_data(out);
     321         [ +  - ]:           2 :         if (ret == ESP_OK) {
     322                 :             :             // Sync RTC with NVS
     323                 :           2 :             rtc_stats_backend_->save(&out, sizeof(PersistentStats));
     324                 :           2 :             ESP_LOGD(TAG, "Loaded stats from NVS");
     325                 :           2 :             return ESP_OK;
     326                 :             :         }
     327                 :             :     }
     328                 :             :     return ret; // nothing valid found
     329                 :             : }
     330                 :             : 
     331                 :           5 : esp_err_t StorageManager::validate_stats_data(const PersistentStats& data)
     332                 :             : {
     333                 :             :     // Validade MAGIC, VERSION and CRC
     334         [ +  + ]:           5 :     if (data.magic != PersistentStats::MAGIC) {
     335                 :             :         ESP_LOGW(TAG, "Stats magic mismatch: 0x%08X", (unsigned int)data.magic);
     336                 :             :         return ESP_ERR_INVALID_STATE;
     337                 :             :     }
     338         [ +  - ]:           4 :     if (data.version != PersistentStats::VERSION) {
     339                 :             :         ESP_LOGW(TAG, "Stats version mismatch: %d", (int)data.version);
     340                 :             :         return ESP_ERR_INVALID_VERSION;
     341                 :             :     }
     342         [ +  + ]:           4 :     if (data.crc != calculate_crc(data)) {
     343                 :           1 :         ESP_LOGW(TAG, "Stats CRC mismatch");
     344                 :           1 :         return ESP_ERR_INVALID_CRC;
     345                 :             :     }
     346                 :             :     return ESP_OK;
     347                 :             : }
     348                 :             : 
     349                 :           4 : bool StorageManager::is_data_dirty(const PersistentStats& new_stats)
     350                 :             : {
     351                 :           4 :     PersistentStats current_rtc;
     352                 :             : 
     353                 :             :     // If we can't load from RTC, assume it's dirty to be safe
     354         [ +  + ]:           4 :     if (rtc_stats_backend_->load(&current_rtc, sizeof(PersistentStats)) != ESP_OK) {
     355                 :             :         return true;
     356                 :             :     }
     357                 :             : 
     358                 :           3 :     return (current_rtc != new_stats);
     359                 :             : }
        

Generated by: LCOV version 2.0-1