删除库进行重构,结构优化不好

This commit is contained in:
冯佳
2025-12-19 09:57:41 +08:00
parent e36b4e0e27
commit 1cd79af915
21 changed files with 422 additions and 7616 deletions

523
API.md
View File

@ -1,523 +0,0 @@
# 菜单组件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语言的嵌入式系统

View File

@ -1,54 +0,0 @@
cmake_minimum_required(VERSION 3.10)
# Project name
project(menu_component C)
# Set C standard
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Force using MinGW Makefiles if on Windows
if(WIN32)
if(NOT DEFINED CMAKE_GENERATOR)
set(CMAKE_GENERATOR "MinGW Makefiles" CACHE INTERNAL "")
endif()
# Set default compiler if not specified
if(NOT DEFINED CMAKE_C_COMPILER)
find_program(MINGW_GCC gcc)
if(MINGW_GCC)
set(CMAKE_C_COMPILER ${MINGW_GCC} CACHE INTERNAL "")
endif()
endif()
endif()
# Compiler flags
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -Os")
# Include directories
include_directories(
api
internal
src/core
src/param
src/lang
src/utils
src/features
port
)
# Source files
set(SOURCES
src/core/menu_core.c
src/param/menu_param.c
src/lang/menu_lang.c
src/utils/menu_utils.c
src/features/menu_modbus.c
port/menu_port.c
examples/menu_example.c
)
# Add example executable
add_executable(menu_example ${SOURCES})
# Debug build configuration
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g")

320
README.md
View File

@ -1,320 +0,0 @@
# 工业级嵌入式菜单组件
## 1. 项目概述
本项目是一个基于C语言开发的工业级嵌入式菜单组件设计用于资源受限的嵌入式系统具有高可移植性、模块化、可裁剪、低资源占用和事件驱动解耦等核心特点。
### 1.1 核心特性
- **分层架构设计**:核心层 + 功能扩展层 + 硬件端口层,彻底解耦硬件与业务逻辑
- **事件驱动机制**:基于事件队列的异步处理,提高系统响应性
- **静态内存管理**:完全使用静态数组,避免内存碎片,适合资源受限系统
- **高度可配置**:通过宏开关实现功能裁剪,可根据项目需求定制功能
- **工业级鲁棒性**:完善的边界检查、错误处理和断言机制
- **Modbus映射支持**内置参数与Modbus寄存器双向映射功能
## 2. 目录结构
```
menu/
├── api/ # 对外API层用户唯一需要引用的头文件
│ ├── menu.h # 主接口头文件
│ └── menu_config.h # 配置文件(功能开关、资源大小)
├── internal/ # 内部头文件层(用户无需关心)
│ ├── menu_core.h # 核心类型定义
│ ├── menu_data.h # 共享全局变量声明
│ ├── menu_def.h # 内部宏和辅助函数
│ └── menu_modbus.h # Modbus映射内部定义
├── port/ # 硬件端口层(用户需适配)
│ ├── menu_port.c # 硬件接口实现(示例)
│ └── menu_port.h # 硬件接口声明
├── src/ # 源码层
│ ├── core/ # 核心逻辑
│ │ └── menu_core.c # 导航、栈管理、主循环
│ ├── features/ # 功能扩展
│ │ └── menu_modbus.c # Modbus映射功能
│ ├── lang/ # 多语言支持
│ │ └── menu_lang.c # 语言切换、字符串管理
│ ├── param/ # 参数管理
│ │ └── menu_param.c # 参数注册、读写、范围检查
│ └── utils/ # 工具函数
│ └── menu_utils.c # 调试打印、断言、系统滴答
└── examples/ # 示例代码
└── menu_example.c # 菜单组件使用示例
```
## 3. 核心功能说明
### 3.1 菜单导航
- 支持多级菜单导航(上下键切换,确认键进入,返回键退出)
- 环形导航(到达边界自动循环)
- 菜单栈管理,支持深度导航
### 3.2 参数管理
- 支持多种参数类型int8/uint8/int16/uint16/int32/uint32/float
- 自动范围检查和边界处理
- 参数与菜单节点绑定,支持菜单直接调整参数
- 默认值恢复功能
### 3.3 多语言支持
- 支持多种语言切换
- 字符串ID映射机制
- 自动回退到默认语言
### 3.4 Modbus映射
- 参数与Modbus寄存器双向映射
- 支持多种Modbus寄存器类型线圈、离散输入、保持寄存器、输入寄存器
- 灵活的读写权限控制
- 支持多种字节序小端、大端、Modbus标准
- 自动类型转换和边界检查
## 4. 快速开始
### 4.1 配置组件
`menu_config.h`中根据项目需求配置功能开关和资源大小:
```c
// 核心配置
#define MENU_CONFIG_MAX_NODES 32 // 最大菜单节点数
#define MENU_CONFIG_STACK_DEPTH 8 // 菜单栈深度
#define MENU_CONFIG_EVENT_QUEUE_LEN 16 // 事件队列长度
// 功能扩展配置
#define MENU_CONFIG_ENABLE_PARAM 1 // 启用参数管理功能
#define MENU_CONFIG_ENABLE_LANG 1 // 启用多语言功能
#define MENU_CONFIG_ENABLE_MODBUS_MAP 1 // 启用Modbus映射功能
// Modbus映射配置
#define MENU_CONFIG_MAX_MODBUS_MAPS 16 // 最大Modbus映射数量
#define MENU_CONFIG_MODBUS_BYTE_ORDER 2 // 默认Modbus字节序
```
### 4.2 适配硬件端口
`port/menu_port.c`中实现硬件相关接口:
```c
// 禁用/启用全局中断
void menu_port_irq_disable(void) {
// 实现禁用中断的逻辑
}
void menu_port_irq_enable(void) {
// 实现启用中断的逻辑
}
// 获取系统滴答时间ms
uint32_t menu_port_get_tick(void) {
// 实现获取系统时间的逻辑
return HAL_GetTick(); // 示例STM32 HAL库
}
// 调试打印接口
void menu_port_printf(const char* fmt, va_list args) {
// 实现打印逻辑如通过UART输出
vprintf(fmt, args);
}
```
### 4.3 基本使用示例
```c
#include "menu.h"
#include "menu_port.h"
// 菜单回调函数
static MenuErrCode menu_cb_enter_param(MenuNodeId node_id) {
MENU_DEBUG("Enter param menu: %d", node_id);
return MENU_OK;
}
int main(void) {
// 1. 初始化硬件UART、LCD、SysTick等
hal_init();
// 2. 初始化菜单组件
menu_init();
// 3. 注册菜单节点
menu_register_node(0, 1, "主菜单", NULL, NULL); // 根节点
menu_register_node(1, 2, "参数设置", menu_cb_enter_param, NULL); // 子节点
menu_register_node(1, 3, "系统信息", NULL, NULL); // 子节点
menu_register_node(2, 4, "温度设置", NULL, NULL); // 孙节点
// 4. 注册参数(可选)
#if MENU_CONFIG_ENABLE_PARAM
menu_param_register(4, 1, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f);
#endif
// 5. 注册Modbus映射可选
#if MENU_CONFIG_ENABLE_MODBUS_MAP
menu_modbus_map_register(1, MODBUS_REG_TYPE_HOLDING_REG, 40001, 2, MODBUS_PERM_READ_WRITE, 2);
#endif
// 6. 主循环
while (1) {
// 扫描硬件按键
menu_port_key_scan();
// 处理菜单事件和刷新显示
menu_main_loop();
// 其他业务逻辑
// ...
}
}
```
## 5. Modbus映射功能详解
### 5.1 核心概念
Modbus映射功能实现了菜单参数与Modbus寄存器的双向数据转换支持
- **参数到寄存器**将菜单参数值转换为Modbus寄存器数据
- **寄存器到参数**将Modbus寄存器数据转换为菜单参数值
- **自动类型转换**:支持不同数据类型之间的自动转换
- **字节序适配**支持多种字节序兼容不同Modbus设备
### 5.2 寄存器类型支持
| 寄存器类型 | 地址范围 | 读写特性 | 支持的参数类型 |
|-----------|---------|---------|--------------|
| 线圈 | 00001-09999 | 读写 | INT8、UINT8 |
| 离散输入 | 10001-19999 | 只读 | INT8、UINT8 |
| 输入寄存器 | 30001-39999 | 只读 | 所有类型 |
| 保持寄存器 | 40001-49999 | 读写 | 所有类型 |
### 5.3 字节序支持
| 字节序类型 | 值 | 描述 |
|-----------|-----|------|
| 小端 | 0 | 低字节在前高字节在后x86架构默认 |
| 大端 | 1 | 高字节在前,低字节在后 |
| Modbus标准 | 2 | 字小端字节大端Modbus协议默认 |
### 5.4 Modbus映射使用示例
```c
// 1. 注册参数(必须在映射之前完成)
menu_param_register(menu_node_id, param_id, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f);
// 2. 注册Modbus映射
menu_modbus_map_register(
param_id, // 参数ID
MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型
40001, // 起始地址
2, // 寄存器数量浮点型占2个16位寄存器
MODBUS_PERM_READ_WRITE, // 读写权限
2 // 字节序Modbus标准
);
// 3. 参数到寄存器转换
uint8_t reg_buf[4] = {0};
uint8_t buf_len = sizeof(reg_buf);
menu_modbus_map_param_to_reg(param_id, reg_buf, &buf_len);
// 4. 寄存器到参数转换
const uint8_t reg_data[4] = {0x00, 0x00, 0x4B, 0x40}; // 25.0f的二进制表示
menu_modbus_map_reg_to_param(param_id, reg_data, sizeof(reg_data));
```
## 6. 配置说明
### 6.1 核心配置项
| 配置项 | 说明 | 默认值 |
|-------|------|-------|
| MENU_CONFIG_MAX_NODES | 最大菜单节点数 | 32 |
| MENU_CONFIG_STACK_DEPTH | 菜单栈深度 | 8 |
| MENU_CONFIG_EVENT_QUEUE_LEN | 事件队列长度 | 16 |
| MENU_CONFIG_ENABLE_ASSERT | 是否启用断言 | 1 |
| MENU_CONFIG_ENABLE_DEBUG | 是否启用调试打印 | 1 |
### 6.2 功能扩展配置
| 配置项 | 说明 | 默认值 |
|-------|------|-------|
| MENU_CONFIG_ENABLE_PARAM | 是否启用参数管理 | 1 |
| MENU_CONFIG_ENABLE_LANG | 是否启用多语言支持 | 1 |
| MENU_CONFIG_ENABLE_MODBUS_MAP | 是否启用Modbus映射 | 1 |
### 6.3 Modbus映射配置
| 配置项 | 说明 | 默认值 |
|-------|------|-------|
| MENU_CONFIG_MAX_MODBUS_MAPS | 最大Modbus映射数量 | 16 |
| MENU_CONFIG_MODBUS_MAX_ADDR | Modbus寄存器地址最大值 | 0xFFFF |
| MENU_CONFIG_MODBUS_BYTE_ORDER | 默认Modbus字节序 | 2Modbus标准 |
| MENU_CONFIG_MODBUS_PERMISSION | 是否启用权限校验 | 1 |
## 7. 移植指南
### 7.1 硬件端口层适配
用户只需适配`port/menu_port.c`中的以下接口:
1. **中断管理**`menu_port_irq_disable()``menu_port_irq_enable()`
2. **系统时间**`menu_port_get_tick()`
3. **调试打印**`menu_port_printf()`
4. **错误处理**`menu_port_error_handler()`
5. **按键扫描**`menu_port_key_scan()`(可选,用户也可以在外部实现)
6. **显示接口**`menu_port_display()`(可选)
### 7.2 编译配置
1.`menu/api`目录添加到Include路径
2. 根据项目需求配置`menu_config.h`
3. 编译时包含所有必要的源文件
## 8. 性能与资源占用
### 8.1 内存占用
- **Flash占用**约5-15KB取决于启用的功能
- **RAM占用**约1-3KB取决于配置的节点数、参数数和Modbus映射数
### 8.2 执行效率
- 事件处理:单次事件处理时间 < 10μs
- 菜单刷新单次刷新时间 < 5μs
- 参数转换单次转换时间 < 2μs
- Modbus映射单次映射转换时间 < 5μs
## 9. 工业级设计考量
1. **无动态内存分配**完全使用静态数组避免内存碎片
2. **事件驱动设计**硬件与软件解耦提高系统响应性
3. **完善的错误处理**每个函数都有明确的错误码返回
4. **边界检查**所有数组访问都有边界检查防止越界
5. **断言机制**关键函数入口有断言检查便于调试
6. **可配置裁剪**通过宏开关可以裁剪不需要的功能
7. **模块化设计**各功能模块独立便于维护和扩展
## 10. 应用场景
- 工业控制器
- 智能家居设备
- 仪器仪表
- 医疗设备
- 嵌入式人机界面
- Modbus从站设备
## 11. 许可证
本项目采用MIT许可证可自由用于商业和非商业项目
## 12. 联系方式
如有问题或建议欢迎提交Issue或Pull Request
---
**版本信息**v1.0.0
**最后更新**2025-12-18
**适用平台**所有支持C语言的嵌入式系统

