优化回退

This commit is contained in:
冯佳
2025-12-22 16:51:27 +08:00
parent 8bddc34c88
commit 03ee79d03b
25 changed files with 2284 additions and 2056 deletions

661
README.md
View File

@ -1,214 +1,525 @@
# 轻量级菜单框架(C语言) # 菜单组件设计文档
## 介绍 ## 1. 概述
### 多级菜单 ### 1.1 产品简介
菜单组件是一个基于状态机的轻量级菜单系统,设计用于嵌入式设备的人机交互界面。它提供了灵活的菜单节点管理、事件驱动的导航机制、权限控制和状态持久化等功能,支持多语言和模块化扩展。
同级菜单以数组的方式体现,父菜单和子菜单的关联则使用链表实现。 ### 1.2 设计目标
- **轻量级**:静态内存分配,无动态内存依赖,适合资源受限的嵌入式设备
- **模块化**:清晰的分层设计,各功能模块可独立开关
- **可扩展**:支持自定义状态转换、事件处理和功能扩展
- **易使用**简洁的API接口便于集成到各种应用中
- **高可靠**:完善的错误处理和状态管理
> 数组元素内容有: ### 1.3 主要特性
> - 基于状态机的菜单导航
> * 菜单选项描述方式可选字符串或文本ID多国语言时可以统一管理翻译文件而文本ID可以适用于数组管理的翻译文件 - 静态菜单节点池管理
> * 菜单选项进入回调函数:当前菜单选项进入时(从父菜单进入)需要执行一次的函数 - 事件驱动的交互机制
> * 菜单选项退出回调函数:当前菜单选项进入后退出时(退出至父菜单)需要执行一次的函数 - 权限管理系统
> * 菜单选项重加载回调函数:当前菜单选项每次加载时(从父菜单进入或子菜单退出)需要执行一次的函数 - 状态持久化
> * 菜单选项周期调度回调函数:当前菜单选项的周期调度函数 - 多语言支持
> * 菜单选项的扩展数据 - 内存监控
> - 模块化设计,功能可配置
> 链表内存可以选择采用动态内存分配或者数组实现
方便对不同菜单界面功能解耦 ## 2. 系统架构
> 大部分菜单采用的都是数组中包含了所有不同级别的菜单选项内容实现,无法做到很好的解耦方式; ### 2.1 分层设计
> 菜单组件采用分层架构设计,从下到上分为:
> 该模块通过动态绑定子菜单和链表的方式可以达到较好的解耦状态
### 显示效果
该框架只负责菜单选项控制操作,不负责在图像界面显示效果,需要在对应的回调函数中实现菜单显示效果。
> 设置对应的效果显示函数,即可为不同的菜单设置不同的菜单显示效果,比如图标式、列表式或右侧弹窗式等。
>
> 可以在不同显示平台体现比如LCD、OLED或终端界面等。
### 多语种
没有直接的语种设置,而是通过联合体提供选择多种选择。
> 文本ID: 适用于多语种采用数组定义的翻译内容
> 字符串: 适用于单语种或者多语种,采用映射的方式实现的翻译内容
### 可扩展
每级菜单选项都可以设置自定义数据,用来实现更多的菜单操作或者显示效果等。
> 不同级别的菜单可以设置自定义数据(比如菜单选项隐藏/图标数据等)
### 可配置
| 配置选项 | 描述 |
| --------------------------- | ------------------------------------------------------------- |
| `_COT_MENU_USE_MALLOC_` | 定义则采用 malloc/free 的方式实现多级菜单, 否则通过数组的形式 |
| `_COT_MENU_USE_SHORTCUT_` | 定义则启用快捷菜单选项进入功能 |
| `COT_MENU_MAX_DEPTH` | 多级菜单深度 |
| `COT_MENU_MAX_NUM` | 菜单支持的最大选项数目 |
### 功能多样化
* [X] 支持快速进入指定菜单界面。
> - 可以通过相对选项索引或者绝对选项索引路径实现
>
* [X] 可以实现有限界面内显示少量的菜单选项内容。
> - 有现成的函数可用,无需担心使用不同尺寸重新实现菜单选项部分可见
>
## 软件设计
## 使用说明
### 菜单初始化和使用
```c
// 定义菜单信息,函数由主菜单模块定义并提供
static cotMainMenuCfg_t sg_tMainMenu = {"主菜单", Hmi_EnterMainHmi, NULL, NULL, NULL};
int main(void)
{
cotMenu_Init(&sg_tMainMenu);
while (1)
{
...
if (timeFlag)
{
timeFlag = 0;
cotMenu_Task(); // 周期调度
}
}
}
```
┌─────────────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
├─────────────────────────────────────────────────────────────────┤
│ API 层 (menu_api) │
├─────────────────────────────────────────────────────────────────┤
│ 核心层 (Core) │ 权限管理 │ 持久化 │ 多语言 │ 参数管理 │
│ ┌─────────────┐ │ ┌─────────┐ │ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │
│ │ menu_core │ │ │ menu_per│ │ │ menu_per│ │ │ menu_lang │ │ │ menu_param│ │
│ │ menu_event │ │ │ mission │ │ │ sistence│ │ └───────┘ │ │ └───────┘ │
│ │ menu_stack │ │ └─────────┘ │ └───────┘ │ │ │
│ │ menu_hash │ │ │ │ │
│ └─────────────┘ │ │ │ │
├─────────────────────────────────────────────────────────────────┤
│ 端口层 (menu_port) │
├─────────────────────────────────────────────────────────────────┤
│ 硬件抽象层 (HAL) │
└─────────────────────────────────────────────────────────────────┘
``` ```
### 主菜单定义和绑定 ### 2.2 模块关系
定义一个主菜单选项内容、主菜单显示效果函数和主菜单进入函数等 各模块之间的依赖关系如下:
- **API层**:对外提供统一的菜单操作接口,依赖核心层和各功能模块
- **核心层**:包含菜单的核心逻辑,如节点管理、事件处理、状态机等
- **功能模块**:基于核心层扩展,提供特定功能,如权限管理、持久化等
- **端口层**:提供平台相关的接口实现,如调试打印、内存操作等
## 3. 核心组件设计
### 3.1 菜单核心 (menu_core)
#### 3.1.1 功能描述
菜单核心是整个组件的心脏,负责:
- 菜单节点的注册、注销和管理
- 状态机的运行和状态转换
- 事件的处理和分发
- 导航路径的管理
#### 3.1.2 核心流程
```mermaid
stateDiagram-v2
[*] --> INIT: 菜单初始化
INIT --> NORMAL: 菜单进入
NORMAL --> PARAM_EDIT: 进入参数编辑
PARAM_EDIT --> NORMAL: 保存参数
PARAM_EDIT --> CONFIRM: 请求确认
CONFIRM --> NORMAL: 确认操作
CONFIRM --> PARAM_EDIT: 取消操作
NORMAL --> ERROR: 发生错误
ERROR --> NORMAL: 恢复正常
NORMAL --> [*]: 菜单退出
```
### 3.2 事件管理 (menu_event)
#### 3.2.1 功能描述
事件管理模块负责:
- 事件的发布和订阅
- 事件队列的管理
- 事件优先级的处理
#### 3.2.2 事件处理流程
```mermaid
flowchart TD
A[事件发布] --> B{事件队列满?}
B -->|是| C[返回错误]
B -->|否| D[入队事件]
D --> E[唤醒事件处理]
E --> F{有事件?}
F -->|是| G[出队最高优先级事件]
G --> H[处理事件]
H --> I{状态转换?}
I -->|是| J[执行状态转换]
I -->|否| K[执行事件回调]
J --> F
K --> F
F -->|否| L[等待下一个事件]
```
### 3.3 节点管理 (menu_core + menu_hash)
#### 3.3.1 功能描述
节点管理模块负责:
- 菜单节点的注册和注销
- 节点的查找和遍历
- 节点关系的维护(父子、兄弟关系)
- 节点的哈希索引
#### 3.3.2 节点数据结构
菜单节点采用静态数组管理,每个节点包含:
- 节点ID、父节点ID
- 节点名称和回调函数
- 子节点和兄弟节点的引用
- 节点状态标志(选中、可见、启用等)
- 权限级别(可选)
- 参数ID可选
### 3.4 导航管理 (menu_stack + menu_core)
#### 3.4.1 功能描述
导航管理模块负责:
- 菜单的上下导航
- 菜单的进入和退出
- 导航栈的管理
- 导航路径的记录
#### 3.4.2 导航流程
```mermaid
flowchart TD
A[当前节点] --> B{按下上键?}
B -->|是| C[切换到前一个兄弟节点]
B -->|否| D{按下下键?}
D -->|是| E[切换到后一个兄弟节点]
D -->|否| F{按下确认键?}
F -->|是| G[进入子节点/执行操作]
F -->|否| H{按下退出键?}
H -->|是| I[返回父节点]
H -->|否| J[处理其他事件]
C --> K[更新当前节点]
E --> K
G --> K
I --> K
J --> K
K --> A
```
## 4. 数据结构设计
### 4.1 核心数据结构
#### 4.1.1 菜单节点 (MenuNode)
```c ```c
typedef struct MenuNode {
MenuNodeId id; ///< 节点ID唯一
MenuNodeId parent_id; ///< 父节点ID根节点为0
const char* name; ///< 菜单名称(或多语言索引)
MenuCallback enter_cb; ///< 进入回调
MenuCallback exit_cb; ///< 退出回调
MenuNodeId first_child_id; ///< 第一个子节点ID
MenuNodeId next_sibling_id; ///< 下一个兄弟节点ID
MenuNodeId prev_sibling_id; ///< 上一个兄弟节点ID
MenuNodeId hash_next_id; ///< 哈希表下一个节点ID
struct {
bool is_registered : 1; ///< 是否已注册
bool is_selected : 1; ///< 是否被选中
bool is_visible : 1; ///< 是否可见
bool is_enabled : 1; ///< 是否启用
unsigned int reserved : 4; ///< 保留位
} flags;
MenuPermissionLevel permission_level; ///< 权限级别
uint16_t param_id; ///< 绑定的参数ID
} MenuNode;
```
// 扩展数据为图标文件名字每个菜单选项描述为文本ID #### 4.1.2 菜单核心上下文 (MenuCoreCtx)
cotMenuList_t sg_MainMenuTable[] = ```c
{ typedef struct MenuCoreCtx {
COT_MENU_ITEM_BIND(TEXT_MUSIC, Hmi_MusicEnter, Hmi_MusicExit, Hmi_MusicLoad, Hmi_MusicTask, (MenuImage_t *)&sgc_MusicImage), MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池
MenuNodeId hash_table[MENU_CONFIG_HASH_TABLE_SIZE]; ///< 哈希表
MenuStack stack; ///< 菜单导航栈
MenuNavPath nav_path; ///< 菜单导航路径
MenuEventQueue event_queue; ///< 事件队列
MenuNodeId current_node_id; ///< 当前选中的节点ID
MenuState current_state; ///< 当前菜单状态
uint32_t last_refresh_tick; ///< 上次刷新时间
bool is_initialized; ///< 是否已初始化
MenuErrCode error_code; ///< 当前错误码
const char* error_msg; ///< 当前错误信息
uint16_t node_count; ///< 已注册节点数量
uint16_t free_node_count; ///< 空闲节点数量
// 功能模块扩展字段...
} MenuCoreCtx;
```
COT_MENU_ITEM_BIND(TEXT_VIDEO, NULL, Hmi_VideoExit, Hmi_VideoLoad, Hmi_VideoTask, (MenuImage_t *)&sgc_VideoImage), ### 4.2 状态和事件定义
COT_MENU_ITEM_BIND(TEXT_CAMERA, Hmi_CameraEnter, Hmi_CameraExit, Hmi_CameraLoad, Hmi_CameraTask, (MenuImage_t *)&sgc_CameraImage), | 状态名称 | 状态值 | 描述 |
|---------|-------|------|
| MENU_STATE_INIT | 0 | 初始化状态 |
| MENU_STATE_NORMAL | 1 | 正常导航状态 |
| MENU_STATE_PARAM_EDIT | 2 | 参数编辑状态 |
| MENU_STATE_CONFIRM | 3 | 确认状态 |
| MENU_STATE_ERROR | 4 | 错误状态 |
COT_MENU_ITEM_BIND(TEXT_SETTING, Hmi_SetEnter, Hmi_SetExit, Hmi_SetLoad, Hmi_SetTask, (MenuImage_t *)&sgc_SettingImage), | 事件名称 | 事件值 | 描述 |
|---------|-------|------|
| MENU_EVENT_NONE | 0 | 无事件 |
| MENU_EVENT_KEY_UP | 1 | 上键事件 |
| MENU_EVENT_KEY_DOWN | 2 | 下键事件 |
| MENU_EVENT_KEY_ENTER | 3 | 确认键事件 |
| MENU_EVENT_KEY_ESC | 4 | 退出键事件 |
| MENU_EVENT_TIMEOUT | 5 | 超时事件 |
| MENU_EVENT_CUSTOM_BASE | 10 | 自定义事件基值 |
## 5. 接口设计
### 5.1 API 概述
菜单组件提供了简洁的API接口主要分为以下几类
| API类别 | 主要功能 | 文件位置 |
|---------|---------|---------|
| 初始化与反初始化 | 组件的初始化和反初始化 | menu_api.h |
| 节点管理 | 节点的注册、注销和更新 | menu_api.h |
| 事件处理 | 事件的发布和处理 | menu_api.h |
| 导航操作 | 菜单的上下导航、进入退出 | menu_api.h |
| 状态查询 | 获取当前状态、节点和路径 | menu_api.h |
| 权限管理 | 角色注册、权限检查 | menu_api.h |
| 持久化 | 状态的保存和恢复 | menu_api.h |
### 5.2 核心API示例
#### 5.2.1 初始化菜单
```c
MenuErrCode ret = menu_init();
if (ret != MENU_ERR_OK) {
// 处理初始化错误
}
```
#### 5.2.2 注册菜单节点
```c
// 注册单个节点
MenuNodeId node_id = menu_register_node(
0, // 自动分配节点ID
parent_node_id, // 父节点ID
"Menu Name", // 节点名称
enter_callback, // 进入回调
exit_callback // 退出回调
);
// 批量注册节点
const struct MenuNode nodes[] = {
{1, 0, "Main Menu", main_enter, main_exit},
{2, 1, "Sub Menu 1", sub1_enter, sub1_exit},
{3, 1, "Sub Menu 2", sub2_enter, sub2_exit},
}; };
menu_register_nodes(nodes, sizeof(nodes)/sizeof(nodes[0]));
/* 主菜单显示效果 */
static void ShowMainMenu(cotMenuShow_t *ptShowInfo)
{
char *pszSelectDesc = get_text(ptShowInfo->pszItemsDesc[ptShowInfo->selectItem].textId); // 根据文本ID找到对应的字符串多语言
oledsize_t idx = (128 - 6 * strlen(pszSelectDesc)) / 2;
cotOled_DrawGraphic(40, 0, (const char *)ptShowInfo->pItemsListExtendData[ptShowInfo->selectItem], 1);
cotOled_SetText(0, 50, " ", 0, FONT_12X12);
cotOled_SetText(idx, 50, pszSelectDesc, 0, FONT_12X12);
}
void Hmi_EnterMainHmi(const cotMenuItemInfo_t *pItemInfo)
{
cotMenu_Bind(sg_MainMenuTable, COT_GET_MENU_NUM(sg_MainMenuTable), ShowMainMenu);
}
``` ```
### 子菜单定义和绑定 #### 5.2.3 事件处理
```c
// 发布事件
menu_post_event(MENU_EVENT_KEY_UP, 0);
如果菜单选项有子菜单,则该菜单选项调用 `cotMenu_Enter`,进入回调函数**不能为NULL**,且该回调函数需调用 `cotMenu_Bind`进行绑定 // 主循环
while (1) {
uint32_t tick = get_system_tick();
menu_main_loop(tick);
// 其他任务...
}
```
## 6. 功能特性
### 6.1 权限管理
#### 6.1.1 功能描述
权限管理模块允许:
- 注册不同的角色和权限级别
- 为菜单节点设置访问权限
- 检查当前角色是否有权访问特定节点
- 动态切换角色
#### 6.1.2 权限检查流程
```mermaid
flowchart TD
A[访问节点请求] --> B{权限功能启用?}
B -->|否| C[允许访问]
B -->|是| D[获取当前角色]
D --> E[获取节点权限级别]
E --> F{角色权限 >= 节点权限?}
F -->|是| C
F -->|否| G[拒绝访问]
```
### 6.2 状态持久化
#### 6.2.1 功能描述
状态持久化模块允许:
- 保存当前菜单状态到非易失性存储
- 恢复上次保存的菜单状态
- 支持自动保存和手动保存
- 可配置的保存间隔
#### 6.2.2 持久化数据结构
```c ```c
typedef struct {
MenuNodeId current_node_id; ///< 当前节点ID
MenuState current_state; ///< 当前菜单状态
MenuNavPath nav_path; ///< 当前导航路径
MenuStack stack; ///< 当前菜单栈
uint32_t timestamp; ///< 持久化时间戳
uint8_t checksum; ///< 数据校验和
} MenuPersistenceData;
```
/* 设置的子菜单内容,每个菜单选项描述为字符串 */ ### 6.3 多语言支持
cotMenuList_t sg_SetMenuTable[] =
{
COT_MENU_ITEM_BIND("语言", NULL, NULL, NULL, OnLanguageFunction, NULL),
COT_MENU_ITEM_BIND("蓝牙", NULL, NULL, NULL, OnBluetoothFunction, NULL),
COT_MENU_ITEM_BIND("电池", NULL, NULL, NULL, OnBatteryFunction, NULL),
COT_MENU_ITEM_BIND("储存", NULL, NULL, NULL, OnStorageFunction, NULL),
COT_MENU_ITEM_BIND("更多", Hmi_MoreSetEnter, Hmi_MoreSetExit, Hmi_MoreSetLoad, Hmi_MoreSetTask, NULL),
};
/* 设置菜单显示效果 */ #### 6.3.1 功能描述
static void ShowSetMenu(cotMenuShow_t *ptShowInfo) 多语言支持模块允许:
{ - 注册不同语言的字符串
uint8_t showNum = 3; - 切换当前语言
menusize_t tmpselect; - 根据当前语言获取对应的字符串
- 支持动态加载语言包
cotMenu_LimitShowListNum(ptShowInfo, &showNum); ### 6.4 内存监控
// 这里直接使用字符串 #### 6.4.1 功能描述
printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", tParentMenuShowInfo.uMenuDesc.pTextString); 内存监控模块允许:
- 获取已使用和空闲的节点数量
- 获取最大节点数量
- 监控内存使用情况
for (int i = 0; i < showNum; i++) ## 7. 配置与定制
{
tmpselect = i + ptShowInfo->showBaseItem;
if (tmpselect == ptShowInfo->selectItem) ### 7.1 配置选项
{
printf("\e[0;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect].pTextString); 菜单组件提供了丰富的配置选项,通过修改 `menu_config.h` 文件可以定制:
}
else | 配置项 | 描述 | 默认值 |
{ |-------|------|-------|
printf("\e[7;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect].pTextString); | MENU_CONFIG_MAX_NODES | 最大菜单节点数 | 32 |
} | MENU_CONFIG_STACK_DEPTH | 菜单栈深度 | 8 |
| MENU_CONFIG_EVENT_QUEUE_LEN | 事件队列长度 | 16 |
| MENU_CONFIG_ENABLE_PERMISSION | 启用权限管理 | 1 |
| MENU_CONFIG_ENABLE_PERSISTENCE | 启用状态持久化 | 1 |
| MENU_CONFIG_ENABLE_LANG | 启用多语言支持 | 1 |
| MENU_CONFIG_ENABLE_DEBUG | 启用调试打印 | 1 |
### 7.2 功能模块开关
通过配置宏可以启用或禁用特定功能模块:
```c
// 启用权限管理
#define MENU_CONFIG_ENABLE_PERMISSION 1U
// 禁用多语言支持
#define MENU_CONFIG_ENABLE_LANG 0U
```
## 8. 示例用法
### 8.1 基础菜单示例
```c
#include "menu_api.h"
// 回调函数
MenuErrCode main_enter(MenuNodeId node_id, struct MenuCoreCtx* core_ctx) {
printf("Enter Main Menu\n");
return MENU_ERR_OK;
}
MenuErrCode main_exit(MenuNodeId node_id, struct MenuCoreCtx* core_ctx) {
printf("Exit Main Menu\n");
return MENU_ERR_OK;
}
// 初始化菜单
void menu_example_init(void) {
// 初始化菜单组件
menu_init();
// 注册菜单节点
MenuNodeId main_node = menu_register_node(0, 0, "Main Menu", main_enter, main_exit);
MenuNodeId sub1_node = menu_register_node(0, main_node, "Sub Menu 1", NULL, NULL);
MenuNodeId sub2_node = menu_register_node(0, main_node, "Sub Menu 2", NULL, NULL);
// 进入菜单
menu_enter();
}
// 主循环
void menu_example_loop(uint32_t tick) {
menu_main_loop(tick);
}
```
### 8.2 带权限管理的示例
```c
#include "menu_api.h"
void permission_example(void) {
// 初始化菜单
menu_init();
// 注册角色
const MenuRole roles[] = {
{0, "Guest", 1}, // 访客角色权限级别1
{1, "User", 5}, // 用户角色权限级别5
{2, "Admin", 10}, // 管理员角色权限级别10
};
for (int i = 0; i < sizeof(roles)/sizeof(roles[0]); i++) {
menu_permission_register_role(&roles[i]);
} }
}
void Hmi_SetEnter(const cotMenuItemInfo_t *pItemInfo) // 设置当前角色
{ menu_permission_set_current_role(0); // 初始为访客角色
// 进入设置选项后绑定子菜单,同时为当前绑定的菜单设置显示效果函数
cotMenu_Bind(sg_SetMenuTable, COT_GET_MENU_NUM(sg_SetMenuTable), ShowSetMenu);
}
// 注册菜单节点,设置不同权限级别
MenuNodeId main_node = menu_register_node(0, 0, "Main", NULL, NULL);
MenuNodeId public_node = menu_register_node(0, main_node, "Public", NULL, NULL);
MenuNodeId user_node = menu_register_node(0, main_node, "User", NULL, NULL);
MenuNodeId admin_node = menu_register_node(0, main_node, "Admin", NULL, NULL);
// 设置节点权限级别
menu_permission_update_node_level(public_node, 1); // 所有人可访问
menu_permission_update_node_level(user_node, 5); // 用户及以上可访问
menu_permission_update_node_level(admin_node, 10); // 仅管理员可访问
// 检查权限
bool can_access = menu_permission_check_node_access(admin_node); // 返回false当前是访客角色
// 切换角色
menu_permission_set_current_role(2); // 切换到管理员角色
can_access = menu_permission_check_node_access(admin_node); // 返回true
}
``` ```
### 菜单控制 ## 9. 目录结构
通过调用相关函数实现菜单选项选择、进入、退出等
```c
// 需要先进入主菜单
cotMenu_MainEnter();
// 选择上一个,支持循环选择(即第一个可跳转到最后一个)
cotMenu_SelectPrevious(true);
// 选择下一个,不支持循环选择(即最后一个不可跳转到第一个)
cotMenu_SelectNext(false);
// 进入,会执行菜单选项的 pfnEnterCallFun 回调函数
cotMenu_Enter();
// 退出,会执行父菜单该选项的 pfnExitCallFun 回调函数,并在退出后父菜单选项列表复位从头选择
cotMenu_Exit(true);
```
menu/
├── api/ # API接口层
│ ├── menu_api.h # 对外API头文件
│ └── menu_api.c # API实现
├── src/ # 源代码层
│ ├── core/ # 核心模块
│ │ ├── menu_core.h/c # 核心逻辑
│ │ ├── menu_event.h/c # 事件处理
│ │ ├── menu_stack.h/c # 栈管理
│ │ ├── menu_hash.h/c # 哈希表
│ │ ├── menu_types.h # 类型定义
│ │ └── menu_config.h # 配置文件
│ ├── param/ # 参数管理
│ │ ├── menu_param.h/c
│ └── lang/ # 多语言支持
│ ├── menu_lang.h/c
├── port/ # 端口层
│ ├── menu_port.h/c
├── examples/ # 示例代码
│ ├── language/ # 多语言示例
│ └── hmi/ # HMI示例
├── demo/ # 演示程序
│ ├── demo.c
│ └── CMakeLists.txt
├── test/ # 测试代码
│ ├── menu_test.c
│ └── CMakeLists.txt
├── CMakeLists.txt # CMake构建文件
└── README.md # 设计文档
``` ```
定义了多个菜单选项表后,用来区分上下级,在某个菜单选项进入时绑定 ## 10. 构建与集成
1. 使用前初始化函数 cotMenu_Init, 设置主菜单内容 ### 10.1 构建方式
2. 周期调用函数 cotMenu_Task, 用来处理菜单显示和执行相关回调函数 菜单组件使用CMake进行构建支持多种编译器和平台
3. 使用其他函数对菜单界面控制
```bash
mkdir build
cd build
cmake ..
make
```
### 10.2 集成到应用
1. 将菜单组件的源文件添加到应用项目中
2. 包含必要的头文件(主要是 `menu_api.h`
3. 根据需要配置 `menu_config.h`
4. 调用API初始化和使用菜单组件
## 11. 版本历史
| 版本号 | 发布日期 | 主要变更 |
|-------|---------|---------|
| 1.0.0 | 2025-12-19 | 初始版本 |
| 1.0.1 | 2025-12-20 | 修复了节点查找的bug优化了事件处理流程 |
## 12. 总结
菜单组件是一个功能丰富、易于扩展的轻量级菜单系统适合各种嵌入式设备的人机交互需求。它的模块化设计允许根据项目需求定制功能静态内存分配使其适合资源受限的环境完善的API设计使其易于集成到各种应用中。
通过合理使用菜单组件,可以快速构建出高效、可靠的嵌入式菜单界面,提高设备的用户体验和可维护性。

View File

@ -1,83 +0,0 @@
# This is the CMakeCache file.
# For build in directory: e:/Jfen_work/local_git_code/menu/examples/build
# It was generated by CMake: C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/bin/cmake.exe
# You can edit this file to change values found and used by cmake.
# If you do not want to change any of the values, simply exit the editor.
# If you do want to change a value, simply edit, save, and exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//Value Computed by CMake.
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=E:/Jfen_work/local_git_code/menu/examples/build/CMakeFiles/pkgRedirects
//Program used to build from makefiles.
CMAKE_MAKE_PROGRAM:STRING=nmake
//Value Computed by CMake
CMAKE_PROJECT_DESCRIPTION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_NAME:STATIC=Project
//Value Computed by CMake
Project_BINARY_DIR:STATIC=E:/Jfen_work/local_git_code/menu/examples/build
//Value Computed by CMake
Project_IS_TOP_LEVEL:STATIC=ON
//Value Computed by CMake
Project_SOURCE_DIR:STATIC=E:/Jfen_work/local_git_code/menu/examples
########################
# INTERNAL cache entries
########################
//This is the directory where this CMakeCache.txt was created
CMAKE_CACHEFILE_DIR:INTERNAL=e:/Jfen_work/local_git_code/menu/examples/build
//Major version of cmake used to create the current loaded cache
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3
//Minor version of cmake used to create the current loaded cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=31
//Patch version of cmake used to create the current loaded cache
CMAKE_CACHE_PATCH_VERSION:INTERNAL=2
//Path to CMake executable.
CMAKE_COMMAND:INTERNAL=C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/bin/cmake.exe
//Path to cpack program executable.
CMAKE_CPACK_COMMAND:INTERNAL=C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/bin/cpack.exe
//Path to ctest program executable.
CMAKE_CTEST_COMMAND:INTERNAL=C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/bin/ctest.exe
//Path to cache edit program executable.
CMAKE_EDIT_COMMAND:INTERNAL=C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/bin/cmake-gui.exe
//Name of external makefile project generator.
CMAKE_EXTRA_GENERATOR:INTERNAL=
//Name of generator.
CMAKE_GENERATOR:INTERNAL=NMake Makefiles
//Generator instance identifier.
CMAKE_GENERATOR_INSTANCE:INTERNAL=
//Name of generator platform.
CMAKE_GENERATOR_PLATFORM:INTERNAL=
//Name of generator toolset.
CMAKE_GENERATOR_TOOLSET:INTERNAL=
//Source directory with the top level CMakeLists.txt file for this
// project
CMAKE_HOME_DIRECTORY:INTERNAL=E:/Jfen_work/local_git_code/menu/examples
//ADVANCED property for variable: CMAKE_MAKE_PROGRAM
CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
//number of local generators
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
//Platform information initialized
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
//Path to CMake installation.
CMAKE_ROOT:INTERNAL=C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/share/cmake-3.31

View File

@ -1,15 +0,0 @@
set(CMAKE_HOST_SYSTEM "Windows-10.0.26200")
set(CMAKE_HOST_SYSTEM_NAME "Windows")
set(CMAKE_HOST_SYSTEM_VERSION "10.0.26200")
set(CMAKE_HOST_SYSTEM_PROCESSOR "AMD64")
set(CMAKE_SYSTEM "Windows-10.0.26200")
set(CMAKE_SYSTEM_NAME "Windows")
set(CMAKE_SYSTEM_VERSION "10.0.26200")
set(CMAKE_SYSTEM_PROCESSOR "AMD64")
set(CMAKE_CROSSCOMPILING "FALSE")
set(CMAKE_SYSTEM_LOADED 1)

View File

@ -1,11 +0,0 @@
---
events:
-
kind: "message-v1"
backtrace:
- "C:/Users/ZHIZHANKEJI/AppData/Local/Programs/Python/Python311/Lib/site-packages/cmake/data/share/cmake-3.31/Modules/CMakeDetermineSystem.cmake:205 (message)"
- "CMakeLists.txt"
message: |
The system is: Windows - 10.0.26200 - AMD64
...

View File

@ -1 +0,0 @@
# This file is generated by cmake for dependency checking of the CMakeCache.txt file

View File

@ -1,13 +0,0 @@
#ifndef HMI_CAMERA_H
#define HMI_CAMERA_H
#include "hmi_common.h"
extern const MenuImage_t sgc_CameraImage;
void Hmi_CameraEnter(const MenuItemInfo_t *pItemInfo);
void Hmi_CameraExit(const MenuItemInfo_t *pItemInfo);
void Hmi_CameraLoad(const MenuItemInfo_t *pItemInfo);
void Hmi_CameraTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,20 +0,0 @@
#ifndef HMI_COMMON_H
#define HMI_COMMON_H
#include "menu.h"
/* 自定义图标数据 */
typedef struct
{
const char *pImageFrame;
const char *pImage;
} MenuImage_t;
// 清除屏幕
#define CLEAR() printf("\033[2J")
// 定位光标
#define MOVETO(x,y) printf("\033[%d;%dH", (x), (y))
void Hmi_OnCommonFunction(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,12 +0,0 @@
#ifndef HMI_MORE_SET_H
#define HMI_MORE_SET_H
#include "hmi_common.h"
void Hmi_MoreSetEnter(const MenuItemInfo_t *pItemInfo);
void Hmi_MoreSetExit(const MenuItemInfo_t *pItemInfo);
void Hmi_MoreSetLoad(const MenuItemInfo_t *pItemInfo);
void Hmi_MoreSetTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,14 +0,0 @@
#ifndef HMI_MUSIC_H
#define HMI_MUSIC_H
#include "hmi_common.h"
extern const MenuImage_t sgc_MusicImage;
void Hmi_MusicEnter(const MenuItemInfo_t *pItemInfo);
void Hmi_MusicExit(const MenuItemInfo_t *pItemInfo);
void Hmi_MusicLoad(const MenuItemInfo_t *pItemInfo);
void Hmi_MusicTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,13 +0,0 @@
#ifndef HMI_SET_H
#define HMI_SET_H
#include "hmi_common.h"
extern const MenuImage_t sgc_SettingImage;
void Hmi_SetEnter(const MenuItemInfo_t *pItemInfo);
void Hmi_SetExit(const MenuItemInfo_t *pItemInfo);
void Hmi_SetLoad(const MenuItemInfo_t *pItemInfo);
void Hmi_SetTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,12 +0,0 @@
#ifndef HMI_VIDEO_H
#define HMI_VIDEO_H
#include "hmi_common.h"
extern const MenuImage_t sgc_VideoImage;
void Hmi_VideoLoad(const MenuItemInfo_t *pItemInfo);
void Hmi_VideoExit(const MenuItemInfo_t *pItemInfo);
void Hmi_VideoTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,9 +0,0 @@
#ifndef MAIN_HMI_H
#define MAIN_HMI_H
#include "hmi_common.h"
void Hmi_LoadMainHmi(const MenuItemInfo_t *pItemInfo);
void Hmi_MainTask(const MenuItemInfo_t *pItemInfo);
#endif

View File

@ -1,103 +0,0 @@
#include "hmi_camera.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
const MenuImage_t sgc_CameraImage = {
"**********",
"&"
};
MenuList_t sg_CameraMenuTable[] =
{
MENU_ITEM_BIND("TEXT_PHOTO", NULL, NULL, NULL, Hmi_OnCommonFunction, NULL),
MENU_ITEM_BIND(TEXT_PHOTOGRAPHY, NULL, NULL, NULL, Hmi_OnCommonFunction, NULL),
};
static void ShowCameraMenu(MenuShow_t *ptShowInfo)
{
uint8_t showNum = 3;
menusize_t tmpselect;
Menu_LimitShowListNum(ptShowInfo, &showNum);
printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", get_text(ptShowInfo->uMenuDesc.textId));
for (int i = 0; i < showNum; i++)
{
tmpselect = i + ptShowInfo->showBaseItem;
if (tmpselect == ptShowInfo->selectItem)
{
printf("\e[0;30;47m %d. %-34s\e[0m\n", tmpselect + 1, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
else
{
printf("\e[7;30;47m %d. %-34s\e[0m\n", tmpselect + 1, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
}
}
void Hmi_CameraEnter(const MenuItemInfo_t *pItemInfo)
{
Menu_Bind(sg_CameraMenuTable, MENU_GET_NUM(sg_CameraMenuTable), ShowCameraMenu);
}
void Hmi_CameraLoad(const MenuItemInfo_t *pItemInfo)
{
CLEAR();
MOVETO(0, 0);
printf("---%s-----\n", get_text(TEXT_LOADING));
}
void Hmi_CameraExit(const MenuItemInfo_t *pItemInfo)
{
printf("--------------------------\n");
printf(" %s%s...\n", get_text(TEXT_VIDEO), get_text(TEXT_STOPPING_PLAYBACK));
printf("--------------------------\n");
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
}
void Hmi_CameraTask(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("%s(0-%s; 1-%s%s; 2-%s; 3-%s; 4-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_RETURN), get_text(TEXT_RETURN), get_text(TEXT_MAIN_MENU),
get_text(TEXT_ENTER), get_text(TEXT_NEXT), get_text(TEXT_PREVIOUS));
scanf(" %d", &cmd);
switch (cmd)
{
case 0:
Menu_Exit(true);
break;
case 1:
Menu_Reset();
break;
case 2:
Menu_Enter();
break;
case 3:
Menu_SelectNext(true);
break;
case 4:
Menu_SelectPrevious(true);
break;
default:
break;
}
}

View File

@ -1,28 +0,0 @@
#include "hmi_common.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
void Hmi_OnCommonFunction(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(pItemInfo->uMenuDesc.textId), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd); // 空格作用是忽略上次的回车
if (cmd == 0)
{
Menu_Exit(false);
}
}

View File

@ -1,110 +0,0 @@
#include "hmi_more_set.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
/* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
MenuList_t sg_MoreSetMenuTable[] =
{
MENU_ITEM_BIND(TEXT_UPGRADE, NULL, NULL, NULL, Hmi_OnCommonFunction, NULL),
MENU_ITEM_BIND(TEXT_ABOUT, NULL, NULL, NULL, Hmi_OnCommonFunction, NULL),
};
static void ShowMoreSetMenu(MenuShow_t *ptShowInfo)
{
uint8_t showNum = 3;
uint8_t showsubNum = 3;
menusize_t tmpselect;
MenuShow_t tParentMenuShowInfo;
if (Menu_QueryParentMenu(&tParentMenuShowInfo, 1) != 0)
{
return;
}
Menu_LimitShowListNum(&tParentMenuShowInfo, &showNum);
Menu_LimitShowListNum(ptShowInfo, &showsubNum);
printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", get_text(tParentMenuShowInfo.uMenuDesc.textId));
for (int i = 0; i < showNum; i++)
{
tmpselect = i + tParentMenuShowInfo.showBaseItem;
if (tmpselect == tParentMenuShowInfo.selectItem)
{
printf("\e[0;30;47m %d. %-16s\e[0m |", tmpselect + 1, get_text(tParentMenuShowInfo.uItemsListDesc[tmpselect].textId));
}
else
{
printf("\e[7;30;47m %d. %-16s\e[0m |", tmpselect + 1, get_text(tParentMenuShowInfo.uItemsListDesc[tmpselect].textId));
}
if (i < showsubNum)
{
tmpselect = i + ptShowInfo->showBaseItem;
if (tmpselect == ptShowInfo->selectItem)
{
printf(" \e[0;30;47m %-14s\e[0m", get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
else
{
printf(" \e[7;30;47m %-14s\e[0m", get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
}
printf("\n");
}
}
void Hmi_MoreSetEnter(const MenuItemInfo_t *pItemInfo)
{
Menu_Bind(sg_MoreSetMenuTable, MENU_GET_NUM(sg_MoreSetMenuTable), ShowMoreSetMenu);
}
void Hmi_MoreSetLoad(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_MoreSetExit(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_MoreSetTask(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("%s(0-%s; 1-%s%s; 2-%s; 3-%s; 4-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_RETURN), get_text(TEXT_RETURN), get_text(TEXT_MAIN_MENU),
get_text(TEXT_ENTER), get_text(TEXT_NEXT), get_text(TEXT_PREVIOUS));
scanf(" %d", &cmd);
switch (cmd)
{
case 0:
Menu_Exit(false);
break;
case 1:
Menu_Reset();
break;
case 2:
Menu_Enter();
break;
case 3:
Menu_SelectNext(true);
break;
case 4:
Menu_SelectPrevious(true);
break;
default:
break;
}
}

View File

@ -1,58 +0,0 @@
#include "hmi_music.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
static bool sg_isInit = false;
const MenuImage_t sgc_MusicImage = {
"mmmmmmmmmm",
"@"
};
void Hmi_MusicEnter(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_MusicExit(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_MusicLoad(const MenuItemInfo_t *pItemInfo)
{
sg_isInit = true;
}
void Hmi_MusicTask(const MenuItemInfo_t *pItemInfo)
{
int cmd = 0;
if (sg_isInit)
{
sg_isInit = false;
printf("--------------------------\n");
printf(" %s%s\n", get_text(pItemInfo->uMenuDesc.textId), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
}
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd);
if (cmd == 0)
{
Menu_Exit(false);
}
}

View File

@ -1,176 +0,0 @@
#include "hmi_set.h"
#include "hmi_more_set.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
const MenuImage_t sgc_SettingImage = {
"$$$$$$$$$$",
"%"
};
static void OnLanguageFunction(const MenuItemInfo_t *pItemInfo);
static void OnBluetoothFunction(const MenuItemInfo_t *pItemInfo);
static void OnBatteryFunction(const MenuItemInfo_t *pItemInfo);
static void OnStorageFunction(const MenuItemInfo_t *pItemInfo);
MenuList_t sg_SetMenuTable[] =
{
MENU_ITEM_BIND(TEXT_LANGUAGE, NULL, NULL, NULL, OnLanguageFunction, NULL),
MENU_ITEM_BIND(TEXT_BLUETOOTH, NULL, NULL, NULL, OnBluetoothFunction, NULL),
MENU_ITEM_BIND(TEXT_BATTERY, NULL, NULL, NULL, OnBatteryFunction, NULL),
MENU_ITEM_BIND(TEXT_STORE, NULL, NULL, NULL, OnStorageFunction, NULL),
MENU_ITEM_BIND(TEXT_MORE, Hmi_MoreSetEnter, Hmi_MoreSetExit, Hmi_MoreSetLoad, Hmi_MoreSetTask, NULL),
};
static void ShowSetMenu(MenuShow_t *ptShowInfo)
{
uint8_t showNum = 3;
menusize_t tmpselect;
Menu_LimitShowListNum(ptShowInfo, &showNum);
printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", get_text(ptShowInfo->uMenuDesc.textId));
for (int i = 0; i < showNum; i++)
{
tmpselect = i + ptShowInfo->showBaseItem;
if (tmpselect == ptShowInfo->selectItem)
{
printf("\e[0;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
else
{
printf("\e[7;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId));
}
}
}
void Hmi_SetEnter(const MenuItemInfo_t *pItemInfo)
{
Menu_Bind(sg_SetMenuTable, MENU_GET_NUM(sg_SetMenuTable), ShowSetMenu);
}
void Hmi_SetLoad(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_SetExit(const MenuItemInfo_t *pItemInfo)
{
}
void Hmi_SetTask(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("%s(0-%s; 1-%s; 2-%s; 3-%s; 4-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_RETURN), get_text(TEXT_RETURN_MAIN_MENU),
get_text(TEXT_ENTER), get_text(TEXT_NEXT), get_text(TEXT_PREVIOUS));
scanf(" %d", &cmd);
switch (cmd)
{
case 0:
Menu_Exit(false);
break;
case 1:
Menu_Reset();
break;
case 2:
Menu_Enter();
break;
case 3:
Menu_SelectNext(true);
break;
case 4:
Menu_SelectPrevious(true);
break;
default:
break;
}
}
static void OnLanguageFunction(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(TEXT_LANGUAGE), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-中文; 1-English): ",
get_text(TEXT_SELECT_OPTION));
scanf(" %d", &cmd);
if (cmd == 0)
{
set_language(SYSTEM_LANGUAGE_CHINESE);
}
else
{
set_language(SYSTEM_LANGUAGE_ENGLISH);
}
Menu_Exit(false);
}
static void OnBluetoothFunction(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(TEXT_BLUETOOTH), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd);
if (cmd == 0)
{
Menu_Exit(false);
}
}
static void OnBatteryFunction(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(TEXT_BATTERY), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd);
if (cmd == 0)
{
Menu_Exit(false);
}
}
static void OnStorageFunction(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(TEXT_STORE), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd);
if (cmd == 0)
{
Menu_Exit(false);
}
}

View File

@ -1,67 +0,0 @@
#include "hmi_video.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
const MenuImage_t sgc_VideoImage = {
"vvvvvvvvvv",
"#"
};
void Hmi_VideoLoad(const MenuItemInfo_t *pItemInfo)
{
printf("--------------------------\n");
printf(" %s %s...\n", get_text(pItemInfo->uMenuDesc.textId), get_text(TEXT_LOADING));
printf("--------------------------\n");
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
CLEAR();
MOVETO(0, 0);
}
void Hmi_VideoExit(const MenuItemInfo_t *pItemInfo)
{
CLEAR();
MOVETO(0, 0);
printf("--------------------------\n");
printf(" %s %s...\n", get_text(pItemInfo->uMenuDesc.textId), get_text(TEXT_EXIT));
printf("--------------------------\n");
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
}
void Hmi_VideoTask(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("--------------------------\n");
printf(" %s%s\n", get_text(pItemInfo->uMenuDesc.textId), get_text(TEXT_FUNCTION_TEST));
printf("--------------------------\n");
printf("%s(0-%s): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT));
scanf(" %d", &cmd);
if (cmd == 0)
{
Menu_Exit(false);
}
}

View File

@ -1,140 +0,0 @@
#include "mainhmi.h"
#include "hmi_camera.h"
#include "hmi_music.h"
#include "hmi_set.h"
#include "hmi_video.h"
#include "hmi_common.h"
#include "menu.h"
#include "language.h"
#include <stdio.h>
#include <string.h>
MenuList_t sg_MainMenuTable[] =
{
MENU_ITEM_BIND(TEXT_MUSIC, Hmi_MusicEnter, Hmi_MusicExit, Hmi_MusicLoad, Hmi_MusicTask, (MenuImage_t *)&sgc_MusicImage),
MENU_ITEM_BIND(TEXT_VIDEO, NULL, Hmi_VideoExit, Hmi_VideoLoad, Hmi_VideoTask, (MenuImage_t *)&sgc_VideoImage),
MENU_ITEM_BIND(TEXT_CAMERA, Hmi_CameraEnter, Hmi_CameraExit, Hmi_CameraLoad, Hmi_CameraTask, (MenuImage_t *)&sgc_CameraImage),
MENU_ITEM_BIND(TEXT_SETTING, Hmi_SetEnter, Hmi_SetExit, Hmi_SetLoad, Hmi_SetTask, (MenuImage_t *)&sgc_SettingImage),
};
static void ShowMainMenu(MenuShow_t *ptShowInfo)
{
uint8_t showNum = 3;
MenuImage_t *pMenuImage;
menusize_t tmpselect;
Menu_LimitShowListNum(ptShowInfo, &showNum);
printf("\e[0;30;47m ------------- %s ------------- \e[0m\n", get_text(ptShowInfo->uMenuDesc.textId));
for (int i = 0; i < showNum; i++)
{
tmpselect = i + ptShowInfo->showBaseItem;
pMenuImage = (MenuImage_t *)ptShowInfo->pItemsListExtendData[tmpselect];
if (tmpselect == ptShowInfo->selectItem)
{
printf("\e[0;30;47m %-10s \e[0m", pMenuImage->pImageFrame);
}
else
{
printf("\e[7;30;47m %-10s \e[0m", pMenuImage->pImageFrame);
}
}
printf("\n");
for (int i = 0; i < showNum; i++)
{
tmpselect = i + ptShowInfo->showBaseItem;
pMenuImage = (MenuImage_t *)ptShowInfo->pItemsListExtendData[tmpselect];
if (tmpselect == ptShowInfo->selectItem)
{
printf("\e[0;30;47m %-s%-8s%-s \e[0m", pMenuImage->pImage, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId), pMenuImage->pImage);
}
else
{
printf("\e[7;30;47m %-s%-8s%-s \e[0m", pMenuImage->pImage, get_text(ptShowInfo->uItemsListDesc[tmpselect].textId), pMenuImage->pImage);
}
}
printf("\n");
for (int i = 0; i < showNum; i++)
{
tmpselect = i + ptShowInfo->showBaseItem;
pMenuImage = (MenuImage_t *)ptShowInfo->pItemsListExtendData[tmpselect];
if (tmpselect == ptShowInfo->selectItem)
{
printf("\e[0;30;47m %-10s \e[0m", pMenuImage->pImageFrame);
}
else
{
printf("\e[7;30;47m %-10s \e[0m", pMenuImage->pImageFrame);
}
}
printf("\n");
}
void Hmi_LoadMainHmi(const MenuItemInfo_t *pItemInfo)
{
Menu_Bind(sg_MainMenuTable, MENU_GET_NUM(sg_MainMenuTable), ShowMainMenu);
}
void Hmi_MainTask(const MenuItemInfo_t *pItemInfo)
{
int cmd;
printf("%s(0-%s; 2-%s; 3-%s; 4-%s; 5-%s(%s); 6-%s(%s); 7-%s(%s)): ",
get_text(TEXT_SELECT_OPTION), get_text(TEXT_EXIT_MAIN_MENU),
get_text(TEXT_ENTER), get_text(TEXT_NEXT), get_text(TEXT_PREVIOUS),
get_text(TEXT_MUSIC), get_text(TEXT_SHORTCUT),
get_text(TEXT_LANGUAGE), get_text(TEXT_SHORTCUT),
get_text(TEXT_MORE_SETTING), get_text(TEXT_SHORTCUT));
scanf(" %d", &cmd);
switch (cmd)
{
case 0:
Menu_MainExit();
break;
case 1:
Menu_Reset();
break;
case 2:
Menu_Enter();
break;
case 3:
Menu_SelectNext(true);
break;
case 4:
Menu_SelectPrevious(true);
break;
case 5:
Menu_ShortcutEnter(true, 1, 0);
break;
case 6:
Menu_ShortcutEnter(true, 2, 3, 0);
break;
case 7:
Menu_ShortcutEnter(true, 2, 3, 4);
break;
default:
break;
}
}

View File

@ -1,70 +0,0 @@
#include "language.h"
static SystemLanguage_e sg_eSystemLanguage = SYSTEM_LANGUAGE_CHINESE;
const char *(sg_kSystemLanguage[TEXT_ALL])[SYSTEM_LANGUAGE_ALL] =
{
[TEXT_MENU] = {"菜单", "menu"},
[TEXT_MAIN_MENU] = {"主菜单", "main menu"},
[TEXT_SELECT_OPTION] = {"选择操作", "select option"},
[TEXT_ENTER] = {"进入", "enter"},
[TEXT_EXIT] = {"退出", "exit"},
[TEXT_RETURN] = {"返回", "return"},
[TEXT_MUSIC] = {"音乐", "music"},
[TEXT_VIDEO] = {"视频", "video"},
[TEXT_CAMERA] = {"摄像机", "camera"},
[TEXT_SETTING] = {"设置", "setting"},
[TEXT_SHORTCUT] = {"快捷", "shortcut"},
[TEXT_MORE] = {"更多", "more"},
[TEXT_LANGUAGE] = {"语言", "language"},
[TEXT_NEXT] = {"下一个", "next"},
[TEXT_PREVIOUS] = {"上一个", "previous"},
[TEXT_FUNCTION_TEST] = {"功能测试", "functional testing"},
[TEXT_TEST] = {"测试", "test"},
[TEXT_BLUETOOTH] = {"蓝牙", "bluetooth"},
[TEXT_BATTERY] = {"电池", "battery"},
[TEXT_STORE] = {"储存", "store"},
[TEXT_UPGRADE] = {"升级", "upgrade"},
[TEXT_ABOUT] = {"关于", "about"},
[TEXT_PHOTO] = {"拍照", "Photo"},
[TEXT_PHOTOGRAPHY] = {"摄影", "photography"},
[TEXT_LOADING] = {"加载中", "loading"},
[TEXT_STOPPING_PLAYBACK] = {"正在停止播放", "stopping playback"},
[TEXT_EXIT_MAIN_MENU] = {"退出主菜单", "exit main menu"},
[TEXT_RETURN_MAIN_MENU] = {"返回主菜单", "return main menu"},
[TEXT_MORE_SETTING] = {"更多设置", "more setting"},
};
void set_language(SystemLanguage_e lang)
{
if (lang >= 0 && lang < SYSTEM_LANGUAGE_ALL)
{
sg_eSystemLanguage = lang;
}
}
const char *get_text(TextId_e id)
{
static const char *pszNullString = "N/A";
if (id >= 0 && id < TEXT_ALL)
{
return sg_kSystemLanguage[id][sg_eSystemLanguage];
}
return pszNullString; // 未找到对应的文本
}
const char *get_text_by_language(SystemLanguage_e lang, TextId_e id)
{
static const char *pszNullString = "N/A";
if (id >= 0 && id < TEXT_ALL)
{
return sg_kSystemLanguage[id][lang];
}
return pszNullString; // 未找到对应的文本
}

View File

@ -1,52 +0,0 @@
#ifndef LANGUAGE_H
#define LANGUAGE_H
typedef enum
{
SYSTEM_LANGUAGE_CHINESE = 0,
SYSTEM_LANGUAGE_ENGLISH,
SYSTEM_LANGUAGE_ALL,
} SystemLanguage_e;
typedef enum
{
TEXT_MENU = 0, // 菜单
TEXT_MAIN_MENU, // 主菜单
TEXT_SELECT_OPTION, // 选择操作
TEXT_ENTER, // 进入
TEXT_EXIT, // 退出
TEXT_RETURN, // 返回
TEXT_MUSIC, // 音乐
TEXT_VIDEO, // 视频
TEXT_CAMERA, // 摄像机
TEXT_SETTING, // 设置
TEXT_SHORTCUT, // 快捷
TEXT_MORE, // 更多
TEXT_LANGUAGE, // 语言
TEXT_NEXT, // 下一个
TEXT_PREVIOUS, // 上一个
TEXT_FUNCTION_TEST, // 功能测试
TEXT_TEST, // 测试
TEXT_BLUETOOTH, // 蓝牙
TEXT_BATTERY, // 电池
TEXT_STORE, // 储存
TEXT_UPGRADE, // 升级
TEXT_ABOUT, // 关于
TEXT_PHOTO, // 拍照
TEXT_PHOTOGRAPHY, // 摄影
TEXT_LOADING, // 加载中
TEXT_STOPPING_PLAYBACK, // 正在停止播放
TEXT_EXIT_MAIN_MENU, // 退出主菜单
TEXT_RETURN_MAIN_MENU, // 返回主菜单
TEXT_MORE_SETTING, // 更多设置
TEXT_ALL,
} TextId_e;
void set_language(SystemLanguage_e lang);
const char *get_text(TextId_e id);
const char *get_text_by_language(SystemLanguage_e lang, TextId_e id);
#endif

View File

@ -1,23 +0,0 @@
# CMakeLists.txt for test directory
# 收集测试所需的源文件
set(TEST_SOURCES
menu_test.c
)
# 创建测试可执行程序
add_executable(menu_test ${TEST_SOURCES})
# 链接menu库
target_link_libraries(menu_test PRIVATE menu)
# 设置包含目录
target_include_directories(menu_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_SOURCE_DIR}/../src/core
)
# 安装规则
install(TARGETS menu_test
RUNTIME DESTINATION bin
)

View File

@ -1,304 +0,0 @@
/**
**********************************************************************************************************************
* @file menu_test.c
* @brief 菜单组件测试用例
* @author menu_component
* @date 2025-12-19
**********************************************************************************************************************
*/
#include "../api/menu_api.h"
#include "../port/menu_port.h"
#include <stdio.h>
#include <stdlib.h>
/* 测试菜单节点ID定义 */
#define TEST_MENU_ID_ROOT 1U
#define TEST_MENU_ID_ITEM1 2U
#define TEST_MENU_ID_ITEM2 3U
#define TEST_MENU_ID_ITEM3 4U
#define TEST_MENU_ID_SUB1 5U
#define TEST_MENU_ID_SUB2 6U
/* 测试全局变量 */
static uint32_t sg_test_tick = 0;
static bool sg_test_callback_called = false;
static MenuNodeId sg_test_last_node_id = 0;
/* 测试回调函数 */
static MenuErrCode test_enter_callback(MenuNodeId node_id, struct MenuCoreCtx* core_ctx)
{
(void)core_ctx;
sg_test_callback_called = true;
sg_test_last_node_id = node_id;
printf("Callback called for node %d\n", node_id);
return MENU_ERR_OK;
}
static MenuErrCode test_exit_callback(MenuNodeId node_id, struct MenuCoreCtx* core_ctx)
{
(void)core_ctx;
sg_test_callback_called = true;
sg_test_last_node_id = node_id;
printf("Exit callback called for node %d\n", node_id);
return MENU_ERR_OK;
}
/* 测试硬件驱动 */
static uint32_t test_get_tick(void)
{
return sg_test_tick++;
}
static void test_printf(const char* fmt, va_list args)
{
vprintf(fmt, args);
}
static MenuPortDriver sg_test_driver = {
.printf = test_printf,
.get_tick = test_get_tick,
.delay_ms = NULL,
.display = NULL,
.key_scan = NULL,
.irq_ctrl = NULL,
.error_handler = NULL,
.modbus_send = NULL,
.modbus_receive = NULL,
};
/* 测试函数 */
/**
* @brief 测试菜单初始化和反初始化
*/
static bool test_menu_init_deinit(void)
{
printf("\n=== Test: Menu Init/Deinit ===\n");
// 初始化硬件端口
MenuErrCode err = menu_port_init(&sg_test_driver);
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to initialize port\n");
return false;
}
// 初始化菜单
err = menu_init();
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to initialize menu\n");
return false;
}
// 反初始化菜单
err = menu_deinit();
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to deinitialize menu\n");
return false;
}
// 反初始化硬件端口
err = menu_port_deinit();
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to deinitialize port\n");
return false;
}
printf("PASS: Menu init/deinit test passed\n");
return true;
}
/**
* @brief 测试菜单节点注册
*/
static bool test_menu_node_register(void)
{
printf("\n=== Test: Menu Node Register ===\n");
// 初始化
menu_port_init(&sg_test_driver);
menu_init();
// 注册根节点
MenuNodeId root_id = menu_register_node(TEST_MENU_ID_ROOT, 0, "Root Menu", test_enter_callback, test_exit_callback);
if (root_id != TEST_MENU_ID_ROOT) {
printf("FAIL: Failed to register root node\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: Root node registered\n");
// 注册子节点
MenuNodeId item1_id = menu_register_node(TEST_MENU_ID_ITEM1, TEST_MENU_ID_ROOT, "Item 1", test_enter_callback, NULL);
if (item1_id != TEST_MENU_ID_ITEM1) {
printf("FAIL: Failed to register item 1\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: Item 1 registered\n");
MenuNodeId item2_id = menu_register_node(TEST_MENU_ID_ITEM2, TEST_MENU_ID_ROOT, "Item 2", test_enter_callback, NULL);
if (item2_id != TEST_MENU_ID_ITEM2) {
printf("FAIL: Failed to register item 2\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: Item 2 registered\n");
// 注册孙子节点
MenuNodeId sub1_id = menu_register_node(TEST_MENU_ID_SUB1, TEST_MENU_ID_ITEM1, "Sub Item 1", test_enter_callback, NULL);
if (sub1_id != TEST_MENU_ID_SUB1) {
printf("FAIL: Failed to register sub item 1\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: Sub item 1 registered\n");
// 清理
menu_deinit();
menu_port_deinit();
printf("PASS: Menu node register test passed\n");
return true;
}
/**
* @brief 测试菜单事件处理
*/
static bool test_menu_event_processing(void)
{
printf("\n=== Test: Menu Event Processing ===\n");
// 初始化
menu_port_init(&sg_test_driver);
menu_init();
// 注册测试节点
menu_register_node(TEST_MENU_ID_ROOT, 0, "Root Menu", test_enter_callback, test_exit_callback);
menu_register_node(TEST_MENU_ID_ITEM1, TEST_MENU_ID_ROOT, "Item 1", test_enter_callback, NULL);
menu_register_node(TEST_MENU_ID_ITEM2, TEST_MENU_ID_ROOT, "Item 2", test_enter_callback, NULL);
// 重置测试状态
sg_test_callback_called = false;
sg_test_last_node_id = 0;
// 发送上键事件
MenuErrCode err = menu_post_event(MENU_EVENT_KEY_UP, 1);
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to post UP event\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: UP event posted\n");
// 发送下键事件
err = menu_post_event(MENU_EVENT_KEY_DOWN, 1);
if (err != MENU_ERR_OK) {
printf("FAIL: Failed to post DOWN event\n");
menu_deinit();
menu_port_deinit();
return false;
}
printf("PASS: DOWN event posted\n");
// 处理菜单主循环
menu_main_loop(test_get_tick());
printf("PASS: Menu main loop processed\n");
// 清理
menu_deinit();
menu_port_deinit();
printf("PASS: Menu event processing test passed\n");
return true;
}
/**
* @brief 测试菜单状态查询
*/
static bool test_menu_state_query(void)
{
printf("\n=== Test: Menu State Query ===\n");
// 初始化
menu_port_init(&sg_test_driver);
menu_init();
// 注册测试节点
menu_register_node(TEST_MENU_ID_ROOT, 0, "Root Menu", NULL, NULL);
// 查询当前状态
MenuState state = menu_get_state();
printf("Current state: %d\n", state);
// 查询当前节点
MenuNodeId curr_node = menu_get_current_node();
printf("Current node: %d\n", curr_node);
// 查询导航深度
uint8_t depth = menu_get_nav_depth();
printf("Navigation depth: %d\n", depth);
// 清理
menu_deinit();
menu_port_deinit();
printf("PASS: Menu state query test passed\n");
return true;
}
/**
* @brief 主测试函数
*/
int main(int argc, char* argv[])
{
(void)argc;
(void)argv;
printf("Menu Component Test Suite\n");
printf("========================\n");
int test_count = 0;
int pass_count = 0;
// 运行所有测试
test_count++;
if (test_menu_init_deinit()) {
pass_count++;
}
test_count++;
if (test_menu_node_register()) {
pass_count++;
}
test_count++;
if (test_menu_event_processing()) {
pass_count++;
}
test_count++;
if (test_menu_state_query()) {
pass_count++;
}
// 打印测试结果
printf("\n========================\n");
printf("Test Results: %d/%d tests passed\n", pass_count, test_count);
printf("========================\n");
if (pass_count == test_count) {
printf("All tests passed!\n");
return EXIT_SUCCESS;
} else {
printf("Some tests failed!\n");
return EXIT_FAILURE;
}
}

1794
tool/menu_config.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,543 +0,0 @@
# 工业级嵌入式菜单组件开发文档
## 1. 项目概述
本项目是一个基于C语言开发的工业级嵌入式菜单组件设计用于资源受限的嵌入式系统具有高可移植性、模块化、可裁剪、低资源占用和事件驱动解耦等核心特点。
## 2. 架构设计
### 2.1 分层架构
项目采用分层架构设计,将硬件与业务逻辑彻底解耦,便于移植和扩展:
| 层级 | 主要职责 | 包含模块 | 文件位置 | 用户可定制性 |
|------|----------|----------|----------|--------------|
| **核心层** | 提供菜单组件的核心功能,包括菜单节点管理、事件处理、状态机等 | 菜单核心menu_core.c、事件队列menu_event.c、哈希表索引menu_hash.c、栈管理menu_stack.c | src/core/ | 不可定制 |
| **功能扩展层** | 基于核心层扩展的功能模块,用户可根据需求裁剪 | 参数管理menu_param.c、多语言支持menu_lang.c | src/param/、src/lang/ | 部分可定制 |
| **硬件端口层** | 提供硬件适配接口,对接具体硬件平台 | 硬件驱动抽象menu_port.c | port/ | 完全可定制 |
| **API层** | 对外提供统一的API接口屏蔽内部实现细节 | 统一API接口menu_api.c | api/ | 不可定制 |
### 2.2 模块间依赖关系
```
+------------------+
| 用户应用层 |
+------------------+
+------------------+
| API层 |
+------------------+
+------------------+
| 功能扩展层 |
+------------------+
+------------------+
| 核心层 |
+------------------+
+------------------+
| 硬件端口层 |
+------------------+
+------------------+
| 硬件平台 |
+------------------+
```
## 3. 核心思想
### 3.1 无动态内存分配
为了保证系统的稳定性和可靠性,避免内存碎片问题,项目采用了完全静态的内存管理方式:
- 所有菜单节点、事件队列、哈希表等均使用静态数组分配
- 提供内存大小查询接口,便于用户根据实际需求调整内存配置
- 每个模块都有明确的内存占用上限,便于资源规划
### 3.2 事件驱动解耦
采用事件驱动设计,将硬件输入与软件逻辑解耦,提高系统响应性:
- 硬件事件通过事件队列异步处理,避免阻塞主程序
- 支持多优先级事件处理,保证关键事件优先处理
- 事件超时机制,避免无效事件占用系统资源
### 3.3 高效节点查找
为了提高菜单节点的查找效率,项目采用了哈希表索引技术:
- 将节点查找时间复杂度从O(n)优化到O(1)
- 采用优化的哈希函数,减少哈希冲突
- 支持动态注册和注销菜单节点,哈希表自动更新
### 3.4 可扩展状态机
设计了灵活的状态机框架,支持动态扩展状态转换规则:
- 内置基础状态转换规则,覆盖常见菜单操作
- 支持动态注册和注销自定义状态转换规则
- 状态转换动作可定制,便于扩展菜单行为
- 支持状态切换和状态查询,便于外部系统集成
### 3.5 模块化设计
采用模块化设计,各功能模块独立,便于维护和扩展:
- 每个模块都有清晰的接口定义,模块间低耦合
- 支持通过宏开关裁剪不需要的功能,减少资源占用
- 模块化的硬件适配层,便于移植到不同硬件平台
## 4. 核心数据结构
### 4.1 菜单节点结构体
```c
typedef struct MenuNode {
MenuNodeId id; ///< 节点ID唯一
MenuNodeId parent_id; ///< 父节点ID根节点为0
const char* name; ///< 菜单名称(或多语言索引)
MenuCallback enter_cb; ///< 进入回调
MenuCallback exit_cb; ///< 退出回调
MenuNodeId first_child_id; ///< 第一个子节点ID替代指针
MenuNodeId next_sibling_id; ///< 下一个兄弟节点ID替代指针
MenuNodeId prev_sibling_id; ///< 上一个兄弟节点ID替代指针
MenuNodeId hash_next_id; ///< 哈希表下一个节点ID替代指针
// 位域:减少内存占用
struct {
bool is_registered : 1; ///< 是否已注册
bool is_selected : 1; ///< 是否被选中
unsigned int reserved : 6; ///< 保留位
} flags;
#if MENU_CONFIG_ENABLE_PARAM
uint16_t param_id; ///< 绑定的参数ID启用参数时有效
#endif
} MenuNode;
```
**设计亮点**
- 使用索引代替指针减少内存占用32位系统节省12字节/节点64位系统节省28字节/节点)
- 采用双向链表结构,便于兄弟节点间遍历
- 使用哈希表索引,提高节点查找效率
- 位域设计,减少内存占用
### 4.2 菜单核心上下文
```c
typedef struct {
MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池(无动态分配)
MenuNode* hash_table[MENU_CONFIG_HASH_TABLE_SIZE]; ///< 哈希表,用于快速查找节点
MenuStack stack; ///< 菜单导航栈
MenuNavPath nav_path; ///< 菜单导航路径
MenuEventQueue event_queue; ///< 事件队列
MenuNodeId current_node_id; ///< 当前选中的节点ID
MenuState current_state; ///< 当前菜单状态
uint32_t last_refresh_tick; ///< 上次刷新时间ms
bool is_initialized; ///< 是否已初始化
MenuErrCode error_code; ///< 当前错误码
const char* error_msg; ///< 当前错误信息
uint16_t node_count; ///< 已注册节点数量
uint16_t free_node_count; ///< 空闲节点数量
// 状态机扩展
#if MENU_CONFIG_ENABLE_STATE_MACHINE_EXT
MenuStateTransition custom_transitions[MENU_CONFIG_MAX_STATE_TRANSITIONS]; ///< 自定义状态转换规则
uint16_t custom_transition_count; ///< 自定义状态转换规则数量
#endif
} MenuCoreCtx;
```
**设计亮点**
- 集中管理菜单组件的所有状态和资源
- 静态数组设计,无动态内存分配
- 包含所有核心数据结构,便于统一管理
- 支持状态机扩展,便于功能扩展
### 4.3 事件队列结构体
```c
typedef struct {
MenuEvent buffer[MENU_CONFIG_EVENT_QUEUE_LEN]; ///< 队列缓冲区
uint8_t head; ///< 入队指针
uint8_t tail; ///< 出队指针
uint8_t count; ///< 队列元素数量
uint8_t reserved; ///< 保留字节,用于对齐
} MenuEventQueue;
```
**设计亮点**
- 环形队列设计,避免内存碎片
- 支持多优先级事件处理
- 队列满时自动替换低优先级事件,保证高优先级事件优先处理
- 事件超时机制,避免无效事件占用资源
### 4.4 状态转换结构体
```c
typedef struct {
MenuState current_state; ///< 当前状态
MenuEventType event; ///< 触发事件
MenuState next_state; ///< 下一个状态
MenuErrCode (*action)(MenuGlobalCtx* global_ctx); ///< 状态转换动作
const char* desc; ///< 状态转换描述(用于调试)
} MenuStateTransition;
```
**设计亮点**
- 灵活的状态转换规则,支持动态注册
- 状态转换动作可定制,便于扩展菜单行为
- 支持状态转换描述,便于调试
### 4.5 参数结构体
```c
typedef struct {
uint16_t id; ///< 参数ID
const char* name; ///< 参数名称
ParamType type; ///< 参数类型
void* value; ///< 参数值指针
void* default_value; ///< 默认值指针
void* min_value; ///< 最小值指针
void* max_value; ///< 最大值指针
bool is_read_only; ///< 是否只读
bool is_modified; ///< 是否已修改
} MenuParam;
```
**设计亮点**
- 支持多种参数类型int8/uint8/int16/uint16/int32/uint32/float
- 自动范围检查和边界处理
- 参数与菜单节点绑定,支持菜单直接调整参数
- 支持默认值恢复功能
### 4.6 语言包结构体
```c
typedef struct {
LangId id; ///< 语言ID
const char* name; ///< 语言名称(如"English"、"中文"
const LangStrItem* strings; ///< 字符串数组
uint16_t string_count; ///< 字符串数量
} LangPack;
```
**设计亮点**
- 支持多种语言切换
- 字符串ID映射机制便于管理和维护
- 自动回退到默认语言,提高系统容错性
- 优化的语言字符串查找算法
## 5. 核心算法
### 5.1 哈希表算法
**功能**用于快速查找菜单节点将节点查找时间复杂度从O(n)优化到O(1)
**实现原理**
```c
static uint16_t menu_core_hash_func(MenuNodeId node_id)
{
// 优化后的哈希函数:使用混合哈希算法,减少冲突,提高分布均匀性
// 结合了旋转和异或操作,适合嵌入式系统的高效计算
uint32_t hash = node_id;
// 旋转和异或操作,提高哈希分布的均匀性
hash = (hash << 15) | (hash >> 17); // 旋转15位
hash ^= 0x61C88647; // 黄金比例常数,提高随机性
hash *= 0x85EBca6B; // 另一个黄金比例常数
hash = (hash << 13) | (hash >> 19); // 旋转13位
hash ^= 0xC2B2AE35; // 再一次异或黄金比例常数
return (uint16_t)(hash % MENU_CONFIG_HASH_TABLE_SIZE);
}
```
**设计亮点**
- 采用优化的混合哈希算法,减少冲突
- 结合旋转和异或操作,提高哈希分布的均匀性
- 适合嵌入式系统的高效计算,执行速度快
- 支持动态添加和删除节点,哈希表自动更新
### 5.2 事件队列算法
**功能**:实现基于优先级的事件处理机制,支持多优先级事件,队列满时自动替换低优先级事件
**实现原理**
- 采用环形队列设计,避免内存碎片
- 支持4级事件优先级低、普通、高、紧急
- 同优先级事件采用FIFO顺序处理
- 队列满时,低优先级事件会被新的高优先级事件替换
- 事件包含时间戳,支持超时处理
**关键函数**
- `menu_post_event`:发送事件到事件队列
- `menu_core_get_event`:从事件队列获取下一个待处理事件
- `menu_main_loop`:菜单主循环,处理事件和刷新显示
### 5.3 状态机算法
**功能**:实现菜单的状态管理和状态转换,支持动态注册自定义状态转换规则
**实现原理**
- 基于有限状态机FSM设计定义了菜单的基本状态和状态转换规则
- 支持动态注册和注销自定义状态转换规则
- 状态转换时执行相应的动作函数,便于扩展菜单行为
- 支持状态查询和状态切换
**内置状态**
- `MENU_STATE_INIT`:初始化状态
- `MENU_STATE_NORMAL`:正常导航状态
- `MENU_STATE_PARAM_EDIT`:参数编辑状态
- `MENU_STATE_CONFIRM`:确认状态
- `MENU_STATE_ERROR`:错误状态
**关键函数**
- `menu_state_register_transition`:注册自定义状态转换规则
- `menu_state_unregister_transition`:注销自定义状态转换规则
- `menu_state_get_current`:获取当前状态
- `menu_state_switch`:切换到指定状态
- `menu_state_transition`:处理状态转换
### 5.4 菜单导航算法
**功能**:实现菜单的上下左右导航,支持多级菜单和栈管理
**实现原理**
- 使用栈数据结构管理导航层级
- 支持环形导航(到达边界自动循环)
- 记录完整的导航路径,便于回溯
- 支持导航深度查询和路径获取
**关键函数**
- `menu_core_navigate_enter`:进入子菜单
- `menu_core_navigate_back`:返回上一级菜单
- `menu_core_navigate_up`:上移菜单项
- `menu_core_navigate_down`:下移菜单项
- `menu_get_nav_path`:获取导航路径
- `menu_get_nav_depth`:获取导航深度
## 6. 核心功能模块
### 6.1 菜单节点管理
**功能**:提供菜单节点的注册、删除、更新和查找功能
**实现原理**
- 使用静态数组存储菜单节点,无动态内存分配
- 采用哈希表索引,提高节点查找效率
- 支持批量注册菜单节点,提高初始化效率
- 支持动态更新和删除菜单节点
**关键函数**
- `menu_register_node`:注册单个菜单节点
- `menu_register_nodes`:批量注册菜单节点
- `menu_unregister_node`:删除菜单节点
- `menu_update_node`:更新菜单节点
- `menu_core_find_node`:查找菜单节点
### 6.2 参数管理
**功能**:提供参数的注册、读写、范围检查和默认值恢复功能
**实现原理**
- 支持多种参数类型int8/uint8/int16/uint16/int32/uint32/float
- 自动范围检查和边界处理
- 参数与菜单节点绑定,支持菜单直接调整参数
- 支持默认值恢复功能
**关键函数**
- `menu_param_register`:注册参数
- `menu_param_set_value`:设置参数值
- `menu_param_get_value`:获取参数值
- `menu_param_get_type`:获取参数类型
### 6.3 多语言支持
**功能**:提供多语言切换和字符串管理功能
**实现原理**
- 支持多种语言切换
- 字符串ID映射机制便于管理和维护
- 自动回退到默认语言,提高系统容错性
- 优化的语言字符串查找算法
**关键函数**
- `menu_lang_set_current`:设置当前语言
- `menu_lang_get_current`:获取当前语言
- `menu_lang_register_str`:注册语言字符串
- `menu_lang_load_pack`:加载语言包
### 6.4 Modbus映射
**功能**实现参数与Modbus寄存器的双向映射
**实现原理**
- 支持多种Modbus寄存器类型线圈、离散输入、保持寄存器、输入寄存器
- 支持多种字节序小端、大端、Modbus标准
- 自动类型转换和边界检查
- 灵活的读写权限控制
**关键函数**
- `menu_modbus_map_register`注册参数与Modbus寄存器的映射关系
- `menu_modbus_map_param_to_reg`参数值转换为Modbus寄存器数据
- `menu_modbus_map_reg_to_param`Modbus寄存器数据转换为参数值
## 7. 硬件适配层
**功能**:提供硬件适配接口,对接具体硬件平台
**实现原理**
- 定义统一的硬件驱动结构体,包含各种硬件接口
- 支持运行时配置硬件驱动,便于动态切换
- 屏蔽不同硬件平台的差异,提高组件的可移植性
**硬件接口**
```c
typedef struct {
void (*printf)(const char* fmt, va_list args); ///< 硬件打印接口
uint32_t (*get_tick)(void); ///< 获取系统滴答时间
void (*delay_ms)(uint32_t ms); ///< 硬件延迟函数
void (*display)(const char* menu_name, uint16_t menu_id); ///< 菜单显示接口
MenuEventType (*key_scan)(void); ///< 按键扫描接口(可选)
void (*irq_ctrl)(bool enable); ///< 中断管理接口(可选)
void (*error_handler)(MenuErrCode err_code); ///< 错误处理接口(可选)
MenuErrCode (*modbus_send)(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len); ///< Modbus发送接口可选
MenuErrCode (*modbus_receive)(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len); ///< Modbus接收接口可选
} MenuPortDriver;
```
**关键函数**
- `menu_port_init`:硬件端口初始化
- `menu_port_deinit`:硬件端口反初始化
## 8. 配置管理
**功能**:提供灵活的配置选项,便于用户根据需求定制功能
**实现原理**
- 所有配置项集中在`menu_config.h`文件中
- 支持通过宏开关裁剪不需要的功能
- 支持调整资源大小,如最大节点数、栈深度、事件队列长度等
- 支持调试开关,便于开发和调试
**核心配置项**
| 配置项 | 说明 | 默认值 |
|-------|------|-------|
| MENU_CONFIG_MAX_NODES | 最大菜单节点数 | 32 |
| MENU_CONFIG_HASH_TABLE_SIZE | 菜单哈希表大小(必须为质数) | 31 |
| MENU_CONFIG_STACK_DEPTH | 菜单栈深度 | 8 |
| MENU_CONFIG_EVENT_QUEUE_LEN | 事件队列长度 | 16 |
| MENU_CONFIG_ENABLE_ASSERT | 是否启用断言 | 1 |
| MENU_CONFIG_ENABLE_DEBUG | 是否启用调试打印 | 1 |
| MENU_CONFIG_ENABLE_MEM_MONITOR | 是否启用内存监控 | 1 |
| MENU_CONFIG_ENABLE_PARAM | 是否启用参数管理 | 1 |
| MENU_CONFIG_ENABLE_LANG | 是否启用多语言支持 | 1 |
| MENU_CONFIG_ENABLE_MODBUS_MAP | 是否启用Modbus映射 | 1 |
## 9. 工业级设计考量
### 9.1 可靠性设计
- **无动态内存分配**:完全使用静态数组,避免内存碎片和内存泄漏
- **完善的错误处理**:每个函数都有明确的错误码返回,便于调试和错误定位
- **边界检查**:所有数组访问都有边界检查,防止越界访问
- **断言机制**:关键函数入口有断言检查,便于调试
- **事件超时处理**:避免无效事件占用系统资源
### 9.2 性能优化
- **高效的哈希表算法**将节点查找时间复杂度从O(n)优化到O(1)
- **位域设计**:减少内存占用,提高缓存命中率
- **索引代替指针**:减少内存占用,提高系统兼容性
- **事件驱动设计**提高系统响应性减少CPU占用
- **模块化设计**:支持功能裁剪,减少资源占用
### 9.3 可移植性设计
- **硬件抽象层**:屏蔽不同硬件平台的差异,提高组件的可移植性
- **标准C语言实现**:不依赖特定的编译器或操作系统
- **灵活的配置选项**:支持根据不同硬件平台调整资源大小
- **模块化设计**:各功能模块独立,便于移植和扩展
### 9.4 可维护性设计
- **清晰的代码结构**:采用分层架构和模块化设计,代码结构清晰
- **完善的文档**提供详细的API文档和开发文档
- **统一的代码风格**:采用清晰的命名规范和注释风格
- **调试支持**:提供丰富的调试打印和日志功能
- **错误定位机制**:每个错误都有明确的错误码和错误信息
## 10. 应用场景
本菜单组件适用于各种资源受限的嵌入式系统,特别是工业控制领域,如:
- 工业控制器
- 智能家居设备
- 仪器仪表
- 医疗设备
- 嵌入式人机界面
- Modbus从站设备
## 11. 开发流程
### 11.1 硬件适配
1. 根据目标硬件平台,实现`menu_port.c`中的硬件接口
2. 配置`menu_config.h`中的资源大小和功能开关
3. 编译和测试硬件适配层
### 11.2 菜单设计
1. 设计菜单结构,确定菜单节点的层级关系
2. 注册菜单节点,构建菜单树
3. 注册参数(可选)
4. 注册Modbus映射可选
5. 注册自定义状态转换规则(可选)
### 11.3 应用集成
1. 在应用程序中初始化菜单组件
2. 在主循环中调用`menu_main_loop`处理事件
3. 调用`menu_post_event`发送硬件事件(如按键事件)
4. 根据需要调用其他API接口实现自定义功能
## 12. 测试与验证
### 12.1 单元测试
项目提供了单元测试框架,位于`test/menu_test.c`,包含以下测试用例:
- 菜单初始化和反初始化测试
- 菜单节点注册和删除测试
- 菜单导航测试
- 事件队列测试
- 参数管理测试
- 多语言支持测试
- Modbus映射测试
### 12.2 集成测试
- 硬件适配测试:验证硬件接口的正确性
- 功能测试:验证各个功能模块的正确性
- 性能测试:测试菜单组件的执行效率和资源占用
- 可靠性测试:测试菜单组件在各种异常情况下的表现
### 12.3 现场测试
- 在目标硬件平台上进行测试,验证菜单组件的实际运行效果
- 测试各种用户操作场景,确保菜单组件的易用性和稳定性
- 测试长时间运行情况下的稳定性
## 13. 版本管理
| 版本 | 发布日期 | 主要变更 |
|------|----------|----------|
| v1.0.0 | 2025-12-19 | 初始版本,包含核心功能 |
## 14. 总结
本项目是一个设计精良的工业级嵌入式菜单组件,具有高可靠性、高性能、高可移植性和高可扩展性等特点。采用分层架构、事件驱动设计、哈希表索引、可扩展状态机等先进技术,适合各种资源受限的嵌入式系统。
通过本开发文档,开发者可以全面了解菜单组件的架构设计、核心思想、数据算法和使用方法,便于快速集成和定制开发。