1184 lines
30 KiB
C
1184 lines
30 KiB
C
.
|
|
|
|
// === Início de: components/storage_service/src/storage_service.c ===
|
|
// =========================
|
|
// storage_service.c (corrigido)
|
|
// - Remove ponteiros out_ptr/inout_len_ptr da mensagem
|
|
// - GET_STR/GET_BLOB devolvem dados via storage_resp_t (cópia)
|
|
// - Evita corrupção se houver timeout
|
|
// =========================
|
|
|
|
#include "storage_service.h"
|
|
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
|
|
#include "esp_log.h"
|
|
#include "esp_err.h"
|
|
#include "nvs.h"
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "freertos/queue.h"
|
|
#include "freertos/semphr.h"
|
|
|
|
static const char *TAG = "storage_service";
|
|
|
|
typedef enum
|
|
{
|
|
OP_SET_U8,
|
|
OP_SET_U16,
|
|
OP_SET_U32,
|
|
OP_SET_STR,
|
|
OP_SET_BLOB,
|
|
|
|
OP_ERASE_KEY,
|
|
|
|
OP_GET_U8,
|
|
OP_GET_U16,
|
|
OP_GET_U32,
|
|
OP_GET_STR,
|
|
OP_GET_BLOB,
|
|
|
|
OP_FLUSH,
|
|
} storage_op_t;
|
|
|
|
typedef enum
|
|
{
|
|
T_U8,
|
|
T_U16,
|
|
T_U32,
|
|
T_STR,
|
|
T_BLOB,
|
|
} storage_type_t;
|
|
|
|
typedef struct
|
|
{
|
|
esp_err_t err;
|
|
uint32_t value; // U8/U16/U32
|
|
size_t len; // STR/BLOB: tamanho (STR sem '\0', BLOB tamanho real)
|
|
uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // payload para STR/BLOB (até MAX)
|
|
} storage_resp_t;
|
|
|
|
typedef struct
|
|
{
|
|
storage_op_t op;
|
|
char ns[STORAGE_NS_MAX_LEN];
|
|
char key[STORAGE_KEY_MAX_LEN];
|
|
|
|
// SET/GET numéricos
|
|
uint32_t value;
|
|
|
|
// SET STR/BLOB: bytes e len (copiados na mensagem => seguro async)
|
|
uint16_t len;
|
|
uint8_t bytes[STORAGE_MAX_VALUE_BYTES];
|
|
|
|
// Apenas para ajudar semântica do GET_BLOB (query vs read)
|
|
bool blob_query_only;
|
|
|
|
QueueHandle_t resp_q; // opcional (GET/FLUSH sync)
|
|
} storage_msg_t;
|
|
|
|
typedef struct
|
|
{
|
|
bool used;
|
|
bool erase;
|
|
char ns[STORAGE_NS_MAX_LEN];
|
|
char key[STORAGE_KEY_MAX_LEN];
|
|
storage_type_t type;
|
|
|
|
uint32_t value; // U8/U16/U32
|
|
uint16_t len; // STR/BLOB (até MAX)
|
|
uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // STR/BLOB
|
|
} pending_item_t;
|
|
|
|
static bool s_inited = false;
|
|
|
|
// Queue principal estática (evita malloc)
|
|
static StaticQueue_t s_qbuf;
|
|
static uint8_t s_qstorage[STORAGE_QUEUE_LEN * sizeof(storage_msg_t)];
|
|
static QueueHandle_t s_q = NULL;
|
|
|
|
// Pending table (evita malloc)
|
|
static pending_item_t s_pending[STORAGE_MAX_PENDING];
|
|
static size_t s_pending_count = 0;
|
|
|
|
// debounce
|
|
static bool s_dirty = false;
|
|
static TickType_t s_commit_deadline = 0;
|
|
|
|
// Sync: fila global + mutex global
|
|
static StaticQueue_t s_sync_qbuf;
|
|
static uint8_t s_sync_qstor[sizeof(storage_resp_t)];
|
|
static QueueHandle_t s_sync_q = NULL;
|
|
|
|
static StaticSemaphore_t s_sync_mtx_buf;
|
|
static SemaphoreHandle_t s_sync_mtx = NULL;
|
|
|
|
static inline esp_err_t map_not_found(esp_err_t e)
|
|
{
|
|
return (e == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : e;
|
|
}
|
|
|
|
static bool safe_copy_str(char *dst, size_t dst_sz, const char *src)
|
|
{
|
|
if (!dst || dst_sz == 0 || !src)
|
|
return false;
|
|
size_t n = strnlen(src, dst_sz);
|
|
if (n >= dst_sz)
|
|
return false;
|
|
memcpy(dst, src, n);
|
|
dst[n] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static int find_pending(const char *ns, const char *key)
|
|
{
|
|
for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
|
|
{
|
|
if (!s_pending[i].used)
|
|
continue;
|
|
if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) == 0 &&
|
|
strncmp(s_pending[i].key, key, STORAGE_KEY_MAX_LEN) == 0)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static bool pending_is_erased(const char *ns, const char *key)
|
|
{
|
|
int idx = find_pending(ns, key);
|
|
if (idx < 0)
|
|
return false;
|
|
return s_pending[idx].used && s_pending[idx].erase;
|
|
}
|
|
|
|
static int alloc_pending_slot(void)
|
|
{
|
|
for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
|
|
{
|
|
if (!s_pending[i].used)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void mark_dirty(void)
|
|
{
|
|
s_dirty = true;
|
|
s_commit_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(STORAGE_COMMIT_DEBOUNCE_MS);
|
|
}
|
|
|
|
static esp_err_t ensure_pending_entry(int *out_idx, const char *ns, const char *key)
|
|
{
|
|
int idx = find_pending(ns, key);
|
|
if (idx < 0)
|
|
{
|
|
idx = alloc_pending_slot();
|
|
if (idx < 0)
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
memset(&s_pending[idx], 0, sizeof(s_pending[idx]));
|
|
s_pending[idx].used = true;
|
|
|
|
if (!safe_copy_str(s_pending[idx].ns, sizeof(s_pending[idx].ns), ns))
|
|
{
|
|
s_pending[idx].used = false;
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (!safe_copy_str(s_pending[idx].key, sizeof(s_pending[idx].key), key))
|
|
{
|
|
s_pending[idx].used = false;
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
s_pending_count++;
|
|
}
|
|
if (out_idx)
|
|
*out_idx = idx;
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t pending_set_num(const char *ns, const char *key, storage_type_t type, uint32_t v)
|
|
{
|
|
int idx = -1;
|
|
esp_err_t err = ensure_pending_entry(&idx, ns, key);
|
|
if (err != ESP_OK)
|
|
return err;
|
|
|
|
s_pending[idx].erase = false;
|
|
s_pending[idx].type = type;
|
|
s_pending[idx].value = v;
|
|
s_pending[idx].len = 0;
|
|
|
|
mark_dirty();
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t pending_set_bytes(const char *ns, const char *key, storage_type_t type,
|
|
const uint8_t *bytes, uint16_t len)
|
|
{
|
|
if (!bytes && len > 0)
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (len > STORAGE_MAX_VALUE_BYTES)
|
|
return ESP_ERR_INVALID_SIZE;
|
|
|
|
int idx = -1;
|
|
esp_err_t err = ensure_pending_entry(&idx, ns, key);
|
|
if (err != ESP_OK)
|
|
return err;
|
|
|
|
s_pending[idx].erase = false;
|
|
s_pending[idx].type = type;
|
|
s_pending[idx].value = 0;
|
|
s_pending[idx].len = len;
|
|
|
|
if (len > 0)
|
|
memcpy(s_pending[idx].bytes, bytes, len);
|
|
if (len < STORAGE_MAX_VALUE_BYTES)
|
|
memset(&s_pending[idx].bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);
|
|
|
|
mark_dirty();
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t pending_erase(const char *ns, const char *key)
|
|
{
|
|
int idx = -1;
|
|
esp_err_t err = ensure_pending_entry(&idx, ns, key);
|
|
if (err != ESP_OK)
|
|
return err;
|
|
|
|
s_pending[idx].erase = true;
|
|
mark_dirty();
|
|
return ESP_OK;
|
|
}
|
|
|
|
static bool pending_get_num(const char *ns, const char *key, storage_type_t type, uint32_t *out)
|
|
{
|
|
int idx = find_pending(ns, key);
|
|
if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type)
|
|
return false;
|
|
|
|
if (out)
|
|
*out = s_pending[idx].value;
|
|
return true;
|
|
}
|
|
|
|
static bool pending_get_bytes(const char *ns, const char *key, storage_type_t type,
|
|
uint8_t *out, size_t out_sz, uint16_t *out_len)
|
|
{
|
|
int idx = find_pending(ns, key);
|
|
if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type)
|
|
return false;
|
|
|
|
uint16_t len = s_pending[idx].len;
|
|
|
|
if (out_len)
|
|
*out_len = len;
|
|
|
|
if (out)
|
|
{
|
|
size_t n = (len <= out_sz) ? (size_t)len : out_sz;
|
|
if (n > 0)
|
|
memcpy(out, s_pending[idx].bytes, n);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Commit: agrupa por namespace e faz 1 commit por namespace
|
|
static esp_err_t commit_all_pending(void)
|
|
{
|
|
if (s_pending_count == 0)
|
|
{
|
|
s_dirty = false;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t overall = ESP_OK;
|
|
|
|
// Lista de namespaces únicos (sem heap)
|
|
char ns_list[STORAGE_MAX_PENDING][STORAGE_NS_MAX_LEN];
|
|
int ns_count = 0;
|
|
|
|
for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
|
|
{
|
|
if (!s_pending[i].used)
|
|
continue;
|
|
|
|
bool seen = false;
|
|
for (int j = 0; j < ns_count; ++j)
|
|
{
|
|
if (strncmp(ns_list[j], s_pending[i].ns, STORAGE_NS_MAX_LEN) == 0)
|
|
{
|
|
seen = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!seen && ns_count < (int)STORAGE_MAX_PENDING)
|
|
{
|
|
strncpy(ns_list[ns_count], s_pending[i].ns, STORAGE_NS_MAX_LEN - 1);
|
|
ns_list[ns_count][STORAGE_NS_MAX_LEN - 1] = '\0';
|
|
ns_count++;
|
|
}
|
|
}
|
|
|
|
for (int n = 0; n < ns_count; ++n)
|
|
{
|
|
const char *ns = ns_list[n];
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(ns, NVS_READWRITE, &h);
|
|
if (err != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "nvs_open('%s') failed: %s", ns, esp_err_to_name(err));
|
|
overall = err;
|
|
continue;
|
|
}
|
|
|
|
for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
|
|
{
|
|
if (!s_pending[i].used)
|
|
continue;
|
|
if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0)
|
|
continue;
|
|
|
|
if (s_pending[i].erase)
|
|
{
|
|
err = nvs_erase_key(h, s_pending[i].key);
|
|
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
|
|
{
|
|
ESP_LOGE(TAG, "erase %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err));
|
|
overall = err;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
switch (s_pending[i].type)
|
|
{
|
|
case T_U8:
|
|
err = nvs_set_u8(h, s_pending[i].key, (uint8_t)(s_pending[i].value & 0xFF));
|
|
break;
|
|
case T_U16:
|
|
err = nvs_set_u16(h, s_pending[i].key, (uint16_t)(s_pending[i].value & 0xFFFF));
|
|
break;
|
|
case T_U32:
|
|
err = nvs_set_u32(h, s_pending[i].key, s_pending[i].value);
|
|
break;
|
|
|
|
case T_STR:
|
|
{
|
|
// garantir null-termination para nvs_set_str
|
|
char tmp[STORAGE_MAX_VALUE_BYTES + 1];
|
|
uint16_t len = s_pending[i].len;
|
|
if (len > STORAGE_MAX_VALUE_BYTES)
|
|
{
|
|
err = ESP_ERR_INVALID_SIZE;
|
|
break;
|
|
}
|
|
if (len > 0)
|
|
memcpy(tmp, s_pending[i].bytes, len);
|
|
tmp[len] = '\0';
|
|
err = nvs_set_str(h, s_pending[i].key, tmp);
|
|
break;
|
|
}
|
|
|
|
case T_BLOB:
|
|
{
|
|
uint16_t len = s_pending[i].len;
|
|
if (len > STORAGE_MAX_VALUE_BYTES)
|
|
{
|
|
err = ESP_ERR_INVALID_SIZE;
|
|
break;
|
|
}
|
|
err = nvs_set_blob(h, s_pending[i].key, s_pending[i].bytes, (size_t)len);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
err = ESP_ERR_INVALID_STATE;
|
|
break;
|
|
}
|
|
|
|
if (err != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "set %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err));
|
|
overall = err;
|
|
}
|
|
}
|
|
|
|
err = nvs_commit(h);
|
|
if (err != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "commit('%s') failed: %s", ns, esp_err_to_name(err));
|
|
overall = err;
|
|
}
|
|
|
|
nvs_close(h);
|
|
|
|
if (err == ESP_OK)
|
|
{
|
|
for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
|
|
{
|
|
if (!s_pending[i].used)
|
|
continue;
|
|
if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0)
|
|
continue;
|
|
s_pending[i].used = false;
|
|
if (s_pending_count > 0)
|
|
s_pending_count--;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (s_pending_count == 0)
|
|
s_dirty = false;
|
|
else
|
|
mark_dirty();
|
|
|
|
return overall;
|
|
}
|
|
|
|
// -------- Read helpers: devolvem para buffers internos (cópia na resposta) --------
|
|
|
|
static esp_err_t nvs_read_num(storage_type_t type, const char *ns, const char *key, uint32_t *out)
|
|
{
|
|
if (pending_is_erased(ns, key))
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
uint32_t pv = 0;
|
|
if (pending_get_num(ns, key, type, &pv))
|
|
{
|
|
if (out)
|
|
*out = pv;
|
|
return ESP_OK;
|
|
}
|
|
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
|
|
if (err != ESP_OK)
|
|
return map_not_found(err);
|
|
|
|
switch (type)
|
|
{
|
|
case T_U8:
|
|
{
|
|
uint8_t v8 = 0;
|
|
err = nvs_get_u8(h, key, &v8);
|
|
if (err == ESP_OK && out)
|
|
*out = v8;
|
|
break;
|
|
}
|
|
case T_U16:
|
|
{
|
|
uint16_t v16 = 0;
|
|
err = nvs_get_u16(h, key, &v16);
|
|
if (err == ESP_OK && out)
|
|
*out = v16;
|
|
break;
|
|
}
|
|
case T_U32:
|
|
{
|
|
uint32_t v32 = 0;
|
|
err = nvs_get_u32(h, key, &v32);
|
|
if (err == ESP_OK && out)
|
|
*out = v32;
|
|
break;
|
|
}
|
|
default:
|
|
err = ESP_ERR_INVALID_ARG;
|
|
break;
|
|
}
|
|
|
|
nvs_close(h);
|
|
return map_not_found(err);
|
|
}
|
|
|
|
static esp_err_t nvs_read_str_to_resp(const char *ns, const char *key, storage_resp_t *resp)
|
|
{
|
|
if (!resp)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
resp->len = 0;
|
|
|
|
if (pending_is_erased(ns, key))
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
uint16_t plen = 0;
|
|
if (pending_get_bytes(ns, key, T_STR, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen))
|
|
{
|
|
resp->len = (size_t)plen;
|
|
return ESP_OK;
|
|
}
|
|
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
|
|
if (err != ESP_OK)
|
|
return map_not_found(err);
|
|
|
|
// 1) query required size (inclui '\0')
|
|
size_t req = 0;
|
|
err = nvs_get_str(h, key, NULL, &req);
|
|
if (err != ESP_OK)
|
|
{
|
|
nvs_close(h);
|
|
return map_not_found(err);
|
|
}
|
|
|
|
if (req == 0)
|
|
{
|
|
nvs_close(h);
|
|
resp->len = 0;
|
|
return ESP_OK;
|
|
}
|
|
|
|
size_t str_len = req - 1; // sem '\0'
|
|
resp->len = str_len;
|
|
|
|
if (str_len > STORAGE_MAX_VALUE_BYTES)
|
|
{
|
|
nvs_close(h);
|
|
return ESP_ERR_NVS_INVALID_LENGTH;
|
|
}
|
|
|
|
// 2) read
|
|
char tmp[STORAGE_MAX_VALUE_BYTES + 1];
|
|
size_t tmp_sz = sizeof(tmp);
|
|
err = nvs_get_str(h, key, tmp, &tmp_sz);
|
|
nvs_close(h);
|
|
|
|
if (err != ESP_OK)
|
|
return map_not_found(err);
|
|
|
|
size_t n = strnlen(tmp, STORAGE_MAX_VALUE_BYTES);
|
|
memcpy(resp->bytes, tmp, n);
|
|
resp->len = n;
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t nvs_read_blob_to_resp(const char *ns, const char *key, storage_resp_t *resp)
|
|
{
|
|
if (!resp)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
resp->len = 0;
|
|
|
|
if (pending_is_erased(ns, key))
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
uint16_t plen = 0;
|
|
if (pending_get_bytes(ns, key, T_BLOB, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen))
|
|
{
|
|
resp->len = (size_t)plen;
|
|
return ESP_OK;
|
|
}
|
|
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
|
|
if (err != ESP_OK)
|
|
return map_not_found(err);
|
|
|
|
// query size
|
|
size_t req = 0;
|
|
err = nvs_get_blob(h, key, NULL, &req);
|
|
if (err != ESP_OK)
|
|
{
|
|
nvs_close(h);
|
|
if (err == ESP_ERR_NVS_NOT_FOUND)
|
|
resp->len = 0;
|
|
return map_not_found(err);
|
|
}
|
|
|
|
resp->len = req;
|
|
|
|
// Se maior que o máximo, não lemos payload (caller pode só estar a fazer query)
|
|
if (req > STORAGE_MAX_VALUE_BYTES)
|
|
{
|
|
nvs_close(h);
|
|
return ESP_OK; // wrapper decide ESP_ERR_NVS_INVALID_LENGTH se tentar ler com buffer
|
|
}
|
|
|
|
// read payload
|
|
size_t tmp = req;
|
|
err = nvs_get_blob(h, key, resp->bytes, &tmp);
|
|
nvs_close(h);
|
|
|
|
if (err != ESP_OK)
|
|
return map_not_found(err);
|
|
|
|
resp->len = tmp;
|
|
return ESP_OK;
|
|
}
|
|
|
|
// -------- Task --------
|
|
|
|
static void storage_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
storage_msg_t msg;
|
|
|
|
while (true)
|
|
{
|
|
TickType_t now = xTaskGetTickCount();
|
|
|
|
TickType_t wait = portMAX_DELAY;
|
|
if (s_dirty)
|
|
{
|
|
if (now >= s_commit_deadline)
|
|
{
|
|
(void)commit_all_pending();
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
wait = s_commit_deadline - now;
|
|
}
|
|
}
|
|
|
|
if (xQueueReceive(s_q, &msg, wait) != pdTRUE)
|
|
{
|
|
if (s_dirty && xTaskGetTickCount() >= s_commit_deadline)
|
|
{
|
|
(void)commit_all_pending();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
storage_resp_t resp;
|
|
memset(&resp, 0, sizeof(resp));
|
|
resp.err = ESP_OK;
|
|
|
|
switch (msg.op)
|
|
{
|
|
case OP_SET_U8:
|
|
resp.err = pending_set_num(msg.ns, msg.key, T_U8, msg.value & 0xFF);
|
|
break;
|
|
case OP_SET_U16:
|
|
resp.err = pending_set_num(msg.ns, msg.key, T_U16, msg.value & 0xFFFF);
|
|
break;
|
|
case OP_SET_U32:
|
|
resp.err = pending_set_num(msg.ns, msg.key, T_U32, msg.value);
|
|
break;
|
|
|
|
case OP_SET_STR:
|
|
resp.err = pending_set_bytes(msg.ns, msg.key, T_STR, msg.bytes, msg.len);
|
|
break;
|
|
|
|
case OP_SET_BLOB:
|
|
resp.err = pending_set_bytes(msg.ns, msg.key, T_BLOB, msg.bytes, msg.len);
|
|
break;
|
|
|
|
case OP_ERASE_KEY:
|
|
resp.err = pending_erase(msg.ns, msg.key);
|
|
break;
|
|
|
|
case OP_GET_U8:
|
|
resp.err = nvs_read_num(T_U8, msg.ns, msg.key, &resp.value);
|
|
break;
|
|
case OP_GET_U16:
|
|
resp.err = nvs_read_num(T_U16, msg.ns, msg.key, &resp.value);
|
|
break;
|
|
case OP_GET_U32:
|
|
resp.err = nvs_read_num(T_U32, msg.ns, msg.key, &resp.value);
|
|
break;
|
|
|
|
case OP_GET_STR:
|
|
resp.err = nvs_read_str_to_resp(msg.ns, msg.key, &resp);
|
|
break;
|
|
|
|
case OP_GET_BLOB:
|
|
// sempre fazemos "query+maybe-read" para preencher resp.len (e bytes se couber)
|
|
resp.err = nvs_read_blob_to_resp(msg.ns, msg.key, &resp);
|
|
break;
|
|
|
|
case OP_FLUSH:
|
|
resp.err = commit_all_pending();
|
|
break;
|
|
|
|
default:
|
|
resp.err = ESP_ERR_INVALID_ARG;
|
|
break;
|
|
}
|
|
|
|
if (msg.resp_q)
|
|
{
|
|
(void)xQueueSend(msg.resp_q, &resp, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------- Public API --------
|
|
|
|
esp_err_t storage_service_init(void)
|
|
{
|
|
if (s_inited)
|
|
return ESP_OK;
|
|
|
|
s_q = xQueueCreateStatic(STORAGE_QUEUE_LEN, sizeof(storage_msg_t), s_qstorage, &s_qbuf);
|
|
if (!s_q)
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
s_sync_q = xQueueCreateStatic(1, sizeof(storage_resp_t), s_sync_qstor, &s_sync_qbuf);
|
|
if (!s_sync_q)
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
s_sync_mtx = xSemaphoreCreateMutexStatic(&s_sync_mtx_buf);
|
|
if (!s_sync_mtx)
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
memset(s_pending, 0, sizeof(s_pending));
|
|
s_pending_count = 0;
|
|
s_dirty = false;
|
|
|
|
BaseType_t ok = xTaskCreate(storage_task, "storage", 8192, NULL, 5, NULL);
|
|
if (ok != pdPASS)
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
s_inited = true;
|
|
ESP_LOGI(TAG, "storage_service init OK (queue=%d pending=%d debounce=%dms max_bytes=%d)",
|
|
STORAGE_QUEUE_LEN, STORAGE_MAX_PENDING, STORAGE_COMMIT_DEBOUNCE_MS, STORAGE_MAX_VALUE_BYTES);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t send_msg(const storage_msg_t *m, TickType_t to)
|
|
{
|
|
if (!s_inited || !s_q)
|
|
return ESP_ERR_INVALID_STATE;
|
|
return (xQueueSend(s_q, m, to) == pdTRUE) ? ESP_OK : ESP_ERR_TIMEOUT;
|
|
}
|
|
|
|
static esp_err_t set_async_num(storage_op_t op, const char *ns, const char *key, uint32_t v)
|
|
{
|
|
storage_msg_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.op = op;
|
|
if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(m.key, sizeof(m.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
m.value = v;
|
|
return send_msg(&m, 0);
|
|
}
|
|
|
|
esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v)
|
|
{
|
|
return set_async_num(OP_SET_U8, ns, key, v);
|
|
}
|
|
|
|
esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v)
|
|
{
|
|
return set_async_num(OP_SET_U16, ns, key, v);
|
|
}
|
|
|
|
esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v)
|
|
{
|
|
return set_async_num(OP_SET_U32, ns, key, v);
|
|
}
|
|
|
|
esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str)
|
|
{
|
|
if (!str)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
// CORRIGIDO: deteta strings > MAX (sem truncar silenciosamente)
|
|
size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1);
|
|
if (len > STORAGE_MAX_VALUE_BYTES)
|
|
return ESP_ERR_INVALID_SIZE;
|
|
|
|
storage_msg_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.op = OP_SET_STR;
|
|
if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(m.key, sizeof(m.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
m.len = (uint16_t)len;
|
|
if (len > 0)
|
|
memcpy(m.bytes, str, len);
|
|
if (len < STORAGE_MAX_VALUE_BYTES)
|
|
memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);
|
|
|
|
return send_msg(&m, 0);
|
|
}
|
|
|
|
esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len)
|
|
{
|
|
if (len > STORAGE_MAX_VALUE_BYTES)
|
|
return ESP_ERR_INVALID_SIZE;
|
|
if (len > 0 && !data)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_msg_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.op = OP_SET_BLOB;
|
|
if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(m.key, sizeof(m.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
m.len = (uint16_t)len;
|
|
if (len > 0)
|
|
memcpy(m.bytes, data, len);
|
|
if (len < STORAGE_MAX_VALUE_BYTES)
|
|
memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);
|
|
|
|
return send_msg(&m, 0);
|
|
}
|
|
|
|
esp_err_t storage_erase_key_async(const char *ns, const char *key)
|
|
{
|
|
storage_msg_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.op = OP_ERASE_KEY;
|
|
if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(m.key, sizeof(m.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
return send_msg(&m, 0);
|
|
}
|
|
|
|
esp_err_t storage_flush_async(void)
|
|
{
|
|
storage_msg_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.op = OP_FLUSH;
|
|
return send_msg(&m, 0);
|
|
}
|
|
|
|
static esp_err_t sync_req(const storage_msg_t *req, storage_resp_t *out_resp, TickType_t to)
|
|
{
|
|
if (!s_inited || !s_sync_q || !s_sync_mtx)
|
|
return ESP_ERR_INVALID_STATE;
|
|
|
|
if (xSemaphoreTake(s_sync_mtx, to) != pdTRUE)
|
|
return ESP_ERR_TIMEOUT;
|
|
|
|
xQueueReset(s_sync_q);
|
|
|
|
storage_msg_t m = *req;
|
|
m.resp_q = s_sync_q;
|
|
|
|
esp_err_t err = send_msg(&m, to);
|
|
if (err != ESP_OK)
|
|
{
|
|
xSemaphoreGive(s_sync_mtx);
|
|
return err;
|
|
}
|
|
|
|
storage_resp_t r;
|
|
memset(&r, 0, sizeof(r));
|
|
|
|
if (xQueueReceive(s_sync_q, &r, to) != pdTRUE)
|
|
{
|
|
xSemaphoreGive(s_sync_mtx);
|
|
return ESP_ERR_TIMEOUT;
|
|
}
|
|
|
|
if (out_resp)
|
|
*out_resp = r;
|
|
|
|
xSemaphoreGive(s_sync_mtx);
|
|
return r.err;
|
|
}
|
|
|
|
esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to)
|
|
{
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_GET_U8;
|
|
if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(req.key, sizeof(req.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_resp_t r;
|
|
esp_err_t err = sync_req(&req, &r, to);
|
|
if (err == ESP_OK && out)
|
|
*out = (uint8_t)(r.value & 0xFF);
|
|
return err;
|
|
}
|
|
|
|
esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to)
|
|
{
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_GET_U16;
|
|
if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(req.key, sizeof(req.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_resp_t r;
|
|
esp_err_t err = sync_req(&req, &r, to);
|
|
if (err == ESP_OK && out)
|
|
*out = (uint16_t)(r.value & 0xFFFF);
|
|
return err;
|
|
}
|
|
|
|
esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to)
|
|
{
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_GET_U32;
|
|
if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(req.key, sizeof(req.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_resp_t r;
|
|
esp_err_t err = sync_req(&req, &r, to);
|
|
if (err == ESP_OK && out)
|
|
*out = r.value;
|
|
return err;
|
|
}
|
|
|
|
esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to)
|
|
{
|
|
if (!out || out_sz == 0)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
out[0] = '\0';
|
|
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_GET_STR;
|
|
if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(req.key, sizeof(req.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_resp_t r;
|
|
esp_err_t err = sync_req(&req, &r, to);
|
|
|
|
if (err != ESP_OK)
|
|
{
|
|
out[out_sz - 1] = '\0';
|
|
return err;
|
|
}
|
|
|
|
// r.len é o tamanho real (sem '\0')
|
|
if (r.len >= out_sz)
|
|
{
|
|
// espelhar semântica tipo NVS (buffer pequeno)
|
|
out[0] = '\0';
|
|
out[out_sz - 1] = '\0';
|
|
return ESP_ERR_NVS_INVALID_LENGTH;
|
|
}
|
|
|
|
if (r.len > 0)
|
|
memcpy(out, r.bytes, r.len);
|
|
out[r.len] = '\0';
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to)
|
|
{
|
|
if (!inout_len)
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_GET_BLOB;
|
|
req.blob_query_only = (out == NULL);
|
|
|
|
if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
|
|
return ESP_ERR_INVALID_ARG;
|
|
if (!safe_copy_str(req.key, sizeof(req.key), key))
|
|
return ESP_ERR_INVALID_ARG;
|
|
|
|
storage_resp_t r;
|
|
esp_err_t err = sync_req(&req, &r, to);
|
|
|
|
if (err != ESP_OK)
|
|
{
|
|
if (out == NULL && err == ESP_ERR_NOT_FOUND)
|
|
*inout_len = 0;
|
|
return err;
|
|
}
|
|
|
|
// query mode: só devolver tamanho requerido
|
|
if (out == NULL)
|
|
{
|
|
*inout_len = r.len;
|
|
return ESP_OK;
|
|
}
|
|
|
|
// read mode: valida tamanhos
|
|
if (r.len > *inout_len)
|
|
{
|
|
*inout_len = r.len;
|
|
return ESP_ERR_NVS_INVALID_LENGTH;
|
|
}
|
|
if (r.len > STORAGE_MAX_VALUE_BYTES)
|
|
{
|
|
*inout_len = r.len;
|
|
return ESP_ERR_NVS_INVALID_LENGTH;
|
|
}
|
|
|
|
if (r.len > 0)
|
|
memcpy(out, r.bytes, r.len);
|
|
*inout_len = r.len;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t storage_flush_sync(TickType_t to)
|
|
{
|
|
storage_msg_t req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.op = OP_FLUSH;
|
|
storage_resp_t r;
|
|
return sync_req(&req, &r, to);
|
|
}
|
|
|
|
// === Fim de: components/storage_service/src/storage_service.c ===
|
|
|
|
|
|
// === Início de: components/storage_service/include/storage_service.h ===
|
|
#pragma once
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
|
|
#include "esp_err.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "nvs.h" // para ESP_ERR_NVS_INVALID_LENGTH (documentação/semântica do blob)
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/**
|
|
* NVS limita namespace e key a 15 chars (+ '\0')
|
|
* (ver docs do NVS: NVS_KEY_NAME_MAX_SIZE / NVS_NS_NAME_MAX_SIZE)
|
|
*/
|
|
#define STORAGE_NS_MAX_LEN 16
|
|
#define STORAGE_KEY_MAX_LEN 16
|
|
|
|
/**
|
|
* Ajusta conforme o teu sistema.
|
|
* Nota: setters async usam xQueueSend(..., 0) -> podem falhar com ESP_ERR_TIMEOUT se a fila estiver cheia.
|
|
*/
|
|
#ifndef STORAGE_QUEUE_LEN
|
|
#define STORAGE_QUEUE_LEN 32
|
|
#endif
|
|
|
|
#ifndef STORAGE_MAX_PENDING
|
|
#define STORAGE_MAX_PENDING 48
|
|
#endif
|
|
|
|
#ifndef STORAGE_COMMIT_DEBOUNCE_MS
|
|
#define STORAGE_COMMIT_DEBOUNCE_MS 500
|
|
#endif
|
|
|
|
/**
|
|
* Tamanho máximo (bytes) para STR/BLOB guardado em pending/queue.
|
|
* IMPORTANTE:
|
|
* - Este limite aplica-se aos SETs (async) e ao cache pending.
|
|
* - Leituras (sync) a partir do NVS também ficam, na prática, limitadas por este design
|
|
* se esperares que valores maiores existam no NVS (recomenda-se alinhar o teu sistema a este máximo).
|
|
*/
|
|
#ifndef STORAGE_MAX_VALUE_BYTES
|
|
#define STORAGE_MAX_VALUE_BYTES 96
|
|
#endif
|
|
|
|
/**
|
|
* Inicializa o serviço e cria a task interna.
|
|
*
|
|
* Requisitos:
|
|
* - nvs_flash_init() deve ter sido chamado antes (normalmente no arranque da app).
|
|
*/
|
|
esp_err_t storage_service_init(void);
|
|
|
|
// -------------------- Async setters (não bloqueiam; commit é debounced) --------------------
|
|
|
|
/**
|
|
* Retorna:
|
|
* - ESP_OK
|
|
* - ESP_ERR_INVALID_ARG (ns/key inválidos ou > 15 chars)
|
|
* - ESP_ERR_TIMEOUT (fila cheia)
|
|
* - ESP_ERR_INVALID_STATE (serviço não inicializado)
|
|
*/
|
|
esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v);
|
|
esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v);
|
|
esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v);
|
|
|
|
/**
|
|
* Retorna:
|
|
* - ESP_OK
|
|
* - ESP_ERR_INVALID_ARG (ns/key/str inválidos)
|
|
* - ESP_ERR_INVALID_SIZE (str > STORAGE_MAX_VALUE_BYTES)
|
|
* - ESP_ERR_TIMEOUT (fila cheia)
|
|
* - ESP_ERR_INVALID_STATE (serviço não inicializado)
|
|
*/
|
|
esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str);
|
|
|
|
/**
|
|
* Retorna:
|
|
* - ESP_OK
|
|
* - ESP_ERR_INVALID_ARG (ns/key inválidos; data NULL com len>0)
|
|
* - ESP_ERR_INVALID_SIZE (len > STORAGE_MAX_VALUE_BYTES)
|
|
* - ESP_ERR_TIMEOUT (fila cheia)
|
|
* - ESP_ERR_INVALID_STATE (serviço não inicializado)
|
|
*/
|
|
esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len);
|
|
|
|
esp_err_t storage_erase_key_async(const char *ns, const char *key);
|
|
|
|
/** Força commit imediato (async). Mesmas notas de fila cheia/invalid state. */
|
|
esp_err_t storage_flush_async(void);
|
|
|
|
// -------------------- Sync getters (bloqueiam até ler do NVS/pending) --------------------
|
|
/**
|
|
* NOTAS IMPORTANTES:
|
|
* - Funções sync NÃO devem ser chamadas em ISR (usam mutex/queues e podem bloquear).
|
|
* - O timeout `to` aplica-se ao lock + envio/receção da resposta.
|
|
*
|
|
* Retorna tipicamente:
|
|
* - ESP_OK
|
|
* - ESP_ERR_NOT_FOUND (chave não existe)
|
|
* - ESP_ERR_TIMEOUT
|
|
* - outros erros do NVS
|
|
*/
|
|
esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to);
|
|
esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to);
|
|
esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to);
|
|
|
|
/**
|
|
* Lê string para `out` com tamanho `out_sz`.
|
|
* Em sucesso, `out` fica sempre null-terminated.
|
|
*
|
|
* Retorna:
|
|
* - ESP_OK
|
|
* - ESP_ERR_NOT_FOUND
|
|
* - ESP_ERR_INVALID_ARG (out/out_sz inválidos; ns/key inválidos)
|
|
* - ESP_ERR_TIMEOUT
|
|
* - erros do NVS
|
|
*/
|
|
esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to);
|
|
|
|
/**
|
|
* Blob sync (semântica semelhante a nvs_get_blob):
|
|
* - Se out == NULL: devolve ESP_OK e coloca em *inout_len o tamanho requerido
|
|
* - Se out != NULL e *inout_len < requerido: devolve ESP_ERR_NVS_INVALID_LENGTH e atualiza *inout_len com requerido
|
|
* - Se OK: copia e atualiza *inout_len com o tamanho real
|
|
*
|
|
* Retorna:
|
|
* - ESP_OK
|
|
* - ESP_ERR_NOT_FOUND
|
|
* - ESP_ERR_NVS_INVALID_LENGTH
|
|
* - ESP_ERR_INVALID_ARG (inout_len NULL; ns/key inválidos)
|
|
* - ESP_ERR_TIMEOUT
|
|
* - erros do NVS
|
|
*/
|
|
esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to);
|
|
|
|
/** Força commit imediato (sync). Mesmas notas de timeout/ISR. */
|
|
esp_err_t storage_flush_sync(TickType_t to);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
// === Fim de: components/storage_service/include/storage_service.h ===
|