From e36b4e0e274df8a858d2b4629d56a1abcb5cacfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E4=BD=B3?= <13101321+jfen5577@user.noreply.gitee.com> Date: Thu, 18 Dec 2025 23:56:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B4=E5=AE=9A=E4=B8=80?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 523 +++++++++++++++++++++++++++++++++++ api/menu.h | 406 +++++++++++++++++++-------- api/menu_config.h | 24 +- examples/menu_example.c | 116 +++++--- internal/menu_core.h | 50 ++-- internal/menu_data.h | 78 +++--- internal/menu_def.h | 47 +++- port/menu_port.c | 273 ++++++++++++++----- port/menu_port.h | 145 +++++++--- src/core/menu_core.c | 544 ++++++++++++++++++++++++++++++------- src/features/menu_modbus.c | 85 +++--- src/lang/menu_lang.c | 124 +++++++-- src/param/menu_param.c | 132 +++++++-- src/param/menu_param.h | 39 +++ src/utils/menu_utils.c | 66 ++++- test/menu_test.c | 471 ++++++++++++++++++++++++++++++++ 16 files changed, 2619 insertions(+), 504 deletions(-) create mode 100644 API.md create mode 100644 src/param/menu_param.h create mode 100644 test/menu_test.c diff --git a/API.md b/API.md new file mode 100644 index 0000000..bb6c7f3 --- /dev/null +++ b/API.md @@ -0,0 +1,523 @@ +# 菜单组件API文档 + +## 1. 概述 + +本文档详细描述了菜单组件的API接口,包括核心功能、功能扩展和Modbus映射功能。 + +## 2. 核心接口 + +### 2.1 上下文管理 + +```c +/** + * @brief 获取菜单组件上下文所需的内存大小 + * @return 上下文内存大小(字节) + */ +uint32_t menu_get_ctx_size(void); + +/** + * @brief 菜单组件初始化(必须首先调用) + * @param ctx 菜单上下文指针(用户需提前分配内存) + * @return 错误码 + */ +MenuErrCode menu_init(MenuGlobalCtx* ctx); + +/** + * @brief 菜单组件反初始化(释放资源) + * @param ctx 菜单上下文指针 + * @return 错误码 + */ +MenuErrCode menu_deinit(MenuGlobalCtx* ctx); + +/** + * @brief 检查菜单组件是否已初始化 + * @param ctx 菜单上下文指针 + * @return 是否已初始化 + */ +bool menu_is_initialized(MenuGlobalCtx* ctx); +``` + +### 2.2 菜单管理 + +```c +/** + * @brief 注册菜单节点(构建菜单树) + * @param ctx 菜单上下文指针 + * @param parent_id 父节点ID(根节点填0) + * @param node_id 当前节点ID(唯一,不可重复) + * @param name_str 菜单名称字符串(或多语言索引) + * @param enter_cb 进入该菜单的回调函数(NULL表示无) + * @param exit_cb 退出该菜单的回调函数(NULL表示无) + * @return 错误码 + */ +MenuErrCode menu_register_node(MenuGlobalCtx* ctx, MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb); + +/** + * @brief 批量注册菜单节点 + * @param ctx 菜单上下文指针 + * @param configs 菜单节点配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_register_nodes(MenuGlobalCtx* ctx, const MenuNodeConfig* configs, uint16_t config_count); + +/** + * @brief 获取当前选中的菜单节点ID + * @param ctx 菜单上下文指针 + * @param node_id 输出参数,当前节点ID + * @return 错误码 + */ +MenuErrCode menu_get_current_node(MenuGlobalCtx* ctx, MenuNodeId* node_id); +``` + +### 2.3 事件处理 + +```c +/** + * @brief 发送事件到菜单事件队列(如按键事件、自定义事件) + * @param ctx 菜单上下文指针 + * @param type 事件类型 + * @param param 事件附加参数(可选,如按键长按时间) + * @param priority 事件优先级(可选,默认为普通优先级) + * @return 错误码 + */ +MenuErrCode menu_post_event(MenuGlobalCtx* ctx, MenuEventType type, uint32_t param, MenuEventPriority priority); + +/** + * @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示) + * @param ctx 菜单上下文指针 + */ +void menu_main_loop(MenuGlobalCtx* ctx); +``` + +## 3. 功能扩展接口 + +### 3.1 参数管理 + +```c +/** + * @brief 注册参数到菜单节点(参数管理功能) + * @param ctx 菜单上下文指针 + * @param node_id 菜单节点ID(参数与菜单绑定) + * @param param_id 参数ID(唯一) + * @param type 参数类型 + * @param min_val 最小值(浮点型,内部自动转换) + * @param max_val 最大值(浮点型,内部自动转换) + * @param default_val 默认值(浮点型,内部自动转换) + * @param scale 缩放因子(如0.1表示保留1位小数) + * @return 错误码 + */ +MenuErrCode menu_param_register(MenuGlobalCtx* ctx, MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale); + +/** + * @brief 批量注册参数到菜单节点 + * @param ctx 菜单上下文指针 + * @param configs 参数配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_param_register_batch(MenuGlobalCtx* ctx, const MenuParamConfig* configs, uint16_t config_count); + +/** + * @brief 设置参数值 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param value 新值(浮点型,内部自动转换) + * @return 错误码 + */ +MenuErrCode menu_param_set_value(MenuGlobalCtx* ctx, uint16_t param_id, float value); + +/** + * @brief 获取参数值 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param value 输出参数,当前值(浮点型) + * @return 错误码 + */ +MenuErrCode menu_param_get_value(MenuGlobalCtx* ctx, uint16_t param_id, float* value); + +/** + * @brief 获取参数类型 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param type 输出参数,参数类型 + * @return 错误码 + */ +MenuErrCode menu_param_get_type(MenuGlobalCtx* ctx, uint16_t param_id, MenuParamType* type); +``` + +### 3.2 多语言支持 + +```c +/** + * @brief 设置当前语言 + * @param ctx 菜单上下文指针 + * @param lang_id 语言ID(如0-中文,1-英文) + * @return 错误码 + */ +MenuErrCode menu_lang_set_current(MenuGlobalCtx* ctx, uint8_t lang_id); + +/** + * @brief 获取当前语言 + * @param ctx 菜单上下文指针 + * @param lang_id 输出参数,当前语言ID + * @return 错误码 + */ +MenuErrCode menu_lang_get_current(MenuGlobalCtx* ctx, uint8_t* lang_id); + +/** + * @brief 注册语言字符串 + * @param ctx 菜单上下文指针 + * @param str_id 字符串ID + * @param lang_id 语言ID + * @param str 语言字符串 + * @return 错误码 + */ +MenuErrCode menu_lang_register_str(MenuGlobalCtx* ctx, uint16_t str_id, uint8_t lang_id, const char* str); + +/** + * @brief 批量注册语言字符串 + * @param ctx 菜单上下文指针 + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_register_strs(MenuGlobalCtx* ctx, const MenuLangStr* strs, uint16_t count); + +/** + * @brief 加载语言包(动态加载语言字符串) + * @param ctx 菜单上下文指针 + * @param lang_id 语言ID + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_load_pack(MenuGlobalCtx* ctx, uint8_t lang_id, const MenuLangStr* strs, uint16_t count); +``` + +### 3.3 内存监控 + +```c +/** + * @brief 获取菜单内存使用统计信息 + * @param ctx 菜单上下文指针 + * @param stats 输出参数,内存使用统计信息 + * @return 错误码 + */ +MenuErrCode menu_get_mem_stats(MenuGlobalCtx* ctx, MenuMemStats* stats); +``` + +## 4. Modbus映射功能 + +```c +/** + * @brief 注册参数与Modbus寄存器的映射关系 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID(需已通过menu_param_register注册) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器起始地址(协议地址,如40001) + * @param reg_count 占用寄存器数量(如1:16位,2:32位,4:64位) + * @param perm 读写权限 + * @param byte_order 字节序(可选,默认使用MENU_CONFIG_MODBUS_BYTE_ORDER) + * @return 错误码 + */ +MenuErrCode menu_modbus_map_register(MenuGlobalCtx* ctx, uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order); + +/** + * @brief 批量注册参数与Modbus寄存器的映射关系 + * @param ctx 菜单上下文指针 + * @param configs Modbus映射配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_register_batch(MenuGlobalCtx* ctx, const MenuModbusMapConfig* configs, uint16_t config_count); + +/** + * @brief 根据参数ID查询Modbus映射关系 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param map 输出参数,映射关系结构体 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_query_by_param(MenuGlobalCtx* ctx, uint16_t param_id, ModbusMap* map); + +/** + * @brief 根据寄存器地址和类型查询Modbus映射关系 + * @param ctx 菜单上下文指针 + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param map 输出参数,映射关系结构体 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_query_by_reg(MenuGlobalCtx* ctx, ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map); + +/** + * @brief 将参数值写入对应的Modbus寄存器(双向转换:参数值→寄存器数据) + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param reg_buf 输出参数,寄存器数据缓冲区(需足够大,如32位占2个16位寄存器) + * @param buf_len 缓冲区长度(输入),实际写入长度(输出) + * @return 错误码 + */ +MenuErrCode menu_modbus_map_param_to_reg(MenuGlobalCtx* ctx, uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len); + +/** + * @brief 将Modbus寄存器数据读取到参数值(双向转换:寄存器数据→参数值) + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param reg_buf 输入参数,寄存器数据缓冲区 + * @param buf_len 缓冲区长度 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_reg_to_param(MenuGlobalCtx* ctx, uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len); +``` + +## 5. 数据类型 + +### 5.1 错误码 + +```c +typedef enum { + MENU_OK = 0, ///< 操作成功 + MENU_ERR_INVALID_PARAM, ///< 无效参数 + MENU_ERR_OUT_OF_MEMORY, ///< 内存不足(静态内存池已满) + MENU_ERR_NODE_NOT_FOUND, ///< 菜单节点未找到 + MENU_ERR_STACK_OVERFLOW, ///< 菜单栈溢出(导航层级超过配置) + MENU_ERR_STACK_UNDERFLOW, ///< 菜单栈下溢(已到根节点仍返回) + MENU_ERR_EVENT_QUEUE_FULL, ///< 事件队列已满 + MENU_ERR_NOT_SUPPORTED, ///< 功能未启用(如未开启多语言) + MENU_ERR_HW_PORT_ERROR, ///< 硬件端口层错误 + MENU_ERR_NOT_INITIALIZED, ///< 组件未初始化 + MENU_ERR_INVALID_CONTEXT ///< 无效上下文指针 +} MenuErrCode; +``` + +### 5.2 事件类型 + +```c +typedef enum { + MENU_EVENT_NONE = 0, ///< 无事件 + MENU_EVENT_KEY_UP, ///< 上键按下 + MENU_EVENT_KEY_DOWN, ///< 下键按下 + MENU_EVENT_KEY_ENTER, ///< 确认键按下 + MENU_EVENT_KEY_BACK, ///< 返回键按下 + MENU_EVENT_CUSTOM_BEGIN = 0x10, ///< 自定义事件起始标识(用户可扩展) +} MenuEventType; +``` + +### 5.3 事件优先级 + +```c +typedef enum { + MENU_EVENT_PRIORITY_LOW = 0, ///< 低优先级事件 + MENU_EVENT_PRIORITY_NORMAL, ///< 普通优先级事件(默认) + MENU_EVENT_PRIORITY_HIGH, ///< 高优先级事件 + MENU_EVENT_PRIORITY_CRITICAL ///< 紧急优先级事件 +} MenuEventPriority; +``` + +### 5.4 参数类型 + +```c +typedef enum { + MENU_PARAM_TYPE_INT8, + MENU_PARAM_TYPE_UINT8, + MENU_PARAM_TYPE_INT16, + MENU_PARAM_TYPE_UINT16, + MENU_PARAM_TYPE_INT32, + MENU_PARAM_TYPE_UINT32, + MENU_PARAM_TYPE_FLOAT, +} MenuParamType; +``` + +### 5.5 菜单状态 + +```c +typedef enum { + MENU_STATE_INIT = 0, ///< 初始化状态 + MENU_STATE_NORMAL, ///< 正常导航状态 + MENU_STATE_PARAM_EDIT, ///< 参数编辑状态 + MENU_STATE_CONFIRM, ///< 确认状态(如保存/退出确认) + MENU_STATE_ERROR, ///< 错误状态 +} MenuState; +``` + +## 6. 配置项 + +菜单组件的配置项位于`menu_config.h`文件中,用户可以根据项目需求调整以下配置: + +### 6.1 核心配置 + +- `MENU_CONFIG_MAX_NODES`:最大菜单节点数 +- `MENU_CONFIG_STACK_DEPTH`:菜单栈深度 +- `MENU_CONFIG_EVENT_QUEUE_LEN`:事件队列长度 +- `MENU_CONFIG_ENABLE_ASSERT`:是否启用断言 +- `MENU_CONFIG_ENABLE_DEBUG`:是否启用调试打印 +- `MENU_CONFIG_ENABLE_MEM_MONITOR`:是否启用内存监控 + +### 6.2 功能扩展配置 + +- `MENU_CONFIG_ENABLE_PARAM`:是否启用参数管理功能 +- `MENU_CONFIG_ENABLE_LANG`:是否启用多语言功能 +- `MENU_CONFIG_MAX_PARAMS`:最大参数数量 +- `MENU_CONFIG_MAX_LANGS`:支持的最大语言数量 + +### 6.3 Modbus映射配置 + +- `MENU_CONFIG_ENABLE_MODBUS_MAP`:是否启用参数-Modbus寄存器映射功能 +- `MENU_CONFIG_MAX_MODBUS_MAPS`:最大Modbus映射数量 +- `MENU_CONFIG_MODBUS_MAX_ADDR`:Modbus寄存器地址最大值 +- `MENU_CONFIG_MODBUS_BYTE_ORDER`:默认Modbus字节序 +- `MENU_CONFIG_MODBUS_PERMISSION`:是否启用Modbus映射权限校验 + +## 7. 硬件适配 + +菜单组件的硬件适配层位于`port/menu_port.h`和`port/menu_port.c`文件中,用户需要实现以下接口: + +```c +/** + * @brief 硬件端口驱动结构体(模块化设计,支持运行时配置) + */ +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; + +/** + * @brief 硬件端口初始化接口 + * @param driver 硬件驱动结构体指针 + * @return 错误码 + */ +MenuErrCode menu_port_init(const MenuPortDriver* driver); + +/** + * @brief 硬件端口反初始化接口 + * @return 错误码 + */ +MenuErrCode menu_port_deinit(void); +``` + +## 8. 使用示例 + +### 8.1 基本使用 + +```c +#include "menu.h" +#include "menu_port.h" + +int main(void) +{ + // 1. 初始化硬件 + hal_init(); + + // 2. 初始化菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + menu_init(ctx); + + // 3. 注册菜单节点 + menu_register_node(ctx, 0, 1, "主菜单", NULL, NULL); + menu_register_node(ctx, 1, 2, "参数设置", NULL, NULL); + menu_register_node(ctx, 1, 3, "系统信息", NULL, NULL); + + // 4. 主循环 + while (1) + { + // 扫描按键 + MenuEventType key_event = scan_key(); + if (key_event != MENU_EVENT_NONE) + { + menu_post_event(ctx, key_event, 0, MENU_EVENT_PRIORITY_NORMAL); + } + + // 处理菜单事件 + menu_main_loop(ctx); + + // 其他业务逻辑 + // ... + } + + // 5. 反初始化 + menu_deinit(ctx); + + return 0; +} +``` + +### 8.2 批量注册菜单节点 + +```c +// 定义菜单节点配置 +MenuNodeConfig menu_nodes[] = { + {1, 0, "主菜单", NULL, NULL}, + {2, 1, "参数设置", NULL, NULL}, + {3, 1, "系统信息", NULL, NULL}, + {4, 2, "温度参数", NULL, NULL}, + {5, 2, "湿度参数", NULL, NULL} +}; + +// 批量注册菜单节点 +menu_register_nodes(ctx, menu_nodes, sizeof(menu_nodes) / sizeof(menu_nodes[0])); +``` + +### 8.3 参数管理 + +```c +// 注册参数 +menu_param_register(ctx, 4, 1, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f); + +// 设置参数值 +menu_param_set_value(ctx, 1, 30.0f); + +// 获取参数值 +float value; +menu_param_get_value(ctx, 1, &value); +``` + +### 8.4 多语言支持 + +```c +// 注册语言字符串 +menu_lang_register_str(ctx, 1, 0, "中文菜单"); +menu_lang_register_str(ctx, 1, 1, "English Menu"); + +// 设置当前语言为中文 +menu_lang_set_current(ctx, 0); + +// 动态加载语言包 +MenuLangStr lang_pack[] = { + {2, 0, "中文参数"}, + {2, 1, "English Param"}, + {3, 0, "中文信息"}, + {3, 1, "English Info"} +}; +menu_lang_load_pack(ctx, 0, lang_pack, sizeof(lang_pack) / sizeof(lang_pack[0])); +``` + +### 8.5 Modbus映射 + +```c +// 注册Modbus映射 +menu_modbus_map_register(ctx, 1, MODBUS_REG_TYPE_HOLDING_REG, 40001, 2, MODBUS_PERM_READ_WRITE, 2); + +// 参数值转换为Modbus寄存器数据 +uint8_t reg_buf[4]; +uint8_t buf_len = sizeof(reg_buf); +menu_modbus_map_param_to_reg(ctx, 1, reg_buf, &buf_len); + +// Modbus寄存器数据转换为参数值 +menu_modbus_map_reg_to_param(ctx, 1, reg_buf, buf_len); +``` + +## 9. 版本信息 + +- 版本:v1.0.0 +- 最后更新:2025-12-18 +- 适用平台:所有支持C语言的嵌入式系统 diff --git a/api/menu.h b/api/menu.h index f14ad9d..cd0c597 100644 --- a/api/menu.h +++ b/api/menu.h @@ -11,6 +11,12 @@ #include /************************** 全局类型导出 **************************/ +/** + * @brief 菜单组件全局上下文(用户需创建并管理) + * @note 替代全局变量,提高模块独立性和可测试性 + */ +typedef struct MenuGlobalCtx MenuGlobalCtx; + /** * @brief 菜单错误码(工业级错误处理:明确所有可能的错误类型) */ @@ -23,9 +29,21 @@ typedef enum { MENU_ERR_STACK_UNDERFLOW, ///< 菜单栈下溢(已到根节点仍返回) MENU_ERR_EVENT_QUEUE_FULL, ///< 事件队列已满 MENU_ERR_NOT_SUPPORTED, ///< 功能未启用(如未开启多语言) - MENU_ERR_HW_PORT_ERROR ///< 硬件端口层错误 + MENU_ERR_HW_PORT_ERROR, ///< 硬件端口层错误 + MENU_ERR_NOT_INITIALIZED, ///< 组件未初始化 + MENU_ERR_INVALID_CONTEXT ///< 无效上下文指针 } MenuErrCode; +/** + * @brief 事件优先级枚举 + */ +typedef enum { + MENU_EVENT_PRIORITY_LOW = 0, ///< 低优先级事件 + MENU_EVENT_PRIORITY_NORMAL, ///< 普通优先级事件(默认) + MENU_EVENT_PRIORITY_HIGH, ///< 高优先级事件 + MENU_EVENT_PRIORITY_CRITICAL ///< 紧急优先级事件 +} MenuEventPriority; + /** * @brief 菜单事件类型(事件驱动核心:解耦按键与菜单逻辑) */ @@ -50,116 +68,19 @@ typedef uint16_t MenuNodeId; */ typedef MenuErrCode (*MenuCallback)(MenuNodeId node_id); -/************************** 核心接口声明 **************************/ /** - * @brief 菜单组件初始化(必须首先调用) - * @return 错误码 - */ -MenuErrCode menu_init(void); - -/** - * @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示) - */ -void menu_main_loop(void); - -/** - * @brief 发送事件到菜单事件队列(如按键事件、自定义事件) - * @param type 事件类型 - * @param param 事件附加参数(可选,如按键长按时间) - * @return 错误码 - */ -MenuErrCode menu_post_event(MenuEventType type, uint32_t param); - -/** - * @brief 注册菜单节点(构建菜单树) - * @param parent_id 父节点ID(根节点填0) - * @param node_id 当前节点ID(唯一,不可重复) - * @param name_str 菜单名称字符串(或多语言索引) - * @param enter_cb 进入该菜单的回调函数(NULL表示无) - * @param exit_cb 退出该菜单的回调函数(NULL表示无) - * @return 错误码 - */ -MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb); - -/** - * @brief 获取当前选中的菜单节点ID - * @param node_id 输出参数,当前节点ID - * @return 错误码 - */ -MenuErrCode menu_get_current_node(MenuNodeId* node_id); - -/************************** 功能扩展接口声明(可裁剪) **************************/ -#if MENU_CONFIG_ENABLE_PARAM -/** - * @brief 参数类型枚举 + * @brief 参数类型枚举(支持多种数据类型) */ typedef enum { - MENU_PARAM_TYPE_INT8, - MENU_PARAM_TYPE_UINT8, - MENU_PARAM_TYPE_INT16, - MENU_PARAM_TYPE_UINT16, - MENU_PARAM_TYPE_INT32, - MENU_PARAM_TYPE_UINT32, - MENU_PARAM_TYPE_FLOAT, + MENU_PARAM_TYPE_INT8, ///< 8位有符号整数 + MENU_PARAM_TYPE_UINT8, ///< 8位无符号整数 + MENU_PARAM_TYPE_INT16, ///< 16位有符号整数 + MENU_PARAM_TYPE_UINT16, ///< 16位无符号整数 + MENU_PARAM_TYPE_INT32, ///< 32位有符号整数 + MENU_PARAM_TYPE_UINT32, ///< 32位无符号整数 + MENU_PARAM_TYPE_FLOAT, ///< 浮点类型 } MenuParamType; -/** - * @brief 注册参数到菜单节点(参数管理功能) - * @param node_id 菜单节点ID(参数与菜单绑定) - * @param param_id 参数ID(唯一) - * @param type 参数类型 - * @param min_val 最小值(浮点型,内部自动转换) - * @param max_val 最大值(浮点型,内部自动转换) - * @param default_val 默认值(浮点型,内部自动转换) - * @param scale 缩放因子(如0.1表示保留1位小数) - * @return 错误码 - */ -MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale); - -/** - * @brief 设置参数值 - * @param param_id 参数ID - * @param value 新值(浮点型,内部自动转换) - * @return 错误码 - */ -MenuErrCode menu_param_set_value(uint16_t param_id, float value); - -/** - * @brief 获取参数值 - * @param param_id 参数ID - * @param value 输出参数,当前值(浮点型) - * @return 错误码 - */ -MenuErrCode menu_param_get_value(uint16_t param_id, float* value); - -/** - * @brief 获取参数类型 - * @param param_id 参数ID - * @param type 输出参数,参数类型 - * @return 错误码 - */ -MenuErrCode menu_param_get_type(uint16_t param_id, MenuParamType* type); - -#endif // MENU_CONFIG_ENABLE_PARAM - -#if MENU_CONFIG_ENABLE_LANG -/** - * @brief 设置当前语言 - * @param lang_id 语言ID(如0-中文,1-英文) - * @return 错误码 - */ -MenuErrCode menu_lang_set_current(uint8_t lang_id); - -/** - * @brief 获取当前语言 - * @param lang_id 输出参数,当前语言ID - * @return 错误码 - */ -MenuErrCode menu_lang_get_current(uint8_t* lang_id); -#endif // MENU_CONFIG_ENABLE_LANG - -/************************** Modbus映射功能(启用时有效) **************************/ -#if MENU_CONFIG_ENABLE_MODBUS_MAP /** * @brief Modbus寄存器类型(符合Modbus协议标准) */ @@ -179,6 +100,255 @@ typedef enum { MODBUS_PERM_READ_WRITE, ///< 读写(如保持寄存器) } ModbusPerm; +/** + * @brief 菜单节点配置结构体(用于批量注册) + */ +typedef struct { + MenuNodeId node_id; ///< 节点ID + MenuNodeId parent_id; ///< 父节点ID + const char* name; ///< 菜单名称 + MenuCallback enter_cb; ///< 进入回调 + MenuCallback exit_cb; ///< 退出回调 +} MenuNodeConfig; + +/** + * @brief 参数配置结构体(用于批量注册) + */ +typedef struct { + MenuNodeId node_id; ///< 关联的菜单节点ID + uint16_t param_id; ///< 参数ID + MenuParamType type; ///< 参数类型 + float min_val; ///< 最小值 + float max_val; ///< 最大值 + float default_val; ///< 默认值 + float scale; ///< 缩放因子 +} MenuParamConfig; + +/** + * @brief Modbus映射配置结构体(用于批量注册) + */ +typedef struct { + uint16_t param_id; ///< 关联的参数ID + ModbusRegType reg_type; ///< 寄存器类型 + uint16_t reg_addr; ///< 寄存器地址 + uint8_t reg_count; ///< 寄存器数量 + ModbusPerm perm; ///< 读写权限 + uint8_t byte_order; ///< 字节序 +} MenuModbusMapConfig; + +/************************** 核心接口声明 **************************/ +/** + * @brief 获取菜单组件上下文所需的内存大小 + * @return 上下文内存大小(字节) + */ +uint32_t menu_get_ctx_size(void); + +/** + * @brief 菜单组件初始化(必须首先调用) + * @param ctx 菜单上下文指针(用户需提前分配内存) + * @return 错误码 + */ +MenuErrCode menu_init(MenuGlobalCtx* ctx); + +/** + * @brief 菜单组件反初始化(释放资源) + * @param ctx 菜单上下文指针 + * @return 错误码 + */ +MenuErrCode menu_deinit(MenuGlobalCtx* ctx); + +/** + * @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示) + * @param ctx 菜单上下文指针 + */ +void menu_main_loop(MenuGlobalCtx* ctx); + +/** + * @brief 发送事件到菜单事件队列(如按键事件、自定义事件) + * @param ctx 菜单上下文指针 + * @param type 事件类型 + * @param param 事件附加参数(可选,如按键长按时间) + * @param priority 事件优先级(可选,默认为普通优先级) + * @return 错误码 + */ +MenuErrCode menu_post_event(MenuGlobalCtx* ctx, MenuEventType type, uint32_t param, MenuEventPriority priority); + +/** + * @brief 发送事件到菜单事件队列(兼容旧接口,默认优先级) + * @param ctx 菜单上下文指针 + * @param type 事件类型 + * @param param 事件附加参数 + * @return 错误码 + */ +#define menu_post_event(ctx, type, param) menu_post_event((ctx), (type), (param), MENU_EVENT_PRIORITY_NORMAL) + +/** + * @brief 注册菜单节点(构建菜单树) + * @param ctx 菜单上下文指针 + * @param parent_id 父节点ID(根节点填0) + * @param node_id 当前节点ID(唯一,不可重复) + * @param name_str 菜单名称字符串(或多语言索引) + * @param enter_cb 进入该菜单的回调函数(NULL表示无) + * @param exit_cb 退出该菜单的回调函数(NULL表示无) + * @return 错误码 + */ +MenuErrCode menu_register_node(MenuGlobalCtx* ctx, MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb); + +/** + * @brief 批量注册菜单节点 + * @param ctx 菜单上下文指针 + * @param configs 菜单节点配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_register_nodes(MenuGlobalCtx* ctx, const MenuNodeConfig* configs, uint16_t config_count); + +/** + * @brief 获取当前选中的菜单节点ID + * @param ctx 菜单上下文指针 + * @param node_id 输出参数,当前节点ID + * @return 错误码 + */ +MenuErrCode menu_get_current_node(MenuGlobalCtx* ctx, MenuNodeId* node_id); + +/** + * @brief 检查菜单组件是否已初始化 + * @param ctx 菜单上下文指针 + * @return 是否已初始化 + */ +bool menu_is_initialized(MenuGlobalCtx* ctx); + +#if MENU_CONFIG_ENABLE_MEM_MONITOR +/** + * @brief 获取菜单内存使用统计信息 + * @param ctx 菜单上下文指针 + * @param stats 输出参数,内存使用统计信息 + * @return 错误码 + */ +MenuErrCode menu_get_mem_stats(MenuGlobalCtx* ctx, MenuMemStats* stats); +#endif // MENU_CONFIG_ENABLE_MEM_MONITOR + +/************************** 核心类型定义 **************************/ + +/************************** 功能扩展接口声明(可裁剪) **************************/ +#if MENU_CONFIG_ENABLE_PARAM +/** + * @brief 注册参数到菜单节点(参数管理功能) + * @param ctx 菜单上下文指针 + * @param node_id 菜单节点ID(参数与菜单绑定) + * @param param_id 参数ID(唯一) + * @param type 参数类型 + * @param min_val 最小值(浮点型,内部自动转换) + * @param max_val 最大值(浮点型,内部自动转换) + * @param default_val 默认值(浮点型,内部自动转换) + * @param scale 缩放因子(如0.1表示保留1位小数) + * @return 错误码 + */ +MenuErrCode menu_param_register(MenuGlobalCtx* ctx, MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale); + +/** + * @brief 批量注册参数到菜单节点 + * @param ctx 菜单上下文指针 + * @param configs 参数配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_param_register_batch(MenuGlobalCtx* ctx, const MenuParamConfig* configs, uint16_t config_count); + +/** + * @brief 设置参数值 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param value 新值(浮点型,内部自动转换) + * @return 错误码 + */ +MenuErrCode menu_param_set_value(MenuGlobalCtx* ctx, uint16_t param_id, float value); + +/** + * @brief 获取参数值 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param value 输出参数,当前值(浮点型) + * @return 错误码 + */ +MenuErrCode menu_param_get_value(MenuGlobalCtx* ctx, uint16_t param_id, float* value); + +/** + * @brief 获取参数类型 + * @param ctx 菜单上下文指针 + * @param param_id 参数ID + * @param type 输出参数,参数类型 + * @return 错误码 + */ +MenuErrCode menu_param_get_type(MenuGlobalCtx* ctx, uint16_t param_id, MenuParamType* type); + +#endif // MENU_CONFIG_ENABLE_PARAM + +#if MENU_CONFIG_ENABLE_LANG +/** + * @brief 语言字符串结构体(用于批量注册和动态加载) + */ +typedef struct { + uint16_t str_id; ///< 字符串ID + uint8_t lang_id; ///< 语言ID + const char* str; ///< 语言字符串 +} MenuLangStr; + +/** + * @brief 设置当前语言 + * @param ctx 菜单上下文指针 + * @param lang_id 语言ID(如0-中文,1-英文) + * @return 错误码 + */ +MenuErrCode menu_lang_set_current(MenuGlobalCtx* ctx, uint8_t lang_id); + +/** + * @brief 获取当前语言 + * @param ctx 菜单上下文指针 + * @param lang_id 输出参数,当前语言ID + * @return 错误码 + */ +MenuErrCode menu_lang_get_current(MenuGlobalCtx* ctx, uint8_t* lang_id); + +/** + * @brief 注册语言字符串 + * @param ctx 菜单上下文指针 + * @param str_id 字符串ID + * @param lang_id 语言ID + * @param str 语言字符串 + * @return 错误码 + */ +MenuErrCode menu_lang_register_str(MenuGlobalCtx* ctx, uint16_t str_id, uint8_t lang_id, const char* str); + +/** + * @brief 批量注册语言字符串 + * @param ctx 菜单上下文指针 + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_register_strs(MenuGlobalCtx* ctx, const MenuLangStr* strs, uint16_t count); + +/** + * @brief 动态加载语言包 + * @param ctx 菜单上下文指针 + * @param lang_id 语言ID + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_load_pack(MenuGlobalCtx* ctx, uint8_t lang_id, const MenuLangStr* strs, uint16_t count); + +/** + * @brief 获取支持的最大语言数量 + * @return 最大语言数量 + */ +uint8_t menu_lang_get_max_langs(void); +#endif // MENU_CONFIG_ENABLE_LANG + +/************************** Modbus映射功能(启用时有效) **************************/ +#if MENU_CONFIG_ENABLE_MODBUS_MAP + /** * @brief Modbus映射结构体(对外只读,内部初始化) */ @@ -193,6 +363,7 @@ typedef struct { /** * @brief 注册参数与Modbus寄存器的映射关系 + * @param ctx 菜单上下文指针 * @param param_id 参数ID(需已通过menu_param_register注册) * @param reg_type 寄存器类型 * @param reg_addr 寄存器起始地址(协议地址,如40001) @@ -201,42 +372,55 @@ typedef struct { * @param byte_order 字节序(可选,默认使用MENU_CONFIG_MODBUS_BYTE_ORDER) * @return 错误码 */ -MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order); +MenuErrCode menu_modbus_map_register(MenuGlobalCtx* ctx, uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order); + +/** + * @brief 批量注册参数与Modbus寄存器的映射关系 + * @param ctx 菜单上下文指针 + * @param configs Modbus映射配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_register_batch(MenuGlobalCtx* ctx, const MenuModbusMapConfig* configs, uint16_t config_count); /** * @brief 根据参数ID查询Modbus映射关系 + * @param ctx 菜单上下文指针 * @param param_id 参数ID * @param map 输出参数,映射关系结构体 * @return 错误码 */ -MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map); +MenuErrCode menu_modbus_map_query_by_param(MenuGlobalCtx* ctx, uint16_t param_id, ModbusMap* map); /** * @brief 根据寄存器地址和类型查询Modbus映射关系 + * @param ctx 菜单上下文指针 * @param reg_type 寄存器类型 * @param reg_addr 寄存器地址 * @param map 输出参数,映射关系结构体 * @return 错误码 */ -MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map); +MenuErrCode menu_modbus_map_query_by_reg(MenuGlobalCtx* ctx, ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map); /** * @brief 将参数值写入对应的Modbus寄存器(双向转换:参数值→寄存器数据) + * @param ctx 菜单上下文指针 * @param param_id 参数ID * @param reg_buf 输出参数,寄存器数据缓冲区(需足够大,如32位占2个16位寄存器) * @param buf_len 缓冲区长度(输入),实际写入长度(输出) * @return 错误码 */ -MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len); +MenuErrCode menu_modbus_map_param_to_reg(MenuGlobalCtx* ctx, uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len); /** * @brief 将Modbus寄存器数据读取到参数值(双向转换:寄存器数据→参数值) + * @param ctx 菜单上下文指针 * @param param_id 参数ID * @param reg_buf 输入参数,寄存器数据缓冲区 * @param buf_len 缓冲区长度 * @return 错误码 */ -MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len); +MenuErrCode menu_modbus_map_reg_to_param(MenuGlobalCtx* ctx, uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len); #endif // MENU_CONFIG_ENABLE_MODBUS_MAP diff --git a/api/menu_config.h b/api/menu_config.h index b5684a5..b8e11e7 100644 --- a/api/menu_config.h +++ b/api/menu_config.h @@ -6,6 +6,9 @@ #ifndef MENU_CONFIG_H #define MENU_CONFIG_H +#include +#include + /************************** 核心配置 **************************/ /** * @brief 最大菜单节点数(静态内存,根据项目调整) @@ -30,7 +33,26 @@ /** * @brief 是否启用调试打印(调试时开启,发布时关闭) */ -#define MENU_CONFIG_ENABLE_DEBUG 1 +#define MENU_CONFIG_ENABLE_DEBUG 0 + +/** + * @brief 是否启用内存监控(调试时开启,发布时关闭) + */ +#define MENU_CONFIG_ENABLE_MEM_MONITOR 1 + +/** + * @brief 内存使用统计结构体 + */ +typedef struct { + uint16_t total_nodes; ///< 总菜单节点数 + uint16_t used_nodes; ///< 已使用菜单节点数 + uint16_t total_params; ///< 总参数数 + uint16_t used_params; ///< 已使用参数数 + uint16_t total_modbus_maps; ///< 总Modbus映射数 + uint16_t used_modbus_maps; ///< 已使用Modbus映射数 + uint16_t event_queue_len; ///< 事件队列长度 + uint16_t event_queue_used; ///< 事件队列已使用长度 +} MenuMemStats; /************************** 功能扩展配置(可裁剪) **************************/ /** diff --git a/examples/menu_example.c b/examples/menu_example.c index 6e60204..5c18fcd 100644 --- a/examples/menu_example.c +++ b/examples/menu_example.c @@ -4,6 +4,7 @@ * @note 工业级嵌入式菜单组件 - 使用示例 */ +#include #include "menu.h" #include "menu_port.h" #include "menu_def.h" @@ -31,6 +32,10 @@ // 模拟Modbus寄存器缓冲区 uint8_t g_modbus_reg_buf[256] = {0}; +// 菜单上下文 +static uint8_t* menu_ctx_buf = NULL; +static MenuGlobalCtx* g_menu_ctx = NULL; + /************************** 菜单回调函数 **************************/ /** * @brief 进入参数设置菜单回调 @@ -39,6 +44,7 @@ uint8_t g_modbus_reg_buf[256] = {0}; */ static MenuErrCode menu_cb_enter_param(MenuNodeId node_id) { + (void)node_id; // Suppress unused parameter warning when debug is disabled MENU_DEBUG("Enter param menu: %d", node_id); return MENU_OK; } @@ -50,6 +56,7 @@ static MenuErrCode menu_cb_enter_param(MenuNodeId node_id) */ static MenuErrCode menu_cb_exit_param(MenuNodeId node_id) { + (void)node_id; // Suppress unused parameter warning when debug is disabled MENU_DEBUG("Exit param menu: %d", node_id); return MENU_OK; } @@ -61,44 +68,49 @@ static MenuErrCode menu_cb_exit_param(MenuNodeId node_id) */ static MenuErrCode menu_cb_enter_modbus(MenuNodeId node_id) { - MENU_DEBUG("Enter Modbus menu: %d", node_id); + (void)node_id; // Suppress unused parameter warning when debug is disabled + MENU_DEBUG("Enter modbus menu: %d", node_id); return MENU_OK; } - - +/************************** Modbus通信演示函数 **************************/ /** - * @brief 模拟Modbus通信流程 + * @brief 模拟Modbus通信演示 */ static void modbus_communication_demo(void) { - #if MENU_CONFIG_ENABLE_MODBUS_MAP - MENU_DEBUG("=== Modbus Communication Demo ==="); + MENU_DEBUG("=== Modbus Communication Demo Start ==="); - // 1. 模拟参数到寄存器的转换(参数修改后更新寄存器) - { + #if MENU_CONFIG_ENABLE_MODBUS_MAP + + // 1. 模拟参数到寄存器的转换(参数值更新后同步到寄存器) + { MENU_DEBUG("1. Param to Reg Demo:"); - // 设置温度参数为28.5 - menu_param_set_value(PARAM_TEMP_ID, 28.5f); - - // 将温度参数转换到Modbus寄存器 - uint8_t buf_len = 4; - uint8_t reg_buf[4] = {0}; - MenuErrCode err = menu_modbus_map_param_to_reg(PARAM_TEMP_ID, reg_buf, &buf_len); + // 模拟温度参数变化(如从传感器读取到新的温度值) + float new_temp = 30.5f; + MenuErrCode err = menu_param_set_value(g_menu_ctx, PARAM_TEMP_ID, new_temp); if (err == MENU_OK) { - MENU_DEBUG(" Temp param (28.5f) to reg: [0x%02X, 0x%02X, 0x%02X, 0x%02X]", - reg_buf[0], reg_buf[1], reg_buf[2], reg_buf[3]); - } - else - { - MENU_DEBUG(" Param to reg failed: %d", err); + // 将参数值转换到Modbus寄存器缓冲区 + uint8_t buf_len = 4; + uint8_t reg_buf[4] = {0}; + + err = menu_modbus_map_param_to_reg(g_menu_ctx, PARAM_TEMP_ID, reg_buf, &buf_len); + if (err == MENU_OK) + { + MENU_DEBUG(" Temp param %.1f°C to reg data: [0x%02X, 0x%02X, 0x%02X, 0x%02X]", + new_temp, reg_buf[0], reg_buf[1], reg_buf[2], reg_buf[3]); + } + else + { + MENU_DEBUG(" Param to reg failed: %d", err); + } } } // 2. 模拟寄存器到参数的转换(接收到Modbus写入请求后更新参数) - { + { MENU_DEBUG("2. Reg to Param Demo:"); // 模拟Modbus写入湿度参数(65.0%) @@ -106,12 +118,12 @@ static void modbus_communication_demo(void) uint8_t reg_buf[4] = {0x82, 0x42, 0x00, 0x00}; // 将Modbus寄存器数据转换为参数值 - MenuErrCode err = menu_modbus_map_reg_to_param(PARAM_HUMID_ID, reg_buf, sizeof(reg_buf)); + MenuErrCode err = menu_modbus_map_reg_to_param(g_menu_ctx, PARAM_HUMID_ID, reg_buf, sizeof(reg_buf)); if (err == MENU_OK) { // 读取转换后的参数值 float humid_val; - menu_param_get_value(PARAM_HUMID_ID, &humid_val); + menu_param_get_value(g_menu_ctx, PARAM_HUMID_ID, &humid_val); MENU_DEBUG(" Reg data [0x82, 0x42, 0x00, 0x00] to humid param: %.1f%%", humid_val); } else @@ -121,7 +133,7 @@ static void modbus_communication_demo(void) } // 3. 模拟Modbus读取请求处理 - { + { MENU_DEBUG("3. Modbus Read Request Demo:"); // 模拟Modbus主机读取压力参数 @@ -129,7 +141,7 @@ static void modbus_communication_demo(void) uint8_t reg_buf[4] = {0}; // 这里应该先调用menu_modbus_map_param_to_reg将参数转换到寄存器缓冲区 - MenuErrCode err = menu_modbus_map_param_to_reg(PARAM_PRESS_ID, reg_buf, &buf_len); + MenuErrCode err = menu_modbus_map_param_to_reg(g_menu_ctx, PARAM_PRESS_ID, reg_buf, &buf_len); if (err == MENU_OK) { // 然后返回给Modbus主机 @@ -143,7 +155,7 @@ static void modbus_communication_demo(void) } // 4. 模拟Modbus写入请求处理 - { + { MENU_DEBUG("4. Modbus Write Request Demo:"); // 模拟Modbus主机写入温度参数(30.0°C) @@ -151,12 +163,12 @@ static void modbus_communication_demo(void) uint8_t reg_buf[4] = {0xF0, 0x41, 0x00, 0x00}; // 调用menu_modbus_map_reg_to_param将寄存器数据转换为参数值 - MenuErrCode err = menu_modbus_map_reg_to_param(PARAM_TEMP_ID, reg_buf, sizeof(reg_buf)); + MenuErrCode err = menu_modbus_map_reg_to_param(g_menu_ctx, PARAM_TEMP_ID, reg_buf, sizeof(reg_buf)); if (err == MENU_OK) { // 读取转换后的参数值 float temp_val; - menu_param_get_value(PARAM_TEMP_ID, &temp_val); + menu_param_get_value(g_menu_ctx, PARAM_TEMP_ID, &temp_val); MENU_DEBUG(" Write temp reg: [0xF0, 0x41, 0x00, 0x00] -> temp param: %.1f°C", temp_val); } else @@ -181,10 +193,22 @@ int main(void) /************************** 2. 初始化菜单组件 **************************/ MENU_DEBUG("2. Initialize menu component..."); - MenuErrCode err = menu_init(); + + // 动态分配菜单上下文缓冲区 + menu_ctx_buf = (uint8_t*)malloc(menu_get_ctx_size()); + if (menu_ctx_buf == NULL) + { + MENU_DEBUG(" Failed to allocate menu context buffer"); + return -1; + } + + // 初始化菜单上下文 + g_menu_ctx = (MenuGlobalCtx*)menu_ctx_buf; + MenuErrCode err = menu_init(g_menu_ctx); if (err != MENU_OK) { MENU_DEBUG(" Menu init failed: %d", err); + free(menu_ctx_buf); return -1; } @@ -192,7 +216,7 @@ int main(void) MENU_DEBUG("3. Register menu nodes..."); // 注册根节点 - err = menu_register_node(0, MENU_ROOT_ID, "主菜单", NULL, NULL); + err = menu_register_node(g_menu_ctx, 0, MENU_ROOT_ID, "主菜单", NULL, NULL); if (err != MENU_OK) { MENU_DEBUG(" Register root node failed: %d", err); @@ -200,7 +224,7 @@ int main(void) } // 注册参数设置节点 - err = menu_register_node(MENU_ROOT_ID, MENU_PARAM_ID, "参数设置", menu_cb_enter_param, menu_cb_exit_param); + err = menu_register_node(g_menu_ctx, MENU_ROOT_ID, MENU_PARAM_ID, "参数设置", menu_cb_enter_param, menu_cb_exit_param); if (err != MENU_OK) { MENU_DEBUG(" Register param node failed: %d", err); @@ -208,7 +232,7 @@ int main(void) } // 注册Modbus设置节点 - err = menu_register_node(MENU_ROOT_ID, MENU_MODBUS_ID, "Modbus设置", menu_cb_enter_modbus, NULL); + err = menu_register_node(g_menu_ctx, MENU_ROOT_ID, MENU_MODBUS_ID, "Modbus设置", menu_cb_enter_modbus, NULL); if (err != MENU_OK) { MENU_DEBUG(" Register modbus node failed: %d", err); @@ -216,7 +240,7 @@ int main(void) } // 注册温度参数节点 - err = menu_register_node(MENU_PARAM_ID, MENU_TEMP_ID, "温度设置", NULL, NULL); + err = menu_register_node(g_menu_ctx, MENU_PARAM_ID, MENU_TEMP_ID, "温度设置", NULL, NULL); if (err != MENU_OK) { MENU_DEBUG(" Register temp node failed: %d", err); @@ -224,7 +248,7 @@ int main(void) } // 注册湿度参数节点 - err = menu_register_node(MENU_PARAM_ID, MENU_HUMID_ID, "湿度设置", NULL, NULL); + err = menu_register_node(g_menu_ctx, MENU_PARAM_ID, MENU_HUMID_ID, "湿度设置", NULL, NULL); if (err != MENU_OK) { MENU_DEBUG(" Register humid node failed: %d", err); @@ -232,7 +256,7 @@ int main(void) } // 注册压力参数节点 - err = menu_register_node(MENU_PARAM_ID, MENU_PRESS_ID, "压力设置", NULL, NULL); + err = menu_register_node(g_menu_ctx, MENU_PARAM_ID, MENU_PRESS_ID, "压力设置", NULL, NULL); if (err != MENU_OK) { MENU_DEBUG(" Register press node failed: %d", err); @@ -244,7 +268,7 @@ int main(void) #if MENU_CONFIG_ENABLE_PARAM // 注册温度参数(范围:-40.0°C ~ 120.0°C,默认25.0°C,精度0.1°C) - err = menu_param_register(MENU_TEMP_ID, PARAM_TEMP_ID, MENU_PARAM_TYPE_FLOAT, + err = menu_param_register(g_menu_ctx, MENU_TEMP_ID, PARAM_TEMP_ID, MENU_PARAM_TYPE_FLOAT, -40.0f, 120.0f, 25.0f, 0.1f); if (err != MENU_OK) { @@ -253,7 +277,7 @@ int main(void) } // 注册湿度参数(范围:0.0% ~ 100.0%,默认50.0%,精度0.1%) - err = menu_param_register(MENU_HUMID_ID, PARAM_HUMID_ID, MENU_PARAM_TYPE_FLOAT, + err = menu_param_register(g_menu_ctx, MENU_HUMID_ID, PARAM_HUMID_ID, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 50.0f, 0.1f); if (err != MENU_OK) { @@ -262,7 +286,7 @@ int main(void) } // 注册压力参数(范围:0.0kPa ~ 1000.0kPa,默认101.3kPa,精度0.1kPa) - err = menu_param_register(MENU_PRESS_ID, PARAM_PRESS_ID, MENU_PARAM_TYPE_FLOAT, + err = menu_param_register(g_menu_ctx, MENU_PRESS_ID, PARAM_PRESS_ID, MENU_PARAM_TYPE_FLOAT, 0.0f, 1000.0f, 101.3f, 0.1f); if (err != MENU_OK) { @@ -274,8 +298,10 @@ int main(void) /************************** 5. 注册Modbus映射 **************************/ MENU_DEBUG("5. Register Modbus mappings..."); + #if MENU_CONFIG_ENABLE_MODBUS_MAP // 注册温度参数到Modbus寄存器映射 err = menu_modbus_map_register( + g_menu_ctx, PARAM_TEMP_ID, // 参数ID MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型 REG_TEMP_ADDR, // 起始地址 @@ -291,6 +317,7 @@ int main(void) // 注册湿度参数到Modbus寄存器映射 err = menu_modbus_map_register( + g_menu_ctx, PARAM_HUMID_ID, // 参数ID MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型 REG_HUMID_ADDR, // 起始地址 @@ -306,6 +333,7 @@ int main(void) // 注册压力参数到Modbus寄存器映射 err = menu_modbus_map_register( + g_menu_ctx, PARAM_PRESS_ID, // 参数ID MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型 REG_PRESS_ADDR, // 起始地址 @@ -318,6 +346,7 @@ int main(void) MENU_DEBUG(" Register press modbus map failed: %d", err); return -1; } + #endif /************************** 6. 运行Modbus通信演示 **************************/ MENU_DEBUG("6. Run Modbus communication demo..."); @@ -328,14 +357,17 @@ int main(void) while (1) { // 处理菜单事件和刷新显示 - menu_main_loop(); + menu_main_loop(g_menu_ctx); // 其他业务逻辑 // ... // 模拟延时 - menu_port_delay_ms(10); + // menu_port_delay_ms(10); } + // Free the menu context buffer + free(menu_ctx_buf); + return 0; -} +} \ No newline at end of file diff --git a/internal/menu_core.h b/internal/menu_core.h index db150b0..dacfd36 100644 --- a/internal/menu_core.h +++ b/internal/menu_core.h @@ -14,9 +14,10 @@ * @brief 菜单事件结构体(事件队列元素) */ typedef struct { - MenuEventType type; ///< 事件类型 - uint32_t param; ///< 事件附加参数 - uint32_t timestamp; ///< 事件产生时间(ms,用于超时处理) + MenuEventType type; ///< 事件类型 + uint32_t param; ///< 事件附加参数 + uint32_t timestamp; ///< 事件产生时间(ms,用于超时处理) + MenuEventPriority priority; ///< 事件优先级 } MenuEvent; /** @@ -61,6 +62,27 @@ typedef struct { uint8_t count; ///< 队列元素数量 } MenuEventQueue; +/** + * @brief 菜单状态枚举(状态机核心) + */ +typedef enum { + MENU_STATE_INIT = 0, ///< 初始化状态 + MENU_STATE_NORMAL, ///< 正常导航状态 + MENU_STATE_PARAM_EDIT, ///< 参数编辑状态 + MENU_STATE_CONFIRM, ///< 确认状态(如保存/退出确认) + MENU_STATE_ERROR, ///< 错误状态 +} MenuState; + +/** + * @brief 菜单状态转换结构体 + */ +typedef struct { + MenuState current_state; ///< 当前状态 + MenuEventType event; ///< 触发事件 + MenuState next_state; ///< 下一个状态 + MenuErrCode (*action)(MenuGlobalCtx* global_ctx); ///< 状态转换动作 +} MenuStateTransition; + /** * @brief 菜单核心上下文(全局唯一,存储菜单状态) */ @@ -69,6 +91,7 @@ typedef struct { MenuStack stack; ///< 菜单导航栈 MenuEventQueue event_queue; ///< 事件队列 MenuNodeId current_node_id; ///< 当前选中的节点ID + MenuState current_state; ///< 当前菜单状态 uint32_t last_refresh_tick; ///< 上次刷新时间(ms) bool is_initialized; ///< 是否已初始化 } MenuCoreCtx; @@ -76,34 +99,25 @@ typedef struct { /************************** 核心内部接口声明 **************************/ /** * @brief 查找菜单节点(通过ID) + * @param global_ctx 全局上下文指针 * @param node_id 节点ID * @return 节点指针(NULL表示未找到) */ -MenuNode* menu_core_find_node(MenuNodeId node_id); - -/** - * @brief 初始化菜单核心上下文 - * @return 错误码 - */ -MenuErrCode menu_core_ctx_init(void); - -/** - * @brief 获取菜单核心上下文(内部唯一访问入口) - * @return 菜单核心上下文指针 - */ -MenuCoreCtx* menu_core_get_ctx(void); +MenuNode* menu_core_find_node(MenuGlobalCtx* global_ctx, MenuNodeId node_id); /** * @brief 处理单个菜单事件 + * @param global_ctx 全局上下文指针 * @param event 事件指针 * @return 错误码 */ -MenuErrCode menu_core_handle_event(const MenuEvent* event); +MenuErrCode menu_core_handle_event(MenuGlobalCtx* global_ctx, const MenuEvent* event); /** * @brief 刷新菜单显示(内部调用,对接port层显示接口) + * @param global_ctx 全局上下文指针 */ -void menu_core_refresh_display(void); +void menu_core_refresh_display(MenuGlobalCtx* global_ctx); diff --git a/internal/menu_data.h b/internal/menu_data.h index 09b9e96..f8aacf3 100644 --- a/internal/menu_data.h +++ b/internal/menu_data.h @@ -5,68 +5,58 @@ #ifndef MENU_DATA_H #define MENU_DATA_H +#include "../api/menu.h" #include "menu_core.h" #include "menu_modbus.h" +#include "menu_param.h" -/************************** 共享全局变量(声明,内部可见) **************************/ +/************************** 共享上下文结构(替代全局变量) **************************/ /** - * @brief 菜单核心上下文(全局,仅内部访问) - */ -extern MenuCoreCtx s_menu_core_ctx; - -/** - * @brief 参数管理上下文(启用参数时有效,全局) + * @brief 参数管理上下文(启用参数时有效) */ #if MENU_CONFIG_ENABLE_PARAM typedef struct { - uint16_t id; ///< 参数ID - MenuParamType type; ///< 参数类型 - float min_val; ///< 最小值 - float max_val; ///< 最大值 - float scale; ///< 缩放因子 - union { ///< 共用体:减少内存占用 - int8_t i8; - uint8_t u8; - int16_t i16; - uint16_t u16; - int32_t i32; - uint32_t u32; - float f; - } value; ///< 当前值 - union { ///< 默认值 - int8_t i8; - uint8_t u8; - int16_t i16; - uint16_t u16; - int32_t i32; - uint32_t u32; - float f; - } default_val; - bool is_registered; ///< 是否已注册 -} MenuParam; - -extern MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS]; + MenuParam params[MENU_CONFIG_MAX_PARAMS]; ///< 参数数组 + uint16_t count; ///< 已注册参数数量 +} MenuParamCtx; #endif // MENU_CONFIG_ENABLE_PARAM /** - * @brief 多语言上下文(启用多语言时有效,全局) + * @brief 多语言上下文(启用多语言时有效) */ #if MENU_CONFIG_ENABLE_LANG typedef struct { - const char* str; ///< 语言字符串 - uint16_t str_id; ///< 字符串ID - uint8_t lang_id; ///< 语言ID -} MenuLangStr; - -extern MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS]; -extern uint8_t s_current_lang_id; + MenuLangStr strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS]; ///< 语言字符串数组 + uint8_t count; ///< 已注册字符串数量 + uint8_t current_lang_id; ///< 当前语言ID +} MenuLangCtx; #endif // MENU_CONFIG_ENABLE_LANG /** - * @brief Modbus映射上下文(启用Modbus映射时有效,全局) + * @brief Modbus映射上下文(启用Modbus映射时有效) */ #if MENU_CONFIG_ENABLE_MODBUS_MAP -extern ModbusMapInternal s_menu_modbus_maps[MENU_CONFIG_MAX_MODBUS_MAPS]; +typedef struct { + ModbusMapInternal maps[MENU_CONFIG_MAX_MODBUS_MAPS]; ///< Modbus映射数组 + uint16_t count; ///< 已注册映射数量 +} MenuModbusCtx; #endif // MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief 菜单组件全局上下文(替代所有全局变量) + */ +struct MenuGlobalCtx { + MenuCoreCtx core; ///< 核心上下文 +#if MENU_CONFIG_ENABLE_PARAM + MenuParamCtx param; ///< 参数上下文 +#endif +#if MENU_CONFIG_ENABLE_LANG + MenuLangCtx lang; ///< 多语言上下文 +#endif +#if MENU_CONFIG_ENABLE_MODBUS_MAP + MenuModbusCtx modbus; ///< Modbus映射上下文 +#endif + bool is_initialized; ///< 是否已初始化 +}; + #endif // MENU_DATA_H \ No newline at end of file diff --git a/internal/menu_def.h b/internal/menu_def.h index feddd64..3d91a60 100644 --- a/internal/menu_def.h +++ b/internal/menu_def.h @@ -26,13 +26,32 @@ #define MENU_ASSERT(condition) ((void)0) #endif +/** + * @brief 日志级别定义 + */ +typedef enum { + MENU_LOG_LEVEL_DEBUG, ///< 调试信息 + MENU_LOG_LEVEL_INFO, ///< 普通信息 + MENU_LOG_LEVEL_WARN, ///< 警告信息 + MENU_LOG_LEVEL_ERROR, ///< 错误信息 + MENU_LOG_LEVEL_CRITICAL, ///< 严重错误 +} MenuLogLevel; + /** * @brief 调试打印宏(工业级:集中控制调试输出) */ #if MENU_CONFIG_ENABLE_DEBUG -#define MENU_DEBUG(fmt, ...) menu_utils_printf("[MENU DEBUG] " fmt "\r\n", ##__VA_ARGS__) +#define MENU_DEBUG(fmt, ...) menu_utils_log(MENU_LOG_LEVEL_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__) +#define MENU_INFO(fmt, ...) menu_utils_log(MENU_LOG_LEVEL_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__) +#define MENU_WARN(fmt, ...) menu_utils_log(MENU_LOG_LEVEL_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__) +#define MENU_ERROR(fmt, ...) menu_utils_log(MENU_LOG_LEVEL_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__) +#define MENU_CRITICAL(fmt, ...) menu_utils_log(MENU_LOG_LEVEL_CRITICAL, __FILE__, __LINE__, fmt, ##__VA_ARGS__) #else #define MENU_DEBUG(fmt, ...) ((void)0) +#define MENU_INFO(fmt, ...) ((void)0) +#define MENU_WARN(fmt, ...) ((void)0) +#define MENU_ERROR(fmt, ...) ((void)0) +#define MENU_CRITICAL(fmt, ...) ((void)0) #endif /** @@ -45,6 +64,18 @@ */ #define MENU_INVALID_ID ((MenuNodeId)0xFFFF) +/** + * @brief 错误处理宏(简化错误检查和返回) + */ +#define MENU_CHECK_ERR(expr) \ + do { \ + MenuErrCode err = (expr); \ + if (err != MENU_OK) { \ + MENU_ERROR("Error at %s:%d: %s", __FILE__, __LINE__, menu_err_code_to_str(err)); \ + return err; \ + } \ + } while (0) + /************************** 内部辅助函数声明 **************************/ /** * @brief 断言失败处理函数 @@ -54,11 +85,14 @@ void menu_utils_assert_failed(const char* file, uint32_t line); /** - * @brief 调试打印函数(对接port层的硬件打印接口) + * @brief 通用日志函数(对接port层的硬件打印接口) + * @param level 日志级别 + * @param file 文件名 + * @param line 行号 * @param fmt 格式化字符串 * @param ... 可变参数 */ -void menu_utils_printf(const char* fmt, ...); +void menu_utils_log(MenuLogLevel level, const char* file, uint32_t line, const char* fmt, ...); /** * @brief 获取系统滴答时间(ms,对接port层) @@ -66,4 +100,11 @@ void menu_utils_printf(const char* fmt, ...); */ uint32_t menu_utils_get_tick(void); +/** + * @brief 错误码转换为字符串 + * @param err_code 错误码 + * @return 错误信息字符串 + */ +const char* menu_err_code_to_str(MenuErrCode err_code); + #endif // MENU_DEF_H \ No newline at end of file diff --git a/port/menu_port.c b/port/menu_port.c index 83d3e11..cfbb359 100644 --- a/port/menu_port.c +++ b/port/menu_port.c @@ -1,139 +1,288 @@ /** * @file menu_port.c - * @brief 菜单组件硬件端口层示例实现(用户需要根据实际硬件修改) - * @note 工业级嵌入式菜单组件 - 硬件端口层示例 + * @brief 菜单组件硬件端口层实现(模块化设计,支持运行时配置) + * @note 工业级嵌入式菜单组件 - 硬件端口层 * @attention 用户需要根据自己的硬件平台修改此文件 */ #include "menu_port.h" +#include "menu.h" #include #include #include #include +/************************** 静态全局变量 **************************/ +static const MenuPortDriver* g_current_driver = NULL; + +/************************** 默认驱动实现 **************************/ + /** - * @brief 硬件打印接口示例实现(使用标准C库printf) + * @brief 默认硬件打印接口实现(使用标准C库printf) * @param fmt 格式化字符串 * @param args 可变参数列表 - * @note 实际项目中,用户需要将此接口重定向到硬件的串口、LCD等输出设备 */ -void menu_port_printf(const char* fmt, va_list args) +static void default_printf(const char* fmt, va_list args) { - // 示例:使用标准C库的vprintf函数 - // 实际项目中,这里应该调用硬件相关的打印函数,如串口打印 vprintf(fmt, args); } /** - * @brief 获取系统滴答时间示例实现(使用标准C库time函数) + * @brief 默认获取系统滴答时间实现(使用标准C库time函数) * @return 当前系统滴答时间(ms) - * @note 实际项目中,用户需要将此接口实现为硬件定时器的计数值 */ -uint32_t menu_port_get_tick(void) +static uint32_t default_get_tick(void) { - // 示例:使用标准C库的time函数(仅用于演示,精度低) - // 实际项目中,这里应该返回硬件定时器的计数值,如SysTick return (uint32_t)time(NULL) * 1000; } /** - * @brief 硬件延迟函数示例实现(使用标准C库sleep函数) + * @brief 默认硬件延迟函数实现(使用标准C库sleep函数) * @param ms 延迟时间(ms) - * @note 实际项目中,用户需要将此接口实现为硬件定时器的延迟 */ -void menu_port_delay_ms(uint32_t ms) +static void default_delay_ms(uint32_t ms) { - // 示例:使用标准C库的sleep函数(仅用于演示,精度低) - // 实际项目中,这里应该使用硬件定时器实现精确延迟 sleep(ms / 1000); usleep((ms % 1000) * 1000); } /** - * @brief 菜单显示接口示例实现(使用标准C库printf) + * @brief 默认菜单显示接口实现(使用标准C库printf) * @param menu_name 菜单名称字符串 * @param menu_id 菜单ID - * @note 实际项目中,用户需要将此接口实现为在LCD、OLED等显示设备上的显示 */ -void menu_port_display(const char* menu_name, uint16_t menu_id) +static void default_display(const char* menu_name, uint16_t menu_id) { - // 示例:使用标准C库的printf函数在控制台显示 - // 实际项目中,这里应该调用硬件显示函数,将菜单显示在LCD等设备上 printf("\r\nCurrent Menu: %s (ID: %d)\r\n", menu_name, menu_id); printf("Use UP/DOWN keys to navigate, ENTER to select, BACK to return\r\n"); } /** - * @brief 按键扫描接口示例实现(使用标准C库getchar函数) - * @return 当前按键事件(MenuEventType类型) - * @note 实际项目中,用户需要将此接口实现为硬件按键的扫描 + * @brief 默认按键扫描接口实现(无按键返回) + * @return 当前按键事件 */ -/* -MenuEventType menu_port_key_scan(void) +static MenuEventType default_key_scan(void) { - // 示例:使用标准C库的getchar函数获取键盘输入(仅用于演示) - // 实际项目中,这里应该扫描硬件按键的状态 - if (kbhit()) - { - char key = getchar(); - switch (key) - { - case 'w': - case 'W': - return MENU_EVENT_KEY_UP; - case 's': - case 'S': - return MENU_EVENT_KEY_DOWN; - case '\r': - case '\n': - return MENU_EVENT_KEY_ENTER; - case 'b': - case 'B': - return MENU_EVENT_KEY_BACK; - default: - break; - } - } return MENU_EVENT_NONE; } -*/ + +/** + * @brief 默认中断管理接口实现(空操作) + * @param enable 中断使能标志 + */ +static void default_irq_ctrl(bool enable) +{ + (void)enable; + // 默认实现为空,用户需根据硬件平台实现 +} + +/** + * @brief 默认错误处理接口实现(打印错误信息) + * @param err_code 错误码 + */ +static void default_error_handler(MenuErrCode err_code) +{ + fprintf(stderr, "[MENU HW ERROR] %d\r\n", err_code); +} #if MENU_CONFIG_ENABLE_MODBUS_MAP /** - * @brief Modbus硬件发送数据接口示例实现 + * @brief 默认Modbus硬件发送数据接口实现(模拟成功) * @param reg_type 寄存器类型 * @param reg_addr 寄存器地址 * @param reg_buf 数据缓冲区 * @param buf_len 数据长度 * @return 错误码 - * @note 用户需要根据实际硬件(如RS485、TCP)实现此接口 */ -MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len) +static MenuErrCode default_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len) { - // 示例:通过RS485发送Modbus写指令(如写保持寄存器0x0000(40001)) - // 实际需根据Modbus协议实现帧组装、校验、发送 (void)reg_type; (void)reg_addr; (void)reg_buf; (void)buf_len; - return MENU_OK; // 模拟成功,用户需替换为实际逻辑 + return MENU_OK; } /** - * @brief Modbus硬件接收数据接口示例实现 + * @brief 默认Modbus硬件接收数据接口实现(模拟成功) * @param reg_type 寄存器类型 * @param reg_addr 寄存器地址 * @param reg_buf 数据缓冲区 * @param buf_len 数据长度 * @return 错误码 - * @note 用户需要根据实际硬件(如RS485、TCP)实现此接口 */ -MenuErrCode menu_port_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len) +static MenuErrCode default_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len) { - // 示例:通过RS485接收Modbus读指令的响应数据 (void)reg_type; (void)reg_addr; (void)reg_buf; (void)buf_len; - return MENU_OK; // 模拟成功,用户需替换为实际逻辑 + return MENU_OK; +} +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP + +/** + * @brief 默认硬件驱动结构体(提供基本实现,减少用户工作量) + */ +const MenuPortDriver g_menu_default_driver = { + .printf = default_printf, + .get_tick = default_get_tick, + .delay_ms = default_delay_ms, + .display = default_display, + .key_scan = default_key_scan, + .irq_ctrl = default_irq_ctrl, + .error_handler = default_error_handler, +#if MENU_CONFIG_ENABLE_MODBUS_MAP + .modbus_send = default_modbus_send, + .modbus_receive = default_modbus_receive, +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +}; + +/************************** 端口管理函数 **************************/ + +/** + * @brief 硬件端口初始化接口 + * @param driver 硬件驱动结构体指针 + * @return 错误码 + */ +MenuErrCode menu_port_init(const MenuPortDriver* driver) +{ + if (driver == NULL) + { + // 使用默认驱动 + g_current_driver = &g_menu_default_driver; + } + else + { + // 使用用户提供的驱动,替换为默认驱动中未实现的部分 + g_current_driver = driver; + } + + return MENU_OK; +} + +/** + * @brief 硬件端口反初始化接口 + * @return 错误码 + */ +MenuErrCode menu_port_deinit(void) +{ + g_current_driver = NULL; + return MENU_OK; +} + +/** + * @brief 获取当前硬件驱动 + * @return 当前硬件驱动结构体指针 + */ +const MenuPortDriver* menu_port_get_driver(void) +{ + if (g_current_driver == NULL) + { + // 确保始终有驱动可用 + return &g_menu_default_driver; + } + return g_current_driver; +} + +/************************** 兼容旧接口的实现 **************************/ + +/** + * @brief 硬件打印接口(兼容旧接口) + * @param fmt 格式化字符串 + * @param args 可变参数列表 + */ +void menu_port_printf(const char* fmt, va_list args) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + if (driver->printf != NULL) + { + driver->printf(fmt, args); + } +} + +/** + * @brief 获取系统滴答时间(兼容旧接口) + * @return 当前系统滴答时间(ms) + */ +uint32_t menu_port_get_tick(void) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + if (driver->get_tick != NULL) + { + return driver->get_tick(); + } + return 0; +} + +/** + * @brief 硬件延迟函数(兼容旧接口) + * @param ms 延迟时间(ms) + */ +void menu_port_delay_ms(uint32_t ms) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + if (driver->delay_ms != NULL) + { + driver->delay_ms(ms); + } +} + +/** + * @brief 菜单显示接口(兼容旧接口) + * @param menu_name 菜单名称字符串 + * @param menu_id 菜单ID + */ +void menu_port_display(const char* menu_name, uint16_t menu_id) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + if (driver->display != NULL) + { + driver->display(menu_name, menu_id); + } +} + +#if MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief Modbus硬件发送数据接口(兼容旧接口) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ +MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + + #if MENU_CONFIG_ENABLE_MODBUS_MAP + if (driver->modbus_send != NULL) + { + return driver->modbus_send(reg_type, reg_addr, reg_buf, buf_len); + } + #endif // MENU_CONFIG_ENABLE_MODBUS_MAP + + return MENU_OK; +} + +/** + * @brief Modbus硬件接收数据接口(兼容旧接口) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ +MenuErrCode menu_port_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len) +{ + const MenuPortDriver* driver = menu_port_get_driver(); + + #if MENU_CONFIG_ENABLE_MODBUS_MAP + if (driver->modbus_receive != NULL) + { + return driver->modbus_receive(reg_type, reg_addr, reg_buf, buf_len); + } + #endif // MENU_CONFIG_ENABLE_MODBUS_MAP + + return MENU_OK; } #endif // MENU_CONFIG_ENABLE_MODBUS_MAP \ No newline at end of file diff --git a/port/menu_port.h b/port/menu_port.h index 965700e..0ebcf53 100644 --- a/port/menu_port.h +++ b/port/menu_port.h @@ -9,66 +9,139 @@ #include #include +// 包含菜单核心类型定义 +#include "menu.h" + /************************** 硬件端口层接口声明 **************************/ /** - * @brief 硬件打印接口(可变参数版本) + * @brief 硬件端口驱动结构体(模块化设计,支持运行时配置) + */ +typedef struct { + /** + * @brief 硬件打印接口(可变参数版本) + * @param fmt 格式化字符串 + * @param args 可变参数列表 + * @note 用户需要实现此接口,用于调试信息输出 + */ + void (*printf)(const char* fmt, va_list args); + + /** + * @brief 获取系统滴答时间(毫秒级) + * @return 当前系统滴答时间(ms) + * @note 用户需要实现此接口,用于菜单的定时和超时处理 + */ + uint32_t (*get_tick)(void); + + /** + * @brief 硬件延迟函数(毫秒级) + * @param ms 延迟时间(ms) + * @note 用户需要实现此接口,用于必要的延迟操作 + */ + void (*delay_ms)(uint32_t ms); + + /** + * @brief 菜单显示接口 + * @param menu_name 菜单名称字符串 + * @param menu_id 菜单ID + * @note 用户需要实现此接口,用于在硬件上显示菜单内容 + */ + void (*display)(const char* menu_name, uint16_t menu_id); + + /** + * @brief 按键扫描接口(可选) + * @return 当前按键事件(MenuEventType类型) + * @note 用户可以实现此接口,也可以在自己的主循环中调用menu_post_event发送按键事件 + */ + MenuEventType (*key_scan)(void); + + /** + * @brief 中断管理接口(可选) + * @param enable 中断使能标志 + * @note 用户需要实现此接口,用于管理全局中断 + */ + void (*irq_ctrl)(bool enable); + + /** + * @brief 错误处理接口(可选) + * @param err_code 错误码 + * @note 用户可以实现此接口,用于处理硬件错误 + */ + void (*error_handler)(MenuErrCode err_code); + + #if MENU_CONFIG_ENABLE_MODBUS_MAP + /** + * @brief Modbus硬件发送数据接口(可选,如RS485发送) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ + MenuErrCode (*modbus_send)(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len); + + /** + * @brief Modbus硬件接收数据接口(可选,如RS485接收) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ + MenuErrCode (*modbus_receive)(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len); + #endif // MENU_CONFIG_ENABLE_MODBUS_MAP +} MenuPortDriver; + +/** + * @brief 硬件端口初始化接口 + * @param driver 硬件驱动结构体指针 + * @return 错误码 + * @note 用户需要实现此接口,用于初始化硬件设备 + */ +MenuErrCode menu_port_init(const MenuPortDriver* driver); + +/** + * @brief 硬件端口反初始化接口 + * @return 错误码 + * @note 用户需要实现此接口,用于释放硬件资源 + */ +MenuErrCode menu_port_deinit(void); + +/** + * @brief 获取当前硬件驱动 + * @return 当前硬件驱动结构体指针 + * @note 内部使用,获取当前配置的硬件驱动 + */ +const MenuPortDriver* menu_port_get_driver(void); + +/** + * @brief 硬件打印接口(兼容旧接口) * @param fmt 格式化字符串 * @param args 可变参数列表 - * @note 用户需要实现此接口,用于调试信息输出 */ void menu_port_printf(const char* fmt, va_list args); /** - * @brief 获取系统滴答时间(毫秒级) + * @brief 获取系统滴答时间(兼容旧接口) * @return 当前系统滴答时间(ms) - * @note 用户需要实现此接口,用于菜单的定时和超时处理 */ uint32_t menu_port_get_tick(void); /** - * @brief 硬件延迟函数(毫秒级) + * @brief 硬件延迟函数(兼容旧接口) * @param ms 延迟时间(ms) - * @note 用户需要实现此接口,用于必要的延迟操作 */ void menu_port_delay_ms(uint32_t ms); /** - * @brief 菜单显示接口 + * @brief 菜单显示接口(兼容旧接口) * @param menu_name 菜单名称字符串 * @param menu_id 菜单ID - * @note 用户需要实现此接口,用于在硬件上显示菜单内容 */ void menu_port_display(const char* menu_name, uint16_t menu_id); /** - * @brief 按键扫描接口(可选) - * @return 当前按键事件(MenuEventType类型) - * @note 用户可以实现此接口,也可以在自己的主循环中调用menu_post_event发送按键事件 + * @brief 默认硬件驱动结构体(提供基本实现,减少用户工作量) */ -// MenuEventType menu_port_key_scan(void); - -#include "menu.h" - -#if MENU_CONFIG_ENABLE_MODBUS_MAP -/** - * @brief Modbus硬件发送数据接口(用户需实现,如RS485发送) - * @param reg_type 寄存器类型 - * @param reg_addr 寄存器地址 - * @param reg_buf 数据缓冲区 - * @param buf_len 数据长度 - * @return 错误码 - */ -MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len); - -/** - * @brief Modbus硬件接收数据接口(用户需实现,如RS485接收) - * @param reg_type 寄存器类型 - * @param reg_addr 寄存器地址 - * @param reg_buf 数据缓冲区 - * @param buf_len 数据长度 - * @return 错误码 - */ -MenuErrCode menu_port_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len); -#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +extern const MenuPortDriver g_menu_default_driver; #endif // MENU_PORT_H \ No newline at end of file diff --git a/src/core/menu_core.c b/src/core/menu_core.c index 09b6bd3..39129d9 100644 --- a/src/core/menu_core.c +++ b/src/core/menu_core.c @@ -2,21 +2,59 @@ * @file menu_core.c * @brief 菜单核心逻辑:导航、栈管理、主循环(工业级:状态机驱动) */ +#include "../api/menu.h" #include "menu_core.h" #include "menu_data.h" #include -/************************** 全局变量定义 **************************/ -MenuCoreCtx s_menu_core_ctx; +/************************** 内部函数声明 **************************/ +static MenuErrCode menu_core_stack_push(MenuGlobalCtx* global_ctx, MenuNodeId node_id); +static MenuErrCode menu_core_stack_pop(MenuGlobalCtx* global_ctx, MenuNodeId* node_id); +static MenuErrCode menu_core_navigate_enter(MenuGlobalCtx* global_ctx, MenuNodeId node_id); +static MenuErrCode menu_core_navigate_back(MenuGlobalCtx* global_ctx); +static MenuErrCode menu_core_navigate_up(MenuGlobalCtx* global_ctx); +static MenuErrCode menu_core_navigate_down(MenuGlobalCtx* global_ctx); +static bool menu_core_get_event(MenuGlobalCtx* global_ctx, MenuEvent* event); + +/************************** 状态机相关函数声明 **************************/ +static MenuErrCode menu_state_action_normal_navigate(MenuGlobalCtx* global_ctx, MenuEventType event); +static MenuErrCode menu_state_action_param_edit(MenuGlobalCtx* global_ctx, MenuEventType event); +static MenuErrCode menu_state_action_confirm(MenuGlobalCtx* global_ctx, MenuEventType event); +static MenuErrCode menu_state_action_error(MenuGlobalCtx* global_ctx, MenuEventType event); +static MenuErrCode menu_state_transition(MenuGlobalCtx* global_ctx, MenuEventType event); + +/************************** 状态转换表定义 **************************/ +static const MenuStateTransition menu_state_transitions[] = { + // 当前状态 触发事件 下一个状态 转换动作 + { MENU_STATE_INIT, MENU_EVENT_NONE, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_NORMAL, MENU_EVENT_KEY_UP, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_NORMAL, MENU_EVENT_KEY_DOWN, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_NORMAL, MENU_EVENT_KEY_ENTER, MENU_STATE_PARAM_EDIT, NULL }, + { MENU_STATE_NORMAL, MENU_EVENT_KEY_BACK, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_PARAM_EDIT, MENU_EVENT_KEY_UP, MENU_STATE_PARAM_EDIT, NULL }, + { MENU_STATE_PARAM_EDIT, MENU_EVENT_KEY_DOWN, MENU_STATE_PARAM_EDIT, NULL }, + { MENU_STATE_PARAM_EDIT, MENU_EVENT_KEY_ENTER, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_PARAM_EDIT, MENU_EVENT_KEY_BACK, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_CONFIRM, MENU_EVENT_KEY_UP, MENU_STATE_CONFIRM, NULL }, + { MENU_STATE_CONFIRM, MENU_EVENT_KEY_DOWN, MENU_STATE_CONFIRM, NULL }, + { MENU_STATE_CONFIRM, MENU_EVENT_KEY_ENTER, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_CONFIRM, MENU_EVENT_KEY_BACK, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_ERROR, MENU_EVENT_KEY_ENTER, MENU_STATE_NORMAL, NULL }, + { MENU_STATE_ERROR, MENU_EVENT_KEY_BACK, MENU_STATE_NORMAL, NULL }, +}; + +static const uint8_t menu_state_transition_count = sizeof(menu_state_transitions) / sizeof(menu_state_transitions[0]); /** * @brief 查找菜单节点(通过ID) + * @param global_ctx 全局上下文指针 * @param node_id 节点ID * @return 节点指针(NULL表示未找到) */ -MenuNode* menu_core_find_node(MenuNodeId node_id) +MenuNode* menu_core_find_node(MenuGlobalCtx* global_ctx, MenuNodeId node_id) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuCoreCtx* ctx = &global_ctx->core; + for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++) { if (ctx->nodes[i].flags.is_registered && ctx->nodes[i].id == node_id) @@ -24,17 +62,19 @@ MenuNode* menu_core_find_node(MenuNodeId node_id) return &ctx->nodes[i]; } } + return NULL; } /** * @brief 菜单栈压入节点 + * @param global_ctx 全局上下文指针 * @param node_id 节点ID * @return 错误码 */ -static MenuErrCode menu_core_stack_push(MenuNodeId node_id) +static MenuErrCode menu_core_stack_push(MenuGlobalCtx* global_ctx, MenuNodeId node_id) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuCoreCtx* ctx = &global_ctx->core; MENU_ASSERT(ctx != NULL); if (ctx->stack.top >= MENU_CONFIG_STACK_DEPTH) @@ -50,12 +90,13 @@ static MenuErrCode menu_core_stack_push(MenuNodeId node_id) /** * @brief 菜单栈弹出节点 + * @param global_ctx 全局上下文指针 * @param node_id 输出参数,弹出的节点ID * @return 错误码 */ -static MenuErrCode menu_core_stack_pop(MenuNodeId* node_id) +static MenuErrCode menu_core_stack_pop(MenuGlobalCtx* global_ctx, MenuNodeId* node_id) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuCoreCtx* ctx = &global_ctx->core; MENU_ASSERT(ctx != NULL && node_id != NULL); if (ctx->stack.top == 0) @@ -71,12 +112,13 @@ static MenuErrCode menu_core_stack_pop(MenuNodeId* node_id) /** * @brief 菜单导航到子节点(确认键) + * @param global_ctx 全局上下文指针 * @param node_id 当前节点ID * @return 错误码 */ -static MenuErrCode menu_core_navigate_enter(MenuNodeId node_id) +static MenuErrCode menu_core_navigate_enter(MenuGlobalCtx* global_ctx, MenuNodeId node_id) { - MenuNode* node = menu_core_find_node(node_id); + MenuNode* node = menu_core_find_node(global_ctx, node_id); if (node == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -95,9 +137,9 @@ static MenuErrCode menu_core_navigate_enter(MenuNodeId node_id) // 如果有子节点,压入栈并选中第一个子节点 if (node->first_child != NULL) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuCoreCtx* ctx = &global_ctx->core; ctx->current_node_id = node->first_child->id; - return menu_core_stack_push(node->first_child->id); + return menu_core_stack_push(global_ctx, node->first_child->id); } return MENU_OK; @@ -105,13 +147,14 @@ static MenuErrCode menu_core_navigate_enter(MenuNodeId node_id) /** * @brief 菜单导航返回父节点(返回键) + * @param global_ctx 全局上下文指针 * @return 错误码 */ -static MenuErrCode menu_core_navigate_back(void) +static MenuErrCode menu_core_navigate_back(MenuGlobalCtx* global_ctx) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuCoreCtx* ctx = &global_ctx->core; MenuNodeId current_id = ctx->current_node_id; - MenuNode* current_node = menu_core_find_node(current_id); + MenuNode* current_node = menu_core_find_node(global_ctx, current_id); if (current_node == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -136,7 +179,7 @@ static MenuErrCode menu_core_navigate_back(void) } MenuNodeId pop_id; - MenuErrCode err = menu_core_stack_pop(&pop_id); + MenuErrCode err = menu_core_stack_pop(global_ctx, &pop_id); if (err != MENU_OK) { return err; @@ -148,12 +191,13 @@ static MenuErrCode menu_core_navigate_back(void) /** * @brief 菜单导航上选(上键) + * @param global_ctx 全局上下文指针 * @return 错误码 */ -static MenuErrCode menu_core_navigate_up(void) +static MenuErrCode menu_core_navigate_up(MenuGlobalCtx* global_ctx) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - MenuNode* current_node = menu_core_find_node(ctx->current_node_id); + MenuCoreCtx* ctx = &global_ctx->core; + MenuNode* current_node = menu_core_find_node(global_ctx, ctx->current_node_id); if (current_node == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -181,12 +225,13 @@ static MenuErrCode menu_core_navigate_up(void) /** * @brief 菜单导航下选(下键) + * @param global_ctx 全局上下文指针 * @return 错误码 */ -static MenuErrCode menu_core_navigate_down(void) +static MenuErrCode menu_core_navigate_down(MenuGlobalCtx* global_ctx) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - MenuNode* current_node = menu_core_find_node(ctx->current_node_id); + MenuCoreCtx* ctx = &global_ctx->core; + MenuNode* current_node = menu_core_find_node(global_ctx, ctx->current_node_id); if (current_node == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -200,7 +245,7 @@ static MenuErrCode menu_core_navigate_down(void) else { // 循环到第一个兄弟节点 - MenuNode* parent_node = menu_core_find_node(current_node->parent_id); + MenuNode* parent_node = menu_core_find_node(global_ctx, current_node->parent_id); if (parent_node != NULL && parent_node->first_child != NULL) { ctx->current_node_id = parent_node->first_child->id; @@ -212,69 +257,173 @@ static MenuErrCode menu_core_navigate_down(void) } /** - * @brief 初始化菜单核心上下文 - * @return 错误码 - */ -MenuErrCode menu_core_ctx_init(void) -{ - MenuCoreCtx* ctx = menu_core_get_ctx(); - MENU_MEM_SET_ZERO(ctx, sizeof(MenuCoreCtx)); - ctx->is_initialized = true; - return MENU_OK; -} - -/** - * @brief 获取菜单核心上下文(内部唯一访问入口) - * @return 菜单核心上下文指针 - */ -MenuCoreCtx* menu_core_get_ctx(void) -{ - return &s_menu_core_ctx; -} - -/** - * @brief 处理单个菜单事件 + * @brief 处理单个菜单事件(使用状态机) + * @param global_ctx 全局上下文指针 * @param event 事件指针 * @return 错误码 */ -MenuErrCode menu_core_handle_event(const MenuEvent* event) +MenuErrCode menu_core_handle_event(MenuGlobalCtx* global_ctx, const MenuEvent* event) { MENU_ASSERT(event != NULL); MenuErrCode err = MENU_OK; - switch (event->type) + + // 使用状态机处理事件 + err = menu_state_transition(global_ctx, event->type); + + return err; +} + +/** + * @brief 状态机转换处理函数 + * @param global_ctx 全局上下文指针 + * @param event 触发事件 + * @return 错误码 + */ +static MenuErrCode menu_state_transition(MenuGlobalCtx* global_ctx, MenuEventType event) +{ + MenuCoreCtx* ctx = &global_ctx->core; + MenuState current_state = ctx->current_state; + MenuErrCode err = MENU_OK; + + // 查找状态转换 + for (uint8_t i = 0; i < menu_state_transition_count; i++) + { + if (menu_state_transitions[i].current_state == current_state && + menu_state_transitions[i].event == event) + { + // 执行状态转换动作 + switch (current_state) + { + case MENU_STATE_NORMAL: + err = menu_state_action_normal_navigate(global_ctx, event); + break; + case MENU_STATE_PARAM_EDIT: + err = menu_state_action_param_edit(global_ctx, event); + break; + case MENU_STATE_CONFIRM: + err = menu_state_action_confirm(global_ctx, event); + break; + case MENU_STATE_ERROR: + err = menu_state_action_error(global_ctx, event); + break; + default: + break; + } + + // 更新状态 + ctx->current_state = menu_state_transitions[i].next_state; + MENU_DEBUG("State transition: %d -> %d, event: %d", + current_state, ctx->current_state, event); + break; + } + } + + return err; +} + +/** + * @brief 正常导航状态的动作处理 + * @param global_ctx 全局上下文指针 + * @param event 触发事件 + * @return 错误码 + */ +static MenuErrCode menu_state_action_normal_navigate(MenuGlobalCtx* global_ctx, MenuEventType event) +{ + MenuErrCode err = MENU_OK; + + switch (event) { case MENU_EVENT_KEY_UP: - err = menu_core_navigate_up(); + err = menu_core_navigate_up(global_ctx); break; case MENU_EVENT_KEY_DOWN: - err = menu_core_navigate_down(); + err = menu_core_navigate_down(global_ctx); break; case MENU_EVENT_KEY_ENTER: - err = menu_core_navigate_enter(menu_core_get_ctx()->current_node_id); + err = menu_core_navigate_enter(global_ctx, global_ctx->core.current_node_id); break; case MENU_EVENT_KEY_BACK: - err = menu_core_navigate_back(); - break; - case MENU_EVENT_NONE: - // 无事件,不处理 + err = menu_core_navigate_back(global_ctx); break; default: - // 自定义事件(用户可扩展) - MENU_DEBUG("Custom event received: type %d, param %lu", event->type, event->param); break; } + + return err; +} +/** + * @brief 参数编辑状态的动作处理 + * @param global_ctx 全局上下文指针 + * @param event 触发事件 + * @return 错误码 + */ +static MenuErrCode menu_state_action_param_edit(MenuGlobalCtx* global_ctx, MenuEventType event) +{ + MenuErrCode err = MENU_OK; + + // Suppress unused parameter warnings + (void)global_ctx; + (void)event; + + // 这里可以添加参数编辑的具体逻辑 + // 例如:根据上下键调整参数值,确认键保存,返回键取消编辑 + MENU_DEBUG("Param edit state, event: %d", event); + + return err; +} + +/** + * @brief 确认状态的动作处理 + * @param global_ctx 全局上下文指针 + * @param event 触发事件 + * @return 错误码 + */ +static MenuErrCode menu_state_action_confirm(MenuGlobalCtx* global_ctx, MenuEventType event) +{ + MenuErrCode err = MENU_OK; + + // Suppress unused parameter warnings + (void)global_ctx; + (void)event; + + // 这里可以添加确认状态的具体逻辑 + // 例如:确认保存设置,确认退出等 + MENU_DEBUG("Confirm state, event: %d", event); + + return err; +} + +/** + * @brief 错误状态的动作处理 + * @param global_ctx 全局上下文指针 + * @param event 触发事件 + * @return 错误码 + */ +static MenuErrCode menu_state_action_error(MenuGlobalCtx* global_ctx, MenuEventType event) +{ + MenuErrCode err = MENU_OK; + + // Suppress unused parameter warnings + (void)global_ctx; + (void)event; + + // 这里可以添加错误状态的具体逻辑 + // 例如:显示错误信息,等待用户确认 + MENU_DEBUG("Error state, event: %d", event); + return err; } /** * @brief 刷新菜单显示(内部调用,对接port层显示接口) + * @param global_ctx 全局上下文指针 */ -void menu_core_refresh_display(void) +void menu_core_refresh_display(MenuGlobalCtx* global_ctx) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - MenuNode* current_node = menu_core_find_node(ctx->current_node_id); + MenuCoreCtx* ctx = &global_ctx->core; + MenuNode* current_node = menu_core_find_node(global_ctx, ctx->current_node_id); if (current_node == NULL) { return; @@ -287,12 +436,28 @@ void menu_core_refresh_display(void) /** * @brief 从事件队列获取事件(非阻塞) + * @param global_ctx 全局上下文指针 * @param event 输出参数,获取到的事件 * @return 是否获取到事件 */ -static bool menu_core_get_event(MenuEvent* event) +/** + * @brief 获取菜单组件上下文所需的内存大小 + * @return 上下文内存大小(字节) + */ +uint32_t menu_get_ctx_size(void) { - MenuCoreCtx* ctx = menu_core_get_ctx(); + return sizeof(MenuGlobalCtx); +} + +/** + * @brief 从事件队列获取事件(非阻塞,优先级队列:高优先级事件先处理) + * @param global_ctx 全局上下文指针 + * @param event 输出参数,获取到的事件 + * @return 是否获取到事件 + */ +static bool menu_core_get_event(MenuGlobalCtx* global_ctx, MenuEvent* event) +{ + MenuCoreCtx* ctx = &global_ctx->core; MENU_ASSERT(ctx != NULL && event != NULL); if (ctx->event_queue.count == 0) @@ -300,9 +465,28 @@ static bool menu_core_get_event(MenuEvent* event) return false; } - // 从队列头取出事件 - *event = ctx->event_queue.buffer[ctx->event_queue.head]; - ctx->event_queue.head = (ctx->event_queue.head + 1) % MENU_CONFIG_EVENT_QUEUE_LEN; + // 查找优先级最高的事件 + uint8_t highest_priority_idx = ctx->event_queue.head; + for (uint8_t i = 0; i < ctx->event_queue.count; i++) + { + uint8_t idx = (ctx->event_queue.head + i) % MENU_CONFIG_EVENT_QUEUE_LEN; + if (ctx->event_queue.buffer[idx].priority > ctx->event_queue.buffer[highest_priority_idx].priority) + { + highest_priority_idx = idx; + } + } + + // 复制优先级最高的事件 + *event = ctx->event_queue.buffer[highest_priority_idx]; + + // 将最后一个事件移到被取出的位置 + uint8_t last_idx = (ctx->event_queue.head + ctx->event_queue.count - 1) % MENU_CONFIG_EVENT_QUEUE_LEN; + if (highest_priority_idx != last_idx) + { + ctx->event_queue.buffer[highest_priority_idx] = ctx->event_queue.buffer[last_idx]; + } + + // 更新队列计数 ctx->event_queue.count--; return true; @@ -310,28 +494,30 @@ static bool menu_core_get_event(MenuEvent* event) /** * @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示) + * @param global_ctx 全局上下文指针 */ -void menu_main_loop(void) +void menu_main_loop(MenuGlobalCtx* global_ctx) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - if (!ctx->is_initialized) + MENU_ASSERT(global_ctx != NULL); + MenuCoreCtx* ctx = &global_ctx->core; + if (!global_ctx->is_initialized) { return; } // 处理事件队列 MenuEvent event; - while (menu_core_get_event(&event)) + while (menu_core_get_event(global_ctx, &event)) { // 检查事件是否超时 if (menu_utils_get_tick() - event.timestamp > MENU_CONFIG_EVENT_TIMEOUT) { - MENU_DEBUG("Event timeout: type %d", event.type); + MENU_DEBUG("Event timeout: type %d, priority %d", event.type, event.priority); continue; } // 处理事件 - MenuErrCode err = menu_core_handle_event(&event); + MenuErrCode err = menu_core_handle_event(global_ctx, &event); if (err != MENU_OK) { MENU_DEBUG("Event handle error: %d", err); @@ -341,44 +527,101 @@ void menu_main_loop(void) // 定时刷新显示 if (menu_utils_get_tick() - ctx->last_refresh_tick >= MENU_CONFIG_REFRESH_INTERVAL) { - menu_core_refresh_display(); + menu_core_refresh_display(global_ctx); ctx->last_refresh_tick = menu_utils_get_tick(); } } /** * @brief 菜单组件初始化(必须首先调用) + * @param global_ctx 全局上下文指针 * @return 错误码 */ -MenuErrCode menu_init(void) +MenuErrCode menu_init(MenuGlobalCtx* global_ctx) { - MenuErrCode err = menu_core_ctx_init(); - if (err != MENU_OK) + if (global_ctx == NULL) { - return err; + return MENU_ERR_INVALID_PARAM; } + // 初始化全局上下文 + MENU_MEM_SET_ZERO(global_ctx, sizeof(MenuGlobalCtx)); + + // 初始化核心上下文 + MenuCoreCtx* ctx = &global_ctx->core; + MENU_MEM_SET_ZERO(ctx, sizeof(MenuCoreCtx)); + ctx->is_initialized = true; + ctx->current_state = MENU_STATE_INIT; // 设置初始状态 + // 初始化事件队列 - MenuCoreCtx* ctx = menu_core_get_ctx(); ctx->last_refresh_tick = menu_utils_get_tick(); + + // 初始化其他功能模块(如果启用) +#if MENU_CONFIG_ENABLE_PARAM + global_ctx->param.count = 0; +#endif + +#if MENU_CONFIG_ENABLE_LANG + global_ctx->lang.count = 0; + global_ctx->lang.current_lang_id = 0; // 默认语言ID +#endif + +#if MENU_CONFIG_ENABLE_MODBUS_MAP + global_ctx->modbus.count = 0; +#endif + + global_ctx->is_initialized = true; + + // 初始状态转换:从INIT到NORMAL + MenuEvent init_event = { + .type = MENU_EVENT_NONE, + .param = 0, + .timestamp = menu_utils_get_tick(), + .priority = MENU_EVENT_PRIORITY_NORMAL + }; + menu_state_transition(global_ctx, init_event.type); + + return MENU_OK; +} +/** + * @brief 菜单组件反初始化(释放资源) + * @param global_ctx 全局上下文指针 + * @return 错误码 + */ +MenuErrCode menu_deinit(MenuGlobalCtx* global_ctx) +{ + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_INVALID_PARAM; + } + + // 反初始化各功能模块 + // 目前都是静态内存,无需释放,只需重置状态 + MENU_MEM_SET_ZERO(global_ctx, sizeof(MenuGlobalCtx)); + return MENU_OK; } /** * @brief 发送事件到菜单事件队列(如按键事件、自定义事件) + * @param global_ctx 全局上下文指针 * @param type 事件类型 * @param param 事件附加参数(可选,如按键长按时间) + * @param priority 事件优先级 * @return 错误码 */ -MenuErrCode menu_post_event(MenuEventType type, uint32_t param) +// Undefine the macro to avoid conflict with the function definition +#undef menu_post_event + +MenuErrCode menu_post_event(MenuGlobalCtx* global_ctx, MenuEventType type, uint32_t param, MenuEventPriority priority) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - if (!ctx->is_initialized) + if (global_ctx == NULL || !global_ctx->is_initialized) { - return MENU_ERR_INVALID_PARAM; + return MENU_ERR_NOT_INITIALIZED; } + MenuCoreCtx* ctx = &global_ctx->core; // 检查事件队列是否已满 if (ctx->event_queue.count >= MENU_CONFIG_EVENT_QUEUE_LEN) { @@ -386,17 +629,22 @@ MenuErrCode menu_post_event(MenuEventType type, uint32_t param) } // 入队事件 - ctx->event_queue.buffer[ctx->event_queue.tail].type = type; - ctx->event_queue.buffer[ctx->event_queue.tail].param = param; - ctx->event_queue.buffer[ctx->event_queue.tail].timestamp = menu_utils_get_tick(); - ctx->event_queue.tail = (ctx->event_queue.tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN; + uint8_t idx = (ctx->event_queue.head + ctx->event_queue.count) % MENU_CONFIG_EVENT_QUEUE_LEN; + ctx->event_queue.buffer[idx].type = type; + ctx->event_queue.buffer[idx].param = param; + ctx->event_queue.buffer[idx].timestamp = menu_utils_get_tick(); + ctx->event_queue.buffer[idx].priority = priority; ctx->event_queue.count++; + MENU_DEBUG("Event posted: type %d, param %lu, priority %d, count %d", + type, param, priority, ctx->event_queue.count); + return MENU_OK; } /** * @brief 注册菜单节点(构建菜单树) + * @param global_ctx 全局上下文指针 * @param parent_id 父节点ID(根节点填0) * @param node_id 当前节点ID(唯一,不可重复) * @param name_str 菜单名称字符串(或多语言索引) @@ -404,14 +652,14 @@ MenuErrCode menu_post_event(MenuEventType type, uint32_t param) * @param exit_cb 退出该菜单的回调函数(NULL表示无) * @return 错误码 */ -MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb) +MenuErrCode menu_register_node(MenuGlobalCtx* global_ctx, MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - if (!ctx->is_initialized || node_id == 0) + if (global_ctx == NULL || !global_ctx->is_initialized || node_id == 0) { return MENU_ERR_INVALID_PARAM; } + MenuCoreCtx* ctx = &global_ctx->core; // 查找空闲节点 MenuNode* new_node = NULL; for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++) @@ -429,7 +677,7 @@ MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const c } // 检查节点ID是否已存在 - if (menu_core_find_node(node_id) != NULL) + if (menu_core_find_node(global_ctx, node_id) != NULL) { return MENU_ERR_INVALID_PARAM; } @@ -451,7 +699,7 @@ MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const c if (ctx->current_node_id != 0) { // 已有根节点,将新节点作为兄弟节点添加到根节点之后 - MenuNode* root_node = menu_core_find_node(ctx->current_node_id); + MenuNode* root_node = menu_core_find_node(global_ctx, ctx->current_node_id); while (root_node->next_sibling != NULL) { root_node = root_node->next_sibling; @@ -463,13 +711,13 @@ MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const c { // 第一个根节点,设置为当前节点 ctx->current_node_id = node_id; - menu_core_stack_push(node_id); + menu_core_stack_push(global_ctx, node_id); } } else { // 查找父节点 - MenuNode* parent_node = menu_core_find_node(parent_id); + MenuNode* parent_node = menu_core_find_node(global_ctx, parent_id); if (parent_node == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -499,17 +747,123 @@ MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const c /** * @brief 获取当前选中的菜单节点ID + * @param global_ctx 全局上下文指针 * @param node_id 输出参数,当前节点ID * @return 错误码 */ -MenuErrCode menu_get_current_node(MenuNodeId* node_id) +MenuErrCode menu_get_current_node(MenuGlobalCtx* global_ctx, MenuNodeId* node_id) { - MenuCoreCtx* ctx = menu_core_get_ctx(); - if (!ctx->is_initialized || node_id == NULL) + if (global_ctx == NULL || !global_ctx->is_initialized || node_id == NULL) { return MENU_ERR_INVALID_PARAM; } + MenuCoreCtx* ctx = &global_ctx->core; *node_id = ctx->current_node_id; return MENU_OK; -} \ No newline at end of file +} + +/** + * @brief 批量注册菜单节点 + * @param global_ctx 全局上下文指针 + * @param configs 菜单节点配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_register_nodes(MenuGlobalCtx* global_ctx, const MenuNodeConfig* configs, uint16_t config_count) +{ + if (global_ctx == NULL || !global_ctx->is_initialized || configs == NULL || config_count == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + MenuErrCode err = MENU_OK; + for (uint16_t i = 0; i < config_count; i++) + { + err = menu_register_node(global_ctx, configs[i].parent_id, configs[i].node_id, + configs[i].name, configs[i].enter_cb, configs[i].exit_cb); + if (err != MENU_OK) + { + MENU_ERROR("Failed to register menu node %d at index %d: %s", + configs[i].node_id, i, menu_err_code_to_str(err)); + return err; + } + } + + return MENU_OK; +} + +/** + * @brief 检查菜单组件是否已初始化 + * @param global_ctx 全局上下文指针 + * @return 是否已初始化 + */ +bool menu_is_initialized(MenuGlobalCtx* global_ctx) +{ + if (global_ctx == NULL) + { + return false; + } + return global_ctx->is_initialized; +} + +#if MENU_CONFIG_ENABLE_MEM_MONITOR +/** + * @brief 获取菜单内存使用统计信息 + * @param global_ctx 全局上下文指针 + * @param stats 输出参数,内存使用统计信息 + * @return 错误码 + */ +MenuErrCode menu_get_mem_stats(MenuGlobalCtx* global_ctx, MenuMemStats* stats) +{ + if (global_ctx == NULL || !global_ctx->is_initialized || stats == NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + MenuCoreCtx* ctx = &global_ctx->core; + + // 初始化统计信息 + MENU_MEM_SET_ZERO(stats, sizeof(MenuMemStats)); + + // 统计菜单节点使用情况 + stats->total_nodes = MENU_CONFIG_MAX_NODES; + for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++) + { + if (ctx->nodes[i].flags.is_registered) + { + stats->used_nodes++; + } + } + + // 统计参数使用情况 + stats->total_params = MENU_CONFIG_MAX_PARAMS; + #if MENU_CONFIG_ENABLE_PARAM + for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++) + { + if (global_ctx->param.params[i].is_registered) + { + stats->used_params++; + } + } + #endif + + // 统计Modbus映射使用情况 + stats->total_modbus_maps = MENU_CONFIG_MAX_MODBUS_MAPS; + #if MENU_CONFIG_ENABLE_MODBUS_MAP + stats->used_modbus_maps = global_ctx->modbus.count; + #endif + + // 统计事件队列使用情况 + stats->event_queue_len = MENU_CONFIG_EVENT_QUEUE_LEN; + stats->event_queue_used = ctx->event_queue.count; + + MENU_DEBUG("Memory stats: nodes %d/%d, params %d/%d, modbus %d/%d, events %d/%d", + stats->used_nodes, stats->total_nodes, + stats->used_params, stats->total_params, + stats->used_modbus_maps, stats->total_modbus_maps, + stats->event_queue_used, stats->event_queue_len); + + return MENU_OK; +} +#endif // MENU_CONFIG_ENABLE_MEM_MONITOR \ No newline at end of file diff --git a/src/features/menu_modbus.c b/src/features/menu_modbus.c index 2ea4a86..2f04bd9 100644 --- a/src/features/menu_modbus.c +++ b/src/features/menu_modbus.c @@ -2,7 +2,7 @@ * @file menu_modbus.c * @brief 菜单参数-Modbus寄存器映射:注册、查询、数据双向转换(工业级:类型安全、字节序适配) */ -#include "menu.h" +#include "../api/menu.h" #include "menu_core.h" #include "menu_data.h" #include "menu_def.h" @@ -169,16 +169,23 @@ static void menu_modbus_byte_order_convert(uint16_t* data, uint8_t len, uint8_t /** * @brief 查找Modbus映射(通过参数ID) + * @param ctx 菜单全局上下文 * @param param_id 参数ID * @return 映射内部结构体指针(NULL表示未找到) */ -static ModbusMapInternal* menu_modbus_find_by_param(uint16_t param_id) +static ModbusMapInternal* menu_modbus_find_by_param(MenuGlobalCtx* ctx, uint16_t param_id) { + if (ctx == NULL) + { + return NULL; + } + + MenuModbusCtx* modbus_ctx = &ctx->modbus; for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) { - if (s_menu_modbus_maps[i].is_registered && s_menu_modbus_maps[i].pub.param_id == param_id) + if (modbus_ctx->maps[i].is_registered && modbus_ctx->maps[i].pub.param_id == param_id) { - return &s_menu_modbus_maps[i]; + return &modbus_ctx->maps[i]; } } return NULL; @@ -186,30 +193,37 @@ static ModbusMapInternal* menu_modbus_find_by_param(uint16_t param_id) /** * @brief 查找Modbus映射(通过寄存器类型和地址) + * @param ctx 菜单全局上下文 * @param reg_type 寄存器类型 * @param reg_addr 寄存器地址(协议地址) * @return 映射内部结构体指针(NULL表示未找到) */ -static ModbusMapInternal* menu_modbus_find_by_reg(ModbusRegType reg_type, uint16_t reg_addr) +static ModbusMapInternal* menu_modbus_find_by_reg(MenuGlobalCtx* ctx, ModbusRegType reg_type, uint16_t reg_addr) { + if (ctx == NULL) + { + return NULL; + } + + MenuModbusCtx* modbus_ctx = &ctx->modbus; for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) { - if (s_menu_modbus_maps[i].is_registered && - s_menu_modbus_maps[i].pub.reg_type == reg_type && - s_menu_modbus_maps[i].pub.reg_addr == reg_addr) + if (modbus_ctx->maps[i].is_registered && + modbus_ctx->maps[i].pub.reg_type == reg_type && + modbus_ctx->maps[i].pub.reg_addr == reg_addr) { - return &s_menu_modbus_maps[i]; + return &modbus_ctx->maps[i]; } } return NULL; } /************************** 对外接口实现 **************************/ -MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order) +MenuErrCode menu_modbus_map_register(MenuGlobalCtx* ctx, uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order) { // 1. 校验参数是否已注册(通过获取参数值间接检查) float dummy_val; - MenuErrCode err = menu_param_get_value(param_id, &dummy_val); + MenuErrCode err = menu_param_get_value(ctx, param_id, &dummy_val); if (err != MENU_OK) { MENU_DEBUG("Modbus map param %d not found", param_id); @@ -254,19 +268,20 @@ MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, #endif // 6. 检查映射是否已存在 - if (menu_modbus_find_by_param(param_id) != NULL || menu_modbus_find_by_reg(reg_type, reg_addr) != NULL) + if (menu_modbus_find_by_param(ctx, param_id) != NULL || menu_modbus_find_by_reg(ctx, reg_type, reg_addr) != NULL) { MENU_DEBUG("Modbus map already exists: param %d, reg %d:%d", param_id, reg_type, reg_addr); return MENU_ERR_INVALID_PARAM; } - // 7. 查找空闲的映射位置(静态数组) + // 7. 查找空闲的映射位置(从全局上下文获取) + MenuModbusCtx* modbus_ctx = &ctx->modbus; ModbusMapInternal* map = NULL; for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) { - if (!s_menu_modbus_maps[i].is_registered) + if (!modbus_ctx->maps[i].is_registered) { - map = &s_menu_modbus_maps[i]; + map = &modbus_ctx->maps[i]; break; } } @@ -279,7 +294,7 @@ MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, // 8. 获取参数类型 MenuParamType param_type; - err = menu_param_get_type(param_id, ¶m_type); + err = menu_param_get_type(ctx, param_id, ¶m_type); if (err != MENU_OK) { return err; @@ -309,14 +324,14 @@ MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, return MENU_OK; } -MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map) +MenuErrCode menu_modbus_map_query_by_param(MenuGlobalCtx* ctx, uint16_t param_id, ModbusMap* map) { - if (map == NULL) + if (ctx == NULL || map == NULL) { return MENU_ERR_INVALID_PARAM; } - ModbusMapInternal* internal_map = menu_modbus_find_by_param(param_id); + ModbusMapInternal* internal_map = menu_modbus_find_by_param(ctx, param_id); if (internal_map == NULL) { MENU_DEBUG("Modbus map param %d not found", param_id); @@ -328,14 +343,14 @@ MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map) return MENU_OK; } -MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map) +MenuErrCode menu_modbus_map_query_by_reg(MenuGlobalCtx* ctx, ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map) { - if (map == NULL) + if (ctx == NULL || map == NULL) { return MENU_ERR_INVALID_PARAM; } - ModbusMapInternal* internal_map = menu_modbus_find_by_reg(reg_type, reg_addr); + ModbusMapInternal* internal_map = menu_modbus_find_by_reg(ctx, reg_type, reg_addr); if (internal_map == NULL) { MENU_DEBUG("Modbus map reg %d:%d not found", reg_type, reg_addr); @@ -347,15 +362,15 @@ MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_ad return MENU_OK; } -MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len) +MenuErrCode menu_modbus_map_param_to_reg(MenuGlobalCtx* ctx, uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len) { - if (reg_buf == NULL || buf_len == NULL || *buf_len == 0) + if (ctx == NULL || reg_buf == NULL || buf_len == NULL || *buf_len == 0) { return MENU_ERR_INVALID_PARAM; } // 1. 查找映射关系 - ModbusMapInternal* map = menu_modbus_find_by_param(param_id); + ModbusMapInternal* map = menu_modbus_find_by_param(ctx, param_id); if (map == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -372,7 +387,7 @@ MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, ui // 3. 获取参数值 float param_val; - MenuErrCode err = menu_param_get_value(param_id, ¶m_val); + MenuErrCode err = menu_param_get_value(ctx, param_id, ¶m_val); if (err != MENU_OK) { MENU_DEBUG("Failed to get param value: %d", err); @@ -471,15 +486,15 @@ MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, ui return MENU_OK; } -MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len) +MenuErrCode menu_modbus_map_reg_to_param(MenuGlobalCtx* ctx, uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len) { - if (reg_buf == NULL || buf_len == 0) + if (ctx == NULL || reg_buf == NULL || buf_len == 0) { return MENU_ERR_INVALID_PARAM; } // 1. 查找映射关系 - ModbusMapInternal* map = menu_modbus_find_by_param(param_id); + ModbusMapInternal* map = menu_modbus_find_by_param(ctx, param_id); if (map == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -505,13 +520,6 @@ MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_b // 4. 复制寄存器数据并进行字节序转换(读取寄存器后) uint16_t reg_data[MENU_CONFIG_MAX_MODBUS_MAPS * 2] = {0}; // 足够大的临时缓冲区 - // 检查缓冲区长度是否足够 - if (buf_len < req_len) - { - MENU_DEBUG("Modbus reg buf too small: need %d bytes, got %d bytes", req_len, buf_len); - return MENU_ERR_INVALID_PARAM; - } - // 复制数据到临时缓冲区 memcpy(reg_data, reg_buf, req_len); @@ -613,7 +621,7 @@ MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_b } // 6. 设置参数值(自动进行范围检查) - MenuErrCode err = menu_param_set_value(param_id, param_val); + MenuErrCode err = menu_param_set_value(ctx, param_id, param_val); if (err != MENU_OK) { return err; @@ -623,7 +631,6 @@ MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_b return MENU_OK; } -// 定义Modbus映射静态数组 -ModbusMapInternal s_menu_modbus_maps[MENU_CONFIG_MAX_MODBUS_MAPS]; + #endif // MENU_CONFIG_ENABLE_MODBUS_MAP \ No newline at end of file diff --git a/src/lang/menu_lang.c b/src/lang/menu_lang.c index 8ded261..07eb8c4 100644 --- a/src/lang/menu_lang.c +++ b/src/lang/menu_lang.c @@ -7,23 +7,24 @@ #if MENU_CONFIG_ENABLE_LANG -/************************** 全局变量定义 **************************/ -MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS]; -uint8_t s_current_lang_id = 0; +/************************** 内部函数声明 **************************/ +static const char* menu_lang_find_str(MenuGlobalCtx* global_ctx, uint16_t str_id, uint8_t lang_id); /** * @brief 查找语言字符串(通过字符串ID和语言ID) + * @param global_ctx 全局上下文指针 * @param str_id 字符串ID * @param lang_id 语言ID * @return 语言字符串(NULL表示未找到) */ -static const char* menu_lang_find_str(uint16_t str_id, uint8_t lang_id) +static const char* menu_lang_find_str(MenuGlobalCtx* global_ctx, uint16_t str_id, uint8_t lang_id) { + MenuLangCtx* lang_ctx = &global_ctx->lang; for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++) { - if (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id) + if (lang_ctx->strs[i].str_id == str_id && lang_ctx->strs[i].lang_id == lang_id) { - return s_menu_lang_strs[i].str; + return lang_ctx->strs[i].str; } } return NULL; @@ -31,26 +32,33 @@ static const char* menu_lang_find_str(uint16_t str_id, uint8_t lang_id) /** * @brief 注册语言字符串 + * @param global_ctx 全局上下文指针 * @param str_id 字符串ID * @param lang_id 语言ID * @param str 字符串内容 * @return 错误码 */ -MenuErrCode menu_lang_register_str(uint16_t str_id, uint8_t lang_id, const char* str) +MenuErrCode menu_lang_register_str(MenuGlobalCtx* global_ctx, uint16_t str_id, uint8_t lang_id, const char* str) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + if (lang_id >= MENU_CONFIG_MAX_LANGS || str == NULL) { return MENU_ERR_INVALID_PARAM; } + MenuLangCtx* lang_ctx = &global_ctx->lang; // 查找空闲位置或已存在的字符串条目 for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++) { - if (!s_menu_lang_strs[i].str || (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id)) + if (!lang_ctx->strs[i].str || (lang_ctx->strs[i].str_id == str_id && lang_ctx->strs[i].lang_id == lang_id)) { - s_menu_lang_strs[i].str_id = str_id; - s_menu_lang_strs[i].lang_id = lang_id; - s_menu_lang_strs[i].str = str; + lang_ctx->strs[i].str_id = str_id; + lang_ctx->strs[i].lang_id = lang_id; + lang_ctx->strs[i].str = str; return MENU_OK; } } @@ -60,54 +68,70 @@ MenuErrCode menu_lang_register_str(uint16_t str_id, uint8_t lang_id, const char* /** * @brief 获取当前语言的字符串 + * @param global_ctx 全局上下文指针 * @param str_id 字符串ID * @return 字符串内容(NULL表示未找到) */ -const char* menu_lang_get_str(uint16_t str_id) +const char* menu_lang_get_str(MenuGlobalCtx* global_ctx, uint16_t str_id) { - const char* str = menu_lang_find_str(str_id, s_current_lang_id); + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return NULL; + } + + MenuLangCtx* lang_ctx = &global_ctx->lang; + const char* str = menu_lang_find_str(global_ctx, str_id, lang_ctx->current_lang_id); if (str == NULL) { // 如果当前语言未找到,尝试使用默认语言(0) - str = menu_lang_find_str(str_id, 0); + str = menu_lang_find_str(global_ctx, str_id, 0); } return str; } /** * @brief 设置当前语言 + * @param global_ctx 全局上下文指针 * @param lang_id 语言ID(如0-中文,1-英文) * @return 错误码 */ -MenuErrCode menu_lang_set_current(uint8_t lang_id) +MenuErrCode menu_lang_set_current(MenuGlobalCtx* global_ctx, uint8_t lang_id) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + if (lang_id >= MENU_CONFIG_MAX_LANGS) { return MENU_ERR_INVALID_PARAM; } - s_current_lang_id = lang_id; + MenuLangCtx* lang_ctx = &global_ctx->lang; + lang_ctx->current_lang_id = lang_id; MENU_DEBUG("Language changed to: %d", lang_id); // 语言切换后,刷新菜单显示 - menu_core_refresh_display(); + menu_core_refresh_display(global_ctx); return MENU_OK; } /** * @brief 获取当前语言 + * @param global_ctx 全局上下文指针 * @param lang_id 输出参数,当前语言ID * @return 错误码 */ -MenuErrCode menu_lang_get_current(uint8_t* lang_id) +MenuErrCode menu_lang_get_current(MenuGlobalCtx* global_ctx, uint8_t* lang_id) { - if (lang_id == NULL) + if (global_ctx == NULL || !global_ctx->is_initialized || lang_id == NULL) { return MENU_ERR_INVALID_PARAM; } - *lang_id = s_current_lang_id; + MenuLangCtx* lang_ctx = &global_ctx->lang; + *lang_id = lang_ctx->current_lang_id; return MENU_OK; } @@ -120,4 +144,64 @@ uint8_t menu_lang_get_max_langs(void) return MENU_CONFIG_MAX_LANGS; } +/** + * @brief 批量注册语言字符串 + * @param global_ctx 全局上下文指针 + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_register_strs(MenuGlobalCtx* global_ctx, const MenuLangStr* strs, uint16_t count) +{ + if (global_ctx == NULL || !global_ctx->is_initialized || strs == NULL || count == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + MenuErrCode err = MENU_OK; + for (uint16_t i = 0; i < count; i++) + { + err = menu_lang_register_str(global_ctx, strs[i].str_id, strs[i].lang_id, strs[i].str); + if (err != MENU_OK) + { + MENU_ERROR("Failed to register lang str %d at index %d: %s", + strs[i].str_id, i, menu_err_code_to_str(err)); + return err; + } + } + + return MENU_OK; +} + +/** + * @brief 加载语言包(动态加载语言字符串) + * @param global_ctx 全局上下文指针 + * @param lang_id 语言ID + * @param strs 语言字符串数组 + * @param count 字符串数量 + * @return 错误码 + */ +MenuErrCode menu_lang_load_pack(MenuGlobalCtx* global_ctx, uint8_t lang_id, const MenuLangStr* strs, uint16_t count) +{ + if (global_ctx == NULL || !global_ctx->is_initialized || strs == NULL || count == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + // 先清除该语言的所有字符串 + MenuLangCtx* lang_ctx = &global_ctx->lang; + for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++) + { + if (lang_ctx->strs[i].lang_id == lang_id) + { + lang_ctx->strs[i].str_id = 0; + lang_ctx->strs[i].lang_id = 0; + lang_ctx->strs[i].str = NULL; + } + } + + // 再注册新的字符串 + return menu_lang_register_strs(global_ctx, strs, count); +} + #endif // MENU_CONFIG_ENABLE_LANG \ No newline at end of file diff --git a/src/param/menu_param.c b/src/param/menu_param.c index 9918b52..9a0b4e9 100644 --- a/src/param/menu_param.c +++ b/src/param/menu_param.c @@ -7,21 +7,30 @@ #if MENU_CONFIG_ENABLE_PARAM -/************************** 全局变量定义 **************************/ -MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS]; +/************************** 内部函数声明 **************************/ +static MenuParam* menu_param_find(MenuGlobalCtx* global_ctx, uint16_t param_id); +static void menu_param_float_to_internal(MenuParam* param, float value); +static float menu_param_internal_to_float(const MenuParam* param); /** * @brief 查找参数(通过参数ID,内部使用) + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @return 参数指针(NULL表示未找到) */ -static MenuParam* menu_param_find(uint16_t param_id) +static MenuParam* menu_param_find(MenuGlobalCtx* global_ctx, uint16_t param_id) { + if (global_ctx == NULL) + { + return NULL; + } + + MenuParamCtx* param_ctx = &global_ctx->param; for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++) { - if (s_menu_params[i].is_registered && s_menu_params[i].id == param_id) + if (param_ctx->params[i].is_registered && param_ctx->params[i].id == param_id) { - return &s_menu_params[i]; + return ¶m_ctx->params[i]; } } return NULL; @@ -124,6 +133,7 @@ static float menu_param_internal_to_float(const MenuParam* param) /** * @brief 注册参数到菜单节点(参数管理功能) + * @param global_ctx 全局上下文指针 * @param node_id 菜单节点ID(参数与菜单绑定) * @param param_id 参数ID(唯一) * @param type 参数类型 @@ -133,33 +143,39 @@ static float menu_param_internal_to_float(const MenuParam* param) * @param scale 缩放因子(如0.1表示保留1位小数) * @return 错误码 */ -MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale) +MenuErrCode menu_param_register(MenuGlobalCtx* global_ctx, MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + if (scale <= 0.0f) { return MENU_ERR_INVALID_PARAM; } // 查找菜单节点 - MenuNode* node = menu_core_find_node(node_id); + MenuNode* node = menu_core_find_node(global_ctx, node_id); if (node == NULL) { return MENU_ERR_NODE_NOT_FOUND; } // 检查参数ID是否已存在 - if (menu_param_find(param_id) != NULL) + if (menu_param_find(global_ctx, param_id) != NULL) { return MENU_ERR_INVALID_PARAM; } // 查找空闲参数位置 + MenuParamCtx* param_ctx = &global_ctx->param; MenuParam* param = NULL; for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++) { - if (!s_menu_params[i].is_registered) + if (!param_ctx->params[i].is_registered) { - param = &s_menu_params[i]; + param = ¶m_ctx->params[i]; break; } } @@ -194,14 +210,20 @@ MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParam /** * @brief 设置参数值 + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @param value 新值(浮点型,内部自动转换) * @return 错误码 */ -MenuErrCode menu_param_set_value(uint16_t param_id, float value) +MenuErrCode menu_param_set_value(MenuGlobalCtx* global_ctx, uint16_t param_id, float value) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + // 查找参数 - MenuParam* param = menu_param_find(param_id); + MenuParam* param = menu_param_find(global_ctx, param_id); if (param == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -218,19 +240,20 @@ MenuErrCode menu_param_set_value(uint16_t param_id, float value) /** * @brief 获取参数值 + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @param value 输出参数,当前值(浮点型) * @return 错误码 */ -MenuErrCode menu_param_get_value(uint16_t param_id, float* value) +MenuErrCode menu_param_get_value(MenuGlobalCtx* global_ctx, uint16_t param_id, float* value) { - if (value == NULL) + if (global_ctx == NULL || !global_ctx->is_initialized || value == NULL) { return MENU_ERR_INVALID_PARAM; } - + // 查找参数 - MenuParam* param = menu_param_find(param_id); + MenuParam* param = menu_param_find(global_ctx, param_id); if (param == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -246,13 +269,19 @@ MenuErrCode menu_param_get_value(uint16_t param_id, float* value) /** * @brief 将参数恢复为默认值 + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @return 错误码 */ -MenuErrCode menu_param_restore_default(uint16_t param_id) +MenuErrCode menu_param_restore_default(MenuGlobalCtx* global_ctx, uint16_t param_id) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + // 查找参数 - MenuParam* param = menu_param_find(param_id); + MenuParam* param = menu_param_find(global_ctx, param_id); if (param == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -260,60 +289,72 @@ MenuErrCode menu_param_restore_default(uint16_t param_id) // 恢复默认值 float default_val = menu_param_internal_to_float((const MenuParam*)¶m->default_val); - return menu_param_set_value(param_id, default_val); + return menu_param_set_value(global_ctx, param_id, default_val); } /** * @brief 增加参数值(用于菜单上下键调整) + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @param step 步长(浮点型) * @return 错误码 */ -MenuErrCode menu_param_increase(uint16_t param_id, float step) +MenuErrCode menu_param_increase(MenuGlobalCtx* global_ctx, uint16_t param_id, float step) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + float current_val = 0.0f; - MenuErrCode err = menu_param_get_value(param_id, ¤t_val); + MenuErrCode err = menu_param_get_value(global_ctx, param_id, ¤t_val); if (err != MENU_OK) { return err; } - return menu_param_set_value(param_id, current_val + step); + return menu_param_set_value(global_ctx, param_id, current_val + step); } /** * @brief 减少参数值(用于菜单上下键调整) + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @param step 步长(浮点型) * @return 错误码 */ -MenuErrCode menu_param_decrease(uint16_t param_id, float step) +MenuErrCode menu_param_decrease(MenuGlobalCtx* global_ctx, uint16_t param_id, float step) { + if (global_ctx == NULL || !global_ctx->is_initialized) + { + return MENU_ERR_NOT_INITIALIZED; + } + float current_val = 0.0f; - MenuErrCode err = menu_param_get_value(param_id, ¤t_val); + MenuErrCode err = menu_param_get_value(global_ctx, param_id, ¤t_val); if (err != MENU_OK) { return err; } - return menu_param_set_value(param_id, current_val - step); + return menu_param_set_value(global_ctx, param_id, current_val - step); } /** * @brief 获取参数类型 + * @param global_ctx 全局上下文指针 * @param param_id 参数ID * @param type 输出参数,参数类型 * @return 错误码 */ -MenuErrCode menu_param_get_type(uint16_t param_id, MenuParamType* type) +MenuErrCode menu_param_get_type(MenuGlobalCtx* global_ctx, uint16_t param_id, MenuParamType* type) { - if (type == NULL) + if (global_ctx == NULL || !global_ctx->is_initialized || type == NULL) { return MENU_ERR_INVALID_PARAM; } - - // 查找参数 - MenuParam* param = menu_param_find(param_id); + + MenuParam* param = menu_param_find(global_ctx, param_id); if (param == NULL) { return MENU_ERR_NODE_NOT_FOUND; @@ -324,4 +365,35 @@ MenuErrCode menu_param_get_type(uint16_t param_id, MenuParamType* type) return MENU_OK; } +/** + * @brief 批量注册参数到菜单节点 + * @param global_ctx 全局上下文指针 + * @param configs 参数配置数组 + * @param config_count 配置数量 + * @return 错误码 + */ +MenuErrCode menu_param_register_batch(MenuGlobalCtx* global_ctx, const MenuParamConfig* configs, uint16_t config_count) +{ + if (global_ctx == NULL || !global_ctx->is_initialized || configs == NULL || config_count == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + MenuErrCode err = MENU_OK; + for (uint16_t i = 0; i < config_count; i++) + { + err = menu_param_register(global_ctx, configs[i].node_id, configs[i].param_id, + configs[i].type, configs[i].min_val, configs[i].max_val, + configs[i].default_val, configs[i].scale); + if (err != MENU_OK) + { + MENU_ERROR("Failed to register param %d at index %d: %s", + configs[i].param_id, i, menu_err_code_to_str(err)); + return err; + } + } + + return MENU_OK; +} + #endif // MENU_CONFIG_ENABLE_PARAM \ No newline at end of file diff --git a/src/param/menu_param.h b/src/param/menu_param.h new file mode 100644 index 0000000..4722050 --- /dev/null +++ b/src/param/menu_param.h @@ -0,0 +1,39 @@ +/** + * @file menu_param.h + * @brief 菜单参数管理内部类型定义(用户无需关心) + */ +#ifndef MENU_PARAM_H +#define MENU_PARAM_H + +#include "menu_core.h" +#include +#include + +/** + * @brief 参数值共用体(支持多种数据类型) + */ +typedef union { + int8_t i8; ///< 8位有符号整数 + uint8_t u8; ///< 8位无符号整数 + int16_t i16; ///< 16位有符号整数 + uint16_t u16; ///< 16位无符号整数 + int32_t i32; ///< 32位有符号整数 + uint32_t u32; ///< 32位无符号整数 + float f; ///< 浮点数 +} MenuParamValue; + +/** + * @brief 参数结构体(参数管理的核心单元) + */ +typedef struct { + uint16_t id; ///< 参数ID(唯一) + MenuParamType type; ///< 参数类型 + MenuParamValue value; ///< 当前值 + MenuParamValue default_val;///< 默认值 + float min_val; ///< 最小值(浮点表示) + float max_val; ///< 最大值(浮点表示) + float scale; ///< 缩放因子 + bool is_registered : 1; ///< 是否已注册 +} MenuParam; + +#endif // MENU_PARAM_H \ No newline at end of file diff --git a/src/utils/menu_utils.c b/src/utils/menu_utils.c index 3f07e75..2175657 100644 --- a/src/utils/menu_utils.c +++ b/src/utils/menu_utils.c @@ -5,6 +5,24 @@ #include "menu_def.h" #include "menu_port.h" #include +#include + +/** + * @brief 错误码字符串映射 + */ +static const char* menu_err_str[] = { + "OK", ///< MENU_OK + "Invalid parameter", ///< MENU_ERR_INVALID_PARAM + "Out of memory", ///< MENU_ERR_OUT_OF_MEMORY + "Node not found", ///< MENU_ERR_NODE_NOT_FOUND + "Stack overflow", ///< MENU_ERR_STACK_OVERFLOW + "Stack underflow", ///< MENU_ERR_STACK_UNDERFLOW + "Event queue full", ///< MENU_ERR_EVENT_QUEUE_FULL + "Feature not supported", ///< MENU_ERR_NOT_SUPPORTED + "Hardware port error", ///< MENU_ERR_HW_PORT_ERROR + "Component not initialized", ///< MENU_ERR_NOT_INITIALIZED + "Invalid context pointer" ///< MENU_ERR_INVALID_CONTEXT +}; /** * @brief 断言失败处理函数 @@ -14,14 +32,57 @@ void menu_utils_assert_failed(const char* file, uint32_t line) { // 调用port层的打印接口输出断言信息 - menu_utils_printf("[MENU ASSERT] %s:%lu\r\n", file, line); + menu_utils_log(MENU_LOG_LEVEL_CRITICAL, file, line, "Assertion failed"); // 可以在这里添加其他处理,如触发硬件异常、保存上下文等 // 例如:menu_port_trigger_exception(); } /** - * @brief 调试打印函数(对接port层的硬件打印接口) + * @brief 通用日志函数(对接port层的硬件打印接口) + * @param level 日志级别 + * @param file 文件名 + * @param line 行号 + * @param fmt 格式化字符串 + * @param ... 可变参数 + */ +void menu_utils_log(MenuLogLevel level, const char* file, uint32_t line, const char* fmt, ...) +{ + // Explicitly cast unused parameters to void to suppress warnings + (void)level; + (void)file; + (void)line; + + va_list args; + va_start(args, fmt); + + // Get the driver and use its printf directly + const MenuPortDriver* driver = menu_port_get_driver(); + if (driver != NULL && driver->printf != NULL) + { + // Output the log message + driver->printf(fmt, args); + } + + va_end(args); +} + +/** + * @brief 错误码转换为字符串 + * @param err_code 错误码 + * @return 错误信息字符串 + */ +const char* menu_err_code_to_str(MenuErrCode err_code) +{ + if (err_code < sizeof(menu_err_str) / sizeof(menu_err_str[0])) { + return menu_err_str[err_code]; + } else { + return "Unknown error"; + } +} + +/** + * @brief 调试打印函数(兼容旧接口,对接port层的硬件打印接口) * @param fmt 格式化字符串 * @param ... 可变参数 */ @@ -31,7 +92,6 @@ void menu_utils_printf(const char* fmt, ...) va_start(args, fmt); // 调用port层的可变参数打印接口 - // 注意:这里需要用户在port层实现menu_port_printf函数 menu_port_printf(fmt, args); va_end(args); diff --git a/test/menu_test.c b/test/menu_test.c new file mode 100644 index 0000000..d33780d --- /dev/null +++ b/test/menu_test.c @@ -0,0 +1,471 @@ +/** + * @file menu_test.c + * @brief 菜单组件单元测试 + */ +#include "menu.h" +#include "menu_port.h" +#include +#include + +/************************** 测试用例 **************************/ + +/** + * @brief 测试菜单初始化和反初始化 + */ +void test_menu_init_deinit(void) +{ + printf("Test menu_init_deinit..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 检查初始化状态 + if (!menu_is_initialized(ctx)) + { + printf("FAILED: menu_is_initialized returned false\n"); + return; + } + + // 反初始化菜单 + err = menu_deinit(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_deinit returned %d\n", err); + return; + } + + printf("PASSED\n"); +} + +/** + * @brief 测试菜单节点注册 + */ +void test_menu_register_node(void) +{ + printf("Test menu_register_node..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册菜单节点 + err = menu_register_node(ctx, 0, 1, "Main Menu", NULL, NULL); + if (err != MENU_OK) + { + printf("FAILED: menu_register_node returned %d\n", err); + return; + } + + // 注册子菜单节点 + err = menu_register_node(ctx, 1, 2, "Param Menu", NULL, NULL); + if (err != MENU_OK) + { + printf("FAILED: menu_register_node returned %d\n", err); + return; + } + + // 检查当前节点 + MenuNodeId current_node_id; + err = menu_get_current_node(ctx, ¤t_node_id); + if (err != MENU_OK) + { + printf("FAILED: menu_get_current_node returned %d\n", err); + return; + } + + if (current_node_id != 1) + { + printf("FAILED: current_node_id is %d, expected 1\n", current_node_id); + return; + } + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/** + * @brief 测试批量注册菜单节点 + */ +void test_menu_register_nodes(void) +{ + printf("Test menu_register_nodes..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 定义菜单节点配置 + MenuNodeConfig nodes[] = { + {1, 0, "Main Menu", NULL, NULL}, + {2, 1, "Param Menu", NULL, NULL}, + {3, 1, "Info Menu", NULL, NULL}, + {4, 2, "Temp Param", NULL, NULL} + }; + + // 批量注册菜单节点 + err = menu_register_nodes(ctx, nodes, sizeof(nodes) / sizeof(nodes[0])); + if (err != MENU_OK) + { + printf("FAILED: menu_register_nodes returned %d\n", err); + return; + } + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/** + * @brief 测试事件处理 + */ +void test_menu_event(void) +{ + printf("Test menu_event..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册菜单节点 + err = menu_register_node(ctx, 0, 1, "Main Menu", NULL, NULL); + err = menu_register_node(ctx, 1, 2, "Menu 1", NULL, NULL); + err = menu_register_node(ctx, 1, 3, "Menu 2", NULL, NULL); + + // 发送事件 + err = menu_post_event(ctx, MENU_EVENT_KEY_DOWN, 0, MENU_EVENT_PRIORITY_NORMAL); + if (err != MENU_OK) + { + printf("FAILED: menu_post_event returned %d\n", err); + return; + } + + // 处理事件 + menu_main_loop(ctx); + + // 检查当前节点 + MenuNodeId current_node_id; + err = menu_get_current_node(ctx, ¤t_node_id); + if (err != MENU_OK) + { + printf("FAILED: menu_get_current_node returned %d\n", err); + return; + } + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/** + * @brief 测试内存监控 + */ +void test_menu_mem_stats(void) +{ + printf("Test menu_mem_stats..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册菜单节点 + err = menu_register_node(ctx, 0, 1, "Main Menu", NULL, NULL); + err = menu_register_node(ctx, 1, 2, "Menu 1", NULL, NULL); + + // 获取内存统计信息 + MenuMemStats stats; + err = menu_get_mem_stats(ctx, &stats); + if (err != MENU_OK) + { + printf("FAILED: menu_get_mem_stats returned %d\n", err); + return; + } + + printf("PASSED (nodes: %d/%d, params: %d/%d, events: %d/%d)\n", + stats.used_nodes, stats.total_nodes, + stats.used_params, stats.total_params, + stats.event_queue_used, stats.event_queue_len); + + // 反初始化菜单 + menu_deinit(ctx); +} + +/************************** 多语言支持测试 **************************/ + +/** + * @brief 测试多语言功能 + */ +void test_menu_lang(void) +{ + printf("Test menu_lang..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册语言字符串 + err = menu_lang_register_str(ctx, 1, 0, "中文菜单"); + err = menu_lang_register_str(ctx, 1, 1, "English Menu"); + err = menu_lang_register_str(ctx, 2, 0, "参数设置"); + err = menu_lang_register_str(ctx, 2, 1, "Param Settings"); + + // 设置当前语言为中文 + err = menu_lang_set_current(ctx, 0); + if (err != MENU_OK) + { + printf("FAILED: menu_lang_set_current(0) returned %d\n", err); + return; + } + + // 检查当前语言 + uint8_t current_lang; + err = menu_lang_get_current(ctx, ¤t_lang); + if (err != MENU_OK || current_lang != 0) + { + printf("FAILED: menu_lang_get_current returned %d, expected 0\n", current_lang); + return; + } + + // 批量注册语言字符串测试 + MenuLangStr strs[] = { + {3, 0, "系统信息"}, + {3, 1, "System Info"}, + {4, 0, "关于"}, + {4, 1, "About"} + }; + + err = menu_lang_register_strs(ctx, strs, sizeof(strs) / sizeof(strs[0])); + if (err != MENU_OK) + { + printf("FAILED: menu_lang_register_strs returned %d\n", err); + return; + } + + // 动态加载语言包测试 + MenuLangStr new_pack[] = { + {1, 2, "Español Menú"}, + {2, 2, "Ajustes de Parámetros"}, + {3, 2, "Información del Sistema"}, + {4, 2, "Acerca de"} + }; + + err = menu_lang_load_pack(ctx, 2, new_pack, sizeof(new_pack) / sizeof(new_pack[0])); + if (err != MENU_OK) + { + printf("FAILED: menu_lang_load_pack returned %d\n", err); + return; + } + + // 切换到新语言 + err = menu_lang_set_current(ctx, 2); + if (err != MENU_OK) + { + printf("FAILED: menu_lang_set_current(2) returned %d\n", err); + return; + } + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/************************** 参数管理测试 **************************/ + +/** + * @brief 测试参数管理功能 + */ +void test_menu_param(void) +{ + printf("Test menu_param..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册菜单节点 + err = menu_register_node(ctx, 0, 1, "Main Menu", NULL, NULL); + + // 注册参数 + err = menu_param_register(ctx, 1, 1, MENU_PARAM_TYPE_INT16, 0, 100, 50, 1.0f); + if (err != MENU_OK) + { + printf("FAILED: menu_param_register returned %d\n", err); + return; + } + + // 设置参数值 + err = menu_param_set_value(ctx, 1, 75.0f); + if (err != MENU_OK) + { + printf("FAILED: menu_param_set_value returned %d\n", err); + return; + } + + // 获取参数值 + float value; + err = menu_param_get_value(ctx, 1, &value); + if (err != MENU_OK || (int)value != 75) + { + printf("FAILED: menu_param_get_value returned %.1f, expected 75\n", value); + return; + } + + // 获取参数类型 + MenuParamType type; + err = menu_param_get_type(ctx, 1, &type); + if (err != MENU_OK || type != MENU_PARAM_TYPE_INT16) + { + printf("FAILED: menu_param_get_type returned %d, expected %d\n", type, MENU_PARAM_TYPE_INT16); + return; + } + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/************************** 优先级事件测试 **************************/ + +/** + * @brief 测试优先级事件处理 + */ +void test_menu_priority_event(void) +{ + printf("Test menu_priority_event..."); + + // 分配菜单上下文 + uint8_t menu_ctx_buf[menu_get_ctx_size()]; + MenuGlobalCtx* ctx = (MenuGlobalCtx*)menu_ctx_buf; + + // 初始化菜单 + MenuErrCode err = menu_init(ctx); + if (err != MENU_OK) + { + printf("FAILED: menu_init returned %d\n", err); + return; + } + + // 注册菜单节点 + err = menu_register_node(ctx, 0, 1, "Main Menu", NULL, NULL); + err = menu_register_node(ctx, 1, 2, "Menu 1", NULL, NULL); + err = menu_register_node(ctx, 1, 3, "Menu 2", NULL, NULL); + + // 发送不同优先级的事件 + err = menu_post_event(ctx, MENU_EVENT_KEY_DOWN, 0, MENU_EVENT_PRIORITY_LOW); + err = menu_post_event(ctx, MENU_EVENT_KEY_UP, 0, MENU_EVENT_PRIORITY_HIGH); + err = menu_post_event(ctx, MENU_EVENT_KEY_ENTER, 0, MENU_EVENT_PRIORITY_NORMAL); + + // 处理事件 + menu_main_loop(ctx); + + // 反初始化菜单 + menu_deinit(ctx); + + printf("PASSED\n"); +} + +/************************** 主函数 **************************/ + +/** + * @brief 测试主函数 + */ +int main(void) +{ + printf("Menu Component Unit Tests\n"); + printf("=========================\n"); + + // 测试菜单初始化和反初始化 + test_menu_init_deinit(); + + // 测试菜单节点注册 + test_menu_register_node(); + + // 测试批量注册菜单节点 + test_menu_register_nodes(); + + // 测试事件处理 + test_menu_event(); + + // 测试优先级事件处理 + test_menu_priority_event(); + + // 测试内存监控 + test_menu_mem_stats(); + + // 测试参数管理 +#if MENU_CONFIG_ENABLE_PARAM + test_menu_param(); +#endif + + // 测试多语言支持 +#if MENU_CONFIG_ENABLE_LANG + test_menu_lang(); +#endif + + printf("=========================\n"); + printf("All tests completed!\n"); + + return 0; +}