feat!: command processing moved to task
This commit is contained in:
1 parent
8dc7385a90
commit
4116a94c5c
5 files changed
+866
-188
No files matched your search
@@ -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
@@ -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
@@ -1 +1 @@
|
|||||||
1.0.0
|
2.0.0
|
||||||
+398
@@ -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"
|
||||||
|
```
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user