初始化版本
This commit is contained in:
52
CMakeLists.txt
Normal file
52
CMakeLists.txt
Normal file
@ -0,0 +1,52 @@
|
||||
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
|
||||
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
|
||||
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")
|
||||
152
api/menu.h
Normal file
152
api/menu.h
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @file menu.h
|
||||
* @brief 菜单组件对外暴露的核心接口(用户唯一需要包含的头文件)
|
||||
* @note 工业级嵌入式菜单组件 - 对外API层
|
||||
*/
|
||||
#ifndef MENU_H
|
||||
#define MENU_H
|
||||
|
||||
#include "menu_config.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/************************** 全局类型导出 **************************/
|
||||
/**
|
||||
* @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 ///< 硬件端口层错误
|
||||
} MenuErrCode;
|
||||
|
||||
/**
|
||||
* @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 菜单组件初始化(必须首先调用)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_init(void);
|
||||
|
||||
/**
|
||||
* @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示)
|
||||
*/
|
||||
void menu_main_loop(void);
|
||||
|
||||
/**
|
||||
* @brief 发送事件到菜单事件队列(如按键事件、自定义事件)
|
||||
* @param type 事件类型
|
||||
* @param param 事件附加参数(可选,如按键长按时间)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_post_event(MenuEventType type, uint32_t param);
|
||||
|
||||
/**
|
||||
* @brief 注册菜单节点(构建菜单树)
|
||||
* @param parent_id 父节点ID(根节点填0)
|
||||
* @param node_id 当前节点ID(唯一,不可重复)
|
||||
* @param name_str 菜单名称字符串(或多语言索引)
|
||||
* @param enter_cb 进入该菜单的回调函数(NULL表示无)
|
||||
* @param exit_cb 退出该菜单的回调函数(NULL表示无)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb);
|
||||
|
||||
/**
|
||||
* @brief 获取当前选中的菜单节点ID
|
||||
* @param node_id 输出参数,当前节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_get_current_node(MenuNodeId* node_id);
|
||||
|
||||
/************************** 功能扩展接口声明(可裁剪) **************************/
|
||||
#if MENU_CONFIG_ENABLE_PARAM
|
||||
/**
|
||||
* @brief 参数类型枚举
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @brief 注册参数到菜单节点(参数管理功能)
|
||||
* @param node_id 菜单节点ID(参数与菜单绑定)
|
||||
* @param param_id 参数ID(唯一)
|
||||
* @param type 参数类型
|
||||
* @param min_val 最小值(浮点型,内部自动转换)
|
||||
* @param max_val 最大值(浮点型,内部自动转换)
|
||||
* @param default_val 默认值(浮点型,内部自动转换)
|
||||
* @param scale 缩放因子(如0.1表示保留1位小数)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale);
|
||||
|
||||
/**
|
||||
* @brief 设置参数值
|
||||
* @param param_id 参数ID
|
||||
* @param value 新值(浮点型,内部自动转换)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_set_value(uint16_t param_id, float value);
|
||||
|
||||
/**
|
||||
* @brief 获取参数值
|
||||
* @param param_id 参数ID
|
||||
* @param value 输出参数,当前值(浮点型)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_get_value(uint16_t param_id, float* value);
|
||||
#endif // MENU_CONFIG_ENABLE_PARAM
|
||||
|
||||
#if MENU_CONFIG_ENABLE_LANG
|
||||
/**
|
||||
* @brief 设置当前语言
|
||||
* @param lang_id 语言ID(如0-中文,1-英文)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_lang_set_current(uint8_t lang_id);
|
||||
|
||||
/**
|
||||
* @brief 获取当前语言
|
||||
* @param lang_id 输出参数,当前语言ID
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_lang_get_current(uint8_t* lang_id);
|
||||
#endif // MENU_CONFIG_ENABLE_LANG
|
||||
|
||||
#endif // MENU_H
|
||||
67
api/menu_config.h
Normal file
67
api/menu_config.h
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @file menu_config.h
|
||||
* @brief 菜单组件用户配置文件(工业级:集中管理所有可配置项)
|
||||
* @note 用户可根据项目需求修改此文件
|
||||
*/
|
||||
#ifndef MENU_CONFIG_H
|
||||
#define MENU_CONFIG_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 1
|
||||
|
||||
/************************** 功能扩展配置(可裁剪) **************************/
|
||||
/**
|
||||
* @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
|
||||
|
||||
#endif // MENU_CONFIG_H
|
||||
304
examples/menu_example.c
Normal file
304
examples/menu_example.c
Normal file
@ -0,0 +1,304 @@
|
||||
/**
|
||||
* @file menu_example.c
|
||||
* @brief 菜单组件使用示例
|
||||
* @note 工业级嵌入式菜单组件 - 使用示例
|
||||
*/
|
||||
#include "menu.h"
|
||||
#include "menu_port.h"
|
||||
#include <stdio.h>
|
||||
|
||||
// 条件编译:处理不同平台的键盘输入函数
|
||||
#if defined(_WIN32)
|
||||
#include <conio.h> // Windows平台
|
||||
#else
|
||||
// MinGW或其他平台的模拟实现
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
int _kbhit(void)
|
||||
{
|
||||
struct termios oldt, newt;
|
||||
int ch;
|
||||
int oldf;
|
||||
|
||||
tcgetattr(STDIN_FILENO, &oldt);
|
||||
newt = oldt;
|
||||
newt.c_lflag &= ~(ICANON | ECHO);
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
|
||||
oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
|
||||
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
|
||||
|
||||
ch = getchar();
|
||||
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
||||
fcntl(STDIN_FILENO, F_SETFL, oldf);
|
||||
|
||||
if(ch != EOF)
|
||||
{
|
||||
ungetc(ch, stdin);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int _getch(void)
|
||||
{
|
||||
struct termios oldt, newt;
|
||||
int ch;
|
||||
|
||||
tcgetattr(STDIN_FILENO, &oldt);
|
||||
newt = oldt;
|
||||
newt.c_lflag &= ~(ICANON | ECHO);
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
|
||||
|
||||
ch = getchar();
|
||||
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
||||
|
||||
return ch;
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************** 菜单节点ID定义 **************************/
|
||||
#define MENU_ID_MAIN 100 ///< 主菜单
|
||||
#define MENU_ID_SETTINGS 200 ///< 设置菜单
|
||||
#define MENU_ID_DISPLAY 201 ///< 显示设置
|
||||
#define MENU_ID_LANGUAGE 202 ///< 语言设置
|
||||
#define MENU_ID_PARAMETER 300 ///< 参数设置
|
||||
#define MENU_ID_ABOUT 400 ///< 关于
|
||||
|
||||
/************************** 参数ID定义 **************************/
|
||||
#define PARAM_ID_BRIGHTNESS 1000 ///< 亮度参数
|
||||
#define PARAM_ID_CONTRAST 1001 ///< 对比度参数
|
||||
|
||||
/************************** 菜单回调函数 **************************/
|
||||
/**
|
||||
* @brief 主菜单进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_main(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter Main Menu\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置菜单进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_settings(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter Settings Menu\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 显示设置进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_display(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter Display Settings\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 语言设置进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_language(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter Language Settings\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 参数设置进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_parameter(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter Parameter Settings\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关于菜单进入回调
|
||||
* @param node_id 菜单节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_cb_about(MenuNodeId node_id)
|
||||
{
|
||||
(void)node_id; // 未使用的参数
|
||||
printf("Enter About Menu\r\n");
|
||||
printf("Industrial Menu Component v1.0\r\n");
|
||||
printf("Highly Portable & Modular\r\n");
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/************************** 辅助函数 **************************/
|
||||
/**
|
||||
* @brief 按键扫描函数(Windows平台示例)
|
||||
* @return 按键事件类型
|
||||
*/
|
||||
static MenuEventType key_scan(void)
|
||||
{
|
||||
if (_kbhit())
|
||||
{
|
||||
int key = _getch();
|
||||
switch (key)
|
||||
{
|
||||
case 'w':
|
||||
case 'W':
|
||||
case 72: // 上箭头
|
||||
return MENU_EVENT_KEY_UP;
|
||||
case 's':
|
||||
case 'S':
|
||||
case 80: // 下箭头
|
||||
return MENU_EVENT_KEY_DOWN;
|
||||
case 13: // 回车键
|
||||
return MENU_EVENT_KEY_ENTER;
|
||||
case 'b':
|
||||
case 'B':
|
||||
case 27: // ESC键
|
||||
return MENU_EVENT_KEY_BACK;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return MENU_EVENT_NONE;
|
||||
}
|
||||
|
||||
/************************** 主函数 **************************/
|
||||
int main(void)
|
||||
{
|
||||
printf("Industrial Menu Component Example\r\n");
|
||||
printf("==================================\r\n");
|
||||
printf("Controls:\r\n");
|
||||
printf(" W/Up Arrow - Up\r\n");
|
||||
printf(" S/Down Arrow - Down\r\n");
|
||||
printf(" Enter - Enter\r\n");
|
||||
printf(" B/ESC - Back\r\n");
|
||||
printf(" Q - Quit\r\n");
|
||||
printf("==================================\r\n\r\n");
|
||||
|
||||
/************************** 初始化菜单组件 **************************/
|
||||
MenuErrCode err = menu_init();
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Menu initialization failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
printf("Menu initialized successfully\r\n\r\n");
|
||||
|
||||
/************************** 注册菜单节点 **************************/
|
||||
// 注册主菜单
|
||||
err = menu_register_node(0, MENU_ID_MAIN, "Main Menu", menu_cb_main, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Main Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册设置菜单
|
||||
err = menu_register_node(MENU_ID_MAIN, MENU_ID_SETTINGS, "Settings", menu_cb_settings, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Settings Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册显示设置菜单
|
||||
err = menu_register_node(MENU_ID_SETTINGS, MENU_ID_DISPLAY, "Display Settings", menu_cb_display, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Display Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册语言设置菜单
|
||||
err = menu_register_node(MENU_ID_SETTINGS, MENU_ID_LANGUAGE, "Language", menu_cb_language, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Language Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册参数设置菜单
|
||||
err = menu_register_node(MENU_ID_MAIN, MENU_ID_PARAMETER, "Parameters", menu_cb_parameter, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Parameter Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册关于菜单
|
||||
err = menu_register_node(MENU_ID_MAIN, MENU_ID_ABOUT, "About", menu_cb_about, NULL);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register About Menu failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************** 注册参数 **************************/
|
||||
#if MENU_CONFIG_ENABLE_PARAM
|
||||
// 注册亮度参数(范围0-100,默认50,步长1)
|
||||
err = menu_param_register(MENU_ID_DISPLAY, PARAM_ID_BRIGHTNESS,
|
||||
MENU_PARAM_TYPE_UINT8, 0.0f, 100.0f, 50.0f, 1.0f);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Brightness Param failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 注册对比度参数(范围0-100,默认50,步长1)
|
||||
err = menu_param_register(MENU_ID_DISPLAY, PARAM_ID_CONTRAST,
|
||||
MENU_PARAM_TYPE_UINT8, 0.0f, 100.0f, 50.0f, 1.0f);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
printf("Register Contrast Param failed: %d\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
printf("Menu nodes registered successfully\r\n\r\n");
|
||||
|
||||
/************************** 主循环 **************************/
|
||||
while (1)
|
||||
{
|
||||
// 扫描按键
|
||||
MenuEventType event = key_scan();
|
||||
if (event != MENU_EVENT_NONE)
|
||||
{
|
||||
// 发送按键事件到菜单组件
|
||||
menu_post_event(event, 0);
|
||||
}
|
||||
|
||||
// 处理菜单主循环
|
||||
menu_main_loop();
|
||||
|
||||
// 检查退出条件
|
||||
if (_kbhit() && _getch() == 'q')
|
||||
{
|
||||
printf("\r\nQuit program\r\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// 轻微延迟,避免CPU占用过高
|
||||
menu_port_delay_ms(10);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
108
internal/menu_core.h
Normal file
108
internal/menu_core.h
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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,用于超时处理)
|
||||
} 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 struct {
|
||||
MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池(无动态分配)
|
||||
MenuStack stack; ///< 菜单导航栈
|
||||
MenuEventQueue event_queue; ///< 事件队列
|
||||
MenuNodeId current_node_id; ///< 当前选中的节点ID
|
||||
uint32_t last_refresh_tick; ///< 上次刷新时间(ms)
|
||||
bool is_initialized; ///< 是否已初始化
|
||||
} MenuCoreCtx;
|
||||
|
||||
/************************** 核心内部接口声明 **************************/
|
||||
/**
|
||||
* @brief 查找菜单节点(通过ID)
|
||||
* @param node_id 节点ID
|
||||
* @return 节点指针(NULL表示未找到)
|
||||
*/
|
||||
MenuNode* menu_core_find_node(MenuNodeId node_id);
|
||||
|
||||
/**
|
||||
* @brief 初始化菜单核心上下文
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_core_ctx_init(void);
|
||||
|
||||
/**
|
||||
* @brief 获取菜单核心上下文(内部唯一访问入口)
|
||||
* @return 菜单核心上下文指针
|
||||
*/
|
||||
MenuCoreCtx* menu_core_get_ctx(void);
|
||||
|
||||
/**
|
||||
* @brief 处理单个菜单事件
|
||||
* @param event 事件指针
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_core_handle_event(const MenuEvent* event);
|
||||
|
||||
/**
|
||||
* @brief 刷新菜单显示(内部调用,对接port层显示接口)
|
||||
*/
|
||||
void menu_core_refresh_display(void);
|
||||
|
||||
#endif // MENU_CORE_H
|
||||
64
internal/menu_data.h
Normal file
64
internal/menu_data.h
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file menu_data.h
|
||||
* @brief 菜单组件共享全局变量(用户无需关心,内部仅通过接口访问)
|
||||
*/
|
||||
#ifndef MENU_DATA_H
|
||||
#define MENU_DATA_H
|
||||
|
||||
#include "menu_core.h"
|
||||
|
||||
/************************** 共享全局变量(声明,内部可见) **************************/
|
||||
/**
|
||||
* @brief 菜单核心上下文(全局,仅内部访问)
|
||||
*/
|
||||
extern MenuCoreCtx s_menu_core_ctx;
|
||||
|
||||
/**
|
||||
* @brief 参数管理上下文(启用参数时有效,全局)
|
||||
*/
|
||||
#if MENU_CONFIG_ENABLE_PARAM
|
||||
typedef struct {
|
||||
uint16_t id; ///< 参数ID
|
||||
MenuParamType type; ///< 参数类型
|
||||
float min_val; ///< 最小值
|
||||
float max_val; ///< 最大值
|
||||
float scale; ///< 缩放因子
|
||||
union { ///< 共用体:减少内存占用
|
||||
int8_t i8;
|
||||
uint8_t u8;
|
||||
int16_t i16;
|
||||
uint16_t u16;
|
||||
int32_t i32;
|
||||
uint32_t u32;
|
||||
float f;
|
||||
} value; ///< 当前值
|
||||
union { ///< 默认值
|
||||
int8_t i8;
|
||||
uint8_t u8;
|
||||
int16_t i16;
|
||||
uint16_t u16;
|
||||
int32_t i32;
|
||||
uint32_t u32;
|
||||
float f;
|
||||
} default_val;
|
||||
bool is_registered; ///< 是否已注册
|
||||
} MenuParam;
|
||||
|
||||
extern MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS];
|
||||
#endif // MENU_CONFIG_ENABLE_PARAM
|
||||
|
||||
/**
|
||||
* @brief 多语言上下文(启用多语言时有效,全局)
|
||||
*/
|
||||
#if MENU_CONFIG_ENABLE_LANG
|
||||
typedef struct {
|
||||
const char* str; ///< 语言字符串
|
||||
uint16_t str_id; ///< 字符串ID
|
||||
uint8_t lang_id; ///< 语言ID
|
||||
} MenuLangStr;
|
||||
|
||||
extern MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS];
|
||||
extern uint8_t s_current_lang_id;
|
||||
#endif // MENU_CONFIG_ENABLE_LANG
|
||||
|
||||
#endif // MENU_DATA_H
|
||||
69
internal/menu_def.h
Normal file
69
internal/menu_def.h
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @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 调试打印宏(工业级:集中控制调试输出)
|
||||
*/
|
||||
#if MENU_CONFIG_ENABLE_DEBUG
|
||||
#define MENU_DEBUG(fmt, ...) menu_utils_printf("[MENU DEBUG] " fmt "\r\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define MENU_DEBUG(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 断言失败处理函数
|
||||
* @param file 文件名
|
||||
* @param line 行号
|
||||
*/
|
||||
void menu_utils_assert_failed(const char* file, uint32_t line);
|
||||
|
||||
/**
|
||||
* @brief 调试打印函数(对接port层的硬件打印接口)
|
||||
* @param fmt 格式化字符串
|
||||
* @param ... 可变参数
|
||||
*/
|
||||
void menu_utils_printf(const char* fmt, ...);
|
||||
|
||||
/**
|
||||
* @brief 获取系统滴答时间(ms,对接port层)
|
||||
* @return 当前滴答时间
|
||||
*/
|
||||
uint32_t menu_utils_get_tick(void);
|
||||
|
||||
#endif // MENU_DEF_H
|
||||
98
port/menu_port.c
Normal file
98
port/menu_port.c
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file menu_port.c
|
||||
* @brief 菜单组件硬件端口层示例实现(用户需要根据实际硬件修改)
|
||||
* @note 工业级嵌入式菜单组件 - 硬件端口层示例
|
||||
* @attention 用户需要根据自己的硬件平台修改此文件
|
||||
*/
|
||||
#include "menu_port.h"
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/**
|
||||
* @brief 硬件打印接口示例实现(使用标准C库printf)
|
||||
* @param fmt 格式化字符串
|
||||
* @param args 可变参数列表
|
||||
* @note 实际项目中,用户需要将此接口重定向到硬件的串口、LCD等输出设备
|
||||
*/
|
||||
void menu_port_printf(const char* fmt, va_list args)
|
||||
{
|
||||
// 示例:使用标准C库的vprintf函数
|
||||
// 实际项目中,这里应该调用硬件相关的打印函数,如串口打印
|
||||
vprintf(fmt, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取系统滴答时间示例实现(使用标准C库time函数)
|
||||
* @return 当前系统滴答时间(ms)
|
||||
* @note 实际项目中,用户需要将此接口实现为硬件定时器的计数值
|
||||
*/
|
||||
uint32_t menu_port_get_tick(void)
|
||||
{
|
||||
// 示例:使用标准C库的time函数(仅用于演示,精度低)
|
||||
// 实际项目中,这里应该返回硬件定时器的计数值,如SysTick
|
||||
return (uint32_t)time(NULL) * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 硬件延迟函数示例实现(使用标准C库sleep函数)
|
||||
* @param ms 延迟时间(ms)
|
||||
* @note 实际项目中,用户需要将此接口实现为硬件定时器的延迟
|
||||
*/
|
||||
void menu_port_delay_ms(uint32_t ms)
|
||||
{
|
||||
// 示例:使用标准C库的sleep函数(仅用于演示,精度低)
|
||||
// 实际项目中,这里应该使用硬件定时器实现精确延迟
|
||||
sleep(ms / 1000);
|
||||
usleep((ms % 1000) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 菜单显示接口示例实现(使用标准C库printf)
|
||||
* @param menu_name 菜单名称字符串
|
||||
* @param menu_id 菜单ID
|
||||
* @note 实际项目中,用户需要将此接口实现为在LCD、OLED等显示设备上的显示
|
||||
*/
|
||||
void menu_port_display(const char* menu_name, uint16_t menu_id)
|
||||
{
|
||||
// 示例:使用标准C库的printf函数在控制台显示
|
||||
// 实际项目中,这里应该调用硬件显示函数,将菜单显示在LCD等设备上
|
||||
printf("\r\nCurrent Menu: %s (ID: %d)\r\n", menu_name, menu_id);
|
||||
printf("Use UP/DOWN keys to navigate, ENTER to select, BACK to return\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按键扫描接口示例实现(使用标准C库getchar函数)
|
||||
* @return 当前按键事件(MenuEventType类型)
|
||||
* @note 实际项目中,用户需要将此接口实现为硬件按键的扫描
|
||||
*/
|
||||
/*
|
||||
MenuEventType menu_port_key_scan(void)
|
||||
{
|
||||
// 示例:使用标准C库的getchar函数获取键盘输入(仅用于演示)
|
||||
// 实际项目中,这里应该扫描硬件按键的状态
|
||||
if (kbhit())
|
||||
{
|
||||
char key = getchar();
|
||||
switch (key)
|
||||
{
|
||||
case 'w':
|
||||
case 'W':
|
||||
return MENU_EVENT_KEY_UP;
|
||||
case 's':
|
||||
case 'S':
|
||||
return MENU_EVENT_KEY_DOWN;
|
||||
case '\r':
|
||||
case '\n':
|
||||
return MENU_EVENT_KEY_ENTER;
|
||||
case 'b':
|
||||
case 'B':
|
||||
return MENU_EVENT_KEY_BACK;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return MENU_EVENT_NONE;
|
||||
}
|
||||
*/
|
||||
50
port/menu_port.h
Normal file
50
port/menu_port.h
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file menu_port.h
|
||||
* @brief 菜单组件硬件端口层接口定义(用户需要实现的硬件相关接口)
|
||||
* @note 工业级嵌入式菜单组件 - 硬件端口层
|
||||
*/
|
||||
#ifndef MENU_PORT_H
|
||||
#define MENU_PORT_H
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/************************** 硬件端口层接口声明 **************************/
|
||||
/**
|
||||
* @brief 硬件打印接口(可变参数版本)
|
||||
* @param fmt 格式化字符串
|
||||
* @param args 可变参数列表
|
||||
* @note 用户需要实现此接口,用于调试信息输出
|
||||
*/
|
||||
void menu_port_printf(const char* fmt, va_list args);
|
||||
|
||||
/**
|
||||
* @brief 获取系统滴答时间(毫秒级)
|
||||
* @return 当前系统滴答时间(ms)
|
||||
* @note 用户需要实现此接口,用于菜单的定时和超时处理
|
||||
*/
|
||||
uint32_t menu_port_get_tick(void);
|
||||
|
||||
/**
|
||||
* @brief 硬件延迟函数(毫秒级)
|
||||
* @param ms 延迟时间(ms)
|
||||
* @note 用户需要实现此接口,用于必要的延迟操作
|
||||
*/
|
||||
void menu_port_delay_ms(uint32_t ms);
|
||||
|
||||
/**
|
||||
* @brief 菜单显示接口
|
||||
* @param menu_name 菜单名称字符串
|
||||
* @param menu_id 菜单ID
|
||||
* @note 用户需要实现此接口,用于在硬件上显示菜单内容
|
||||
*/
|
||||
void menu_port_display(const char* menu_name, uint16_t menu_id);
|
||||
|
||||
/**
|
||||
* @brief 按键扫描接口(可选)
|
||||
* @return 当前按键事件(MenuEventType类型)
|
||||
* @note 用户可以实现此接口,也可以在自己的主循环中调用menu_post_event发送按键事件
|
||||
*/
|
||||
// MenuEventType menu_port_key_scan(void);
|
||||
|
||||
#endif // MENU_PORT_H
|
||||
515
src/core/menu_core.c
Normal file
515
src/core/menu_core.c
Normal file
@ -0,0 +1,515 @@
|
||||
/**
|
||||
* @file menu_core.c
|
||||
* @brief 菜单核心逻辑:导航、栈管理、主循环(工业级:状态机驱动)
|
||||
*/
|
||||
#include "menu_core.h"
|
||||
#include "menu_data.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/************************** 全局变量定义 **************************/
|
||||
MenuCoreCtx s_menu_core_ctx;
|
||||
|
||||
/**
|
||||
* @brief 查找菜单节点(通过ID)
|
||||
* @param node_id 节点ID
|
||||
* @return 节点指针(NULL表示未找到)
|
||||
*/
|
||||
MenuNode* menu_core_find_node(MenuNodeId node_id)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
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 node_id 节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_stack_push(MenuNodeId node_id)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
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 node_id 输出参数,弹出的节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_stack_pop(MenuNodeId* node_id)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
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 node_id 当前节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_navigate_enter(MenuNodeId node_id)
|
||||
{
|
||||
MenuNode* node = menu_core_find_node(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 = menu_core_get_ctx();
|
||||
ctx->current_node_id = node->first_child->id;
|
||||
return menu_core_stack_push(node->first_child->id);
|
||||
}
|
||||
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 菜单导航返回父节点(返回键)
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_navigate_back(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MenuNodeId current_id = ctx->current_node_id;
|
||||
MenuNode* current_node = menu_core_find_node(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(&pop_id);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
ctx->current_node_id = parent_id;
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 菜单导航上选(上键)
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_navigate_up(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MenuNode* current_node = menu_core_find_node(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 菜单导航下选(下键)
|
||||
* @return 错误码
|
||||
*/
|
||||
static MenuErrCode menu_core_navigate_down(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MenuNode* current_node = menu_core_find_node(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(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 初始化菜单核心上下文
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_core_ctx_init(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MENU_MEM_SET_ZERO(ctx, sizeof(MenuCoreCtx));
|
||||
ctx->is_initialized = true;
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取菜单核心上下文(内部唯一访问入口)
|
||||
* @return 菜单核心上下文指针
|
||||
*/
|
||||
MenuCoreCtx* menu_core_get_ctx(void)
|
||||
{
|
||||
return &s_menu_core_ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理单个菜单事件
|
||||
* @param event 事件指针
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_core_handle_event(const MenuEvent* event)
|
||||
{
|
||||
MENU_ASSERT(event != NULL);
|
||||
|
||||
MenuErrCode err = MENU_OK;
|
||||
switch (event->type)
|
||||
{
|
||||
case MENU_EVENT_KEY_UP:
|
||||
err = menu_core_navigate_up();
|
||||
break;
|
||||
case MENU_EVENT_KEY_DOWN:
|
||||
err = menu_core_navigate_down();
|
||||
break;
|
||||
case MENU_EVENT_KEY_ENTER:
|
||||
err = menu_core_navigate_enter(menu_core_get_ctx()->current_node_id);
|
||||
break;
|
||||
case MENU_EVENT_KEY_BACK:
|
||||
err = menu_core_navigate_back();
|
||||
break;
|
||||
case MENU_EVENT_NONE:
|
||||
// 无事件,不处理
|
||||
break;
|
||||
default:
|
||||
// 自定义事件(用户可扩展)
|
||||
MENU_DEBUG("Custom event received: type %d, param %lu", event->type, event->param);
|
||||
break;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 刷新菜单显示(内部调用,对接port层显示接口)
|
||||
*/
|
||||
void menu_core_refresh_display(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MenuNode* current_node = menu_core_find_node(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 event 输出参数,获取到的事件
|
||||
* @return 是否获取到事件
|
||||
*/
|
||||
static bool menu_core_get_event(MenuEvent* event)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
MENU_ASSERT(ctx != NULL && event != NULL);
|
||||
|
||||
if (ctx->event_queue.count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从队列头取出事件
|
||||
*event = ctx->event_queue.buffer[ctx->event_queue.head];
|
||||
ctx->event_queue.head = (ctx->event_queue.head + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||
ctx->event_queue.count--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示)
|
||||
*/
|
||||
void menu_main_loop(void)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
if (!ctx->is_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理事件队列
|
||||
MenuEvent event;
|
||||
while (menu_core_get_event(&event))
|
||||
{
|
||||
// 检查事件是否超时
|
||||
if (menu_utils_get_tick() - event.timestamp > MENU_CONFIG_EVENT_TIMEOUT)
|
||||
{
|
||||
MENU_DEBUG("Event timeout: type %d", event.type);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理事件
|
||||
MenuErrCode err = menu_core_handle_event(&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();
|
||||
ctx->last_refresh_tick = menu_utils_get_tick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 菜单组件初始化(必须首先调用)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_init(void)
|
||||
{
|
||||
MenuErrCode err = menu_core_ctx_init();
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
// 初始化事件队列
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
ctx->last_refresh_tick = menu_utils_get_tick();
|
||||
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送事件到菜单事件队列(如按键事件、自定义事件)
|
||||
* @param type 事件类型
|
||||
* @param param 事件附加参数(可选,如按键长按时间)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_post_event(MenuEventType type, uint32_t param)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
if (!ctx->is_initialized)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 检查事件队列是否已满
|
||||
if (ctx->event_queue.count >= MENU_CONFIG_EVENT_QUEUE_LEN)
|
||||
{
|
||||
return MENU_ERR_EVENT_QUEUE_FULL;
|
||||
}
|
||||
|
||||
// 入队事件
|
||||
ctx->event_queue.buffer[ctx->event_queue.tail].type = type;
|
||||
ctx->event_queue.buffer[ctx->event_queue.tail].param = param;
|
||||
ctx->event_queue.buffer[ctx->event_queue.tail].timestamp = menu_utils_get_tick();
|
||||
ctx->event_queue.tail = (ctx->event_queue.tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||
ctx->event_queue.count++;
|
||||
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册菜单节点(构建菜单树)
|
||||
* @param parent_id 父节点ID(根节点填0)
|
||||
* @param node_id 当前节点ID(唯一,不可重复)
|
||||
* @param name_str 菜单名称字符串(或多语言索引)
|
||||
* @param enter_cb 进入该菜单的回调函数(NULL表示无)
|
||||
* @param exit_cb 退出该菜单的回调函数(NULL表示无)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
if (!ctx->is_initialized || node_id == 0)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 查找空闲节点
|
||||
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(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(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(node_id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 查找父节点
|
||||
MenuNode* parent_node = menu_core_find_node(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 node_id 输出参数,当前节点ID
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_get_current_node(MenuNodeId* node_id)
|
||||
{
|
||||
MenuCoreCtx* ctx = menu_core_get_ctx();
|
||||
if (!ctx->is_initialized || node_id == NULL)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
*node_id = ctx->current_node_id;
|
||||
return MENU_OK;
|
||||
}
|
||||
123
src/lang/menu_lang.c
Normal file
123
src/lang/menu_lang.c
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @file menu_lang.c
|
||||
* @brief 菜单多语言支持模块(可裁剪功能)
|
||||
*/
|
||||
#include "menu_core.h"
|
||||
#include "menu_data.h"
|
||||
|
||||
#if MENU_CONFIG_ENABLE_LANG
|
||||
|
||||
/************************** 全局变量定义 **************************/
|
||||
MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS];
|
||||
uint8_t s_current_lang_id = 0;
|
||||
|
||||
/**
|
||||
* @brief 查找语言字符串(通过字符串ID和语言ID)
|
||||
* @param str_id 字符串ID
|
||||
* @param lang_id 语言ID
|
||||
* @return 语言字符串(NULL表示未找到)
|
||||
*/
|
||||
static const char* menu_lang_find_str(uint16_t str_id, uint8_t lang_id)
|
||||
{
|
||||
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++)
|
||||
{
|
||||
if (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id)
|
||||
{
|
||||
return s_menu_lang_strs[i].str;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册语言字符串
|
||||
* @param str_id 字符串ID
|
||||
* @param lang_id 语言ID
|
||||
* @param str 字符串内容
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_lang_register_str(uint16_t str_id, uint8_t lang_id, const char* str)
|
||||
{
|
||||
if (lang_id >= MENU_CONFIG_MAX_LANGS || str == NULL)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 查找空闲位置或已存在的字符串条目
|
||||
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++)
|
||||
{
|
||||
if (!s_menu_lang_strs[i].str || (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id))
|
||||
{
|
||||
s_menu_lang_strs[i].str_id = str_id;
|
||||
s_menu_lang_strs[i].lang_id = lang_id;
|
||||
s_menu_lang_strs[i].str = str;
|
||||
return MENU_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return MENU_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前语言的字符串
|
||||
* @param str_id 字符串ID
|
||||
* @return 字符串内容(NULL表示未找到)
|
||||
*/
|
||||
const char* menu_lang_get_str(uint16_t str_id)
|
||||
{
|
||||
const char* str = menu_lang_find_str(str_id, s_current_lang_id);
|
||||
if (str == NULL)
|
||||
{
|
||||
// 如果当前语言未找到,尝试使用默认语言(0)
|
||||
str = menu_lang_find_str(str_id, 0);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置当前语言
|
||||
* @param lang_id 语言ID(如0-中文,1-英文)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_lang_set_current(uint8_t lang_id)
|
||||
{
|
||||
if (lang_id >= MENU_CONFIG_MAX_LANGS)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
s_current_lang_id = lang_id;
|
||||
MENU_DEBUG("Language changed to: %d", lang_id);
|
||||
|
||||
// 语言切换后,刷新菜单显示
|
||||
menu_core_refresh_display();
|
||||
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前语言
|
||||
* @param lang_id 输出参数,当前语言ID
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_lang_get_current(uint8_t* lang_id)
|
||||
{
|
||||
if (lang_id == NULL)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
*lang_id = s_current_lang_id;
|
||||
return MENU_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取支持的最大语言数量
|
||||
* @return 最大语言数量
|
||||
*/
|
||||
uint8_t menu_lang_get_max_langs(void)
|
||||
{
|
||||
return MENU_CONFIG_MAX_LANGS;
|
||||
}
|
||||
|
||||
#endif // MENU_CONFIG_ENABLE_LANG
|
||||
302
src/param/menu_param.c
Normal file
302
src/param/menu_param.c
Normal file
@ -0,0 +1,302 @@
|
||||
/**
|
||||
* @file menu_param.c
|
||||
* @brief 菜单参数管理模块(可裁剪功能)
|
||||
*/
|
||||
#include "menu_core.h"
|
||||
#include "menu_data.h"
|
||||
|
||||
#if MENU_CONFIG_ENABLE_PARAM
|
||||
|
||||
/************************** 全局变量定义 **************************/
|
||||
MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS];
|
||||
|
||||
/**
|
||||
* @brief 查找参数(通过参数ID)
|
||||
* @param param_id 参数ID
|
||||
* @return 参数指针(NULL表示未找到)
|
||||
*/
|
||||
static MenuParam* menu_param_find(uint16_t param_id)
|
||||
{
|
||||
for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++)
|
||||
{
|
||||
if (s_menu_params[i].is_registered && s_menu_params[i].id == param_id)
|
||||
{
|
||||
return &s_menu_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 node_id 菜单节点ID(参数与菜单绑定)
|
||||
* @param param_id 参数ID(唯一)
|
||||
* @param type 参数类型
|
||||
* @param min_val 最小值(浮点型,内部自动转换)
|
||||
* @param max_val 最大值(浮点型,内部自动转换)
|
||||
* @param default_val 默认值(浮点型,内部自动转换)
|
||||
* @param scale 缩放因子(如0.1表示保留1位小数)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale)
|
||||
{
|
||||
if (scale <= 0.0f)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 查找菜单节点
|
||||
MenuNode* node = menu_core_find_node(node_id);
|
||||
if (node == NULL)
|
||||
{
|
||||
return MENU_ERR_NODE_NOT_FOUND;
|
||||
}
|
||||
|
||||
// 检查参数ID是否已存在
|
||||
if (menu_param_find(param_id) != NULL)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 查找空闲参数位置
|
||||
MenuParam* param = NULL;
|
||||
for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++)
|
||||
{
|
||||
if (!s_menu_params[i].is_registered)
|
||||
{
|
||||
param = &s_menu_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*)¶m->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 param_id 参数ID
|
||||
* @param value 新值(浮点型,内部自动转换)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_set_value(uint16_t param_id, float value)
|
||||
{
|
||||
// 查找参数
|
||||
MenuParam* param = menu_param_find(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 param_id 参数ID
|
||||
* @param value 输出参数,当前值(浮点型)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_get_value(uint16_t param_id, float* value)
|
||||
{
|
||||
if (value == NULL)
|
||||
{
|
||||
return MENU_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
// 查找参数
|
||||
MenuParam* param = menu_param_find(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 param_id 参数ID
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_restore_default(uint16_t param_id)
|
||||
{
|
||||
// 查找参数
|
||||
MenuParam* param = menu_param_find(param_id);
|
||||
if (param == NULL)
|
||||
{
|
||||
return MENU_ERR_NODE_NOT_FOUND;
|
||||
}
|
||||
|
||||
// 恢复默认值
|
||||
float default_val = menu_param_internal_to_float((const MenuParam*)¶m->default_val);
|
||||
return menu_param_set_value(param_id, default_val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 增加参数值(用于菜单上下键调整)
|
||||
* @param param_id 参数ID
|
||||
* @param step 步长(浮点型)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_increase(uint16_t param_id, float step)
|
||||
{
|
||||
float current_val = 0.0f;
|
||||
MenuErrCode err = menu_param_get_value(param_id, ¤t_val);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
return menu_param_set_value(param_id, current_val + step);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 减少参数值(用于菜单上下键调整)
|
||||
* @param param_id 参数ID
|
||||
* @param step 步长(浮点型)
|
||||
* @return 错误码
|
||||
*/
|
||||
MenuErrCode menu_param_decrease(uint16_t param_id, float step)
|
||||
{
|
||||
float current_val = 0.0f;
|
||||
MenuErrCode err = menu_param_get_value(param_id, ¤t_val);
|
||||
if (err != MENU_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
return menu_param_set_value(param_id, current_val - step);
|
||||
}
|
||||
|
||||
#endif // MENU_CONFIG_ENABLE_PARAM
|
||||
77
src/utils/menu_utils.c
Normal file
77
src/utils/menu_utils.c
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @file menu_utils.c
|
||||
* @brief 菜单工具模块:断言、打印、系统滴答等
|
||||
*/
|
||||
#include "menu_def.h"
|
||||
#include "menu_port.h"
|
||||
#include <stdarg.h>
|
||||
|
||||
/**
|
||||
* @brief 断言失败处理函数
|
||||
* @param file 文件名
|
||||
* @param line 行号
|
||||
*/
|
||||
void menu_utils_assert_failed(const char* file, uint32_t line)
|
||||
{
|
||||
// 调用port层的打印接口输出断言信息
|
||||
menu_utils_printf("[MENU ASSERT] %s:%lu\r\n", file, line);
|
||||
|
||||
// 可以在这里添加其他处理,如触发硬件异常、保存上下文等
|
||||
// 例如:menu_port_trigger_exception();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 调试打印函数(对接port层的硬件打印接口)
|
||||
* @param fmt 格式化字符串
|
||||
* @param ... 可变参数
|
||||
*/
|
||||
void menu_utils_printf(const char* fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
|
||||
// 调用port层的可变参数打印接口
|
||||
// 注意:这里需要用户在port层实现menu_port_printf函数
|
||||
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);
|
||||
}
|
||||
819
update.md
Normal file
819
update.md
Normal file
@ -0,0 +1,819 @@
|
||||
你需要在已有的工业级嵌入式菜单组件中,拓展**参数与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→0,00001→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, ®_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, ¶m_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写指令(如写保持寄存器0x0000(40001))
|
||||
// 实际需根据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(地址40001,2个寄存器,读写权限,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寄存器的灵活映射需求。
|
||||
Reference in New Issue
Block a user