/* * This file is part of the zh_menu component, which is a derivative work * based on the original project "menu_manager" by Marcio Bulla, available at: * https://github.com/MarcioBulla/menu_manager * The original project is distributed under the Apache License, Version 2.0. * The original code was adapted, modified and extended for use with ESP-IDF * as the zh_menu component. */ #include "zh_menu.h" static const char *TAG = "zh_menu"; #define ZH_LOGI(msg, ...) ESP_LOGI(TAG, msg, ##__VA_ARGS__) #define ZH_LOGE(msg, err, ...) ESP_LOGE(TAG, "[%s:%d:%s] " msg, __FILE__, __LINE__, esp_err_to_name(err), ##__VA_ARGS__) #define ZH_ERROR_CHECK(cond, err, cleanup, msg, ...) \ if (!(cond)) \ { \ ZH_LOGE(msg, err, ##__VA_ARGS__); \ cleanup; \ return err; \ } #define ZH_ERROR_CHECK_CONT(cond, cleanup, msg, ...) \ if (!(cond)) \ { \ ZH_LOGE(msg, ESP_FAIL, ##__VA_ARGS__); \ cleanup; \ } #define ZH_MENU_MAX_DEPTH 8 #define ZH_MENU_ARG_MAX_SIZE 128 /** * @brief Internal representation of the menu handle. * * Stores the stack of traversed levels, the current and root nodes, * user callbacks, state flags and the current index of the highlighted * item. * * @note The menu_stack is used to return to the previous level on the ZH_MENU_BACK command. * * @warning When ZH_MENU_MAX_DEPTH is reached, further descent is not performed to avoid stack overflow. */ struct _zh_menu_handle_t { zh_menu_node_t *menu_stack[ZH_MENU_MAX_DEPTH]; /*!< Stack of traversed menu levels */ zh_menu_node_t *current_menu; /*!< Current active menu level */ zh_menu_node_t *root; /*!< Root node of the menu tree */ void (*change_cb)(zh_menu_node_t *current_path, uint8_t current_index); /*!< Callback for the current position change */ void (*enter_cb)(void *args); /*!< Callback for entering the menu */ void (*exit_cb)(void *args); /*!< Callback for exiting the menu */ bool loop; /*!< Cyclic navigation flag */ uint8_t menu_depth; /*!< Current nesting depth */ uint8_t current_index; /*!< Index of the highlighted item on the current level */ bool is_activated; /*!< Menu activity flag */ bool is_selected; /*!< Selection state flag of the current item */ portMUX_TYPE lock; /*!< Spinlock protecting fields accessed from ISR and task context */ }; /** * @brief Internal queue item for passing navigation commands. * * Stores the command, associated handle, argument size and a copy of * the user argument data. Used to transfer commands from ISR/task context * to the background processing task. */ typedef struct { zh_menu_navigate_t command; /*!< Navigation command to process */ zh_menu_handle_t *handle; /*!< Menu handle associated with the command */ uint32_t arg_size; /*!< Size of the argument data in bytes */ uint8_t arg[ZH_MENU_ARG_MAX_SIZE]; /*!< Argument data buffer passed to user callbacks */ } zh_menu_queue_t; TaskHandle_t zh_menu = NULL; static QueueHandle_t _queue_handle = NULL; static zh_menu_stats_t _stats = {0}; volatile static uint8_t _menu_counter = 0; /** * @brief Validate the initialization configuration. * * Checks task priority, stack size, queue size and root node parameters. * Copies user callbacks and initial menu state into the handle. * * @param config Pointer to the initialization configuration (must not be NULL) * @param handle Pointer to the menu handle being initialized (must not be NULL) * * @return ESP_OK on success * @return ESP_ERR_INVALID_ARG if any configuration parameter is out of range or root is invalid */ static esp_err_t _zh_menu_validate_config(const zh_menu_init_config_t *config, zh_menu_handle_t *handle); /** * @brief Create the internal command queue if it does not exist. * * The queue is created only once, on the first initialization. * Subsequent calls return success without recreating the queue. * * @param config Pointer to the initialization configuration (must not be NULL) * * @return ESP_OK on success * @return ESP_FAIL if queue creation fails */ static esp_err_t _zh_menu_resources_init(const zh_menu_init_config_t *config); /** * @brief Create the processing task if it does not exist. * * The task is created only once, on the first initialization. * Subsequent calls return success without recreating the task. * * @param config Pointer to the initialization configuration (must not be NULL) * * @return ESP_OK on success * @return ESP_FAIL if task creation fails */ static esp_err_t _zh_menu_task_init(const zh_menu_init_config_t *config); /** * @brief Delete the processing task and queue on rollback. * * Called when initialization fails after task and queue have been created. * Cleans up resources to avoid leaks. */ static void _zh_menu_rollback_task_and_resources(void); /** * @brief Background task that processes menu navigation commands from the queue. * * Receives commands and executes state transitions, invoking user callbacks * for position changes, menu entry, exit and item selection. * * @param pvParameter Task parameter (unused). */ static void _zh_menu_isr_processing_task(void *pvParameter); esp_err_t zh_menu_init(const zh_menu_init_config_t *config, zh_menu_handle_t **handle) { ZH_LOGI("Menu initialization started."); ZH_ERROR_CHECK(config != NULL && handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu initialization failed. Invalid argument."); ZH_ERROR_CHECK(*handle == NULL, ESP_ERR_INVALID_STATE, NULL, "Menu initialization failed. Menu is already initialized."); *handle = heap_caps_calloc(1, sizeof(zh_menu_handle_t), MALLOC_CAP_8BIT); ZH_ERROR_CHECK(*handle != NULL, ESP_ERR_NO_MEM, NULL, "Menu initialization failed. Failed to allocate menu handle."); ZH_ERROR_CHECK(_zh_menu_validate_config(config, *handle) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Menu initialization failed. Initial configuration check failed."); ZH_ERROR_CHECK(_zh_menu_resources_init(config) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Menu initialization failed. Resources initialization failed."); ZH_ERROR_CHECK(_zh_menu_task_init(config) == ESP_OK, ESP_FAIL, _zh_menu_rollback_task_and_resources(); heap_caps_free(*handle); *handle = NULL, "Menu initialization failed. Processing task initialization failed."); ++_menu_counter; portMUX_INITIALIZE(&(*handle)->lock); if (_stats.min_stack_size == 0) { _stats.min_stack_size = config->stack_size; } ZH_LOGI("Menu initialization completed successfully."); return ESP_OK; } esp_err_t zh_menu_deinit(zh_menu_handle_t **handle) { ZH_LOGI("Menu deinitialization started."); ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu deinitialization failed. Invalid argument."); if (--_menu_counter == 0) { if (zh_menu != NULL) { vTaskDelete(zh_menu); zh_menu = NULL; } if (_queue_handle != NULL) { vQueueDelete(_queue_handle); _queue_handle = NULL; } } heap_caps_free(*handle); *handle = NULL; ZH_LOGI("Menu deinitialization completed successfully."); return ESP_OK; } esp_err_t zh_menu_set(zh_menu_handle_t **handle, zh_menu_navigate_t command, const void *arg, uint8_t arg_size) { ZH_LOGI("Menu set position started."); ZH_ERROR_CHECK(handle != NULL && *handle != NULL && command >= ZH_MENU_ENTER && command < ZH_MENU_MAX, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument."); ZH_ERROR_CHECK(arg_size <= ZH_MENU_ARG_MAX_SIZE, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument."); ZH_ERROR_CHECK(arg_size == 0 || arg != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument."); taskENTER_CRITICAL(&(*handle)->lock); zh_menu_queue_t menu_queue = {.handle = *handle, .command = command, .arg_size = arg_size}; if (arg_size > 0) { memcpy(menu_queue.arg, arg, arg_size); } taskEXIT_CRITICAL(&(*handle)->lock); ZH_ERROR_CHECK_CONT(xQueueSend(_queue_handle, &menu_queue, pdMS_TO_TICKS(10)) == pdTRUE, ++_stats.queue_overflow_error, "Menu isr processing failed. Queue overflow."); ZH_LOGI("Menu set position completed successfully."); return ESP_OK; } esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status) { ZH_LOGI("Menu get status started."); ZH_ERROR_CHECK(handle != NULL && *handle != NULL && status != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu get status failed. Invalid argument."); taskENTER_CRITICAL(&(*handle)->lock); status->is_active = (*handle)->is_activated; status->is_selected = (*handle)->is_selected; taskEXIT_CRITICAL(&(*handle)->lock); ZH_LOGI("Menu get status completed successfully."); return ESP_OK; } const zh_menu_stats_t *zh_menu_get_stats(void) { return &_stats; } void zh_menu_reset_stats(void) { ZH_LOGI("Error statistic reset started."); _stats.queue_overflow_error = 0; _stats.min_stack_size = 0; ZH_LOGI("Error statistic reset successfully."); } static esp_err_t _zh_menu_validate_config(const zh_menu_init_config_t *config, zh_menu_handle_t *handle) { ZH_ERROR_CHECK(config->task_priority >= 1 && config->stack_size >= configMINIMAL_STACK_SIZE, ESP_ERR_INVALID_ARG, NULL, "Invalid task settings."); ZH_ERROR_CHECK(config->queue_size >= 1, ESP_ERR_INVALID_ARG, NULL, "Invalid queue size."); ZH_ERROR_CHECK(config->root != NULL && config->root->submenu != NULL && config->root->item_numbers > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid argument."); handle->change_cb = config->change_cb; handle->enter_cb = config->enter_cb; handle->exit_cb = config->exit_cb; handle->current_menu = config->root; handle->root = config->root; handle->loop = config->loop; return ESP_OK; } static esp_err_t _zh_menu_resources_init(const zh_menu_init_config_t *config) { if (_menu_counter == 0) { _queue_handle = xQueueCreate(config->queue_size, sizeof(zh_menu_queue_t)); ZH_ERROR_CHECK(_queue_handle != NULL, ESP_FAIL, NULL, "Failed to create queue."); } return ESP_OK; } static esp_err_t _zh_menu_task_init(const zh_menu_init_config_t *config) { if (_menu_counter == 0) { ZH_ERROR_CHECK(xTaskCreatePinnedToCore(&_zh_menu_isr_processing_task, "zh_menu_isr_processing", config->stack_size, NULL, config->task_priority, &zh_menu, tskNO_AFFINITY) == pdPASS, ESP_FAIL, NULL, "Failed to create isr processing task."); } return ESP_OK; } static void _zh_menu_rollback_task_and_resources(void) { if (_menu_counter == 0) { if (zh_menu != NULL) { vTaskDelete(zh_menu); zh_menu = NULL; } if (_queue_handle != NULL) { vQueueDelete(_queue_handle); _queue_handle = NULL; } } } static void _zh_menu_isr_processing_task(void *pvParameter) { (void)pvParameter; zh_menu_queue_t menu_queue = {0}; while (xQueueReceive(_queue_handle, &menu_queue, portMAX_DELAY) == pdTRUE) { zh_menu_handle_t *handle = menu_queue.handle; void (*enter_cb)(void *) = NULL; void (*exit_cb)(void *) = NULL; void (*item_cb)(void *) = NULL; void (*change_cb)(zh_menu_node_t *, uint8_t) = NULL; zh_menu_node_t *change_node = NULL; uint8_t change_index = 0; void *cb_arg = (menu_queue.arg_size > 0) ? (void *)menu_queue.arg : NULL; taskENTER_CRITICAL(&handle->lock); switch (menu_queue.command) { case ZH_MENU_ENTER: if (handle->is_activated == false) { handle->is_activated = true; handle->is_selected = false; handle->current_menu = handle->root; handle->current_index = 0; handle->menu_depth = 0; memset(handle->menu_stack, 0, sizeof(handle->menu_stack)); enter_cb = handle->enter_cb; if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } break; case ZH_MENU_SHOW: if (handle->is_activated == true && handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } break; case ZH_MENU_UP: if (handle->current_menu == NULL || handle->current_menu->item_numbers == 0) { break; } if (handle->is_activated == true && handle->is_selected == false) { if (handle->current_index > 0) { --handle->current_index; } else if (handle->loop == true) { handle->current_index = handle->current_menu->item_numbers - 1; } if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } break; case ZH_MENU_DOWN: if (handle->current_menu == NULL || handle->current_menu->item_numbers == 0) { break; } if (handle->is_activated == true && handle->is_selected == false) { if (handle->current_index < handle->current_menu->item_numbers - 1) { ++handle->current_index; } else if (handle->loop == true) { handle->current_index = 0; } if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } break; case ZH_MENU_SELECT: if (handle->is_activated != true) { break; } if (handle->current_menu == NULL || handle->current_menu->submenu == NULL || handle->current_index >= handle->current_menu->item_numbers) { break; } { zh_menu_node_t *item = &handle->current_menu->submenu[handle->current_index]; if (handle->is_selected == true) { if (item->item_cb != NULL) { item_cb = item->item_cb; } } else if (item->selectable == true) { handle->is_selected = true; if (item->item_cb != NULL) { item_cb = item->item_cb; } } else if (item->item_cb != NULL) { item_cb = item->item_cb; } else if (item->submenu != NULL && item->item_numbers > 0) { if (handle->menu_depth < ZH_MENU_MAX_DEPTH) { handle->menu_stack[handle->menu_depth] = handle->current_menu; ++handle->menu_depth; handle->current_menu = item; handle->current_index = 0; if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } } } break; case ZH_MENU_BACK: if (handle->is_activated != true) { break; } if (handle->is_selected == true) { handle->is_selected = false; if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } else if (handle->menu_depth > 0) { --handle->menu_depth; handle->current_menu = handle->menu_stack[handle->menu_depth]; handle->menu_stack[handle->menu_depth] = NULL; handle->current_index = 0; if (handle->change_cb != NULL) { change_cb = handle->change_cb; change_node = handle->current_menu; change_index = handle->current_index; } } else { handle->is_activated = false; handle->is_selected = false; handle->current_menu = handle->root; handle->current_index = 0; handle->menu_depth = 0; memset(handle->menu_stack, 0, sizeof(handle->menu_stack)); exit_cb = handle->exit_cb; } break; default: break; } taskEXIT_CRITICAL(&handle->lock); if (enter_cb != NULL) { enter_cb(cb_arg); } if (change_cb != NULL) { change_cb(change_node, change_index); } if (item_cb != NULL) { item_cb(cb_arg); } if (exit_cb != NULL) { exit_cb(cb_arg); } _stats.min_stack_size = (uint32_t)uxTaskGetStackHighWaterMark(NULL); } vTaskDelete(NULL); }