4116a94c5c5536ce2bc082eba71cbb968fe11936
ESP32 ESP-IDF component for hierarchical menu
Origin
This component is a derivative work based on the original project 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
- Multi-level hierarchical menu with support for nested submenus up to a maximum depth of 8 levels.
- Abstract navigation commands independent of any input device.
- Configurable callbacks for menu enter, menu exit and current position change.
- Support for selectable items that enter a dedicated adjustment mode with repeated selection handling.
- Support for simple action items that trigger a one-shot callback on selection.
- Built-in navigation stack that allows returning to the previous menu level.
- Optional cyclic navigation mode for scrolling through menu items in a loop.
- Minimal memory footprint with a single dynamically allocated handle and no per-node heap usage.
- Built-in statistics for queue overflow errors and task stack watermark.
Using
In an existing project, run the following command to install the component:
cd ../your_project/components
git clone https://github.com/aZholtikov/zh_menu
In the application, add the component:
#include "zh_menu.h"
Example
#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_log_level_set("zh_vector", 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 = ZH_MENU_INIT_CONFIG_DEFAULT();
menu_config.task_priority = 5;
menu_config.stack_size = configMINIMAL_STACK_SIZE;
menu_config.queue_size = 5;
menu_config.root = &main_menu;
menu_config.loop = true;
menu_config.enter_cb = &zh_menu_enter_main_menu_callback; // Optional.
menu_config.exit_cb = &zh_menu_exit_main_menu_callback; // Optional.
menu_config.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, 0) : menu_status.is_selected == false ? zh_menu_set(&menu_handle, ZH_MENU_SELECT, NULL, 0)
: zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
}
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, sizeof(zh_encoder_event_on_isr_t));
}
else
{
encoder_event->encoder_status == true ? zh_menu_set(&menu_handle, ZH_MENU_UP, NULL, 0) : zh_menu_set(&menu_handle, ZH_MENU_DOWN, NULL, 0);
}
}
break;
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, 0);
}
Description
ESP32 ESP-IDF component for hierarchical menu.
99 KiB
0 Stars
1 Watchers
0 Forks
Languages
C
99.8%
CMake
0.2%