View File

@ -1,427 +0,0 @@
/**
* @file menu.h
* @brief 菜单组件对外暴露的核心接口(用户唯一需要包含的头文件)
* @note 工业级嵌入式菜单组件 - 对外API层
*/
#ifndef MENU_H
#define MENU_H
#include "menu_config.h"
#include <stdint.h>
#include <stdbool.h>
/************************** 全局类型导出 **************************/
/**
* @brief 菜单组件全局上下文(用户需创建并管理)
* @note 替代全局变量,提高模块独立性和可测试性
*/
typedef struct MenuGlobalCtx MenuGlobalCtx;
/**
* @brief 菜单错误码(工业级错误处理:明确所有可能的错误类型)
*/
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;
/**
* @brief 事件优先级枚举
*/
typedef enum {
MENU_EVENT_PRIORITY_LOW = 0, ///< 低优先级事件
MENU_EVENT_PRIORITY_NORMAL, ///< 普通优先级事件(默认)
MENU_EVENT_PRIORITY_HIGH, ///< 高优先级事件
MENU_EVENT_PRIORITY_CRITICAL ///< 紧急优先级事件
} MenuEventPriority;
/**
* @brief 菜单事件类型(事件驱动核心:解耦按键与菜单逻辑)
*/
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;
/**
* @brief 菜单节点ID类型统一标识
*/
typedef uint16_t MenuNodeId;
/**
* @brief 菜单回调函数类型(菜单选中/退出时的执行逻辑)
* @param node_id 触发回调的菜单节点ID
* @return 错误码
*/
typedef MenuErrCode (*MenuCallback)(MenuNodeId node_id);
/**
* @brief 参数类型枚举(支持多种数据类型)
*/
typedef enum {
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 Modbus寄存器类型符合Modbus协议标准
*/
typedef enum {
MODBUS_REG_TYPE_COIL = 0, ///< 线圈1位地址范围00001-09999
MODBUS_REG_TYPE_DISCRETE_INPUT, ///< 离散输入1位地址范围10001-19999
MODBUS_REG_TYPE_HOLDING_REG, ///< 保持寄存器16位地址范围40001-49999
MODBUS_REG_TYPE_INPUT_REG, ///< 输入寄存器16位地址范围30001-39999
} ModbusRegType;
/**
* @brief Modbus寄存器读写权限
*/
typedef enum {
MODBUS_PERM_READ_ONLY = 0, ///< 只读(如离散输入、输入寄存器)
MODBUS_PERM_WRITE_ONLY, ///< 只写(如线圈)
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映射结构体对外只读内部初始化
*/
typedef struct {
uint16_t param_id; ///< 关联的参数ID
ModbusRegType reg_type; ///< 寄存器类型
uint16_t reg_addr; ///< 寄存器起始地址协议地址如40001
uint8_t reg_count; ///< 占用寄存器数量如32位浮点占2个16位寄存器
ModbusPerm perm; ///< 读写权限
uint8_t byte_order; ///< 字节序0-小端1-大端2-Modbus标准
} ModbusMap;
/**
* @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);
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
#endif // MENU_H

View File

@ -1,115 +0,0 @@
/**
* @file menu_config.h
* @brief 菜单组件用户配置文件(工业级:集中管理所有可配置项)
* @note 用户可根据项目需求修改此文件
*/
#ifndef MENU_CONFIG_H
#define MENU_CONFIG_H
#include <stdint.h>
#include <stdbool.h>
/************************** 核心配置 **************************/
/**
* @brief 最大菜单节点数(静态内存,根据项目调整)
*/
#define MENU_CONFIG_MAX_NODES 32
/**
* @brief 菜单栈深度最大导航层级如根→子→孙深度为3
*/
#define MENU_CONFIG_STACK_DEPTH 8
/**
* @brief 事件队列长度(存储按键/自定义事件,避免丢失)
*/
#define MENU_CONFIG_EVENT_QUEUE_LEN 16
/**
* @brief 是否启用断言(调试时开启,发布时关闭)
*/
#define MENU_CONFIG_ENABLE_ASSERT 1
/**
* @brief 是否启用调试打印(调试时开启,发布时关闭)
*/
#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;
/************************** 功能扩展配置(可裁剪) **************************/
/**
* @brief 是否启用参数管理功能
*/
#define MENU_CONFIG_ENABLE_PARAM 1
/**
* @brief 是否启用多语言功能
*/
#define MENU_CONFIG_ENABLE_LANG 1
/**
* @brief 最大参数数量(启用参数管理时有效)
*/
#define MENU_CONFIG_MAX_PARAMS 16
/**
* @brief 支持的最大语言数量(启用多语言时有效)
*/
#define MENU_CONFIG_MAX_LANGS 2
/************************** 硬件无关配置 **************************/
/**
* @brief 事件处理超时时间ms防止事件队列阻塞
*/
#define MENU_CONFIG_EVENT_TIMEOUT 100
/**
* @brief 菜单刷新间隔ms控制显示刷新频率
*/
#define MENU_CONFIG_REFRESH_INTERVAL 50
/************************** Modbus映射功能配置可裁剪 **************************/
/**
* @brief 是否启用参数-Modbus寄存器映射功能
*/
#define MENU_CONFIG_ENABLE_MODBUS_MAP 1
/**
* @brief 最大Modbus映射数量静态内存根据项目调整
*/
#define MENU_CONFIG_MAX_MODBUS_MAPS 16
/**
* @brief Modbus寄存器地址最大值根据实际Modbus从站配置
*/
#define MENU_CONFIG_MODBUS_MAX_ADDR 0x0FFF
/**
* @brief 默认Modbus字节序0-小端1-大端2-字小端字节大端Modbus标准
*/
#define MENU_CONFIG_MODBUS_BYTE_ORDER 2
/**
* @brief 是否启用Modbus映射权限校验与参数权限联动
*/
#define MENU_CONFIG_MODBUS_PERMISSION 1
#endif // MENU_CONFIG_H

View File

@ -1,373 +0,0 @@
/**
* @file menu_example.c
* @brief 菜单组件完整使用示例包含Modbus映射功能演示
* @note 工业级嵌入式菜单组件 - 使用示例
*/
#include <stdlib.h>
#include "menu.h"
#include "menu_port.h"
#include "menu_def.h"
/************************** 示例配置 **************************/
// 菜单节点ID定义
#define MENU_ROOT_ID 1 // 根节点
#define MENU_PARAM_ID 2 // 参数设置节点
#define MENU_MODBUS_ID 3 // Modbus设置节点
#define MENU_TEMP_ID 4 // 温度参数节点
#define MENU_HUMID_ID 5 // 湿度参数节点
#define MENU_PRESS_ID 6 // 压力参数节点
// 参数ID定义
#define PARAM_TEMP_ID 1 // 温度参数ID
#define PARAM_HUMID_ID 2 // 湿度参数ID
#define PARAM_PRESS_ID 3 // 压力参数ID
// Modbus寄存器地址定义
#define REG_TEMP_ADDR 40001 // 温度寄存器地址
#define REG_HUMID_ADDR 40003 // 湿度寄存器地址
#define REG_PRESS_ADDR 40005 // 压力寄存器地址
/************************** 全局变量 **************************/
// 模拟Modbus寄存器缓冲区
uint8_t g_modbus_reg_buf[256] = {0};
// 菜单上下文
static uint8_t* menu_ctx_buf = NULL;
static MenuGlobalCtx* g_menu_ctx = NULL;
/************************** 菜单回调函数 **************************/
/**
* @brief 进入参数设置菜单回调
* @param node_id 菜单节点ID
* @return 错误码
*/
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;
}
/**
* @brief 退出参数设置菜单回调
* @param node_id 菜单节点ID
* @return 错误码
*/
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;
}
/**
* @brief 进入Modbus设置菜单回调
* @param node_id 菜单节点ID
* @return 错误码
*/
static MenuErrCode menu_cb_enter_modbus(MenuNodeId 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通信演示
*/
static void modbus_communication_demo(void)
{
MENU_DEBUG("=== Modbus Communication Demo Start ===");
#if MENU_CONFIG_ENABLE_MODBUS_MAP
// 1. 模拟参数到寄存器的转换(参数值更新后同步到寄存器)
{
MENU_DEBUG("1. Param to Reg Demo:");
// 模拟温度参数变化(如从传感器读取到新的温度值)
float new_temp = 30.5f;
MenuErrCode err = menu_param_set_value(g_menu_ctx, PARAM_TEMP_ID, new_temp);
if (err == MENU_OK)
{
// 将参数值转换到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%
// 65.0f的二进制表示大端0x42820000对应Modbus标准字节序为0x8242 0x0000
uint8_t reg_buf[4] = {0x82, 0x42, 0x00, 0x00};
// 将Modbus寄存器数据转换为参数值
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(g_menu_ctx, PARAM_HUMID_ID, &humid_val);
MENU_DEBUG(" Reg data [0x82, 0x42, 0x00, 0x00] to humid param: %.1f%%", humid_val);
}
else
{
MENU_DEBUG(" Reg to param failed: %d", err);
}
}
// 3. 模拟Modbus读取请求处理
{
MENU_DEBUG("3. Modbus Read Request Demo:");
// 模拟Modbus主机读取压力参数
uint8_t buf_len = 4;
uint8_t reg_buf[4] = {0};
// 这里应该先调用menu_modbus_map_param_to_reg将参数转换到寄存器缓冲区
MenuErrCode err = menu_modbus_map_param_to_reg(g_menu_ctx, PARAM_PRESS_ID, reg_buf, &buf_len);
if (err == MENU_OK)
{
// 然后返回给Modbus主机
MENU_DEBUG(" Read pressure reg: [0x%02X, 0x%02X, 0x%02X, 0x%02X]",
reg_buf[0], reg_buf[1], reg_buf[2], reg_buf[3]);
}
else
{
MENU_DEBUG(" Read pressure failed: %d", err);
}
}
// 4. 模拟Modbus写入请求处理
{
MENU_DEBUG("4. Modbus Write Request Demo:");
// 模拟Modbus主机写入温度参数30.0°C
// 30.0f的二进制表示大端0x41F00000对应Modbus标准字节序为0xF041 0x0000
uint8_t reg_buf[4] = {0xF0, 0x41, 0x00, 0x00};
// 调用menu_modbus_map_reg_to_param将寄存器数据转换为参数值
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(g_menu_ctx, PARAM_TEMP_ID, &temp_val);
MENU_DEBUG(" Write temp reg: [0xF0, 0x41, 0x00, 0x00] -> temp param: %.1f°C", temp_val);
}
else
{
MENU_DEBUG(" Write temp failed: %d", err);
}
}
MENU_DEBUG("=== Modbus Communication Demo End ===");
#endif
}
/************************** 主函数 **************************/
int main(void)
{
MENU_DEBUG("=== Menu Component Example ===");
/************************** 1. 初始化硬件 **************************/
MENU_DEBUG("1. Initialize hardware...");
// 这里应该初始化硬件如UART、LCD、SysTick等
// hal_init();
/************************** 2. 初始化菜单组件 **************************/
MENU_DEBUG("2. Initialize menu component...");
// 动态分配菜单上下文缓冲区
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;
}
/************************** 3. 注册菜单节点 **************************/
MENU_DEBUG("3. Register menu nodes...");
// 注册根节点
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);
return -1;
}
// 注册参数设置节点
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);
return -1;
}
// 注册Modbus设置节点
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);
return -1;
}
// 注册温度参数节点
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);
return -1;
}
// 注册湿度参数节点
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);
return -1;
}
// 注册压力参数节点
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);
return -1;
}
/************************** 4. 注册参数 **************************/
MENU_DEBUG("4. Register parameters...");
#if MENU_CONFIG_ENABLE_PARAM
// 注册温度参数(范围:-40.0°C ~ 120.0°C默认25.0°C精度0.1°C
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)
{
MENU_DEBUG(" Register temp param failed: %d", err);
return -1;
}
// 注册湿度参数范围0.0% ~ 100.0%默认50.0%精度0.1%
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)
{
MENU_DEBUG(" Register humid param failed: %d", err);
return -1;
}
// 注册压力参数范围0.0kPa ~ 1000.0kPa默认101.3kPa精度0.1kPa
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)
{
MENU_DEBUG(" Register press param failed: %d", err);
return -1;
}
#endif
/************************** 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, // 起始地址
2, // 寄存器数量浮点型占2个16位寄存器
MODBUS_PERM_READ_WRITE, // 读写权限
2 // 字节序Modbus标准
);
if (err != MENU_OK)
{
MENU_DEBUG(" Register temp modbus map failed: %d", err);
return -1;
}
// 注册湿度参数到Modbus寄存器映射
err = menu_modbus_map_register(
g_menu_ctx,
PARAM_HUMID_ID, // 参数ID
MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型
REG_HUMID_ADDR, // 起始地址
2, // 寄存器数量
MODBUS_PERM_READ_WRITE, // 读写权限
2 // 字节序Modbus标准
);
if (err != MENU_OK)
{
MENU_DEBUG(" Register humid modbus map failed: %d", err);
return -1;
}
// 注册压力参数到Modbus寄存器映射
err = menu_modbus_map_register(
g_menu_ctx,
PARAM_PRESS_ID, // 参数ID
MODBUS_REG_TYPE_HOLDING_REG, // 寄存器类型
REG_PRESS_ADDR, // 起始地址
2, // 寄存器数量
MODBUS_PERM_READ_WRITE, // 读写权限
2 // 字节序Modbus标准
);
if (err != MENU_OK)
{
MENU_DEBUG(" Register press modbus map failed: %d", err);
return -1;
}
#endif
/************************** 6. 运行Modbus通信演示 **************************/
MENU_DEBUG("6. Run Modbus communication demo...");
modbus_communication_demo();
/************************** 7. 进入主循环 **************************/
MENU_DEBUG("7. Enter main loop...");
while (1)
{
// 处理菜单事件和刷新显示
menu_main_loop(g_menu_ctx);
// 其他业务逻辑
// ...
// 模拟延时
// menu_port_delay_ms(10);
}
// Free the menu context buffer
free(menu_ctx_buf);
return 0;
}

