feat!: command processing moved to task

This commit is contained in:
aZholtikov committed 2026-10-19 14:03:11 +03:00
1 parent 8dc7385a90
commit 4116a94c5c
5 files changed
+866 -188

No files matched your search

+22 -16
View File
@@ -7,15 +7,15 @@ which is distributed under the Apache License, Version 2.0. The original code ha
## Features ## Features
1. Multi-level hierarchical menu with support for nested submenus up to a configurable depth limit. 1. Multi-level hierarchical menu with support for nested submenus up to a maximum depth of 8 levels.
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. 2. Abstract navigation commands independent of any input device.
3. Configurable callbacks for menu enter, menu exit and current position change. 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. 4. Support for selectable items that enter a dedicated adjustment mode with repeated selection handling.
5. Support for simple action items that trigger a one-shot callback on selection. 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`. 6. Built-in navigation stack that allows returning to the previous menu level.
7. Optional cyclic navigation mode for scrolling through menu items in a loop. 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. 8. Minimal memory footprint with a single dynamically allocated handle and no per-node heap usage.
9. Minimal memory footprint with a single dynamically allocated handle and no per-node heap usage. 9. Built-in statistics for queue overflow errors and task stack watermark.
## Using ## Using
@@ -98,6 +98,7 @@ void app_main(void)
{ {
esp_log_level_set("zh_menu", ESP_LOG_ERROR); esp_log_level_set("zh_menu", ESP_LOG_ERROR);
esp_log_level_set("zh_encoder", 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_loop_create_default();
esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID, &zh_encoder_event_handler, NULL, NULL); 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(); zh_encoder_init_config_t encoder_config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
@@ -112,11 +113,15 @@ void app_main(void)
encoder_config.encoder_step = 1; encoder_config.encoder_step = 1;
encoder_config.encoder_number = ENCODER_NUMBER; encoder_config.encoder_number = ENCODER_NUMBER;
zh_encoder_init(&encoder_config, &encoder_handle); zh_encoder_init(&encoder_config, &encoder_handle);
zh_menu_init_config_t menu_config = {.root = &main_menu, zh_menu_init_config_t menu_config = ZH_MENU_INIT_CONFIG_DEFAULT();
.loop = true, menu_config.task_priority = 5;
.enter_cb = &zh_menu_enter_main_menu_callback, // Optional. menu_config.stack_size = configMINIMAL_STACK_SIZE;
.exit_cb = &zh_menu_exit_main_menu_callback, // Optional. menu_config.queue_size = 5;
.change_cb = &zh_menu_change_menu_callback}; 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); zh_menu_init(&menu_config, &menu_handle);
} }
@@ -131,8 +136,8 @@ void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
zh_encoder_button_event_on_isr_t *button_event = event_data; zh_encoder_button_event_on_isr_t *button_event = event_data;
if (button_event->button_status == true) 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) 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); : zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
} }
break; break;
case ZH_ENCODER_EVENT: case ZH_ENCODER_EVENT:
@@ -141,13 +146,14 @@ void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
{ {
if (menu_status.is_selected == true) if (menu_status.is_selected == true)
{ {
zh_menu_set(&menu_handle, ZH_MENU_SELECT, encoder_event); zh_menu_set(&menu_handle, ZH_MENU_SELECT, encoder_event, sizeof(zh_encoder_event_on_isr_t));
} }
else else
{ {
encoder_event->encoder_status == true ? zh_menu_set(&menu_handle, ZH_MENU_UP, NULL) : zh_menu_set(&menu_handle, ZH_MENU_DOWN, NULL); 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: default:
break; break;
} }
@@ -197,6 +203,6 @@ void zh_menu_simple_item_callback(void *arg)
void zh_menu_back_command_callback(void *arg) void zh_menu_back_command_callback(void *arg)
{ {
(void)arg; (void)arg;
zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL); zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
} }
``` ```
+54 -8
View File
@@ -23,9 +23,9 @@
* - Flexible handling of selectable and non-selectable items * - Flexible handling of selectable and non-selectable items
* - Low memory footprint due to dynamic allocation of the handle * - Low memory footprint due to dynamic allocation of the handle
* *
* @note The component functions are thread-safe only to the extent * @note Internal state is protected by a spinlock. Callbacks are invoked
* provided by the caller. External synchronization is required * outside the critical section. Callers must not call functions on the
* when accessed from multiple tasks. * same handle concurrently from multiple contexts.
*/ */
#pragma once #pragma once
@@ -34,12 +34,28 @@
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define ZH_MENU_INIT_CONFIG_DEFAULT() \
{ \
.task_priority = 1, \
.stack_size = configMINIMAL_STACK_SIZE, \
.queue_size = 1}
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C"
{ {
#endif #endif
/**
* @brief External task handle for the menu processing task.
*
* Declared as extern for access by internal modules that need
* to reference the shared processing task directly.
*/
extern TaskHandle_t zh_menu;
/** /**
* @brief Opaque menu handle. * @brief Opaque menu handle.
* *
@@ -108,8 +124,17 @@ extern "C"
void (*enter_cb)(void *args); /*!< Callback for entering the menu */ void (*enter_cb)(void *args); /*!< Callback for entering the menu */
void (*exit_cb)(void *args); /*!< Callback for exiting the menu */ void (*exit_cb)(void *args); /*!< Callback for exiting the menu */
bool loop; /*!< Cyclic navigation flag over items */ bool loop; /*!< Cyclic navigation flag over items */
uint16_t stack_size; /*!< Stack size for the shared processing task, minimum configMINIMAL_STACK_SIZE */
uint8_t task_priority; /*!< Task priority for the shared processing task, minimum value 1 */
uint8_t queue_size; /*!< Queue size for the shared processing queue, minimum value 1 */
} zh_menu_init_config_t; } zh_menu_init_config_t;
typedef struct
{
uint32_t queue_overflow_error; /*!< Number of queue overflow errors */
uint32_t min_stack_size; /*!< Minimum free stack size (watermark) */
} zh_menu_stats_t;
/** /**
* @brief Initialize the menu. * @brief Initialize the menu.
* *
@@ -118,7 +143,7 @@ extern "C"
* an inactive state until the ZH_MENU_ENTER command. * an inactive state until the ZH_MENU_ENTER command.
* *
* @param[in] config Initialization configuration (must not be NULL) * @param[in] config Initialization configuration (must not be NULL)
* @param[out] handle Pointer to the handle pointer (must be NULL) * @param[out] handle Pointer to the handle pointer (must point to NULL)
* *
* @return ESP_OK on success * @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if config, config->root or handle is NULL * @return ESP_ERR_INVALID_ARG if config, config->root or handle is NULL
@@ -136,6 +161,8 @@ extern "C"
* *
* @return ESP_OK on success * @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle or *handle is NULL * @return ESP_ERR_INVALID_ARG if handle or *handle is NULL
*
* @note The processing task and queue are deleted only when the last menu handle is deinitialized.
*/ */
esp_err_t zh_menu_deinit(zh_menu_handle_t **handle); esp_err_t zh_menu_deinit(zh_menu_handle_t **handle);
@@ -147,15 +174,16 @@ extern "C"
* The exact behavior depends on the current activity and selection state. * 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,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] command Navigation command. Must be >= ZH_MENU_ENTER and < ZH_MENU_MAX
* @param[in] arg Arbitrary argument passed to the user callbacks (enter_cb, exit_cb, item_cb) (may be NULL) * @param[in] arg Arbitrary argument passed to the user callbacks (enter_cb, exit_cb, item_cb) (may be NULL when arg_size is 0)
* @param[in] arg_size Size of the argument data in bytes (must be <= ZH_MENU_ARG_MAX_SIZE)
* *
* @return ESP_OK on success * @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle, *handle is NULL or command >= ZH_MENU_MAX * @return ESP_ERR_INVALID_ARG if handle or *handle is NULL, command is out of range, or arg_size exceeds limit
* *
* @note Commands that are invalid in the current state are ignored and do not cause an error. * @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); esp_err_t zh_menu_set(zh_menu_handle_t **handle, zh_menu_navigate_t command, const void *arg, uint8_t arg_size);
/** /**
* @brief Get the current menu state. * @brief Get the current menu state.
@@ -170,6 +198,24 @@ extern "C"
*/ */
esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status); esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status);
/**
* @brief Retrieve statistics about the menu.
*
* Returns a pointer to the current statistics structure containing
* queue overflow error count and minimum free stack size.
*
* @return Pointer to the statistics structure (never NULL)
*/
const zh_menu_stats_t *zh_menu_get_stats(void);
/**
* @brief Reset all menu statistics to zero.
*
* Clears the queue overflow error counter and resets the minimum
* free stack size watermark.
*/
void zh_menu_reset_stats(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+1 -1
View File
@@ -1 +1 @@
1.0.0 2.0.0
+398
View File
@@ -0,0 +1,398 @@
# Описание компонента zh_menu
## Общие сведения
**zh_menu** — это компонент для ESP-IDF, реализующий иерархическое многоуровневое меню с поддержкой вложенных подменю, конфигурируемых обратных вызовов и абстрактных команд навигации, независимых от конкретного устройства ввода (кнопок, энкодера, команд интерфейса).
Компонент управляет стеком уровней меню, обрабатывает команды навигации в фоновой задаче FreeRTOS и рассылает состояние через пользовательские колбэки, позволяя внешнему коду рендерить меню на дисплее или в консоли.
## Основные возможности
1. **Вложенные подменю** — поддержка глубины вложенности до 8 уровней (`ZH_MENU_MAX_DEPTH`) с автоматическим стеком навигации.
2. **Абстрактные команды навигации** — `ZH_MENU_ENTER`, `ZH_MENU_SHOW`, `ZH_MENU_UP`, `ZH_MENU_DOWN`, `ZH_MENU_SELECT`, `ZH_MENU_BACK` не привязаны к конкретным устройствам ввода.
3. **Настраиваемые обратные вызовы** — `enter_cb`, `exit_cb` и `change_cb` для реакции на вход в меню, выход из него и изменение текущей позиции.
4. **Выбираемые и простые элементы** — флаг `selectable` разделяет элементы, входящие в режим ручной корректировки, и элементы с одноразовым действием.
5. **Циклическая навигация** — опциональный режим зацикливания при перемещении `UP` / `DOWN` по элементам.
6. **Минимальный расход памяти** — динамически выделяется только один хэндл; узлы меню (`zh_menu_node_t`) не копируются и используют пользовательскую память.
7. **Встроенная статистика** — счётчик переполнений очереди обработки и watermark свободного стека задачи.
## Внутреннее устройство
### Структура `zh_menu_handle_t`
Внутренняя структура хэндла меню (opaque-тип, доступен только через API компонента):
| Поле | Тип | Описание |
|------|-----|----------|
| `menu_stack` | `zh_menu_node_t *[ZH_MENU_MAX_DEPTH]` | Стек пройденных уровней меню для навигации `ZH_MENU_BACK` |
| `current_menu` | `zh_menu_node_t *` | Указатель на текущий активный уровень меню |
| `root` | `zh_menu_node_t *` | Корневой узел дерева меню, установленный при инициализации |
| `change_cb` | `void (*)(zh_menu_node_t *, uint8_t)` | Обратный вызов при изменении текущей позиции |
| `enter_cb` | `void (*)(void *)` | Обратный вызов при входе в меню (`ZH_MENU_ENTER`) |
| `exit_cb` | `void (*)(void *)` | Обратный вызов при выходе из меню (`ZH_MENU_BACK` с deepest level) |
| `loop` | `bool` | Флаг циклической навигации по элементам |
| `menu_depth` | `uint8_t` | Текущая глубина вложенности (0 — корневой уровень) |
| `current_index` | `uint8_t` | Индекс выделенного элемента на текущем уровне |
| `is_activated` | `bool` | Флаг активации меню (true после `ZH_MENU_ENTER`) |
| `is_selected` | `bool` | Флаг состояния выбора (true — режим корректировки выбираемого элемента) |
| `lock` | `portMUX_TYPE` | Спинлок для защиты полей от одновременного доступа |
### Структура узла меню `zh_menu_node_t`
Описывает один элемент меню, определённый пользователем в статическом массиве:
| Поле | Тип | Описание |
|------|-----|----------|
| `label` | `const char *` | Отображаемое имя элемента |
| `submenu` | `struct zh_menu_node_t *` | Указатель на массив вложенных элементов или `NULL` |
| `item_numbers` | `uint8_t` | Количество элементов в массиве `submenu` |
| `item_cb` | `void (*)(void *)` | Обратный вызов при выборе элемента |
| `selectable` | `bool` | Флаг: элемент может входить в режим ручной корректировки |
Пользователь обязан обеспечить время жизни массива узлов меню на протяжении всего времени работы меню — компонент не копирует узлы.
### Конфигурация инициализации `zh_menu_init_config_t`
Передаётся в `zh_menu_init()` для настройки меню:
| Поле | Тип | Описание |
|------|-----|----------|
| `root` | `zh_menu_node_t *` | Корневой узел дерева меню (must not be NULL) |
| `change_cb` | `void (*)(zh_menu_node_t *, uint8_t)` | Колбэк изменения позиции (может быть NULL) |
| `enter_cb` | `void (*)(void *)` | Колбэк входа в меню (может быть NULL) |
| `exit_cb` | `void (*)(void *)` | Колбэк выхода из меню (может быть NULL) |
| `loop` | `bool` | Флаг циклической навигации |
| `stack_size` | `uint16_t` | Размер стека задачи обработки (минимум `configMINIMAL_STACK_SIZE`) |
| `task_priority` | `uint8_t` | Приоритет задачи обработки (минимум 1) |
| `queue_size` | `uint8_t` | Размер очереди команд (минимум 1) |
### Статистика `zh_menu_stats_t`
Возвращается `zh_menu_get_stats()` для мониторинга:
| Поле | Тип | Описание |
|------|-----|----------|
| `queue_overflow_error` | `uint32_t` | Счётчик переполнений очереди команд |
| `min_stack_size` | `uint32_t` | Минимальный остаток свободного стека задачи (watermark) |
### Задача обработки и очередь
Компонент создаёт фоновую задачу FreeRTOS (`zh_menu`) и очередь сообщений (`_queue_handle`), которые разделяются между всеми инициализированными меню. Параметры (приоритет, размер стека, размер очереди) берутся из конфигурации **первого** инициализированного меню и применяются ко всем последующим. При деинициализации последнего меню задача и очередь удаляются.
Очередь передаёт команды навигации из контекста вызова публичных функций в фоновую задачу:
| Поле | Тип | Описание |
|------|-----|----------|
| `command` | `zh_menu_navigate_t` | Команда навигации |
| `handle` | `zh_menu_handle_t *` | Указатель на хэндл меню |
| `arg_size` | `uint32_t` | Размер аргумента в байтах |
| `arg` | `uint8_t[ZH_MENU_ARG_MAX_SIZE]` | Буфер аргумента, передаваемого в пользовательские колбэки |
## API (интерфейс)
### Инициализация и освобождение
| Функция | Описание |
|---------|----------|
| `zh_menu_init()` | Создаёт хэндл меню, проверяет конфигурацию, выделяет память и готовит внутреннее состояние. Меню переходит в неактивное состояние до отправки команды `ZH_MENU_ENTER`. Требует, чтобы входной `*handle` указывал на `NULL`. |
| `zh_menu_deinit()` | Освобождает память хэндла и устанавливает указатель в `NULL`. При деинициализации последнего меню удаляет общую задачу обработки и очередь. |
### Команды навигации
| Функция | Описание |
|---------|----------|
| `zh_menu_set()` | Отправляет команду навигации (`ZH_MENU_ENTER`, `ZH_MENU_SHOW`, `ZH_MENU_UP`, `ZH_MENU_DOWN`, `ZH_MENU_SELECT`, `ZH_MENU_BACK`) в фоновую задачу обработки. Невалидные команды в текущем состоянии игнорируются без ошибки. |
### Получение состояния
| Функция | Описание |
|---------|----------|
| `zh_menu_get()` | Заполняет структуру `zh_menu_status_t` флагами активности меню и состояния выбора текущего элемента. |
### Статистика
| Функция | Описание |
|---------|----------|
| `zh_menu_get_stats()` | Возвращает read-only указатель на глобальную структуру статистики `zh_menu_stats_t`. Указатель действителен до вызова `zh_menu_reset_stats()`. |
| `zh_menu_reset_stats()` | Обнуляет все счётчики статистики: переполнения очереди и watermark стека. |
### Типы
| Элемент | Описание |
| --------- | ---------- |
| `zh_menu_handle_t` | Opaque-хэндл меню, представляющий состояние меню. Все операции выполняются через указатель на этот тип. |
| `zh_menu_navigate_t` | Перечисление команд навигации: `ZH_MENU_ENTER`, `ZH_MENU_SHOW`, `ZH_MENU_UP`, `ZH_MENU_DOWN`, `ZH_MENU_SELECT`, `ZH_MENU_BACK`, `ZH_MENU_MAX`. |
| `zh_menu_node_t` | Узел дерева меню, определённый пользователем в статическом массиве. |
| `zh_menu_init_config_t` | Конфигурация инициализации: корневой узел, колбэки, флаг циклической навигации, параметры задачи. |
| `zh_menu_status_t` | Текущее состояние меню: `is_active` и `is_selected`. |
| `zh_menu_stats_t` | Статистика: `queue_overflow_error` и `min_stack_size`. |
| `ZH_MENU_INIT_CONFIG_DEFAULT()` | Макрос для инициализации конфигурации по умолчанию: приоритет 1, стек `configMINIMAL_STACK_SIZE`, размер очереди 1. |
## Потокобезопасность
1. **Спинлок (`portMUX_TYPE`)** — защищает поля внутреннего хэндла (`current_menu`, `current_index`, `is_activated`, `is_selected` и другие) от одновременного доступа из фоновой задачи обработки и публичных функций API. В `zh_menu_set()` и `zh_menu_get()` доступ к полям обернут в `taskENTER_CRITICAL()` / `taskEXIT_CRITICAL()`.
2. **Очередь FreeRTOS** — команды навигации передаются из пользовательского контекста в фоновую задачу через очередь `xQueueSend()` с таймаутом 10 мс. Переполнение очереди фиксируется счётчиком `queue_overflow_error`.
3. **Фоновая задача** — вся логика навигации и вызовы пользовательских колбэков (`change_cb`, `enter_cb`, `exit_cb`, `item_cb`) выполняются в отдельной задаче FreeRTOS, что исключает блокировки в вызывающем коде. Колбэки вызываются вне критической секции.
4. **Разделяемые ресурсы** — задача обработки и очередь общие для всех инициализированных меню; их создание и удаление синхронизируется счётчиком `_menu_counter`.
Публичные функции не защищены мьютексом между вызовами из разных задач — их следует вызывать из одной задачи или обеспечивать внешнюю синхронизацию.
## Алгоритмы
### Навигация по уровням меню
При каждой команде навигации фоновая задача выполняет следующие шаги:
1. Чтение команды из очереди и извлечение хэндла меню.
2. `taskENTER_CRITICAL()` — блокировка спинлока для чтения текущего состояния.
3. Выполнение перехода по `switch (command)`:
- `ZH_MENU_ENTER` — установка `is_activated = true`, `current_menu = root`, `current_index = 0`, очистка стека. Вызов `enter_cb`.
- `ZH_MENU_SHOW` — принудительный вызов `change_cb` для рендеринга текущего состояния (только если меню активно и элемент не в режиме выбора).
- `ZH_MENU_UP` / `ZH_MENU_DOWN` — инкремент/декремент `current_index` с учётом циклической навигации (`loop`). При циклическом режиме переход с первого на последний элемент и наоборот.
- `ZH_MENU_SELECT` — если элемент `selectable`, вход в режим выбора (`is_selected = true`); если элемент имеет `submenu`, спуск в подменю (сохранение текущего уровня в `menu_stack`); вызов `item_cb`.
- `ZH_MENU_BACK` — если в режиме выбора, выход из него; если есть предыдущий уровень в стеке, возврат (`menu_depth--`, восстановление `current_menu` из `menu_stack`); если стек пуст, деактивация меню и вызов `exit_cb`.
4. `taskEXIT_CRITICAL()` — разблокировка спинлока.
5. Вызов накопленных колбэков вне критической секции.
### Режим выбора (`is_selected`)
Когда пользователь выделяет элемент с флагом `selectable` и отправляет `ZH_MENU_SELECT`, меню переходит в режим выбора:
- В этом режиме команды `UP`, `DOWN`, `SHOW`, `ENTER` игнорируются.
- Допустимы только `ZH_MENU_SELECT` (повторный вызов `item_cb`) и `ZH_MENU_BACK` (выход из режима выбора).
- Это позволяет использовать энкодер для ручной корректировки значения элемента без вмешательства навигации по меню.
## Примеры использования
### Базовый пример: создание меню с энкодером
```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_callback(void *arg);
void zh_menu_exit_callback(void *arg);
void zh_menu_change_callback(zh_menu_node_t *node, uint8_t index);
void zh_menu_selectable_callback(void *arg);
void zh_menu_simple_callback(void *args);
// Определение дерева меню
zh_menu_node_t main_menu = {
.label = "Main menu",
.submenu = (zh_menu_node_t[]){
{
.label = "Settings",
.submenu = (zh_menu_node_t[]){
{
.label = "Brightness",
.item_cb = &zh_menu_selectable_callback,
.selectable = true,
},
{
.label = "Volume",
.item_cb = &zh_menu_selectable_callback,
.selectable = true,
},
{
.label = "Back",
.item_cb = NULL, // нет колбэка для возврата
},
},
.item_numbers = 3,
},
{
.label = "Info",
.submenu = (zh_menu_node_t[]){
{
.label = "Version",
.item_cb = &zh_menu_simple_callback,
},
{
.label = "Back",
.item_cb = NULL,
},
},
.item_numbers = 2,
},
{
.label = "Exit",
.item_cb = NULL,
},
},
.item_numbers = 3,
};
void app_main(void)
{
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(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.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_number = ENCODER_NUMBER;
zh_encoder_init(&encoder_config, &encoder_handle);
// Инициализация меню
zh_menu_init_config_t menu_config = ZH_MENU_INIT_CONFIG_DEFAULT();
menu_config.root = &main_menu;
menu_config.loop = true;
menu_config.enter_cb = &zh_menu_enter_callback;
menu_config.exit_cb = &zh_menu_exit_callback;
menu_config.change_cb = &zh_menu_change_callback;
ESP_ERROR_CHECK(zh_menu_init(&menu_config, &menu_handle));
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
// По завершении работы: zh_menu_deinit(&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 status = {0};
zh_menu_get(&menu_handle, &status);
if (event_id == ZH_BUTTON_EVENT) {
zh_encoder_button_event_on_isr_t *btn = event_data;
if (btn->button_status == true) {
if (!status.is_active) {
zh_menu_set(&menu_handle, ZH_MENU_ENTER, NULL, 0);
} else if (!status.is_selected) {
zh_menu_set(&menu_handle, ZH_MENU_SELECT, NULL, 0);
} else {
zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
}
}
} else if (event_id == ZH_ENCODER_EVENT && status.is_active) {
zh_encoder_event_on_isr_t *enc = event_data;
if (status.is_selected) {
zh_menu_set(&menu_handle, ZH_MENU_SELECT, enc, sizeof(zh_encoder_event_on_isr_t));
} else {
zh_menu_navigate_t cmd = enc->encoder_status ? ZH_MENU_UP : ZH_MENU_DOWN;
zh_menu_set(&menu_handle, cmd, NULL, 0);
}
}
}
void zh_menu_enter_callback(void *arg)
{
(void)arg;
printf("Menu activated.\n");
}
void zh_menu_exit_callback(void *arg)
{
(void)arg;
printf("Menu deactivated.\n");
}
void zh_menu_change_callback(zh_menu_node_t *node, uint8_t index)
{
printf("\n=== %s ===\n", node->label);
for (uint8_t i = 0; i < node->item_numbers; ++i) {
printf("%c %s\n", i == index ? '>' : ' ', node->submenu[i].label);
}
fflush(stdout);
}
void zh_menu_selectable_callback(void *arg)
{
if (arg != NULL) {
zh_encoder_event_on_isr_t *enc = arg;
printf("Adjustable value changed by encoder %d: %0.0f\n",
enc->encoder_number, enc->encoder_position);
}
}
void zh_menu_simple_callback(void *args)
{
(void)args;
printf("Simple item action performed.\n");
}
```
### Мониторинг статистики
```c
#include "zh_menu.h"
zh_menu_handle_t *menu_handle = NULL;
void app_main(void)
{
zh_menu_init_config_t config = ZH_MENU_INIT_CONFIG_DEFAULT();
config.root = /* корневой узел меню */ NULL;
zh_menu_init(&config, &menu_handle);
for (;;) {
const zh_menu_stats_t *stats = zh_menu_get_stats();
if (stats->queue_overflow_error > 0) {
printf("Queue overflow: %lu\n", (unsigned long)stats->queue_overflow_error);
}
if (stats->min_stack_size < 512) {
printf("Low stack: %lu\n", (unsigned long)stats->min_stack_size);
}
zh_menu_reset_stats();
vTaskDelay(pdMS_TO_TICKS(60000));
}
// По завершении работы: zh_menu_deinit(&menu_handle);
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_menu"`. Сообщения об ошибках включают имя файла, номер строки и имя кода ошибки.
```c
esp_log_level_set("zh_menu", ESP_LOG_ERROR);
```
## Ошибки
| Код | Описание |
| ----- | ---------- |
| `ESP_OK` | Операция выполнена успешно |
| `ESP_ERR_INVALID_ARG` | NULL-аргументы, неверные параметры конфигурации (приоритет, размер стека, размер очереди, корневой узел), команда >= ZH_MENU_MAX, размер аргумента превышает лимит |
| `ESP_ERR_INVALID_STATE` | Меню по входному хэндлу уже инициализировано (`*handle != NULL`) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память под хэндл меню |
| `ESP_FAIL` | Ошибка инициализации ресурсов или задачи обработки |
## Ограничения
1. **Максимальная глубина вложенности — 8 уровней** — ограничена константой `ZH_MENU_MAX_DEPTH`; при достижении лимита спуск в подменю не выполняется для предотвращения переполнения стека.
2. **Задача и очередь общие для всех меню** — параметры задачи (приоритет, размер стека, размер очереди) берутся из конфигурации первого инициализированного меню и применяются ко всем последующим; для последующих меню они игнорируются.
3. **Узлы меню не копируются** — пользователь обязан обеспечить время жизни массива `zh_menu_node_t` на протяжении всего времени работы меню.
4. **Ёмкость очереди по умолчанию равна 1** — при быстрой навигации возможны потери команд; факт потери фиксируется счётчиком `queue_overflow_error`.
5. **Колбэки вызываются вне критической секции** — пользовательские функции (`change_cb`, `enter_cb`, `exit_cb`, `item_cb`) не должны выполнять длительные блокирующие операции.
6. **Размер аргумента ограничен 128 байтами** — буфер `arg` в очереди команд имеет фиксированный размер `ZH_MENU_ARG_MAX_SIZE`.
7. **Публичные функции не защищены мьютексом** — их следует вызывать из одной задачи или обеспечивать внешнюю синхронизацию при доступе из нескольких контекстов.
## Требования для сборки
- ESP-IDF версии **5.0 или выше** — задачи FreeRTOS, очереди, спинлоки.
- FreeRTOS — фоновая задача обработки команд меню.
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_menu
```
```c
#include "zh_menu.h"
```
+391 -163
View File
@@ -22,7 +22,15 @@ static const char *TAG = "zh_menu";
return err; \ return err; \
} }
#define ZH_MENU_MAX_DEPTH 8 /*!< Maximum depth of nested submenus. Limits the navigation stack size */ #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. * @brief Internal representation of the menu handle.
@@ -48,21 +56,105 @@ struct _zh_menu_handle_t
uint8_t current_index; /*!< Index of the highlighted item on the current level */ uint8_t current_index; /*!< Index of the highlighted item on the current level */
bool is_activated; /*!< Menu activity flag */ bool is_activated; /*!< Menu activity flag */
bool is_selected; /*!< Selection state flag of the current item */ 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) esp_err_t zh_menu_init(const zh_menu_init_config_t *config, zh_menu_handle_t **handle)
{ {
ZH_LOGI("Menu initialization started."); ZH_LOGI("Menu initialization started.");
ZH_ERROR_CHECK(config != NULL && config->root != NULL && handle != NULL && config->root->submenu != NULL && config->root->item_numbers != 0, ESP_ERR_INVALID_ARG, NULL, "Menu initialization failed. Invalid argument."); 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."); 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); *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(*handle != NULL, ESP_ERR_NO_MEM, NULL, "Menu initialization failed. Failed to allocate menu handle.");
(*handle)->change_cb = config->change_cb; 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.");
(*handle)->enter_cb = config->enter_cb; ZH_ERROR_CHECK(_zh_menu_resources_init(config) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Menu initialization failed. Resources initialization failed.");
(*handle)->exit_cb = config->exit_cb; ZH_ERROR_CHECK(_zh_menu_task_init(config) == ESP_OK, ESP_FAIL, _zh_menu_rollback_task_and_resources();
(*handle)->current_menu = config->root; heap_caps_free(*handle); *handle = NULL, "Menu initialization failed. Processing task initialization failed.");
(*handle)->root = config->root; ++_menu_counter;
(*handle)->loop = config->loop; portMUX_INITIALIZE(&(*handle)->lock);
if (_stats.min_stack_size == 0)
{
_stats.min_stack_size = config->stack_size;
}
ZH_LOGI("Menu initialization completed successfully."); ZH_LOGI("Menu initialization completed successfully.");
return ESP_OK; return ESP_OK;
} }
@@ -71,172 +163,41 @@ esp_err_t zh_menu_deinit(zh_menu_handle_t **handle)
{ {
ZH_LOGI("Menu deinitialization started."); ZH_LOGI("Menu deinitialization started.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Menu deinitialization failed. Invalid argument."); 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); heap_caps_free(*handle);
*handle = NULL; *handle = NULL;
ZH_LOGI("Menu deinitialization completed successfully."); ZH_LOGI("Menu deinitialization completed successfully.");
return ESP_OK; return ESP_OK;
} }
esp_err_t zh_menu_set(zh_menu_handle_t **handle, zh_menu_navigate_t command, void *arg) 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_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(handle != NULL && *handle != NULL && command >= ZH_MENU_ENTER && command < ZH_MENU_MAX, ESP_ERR_INVALID_ARG, NULL, "Menu set position failed. Invalid argument.");
switch (command) 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)
{ {
case ZH_MENU_ENTER: memcpy(menu_queue.arg, arg, arg_size);
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));
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)->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)
{
(*handle)->change_cb((*handle)->current_menu, (*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)
{
(*handle)->change_cb((*handle)->current_menu, (*handle)->current_index);
}
}
break;
case ZH_MENU_SELECT:
if ((*handle)->is_activated == true)
{
if ((*handle)->current_menu->submenu == NULL || (*handle)->current_index >= (*handle)->current_menu->item_numbers)
{
break;
}
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)
{
break;
}
(*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)->menu_stack[(*handle)->menu_depth] = NULL;
(*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;
memset((*handle)->menu_stack, 0, sizeof((*handle)->menu_stack));
if ((*handle)->exit_cb != NULL)
{
(*handle)->exit_cb(arg);
}
}
}
break;
default:
break;
} }
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."); ZH_LOGI("Menu set position completed successfully.");
return ESP_OK; return ESP_OK;
} }
@@ -245,8 +206,275 @@ esp_err_t zh_menu_get(zh_menu_handle_t **handle, zh_menu_status_t *status)
{ {
ZH_LOGI("Menu get status started."); 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."); 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_active = (*handle)->is_activated;
status->is_selected = (*handle)->is_selected; status->is_selected = (*handle)->is_selected;
taskEXIT_CRITICAL(&(*handle)->lock);
ZH_LOGI("Menu get status completed successfully."); ZH_LOGI("Menu get status completed successfully.");
return ESP_OK; 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);
} }