From 3522c5b9505610a0c044ffa4631845931c15907e Mon Sep 17 00:00:00 2001 From: Alexey Zholtikov Date: Sun, 20 Sep 2026 22:38:05 +0300 Subject: [PATCH] feat: initial --- .gitignore | 1 + CMakeLists.txt | 2 +- README.md | 203 ++++++++++++++++++++++++++++++++++++++- idf_component.yml | 8 ++ include/main.h | 0 include/zh_menu.h | 174 ++++++++++++++++++++++++++++++++++ main.c | 0 version.txt | 1 + zh_menu.c | 236 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 622 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 idf_component.yml delete mode 100644 include/main.h create mode 100644 include/zh_menu.h delete mode 100644 main.c create mode 100644 zh_menu.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ab5c51..05def4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1 +1 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS "include") \ No newline at end of file +idf_component_register(SRCS "zh_menu.c" INCLUDE_DIRS "include") \ No newline at end of file diff --git a/README.md b/README.md index 3df3e4d..dcd24c5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,202 @@ -# esp_component_template +# ESP32 ESP-IDF component for hierarchical menu -esp_component_template \ No newline at end of file +## Origin + +This component is a derivative work based on the original project [menu_manager](https://github.com/MarcioBulla/menu_manager) by Marcio Bulla, +which is distributed under the Apache License, Version 2.0. The original code has been adapted, modified and extended for use with ESP-IDF as the `zh_menu` component. + +## Features + +1. Multi-level hierarchical menu with support for nested submenus up to a configurable depth limit. +2. Abstract navigation commands (`ZH_MENU_ENTER`, `ZH_MENU_SHOW`, `ZH_MENU_UP`, `ZH_MENU_DOWN`, `ZH_MENU_SELECT`, `ZH_MENU_BACK`) independent of any input device. +3. Configurable callbacks for menu enter, menu exit and current position change. +4. Support for selectable items that can enter a dedicated adjustment mode with repeated `ZH_MENU_SELECT` handling. +5. Support for simple action items that trigger a one-shot callback on selection. +6. Built-in navigation stack that allows returning to the previous menu level with `ZH_MENU_BACK`. +7. Optional cyclic navigation mode for scrolling through menu items in a loop. +8. Per-item selectable flag that separates stateful items from plain action items. +9. Minimal memory footprint with a single dynamically allocated handle and no per-node heap usage. + +## Using + +In an existing project, run the following command to install the component: + +```bash +cd ../your_project/components +git clone https://github.com/aZholtikov/zh_menu +``` + +In the application, add the component: + +```c +#include "zh_menu.h" +``` + +## Example + +```c +#include "zh_menu.h" +#include "zh_encoder.h" // https://github.com/aZholtikov/zh_encoder + +#define ENCODER_NUMBER 0x01 + +zh_encoder_handle_t *encoder_handle = NULL; +zh_menu_handle_t *menu_handle = NULL; + +void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + +void zh_menu_enter_main_menu_callback(void *arg); +void zh_menu_exit_main_menu_callback(void *arg); +void zh_menu_change_menu_callback(zh_menu_node_t *current_node, uint8_t current_index); +void zh_menu_selectable_item_callback(void *arg); +void zh_menu_simple_item_callback(void *args); +void zh_menu_back_command_callback(void *arg); + +zh_menu_node_t main_menu = { + .label = "Main menu", + .submenu = (zh_menu_node_t[]){ + {.label = "Submenu 1", .submenu = (zh_menu_node_t[]){ + // After select this item (ZH_MENU_SELECT) `zh_menu_selectable_item_callback` will called. All commands except `ZH_MENU_BACK` and 'ZH_MENU_SELECT' will inactive. + // For example, in this mode you can use an encoder to change the value of this menu item. You can send anytime 'ZH_MENU_SELECT' for call `zh_menu_selectable_item_callback` again (optional). + // Command `ZH_MENU_BACK` will call `zh_menu_change_menu_callback' and activates the other commands. + {.label = "Submenu 1 Sel 1", .item_cb = &zh_menu_selectable_item_callback, .selectable = true}, + // After select this item (ZH_MENU_SELECT) `zh_menu_simple_item_callback` will called. + {.label = "Submenu 1 Item 2", .item_cb = &zh_menu_simple_item_callback}, + {.label = "Submenu 1 Item 3", .item_cb = &zh_menu_simple_item_callback}, + // After select this item (ZH_MENU_SELECT) `zh_menu_back_command_callback` will called for send 'ZH_MENU_BACK' command. + {.label = "Back", .item_cb = &zh_menu_back_command_callback}, + }, + .item_numbers = 4}, + {.label = "Submenu 2", .submenu = (zh_menu_node_t[]){ + // After select this item (ZH_MENU_SELECT) all commands except `ZH_MENU_BACK` and 'ZH_MENU_SELECT' will inactive. + // Command `ZH_MENU_BACK` will call `zh_menu_change_menu_callback' and activates the other commands. + {.label = "Submenu 2 Sel 1", .selectable = true}, + {.label = "Submenu 2 Item 2", .item_cb = &zh_menu_simple_item_callback}, + {.label = "Back", .item_cb = &zh_menu_back_command_callback}, + }, + .item_numbers = 3}, + {.label = "Submenu 3", .submenu = (zh_menu_node_t[]){ + {.label = "Subsubmenu 3", .submenu = (zh_menu_node_t[]){ + {.label = "Subsubmenu 3 Sel 1", .item_cb = &zh_menu_selectable_item_callback, .selectable = true}, + {.label = "Subsubmenu 3 Item 2", .item_cb = &zh_menu_simple_item_callback}, + {.label = "Subsubmenu 3 Item 3", .item_cb = &zh_menu_simple_item_callback}, + {.label = "Back", .item_cb = &zh_menu_back_command_callback}, + }, + .item_numbers = 4}, + {.label = "Back", .item_cb = &zh_menu_back_command_callback}, + }, + .item_numbers = 2}, + // After select this item (ZH_MENU_SELECT) `zh_menu_back_command_callback` will called for send 'ZH_MENU_BACK' command. + // After 'ZH_MENU_BACK' command menu will inactive mode. Current menu position will reset and `zh_menu_exit_main_menu_callback` will called. + // For activate again send 'ZH_MENU_ENTER' command. + {.label = "Exit", .item_cb = &zh_menu_back_command_callback}, + }, + .item_numbers = 4, +}; + +void app_main(void) +{ + esp_log_level_set("zh_menu", ESP_LOG_ERROR); + esp_log_level_set("zh_encoder", ESP_LOG_ERROR); + esp_event_loop_create_default(); + esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID, &zh_encoder_event_handler, NULL, NULL); + zh_encoder_init_config_t encoder_config = ZH_ENCODER_INIT_CONFIG_DEFAULT(); + encoder_config.task_priority = 5; + encoder_config.stack_size = configMINIMAL_STACK_SIZE; + encoder_config.queue_size = 5; + encoder_config.a_gpio_number = GPIO_NUM_4; + encoder_config.b_gpio_number = GPIO_NUM_16; + encoder_config.s_gpio_number = GPIO_NUM_15; + encoder_config.encoder_min_value = -1000000; + encoder_config.encoder_max_value = 1000000; + encoder_config.encoder_step = 1; + encoder_config.encoder_number = ENCODER_NUMBER; + zh_encoder_init(&encoder_config, &encoder_handle); + zh_menu_init_config_t menu_config = {.root = &main_menu, + .loop = true, + .enter_cb = &zh_menu_enter_main_menu_callback, // Optional. + .exit_cb = &zh_menu_exit_main_menu_callback, // Optional. + .change_cb = &zh_menu_change_menu_callback}; + zh_menu_init(&menu_config, &menu_handle); +} + +void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + (void)arg; + zh_menu_status_t menu_status = {0}; + zh_menu_get(&menu_handle, &menu_status); + switch (event_id) + { + case ZH_BUTTON_EVENT: + zh_encoder_button_event_on_isr_t *button_event = event_data; + if (button_event->button_status == true) + { + menu_status.is_active == false ? zh_menu_set(&menu_handle, ZH_MENU_ENTER, NULL) : menu_status.is_selected == false ? zh_menu_set(&menu_handle, ZH_MENU_SELECT, NULL) + : zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL); + } + break; + case ZH_ENCODER_EVENT: + zh_encoder_event_on_isr_t *encoder_event = event_data; + if (menu_status.is_active == true) + { + if (menu_status.is_selected == true) + { + zh_menu_set(&menu_handle, ZH_MENU_SELECT, encoder_event); + } + else + { + encoder_event->encoder_status == true ? zh_menu_set(&menu_handle, ZH_MENU_UP, NULL) : zh_menu_set(&menu_handle, ZH_MENU_DOWN, NULL); + } + } + default: + break; + } +} + +void zh_menu_enter_main_menu_callback(void *arg) +{ + (void)arg; + printf("Main menu activated.\n"); +} + +void zh_menu_exit_main_menu_callback(void *arg) +{ + (void)arg; + printf("Main menu deactivated.\n"); +} + +void zh_menu_change_menu_callback(zh_menu_node_t *current_node, uint8_t current_index) +{ + printf("\n========== MENU ==========\n"); + printf("Menu: %s\n", current_node->label); + printf("--------------------------\n"); + for (uint8_t i = 0; i < current_node->item_numbers; ++i) + { + printf("%c %s\n", i == current_index ? '>' : ' ', current_node->submenu[i].label); + } + printf("==========================\n\n"); + fflush(stdout); +} + +void zh_menu_selectable_item_callback(void *arg) +{ + printf("Selectable item callback performed.\n"); + if (arg != NULL) + { + zh_encoder_event_on_isr_t *encoder_event = arg; + printf("Encoder number %d position %0.0f was %s.\n", encoder_event->encoder_number, encoder_event->encoder_position, (encoder_event->encoder_status == true) ? "increased" : "reduced"); + } +} + +void zh_menu_simple_item_callback(void *arg) +{ + (void)arg; + printf("Simple item callback performed.\n"); +} + +void zh_menu_back_command_callback(void *arg) +{ + (void)arg; + zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL); +} +``` diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..98829f4 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,8 @@ +description: ESP32 ESP-IDF component for hierarchical menu +url: https://github.com/aZholtikov/zh_menu +version: "1.0.0" +license: "Apache-2.0" +maintainers: + - "Alexey Zholtikov " +dependencies: + idf: ">=5.0" \ No newline at end of file diff --git a/include/main.h b/include/main.h deleted file mode 100644 index e69de29..0000000 diff --git a/include/zh_menu.h b/include/zh_menu.h new file mode 100644 index 0000000..81776d8 --- /dev/null +++ b/include/zh_menu.h @@ -0,0 +1,174 @@ +/** + * @file zh_menu.h + * + * @brief Hierarchical menu component for ESP-IDF. + * + * This header provides types and functions for building a multi-level + * menu with navigation, selection handling and user callback support. + * The component is independent of any particular input method (buttons, + * encoder, interface commands) and operates with abstract navigation + * commands. + * + * This component 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. + * + * Key features: + * - Support for nested submenus with a depth limit + * - Configurable callbacks for position change, enter and exit + * - Cyclic navigation mode over menu items + * - Flexible handling of selectable and non-selectable items + * - Low memory footprint due to dynamic allocation of the handle + * + * @note The component functions are thread-safe only to the extent + * provided by the caller. External synchronization is required + * when accessed from multiple tasks. + */ + +#pragma once + +#include "esp_err.h" +#include "esp_log.h" +#include "esp_heap_caps.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Opaque menu handle. + * + * The internal structure is hidden from the user and is defined + * in the implementation file. All operations are performed through + * a pointer to this type. + */ + typedef struct _zh_menu_handle_t zh_menu_handle_t; + + /** + * @brief Menu navigation commands. + */ + typedef enum + { + ZH_MENU_ENTER, /*!< Activate the menu and go to the root level */ + ZH_MENU_SHOW, /*!< Force refresh of the current state via callback */ + ZH_MENU_UP, /*!< Move selection to the previous item */ + ZH_MENU_DOWN, /*!< Move selection to the next item */ + ZH_MENU_SELECT, /*!< Select the current item or enter a submenu */ + ZH_MENU_BACK, /*!< Return to the previous level or exit the menu */ + ZH_MENU_MAX /*!< Upper bound of valid commands, not a command itself */ + } zh_menu_navigate_t; + + /** + * @brief Menu tree node. + * + * Describes a single menu item: its displayed label, nested submenu, + * number of items in the submenu, user callback and selectable flag. + * + * @warning The submenu array must exist during the entire lifetime + * of the menu. The component does not copy nodes. + */ + typedef struct zh_menu_node_t + { + const char *label; /*!< Displayed label of the menu item */ + struct zh_menu_node_t *submenu; /*!< Pointer to an array of nested items or NULL */ + uint8_t item_numbers; /*!< Number of items in the nested submenu array */ + void (*item_cb)(void *args); /*!< Callback invoked when the item is selected */ + bool selectable; /*!< Flag indicating that the item can be selected */ + } zh_menu_node_t; + + /** + * @brief Current menu state. + * + * Used to pass activity and selection status to external code + * through the zh_menu_get() function. + */ + typedef struct + { + bool is_active; /*!< Flag indicating that the menu is activated */ + bool is_selected; /*!< Flag indicating that the current item is in the selection state */ + } zh_menu_status_t; + + /** + * @brief Menu initialization configuration. + * + * Contains the root node, a set of user callbacks and the cyclic navigation flag. + * + * @note The change_cb, enter_cb and exit_cb callbacks may be NULL + * if the corresponding handling is not required. + */ + typedef struct + { + 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 over items */ + } zh_menu_init_config_t; + + /** + * @brief Initialize the menu. + * + * Creates the menu handle, stores the configuration and prepares + * the internal state. After a successful call, the menu is in + * an inactive state until the ZH_MENU_ENTER command. + * + * @param[in] config Initialization configuration (must not be NULL) + * @param[out] handle Pointer to the handle pointer (must be NULL) + * + * @return ESP_OK on success + * @return ESP_ERR_INVALID_ARG if config, config->root or handle is NULL + * @return ESP_ERR_INVALID_STATE if *handle is already initialized + * @return ESP_ERR_NO_MEM if memory allocation for the handle failed + */ + esp_err_t zh_menu_init(const zh_menu_init_config_t *config, zh_menu_handle_t **handle); + + /** + * @brief Deinitialize the menu. + * + * Frees the dynamic memory occupied by the handle and resets the pointer to it. + * + * @param[in,out] handle Pointer to the handle pointer (must not be NULL) + * + * @return ESP_OK on success + * @return ESP_ERR_INVALID_ARG if handle or *handle is NULL + */ + esp_err_t zh_menu_deinit(zh_menu_handle_t **handle); + + /** + * @brief Execute a menu navigation command. + * + * Processes one of the zh_menu_navigate_t commands, changing + * the internal menu state and invoking the corresponding callbacks. + * The exact behavior depends on the current activity and selection state. + * + * @param[in,out] handle Pointer to the handle pointer (must not be NULL) + * @param[in] command Navigation command. Must be less than ZH_MENU_MAX + * @param[in] arg Arbitrary argument passed to the user callbacks (enter_cb, exit_cb, item_cb) (may be NULL) + * + * @return ESP_OK on success + * @return ESP_ERR_INVALID_ARG if handle, *handle is NULL or command >= ZH_MENU_MAX + * + * @note Commands that are invalid in the current state are ignored and do not cause an error. + */ + esp_err_t zh_menu_set(zh_menu_handle_t **handle, zh_menu_navigate_t command, void *arg); + + /** + * @brief Get the current menu state. + * + * Fills the status structure with the activity and selection flags. + * + * @param[in] handle Pointer to the handle pointer (must not be NULL) + * @param[out] status Pointer to the status structure to be filled (must not be NULL) + * + * @return ESP_OK on success + * @return ESP_ERR_INVALID_ARG if handle, *handle or status is NULL + */ + esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main.c b/main.c deleted file mode 100644 index e69de29..0000000 diff --git a/version.txt b/version.txt index e69de29..afaf360 100644 --- a/version.txt +++ b/version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/zh_menu.c b/zh_menu.c new file mode 100644 index 0000000..5ac98bf --- /dev/null +++ b/zh_menu.c @@ -0,0 +1,236 @@ +/* + * 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_MENU_MAX_DEPTH 8 /*!< Maximum depth of nested submenus. Limits the navigation stack size. */ + +/** + * @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 */ +}; + +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 && config->root != 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."); + (*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; + 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."); + 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, void *arg) +{ + ZH_LOGI("Menu set position started."); + ZH_ERROR_CHECK(handle != NULL && *handle != NULL && command < ZH_MENU_MAX, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument."); + switch (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; + if ((*handle)->enter_cb != NULL) + { + (*handle)->enter_cb(arg); + } + if ((*handle)->change_cb != NULL) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + } + break; + case ZH_MENU_SHOW: + if ((*handle)->is_activated == true) + { + if ((*handle)->change_cb != NULL) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + } + break; + case ZH_MENU_UP: + 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) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + } + break; + case ZH_MENU_DOWN: + 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) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + } + break; + case ZH_MENU_SELECT: + if ((*handle)->is_activated == true) + { + if ((*handle)->is_selected == true) + { + if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL) + { + (*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg); + } + } + else if ((*handle)->current_menu->submenu[(*handle)->current_index].selectable == true) + { + (*handle)->is_selected = true; + if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL) + { + (*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg); + } + } + else if ((*handle)->current_menu->submenu[(*handle)->current_index].item_cb != NULL) + { + (*handle)->current_menu->submenu[(*handle)->current_index].item_cb(arg); + } + else if ((*handle)->current_menu->submenu[(*handle)->current_index].submenu != NULL && (*handle)->current_menu->submenu[(*handle)->current_index].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 = &(*handle)->current_menu->submenu[(*handle)->current_index]; + (*handle)->current_index = 0; + if ((*handle)->change_cb != NULL) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + } + } + break; + case ZH_MENU_BACK: + if ((*handle)->is_activated == true) + { + if ((*handle)->is_selected == true) + { + (*handle)->is_selected = false; + if ((*handle)->change_cb != NULL) + { + (*handle)->change_cb((*handle)->current_menu, (*handle)->current_index); + } + break; + } + if ((*handle)->menu_depth > 0) + { + --(*handle)->menu_depth; + (*handle)->current_menu = (*handle)->menu_stack[(*handle)->menu_depth]; + (*handle)->current_index = 0; + if ((*handle)->change_cb != NULL) + { + (*handle)->change_cb((*handle)->current_menu, (*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; + if ((*handle)->exit_cb != NULL) + { + (*handle)->exit_cb(arg); + } + } + } + break; + default: + break; + } + 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."); + status->is_active = (*handle)->is_activated; + status->is_selected = (*handle)->is_selected; + ZH_LOGI("Menu get status completed successfully."); + return ESP_OK; +} \ No newline at end of file