/* * SPDX-FileCopyrightText: 2025 Kozmotronik Tech * * SPDX-License-Identifier: MIT */ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_check.h" #include "esp_log.h" #include "nvs.h" #include "relay_chn_nvs.h" #define RELAY_CHN_KEY_DIR "dir" /*!< Direction key */ #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT #if CONFIG_RELAY_CHN_COUNT > 1 #define RELAY_CHN_KEY_RLIM_FMT "rlim_%d" /*!< Run limit key format for multi-channel */ #else #define RELAY_CHN_KEY_RLIM "rlim_0" /*!< Run limit key for single-channel */ #endif #endif #if CONFIG_RELAY_CHN_ENABLE_TILTING #if CONFIG_RELAY_CHN_COUNT > 1 #define RELAY_CHN_KEY_TSENS_FMT "tsens_%d" /*!< Tilt sensitivity key format for multi-channel */ #define RELAY_CHN_KEY_TCNT_FMT "tcnt_%d" /*!< Tilt count key format for multi-channel */ #else #define RELAY_CHN_KEY_TSENS "tsens_0" /*!< Tilt sensitivity key for single-channel */ #define RELAY_CHN_KEY_TCNT "tcnt_0" /*!< Tilt count key for single-channel */ #endif #endif // --- Task and message queue config --- #define RELAY_CHN_NVS_QUEUE_LEN (8 + CONFIG_RELAY_CHN_COUNT * 8) #define RELAY_CHN_NVS_TASK_STACK 2048 #define RELAY_CHN_NVS_COMMIT_TIMEOUT_MS 200 #define RELAY_CHN_NVS_TASK_PRIO (tskIDLE_PRIORITY + 4) typedef enum { RELAY_CHN_NVS_OP_ERASE_ALL, RELAY_CHN_NVS_OP_SET_DIRECTION, #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT RELAY_CHN_NVS_OP_SET_RUN_LIMIT, #endif #if CONFIG_RELAY_CHN_ENABLE_TILTING RELAY_CHN_NVS_OP_SET_TILT_SENSITIVITY, RELAY_CHN_NVS_OP_SET_TILT_COUNT, #endif RELAY_CHN_NVS_OP_DEINIT, } relay_chn_nvs_op_t; typedef struct { relay_chn_nvs_op_t op; uint8_t ch; union { uint16_t data_u16; uint8_t data_u8; } data; } relay_chn_nvs_msg_t; static const char *TAG = "RELAY_CHN_NVS"; static nvs_handle_t s_relay_chn_nvs; static QueueHandle_t s_nvs_ops_queue = NULL; static TaskHandle_t s_nvs_ops_task = NULL; static SemaphoreHandle_t s_nvs_deinit_sem = NULL; static void relay_chn_nvs_task(void *arg); esp_err_t relay_chn_nvs_init() { // Already initialized? if (s_nvs_ops_queue != NULL) { return ESP_OK; } s_nvs_deinit_sem = xSemaphoreCreateBinary(); if (!s_nvs_deinit_sem) { ESP_LOGE(TAG, "Failed to create deinit semaphore"); return ESP_ERR_NO_MEM; } s_nvs_ops_queue = xQueueCreate(RELAY_CHN_NVS_QUEUE_LEN, sizeof(relay_chn_nvs_msg_t)); if (!s_nvs_ops_queue) { ESP_LOGE(TAG, "Failed to create NVS queue"); return ESP_ERR_NO_MEM; } BaseType_t res = xTaskCreate(relay_chn_nvs_task, "task_rlch_nvs", RELAY_CHN_NVS_TASK_STACK, NULL, RELAY_CHN_NVS_TASK_PRIO, &s_nvs_ops_task); if (res != pdPASS) { ESP_LOGE(TAG, "Failed to create NVS task"); return ESP_ERR_NO_MEM; } esp_err_t ret; #if CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION ret = nvs_open_from_partition(CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION_NAME, CONFIG_RELAY_CHN_NVS_NAMESPACE, NVS_READWRITE, &s_relay_chn_nvs); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to open NVS namespace '%s' from partition '%s' with error %s", CONFIG_RELAY_CHN_NVS_NAMESPACE, CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION_NAME, esp_err_to_name(ret)); #else ret = nvs_open(CONFIG_RELAY_CHN_NVS_NAMESPACE, NVS_READWRITE, &s_relay_chn_nvs); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to open NVS namespace '%s'", CONFIG_RELAY_CHN_NVS_NAMESPACE); #endif // CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION return ESP_OK; } static esp_err_t relay_chn_nvs_enqueue(relay_chn_nvs_msg_t *msg, const char *op_name) { if (!s_nvs_ops_queue) { return ESP_ERR_INVALID_STATE; } if (msg->op == RELAY_CHN_NVS_OP_DEINIT || msg->op == RELAY_CHN_NVS_OP_ERASE_ALL) { // Send DEINIT or ERASE_ALL to the front and wait up to 1 sec if needed if (xQueueSendToFront(s_nvs_ops_queue, msg, pdMS_TO_TICKS(1000)) != pdTRUE) { ESP_LOGW(TAG, "NVS queue is full, dropping %s for #%d", op_name, msg->ch); return ESP_FAIL; } } else { // Send async if (xQueueSend(s_nvs_ops_queue, msg, 0) != pdTRUE) { ESP_LOGW(TAG, "NVS queue is full, dropping %s for #%d", op_name, msg->ch); return ESP_FAIL; } } return ESP_OK; } esp_err_t relay_chn_nvs_set_direction(uint8_t ch, relay_chn_direction_t direction) { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_SET_DIRECTION, .ch = ch, .data.data_u8 = (uint8_t) direction, }; return relay_chn_nvs_enqueue(&msg, "SET_DIRECTION"); } static esp_err_t relay_chn_nvs_task_set_direction(uint8_t ch, uint8_t direction) { uint8_t direction_val = 0; esp_err_t ret = nvs_get_u8(s_relay_chn_nvs, RELAY_CHN_KEY_DIR, &direction_val); if (ret != ESP_OK && ret != ESP_ERR_NVS_NOT_FOUND) { ESP_RETURN_ON_ERROR(ret, TAG, "Failed to get direction from NVS with error: %s", esp_err_to_name(ret)); } direction_val &= ~(1 << ch); // Clear the bit for the channel direction_val |= (((uint8_t) direction) << ch); // Set the new direction bit ret = nvs_set_u8(s_relay_chn_nvs, RELAY_CHN_KEY_DIR, direction_val); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set direction for channel %d", ch); return ESP_OK; } esp_err_t relay_chn_nvs_get_direction(uint8_t ch, relay_chn_direction_t *direction, relay_chn_direction_t default_val) { ESP_RETURN_ON_FALSE(direction != NULL, ESP_ERR_INVALID_ARG, TAG, "Direction pointer is NULL"); uint8_t direction_val; esp_err_t ret = nvs_get_u8(s_relay_chn_nvs, RELAY_CHN_KEY_DIR, &direction_val); if (ret == ESP_ERR_NVS_NOT_FOUND) { *direction = default_val; return ESP_OK; } else if (ret != ESP_OK) { return ret; // A real error occurred, return it } // If ret is ESP_OK, direction_val has the stored value. *direction = (relay_chn_direction_t)((direction_val >> ch) & 0x01); return ESP_OK; } #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT esp_err_t relay_chn_nvs_set_run_limit(uint8_t ch, uint16_t limit_sec) { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_SET_RUN_LIMIT, .ch = ch, .data.data_u16 = limit_sec, }; return relay_chn_nvs_enqueue(&msg, "SET_RUN_LIMIT"); } static esp_err_t relay_chn_nvs_task_set_run_limit(uint8_t ch, uint16_t limit_sec) { esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_RLIM_FMT, ch); ret = nvs_set_u16(s_relay_chn_nvs, key, limit_sec); #else ret = nvs_set_u16(s_relay_chn_nvs, RELAY_CHN_KEY_RLIM, limit_sec); #endif ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set run limit for channel %d", ch); return ESP_OK; } esp_err_t relay_chn_nvs_get_run_limit(uint8_t ch, uint16_t *limit_sec, uint16_t default_val) { ESP_RETURN_ON_FALSE(limit_sec != NULL, ESP_ERR_INVALID_ARG, TAG, "Run limit value pointer is NULL"); esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_RLIM_FMT, ch); ret = nvs_get_u16(s_relay_chn_nvs, key, limit_sec); #else ret = nvs_get_u16(s_relay_chn_nvs, RELAY_CHN_KEY_RLIM, limit_sec); #endif if (ret == ESP_ERR_NVS_NOT_FOUND) { *limit_sec = default_val; return ESP_OK; } return ret; } #endif // CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT == 1 #if CONFIG_RELAY_CHN_ENABLE_TILTING esp_err_t relay_chn_nvs_set_tilt_sensitivity(uint8_t ch, uint8_t sensitivity) { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_SET_TILT_SENSITIVITY, .ch = ch, .data.data_u8 = sensitivity, }; return relay_chn_nvs_enqueue(&msg, "SET_TILT_SENSITIVITY"); } static esp_err_t relay_chn_nvs_task_set_tilt_sensitivity(uint8_t ch, uint8_t sensitivity) { esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_TSENS_FMT, ch); ret = nvs_set_u8(s_relay_chn_nvs, key, sensitivity); #else ret = nvs_set_u8(s_relay_chn_nvs, RELAY_CHN_KEY_TSENS, sensitivity); #endif ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set tilt sensitivity for channel %d", ch); return ESP_OK; } esp_err_t relay_chn_nvs_get_tilt_sensitivity(uint8_t ch, uint8_t *sensitivity, uint8_t default_val) { ESP_RETURN_ON_FALSE(sensitivity != NULL, ESP_ERR_INVALID_ARG, TAG, "Sensitivity pointer is NULL"); esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_TSENS_FMT, ch); ret = nvs_get_u8(s_relay_chn_nvs, key, sensitivity); #else ret = nvs_get_u8(s_relay_chn_nvs, RELAY_CHN_KEY_TSENS, sensitivity); #endif if (ret == ESP_ERR_NVS_NOT_FOUND) { *sensitivity = default_val; return ESP_OK; } return ret; } esp_err_t relay_chn_nvs_set_tilt_count(uint8_t ch, uint16_t tilt_count) { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_SET_TILT_COUNT, .ch = ch, .data.data_u16 = tilt_count, }; return relay_chn_nvs_enqueue(&msg, "SET_TILT_COUNT"); } static esp_err_t relay_chn_nvs_task_set_tilt_count(uint8_t ch, uint16_t tilt_count) { esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_TCNT_FMT, ch); ret = nvs_set_u16(s_relay_chn_nvs, key, tilt_count); #else ret = nvs_set_u16(s_relay_chn_nvs, RELAY_CHN_KEY_TCNT, tilt_count); #endif ESP_RETURN_ON_ERROR(ret, TAG, "Failed to save tilt_count tilt counter"); return ESP_OK; } esp_err_t relay_chn_nvs_get_tilt_count(uint8_t ch, uint16_t *tilt_count, uint16_t default_val) { ESP_RETURN_ON_FALSE(tilt_count != NULL, ESP_ERR_INVALID_ARG, TAG, "Counter pointers are NULL"); esp_err_t ret; #if CONFIG_RELAY_CHN_COUNT > 1 char key[NVS_KEY_NAME_MAX_SIZE]; snprintf(key, sizeof(key), RELAY_CHN_KEY_TCNT_FMT, ch); ret = nvs_get_u16(s_relay_chn_nvs, key, tilt_count); #else ret = nvs_get_u16(s_relay_chn_nvs, RELAY_CHN_KEY_TCNT, tilt_count); #endif if (ret == ESP_ERR_NVS_NOT_FOUND) { *tilt_count = default_val; return ESP_OK; } return ret; } #endif // CONFIG_RELAY_CHN_ENABLE_TILTING esp_err_t relay_chn_nvs_erase_all() { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_ERASE_ALL, }; return relay_chn_nvs_enqueue(&msg, "ERASE_ALL"); } static esp_err_t do_nvs_deinit() { relay_chn_nvs_msg_t msg = { .op = RELAY_CHN_NVS_OP_DEINIT, }; return relay_chn_nvs_enqueue(&msg, "DEINIT"); } static esp_err_t do_nvs_erase_all() { // Flush all pending SET operations since ERASE_ALL requested xQueueReset(s_nvs_ops_queue); // Erase all key-value pairs in the relay_chn NVS namespace esp_err_t ret = nvs_erase_all(s_relay_chn_nvs); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to erase all keys in NVS namespace '%s'", CONFIG_RELAY_CHN_NVS_NAMESPACE); return ESP_OK; } void relay_chn_nvs_deinit() { if (s_nvs_ops_task) { if (do_nvs_deinit() == ESP_OK) { if (s_nvs_deinit_sem && xSemaphoreTake(s_nvs_deinit_sem, pdMS_TO_TICKS(2000)) != pdTRUE) { ESP_LOGE(TAG, "Failed to get deinit confirmation from NVS task. Forcing deletion."); vTaskDelete(s_nvs_ops_task); // Last resort } } else { ESP_LOGE(TAG, "Failed to send deinit message to NVS task. Forcing deletion."); vTaskDelete(s_nvs_ops_task); } } if (s_nvs_ops_queue) { vQueueDelete(s_nvs_ops_queue); s_nvs_ops_queue = NULL; } if (s_nvs_deinit_sem) { vSemaphoreDelete(s_nvs_deinit_sem); s_nvs_deinit_sem = NULL; } // Close NVS handle here, after task has stopped and queue is deleted. nvs_close(s_relay_chn_nvs); s_nvs_ops_task = NULL; } static esp_err_t relay_chn_nvs_task_process_message(const relay_chn_nvs_msg_t *msg, bool *running, bool *dirty) { esp_err_t ret = ESP_OK; switch (msg->op) { case RELAY_CHN_NVS_OP_SET_DIRECTION: ret = relay_chn_nvs_task_set_direction(msg->ch, msg->data.data_u8); if (ret == ESP_OK) *dirty = true; break; #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT case RELAY_CHN_NVS_OP_SET_RUN_LIMIT: ret = relay_chn_nvs_task_set_run_limit(msg->ch, msg->data.data_u16); if (ret == ESP_OK) *dirty = true; break; #endif #if CONFIG_RELAY_CHN_ENABLE_TILTING case RELAY_CHN_NVS_OP_SET_TILT_SENSITIVITY: ret = relay_chn_nvs_task_set_tilt_sensitivity(msg->ch, msg->data.data_u8); if (ret == ESP_OK) *dirty = true; break; case RELAY_CHN_NVS_OP_SET_TILT_COUNT: ret = relay_chn_nvs_task_set_tilt_count(msg->ch, msg->data.data_u16); if (ret == ESP_OK) *dirty = true; break; #endif case RELAY_CHN_NVS_OP_ERASE_ALL: ret = do_nvs_erase_all(); if (ret == ESP_OK) *dirty = true; break; case RELAY_CHN_NVS_OP_DEINIT: *running = false; break; default: ESP_LOGE(TAG, "Unknown operation in NVS queue: %d", msg->op); ret = ESP_ERR_INVALID_ARG; break; } return ret; } /* * The ESP-IDF NVS functions are protected by an internal mutex. If this task is killed * while it's holding that mutex, the mutex is never released, which may result in * deadlocks. This is why this task must be terminated gracefully. */ static void relay_chn_nvs_task(void *arg) { relay_chn_nvs_msg_t msg; bool dirty = false; bool running = true; while (running) { // Block indefinitely waiting for the first message of a potential batch. if (xQueueReceive(s_nvs_ops_queue, &msg, portMAX_DELAY) == pdTRUE) { // A batch of operations has started. Use a do-while to process the first message // and any subsequent messages that arrive within the timeout. do { esp_err_t ret = relay_chn_nvs_task_process_message(&msg, &running, &dirty); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to process operation %d for #%d with error %s", msg.op, msg.ch, esp_err_to_name(ret)); } } while (running && xQueueReceive(s_nvs_ops_queue, &msg, pdMS_TO_TICKS(RELAY_CHN_NVS_COMMIT_TIMEOUT_MS)) == pdTRUE); // The burst of messages is over (timeout occurred). Commit if anything changed. if (dirty) { esp_err_t commit_ret = nvs_commit(s_relay_chn_nvs); if (commit_ret == ESP_OK) { dirty = false; } else { ESP_LOGE(TAG, "NVS batch commit failed"); // Don't reset dirty flag, so we can try to commit again later. } } } } // Before exiting, do one final commit if there are pending changes. if (dirty) { if (nvs_commit(s_relay_chn_nvs) != ESP_OK) { ESP_LOGE(TAG, "Final NVS commit failed on deinit"); } } xSemaphoreGive(s_nvs_deinit_sem); s_nvs_ops_task = NULL; vTaskDelete(NULL); }