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(¤t_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(¤t_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(¤t_rtc, sizeof(PersistentStats)) != ESP_OK) {
355 : : return true;
356 : : }
357 : :
358 : 3 : return (current_rtc != new_stats);
359 : : }
|