View File

@ -1,124 +0,0 @@
/**
* @file menu_core.h
* @brief 菜单组件核心类型定义(用户无需关心)
*/
#ifndef MENU_CORE_H
#define MENU_CORE_H
#include "menu_def.h"
#include <stdint.h>
#include <stdbool.h>
/************************** 核心数据结构 **************************/
/**
* @brief 菜单事件结构体(事件队列元素)
*/
typedef struct {
MenuEventType type; ///< 事件类型
uint32_t param; ///< 事件附加参数
uint32_t timestamp; ///< 事件产生时间ms用于超时处理
MenuEventPriority priority; ///< 事件优先级
} MenuEvent;
/**
* @brief 菜单节点结构体(菜单树的基本单元)
* @note 紧凑设计:使用位域和共用体减少内存占用(工业级嵌入式低内存优化)
*/
typedef struct MenuNode {
MenuNodeId id; ///< 节点ID唯一
MenuNodeId parent_id; ///< 父节点ID根节点为0
const char* name; ///< 菜单名称(或多语言索引)
MenuCallback enter_cb; ///< 进入回调
MenuCallback exit_cb; ///< 退出回调
struct MenuNode* first_child;///< 第一个子节点
struct MenuNode* next_sibling;///< 下一个兄弟节点
struct MenuNode* prev_sibling;///< 上一个兄弟节点
// 位域减少内存占用1字节代替多个u8变量
struct {
bool is_registered : 1; ///< 是否已注册
bool is_selected : 1; ///< 是否被选中
unsigned int reserved : 6; ///< 保留位
} flags;
#if MENU_CONFIG_ENABLE_PARAM
uint16_t param_id; ///< 绑定的参数ID启用参数时有效
#endif
} MenuNode;
/**
* @brief 菜单栈结构体(管理导航层级)
*/
typedef struct {
MenuNodeId nodes[MENU_CONFIG_STACK_DEPTH]; ///< 栈元素存储节点ID
uint8_t top; ///< 栈顶指针
} MenuStack;
/**
* @brief 事件队列结构体(环形队列,工业级:避免溢出)
*/
typedef struct {
MenuEvent buffer[MENU_CONFIG_EVENT_QUEUE_LEN]; ///< 队列缓冲区
uint8_t head; ///< 入队指针
uint8_t tail; ///< 出队指针
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 菜单核心上下文(全局唯一,存储菜单状态)
*/
typedef struct {
MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池(无动态分配)
MenuStack stack; ///< 菜单导航栈
MenuEventQueue event_queue; ///< 事件队列
MenuNodeId current_node_id; ///< 当前选中的节点ID
MenuState current_state; ///< 当前菜单状态
uint32_t last_refresh_tick; ///< 上次刷新时间ms
bool is_initialized; ///< 是否已初始化
} MenuCoreCtx;
/************************** 核心内部接口声明 **************************/
/**
* @brief 查找菜单节点通过ID
* @param global_ctx 全局上下文指针
* @param node_id 节点ID
* @return 节点指针NULL表示未找到
*/
MenuNode* menu_core_find_node(MenuGlobalCtx* global_ctx, MenuNodeId node_id);
/**
* @brief 处理单个菜单事件
* @param global_ctx 全局上下文指针
* @param event 事件指针
* @return 错误码
*/
MenuErrCode menu_core_handle_event(MenuGlobalCtx* global_ctx, const MenuEvent* event);
/**
* @brief 刷新菜单显示内部调用对接port层显示接口
* @param global_ctx 全局上下文指针
*/
void menu_core_refresh_display(MenuGlobalCtx* global_ctx);
#endif // MENU_CORE_H

View File

@ -1,62 +0,0 @@
/**
* @file menu_data.h
* @brief 菜单组件共享全局变量(用户无需关心,内部仅通过接口访问)
*/
#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 参数管理上下文(启用参数时有效)
*/
#if MENU_CONFIG_ENABLE_PARAM
typedef struct {
MenuParam params[MENU_CONFIG_MAX_PARAMS]; ///< 参数数组
uint16_t count; ///< 已注册参数数量
} MenuParamCtx;
#endif // MENU_CONFIG_ENABLE_PARAM
/**
* @brief 多语言上下文(启用多语言时有效)
*/
#if MENU_CONFIG_ENABLE_LANG
typedef struct {
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映射时有效
*/
#if MENU_CONFIG_ENABLE_MODBUS_MAP
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

View File

@ -1,110 +0,0 @@
/**
* @file menu_def.h
* @brief 菜单组件内部宏定义和辅助函数(用户无需关心)
*/
#ifndef MENU_DEF_H
#define MENU_DEF_H
#include "menu_config.h"
#include "menu.h"
#include <stdint.h>
#include <string.h>
/************************** 内部宏定义 **************************/
/**
* @brief 断言宏(工业级:调试时检查,发布时忽略)
*/
#if MENU_CONFIG_ENABLE_ASSERT
#define MENU_ASSERT(condition) \
do { \
if (!(condition)) { \
menu_utils_assert_failed(__FILE__, __LINE__); \
while (1); \
} \
} while (0)
#else
#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_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
/**
* @brief 内存清零宏
*/
#define MENU_MEM_SET_ZERO(ptr, size) memset((ptr), 0, (size))
/**
* @brief 无效ID定义
*/
#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 断言失败处理函数
* @param file 文件名
* @param line 行号
*/
void menu_utils_assert_failed(const char* file, uint32_t line);
/**
* @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, ...);
/**
* @brief 获取系统滴答时间ms对接port层
* @return 当前滴答时间
*/
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

View File

@ -1,23 +0,0 @@
/**
* @file menu_modbus.h
* @brief 菜单Modbus映射内部头文件用户无需关心
* @note 工业级嵌入式菜单组件 - Modbus映射内部定义
*/
#ifndef MENU_MODBUS_H
#define MENU_MODBUS_H
#include "menu.h"
#include "menu_def.h"
/************************** 内部类型定义 **************************/
/**
* @brief Modbus映射内部结构体扩展对外结构体增加内部状态
*/
typedef struct {
ModbusMap pub; ///< 对外暴露的映射信息(只读)
MenuParamType param_type; ///< 参数类型(缓存,避免重复查找)
uint16_t reg_addr_offset; ///< 寄存器偏移地址(转换后的实际偏移)
bool is_registered; ///< 是否已注册
} ModbusMapInternal;
#endif // MENU_MODBUS_H

View File

@ -1,288 +0,0 @@
/**
* @file menu_port.c
* @brief 菜单组件硬件端口层实现(模块化设计,支持运行时配置)
* @note 工业级嵌入式菜单组件 - 硬件端口层
* @attention 用户需要根据自己的硬件平台修改此文件
*/
#include "menu_port.h"
#include "menu.h"
#include <stdio.h>
#include <stddef.h>
#include <time.h>
#include <unistd.h>
/************************** 静态全局变量 **************************/
static const MenuPortDriver* g_current_driver = NULL;
/************************** 默认驱动实现 **************************/
/**
* @brief 默认硬件打印接口实现使用标准C库printf
* @param fmt 格式化字符串
* @param args 可变参数列表
*/
static void default_printf(const char* fmt, va_list args)
{
vprintf(fmt, args);
}
/**
* @brief 默认获取系统滴答时间实现使用标准C库time函数
* @return 当前系统滴答时间ms
*/
static uint32_t default_get_tick(void)
{
return (uint32_t)time(NULL) * 1000;
}
/**
* @brief 默认硬件延迟函数实现使用标准C库sleep函数
* @param ms 延迟时间ms
*/
static void default_delay_ms(uint32_t ms)
{
sleep(ms / 1000);
usleep((ms % 1000) * 1000);
}
/**
* @brief 默认菜单显示接口实现使用标准C库printf
* @param menu_name 菜单名称字符串
* @param menu_id 菜单ID
*/
static void default_display(const char* menu_name, uint16_t menu_id)
{
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 默认按键扫描接口实现(无按键返回)
* @return 当前按键事件
*/
static MenuEventType default_key_scan(void)
{
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硬件发送数据接口实现模拟成功
* @param reg_type 寄存器类型
* @param reg_addr 寄存器地址
* @param reg_buf 数据缓冲区
* @param buf_len 数据长度
* @return 错误码
*/
static MenuErrCode default_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len)
{
(void)reg_type;
(void)reg_addr;
(void)reg_buf;
(void)buf_len;
return MENU_OK;
}
/**
* @brief 默认Modbus硬件接收数据接口实现模拟成功
* @param reg_type 寄存器类型
* @param reg_addr 寄存器地址
* @param reg_buf 数据缓冲区
* @param buf_len 数据长度
* @return 错误码
*/
static MenuErrCode default_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len)
{
(void)reg_type;
(void)reg_addr;
(void)reg_buf;
(void)buf_len;
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

View File

@ -1,147 +0,0 @@
/**
* @file menu_port.h
* @brief 菜单组件硬件端口层接口定义(用户需要实现的硬件相关接口)
* @note 工业级嵌入式菜单组件 - 硬件端口层
*/
#ifndef MENU_PORT_H
#define MENU_PORT_H
#include <stdarg.h>
#include <stdint.h>
// 包含菜单核心类型定义
#include "menu.h"
/************************** 硬件端口层接口声明 **************************/
/**
* @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 可变参数列表
*/
void menu_port_printf(const char* fmt, va_list args);
/**
* @brief 获取系统滴答时间(兼容旧接口)
* @return 当前系统滴答时间ms
*/
uint32_t menu_port_get_tick(void);
/**
* @brief 硬件延迟函数(兼容旧接口)
* @param ms 延迟时间ms
*/
void menu_port_delay_ms(uint32_t ms);
/**
* @brief 菜单显示接口(兼容旧接口)
* @param menu_name 菜单名称字符串
* @param menu_id 菜单ID
*/
void menu_port_display(const char* menu_name, uint16_t menu_id);
/**
* @brief 默认硬件驱动结构体(提供基本实现,减少用户工作量)
*/
extern const MenuPortDriver g_menu_default_driver;
#endif // MENU_PORT_H

View File

@ -1,869 +0,0 @@
/**
* @file menu_core.c
* @brief 菜单核心逻辑:导航、栈管理、主循环(工业级:状态机驱动)
*/
#include "../api/menu.h"
#include "menu_core.h"
#include "menu_data.h"
#include <stddef.h>
/************************** 内部函数声明 **************************/
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(MenuGlobalCtx* global_ctx, MenuNodeId node_id)
{
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)
{
return &ctx->nodes[i];
}
}
return NULL;
}
/**
* @brief 菜单栈压入节点
* @param global_ctx 全局上下文指针
* @param node_id 节点ID
* @return 错误码
*/
static MenuErrCode menu_core_stack_push(MenuGlobalCtx* global_ctx, MenuNodeId node_id)
{
MenuCoreCtx* ctx = &global_ctx->core;
MENU_ASSERT(ctx != NULL);
if (ctx->stack.top >= MENU_CONFIG_STACK_DEPTH)
{
MENU_DEBUG("Stack overflow: depth %d", MENU_CONFIG_STACK_DEPTH);
return MENU_ERR_STACK_OVERFLOW;
}
ctx->stack.nodes[ctx->stack.top++] = node_id;
MENU_DEBUG("Stack push: node %d, top %d", node_id, ctx->stack.top);
return MENU_OK;
}
/**
* @brief 菜单栈弹出节点
* @param global_ctx 全局上下文指针
* @param node_id 输出参数弹出的节点ID
* @return 错误码
*/
static MenuErrCode menu_core_stack_pop(MenuGlobalCtx* global_ctx, MenuNodeId* node_id)
{
MenuCoreCtx* ctx = &global_ctx->core;
MENU_ASSERT(ctx != NULL && node_id != NULL);
if (ctx->stack.top == 0)
{
MENU_DEBUG("Stack underflow");
return MENU_ERR_STACK_UNDERFLOW;
}
*node_id = ctx->stack.nodes[--ctx->stack.top];
MENU_DEBUG("Stack pop: node %d, top %d", *node_id, ctx->stack.top);
return MENU_OK;
}
/**
* @brief 菜单导航到子节点(确认键)
* @param global_ctx 全局上下文指针
* @param node_id 当前节点ID
* @return 错误码
*/
static MenuErrCode menu_core_navigate_enter(MenuGlobalCtx* global_ctx, MenuNodeId node_id)
{
MenuNode* node = menu_core_find_node(global_ctx, node_id);
if (node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 执行进入回调
if (node->enter_cb != NULL)
{
MenuErrCode err = node->enter_cb(node_id);
if (err != MENU_OK)
{
return err;
}
}
// 如果有子节点,压入栈并选中第一个子节点
if (node->first_child != NULL)
{
MenuCoreCtx* ctx = &global_ctx->core;
ctx->current_node_id = node->first_child->id;
return menu_core_stack_push(global_ctx, node->first_child->id);
}
return MENU_OK;
}
/**
* @brief 菜单导航返回父节点(返回键)
* @param global_ctx 全局上下文指针
* @return 错误码
*/
static MenuErrCode menu_core_navigate_back(MenuGlobalCtx* global_ctx)
{
MenuCoreCtx* ctx = &global_ctx->core;
MenuNodeId current_id = ctx->current_node_id;
MenuNode* current_node = menu_core_find_node(global_ctx, current_id);
if (current_node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 执行退出回调
if (current_node->exit_cb != NULL)
{
MenuErrCode err = current_node->exit_cb(current_id);
if (err != MENU_OK)
{
return err;
}
}
// 弹出栈,回到父节点
MenuNodeId parent_id = current_node->parent_id;
if (parent_id == 0) // 根节点
{
ctx->current_node_id = current_id;
return MENU_OK;
}
MenuNodeId pop_id;
MenuErrCode err = menu_core_stack_pop(global_ctx, &pop_id);
if (err != MENU_OK)
{
return err;
}
ctx->current_node_id = parent_id;
return MENU_OK;
}
/**
* @brief 菜单导航上选(上键)
* @param global_ctx 全局上下文指针
* @return 错误码
*/
static MenuErrCode menu_core_navigate_up(MenuGlobalCtx* global_ctx)
{
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;
}
// 切换到上一个兄弟节点
if (current_node->prev_sibling != NULL)
{
ctx->current_node_id = current_node->prev_sibling->id;
}
else
{
// 循环到最后一个兄弟节点(工业级:友好的交互设计)
MenuNode* last_node = current_node;
while (last_node->next_sibling != NULL)
{
last_node = last_node->next_sibling;
}
ctx->current_node_id = last_node->id;
}
MENU_DEBUG("Navigate up: current node %d", ctx->current_node_id);
return MENU_OK;
}
/**
* @brief 菜单导航下选(下键)
* @param global_ctx 全局上下文指针
* @return 错误码
*/
static MenuErrCode menu_core_navigate_down(MenuGlobalCtx* global_ctx)
{
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;
}
// 切换到下一个兄弟节点
if (current_node->next_sibling != NULL)
{
ctx->current_node_id = current_node->next_sibling->id;
}
else
{
// 循环到第一个兄弟节点
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;
}
}
MENU_DEBUG("Navigate down: current node %d", ctx->current_node_id);
return MENU_OK;
}
/**
* @brief 处理单个菜单事件(使用状态机)
* @param global_ctx 全局上下文指针
* @param event 事件指针
* @return 错误码
*/
MenuErrCode menu_core_handle_event(MenuGlobalCtx* global_ctx, const MenuEvent* event)
{
MENU_ASSERT(event != NULL);
MenuErrCode err = MENU_OK;
// 使用状态机处理事件
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(global_ctx);
break;
case MENU_EVENT_KEY_DOWN:
err = menu_core_navigate_down(global_ctx);
break;
case MENU_EVENT_KEY_ENTER:
err = menu_core_navigate_enter(global_ctx, global_ctx->core.current_node_id);
break;
case MENU_EVENT_KEY_BACK:
err = menu_core_navigate_back(global_ctx);
break;
default:
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(MenuGlobalCtx* global_ctx)
{
MenuCoreCtx* ctx = &global_ctx->core;
MenuNode* current_node = menu_core_find_node(global_ctx, ctx->current_node_id);
if (current_node == NULL)
{
return;
}
// 这里应该调用port层的显示接口
// 例如menu_port_display(current_node->name, current_node->id);
MENU_DEBUG("Refresh display: current menu %s (ID: %d)", current_node->name, current_node->id);
}
/**
* @brief 从事件队列获取事件(非阻塞)
* @param global_ctx 全局上下文指针
* @param event 输出参数,获取到的事件
* @return 是否获取到事件
*/
/**
* @brief 获取菜单组件上下文所需的内存大小
* @return 上下文内存大小(字节)
*/
uint32_t menu_get_ctx_size(void)
{
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)
{
return false;
}
// 查找优先级最高的事件
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;
}
/**
* @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示)
* @param global_ctx 全局上下文指针
*/
void menu_main_loop(MenuGlobalCtx* global_ctx)
{
MENU_ASSERT(global_ctx != NULL);
MenuCoreCtx* ctx = &global_ctx->core;
if (!global_ctx->is_initialized)
{
return;
}
// 处理事件队列
MenuEvent 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, priority %d", event.type, event.priority);
continue;
}
// 处理事件
MenuErrCode err = menu_core_handle_event(global_ctx, &event);
if (err != MENU_OK)
{
MENU_DEBUG("Event handle error: %d", err);
}
}
// 定时刷新显示
if (menu_utils_get_tick() - ctx->last_refresh_tick >= MENU_CONFIG_REFRESH_INTERVAL)
{
menu_core_refresh_display(global_ctx);
ctx->last_refresh_tick = menu_utils_get_tick();
}
}
/**
* @brief 菜单组件初始化(必须首先调用)
* @param global_ctx 全局上下文指针
* @return 错误码
*/
MenuErrCode menu_init(MenuGlobalCtx* global_ctx)
{
if (global_ctx == NULL)
{
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; // 设置初始状态
// 初始化事件队列
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 错误码
*/
// 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)
{
if (global_ctx == NULL || !global_ctx->is_initialized)
{
return MENU_ERR_NOT_INITIALIZED;
}
MenuCoreCtx* ctx = &global_ctx->core;
// 检查事件队列是否已满
if (ctx->event_queue.count >= MENU_CONFIG_EVENT_QUEUE_LEN)
{
return MENU_ERR_EVENT_QUEUE_FULL;
}
// 入队事件
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 菜单名称字符串(或多语言索引)
* @param enter_cb 进入该菜单的回调函数NULL表示无
* @param exit_cb 退出该菜单的回调函数NULL表示无
* @return 错误码
*/
MenuErrCode menu_register_node(MenuGlobalCtx* global_ctx, MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb)
{
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++)
{
if (!ctx->nodes[i].flags.is_registered)
{
new_node = &ctx->nodes[i];
break;
}
}
if (new_node == NULL)
{
return MENU_ERR_OUT_OF_MEMORY;
}
// 检查节点ID是否已存在
if (menu_core_find_node(global_ctx, node_id) != NULL)
{
return MENU_ERR_INVALID_PARAM;
}
// 初始化新节点
MENU_MEM_SET_ZERO(new_node, sizeof(MenuNode));
new_node->id = node_id;
new_node->parent_id = parent_id;
new_node->name = name_str;
new_node->enter_cb = enter_cb;
new_node->exit_cb = exit_cb;
new_node->flags.is_registered = true;
new_node->flags.is_selected = false;
// 如果是根节点parent_id为0
if (parent_id == 0)
{
// 检查是否已有根节点
if (ctx->current_node_id != 0)
{
// 已有根节点,将新节点作为兄弟节点添加到根节点之后
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;
}
root_node->next_sibling = new_node;
new_node->prev_sibling = root_node;
}
else
{
// 第一个根节点,设置为当前节点
ctx->current_node_id = node_id;
menu_core_stack_push(global_ctx, node_id);
}
}
else
{
// 查找父节点
MenuNode* parent_node = menu_core_find_node(global_ctx, parent_id);
if (parent_node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 将新节点添加到父节点的子节点列表中
if (parent_node->first_child == NULL)
{
// 父节点还没有子节点,作为第一个子节点
parent_node->first_child = new_node;
}
else
{
// 已有子节点,添加到最后一个子节点之后
MenuNode* last_child = parent_node->first_child;
while (last_child->next_sibling != NULL)
{
last_child = last_child->next_sibling;
}
last_child->next_sibling = new_node;
new_node->prev_sibling = last_child;
}
}
return MENU_OK;
}
/**
* @brief 获取当前选中的菜单节点ID
* @param global_ctx 全局上下文指针
* @param node_id 输出参数当前节点ID
* @return 错误码
*/
MenuErrCode menu_get_current_node(MenuGlobalCtx* global_ctx, MenuNodeId* node_id)
{
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;
}
/**
* @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

View File

@ -1,636 +0,0 @@
/**
* @file menu_modbus.c
* @brief 菜单参数-Modbus寄存器映射注册、查询、数据双向转换工业级类型安全、字节序适配
*/
#include "../api/menu.h"
#include "menu_core.h"
#include "menu_data.h"
#include "menu_def.h"
#include "menu_modbus.h"
#include <stddef.h>
#include <string.h>
#if MENU_CONFIG_ENABLE_MODBUS_MAP
/************************** 内部辅助函数 **************************/
/**
* @brief 转换Modbus协议地址到实际偏移地址如40001→000001→0
* @param reg_type 寄存器类型
* @param reg_addr 协议地址如40001
* @param offset 输出参数,实际偏移地址
* @return 错误码
*/
static MenuErrCode menu_modbus_convert_addr(ModbusRegType reg_type, uint16_t reg_addr, uint16_t* offset)
{
if (offset == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
// 校验协议地址范围符合Modbus标准
switch (reg_type)
{
case MODBUS_REG_TYPE_COIL:
if (reg_addr < 1 || reg_addr > 9999)
{
MENU_DEBUG("Modbus coil addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 1;
break;
case MODBUS_REG_TYPE_DISCRETE_INPUT:
if (reg_addr < 10001 || reg_addr > 19999)
{
MENU_DEBUG("Modbus discrete input addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 10001;
break;
case MODBUS_REG_TYPE_HOLDING_REG:
if (reg_addr < 40001 || reg_addr > 49999)
{
MENU_DEBUG("Modbus holding reg addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 40001;
break;
case MODBUS_REG_TYPE_INPUT_REG:
if (reg_addr < 30001 || reg_addr > 39999)
{
MENU_DEBUG("Modbus input reg addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 30001;
break;
default:
return MENU_ERR_INVALID_PARAM;
}
// 校验实际偏移地址不超过配置的最大值
// 注意MENU_CONFIG_MODBUS_MAX_ADDR 不应超过 uint16_t 的最大值 0xFFFF
if (*offset > (uint16_t)MENU_CONFIG_MODBUS_MAX_ADDR)
{
MENU_DEBUG("Modbus reg offset out of range: %d, max allowed: %d", *offset, MENU_CONFIG_MODBUS_MAX_ADDR);
return MENU_ERR_INVALID_PARAM;
}
return MENU_OK;
}
/**
* @brief 校验寄存器类型与参数类型的匹配性(工业级:类型安全)
* @param param_type 参数类型
* @param reg_type 寄存器类型
* @return 错误码
*/
static MenuErrCode menu_modbus_check_type_match(MenuParamType param_type, ModbusRegType reg_type)
{
// 1位寄存器线圈、离散输入只能匹配布尔型/8位整型参数
if (reg_type == MODBUS_REG_TYPE_COIL || reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT)
{
if (param_type != MENU_PARAM_TYPE_INT8 && param_type != MENU_PARAM_TYPE_UINT8)
{
MENU_DEBUG("Modbus 1-bit reg not match param type: %d", param_type);
return MENU_ERR_INVALID_PARAM;
}
return MENU_OK;
}
// 16位寄存器保持、输入可匹配所有类型16/32/浮点需多寄存器)
return MENU_OK;
}
/**
* @brief 字节序转换适配不同的Modbus字节序工业级兼容不同从站
* @param data 数据缓冲区16位为单位
* @param len 数据长度16位的数量
* @param byte_order 字节序0-小端1-大端2-Modbus标准字小端字节大端
* @param is_reverse 是否反向转换(用于读取/写入)
*/
static void menu_modbus_byte_order_convert(uint16_t* data, uint8_t len, uint8_t byte_order, bool is_reverse)
{
if (data == NULL || len == 0)
{
return;
}
// 默认使用配置的字节序
if (byte_order > 2)
{
byte_order = MENU_CONFIG_MODBUS_BYTE_ORDER;
}
switch (byte_order)
{
case 0: // 小端(低字节在前,高字节在后)
if (is_reverse)
{
for (uint8_t i = 0; i < len; i++)
{
data[i] = (data[i] << 8) | (data[i] >> 8);
}
}
break;
case 1: // 大端(高字节在前,低字节在后)
// 无需转换Modbus默认是大端
break;
case 2: // Modbus标准字小端字节大端如32位值0x12345678→0x5678 0x1234
if (is_reverse)
{
// 读取时将Modbus顺序转换为主机顺序
for (uint8_t i = 0; i < len; i += 2)
{
if (i + 1 < len)
{
uint16_t temp = data[i];
data[i] = data[i + 1];
data[i + 1] = temp;
}
}
}
else
{
// 写入时将主机顺序转换为Modbus顺序
for (uint8_t i = 0; i < len; i += 2)
{
if (i + 1 < len)
{
uint16_t temp = data[i];
data[i] = data[i + 1];
data[i + 1] = temp;
}
}
}
break;
default:
break;
}
}
/**
* @brief 查找Modbus映射通过参数ID
* @param ctx 菜单全局上下文
* @param param_id 参数ID
* @return 映射内部结构体指针NULL表示未找到
*/
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 (modbus_ctx->maps[i].is_registered && modbus_ctx->maps[i].pub.param_id == param_id)
{
return &modbus_ctx->maps[i];
}
}
return NULL;
}
/**
* @brief 查找Modbus映射通过寄存器类型和地址
* @param ctx 菜单全局上下文
* @param reg_type 寄存器类型
* @param reg_addr 寄存器地址(协议地址)
* @return 映射内部结构体指针NULL表示未找到
*/
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 (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 &modbus_ctx->maps[i];
}
}
return NULL;
}
/************************** 对外接口实现 **************************/
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(ctx, param_id, &dummy_val);
if (err != MENU_OK)
{
MENU_DEBUG("Modbus map param %d not found", param_id);
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验寄存器地址和转换为偏移
uint16_t reg_offset;
err = menu_modbus_convert_addr(reg_type, reg_addr, &reg_offset);
if (err != MENU_OK)
{
return err;
}
// 3. 校验寄存器数量至少1个
if (reg_count == 0)
{
MENU_DEBUG("Modbus map reg count is zero");
return MENU_ERR_INVALID_PARAM;
}
// 4. 由于无法直接获取参数类型,这里跳过类型匹配校验
// 实际项目中,可以考虑在参数模块中添加获取参数类型的接口
// err = menu_modbus_check_type_match(param->type, reg_type);
// if (err != MENU_OK)
// {
// return err;
// }
// 5. 校验读写权限(与寄存器类型联动,如离散输入只能只读)
#if MENU_CONFIG_MODBUS_PERMISSION
if ((reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT || reg_type == MODBUS_REG_TYPE_INPUT_REG) && perm != MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus input reg only support read");
return MENU_ERR_INVALID_PARAM;
}
if (reg_type == MODBUS_REG_TYPE_COIL && perm == MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus coil not support read only");
return MENU_ERR_INVALID_PARAM;
}
#endif
// 6. 检查映射是否已存在
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. 查找空闲的映射位置(从全局上下文获取)
MenuModbusCtx* modbus_ctx = &ctx->modbus;
ModbusMapInternal* map = NULL;
for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++)
{
if (!modbus_ctx->maps[i].is_registered)
{
map = &modbus_ctx->maps[i];
break;
}
}
if (map == NULL)
{
MENU_DEBUG("Modbus map out of memory: max %d", MENU_CONFIG_MAX_MODBUS_MAPS);
return MENU_ERR_OUT_OF_MEMORY;
}
// 8. 获取参数类型
MenuParamType param_type;
err = menu_param_get_type(ctx, param_id, &param_type);
if (err != MENU_OK)
{
return err;
}
// 9. 校验寄存器类型与参数类型匹配
err = menu_modbus_check_type_match(param_type, reg_type);
if (err != MENU_OK)
{
return err;
}
// 10. 初始化映射关系
MENU_MEM_SET_ZERO(map, sizeof(ModbusMapInternal));
map->pub.param_id = param_id;
map->pub.reg_type = reg_type;
map->pub.reg_addr = reg_addr;
map->pub.reg_count = reg_count;
map->pub.perm = perm;
map->pub.byte_order = byte_order;
map->param_type = param_type;
map->reg_addr_offset = reg_offset;
map->is_registered = true;
MENU_DEBUG("Modbus map registered: param %d → reg %d:%d (count %d, perm %d)",
param_id, reg_type, reg_addr, reg_count, perm);
return MENU_OK;
}
MenuErrCode menu_modbus_map_query_by_param(MenuGlobalCtx* ctx, uint16_t param_id, ModbusMap* map)
{
if (ctx == NULL || map == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
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);
return MENU_ERR_NODE_NOT_FOUND;
}
// 复制对外暴露的信息
*map = internal_map->pub;
return MENU_OK;
}
MenuErrCode menu_modbus_map_query_by_reg(MenuGlobalCtx* ctx, ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map)
{
if (ctx == NULL || map == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
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);
return MENU_ERR_NODE_NOT_FOUND;
}
// 复制对外暴露的信息
*map = internal_map->pub;
return MENU_OK;
}
MenuErrCode menu_modbus_map_param_to_reg(MenuGlobalCtx* ctx, uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len)
{
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(ctx, param_id);
if (map == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验读写权限
#if MENU_CONFIG_MODBUS_PERMISSION
if (map->pub.perm == MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus map param %d is read only", param_id);
return MENU_ERR_HW_PORT_ERROR; // 复用原有错误码
}
#endif
// 3. 获取参数值
float param_val;
MenuErrCode err = menu_param_get_value(ctx, param_id, &param_val);
if (err != MENU_OK)
{
MENU_DEBUG("Failed to get param value: %d", err);
return err;
}
// 4. 转换参数值到寄存器数据(按类型和寄存器数量)
uint16_t* reg_data = (uint16_t*)reg_buf;
uint8_t req_len = map->pub.reg_count;
// 检查缓冲区长度是否足够
if (*buf_len < req_len * 2) // 寄存器数据按16位计算每个16位占2字节
{
MENU_DEBUG("Modbus reg buf too small: need %d bytes, got %d bytes", req_len * 2, *buf_len);
return MENU_ERR_INVALID_PARAM;
}
// 清空缓冲区
memset(reg_data, 0, req_len * 2);
// 根据参数类型转换
// 使用之前保存的参数类型不需要再次调用menu_param_find
switch (map->pub.reg_type)
{
case MODBUS_REG_TYPE_COIL:
case MODBUS_REG_TYPE_DISCRETE_INPUT:
// 1位寄存器取参数值的最低位
if (req_len != 1)
{
MENU_DEBUG("Invalid reg count for 1-bit reg: %d, expected 1", req_len);
return MENU_ERR_INVALID_PARAM;
}
reg_data[0] = (uint16_t)(param_val > 0 ? 1 : 0);
break;
case MODBUS_REG_TYPE_HOLDING_REG:
case MODBUS_REG_TYPE_INPUT_REG:
// 16位寄存器根据参数类型和数量转换
if (map->param_type == MENU_PARAM_TYPE_INT16 || map->param_type == MENU_PARAM_TYPE_UINT16)
{
if (req_len != 1)
{
MENU_DEBUG("Invalid reg count for 16-bit param: %d, expected 1", req_len);
return MENU_ERR_INVALID_PARAM;
}
reg_data[0] = (uint16_t)param_val;
}
else if (map->param_type == MENU_PARAM_TYPE_INT32 || map->param_type == MENU_PARAM_TYPE_UINT32)
{
if (req_len != 2)
{
MENU_DEBUG("Invalid reg count for 32-bit param: %d, expected 2", req_len);
return MENU_ERR_INVALID_PARAM;
}
uint32_t val = (uint32_t)param_val;
reg_data[0] = (uint16_t)(val & 0xFFFF);
reg_data[1] = (uint16_t)((val >> 16) & 0xFFFF);
}
else if (map->param_type == MENU_PARAM_TYPE_FLOAT)
{
if (req_len != 2)
{
MENU_DEBUG("Invalid reg count for float param: %d, expected 2", req_len);
return MENU_ERR_INVALID_PARAM;
}
// 浮点型转换为32位二进制占2个16位寄存器
union {
float f;
uint32_t u32;
} float_val;
float_val.f = param_val;
reg_data[0] = (uint16_t)(float_val.u32 & 0xFFFF);
reg_data[1] = (uint16_t)((float_val.u32 >> 16) & 0xFFFF);
}
else if (map->param_type == MENU_PARAM_TYPE_INT8 || map->param_type == MENU_PARAM_TYPE_UINT8)
{
if (req_len != 1)
{
MENU_DEBUG("Invalid reg count for 8-bit param: %d, expected 1", req_len);
return MENU_ERR_INVALID_PARAM;
}
reg_data[0] = (uint16_t)param_val;
}
break;
default:
MENU_DEBUG("Invalid reg type: %d", map->pub.reg_type);
return MENU_ERR_INVALID_PARAM;
}
// 5. 字节序转换(写入寄存器前)
menu_modbus_byte_order_convert(reg_data, req_len, map->pub.byte_order, false);
// 6. 设置实际写入长度
*buf_len = req_len * 2;
MENU_DEBUG("Modbus param %d to reg: val %f → reg data len %d", param_id, param_val, *buf_len);
return MENU_OK;
}
MenuErrCode menu_modbus_map_reg_to_param(MenuGlobalCtx* ctx, uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len)
{
if (ctx == NULL || reg_buf == NULL || buf_len == 0)
{
return MENU_ERR_INVALID_PARAM;
}
// 1. 查找映射关系
ModbusMapInternal* map = menu_modbus_find_by_param(ctx, param_id);
if (map == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验读写权限
#if MENU_CONFIG_MODBUS_PERMISSION
if (map->pub.perm == MODBUS_PERM_WRITE_ONLY)
{
MENU_DEBUG("Modbus map param %d is write only", param_id);
return MENU_ERR_HW_PORT_ERROR; // 复用原有错误码
}
#endif
// 3. 校验缓冲区长度
uint8_t req_len = map->pub.reg_count * 2;
if (buf_len < req_len)
{
MENU_DEBUG("Modbus reg buf too small: need %d, got %d", req_len, buf_len);
return MENU_ERR_INVALID_PARAM;
}
// 4. 复制寄存器数据并进行字节序转换(读取寄存器后)
uint16_t reg_data[MENU_CONFIG_MAX_MODBUS_MAPS * 2] = {0}; // 足够大的临时缓冲区
// 复制数据到临时缓冲区
memcpy(reg_data, reg_buf, req_len);
// 字节序转换
menu_modbus_byte_order_convert(reg_data, map->pub.reg_count, map->pub.byte_order, true);
// 5. 转换寄存器数据到参数值
float param_val = 0.0f;
// 使用之前保存的参数类型不需要再次调用menu_param_find
switch (map->pub.reg_type)
{
case MODBUS_REG_TYPE_COIL:
case MODBUS_REG_TYPE_DISCRETE_INPUT:
// 1位寄存器0→0非0→1
if (map->pub.reg_count != 1)
{
MENU_DEBUG("Invalid reg count for 1-bit reg: %d, expected 1", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
param_val = (reg_data[0] & 0x01) ? 1.0f : 0.0f;
break;
case MODBUS_REG_TYPE_HOLDING_REG:
case MODBUS_REG_TYPE_INPUT_REG:
// 16位寄存器根据参数类型转换
if (map->param_type == MENU_PARAM_TYPE_INT8)
{
if (map->pub.reg_count != 1)
{
MENU_DEBUG("Invalid reg count for 8-bit param: %d, expected 1", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
param_val = (int8_t)reg_data[0];
}
else if (map->param_type == MENU_PARAM_TYPE_UINT8)
{
if (map->pub.reg_count != 1)
{
MENU_DEBUG("Invalid reg count for 8-bit param: %d, expected 1", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
param_val = (uint8_t)reg_data[0];
}
else if (map->param_type == MENU_PARAM_TYPE_INT16)
{
if (map->pub.reg_count != 1)
{
MENU_DEBUG("Invalid reg count for 16-bit param: %d, expected 1", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
param_val = (int16_t)reg_data[0];
}
else if (map->param_type == MENU_PARAM_TYPE_UINT16)
{
if (map->pub.reg_count != 1)
{
MENU_DEBUG("Invalid reg count for 16-bit param: %d, expected 1", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
param_val = (uint16_t)reg_data[0];
}
else if (map->param_type == MENU_PARAM_TYPE_INT32)
{
if (map->pub.reg_count != 2)
{
MENU_DEBUG("Invalid reg count for 32-bit param: %d, expected 2", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = (int32_t)val;
}
else if (map->param_type == MENU_PARAM_TYPE_UINT32)
{
if (map->pub.reg_count != 2)
{
MENU_DEBUG("Invalid reg count for 32-bit param: %d, expected 2", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = val;
}
else if (map->param_type == MENU_PARAM_TYPE_FLOAT)
{
if (map->pub.reg_count != 2)
{
MENU_DEBUG("Invalid reg count for float param: %d, expected 2", map->pub.reg_count);
return MENU_ERR_INVALID_PARAM;
}
union {
float f;
uint32_t u32;
} float_val;
float_val.u32 = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = float_val.f;
}
break;
default:
MENU_DEBUG("Invalid reg type: %d", map->pub.reg_type);
return MENU_ERR_INVALID_PARAM;
}
// 6. 设置参数值(自动进行范围检查)
MenuErrCode err = menu_param_set_value(ctx, param_id, param_val);
if (err != MENU_OK)
{
return err;
}
MENU_DEBUG("Modbus reg to param %d: reg data len %d → val %f", param_id, buf_len, param_val);
return MENU_OK;
}
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP

View File

@ -1,207 +0,0 @@
/**
* @file menu_lang.c
* @brief 菜单多语言支持模块(可裁剪功能)
*/
#include "menu_core.h"
#include "menu_data.h"
#if MENU_CONFIG_ENABLE_LANG
/************************** 内部函数声明 **************************/
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(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 (lang_ctx->strs[i].str_id == str_id && lang_ctx->strs[i].lang_id == lang_id)
{
return lang_ctx->strs[i].str;
}
}
return NULL;
}
/**
* @brief 注册语言字符串
* @param global_ctx 全局上下文指针
* @param str_id 字符串ID
* @param lang_id 语言ID
* @param str 字符串内容
* @return 错误码
*/
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 (!lang_ctx->strs[i].str || (lang_ctx->strs[i].str_id == str_id && lang_ctx->strs[i].lang_id == lang_id))
{
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;
}
}
return MENU_ERR_OUT_OF_MEMORY;
}
/**
* @brief 获取当前语言的字符串
* @param global_ctx 全局上下文指针
* @param str_id 字符串ID
* @return 字符串内容NULL表示未找到
*/
const char* menu_lang_get_str(MenuGlobalCtx* global_ctx, uint16_t str_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(global_ctx, str_id, 0);
}
return str;
}
/**
* @brief 设置当前语言
* @param global_ctx 全局上下文指针
* @param lang_id 语言ID如0-中文1-英文)
* @return 错误码
*/
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;
}
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(global_ctx);
return MENU_OK;
}
/**
* @brief 获取当前语言
* @param global_ctx 全局上下文指针
* @param lang_id 输出参数当前语言ID
* @return 错误码
*/
MenuErrCode menu_lang_get_current(MenuGlobalCtx* global_ctx, uint8_t* lang_id)
{
if (global_ctx == NULL || !global_ctx->is_initialized || lang_id == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
MenuLangCtx* lang_ctx = &global_ctx->lang;
*lang_id = lang_ctx->current_lang_id;
return MENU_OK;
}
/**
* @brief 获取支持的最大语言数量
* @return 最大语言数量
*/
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

View File

@ -1,399 +0,0 @@
/**
* @file menu_param.c
* @brief 菜单参数管理模块(可裁剪功能)
*/
#include "menu_core.h"
#include "menu_data.h"
#if MENU_CONFIG_ENABLE_PARAM
/************************** 内部函数声明 **************************/
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(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 (param_ctx->params[i].is_registered && param_ctx->params[i].id == param_id)
{
return &param_ctx->params[i];
}
}
return NULL;
}
/**
* @brief 将浮点值转换为参数内部值(根据类型)
* @param param 参数指针
* @param value 浮点值
* @return 转换后的内部值(通过指针返回)
*/
static void menu_param_float_to_internal(MenuParam* param, float value)
{
MENU_ASSERT(param != NULL);
// 确保值在范围内
if (value < param->min_val)
{
value = param->min_val;
}
else if (value > param->max_val)
{
value = param->max_val;
}
// 应用缩放因子
value /= param->scale;
// 根据参数类型转换
switch (param->type)
{
case MENU_PARAM_TYPE_INT8:
param->value.i8 = (int8_t)value;
break;
case MENU_PARAM_TYPE_UINT8:
param->value.u8 = (uint8_t)value;
break;
case MENU_PARAM_TYPE_INT16:
param->value.i16 = (int16_t)value;
break;
case MENU_PARAM_TYPE_UINT16:
param->value.u16 = (uint16_t)value;
break;
case MENU_PARAM_TYPE_INT32:
param->value.i32 = (int32_t)value;
break;
case MENU_PARAM_TYPE_UINT32:
param->value.u32 = (uint32_t)value;
break;
case MENU_PARAM_TYPE_FLOAT:
param->value.f = value;
break;
default:
break;
}
}
/**
* @brief 将参数内部值转换为浮点值(根据类型)
* @param param 参数指针
* @return 浮点值
*/
static float menu_param_internal_to_float(const MenuParam* param)
{
MENU_ASSERT(param != NULL);
float value = 0.0f;
// 根据参数类型转换
switch (param->type)
{
case MENU_PARAM_TYPE_INT8:
value = (float)param->value.i8;
break;
case MENU_PARAM_TYPE_UINT8:
value = (float)param->value.u8;
break;
case MENU_PARAM_TYPE_INT16:
value = (float)param->value.i16;
break;
case MENU_PARAM_TYPE_UINT16:
value = (float)param->value.u16;
break;
case MENU_PARAM_TYPE_INT32:
value = (float)param->value.i32;
break;
case MENU_PARAM_TYPE_UINT32:
value = (float)param->value.u32;
break;
case MENU_PARAM_TYPE_FLOAT:
value = param->value.f;
break;
default:
break;
}
// 应用缩放因子
return value * param->scale;
}
/**
* @brief 注册参数到菜单节点(参数管理功能)
* @param global_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* 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(global_ctx, node_id);
if (node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 检查参数ID是否已存在
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 (!param_ctx->params[i].is_registered)
{
param = &param_ctx->params[i];
break;
}
}
if (param == NULL)
{
return MENU_ERR_OUT_OF_MEMORY;
}
// 初始化参数
MENU_MEM_SET_ZERO(param, sizeof(MenuParam));
param->id = param_id;
param->type = type;
param->min_val = min_val;
param->max_val = max_val;
param->scale = scale;
param->is_registered = true;
// 设置默认值
menu_param_float_to_internal(param, default_val);
// 保存默认值
menu_param_float_to_internal((MenuParam*)&param->default_val, default_val);
// 将参数与菜单节点绑定
node->param_id = param_id;
MENU_DEBUG("Param registered: ID=%d, Type=%d, Range=[%0.2f, %0.2f], Default=%0.2f, Scale=%0.2f",
param_id, type, min_val, max_val, default_val, scale);
return MENU_OK;
}
/**
* @brief 设置参数值
* @param global_ctx 全局上下文指针
* @param param_id 参数ID
* @param value 新值(浮点型,内部自动转换)
* @return 错误码
*/
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(global_ctx, param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 设置新值
menu_param_float_to_internal(param, value);
MENU_DEBUG("Param set: ID=%d, Value=%0.2f (Internal: %0.2f)",
param_id, value, menu_param_internal_to_float(param));
return MENU_OK;
}
/**
* @brief 获取参数值
* @param global_ctx 全局上下文指针
* @param param_id 参数ID
* @param value 输出参数,当前值(浮点型)
* @return 错误码
*/
MenuErrCode menu_param_get_value(MenuGlobalCtx* global_ctx, uint16_t param_id, float* value)
{
if (global_ctx == NULL || !global_ctx->is_initialized || value == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
// 查找参数
MenuParam* param = menu_param_find(global_ctx, param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 获取当前值
*value = menu_param_internal_to_float(param);
MENU_DEBUG("Param get: ID=%d, Value=%0.2f", param_id, *value);
return MENU_OK;
}
/**
* @brief 将参数恢复为默认值
* @param global_ctx 全局上下文指针
* @param param_id 参数ID
* @return 错误码
*/
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(global_ctx, param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 恢复默认值
float default_val = menu_param_internal_to_float((const MenuParam*)&param->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(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(global_ctx, param_id, &current_val);
if (err != MENU_OK)
{
return err;
}
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(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(global_ctx, param_id, &current_val);
if (err != MENU_OK)
{
return err;
}
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(MenuGlobalCtx* global_ctx, uint16_t param_id, MenuParamType* type)
{
if (global_ctx == NULL || !global_ctx->is_initialized || type == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
MenuParam* param = menu_param_find(global_ctx, param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 返回参数类型
*type = param->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

View File

@ -1,39 +0,0 @@
/**
* @file menu_param.h
* @brief 菜单参数管理内部类型定义(用户无需关心)
*/
#ifndef MENU_PARAM_H
#define MENU_PARAM_H
#include "menu_core.h"
#include <stdint.h>
#include <stdbool.h>
/**
* @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

View File

@ -1,137 +0,0 @@
/**
* @file menu_utils.c
* @brief 菜单工具模块:断言、打印、系统滴答等
*/
#include "menu_def.h"
#include "menu_port.h"
#include <stdarg.h>
#include <stdio.h>
/**
* @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 断言失败处理函数
* @param file 文件名
* @param line 行号
*/
void menu_utils_assert_failed(const char* file, uint32_t line)
{
// 调用port层的打印接口输出断言信息
menu_utils_log(MENU_LOG_LEVEL_CRITICAL, file, line, "Assertion failed");
// 可以在这里添加其他处理,如触发硬件异常、保存上下文等
// 例如menu_port_trigger_exception();
}
/**
* @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 ... 可变参数
*/
void menu_utils_printf(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
// 调用port层的可变参数打印接口
menu_port_printf(fmt, args);
va_end(args);
}
/**
* @brief 获取系统滴答时间ms对接port层
* @return 当前滴答时间
*/
uint32_t menu_utils_get_tick(void)
{
// 调用port层的系统滴答获取接口
return menu_port_get_tick();
}
/**
* @brief 计算两个32位无符号整数的差值处理溢出情况
* @param now 当前时间
* @param last 上次时间
* @return 差值ms
*/
uint32_t menu_utils_diff_tick(uint32_t now, uint32_t last)
{
if (now >= last)
{
return now - last;
}
else
{
// 处理溢出情况
return (UINT32_MAX - last) + now + 1;
}
}
/**
* @brief 延迟函数毫秒级对接port层
* @param ms 延迟时间ms
*/
void menu_utils_delay_ms(uint32_t ms)
{
// 调用port层的延迟接口
menu_port_delay_ms(ms);
}

View File

@ -1,471 +0,0 @@
/**
* @file menu_test.c
* @brief 菜单组件单元测试
*/
#include "menu.h"
#include "menu_port.h"
#include <stdio.h>
#include <stdlib.h>
/************************** 测试用例 **************************/
/**
* @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, &current_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, &current_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, &current_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;
}

819
update.md
View File

@ -1,819 +0,0 @@
你需要在已有的工业级嵌入式菜单组件中,拓展**参数与Modbus寄存器地址的灵活映射功能**让每个参数能根据自身性质数据类型、读写权限、业务属性匹配对应的Modbus寄存器包括线圈、离散输入、保持寄存器、输入寄存器支持寄存器地址的配置、映射查询以及参数值与寄存器数据的双向转换同时保持与原有组件的解耦和工业级的鲁棒性。
以下是基于原有目录结构的详细实现方案,遵循**分层解耦、静态内存、可配置、类型安全**的工业级设计原则。
---
## 一、设计核心思路(工业级考量)
1. **解耦设计**Modbus映射模块作为独立的功能扩展层依赖原有参数模块的接口不侵入核心逻辑。
2. **静态内存**使用静态数组存储Modbus映射关系避免动态分配适配嵌入式内存约束。
3. **类型匹配**参数类型与Modbus寄存器数据类型1位、8位、16位、32位、浮点自动转换支持大小端配置。
4. **权限控制**:映射时指定寄存器的读写权限(只读/只写/读写),与参数的权限联动。
5. **灵活配置**支持单参数映射单寄存器、单参数映射多寄存器如32位浮点占2个16位保持寄存器
6. **可裁剪**通过宏开关控制Modbus映射功能的启用/关闭,不影响原有功能。
---
## 二、代码实现(按目录结构补充/修改)
### 1. 对外API层`menu/api/`
#### 1.1 修改`menu/api/menu_config.h`添加Modbus映射配置宏
在原有配置后新增Modbus相关配置保持可裁剪性
```c
/************************** Modbus映射功能配置可裁剪 **************************/
/**
* @brief 是否启用参数-Modbus寄存器映射功能
*/
#define MENU_CONFIG_ENABLE_MODBUS_MAP 1
/**
* @brief 最大Modbus映射数量静态内存根据项目调整
*/
#define MENU_CONFIG_MAX_MODBUS_MAPS 16
/**
* @brief Modbus寄存器地址最大值根据实际Modbus从站配置
*/
#define MENU_CONFIG_MODBUS_MAX_ADDR 0xFFFF
/**
* @brief 默认Modbus字节序0-小端1-大端2-字小端字节大端Modbus标准
*/
#define MENU_CONFIG_MODBUS_BYTE_ORDER 2
/**
* @brief 是否启用Modbus映射权限校验与参数权限联动
*/
#define MENU_CONFIG_MODBUS_PERMISSION 1
```
#### 1.2 修改`menu/api/menu.h`添加Modbus映射的对外接口和类型
在原有内容后新增Modbus相关的类型和接口作为用户可调用的API
```c
/************************** Modbus映射功能启用时有效 **************************/
#if MENU_CONFIG_ENABLE_MODBUS_MAP
/**
* @brief Modbus寄存器类型符合Modbus协议标准
*/
typedef enum {
MODBUS_REG_TYPE_COIL = 0, ///< 线圈1位地址范围00001-09999
MODBUS_REG_TYPE_DISCRETE_INPUT, ///< 离散输入1位地址范围10001-19999
MODBUS_REG_TYPE_HOLDING_REG, ///< 保持寄存器16位地址范围40001-49999
MODBUS_REG_TYPE_INPUT_REG, ///< 输入寄存器16位地址范围30001-39999
} ModbusRegType;
/**
* @brief Modbus寄存器读写权限
*/
typedef enum {
MODBUS_PERM_READ_ONLY = 0, ///< 只读(如离散输入、输入寄存器)
MODBUS_PERM_WRITE_ONLY, ///< 只写(如线圈)
MODBUS_PERM_READ_WRITE, ///< 读写(如保持寄存器)
} ModbusPerm;
/**
* @brief Modbus映射结构体对外只读内部初始化
*/
typedef struct {
uint16_t param_id; ///< 关联的参数ID
ModbusRegType reg_type; ///< 寄存器类型
uint16_t reg_addr; ///< 寄存器起始地址协议地址如40001
uint8_t reg_count; ///< 占用寄存器数量如32位浮点占2个16位寄存器
ModbusPerm perm; ///< 读写权限
uint8_t byte_order; ///< 字节序0-小端1-大端2-Modbus标准
} ModbusMap;
/**
* @brief 注册参数与Modbus寄存器的映射关系
* @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(uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order);
/**
* @brief 根据参数ID查询Modbus映射关系
* @param param_id 参数ID
* @param map 输出参数,映射关系结构体
* @return 错误码
*/
MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map);
/**
* @brief 根据寄存器地址和类型查询Modbus映射关系
* @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);
/**
* @brief 将参数值写入对应的Modbus寄存器双向转换参数值→寄存器数据
* @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);
/**
* @brief 将Modbus寄存器数据读取到参数值双向转换寄存器数据→参数值
* @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);
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
```
### 2. 内部头文件层(`menu/internal/`
#### 2.1 修改`menu/internal/menu_core.h`添加Modbus映射的内部类型
在原有核心类型后新增Modbus映射的内部结构体比对外结构体更详细包含数据转换的辅助信息
```c
#if MENU_CONFIG_ENABLE_MODBUS_MAP
/**
* @brief Modbus映射内部结构体包含数据转换的辅助信息用户无需关心
*/
typedef struct {
ModbusMap pub; ///< 对外暴露的映射信息(兼容外部结构体)
bool is_registered; ///< 是否已注册
MenuParamType param_type; ///< 关联的参数类型(缓存,避免重复查询)
uint16_t reg_addr_offset; ///< 寄存器地址偏移协议地址→实际地址如40001→0
} ModbusMapInternal;
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
```
#### 2.2 修改`menu/internal/menu_data.h`添加Modbus映射的静态全局数据
在原有共享数据后新增Modbus映射的静态数组静态内存无动态分配
```c
#if MENU_CONFIG_ENABLE_MODBUS_MAP
/**
* @brief Modbus映射上下文静态全局仅内部访问
*/
static ModbusMapInternal s_menu_modbus_maps[MENU_CONFIG_MAX_MODBUS_MAPS];
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
```
### 3. 功能扩展层(`menu/src/features/`
#### 3.1 新增`menu/src/features/menu_modbus.c`Modbus映射核心逻辑
实现Modbus映射的注册、查询、数据双向转换与原有参数模块无缝集成
```c
/**
* @file menu_modbus.c
* @brief 菜单参数-Modbus寄存器映射注册、查询、数据双向转换工业级类型安全、字节序适配
*/
#if MENU_CONFIG_ENABLE_MODBUS_MAP
#include "menu.h"
#include "menu_core.h"
#include "menu_data.h"
#include "menu_def.h"
#include <stddef.h>
#include <string.h>
/************************** 内部辅助函数 **************************/
/**
* @brief 转换Modbus协议地址到实际偏移地址如40001→000001→0
* @param reg_type 寄存器类型
* @param reg_addr 协议地址如40001
* @param offset 输出参数,实际偏移地址
* @return 错误码
*/
static MenuErrCode menu_modbus_convert_addr(ModbusRegType reg_type, uint16_t reg_addr, uint16_t* offset)
{
if (offset == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
// 校验协议地址范围符合Modbus标准
switch (reg_type)
{
case MODBUS_REG_TYPE_COIL:
if (reg_addr < 1 || reg_addr > 9999)
{
MENU_DEBUG("Modbus coil addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 1;
break;
case MODBUS_REG_TYPE_DISCRETE_INPUT:
if (reg_addr < 10001 || reg_addr > 19999)
{
MENU_DEBUG("Modbus discrete input addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 10001;
break;
case MODBUS_REG_TYPE_HOLDING_REG:
if (reg_addr < 40001 || reg_addr > 49999)
{
MENU_DEBUG("Modbus holding reg addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 40001;
break;
case MODBUS_REG_TYPE_INPUT_REG:
if (reg_addr < 30001 || reg_addr > 39999)
{
MENU_DEBUG("Modbus input reg addr out of range: %d", reg_addr);
return MENU_ERR_INVALID_PARAM;
}
*offset = reg_addr - 30001;
break;
default:
return MENU_ERR_INVALID_PARAM;
}
// 校验实际偏移地址不超过配置的最大值
if (*offset > MENU_CONFIG_MODBUS_MAX_ADDR)
{
MENU_DEBUG("Modbus reg offset out of range: %d", *offset);
return MENU_ERR_INVALID_PARAM;
}
return MENU_OK;
}
/**
* @brief 校验寄存器类型与参数类型的匹配性(工业级:类型安全)
* @param param_type 参数类型
* @param reg_type 寄存器类型
* @return 错误码
*/
static MenuErrCode menu_modbus_check_type_match(MenuParamType param_type, ModbusRegType reg_type)
{
// 1位寄存器线圈、离散输入只能匹配布尔型/8位整型参数
if (reg_type == MODBUS_REG_TYPE_COIL || reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT)
{
if (param_type != MENU_PARAM_TYPE_INT8 && param_type != MENU_PARAM_TYPE_UINT8)
{
MENU_DEBUG("Modbus 1-bit reg not match param type: %d", param_type);
return MENU_ERR_INVALID_PARAM;
}
return MENU_OK;
}
// 16位寄存器保持、输入可匹配所有类型16/32/浮点需多寄存器)
return MENU_OK;
}
/**
* @brief 字节序转换适配不同的Modbus字节序工业级兼容不同从站
* @param data 数据缓冲区16位为单位
* @param len 数据长度16位的数量
* @param byte_order 字节序0-小端1-大端2-Modbus标准字小端字节大端
* @param is_reverse 是否反向转换(用于读取/写入)
*/
static void menu_modbus_byte_order_convert(uint16_t* data, uint8_t len, uint8_t byte_order, bool is_reverse)
{
if (data == NULL || len == 0)
{
return;
}
// 默认使用配置的字节序
if (byte_order > 2)
{
byte_order = MENU_CONFIG_MODBUS_BYTE_ORDER;
}
switch (byte_order)
{
case 0: // 小端(低字节在前,高字节在后)
if (is_reverse)
{
for (uint8_t i = 0; i < len; i++)
{
data[i] = (data[i] << 8) | (data[i] >> 8);
}
}
break;
case 1: // 大端(高字节在前,低字节在后)
// 无需转换Modbus默认是大端
break;
case 2: // Modbus标准字小端字节大端如32位值0x12345678→0x5678 0x1234
if (is_reverse)
{
// 读取时将Modbus顺序转换为主机顺序
for (uint8_t i = 0; i < len; i += 2)
{
if (i + 1 < len)
{
uint16_t temp = data[i];
data[i] = data[i + 1];
data[i + 1] = temp;
}
}
}
else
{
// 写入时将主机顺序转换为Modbus顺序
for (uint8_t i = 0; i < len; i += 2)
{
if (i + 1 < len)
{
uint16_t temp = data[i];
data[i] = data[i + 1];
data[i + 1] = temp;
}
}
}
break;
default:
break;
}
}
/**
* @brief 查找Modbus映射通过参数ID
* @param param_id 参数ID
* @return 映射内部结构体指针NULL表示未找到
*/
static ModbusMapInternal* menu_modbus_find_by_param(uint16_t param_id)
{
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)
{
return &s_menu_modbus_maps[i];
}
}
return NULL;
}
/**
* @brief 查找Modbus映射通过寄存器类型和地址
* @param reg_type 寄存器类型
* @param reg_addr 寄存器地址(协议地址)
* @return 映射内部结构体指针NULL表示未找到
*/
static ModbusMapInternal* menu_modbus_find_by_reg(ModbusRegType reg_type, uint16_t reg_addr)
{
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)
{
return &s_menu_modbus_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)
{
// 1. 校验参数是否已注册
MenuParam* param = menu_param_find(param_id);
if (param == NULL)
{
MENU_DEBUG("Modbus map param %d not found", param_id);
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验寄存器地址和转换为偏移
uint16_t reg_offset;
MenuErrCode err = menu_modbus_convert_addr(reg_type, reg_addr, &reg_offset);
if (err != MENU_OK)
{
return err;
}
// 3. 校验寄存器数量至少1个
if (reg_count == 0)
{
MENU_DEBUG("Modbus map reg count is zero");
return MENU_ERR_INVALID_PARAM;
}
// 4. 校验寄存器类型与参数类型匹配
err = menu_modbus_check_type_match(param->type, reg_type);
if (err != MENU_OK)
{
return err;
}
// 5. 校验读写权限(与寄存器类型联动,如离散输入只能只读)
#if MENU_CONFIG_MODBUS_PERMISSION
if ((reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT || reg_type == MODBUS_REG_TYPE_INPUT_REG) && perm != MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus input reg only support read");
return MENU_ERR_INVALID_PARAM;
}
if (reg_type == MODBUS_REG_TYPE_COIL && perm == MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus coil not support read only");
return MENU_ERR_INVALID_PARAM;
}
#endif
// 6. 检查映射是否已存在
if (menu_modbus_find_by_param(param_id) != NULL || menu_modbus_find_by_reg(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. 查找空闲的映射位置(静态数组)
ModbusMapInternal* map = NULL;
for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++)
{
if (!s_menu_modbus_maps[i].is_registered)
{
map = &s_menu_modbus_maps[i];
break;
}
}
if (map == NULL)
{
MENU_DEBUG("Modbus map out of memory: max %d", MENU_CONFIG_MAX_MODBUS_MAPS);
return MENU_ERR_OUT_OF_MEMORY;
}
// 8. 初始化映射关系
MENU_MEM_SET_ZERO(map, sizeof(ModbusMapInternal));
map->pub.param_id = param_id;
map->pub.reg_type = reg_type;
map->pub.reg_addr = reg_addr;
map->pub.reg_count = reg_count;
map->pub.perm = perm;
map->pub.byte_order = byte_order;
map->param_type = param->type;
map->reg_addr_offset = reg_offset;
map->is_registered = true;
MENU_DEBUG("Modbus map registered: param %d → reg %d:%d (count %d, perm %d)",
param_id, reg_type, reg_addr, reg_count, perm);
return MENU_OK;
}
MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map)
{
if (map == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
ModbusMapInternal* internal_map = menu_modbus_find_by_param(param_id);
if (internal_map == NULL)
{
MENU_DEBUG("Modbus map param %d not found", param_id);
return MENU_ERR_NODE_NOT_FOUND;
}
// 复制对外暴露的信息
*map = internal_map->pub;
return MENU_OK;
}
MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map)
{
if (map == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
ModbusMapInternal* internal_map = menu_modbus_find_by_reg(reg_type, reg_addr);
if (internal_map == NULL)
{
MENU_DEBUG("Modbus map reg %d:%d not found", reg_type, reg_addr);
return MENU_ERR_NODE_NOT_FOUND;
}
// 复制对外暴露的信息
*map = internal_map->pub;
return MENU_OK;
}
MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len)
{
if (reg_buf == NULL || buf_len == NULL || *buf_len == 0)
{
return MENU_ERR_INVALID_PARAM;
}
// 1. 查找映射关系
ModbusMapInternal* map = menu_modbus_find_by_param(param_id);
if (map == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验读写权限
#if MENU_CONFIG_MODBUS_PERMISSION
if (map->pub.perm == MODBUS_PERM_READ_ONLY)
{
MENU_DEBUG("Modbus map param %d is read only", param_id);
return MENU_ERR_PARAM_WRITE_DENIED; // 复用原有错误码,或新增
}
#endif
// 3. 获取参数值
float param_val;
MenuErrCode err = menu_param_get_value(param_id, &param_val);
if (err != MENU_OK)
{
return err;
}
// 4. 转换参数值到寄存器数据(按类型和寄存器数量)
uint16_t* reg_data = (uint16_t*)reg_buf;
uint8_t req_len = map->pub.reg_count;
if (req_len > *buf_len / 2) // 寄存器数据按16位计算
{
MENU_DEBUG("Modbus reg buf too small: need %d, got %d", req_len * 2, *buf_len);
return MENU_ERR_INVALID_PARAM;
}
// 清空缓冲区
memset(reg_data, 0, req_len * 2);
// 根据参数类型转换
MenuParam* param = menu_param_find(param_id);
switch (map->pub.reg_type)
{
case MODBUS_REG_TYPE_COIL:
case MODBUS_REG_TYPE_DISCRETE_INPUT:
// 1位寄存器取参数值的最低位
reg_data[0] = (uint16_t)(param_val > 0 ? 1 : 0);
break;
case MODBUS_REG_TYPE_HOLDING_REG:
case MODBUS_REG_TYPE_INPUT_REG:
// 16位寄存器根据参数类型和数量转换
if (param->type == MENU_PARAM_TYPE_INT16 || param->type == MENU_PARAM_TYPE_UINT16)
{
reg_data[0] = (uint16_t)param_val;
}
else if (param->type == MENU_PARAM_TYPE_INT32 || param->type == MENU_PARAM_TYPE_UINT32)
{
uint32_t val = (uint32_t)param_val;
reg_data[0] = (uint16_t)(val & 0xFFFF);
reg_data[1] = (uint16_t)((val >> 16) & 0xFFFF);
}
else if (param->type == MENU_PARAM_TYPE_FLOAT)
{
// 浮点型转换为32位二进制占2个16位寄存器
union {
float f;
uint32_t u32;
} float_val;
float_val.f = param_val;
reg_data[0] = (uint16_t)(float_val.u32 & 0xFFFF);
reg_data[1] = (uint16_t)((float_val.u32 >> 16) & 0xFFFF);
}
else if (param->type == MENU_PARAM_TYPE_INT8 || param->type == MENU_PARAM_TYPE_UINT8)
{
reg_data[0] = (uint16_t)param_val;
}
break;
default:
return MENU_ERR_INVALID_PARAM;
}
// 5. 字节序转换(写入寄存器前)
menu_modbus_byte_order_convert(reg_data, req_len, map->pub.byte_order, false);
// 6. 设置实际写入长度
*buf_len = req_len * 2;
MENU_DEBUG("Modbus param %d to reg: val %f → reg data len %d", param_id, param_val, *buf_len);
return MENU_OK;
}
MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len)
{
if (reg_buf == NULL || buf_len == 0)
{
return MENU_ERR_INVALID_PARAM;
}
// 1. 查找映射关系
ModbusMapInternal* map = menu_modbus_find_by_param(param_id);
if (map == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 2. 校验读写权限
#if MENU_CONFIG_MODBUS_PERMISSION
if (map->pub.perm == MODBUS_PERM_WRITE_ONLY)
{
MENU_DEBUG("Modbus map param %d is write only", param_id);
return MENU_ERR_PARAM_WRITE_DENIED;
}
#endif
// 3. 校验缓冲区长度
uint8_t req_len = map->pub.reg_count * 2;
if (buf_len < req_len)
{
MENU_DEBUG("Modbus reg buf too small: need %d, got %d", req_len, buf_len);
return MENU_ERR_INVALID_PARAM;
}
// 4. 复制寄存器数据并进行字节序转换(读取寄存器后)
uint16_t reg_data[MENU_CONFIG_MAX_MODBUS_MAPS * 2] = {0}; // 足够大的临时缓冲区
memcpy(reg_data, reg_buf, req_len);
menu_modbus_byte_order_convert(reg_data, map->pub.reg_count, map->pub.byte_order, true);
// 5. 转换寄存器数据到参数值
float param_val = 0.0f;
MenuParam* param = menu_param_find(param_id);
switch (map->pub.reg_type)
{
case MODBUS_REG_TYPE_COIL:
case MODBUS_REG_TYPE_DISCRETE_INPUT:
// 1位寄存器0→0非0→1
param_val = (reg_data[0] & 0x01) ? 1.0f : 0.0f;
break;
case MODBUS_REG_TYPE_HOLDING_REG:
case MODBUS_REG_TYPE_INPUT_REG:
// 16位寄存器根据参数类型转换
if (param->type == MENU_PARAM_TYPE_INT8)
{
param_val = (int8_t)reg_data[0];
}
else if (param->type == MENU_PARAM_TYPE_UINT8)
{
param_val = (uint8_t)reg_data[0];
}
else if (param->type == MENU_PARAM_TYPE_INT16)
{
param_val = (int16_t)reg_data[0];
}
else if (param->type == MENU_PARAM_TYPE_UINT16)
{
param_val = (uint16_t)reg_data[0];
}
else if (param->type == MENU_PARAM_TYPE_INT32)
{
uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = (int32_t)val;
}
else if (param->type == MENU_PARAM_TYPE_UINT32)
{
uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = val;
}
else if (param->type == MENU_PARAM_TYPE_FLOAT)
{
union {
float f;
uint32_t u32;
} float_val;
float_val.u32 = ((uint32_t)reg_data[1] << 16) | reg_data[0];
param_val = float_val.f;
}
break;
default:
return MENU_ERR_INVALID_PARAM;
}
// 6. 设置参数值(自动进行范围检查)
MenuErrCode err = menu_param_set_value(param_id, param_val);
if (err != MENU_OK)
{
return err;
}
MENU_DEBUG("Modbus reg to param %d: reg data len %d → val %f", param_id, buf_len, param_val);
return MENU_OK;
}
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
```
### 4. 硬件端口层(`menu/src/port/`
#### 4.1 可选修改`menu/src/port/menu_port.h`添加Modbus硬件接口用户适配
如果需要对接实际的Modbus硬件如RS485、TCP可在port层添加硬件接口与映射逻辑解耦
```c
#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
```
#### 4.2 可选修改`menu/src/port/menu_port.c`Modbus硬件接口示例实现
用户根据实际硬件如STM32的RS485实现Modbus数据的发送和接收
```c
#if MENU_CONFIG_ENABLE_MODBUS_MAP
MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len)
{
// 示例通过RS485发送Modbus写指令如写保持寄存器0x000040001
// 实际需根据Modbus协议实现帧组装、校验、发送
(void)reg_type;
(void)reg_addr;
(void)reg_buf;
(void)buf_len;
return MENU_OK; // 模拟成功,用户需替换为实际逻辑
}
MenuErrCode menu_port_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; // 模拟成功,用户需替换为实际逻辑
}
#endif // MENU_CONFIG_ENABLE_MODBUS_MAP
```
### 5. 使用示例(用户代码)
在原有菜单使用示例中添加Modbus映射的注册和数据交互
```c
/**
* @brief 菜单组件+Modbus映射使用示例用户主函数
*/
#include "menu.h"
#include "menu_port.h"
// 原有回调函数...
int main(void)
{
// 1. 初始化硬件和菜单组件
// hal_init();
menu_init();
// 2. 注册菜单节点和参数
menu_register_node(0, 1, "Main Menu", NULL, NULL);
menu_register_node(1, 2, "Temperature", NULL, NULL);
menu_param_register(2, 1, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f); // 温度参数ID=1
// 3. 注册参数与Modbus寄存器的映射核心新增逻辑
#if MENU_CONFIG_ENABLE_MODBUS_MAP
// 温度参数ID=1→ Modbus保持寄存器40001地址400012个寄存器读写权限Modbus标准字节序
menu_modbus_map_register(1, MODBUS_REG_TYPE_HOLDING_REG, 40001, 2, MODBUS_PERM_READ_WRITE, 2);
#endif
// 4. 主循环
while (1)
{
// 原有逻辑:按键扫描、菜单主循环
menu_port_key_scan();
menu_main_loop();
// 5. Modbus数据交互示例新增逻辑
#if MENU_CONFIG_ENABLE_MODBUS_MAP
{
// 示例1将参数值写入Modbus寄存器并发送
uint8_t reg_buf[4] = {0}; // 2个16位寄存器占4字节
uint8_t buf_len = sizeof(reg_buf);
if (menu_modbus_map_param_to_reg(1, reg_buf, &buf_len) == MENU_OK)
{
// 发送到Modbus硬件用户需实现menu_port_modbus_send
menu_port_modbus_send(MODBUS_REG_TYPE_HOLDING_REG, 40001, reg_buf, buf_len);
}
// 示例2从Modbus寄存器接收数据并更新参数值
if (menu_port_modbus_receive(MODBUS_REG_TYPE_HOLDING_REG, 40001, reg_buf, buf_len) == MENU_OK)
{
menu_modbus_map_reg_to_param(1, reg_buf, buf_len);
}
}
#endif
// 其他业务逻辑
// ...
}
}
```
---
## 三、总结
### 核心关键点回顾
1. **解耦与可裁剪**Modbus映射模块作为独立功能扩展层通过`MENU_CONFIG_ENABLE_MODBUS_MAP`宏控制启用/关闭,不影响原有菜单和参数模块的逻辑。
2. **静态内存与工业级鲁棒性**:使用静态数组存储映射关系,避免动态分配;加入地址范围校验、类型匹配校验、权限校验、字节序适配,确保工业环境下的稳定性。
3. **灵活映射与数据转换**:支持单/多寄存器映射、参数类型与寄存器类型自动转换、多种字节序配置适配不同的Modbus从站设备。
4. **无缝集成**:与原有参数模块联动,参数值与寄存器数据的双向转换自动触发参数的范围检查,无需额外逻辑。
5. **硬件解耦**Modbus映射逻辑与硬件接口分离用户只需在port层实现Modbus硬件的发送/接收接口即可快速移植到不同的MCU和Modbus硬件RS485/TCP
该扩展功能完全遵循工业级嵌入式组件的设计标准可满足工业控制器、仪器仪表、智能家居等设备中参数与Modbus寄存器的灵活映射需求。

File diff suppressed because it is too large Load Diff