Compare commits
6 Commits
331c9a285d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf1803dd77 | |||
| 3a5491c11e | |||
| cc9f363ff8 | |||
| 0ec8a5b380 | |||
| 07790e128d | |||
| 03ee79d03b |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.cache/*
|
||||||
|
build/*
|
||||||
|
|
||||||
25
CMakeLists.txt
Normal file
25
CMakeLists.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(MenuSystem C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
|
||||||
|
include_directories(
|
||||||
|
api
|
||||||
|
src/core
|
||||||
|
src/param
|
||||||
|
src/lang
|
||||||
|
port
|
||||||
|
)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE SOURCES
|
||||||
|
api/*.c
|
||||||
|
src/core/*.c
|
||||||
|
src/param/*.c
|
||||||
|
src/lang/*.c
|
||||||
|
port/*.c
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(menu STATIC ${SOURCES})
|
||||||
|
|
||||||
|
add_executable(menu_demo examples/demo.c)
|
||||||
|
target_link_libraries(menu_demo menu)
|
||||||
528
README.md
Normal file
528
README.md
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
# 菜单组件设计文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
### 1.1 产品简介
|
||||||
|
菜单组件是一个基于状态机的轻量级菜单系统,设计用于嵌入式设备的人机交互界面。它提供了灵活的菜单节点管理、事件驱动的导航机制、权限控制和状态持久化等功能,支持多语言和模块化扩展。
|
||||||
|
|
||||||
|
### 1.2 设计目标
|
||||||
|
- **轻量级**:静态内存分配,无动态内存依赖,适合资源受限的嵌入式设备
|
||||||
|
- **模块化**:清晰的分层设计,各功能模块可独立开关
|
||||||
|
- **可扩展**:支持自定义状态转换、事件处理和功能扩展
|
||||||
|
- **易使用**:简洁的API接口,便于集成到各种应用中
|
||||||
|
- **高可靠**:完善的错误处理和状态管理
|
||||||
|
|
||||||
|
### 1.3 主要特性
|
||||||
|
- 基于状态机的菜单导航
|
||||||
|
- 静态菜单节点池管理
|
||||||
|
- 事件驱动的交互机制
|
||||||
|
- 权限管理系统
|
||||||
|
- 状态持久化
|
||||||
|
- 多语言支持
|
||||||
|
- 内存监控
|
||||||
|
- 模块化设计,功能可配置
|
||||||
|
|
||||||
|
## 2. 系统架构
|
||||||
|
|
||||||
|
### 2.1 分层设计
|
||||||
|
菜单组件采用分层架构设计,从下到上分为:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 应用层 (Application) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ API 层 (menu_api) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 核心层 (Core) │ 权限管理 │ 持久化 │ 多语言 │ 参数管理 │
|
||||||
|
│ ┌─────────────┐ │ ┌─────────┐ │ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │
|
||||||
|
│ │ menu_core │ │ │ menu_per│ │ │ menu_per│ │ │ menu_lang │ │ │ menu_param│ │
|
||||||
|
│ │ menu_event │ │ │ mission │ │ │ sistence│ │ └───────┘ │ │ └───────┘ │
|
||||||
|
│ │ menu_stack │ │ └─────────┘ │ └───────┘ │ │ │
|
||||||
|
│ │ menu_hash │ │ │ │ │
|
||||||
|
│ └─────────────┘ │ │ │ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 端口层 (menu_port) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 硬件抽象层 (HAL) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 模块关系
|
||||||
|
|
||||||
|
各模块之间的依赖关系如下:
|
||||||
|
|
||||||
|
- **API层**:对外提供统一的菜单操作接口,依赖核心层和各功能模块
|
||||||
|
- **核心层**:包含菜单的核心逻辑,如节点管理、事件处理、状态机等
|
||||||
|
- **功能模块**:基于核心层扩展,提供特定功能,如权限管理、持久化等
|
||||||
|
- **端口层**:提供平台相关的接口实现,如调试打印、内存操作等
|
||||||
|
|
||||||
|
## 3. 核心组件设计
|
||||||
|
|
||||||
|
### 3.1 菜单核心 (menu_core)
|
||||||
|
|
||||||
|
#### 3.1.1 功能描述
|
||||||
|
菜单核心是整个组件的心脏,负责:
|
||||||
|
- 菜单节点的注册、注销和管理
|
||||||
|
- 状态机的运行和状态转换
|
||||||
|
- 事件的处理和分发
|
||||||
|
- 导航路径的管理
|
||||||
|
|
||||||
|
#### 3.1.2 核心流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> INIT: 菜单初始化
|
||||||
|
INIT --> NORMAL: 菜单进入
|
||||||
|
NORMAL --> PARAM_EDIT: 进入参数编辑
|
||||||
|
PARAM_EDIT --> NORMAL: 保存参数
|
||||||
|
PARAM_EDIT --> CONFIRM: 请求确认
|
||||||
|
CONFIRM --> NORMAL: 确认操作
|
||||||
|
CONFIRM --> PARAM_EDIT: 取消操作
|
||||||
|
NORMAL --> ERROR: 发生错误
|
||||||
|
ERROR --> NORMAL: 恢复正常
|
||||||
|
NORMAL --> [*]: 菜单退出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 事件管理 (menu_event)
|
||||||
|
|
||||||
|
#### 3.2.1 功能描述
|
||||||
|
事件管理模块负责:
|
||||||
|
- 事件的发布和订阅
|
||||||
|
- 事件队列的管理
|
||||||
|
- 事件优先级的处理
|
||||||
|
|
||||||
|
#### 3.2.2 事件处理流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[事件发布] --> B{事件队列满?}
|
||||||
|
B -->|是| C[返回错误]
|
||||||
|
B -->|否| D[入队事件]
|
||||||
|
D --> E[唤醒事件处理]
|
||||||
|
E --> F{有事件?}
|
||||||
|
F -->|是| G[出队最高优先级事件]
|
||||||
|
G --> H[处理事件]
|
||||||
|
H --> I{状态转换?}
|
||||||
|
I -->|是| J[执行状态转换]
|
||||||
|
I -->|否| K[执行事件回调]
|
||||||
|
J --> F
|
||||||
|
K --> F
|
||||||
|
F -->|否| L[等待下一个事件]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 节点管理 (menu_core + menu_hash)
|
||||||
|
|
||||||
|
#### 3.3.1 功能描述
|
||||||
|
节点管理模块负责:
|
||||||
|
- 菜单节点的注册和注销
|
||||||
|
- 节点的查找和遍历
|
||||||
|
- 节点关系的维护(父子、兄弟关系)
|
||||||
|
- 节点的哈希索引
|
||||||
|
|
||||||
|
#### 3.3.2 节点数据结构
|
||||||
|
|
||||||
|
菜单节点采用静态数组管理,每个节点包含:
|
||||||
|
- 节点ID、父节点ID
|
||||||
|
- 节点名称和回调函数
|
||||||
|
- 子节点和兄弟节点的引用
|
||||||
|
- 节点状态标志(选中、可见、启用等)
|
||||||
|
- 权限级别(可选)
|
||||||
|
- 参数ID(可选)
|
||||||
|
|
||||||
|
### 3.4 导航管理 (menu_stack + menu_core)
|
||||||
|
|
||||||
|
#### 3.4.1 功能描述
|
||||||
|
导航管理模块负责:
|
||||||
|
- 菜单的上下导航
|
||||||
|
- 菜单的进入和退出
|
||||||
|
- 导航栈的管理
|
||||||
|
- 导航路径的记录
|
||||||
|
|
||||||
|
#### 3.4.2 导航流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[当前节点] --> B{按下上键?}
|
||||||
|
B -->|是| C[切换到前一个兄弟节点]
|
||||||
|
B -->|否| D{按下下键?}
|
||||||
|
D -->|是| E[切换到后一个兄弟节点]
|
||||||
|
D -->|否| F{按下确认键?}
|
||||||
|
F -->|是| G[进入子节点/执行操作]
|
||||||
|
F -->|否| H{按下退出键?}
|
||||||
|
H -->|是| I[返回父节点]
|
||||||
|
H -->|否| J[处理其他事件]
|
||||||
|
C --> K[更新当前节点]
|
||||||
|
E --> K
|
||||||
|
G --> K
|
||||||
|
I --> K
|
||||||
|
J --> K
|
||||||
|
K --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据结构设计
|
||||||
|
|
||||||
|
### 4.1 核心数据结构
|
||||||
|
|
||||||
|
#### 4.1.1 菜单节点 (MenuNode)
|
||||||
|
```c
|
||||||
|
typedef struct MenuNode {
|
||||||
|
MenuNodeId id; ///< 节点ID(唯一)
|
||||||
|
MenuNodeId parent_id; ///< 父节点ID(根节点为0)
|
||||||
|
const char* name; ///< 菜单名称(或多语言索引)
|
||||||
|
MenuCallback enter_cb; ///< 进入回调
|
||||||
|
MenuCallback exit_cb; ///< 退出回调
|
||||||
|
MenuNodeId first_child_id; ///< 第一个子节点ID
|
||||||
|
MenuNodeId next_sibling_id; ///< 下一个兄弟节点ID
|
||||||
|
MenuNodeId prev_sibling_id; ///< 上一个兄弟节点ID
|
||||||
|
MenuNodeId hash_next_id; ///< 哈希表下一个节点ID
|
||||||
|
struct {
|
||||||
|
bool is_registered : 1; ///< 是否已注册
|
||||||
|
bool is_selected : 1; ///< 是否被选中
|
||||||
|
bool is_visible : 1; ///< 是否可见
|
||||||
|
bool is_enabled : 1; ///< 是否启用
|
||||||
|
unsigned int reserved : 4; ///< 保留位
|
||||||
|
} flags;
|
||||||
|
MenuPermissionLevel permission_level; ///< 权限级别
|
||||||
|
uint16_t param_id; ///< 绑定的参数ID
|
||||||
|
} MenuNode;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.1.2 菜单核心上下文 (MenuCoreCtx)
|
||||||
|
```c
|
||||||
|
typedef struct MenuCoreCtx {
|
||||||
|
MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池
|
||||||
|
MenuNodeId hash_table[MENU_CONFIG_HASH_TABLE_SIZE]; ///< 哈希表
|
||||||
|
MenuStack stack; ///< 菜单导航栈
|
||||||
|
MenuNavPath nav_path; ///< 菜单导航路径
|
||||||
|
MenuEventQueue event_queue; ///< 事件队列
|
||||||
|
MenuNodeId current_node_id; ///< 当前选中的节点ID
|
||||||
|
MenuState current_state; ///< 当前菜单状态
|
||||||
|
uint32_t last_refresh_tick; ///< 上次刷新时间
|
||||||
|
bool is_initialized; ///< 是否已初始化
|
||||||
|
MenuErrCode error_code; ///< 当前错误码
|
||||||
|
const char* error_msg; ///< 当前错误信息
|
||||||
|
uint16_t node_count; ///< 已注册节点数量
|
||||||
|
uint16_t free_node_count; ///< 空闲节点数量
|
||||||
|
// 功能模块扩展字段...
|
||||||
|
} MenuCoreCtx;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 状态和事件定义
|
||||||
|
|
||||||
|
| 状态名称 | 状态值 | 描述 |
|
||||||
|
|---------|-------|------|
|
||||||
|
| MENU_STATE_INIT | 0 | 初始化状态 |
|
||||||
|
| MENU_STATE_NORMAL | 1 | 正常导航状态 |
|
||||||
|
| MENU_STATE_PARAM_EDIT | 2 | 参数编辑状态 |
|
||||||
|
| MENU_STATE_CONFIRM | 3 | 确认状态 |
|
||||||
|
| MENU_STATE_ERROR | 4 | 错误状态 |
|
||||||
|
|
||||||
|
| 事件名称 | 事件值 | 描述 |
|
||||||
|
|---------|-------|------|
|
||||||
|
| MENU_EVENT_NONE | 0 | 无事件 |
|
||||||
|
| MENU_EVENT_KEY_UP | 1 | 上键事件 |
|
||||||
|
| MENU_EVENT_KEY_DOWN | 2 | 下键事件 |
|
||||||
|
| MENU_EVENT_KEY_ENTER | 3 | 确认键事件 |
|
||||||
|
| MENU_EVENT_KEY_ESC | 4 | 退出键事件 |
|
||||||
|
| MENU_EVENT_TIMEOUT | 5 | 超时事件 |
|
||||||
|
| MENU_EVENT_CUSTOM_BASE | 10 | 自定义事件基值 |
|
||||||
|
|
||||||
|
## 5. 接口设计
|
||||||
|
|
||||||
|
### 5.1 API 概述
|
||||||
|
|
||||||
|
菜单组件提供了简洁的API接口,主要分为以下几类:
|
||||||
|
|
||||||
|
| API类别 | 主要功能 | 文件位置 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 初始化与反初始化 | 组件的初始化和反初始化 | menu_api.h |
|
||||||
|
| 节点管理 | 节点的注册、注销和更新 | menu_api.h |
|
||||||
|
| 事件处理 | 事件的发布和处理 | menu_api.h |
|
||||||
|
| 导航操作 | 菜单的上下导航、进入退出 | menu_api.h |
|
||||||
|
| 状态查询 | 获取当前状态、节点和路径 | menu_api.h |
|
||||||
|
| 权限管理 | 角色注册、权限检查 | menu_api.h |
|
||||||
|
| 持久化 | 状态的保存和恢复 | menu_api.h |
|
||||||
|
|
||||||
|
### 5.2 核心API示例
|
||||||
|
|
||||||
|
#### 5.2.1 初始化菜单
|
||||||
|
```c
|
||||||
|
MenuErrCode ret = menu_init();
|
||||||
|
if (ret != MENU_ERR_OK) {
|
||||||
|
// 处理初始化错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.2 注册菜单节点
|
||||||
|
```c
|
||||||
|
// 注册单个节点
|
||||||
|
MenuNodeId node_id = menu_register_node(
|
||||||
|
0, // 自动分配节点ID
|
||||||
|
parent_node_id, // 父节点ID
|
||||||
|
"Menu Name", // 节点名称
|
||||||
|
enter_callback, // 进入回调
|
||||||
|
exit_callback // 退出回调
|
||||||
|
);
|
||||||
|
|
||||||
|
// 批量注册节点
|
||||||
|
const struct MenuNode nodes[] = {
|
||||||
|
{1, 0, "Main Menu", main_enter, main_exit},
|
||||||
|
{2, 1, "Sub Menu 1", sub1_enter, sub1_exit},
|
||||||
|
{3, 1, "Sub Menu 2", sub2_enter, sub2_exit},
|
||||||
|
};
|
||||||
|
menu_register_nodes(nodes, sizeof(nodes)/sizeof(nodes[0]));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.3 事件处理
|
||||||
|
```c
|
||||||
|
// 发布事件 - 带优先级
|
||||||
|
menu_post_event(MENU_EVENT_KEY_UP, 0, MENU_EVENT_PRIORITY_NORMAL);
|
||||||
|
|
||||||
|
// 发布事件 - 简化方式(使用正常优先级)
|
||||||
|
menu_post_event_normal(MENU_EVENT_KEY_UP, 0);
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
while (1) {
|
||||||
|
uint32_t tick = get_system_tick();
|
||||||
|
menu_main_loop(tick);
|
||||||
|
// 其他任务...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 功能特性
|
||||||
|
|
||||||
|
### 6.1 权限管理
|
||||||
|
|
||||||
|
#### 6.1.1 功能描述
|
||||||
|
权限管理模块允许:
|
||||||
|
- 注册不同的角色和权限级别
|
||||||
|
- 为菜单节点设置访问权限
|
||||||
|
- 检查当前角色是否有权访问特定节点
|
||||||
|
- 动态切换角色
|
||||||
|
|
||||||
|
#### 6.1.2 权限检查流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[访问节点请求] --> B{权限功能启用?}
|
||||||
|
B -->|否| C[允许访问]
|
||||||
|
B -->|是| D[获取当前角色]
|
||||||
|
D --> E[获取节点权限级别]
|
||||||
|
E --> F{角色权限 >= 节点权限?}
|
||||||
|
F -->|是| C
|
||||||
|
F -->|否| G[拒绝访问]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 状态持久化
|
||||||
|
|
||||||
|
#### 6.2.1 功能描述
|
||||||
|
状态持久化模块允许:
|
||||||
|
- 保存当前菜单状态到非易失性存储
|
||||||
|
- 恢复上次保存的菜单状态
|
||||||
|
- 支持自动保存和手动保存
|
||||||
|
- 可配置的保存间隔
|
||||||
|
|
||||||
|
#### 6.2.2 持久化数据结构
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
MenuNodeId current_node_id; ///< 当前节点ID
|
||||||
|
MenuState current_state; ///< 当前菜单状态
|
||||||
|
MenuNavPath nav_path; ///< 当前导航路径
|
||||||
|
MenuStack stack; ///< 当前菜单栈
|
||||||
|
uint32_t timestamp; ///< 持久化时间戳
|
||||||
|
uint8_t checksum; ///< 数据校验和
|
||||||
|
} MenuPersistenceData;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 多语言支持
|
||||||
|
|
||||||
|
#### 6.3.1 功能描述
|
||||||
|
多语言支持模块允许:
|
||||||
|
- 注册不同语言的字符串
|
||||||
|
- 切换当前语言
|
||||||
|
- 根据当前语言获取对应的字符串
|
||||||
|
- 支持动态加载语言包
|
||||||
|
|
||||||
|
### 6.4 内存监控
|
||||||
|
|
||||||
|
#### 6.4.1 功能描述
|
||||||
|
内存监控模块允许:
|
||||||
|
- 获取已使用和空闲的节点数量
|
||||||
|
- 获取最大节点数量
|
||||||
|
- 监控内存使用情况
|
||||||
|
|
||||||
|
## 7. 配置与定制
|
||||||
|
|
||||||
|
### 7.1 配置选项
|
||||||
|
|
||||||
|
菜单组件提供了丰富的配置选项,通过修改 `menu_config.h` 文件可以定制:
|
||||||
|
|
||||||
|
| 配置项 | 描述 | 默认值 |
|
||||||
|
|-------|------|-------|
|
||||||
|
| MENU_CONFIG_MAX_NODES | 最大菜单节点数 | 32 |
|
||||||
|
| MENU_CONFIG_STACK_DEPTH | 菜单栈深度 | 8 |
|
||||||
|
| MENU_CONFIG_EVENT_QUEUE_LEN | 事件队列长度 | 16 |
|
||||||
|
| MENU_CONFIG_ENABLE_PERMISSION | 启用权限管理 | 1 |
|
||||||
|
| MENU_CONFIG_ENABLE_PERSISTENCE | 启用状态持久化 | 1 |
|
||||||
|
| MENU_CONFIG_ENABLE_LANG | 启用多语言支持 | 1 |
|
||||||
|
| MENU_CONFIG_ENABLE_DEBUG | 启用调试打印 | 1 |
|
||||||
|
|
||||||
|
### 7.2 功能模块开关
|
||||||
|
|
||||||
|
通过配置宏可以启用或禁用特定功能模块:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 启用权限管理
|
||||||
|
#define MENU_CONFIG_ENABLE_PERMISSION 1U
|
||||||
|
|
||||||
|
// 禁用多语言支持
|
||||||
|
#define MENU_CONFIG_ENABLE_LANG 0U
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 示例用法
|
||||||
|
|
||||||
|
### 8.1 基础菜单示例
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "menu_api.h"
|
||||||
|
|
||||||
|
// 回调函数
|
||||||
|
MenuErrCode main_enter(MenuNodeId node_id, struct MenuCoreCtx* core_ctx) {
|
||||||
|
printf("Enter Main Menu\n");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode main_exit(MenuNodeId node_id, struct MenuCoreCtx* core_ctx) {
|
||||||
|
printf("Exit Main Menu\n");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化菜单
|
||||||
|
void menu_example_init(void) {
|
||||||
|
// 初始化菜单组件
|
||||||
|
menu_init();
|
||||||
|
|
||||||
|
// 注册菜单节点
|
||||||
|
MenuNodeId main_node = menu_register_node(0, 0, "Main Menu", main_enter, main_exit);
|
||||||
|
MenuNodeId sub1_node = menu_register_node(0, main_node, "Sub Menu 1", NULL, NULL);
|
||||||
|
MenuNodeId sub2_node = menu_register_node(0, main_node, "Sub Menu 2", NULL, NULL);
|
||||||
|
|
||||||
|
// 进入菜单
|
||||||
|
menu_enter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
void menu_example_loop(uint32_t tick) {
|
||||||
|
menu_main_loop(tick);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 带权限管理的示例
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "menu_api.h"
|
||||||
|
|
||||||
|
void permission_example(void) {
|
||||||
|
// 初始化菜单
|
||||||
|
menu_init();
|
||||||
|
|
||||||
|
// 注册角色
|
||||||
|
const MenuRole roles[] = {
|
||||||
|
{0, "Guest", 1}, // 访客角色,权限级别1
|
||||||
|
{1, "User", 5}, // 用户角色,权限级别5
|
||||||
|
{2, "Admin", 10}, // 管理员角色,权限级别10
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < sizeof(roles)/sizeof(roles[0]); i++) {
|
||||||
|
menu_permission_register_role(&roles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前角色
|
||||||
|
menu_permission_set_current_role(0); // 初始为访客角色
|
||||||
|
|
||||||
|
// 注册菜单节点,设置不同权限级别
|
||||||
|
MenuNodeId main_node = menu_register_node(0, 0, "Main", NULL, NULL);
|
||||||
|
MenuNodeId public_node = menu_register_node(0, main_node, "Public", NULL, NULL);
|
||||||
|
MenuNodeId user_node = menu_register_node(0, main_node, "User", NULL, NULL);
|
||||||
|
MenuNodeId admin_node = menu_register_node(0, main_node, "Admin", NULL, NULL);
|
||||||
|
|
||||||
|
// 设置节点权限级别
|
||||||
|
menu_permission_update_node_level(public_node, 1); // 所有人可访问
|
||||||
|
menu_permission_update_node_level(user_node, 5); // 用户及以上可访问
|
||||||
|
menu_permission_update_node_level(admin_node, 10); // 仅管理员可访问
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
bool can_access = menu_permission_check_node_access(admin_node); // 返回false(当前是访客角色)
|
||||||
|
|
||||||
|
// 切换角色
|
||||||
|
menu_permission_set_current_role(2); // 切换到管理员角色
|
||||||
|
can_access = menu_permission_check_node_access(admin_node); // 返回true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
menu/
|
||||||
|
├── api/ # API接口层
|
||||||
|
│ ├── menu_api.h # 对外API头文件
|
||||||
|
│ └── menu_api.c # API实现
|
||||||
|
├── src/ # 源代码层
|
||||||
|
│ ├── core/ # 核心模块
|
||||||
|
│ │ ├── menu_core.h/c # 核心逻辑
|
||||||
|
│ │ ├── menu_event.h/c # 事件处理
|
||||||
|
│ │ ├── menu_stack.h/c # 栈管理
|
||||||
|
│ │ ├── menu_hash.h/c # 哈希表
|
||||||
|
│ │ ├── menu_types.h # 类型定义
|
||||||
|
│ │ └── menu_config.h # 配置文件
|
||||||
|
│ ├── param/ # 参数管理
|
||||||
|
│ │ ├── menu_param.h/c
|
||||||
|
│ └── lang/ # 多语言支持
|
||||||
|
│ ├── menu_lang.h/c
|
||||||
|
├── port/ # 端口层
|
||||||
|
│ ├── menu_port.h/c
|
||||||
|
├── examples/ # 示例代码
|
||||||
|
│ ├── language/ # 多语言示例
|
||||||
|
│ └── hmi/ # HMI示例
|
||||||
|
├── demo/ # 演示程序
|
||||||
|
│ ├── demo.c
|
||||||
|
│ └── CMakeLists.txt
|
||||||
|
├── test/ # 测试代码
|
||||||
|
│ ├── menu_test.c
|
||||||
|
│ └── CMakeLists.txt
|
||||||
|
├── CMakeLists.txt # CMake构建文件
|
||||||
|
└── README.md # 设计文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 构建与集成
|
||||||
|
|
||||||
|
### 10.1 构建方式
|
||||||
|
菜单组件使用CMake进行构建,支持多种编译器和平台:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 集成到应用
|
||||||
|
|
||||||
|
1. 将菜单组件的源文件添加到应用项目中
|
||||||
|
2. 包含必要的头文件(主要是 `menu_api.h`)
|
||||||
|
3. 根据需要配置 `menu_config.h`
|
||||||
|
4. 调用API初始化和使用菜单组件
|
||||||
|
|
||||||
|
## 11. 版本历史
|
||||||
|
|
||||||
|
| 版本号 | 发布日期 | 主要变更 |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| 1.0.0 | 2025-12-19 | 初始版本 |
|
||||||
|
| 1.0.1 | 2025-12-20 | 修复了节点查找的bug,优化了事件处理流程 |
|
||||||
|
|
||||||
|
## 12. 总结
|
||||||
|
|
||||||
|
菜单组件是一个功能丰富、易于扩展的轻量级菜单系统,适合各种嵌入式设备的人机交互需求。它的模块化设计允许根据项目需求定制功能,静态内存分配使其适合资源受限的环境,完善的API设计使其易于集成到各种应用中。
|
||||||
|
|
||||||
|
通过合理使用菜单组件,可以快速构建出高效、可靠的嵌入式菜单界面,提高设备的用户体验和可维护性。
|
||||||
193
api/menu_api.c
Normal file
193
api/menu_api.c
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
#include "menu_api.h"
|
||||||
|
#include "../src/core/menu_core.h"
|
||||||
|
#include "../src/core/menu_event.h"
|
||||||
|
#include "../src/core/menu_stack.h"
|
||||||
|
#include "../src/core/menu_permission.h"
|
||||||
|
#include "../src/core/menu_persistence.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static MenuCoreCtx g_menu_ctx;
|
||||||
|
|
||||||
|
MenuErrCode menu_init(void) {
|
||||||
|
return menu_core_init(&g_menu_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_deinit(void) {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_main_loop(uint32_t tick) {
|
||||||
|
menu_core_loop(&g_menu_ctx, tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuNodeId menu_register_node(
|
||||||
|
MenuNodeId id,
|
||||||
|
MenuNodeId parent_id,
|
||||||
|
const char* name,
|
||||||
|
MenuCallback enter_cb,
|
||||||
|
MenuCallback exit_cb
|
||||||
|
) {
|
||||||
|
return menu_register_node_ex(id, parent_id, name, enter_cb, exit_cb, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuNodeId menu_register_node_ex(
|
||||||
|
MenuNodeId id,
|
||||||
|
MenuNodeId parent_id,
|
||||||
|
const char* name,
|
||||||
|
MenuCallback enter_cb,
|
||||||
|
MenuCallback exit_cb,
|
||||||
|
MenuCallback render_cb,
|
||||||
|
void* user_data
|
||||||
|
) {
|
||||||
|
MenuNode node = {0};
|
||||||
|
node.id = id;
|
||||||
|
node.parent_id = parent_id;
|
||||||
|
node.name = name;
|
||||||
|
node.enter_cb = enter_cb;
|
||||||
|
node.exit_cb = exit_cb;
|
||||||
|
node.render_cb = render_cb;
|
||||||
|
node.user_data = user_data;
|
||||||
|
node.flags.is_enabled = true;
|
||||||
|
node.flags.is_visible = true;
|
||||||
|
node.flags.is_registered = false; // Will be set by core
|
||||||
|
node.param_id = 0;
|
||||||
|
node.permission_level = 0;
|
||||||
|
|
||||||
|
MenuNodeId new_id = 0;
|
||||||
|
if (menu_core_register_node(&g_menu_ctx, &node, &new_id) == MENU_ERR_OK) {
|
||||||
|
return new_id;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_register_nodes(
|
||||||
|
const MenuNode* nodes,
|
||||||
|
size_t count,
|
||||||
|
MenuNodeId* out_ids
|
||||||
|
) {
|
||||||
|
if (!nodes || count == 0) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
MenuNode node = nodes[i];
|
||||||
|
MenuNodeId new_id = 0;
|
||||||
|
|
||||||
|
node.flags.is_enabled = true;
|
||||||
|
node.flags.is_visible = true;
|
||||||
|
node.flags.is_registered = false;
|
||||||
|
node.param_id = node.param_id ? node.param_id : 0;
|
||||||
|
node.permission_level = node.permission_level ? node.permission_level : 0;
|
||||||
|
|
||||||
|
MenuErrCode err = menu_core_register_node(&g_menu_ctx, &node, &new_id);
|
||||||
|
if (err != MENU_ERR_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_ids) {
|
||||||
|
out_ids[i] = new_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_post_event(MenuEventType type, uint32_t param, MenuEventPriority priority) {
|
||||||
|
return menu_event_post(&g_menu_ctx.event_queue, type, param, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_post_event_normal(MenuEventType type, uint32_t param) {
|
||||||
|
return menu_event_post(&g_menu_ctx.event_queue, type, param, MENU_EVENT_PRIORITY_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_enter(void) {
|
||||||
|
// We try to enter the first available root node.
|
||||||
|
// We can trigger an event or just let the loop handle it.
|
||||||
|
// If we rely on handle_event to enter root on NULL current,
|
||||||
|
// we just need to ensure loop is called.
|
||||||
|
// But explicitly:
|
||||||
|
if (g_menu_ctx.current_node_id == 0) {
|
||||||
|
// Force finding a root
|
||||||
|
// Since we don't have public access to ctx nodes from here easily without exposing everything,
|
||||||
|
// we can just call handle_event with a dummy event or NONE event if handle_event checks NULL current.
|
||||||
|
// But handle_event checks event type.
|
||||||
|
// We can add MENU_EVENT_NONE handling in handle_event to check entry?
|
||||||
|
// Or just manually find root here.
|
||||||
|
// We have access to g_menu_ctx.
|
||||||
|
for(int i=0; i<MENU_CONFIG_MAX_NODES; i++) {
|
||||||
|
if(g_menu_ctx.nodes[i].flags.is_registered && g_menu_ctx.nodes[i].parent_id == 0) {
|
||||||
|
// Enter root
|
||||||
|
MenuErrCode ret = menu_core_enter_node(&g_menu_ctx, g_menu_ctx.nodes[i].id);
|
||||||
|
if (ret != MENU_ERR_OK) return ret;
|
||||||
|
|
||||||
|
// Optimization: Automatically enter first child if root has children
|
||||||
|
// This makes the menu start with the first item selected
|
||||||
|
if (g_menu_ctx.nodes[i].first_child_id != 0) {
|
||||||
|
// We need to simulate Enter key or just manually transition
|
||||||
|
// Manual transition:
|
||||||
|
// Push root to stack
|
||||||
|
// But menu_core_enter_node logic doesn't push to stack automatically unless via handle_event
|
||||||
|
// So we mimic handle_event logic
|
||||||
|
menu_stack_push(&g_menu_ctx.stack, g_menu_ctx.nodes[i].id);
|
||||||
|
// Exit root (deselect)
|
||||||
|
menu_core_exit_node(&g_menu_ctx, g_menu_ctx.nodes[i].id);
|
||||||
|
// Enter child
|
||||||
|
return menu_core_enter_node(&g_menu_ctx, g_menu_ctx.nodes[i].first_child_id);
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_back(void) {
|
||||||
|
return menu_post_event_normal(MENU_EVENT_KEY_ESC, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_node_bind_param(MenuNodeId node_id, uint16_t param_id) {
|
||||||
|
MenuNode* node = menu_core_get_node(&g_menu_ctx, node_id);
|
||||||
|
if (!node) return MENU_ERR_NOT_FOUND;
|
||||||
|
node->param_id = param_id;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission management functions
|
||||||
|
MenuErrCode menu_permission_register_role(uint8_t role_id, const char* name, MenuPermissionLevel level) {
|
||||||
|
return menu_permission_register_role_impl(&g_menu_ctx, role_id, name, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_set_current_role(uint8_t role_id) {
|
||||||
|
return menu_permission_set_current_role_impl(&g_menu_ctx, role_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_update_node_level(MenuNodeId node_id, MenuPermissionLevel level) {
|
||||||
|
return menu_permission_update_node_level_impl(&g_menu_ctx, node_id, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_permission_check_node_access(MenuNodeId node_id) {
|
||||||
|
return menu_permission_check_node_access_impl(&g_menu_ctx, node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistence functions
|
||||||
|
MenuErrCode menu_persistence_save(void) {
|
||||||
|
return menu_persistence_save_impl(&g_menu_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_load(void) {
|
||||||
|
return menu_persistence_load_impl(&g_menu_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_clear(void) {
|
||||||
|
return menu_persistence_clear_impl(&g_menu_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_persistence_is_dirty(void) {
|
||||||
|
return menu_persistence_is_dirty_impl(&g_menu_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_refresh(void) {
|
||||||
|
MenuErrCode err = menu_post_event_normal(MENU_EVENT_RENDER, 0);
|
||||||
|
if (err != MENU_ERR_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
74
api/menu_api.h
Normal file
74
api/menu_api.h
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
#ifndef MENU_API_H
|
||||||
|
#define MENU_API_H
|
||||||
|
|
||||||
|
#include "../src/core/menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
MenuErrCode menu_init(void);
|
||||||
|
void menu_deinit(void);
|
||||||
|
|
||||||
|
// Core Loop
|
||||||
|
void menu_main_loop(uint32_t tick);
|
||||||
|
|
||||||
|
// Node Registration
|
||||||
|
MenuNodeId menu_register_node(
|
||||||
|
MenuNodeId id,
|
||||||
|
MenuNodeId parent_id,
|
||||||
|
const char* name,
|
||||||
|
MenuCallback enter_cb,
|
||||||
|
MenuCallback exit_cb
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extended registration (Optimization)
|
||||||
|
MenuNodeId menu_register_node_ex(
|
||||||
|
MenuNodeId id,
|
||||||
|
MenuNodeId parent_id,
|
||||||
|
const char* name,
|
||||||
|
MenuCallback enter_cb,
|
||||||
|
MenuCallback exit_cb,
|
||||||
|
MenuCallback render_cb,
|
||||||
|
void* user_data
|
||||||
|
);
|
||||||
|
|
||||||
|
// Batch node registration
|
||||||
|
MenuErrCode menu_register_nodes(
|
||||||
|
const MenuNode* nodes,
|
||||||
|
size_t count,
|
||||||
|
MenuNodeId* out_ids
|
||||||
|
);
|
||||||
|
|
||||||
|
// Event Handling
|
||||||
|
MenuErrCode menu_post_event(MenuEventType type, uint32_t param, MenuEventPriority priority);
|
||||||
|
MenuErrCode menu_post_event_normal(MenuEventType type, uint32_t param);
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
MenuErrCode menu_enter(void); // Enter root
|
||||||
|
MenuErrCode menu_back(void);
|
||||||
|
|
||||||
|
// Parameter Binding
|
||||||
|
MenuErrCode menu_node_bind_param(MenuNodeId node_id, uint16_t param_id);
|
||||||
|
|
||||||
|
// Permission Management
|
||||||
|
MenuErrCode menu_permission_register_role(uint8_t role_id, const char* name, MenuPermissionLevel level);
|
||||||
|
MenuErrCode menu_permission_set_current_role(uint8_t role_id);
|
||||||
|
MenuErrCode menu_permission_update_node_level(MenuNodeId node_id, MenuPermissionLevel level);
|
||||||
|
bool menu_permission_check_node_access(MenuNodeId node_id);
|
||||||
|
|
||||||
|
// Persistence Management
|
||||||
|
MenuErrCode menu_persistence_save(void);
|
||||||
|
MenuErrCode menu_persistence_load(void);
|
||||||
|
MenuErrCode menu_persistence_clear(void);
|
||||||
|
bool menu_persistence_is_dirty(void);
|
||||||
|
|
||||||
|
// Dynamic menu updates
|
||||||
|
MenuErrCode menu_refresh(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_API_H
|
||||||
148
examples/demo.c
Normal file
148
examples/demo.c
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#include "menu_api.h"
|
||||||
|
#include "menu_param.h"
|
||||||
|
#include "menu_lang.h"
|
||||||
|
#include "menu_port.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// Globals
|
||||||
|
int32_t g_volume = 50;
|
||||||
|
int32_t g_brightness = 80;
|
||||||
|
uint8_t g_language = 1; // 0=EN, 1=CN
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
MenuErrCode on_enter_main(MenuNodeId id, struct MenuCoreCtx* ctx) {
|
||||||
|
menu_port_log(">> Entered Main Menu");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode on_enter_settings(MenuNodeId id, struct MenuCoreCtx* ctx) {
|
||||||
|
menu_port_log(">> Entered Settings");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode on_enter_volume(MenuNodeId id, struct MenuCoreCtx* ctx) {
|
||||||
|
menu_port_log(">> Entered Volume Param Edit");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode on_enter_brightness(MenuNodeId id, struct MenuCoreCtx* ctx) {
|
||||||
|
menu_port_log(">> Entered Brightness Param Edit");
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode on_render(MenuNodeId id, struct MenuCoreCtx* ctx) {
|
||||||
|
// In a real system, this would draw to screen
|
||||||
|
// Here we just print the current state occasionally or on change
|
||||||
|
// For demo simplicity, we rely on enter/exit logs
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_params(void) {
|
||||||
|
menu_param_init();
|
||||||
|
|
||||||
|
MenuParam p_vol = {0};
|
||||||
|
p_vol.id = 1;
|
||||||
|
p_vol.type = MENU_PARAM_TYPE_INT32;
|
||||||
|
p_vol.name = "Volume";
|
||||||
|
p_vol.value_ptr = &g_volume;
|
||||||
|
p_vol.meta.num.min = 0;
|
||||||
|
p_vol.meta.num.max = 100;
|
||||||
|
p_vol.meta.num.step = 5;
|
||||||
|
menu_param_register(&p_vol);
|
||||||
|
|
||||||
|
MenuParam p_bri = {0};
|
||||||
|
p_bri.id = 2;
|
||||||
|
p_bri.type = MENU_PARAM_TYPE_INT32;
|
||||||
|
p_bri.name = "Brightness";
|
||||||
|
p_bri.value_ptr = &g_brightness;
|
||||||
|
p_bri.meta.num.min = 10;
|
||||||
|
p_bri.meta.num.max = 100;
|
||||||
|
p_bri.meta.num.step = 10;
|
||||||
|
menu_param_register(&p_bri);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_menu(void) {
|
||||||
|
menu_init();
|
||||||
|
|
||||||
|
// Root: Main Menu
|
||||||
|
MenuNodeId main = menu_register_node(0, 0, "$MAIN", on_enter_main, NULL);
|
||||||
|
|
||||||
|
// Child: Settings
|
||||||
|
MenuNodeId settings = menu_register_node(0, main, "$SETTING", on_enter_settings, NULL);
|
||||||
|
|
||||||
|
// Child: Info
|
||||||
|
MenuNodeId info = menu_register_node(0, main, "$INFO", NULL, NULL);
|
||||||
|
|
||||||
|
// Child of Settings: Volume
|
||||||
|
MenuNodeId vol_node = menu_register_node(0, settings, "Volume", on_enter_volume, NULL);
|
||||||
|
menu_node_bind_param(vol_node, 1); // Bind to Volume param (ID 1)
|
||||||
|
|
||||||
|
// Child of Settings: Brightness
|
||||||
|
MenuNodeId bri_node = menu_register_node(0, settings, "Brightness", on_enter_brightness, NULL);
|
||||||
|
menu_node_bind_param(bri_node, 2); // Bind to Brightness param (ID 2)
|
||||||
|
|
||||||
|
// Set permission levels for nodes
|
||||||
|
menu_permission_update_node_level(settings, 1); // User level required for Settings
|
||||||
|
menu_permission_update_node_level(vol_node, 1); // User level required for Volume
|
||||||
|
menu_permission_update_node_level(bri_node, 1); // User level required for Brightness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub for binding param (will implement in API)
|
||||||
|
void bind_param(MenuNodeId node, uint16_t param) {
|
||||||
|
// Need API support
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
menu_port_init();
|
||||||
|
setup_params();
|
||||||
|
setup_menu();
|
||||||
|
|
||||||
|
menu_port_log("Menu System Demo Started");
|
||||||
|
menu_enter(); // Enter root
|
||||||
|
|
||||||
|
// Demo: Permission management
|
||||||
|
menu_port_log("=== Permission Management Demo ===");
|
||||||
|
menu_port_log("Current role: Guest");
|
||||||
|
menu_port_log("Trying to enter Settings...");
|
||||||
|
menu_post_event_normal(MENU_EVENT_KEY_ENTER, 0);
|
||||||
|
menu_main_loop(0);
|
||||||
|
|
||||||
|
menu_port_log("Switching to User role...");
|
||||||
|
menu_permission_set_current_role(2); // Switch to User role
|
||||||
|
menu_post_event_normal(MENU_EVENT_KEY_ENTER, 0);
|
||||||
|
menu_main_loop(0);
|
||||||
|
|
||||||
|
// Navigate to Volume setting
|
||||||
|
menu_post_event_normal(MENU_EVENT_KEY_DOWN, 0);
|
||||||
|
menu_main_loop(0);
|
||||||
|
menu_post_event_normal(MENU_EVENT_KEY_ENTER, 0);
|
||||||
|
menu_main_loop(0);
|
||||||
|
|
||||||
|
// Demo: Persistence
|
||||||
|
menu_port_log("=== Persistence Demo ===");
|
||||||
|
menu_port_log("Saving current state...");
|
||||||
|
menu_persistence_save();
|
||||||
|
|
||||||
|
menu_port_log("Exiting Volume menu...");
|
||||||
|
menu_post_event_normal(MENU_EVENT_MENU_EXIT, 0);
|
||||||
|
menu_main_loop(0);
|
||||||
|
|
||||||
|
menu_port_log("Loading saved state...");
|
||||||
|
menu_persistence_load();
|
||||||
|
menu_main_loop(0);
|
||||||
|
|
||||||
|
// Demo: Clear persistence
|
||||||
|
menu_port_log("=== Clear Persistence Demo ===");
|
||||||
|
menu_port_log("Clearing saved state...");
|
||||||
|
menu_persistence_clear();
|
||||||
|
|
||||||
|
menu_port_log("Trying to load cleared state (should fail)...");
|
||||||
|
MenuErrCode err = menu_persistence_load();
|
||||||
|
if (err != MENU_ERR_OK) {
|
||||||
|
menu_port_log("Load failed as expected (state was cleared)");
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_port_log("Menu System Demo Completed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
21
port/menu_port.c
Normal file
21
port/menu_port.c
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#include "menu_port.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
void menu_port_init(void) {
|
||||||
|
// Platform specific init
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t menu_port_get_tick(void) {
|
||||||
|
// Return ms
|
||||||
|
return (uint32_t)(clock() * 1000 / CLOCKS_PER_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_port_log(const char* fmt, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vprintf(fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
18
port/menu_port.h
Normal file
18
port/menu_port.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#ifndef MENU_PORT_H
|
||||||
|
#define MENU_PORT_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void menu_port_init(void);
|
||||||
|
uint32_t menu_port_get_tick(void);
|
||||||
|
void menu_port_log(const char* fmt, ...);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_PORT_H
|
||||||
49
src/core/menu_config.h
Normal file
49
src/core/menu_config.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#ifndef MENU_CONFIG_H
|
||||||
|
#define MENU_CONFIG_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Maximum number of menu nodes
|
||||||
|
#ifndef MENU_CONFIG_MAX_NODES
|
||||||
|
#define MENU_CONFIG_MAX_NODES 32
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Maximum stack depth for navigation
|
||||||
|
#ifndef MENU_CONFIG_STACK_DEPTH
|
||||||
|
#define MENU_CONFIG_STACK_DEPTH 8
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Event queue length
|
||||||
|
#ifndef MENU_CONFIG_EVENT_QUEUE_LEN
|
||||||
|
#define MENU_CONFIG_EVENT_QUEUE_LEN 16
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Hash table size (should be a power of 2 for efficiency)
|
||||||
|
#ifndef MENU_CONFIG_HASH_TABLE_SIZE
|
||||||
|
#define MENU_CONFIG_HASH_TABLE_SIZE 16
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Feature switches
|
||||||
|
#ifndef MENU_CONFIG_ENABLE_PERMISSION
|
||||||
|
#define MENU_CONFIG_ENABLE_PERMISSION 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MENU_CONFIG_ENABLE_PERSISTENCE
|
||||||
|
#define MENU_CONFIG_ENABLE_PERSISTENCE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MENU_CONFIG_ENABLE_LANG
|
||||||
|
#define MENU_CONFIG_ENABLE_LANG 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MENU_CONFIG_ENABLE_DEBUG
|
||||||
|
#define MENU_CONFIG_ENABLE_DEBUG 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_CONFIG_H
|
||||||
453
src/core/menu_core.c
Normal file
453
src/core/menu_core.c
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
#include "menu_core.h"
|
||||||
|
#include "menu_event.h"
|
||||||
|
#include "menu_stack.h"
|
||||||
|
#include "menu_hash.h"
|
||||||
|
#include "menu_param.h"
|
||||||
|
#include "menu_permission.h"
|
||||||
|
#include "menu_persistence.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// Internal helper to get node pointer from ID
|
||||||
|
MenuNode* menu_core_get_node(MenuCoreCtx* ctx, MenuNodeId id) {
|
||||||
|
if (!ctx || id == 0) return NULL;
|
||||||
|
return menu_hash_find(ctx, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_init(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
memset(ctx, 0, sizeof(MenuCoreCtx));
|
||||||
|
|
||||||
|
menu_event_init(&ctx->event_queue);
|
||||||
|
menu_stack_init(&ctx->stack);
|
||||||
|
menu_hash_init(ctx);
|
||||||
|
menu_permission_init_impl(ctx);
|
||||||
|
|
||||||
|
ctx->is_initialized = true;
|
||||||
|
ctx->current_state = MENU_STATE_INIT;
|
||||||
|
ctx->free_node_count = MENU_CONFIG_MAX_NODES;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_register_node(MenuCoreCtx* ctx, const MenuNode* node, MenuNodeId* out_id) {
|
||||||
|
if (!ctx || !node) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (ctx->node_count >= MENU_CONFIG_MAX_NODES) return MENU_ERR_NO_MEM;
|
||||||
|
|
||||||
|
// Find a free slot
|
||||||
|
int slot = -1;
|
||||||
|
for (int i = 0; i < MENU_CONFIG_MAX_NODES; i++) {
|
||||||
|
if (!ctx->nodes[i].flags.is_registered) {
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == -1) return MENU_ERR_NO_MEM;
|
||||||
|
|
||||||
|
MenuNode* new_node = &ctx->nodes[slot];
|
||||||
|
*new_node = *node; // Copy data
|
||||||
|
|
||||||
|
// Assign ID if 0
|
||||||
|
if (new_node->id == 0) {
|
||||||
|
new_node->id = slot + 1; // 1-based ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ID conflict (simple check)
|
||||||
|
MenuNode* existing = menu_core_get_node(ctx, new_node->id);
|
||||||
|
if (existing && existing != new_node) {
|
||||||
|
return MENU_ERR_INVALID_PARAM; // ID conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as registered
|
||||||
|
new_node->flags.is_registered = true;
|
||||||
|
ctx->node_count++;
|
||||||
|
ctx->free_node_count--;
|
||||||
|
|
||||||
|
// Link to parent
|
||||||
|
if (new_node->parent_id != 0) {
|
||||||
|
MenuNode* parent = menu_core_get_node(ctx, new_node->parent_id);
|
||||||
|
if (parent) {
|
||||||
|
if (parent->first_child_id == 0) {
|
||||||
|
parent->first_child_id = new_node->id;
|
||||||
|
} else {
|
||||||
|
// Add to end of sibling list
|
||||||
|
MenuNode* sibling = menu_core_get_node(ctx, parent->first_child_id);
|
||||||
|
while (sibling && sibling->next_sibling_id != 0) {
|
||||||
|
sibling = menu_core_get_node(ctx, sibling->next_sibling_id);
|
||||||
|
}
|
||||||
|
if (sibling) {
|
||||||
|
sibling->next_sibling_id = new_node->id;
|
||||||
|
new_node->prev_sibling_id = sibling->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to hash
|
||||||
|
menu_hash_insert(ctx, new_node->id, slot);
|
||||||
|
|
||||||
|
if (out_id) *out_id = new_node->id;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_unregister_node(MenuCoreCtx* ctx, MenuNodeId id) {
|
||||||
|
if (!ctx || id == 0) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, id);
|
||||||
|
if (!node) return MENU_ERR_NOT_FOUND;
|
||||||
|
|
||||||
|
// Find slot index
|
||||||
|
int slot = -1;
|
||||||
|
for (int i = 0; i < MENU_CONFIG_MAX_NODES; i++) {
|
||||||
|
if (ctx->nodes[i].flags.is_registered && ctx->nodes[i].id == id) {
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == -1) return MENU_ERR_NOT_FOUND;
|
||||||
|
|
||||||
|
// Update parent-child relationship
|
||||||
|
if (node->parent_id != 0) {
|
||||||
|
MenuNode* parent = menu_core_get_node(ctx, node->parent_id);
|
||||||
|
if (parent) {
|
||||||
|
// Check if this is the first child
|
||||||
|
if (parent->first_child_id == id) {
|
||||||
|
parent->first_child_id = node->next_sibling_id;
|
||||||
|
} else {
|
||||||
|
// Find previous sibling to update links
|
||||||
|
MenuNode* prev_sibling = menu_core_get_node(ctx, node->prev_sibling_id);
|
||||||
|
if (prev_sibling) {
|
||||||
|
prev_sibling->next_sibling_id = node->next_sibling_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update next sibling's prev_sibling_id
|
||||||
|
if (node->next_sibling_id != 0) {
|
||||||
|
MenuNode* next_sibling = menu_core_get_node(ctx, node->next_sibling_id);
|
||||||
|
if (next_sibling) {
|
||||||
|
next_sibling->prev_sibling_id = node->prev_sibling_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear node data
|
||||||
|
ctx->nodes[slot].flags.is_registered = false;
|
||||||
|
ctx->nodes[slot].id = 0;
|
||||||
|
ctx->nodes[slot].parent_id = 0;
|
||||||
|
ctx->nodes[slot].first_child_id = 0;
|
||||||
|
ctx->nodes[slot].next_sibling_id = 0;
|
||||||
|
ctx->nodes[slot].prev_sibling_id = 0;
|
||||||
|
ctx->nodes[slot].hash_next_id = 0;
|
||||||
|
ctx->nodes[slot].flags.is_selected = false;
|
||||||
|
ctx->nodes[slot].flags.is_visible = false;
|
||||||
|
ctx->nodes[slot].flags.is_enabled = false;
|
||||||
|
ctx->nodes[slot].permission_level = 0;
|
||||||
|
ctx->nodes[slot].param_id = 0;
|
||||||
|
ctx->nodes[slot].user_data = NULL;
|
||||||
|
ctx->nodes[slot].name = NULL;
|
||||||
|
ctx->nodes[slot].enter_cb = NULL;
|
||||||
|
ctx->nodes[slot].exit_cb = NULL;
|
||||||
|
ctx->nodes[slot].render_cb = NULL;
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
ctx->node_count--;
|
||||||
|
ctx->free_node_count++;
|
||||||
|
|
||||||
|
// Remove node from hash table using optimized removal
|
||||||
|
menu_hash_remove(ctx, id, slot);
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_enter_node(MenuCoreCtx* ctx, MenuNodeId node_id) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, node_id);
|
||||||
|
if (!node) return MENU_ERR_NOT_FOUND;
|
||||||
|
|
||||||
|
// Permission check
|
||||||
|
#if MENU_CONFIG_ENABLE_PERMISSION
|
||||||
|
if (!menu_permission_check_node_access_impl(ctx, node_id)) {
|
||||||
|
return MENU_ERR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Call enter callback
|
||||||
|
if (node->enter_cb) {
|
||||||
|
node->enter_cb(node_id, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->current_node_id = node_id;
|
||||||
|
node->flags.is_selected = true;
|
||||||
|
|
||||||
|
// Update path
|
||||||
|
ctx->nav_path.depth = 0;
|
||||||
|
MenuNodeId curr = node_id;
|
||||||
|
MenuNodeId temp_path[MENU_CONFIG_STACK_DEPTH];
|
||||||
|
int depth = 0;
|
||||||
|
while(curr != 0 && depth < MENU_CONFIG_STACK_DEPTH) {
|
||||||
|
temp_path[depth++] = curr;
|
||||||
|
MenuNode* n = menu_core_get_node(ctx, curr);
|
||||||
|
if (n) curr = n->parent_id;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
// Reverse
|
||||||
|
for(int i=0; i<depth; i++) {
|
||||||
|
ctx->nav_path.nodes[i] = temp_path[depth-1-i];
|
||||||
|
}
|
||||||
|
ctx->nav_path.depth = depth;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_exit_node(MenuCoreCtx* ctx, MenuNodeId node_id) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, node_id);
|
||||||
|
if (node) {
|
||||||
|
if (node->exit_cb) {
|
||||||
|
node->exit_cb(node_id, ctx);
|
||||||
|
}
|
||||||
|
node->flags.is_selected = false;
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_handle_event(MenuCoreCtx* ctx, const MenuEvent* event) {
|
||||||
|
if (!ctx || !event) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuNode* current = menu_core_get_node(ctx, ctx->current_node_id);
|
||||||
|
// If no current node, maybe we need to enter root?
|
||||||
|
if (!current) {
|
||||||
|
// Try to find a root node (parent_id == 0)
|
||||||
|
if (ctx->node_count > 0) {
|
||||||
|
for(int i=0; i<MENU_CONFIG_MAX_NODES; i++) {
|
||||||
|
if(ctx->nodes[i].flags.is_registered && ctx->nodes[i].parent_id == 0) {
|
||||||
|
return menu_core_enter_node(ctx, ctx->nodes[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MENU_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Parameter Edit State
|
||||||
|
if (ctx->current_state == MENU_STATE_PARAM_EDIT) {
|
||||||
|
if (current->param_id == 0) {
|
||||||
|
ctx->current_state = MENU_STATE_NORMAL;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuParam* param = menu_param_get(current->param_id);
|
||||||
|
if (!param) {
|
||||||
|
ctx->current_state = MENU_STATE_NORMAL;
|
||||||
|
return MENU_ERR_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->type) {
|
||||||
|
case MENU_EVENT_KEY_UP:
|
||||||
|
// Increase value
|
||||||
|
// For simplicity assuming int types
|
||||||
|
if (param->type >= MENU_PARAM_TYPE_INT8 && param->type <= MENU_PARAM_TYPE_UINT32) {
|
||||||
|
int32_t val = menu_param_get_int(param->id);
|
||||||
|
val += param->meta.num.step;
|
||||||
|
if (val > param->meta.num.max) val = param->meta.num.min; // Wrap or clamp?
|
||||||
|
menu_param_set_int(param->id, val);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MENU_EVENT_KEY_DOWN:
|
||||||
|
// Decrease value
|
||||||
|
if (param->type >= MENU_PARAM_TYPE_INT8 && param->type <= MENU_PARAM_TYPE_UINT32) {
|
||||||
|
int32_t val = menu_param_get_int(param->id);
|
||||||
|
val -= param->meta.num.step;
|
||||||
|
if (val < param->meta.num.min) val = param->meta.num.max;
|
||||||
|
menu_param_set_int(param->id, val);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MENU_EVENT_KEY_ENTER:
|
||||||
|
// Confirm
|
||||||
|
ctx->current_state = MENU_STATE_NORMAL;
|
||||||
|
break;
|
||||||
|
case MENU_EVENT_KEY_ESC:
|
||||||
|
// Cancel (could revert if we stored old value)
|
||||||
|
ctx->current_state = MENU_STATE_NORMAL;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->type) {
|
||||||
|
case MENU_EVENT_KEY_UP:
|
||||||
|
if (current->prev_sibling_id != 0) {
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, current->prev_sibling_id);
|
||||||
|
} else {
|
||||||
|
// Wrap around to last sibling
|
||||||
|
MenuNode* last = current;
|
||||||
|
while(last->next_sibling_id != 0) {
|
||||||
|
MenuNode* next = menu_core_get_node(ctx, last->next_sibling_id);
|
||||||
|
if(next) last = next; else break;
|
||||||
|
}
|
||||||
|
if (last != current) {
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, last->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_KEY_DOWN:
|
||||||
|
if (current->next_sibling_id != 0) {
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, current->next_sibling_id);
|
||||||
|
} else {
|
||||||
|
// Wrap to first sibling
|
||||||
|
// First sibling is the first_child of parent
|
||||||
|
if (current->parent_id != 0) {
|
||||||
|
MenuNode* parent = menu_core_get_node(ctx, current->parent_id);
|
||||||
|
if (parent && parent->first_child_id != 0 && parent->first_child_id != current->id) {
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, parent->first_child_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Root level wrap
|
||||||
|
// We need to find the first root node
|
||||||
|
// But we don't track "first root".
|
||||||
|
// We can traverse back using prev_sibling
|
||||||
|
MenuNode* first = current;
|
||||||
|
while(first->prev_sibling_id != 0) {
|
||||||
|
MenuNode* prev = menu_core_get_node(ctx, first->prev_sibling_id);
|
||||||
|
if(prev) first = prev; else break;
|
||||||
|
}
|
||||||
|
if (first != current) {
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, first->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_KEY_ENTER:
|
||||||
|
if (current->first_child_id != 0) {
|
||||||
|
// Enter child
|
||||||
|
menu_stack_push(&ctx->stack, ctx->current_node_id);
|
||||||
|
// Note: We don't 'exit' the parent in terms of callback if we consider it "active in background"?
|
||||||
|
// But normally in a menu system, you leave the parent screen and enter child screen.
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, current->first_child_id);
|
||||||
|
} else {
|
||||||
|
// Leaf node action
|
||||||
|
// Can trigger a custom event or just let the enter_cb handle it.
|
||||||
|
// If we want to support "editing", we check if param_id is set.
|
||||||
|
if (current->param_id != 0) {
|
||||||
|
ctx->current_state = MENU_STATE_PARAM_EDIT;
|
||||||
|
// Trigger redraw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_KEY_ESC:
|
||||||
|
if (!menu_stack_is_empty(&ctx->stack)) {
|
||||||
|
MenuNodeId parent_id;
|
||||||
|
menu_stack_pop(&ctx->stack, &parent_id);
|
||||||
|
menu_core_exit_node(ctx, ctx->current_node_id);
|
||||||
|
menu_core_enter_node(ctx, parent_id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_RENDER:
|
||||||
|
// Handle render event - this could trigger custom render callbacks or refresh the display
|
||||||
|
// For now, we'll just update the last refresh tick
|
||||||
|
ctx->last_refresh_tick = 0; // TODO: Use actual tick parameter from event
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_PARAM_CHANGED:
|
||||||
|
// Handle parameter change event
|
||||||
|
// This could be used to update the display or trigger other actions when a parameter changes
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_MENU_ENTER:
|
||||||
|
// Handle menu enter event
|
||||||
|
// This event is triggered when entering a menu node
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_MENU_EXIT:
|
||||||
|
// Handle menu exit event
|
||||||
|
// This event is triggered when exiting a menu node
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_STATE_CHANGED:
|
||||||
|
// Handle state change event
|
||||||
|
// This event is triggered when the menu state changes
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_ERROR:
|
||||||
|
// Handle error event
|
||||||
|
// This event is triggered when an error occurs
|
||||||
|
ctx->current_state = MENU_STATE_ERROR;
|
||||||
|
ctx->error_code = (MenuErrCode)event->param;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_EVENT_REFRESH:
|
||||||
|
// Handle refresh event
|
||||||
|
// This event is used to force a refresh of the menu display
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle custom events
|
||||||
|
if (event->type >= MENU_EVENT_CUSTOM_BASE) {
|
||||||
|
// Custom events can be handled by user-defined callbacks
|
||||||
|
// For now, we'll just ignore them
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate_checksum is now implemented in menu_persistence.c
|
||||||
|
// Keeping this for backward compatibility in case it's used elsewhere
|
||||||
|
static uint8_t calculate_checksum(const MenuPersistenceData* data) {
|
||||||
|
(void)data; // Suppress unused parameter warning
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_save_state(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
return menu_persistence_save_impl(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_core_load_state(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
return menu_persistence_load_impl(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_core_loop(MenuCoreCtx* ctx, uint32_t tick) {
|
||||||
|
if (!ctx || !ctx->is_initialized) return;
|
||||||
|
|
||||||
|
// Process events
|
||||||
|
while (!menu_event_is_empty(&ctx->event_queue)) {
|
||||||
|
MenuEvent evt;
|
||||||
|
if (menu_event_get(&ctx->event_queue, &evt) == MENU_ERR_OK) {
|
||||||
|
menu_core_handle_event(ctx, &evt);
|
||||||
|
ctx->persistence.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
MenuNode* current = menu_core_get_node(ctx, ctx->current_node_id);
|
||||||
|
if (current && current->render_cb) {
|
||||||
|
current->render_cb(current->id, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save if dirty
|
||||||
|
#if MENU_CONFIG_ENABLE_PERSISTENCE
|
||||||
|
if (ctx->persistence.dirty) {
|
||||||
|
menu_core_save_state(ctx);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
36
src/core/menu_core.h
Normal file
36
src/core/menu_core.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#ifndef MENU_CORE_H
|
||||||
|
#define MENU_CORE_H
|
||||||
|
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Core initialization
|
||||||
|
MenuErrCode menu_core_init(MenuCoreCtx* ctx);
|
||||||
|
|
||||||
|
// Node management
|
||||||
|
MenuErrCode menu_core_register_node(MenuCoreCtx* ctx, const MenuNode* node, MenuNodeId* out_id);
|
||||||
|
MenuNode* menu_core_get_node(MenuCoreCtx* ctx, MenuNodeId id);
|
||||||
|
MenuErrCode menu_core_unregister_node(MenuCoreCtx* ctx, MenuNodeId id);
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
void menu_core_loop(MenuCoreCtx* ctx, uint32_t tick);
|
||||||
|
|
||||||
|
// State machine
|
||||||
|
MenuErrCode menu_core_handle_event(MenuCoreCtx* ctx, const MenuEvent* event);
|
||||||
|
|
||||||
|
// Internal helpers
|
||||||
|
MenuErrCode menu_core_enter_node(MenuCoreCtx* ctx, MenuNodeId node_id);
|
||||||
|
MenuErrCode menu_core_exit_node(MenuCoreCtx* ctx, MenuNodeId node_id);
|
||||||
|
|
||||||
|
// Persistence management
|
||||||
|
MenuErrCode menu_core_save_state(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_core_load_state(MenuCoreCtx* ctx);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_CORE_H
|
||||||
80
src/core/menu_event.c
Normal file
80
src/core/menu_event.c
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#include "menu_event.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
MenuErrCode menu_event_init(MenuEventQueue* queue) {
|
||||||
|
if (!queue) return MENU_ERR_INVALID_PARAM;
|
||||||
|
memset(queue, 0, sizeof(MenuEventQueue));
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_event_post(MenuEventQueue* queue, MenuEventType type, uint32_t param, MenuEventPriority priority) {
|
||||||
|
if (!queue) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
if (queue->count >= MENU_CONFIG_EVENT_QUEUE_LEN) {
|
||||||
|
return MENU_ERR_QUEUE_FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new event
|
||||||
|
MenuEvent new_event;
|
||||||
|
new_event.type = type;
|
||||||
|
new_event.param = param;
|
||||||
|
new_event.timestamp = 0; // timestamp handling can be added later if needed
|
||||||
|
new_event.priority = priority;
|
||||||
|
|
||||||
|
if (queue->count == 0) {
|
||||||
|
// Queue is empty, add at tail
|
||||||
|
queue->events[queue->tail] = new_event;
|
||||||
|
queue->tail = (queue->tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
queue->count++;
|
||||||
|
} else {
|
||||||
|
// Find insertion position based on priority
|
||||||
|
uint8_t insert_pos = queue->tail;
|
||||||
|
uint8_t i = queue->head;
|
||||||
|
|
||||||
|
// Traverse the queue to find the first event with lower or equal priority
|
||||||
|
for (uint8_t j = 0; j < queue->count; j++) {
|
||||||
|
if (queue->events[i].priority < priority) {
|
||||||
|
insert_pos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i = (i + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift events to make room for the new event
|
||||||
|
if (insert_pos != queue->tail) {
|
||||||
|
uint8_t current = queue->tail;
|
||||||
|
uint8_t prev = (current - 1 + MENU_CONFIG_EVENT_QUEUE_LEN) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
|
||||||
|
while (current != insert_pos) {
|
||||||
|
queue->events[current] = queue->events[prev];
|
||||||
|
current = prev;
|
||||||
|
prev = (prev - 1 + MENU_CONFIG_EVENT_QUEUE_LEN) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the new event
|
||||||
|
queue->events[insert_pos] = new_event;
|
||||||
|
queue->tail = (queue->tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
queue->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_event_get(MenuEventQueue* queue, MenuEvent* event) {
|
||||||
|
if (!queue || !event) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
if (queue->count == 0) {
|
||||||
|
return MENU_ERR_QUEUE_EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
*event = queue->events[queue->head];
|
||||||
|
queue->head = (queue->head + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
|
||||||
|
queue->count--;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_event_is_empty(const MenuEventQueue* queue) {
|
||||||
|
return (queue == NULL) || (queue->count == 0);
|
||||||
|
}
|
||||||
19
src/core/menu_event.h
Normal file
19
src/core/menu_event.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#ifndef MENU_EVENT_H
|
||||||
|
#define MENU_EVENT_H
|
||||||
|
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
MenuErrCode menu_event_init(MenuEventQueue* queue);
|
||||||
|
MenuErrCode menu_event_post(MenuEventQueue* queue, MenuEventType type, uint32_t param, MenuEventPriority priority);
|
||||||
|
MenuErrCode menu_event_get(MenuEventQueue* queue, MenuEvent* event);
|
||||||
|
bool menu_event_is_empty(const MenuEventQueue* queue);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_EVENT_H
|
||||||
86
src/core/menu_hash.c
Normal file
86
src/core/menu_hash.c
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#include "menu_hash.h"
|
||||||
|
#include "menu_core.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
MenuErrCode menu_hash_init(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
memset(ctx->hash_table, 0, sizeof(ctx->hash_table));
|
||||||
|
|
||||||
|
// Initialize hash_next_id to 0 for all nodes
|
||||||
|
for (int i = 0; i < MENU_CONFIG_MAX_NODES; i++) {
|
||||||
|
ctx->nodes[i].hash_next_id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_hash_insert(MenuCoreCtx* ctx, MenuNodeId id, uint16_t index) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
uint16_t hash_index = id % MENU_CONFIG_HASH_TABLE_SIZE;
|
||||||
|
uint16_t current = ctx->hash_table[hash_index];
|
||||||
|
|
||||||
|
if (current == 0) {
|
||||||
|
ctx->hash_table[hash_index] = index + 1;
|
||||||
|
} else {
|
||||||
|
MenuNode* prev_node = &ctx->nodes[current - 1];
|
||||||
|
while (prev_node->hash_next_id != 0) {
|
||||||
|
prev_node = &ctx->nodes[prev_node->hash_next_id - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_node->hash_next_id = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuNode* menu_hash_find(MenuCoreCtx* ctx, MenuNodeId id) {
|
||||||
|
if (!ctx) return NULL;
|
||||||
|
|
||||||
|
uint16_t hash_index = id % MENU_CONFIG_HASH_TABLE_SIZE;
|
||||||
|
uint16_t current = ctx->hash_table[hash_index];
|
||||||
|
|
||||||
|
while (current != 0) {
|
||||||
|
uint16_t node_index = current - 1;
|
||||||
|
MenuNode* node = &ctx->nodes[node_index];
|
||||||
|
if (node->flags.is_registered && node->id == id) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = node->hash_next_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_hash_remove(MenuCoreCtx* ctx, MenuNodeId id, uint16_t index) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
uint16_t hash_index = id % MENU_CONFIG_HASH_TABLE_SIZE;
|
||||||
|
uint16_t current = ctx->hash_table[hash_index];
|
||||||
|
uint16_t prev = 0;
|
||||||
|
|
||||||
|
while (current != 0) {
|
||||||
|
uint16_t node_index = current - 1;
|
||||||
|
|
||||||
|
if (node_index == index) {
|
||||||
|
if (prev == 0) {
|
||||||
|
// Remove head of the list
|
||||||
|
ctx->hash_table[hash_index] = ctx->nodes[node_index].hash_next_id;
|
||||||
|
} else {
|
||||||
|
// Remove from middle/end
|
||||||
|
ctx->nodes[prev - 1].hash_next_id = ctx->nodes[node_index].hash_next_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the hash_next_id for this node
|
||||||
|
ctx->nodes[node_index].hash_next_id = 0;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = current;
|
||||||
|
current = ctx->nodes[node_index].hash_next_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
19
src/core/menu_hash.h
Normal file
19
src/core/menu_hash.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#ifndef MENU_HASH_H
|
||||||
|
#define MENU_HASH_H
|
||||||
|
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
MenuErrCode menu_hash_init(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_hash_insert(MenuCoreCtx* ctx, MenuNodeId id, uint16_t index);
|
||||||
|
MenuNode* menu_hash_find(MenuCoreCtx* ctx, MenuNodeId id);
|
||||||
|
MenuErrCode menu_hash_remove(MenuCoreCtx* ctx, MenuNodeId id, uint16_t index);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_HASH_H
|
||||||
157
src/core/menu_permission.c
Normal file
157
src/core/menu_permission.c
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
#include "menu_permission.h"
|
||||||
|
#include "menu_core.h"
|
||||||
|
#include "menu_hash.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_init_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
memset(&ctx->permission, 0, sizeof(ctx->permission));
|
||||||
|
ctx->permission.role_count = 0;
|
||||||
|
ctx->permission.current_role_id = 0;
|
||||||
|
|
||||||
|
// Register default roles
|
||||||
|
menu_permission_register_role_impl(ctx, 1, "Guest", 0);
|
||||||
|
menu_permission_register_role_impl(ctx, 2, "User", 1);
|
||||||
|
menu_permission_register_role_impl(ctx, 3, "Admin", 2);
|
||||||
|
|
||||||
|
// Set default role to Guest
|
||||||
|
ctx->permission.current_role_id = 1;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_register_role_impl(MenuCoreCtx* ctx, uint8_t role_id, const char* name, MenuPermissionLevel level) {
|
||||||
|
if (!ctx || !name) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (ctx->permission.role_count >= 8) return MENU_ERR_NO_MEM;
|
||||||
|
|
||||||
|
// Check if role already exists
|
||||||
|
for (uint8_t i = 0; i < ctx->permission.role_count; i++) {
|
||||||
|
if (ctx->permission.roles[i].id == role_id) {
|
||||||
|
return MENU_ERR_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuRole* role = &ctx->permission.roles[ctx->permission.role_count];
|
||||||
|
role->id = role_id;
|
||||||
|
role->name = name;
|
||||||
|
role->level = level;
|
||||||
|
ctx->permission.role_count++;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_unregister_role_impl(MenuCoreCtx* ctx, uint8_t role_id) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < ctx->permission.role_count; i++) {
|
||||||
|
if (ctx->permission.roles[i].id == role_id) {
|
||||||
|
// Shift roles up
|
||||||
|
for (uint8_t j = i; j < ctx->permission.role_count - 1; j++) {
|
||||||
|
ctx->permission.roles[j] = ctx->permission.roles[j + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->permission.role_count--;
|
||||||
|
|
||||||
|
// If we removed the current role, set to default
|
||||||
|
if (ctx->permission.current_role_id == role_id) {
|
||||||
|
ctx->permission.current_role_id = 1; // Guest role
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_set_current_role_impl(MenuCoreCtx* ctx, uint8_t role_id) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
// Check if role exists
|
||||||
|
if (!menu_permission_role_exists_impl(ctx, role_id)) {
|
||||||
|
return MENU_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->permission.current_role_id = role_id;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t menu_permission_get_current_role_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return 0;
|
||||||
|
return ctx->permission.current_role_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_update_node_level_impl(MenuCoreCtx* ctx, MenuNodeId node_id, MenuPermissionLevel level) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, node_id);
|
||||||
|
if (!node) return MENU_ERR_NOT_FOUND;
|
||||||
|
|
||||||
|
node->permission_level = level;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_permission_check_node_access_impl(MenuCoreCtx* ctx, MenuNodeId node_id) {
|
||||||
|
if (!ctx) return false;
|
||||||
|
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, node_id);
|
||||||
|
if (!node) return false;
|
||||||
|
|
||||||
|
#if MENU_CONFIG_ENABLE_PERMISSION
|
||||||
|
MenuRole* role_node = NULL;
|
||||||
|
for (uint8_t i = 0; i < ctx->permission.role_count; i++) {
|
||||||
|
if (ctx->permission.roles[i].id == ctx->permission.current_role_id) {
|
||||||
|
role_node = &ctx->permission.roles[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!role_node || role_node->level < node->permission_level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_permission_check_and_enter_node_impl(MenuCoreCtx* ctx, MenuNodeId node_id) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
// Check permission first
|
||||||
|
if (!menu_permission_check_node_access_impl(ctx, node_id)) {
|
||||||
|
return MENU_ERR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If permission granted, enter the node
|
||||||
|
return menu_core_enter_node(ctx, node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuRole* menu_permission_get_role_impl(MenuCoreCtx* ctx, uint8_t role_id) {
|
||||||
|
if (!ctx) return NULL;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < ctx->permission.role_count; i++) {
|
||||||
|
if (ctx->permission.roles[i].id == role_id) {
|
||||||
|
return &ctx->permission.roles[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t menu_permission_get_role_count_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return 0;
|
||||||
|
return ctx->permission.role_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_permission_role_exists_impl(MenuCoreCtx* ctx, uint8_t role_id) {
|
||||||
|
if (!ctx) return false;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < ctx->permission.role_count; i++) {
|
||||||
|
if (ctx->permission.roles[i].id == role_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
31
src/core/menu_permission.h
Normal file
31
src/core/menu_permission.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#ifndef MENU_PERMISSION_H
|
||||||
|
#define MENU_PERMISSION_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Permission management functions
|
||||||
|
MenuErrCode menu_permission_init_impl(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_permission_register_role_impl(MenuCoreCtx* ctx, uint8_t role_id, const char* name, MenuPermissionLevel level);
|
||||||
|
MenuErrCode menu_permission_unregister_role_impl(MenuCoreCtx* ctx, uint8_t role_id);
|
||||||
|
MenuErrCode menu_permission_set_current_role_impl(MenuCoreCtx* ctx, uint8_t role_id);
|
||||||
|
uint8_t menu_permission_get_current_role_impl(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_permission_update_node_level_impl(MenuCoreCtx* ctx, MenuNodeId node_id, MenuPermissionLevel level);
|
||||||
|
bool menu_permission_check_node_access_impl(MenuCoreCtx* ctx, MenuNodeId node_id);
|
||||||
|
MenuErrCode menu_permission_check_and_enter_node_impl(MenuCoreCtx* ctx, MenuNodeId node_id);
|
||||||
|
|
||||||
|
// Role query functions
|
||||||
|
MenuRole* menu_permission_get_role_impl(MenuCoreCtx* ctx, uint8_t role_id);
|
||||||
|
uint8_t menu_permission_get_role_count_impl(MenuCoreCtx* ctx);
|
||||||
|
bool menu_permission_role_exists_impl(MenuCoreCtx* ctx, uint8_t role_id);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_PERMISSION_H
|
||||||
109
src/core/menu_persistence.c
Normal file
109
src/core/menu_persistence.c
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#include "menu_persistence.h"
|
||||||
|
#include "menu_types.h"
|
||||||
|
#include "menu_core.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// Mock storage buffer for persistence (simulating non-volatile storage)
|
||||||
|
static MenuPersistenceData g_persistence_storage = {0};
|
||||||
|
static bool g_storage_initialized = false;
|
||||||
|
|
||||||
|
// Calculate checksum for integrity verification
|
||||||
|
static uint16_t calculate_checksum(const MenuPersistenceData* data) {
|
||||||
|
if (!data) return 0;
|
||||||
|
|
||||||
|
const uint8_t* bytes = (const uint8_t*)data;
|
||||||
|
uint16_t checksum = 0;
|
||||||
|
uint16_t len = sizeof(MenuPersistenceData) - sizeof(data->checksum);
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
checksum ^= bytes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_init_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
ctx->persistence.dirty = false;
|
||||||
|
|
||||||
|
// Initialize storage if not already initialized
|
||||||
|
if (!g_storage_initialized) {
|
||||||
|
memset(&g_persistence_storage, 0, sizeof(g_persistence_storage));
|
||||||
|
g_storage_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_save_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuPersistenceData data = {
|
||||||
|
.current_node_id = ctx->current_node_id,
|
||||||
|
.current_state = ctx->current_state,
|
||||||
|
.nav_path = ctx->nav_path,
|
||||||
|
.stack = ctx->stack,
|
||||||
|
.timestamp = 0, // In a real system, this would be a actual timestamp
|
||||||
|
.checksum = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate checksum for integrity verification
|
||||||
|
data.checksum = calculate_checksum(&data);
|
||||||
|
|
||||||
|
// Write to mock storage
|
||||||
|
memcpy(&g_persistence_storage, &data, sizeof(MenuPersistenceData));
|
||||||
|
|
||||||
|
// Mark as not dirty
|
||||||
|
ctx->persistence.dirty = false;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_load_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
MenuPersistenceData data;
|
||||||
|
|
||||||
|
// Read from mock storage
|
||||||
|
memcpy(&data, &g_persistence_storage, sizeof(MenuPersistenceData));
|
||||||
|
|
||||||
|
// Verify checksum for integrity
|
||||||
|
uint16_t expected_checksum = calculate_checksum(&data);
|
||||||
|
if (data.checksum != expected_checksum) {
|
||||||
|
return MENU_ERR_CORRUPTED_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the current_node_id exists in the node table
|
||||||
|
MenuNode* node = menu_core_get_node(ctx, data.current_node_id);
|
||||||
|
if (!node) {
|
||||||
|
return MENU_ERR_INVALID_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the state into the context
|
||||||
|
ctx->current_node_id = data.current_node_id;
|
||||||
|
ctx->current_state = data.current_state;
|
||||||
|
ctx->nav_path = data.nav_path;
|
||||||
|
ctx->stack = data.stack;
|
||||||
|
|
||||||
|
ctx->persistence.dirty = false;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_clear_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
// Clear mock storage
|
||||||
|
memset(&g_persistence_storage, 0, sizeof(g_persistence_storage));
|
||||||
|
g_persistence_storage.checksum = calculate_checksum(&g_persistence_storage);
|
||||||
|
|
||||||
|
ctx->persistence.dirty = true;
|
||||||
|
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_persistence_is_dirty_impl(MenuCoreCtx* ctx) {
|
||||||
|
if (!ctx) return false;
|
||||||
|
return ctx->persistence.dirty;
|
||||||
|
}
|
||||||
36
src/core/menu_persistence.h
Normal file
36
src/core/menu_persistence.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* menu_persistence.h
|
||||||
|
*
|
||||||
|
* Created on: 2024-07-22
|
||||||
|
* Author: Jfen
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MENU_PERSISTENCE_H
|
||||||
|
#define MENU_PERSISTENCE_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
// Persistence management functions
|
||||||
|
MenuErrCode menu_persistence_init_impl(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_persistence_save_impl(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_persistence_load_impl(MenuCoreCtx* ctx);
|
||||||
|
MenuErrCode menu_persistence_clear_impl(MenuCoreCtx* ctx);
|
||||||
|
bool menu_persistence_is_dirty_impl(MenuCoreCtx* ctx);
|
||||||
|
|
||||||
|
// Storage write/read callbacks for platform-specific implementation
|
||||||
|
typedef MenuErrCode (*MenuPersistenceWriteFunc)(const void* data, size_t size);
|
||||||
|
typedef MenuErrCode (*MenuPersistenceReadFunc)(void* data, size_t size);
|
||||||
|
|
||||||
|
MenuErrCode menu_persistence_register_storage_impl(MenuCoreCtx* ctx,
|
||||||
|
MenuPersistenceWriteFunc write_func,
|
||||||
|
MenuPersistenceReadFunc read_func);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_PERSISTENCE_H
|
||||||
50
src/core/menu_stack.c
Normal file
50
src/core/menu_stack.c
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#include "menu_stack.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
MenuErrCode menu_stack_init(MenuStack* stack) {
|
||||||
|
if (!stack) return MENU_ERR_INVALID_PARAM;
|
||||||
|
memset(stack, 0, sizeof(MenuStack));
|
||||||
|
stack->top = 0;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_stack_push(MenuStack* stack, MenuNodeId id) {
|
||||||
|
if (!stack) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (stack->top >= MENU_CONFIG_STACK_DEPTH) {
|
||||||
|
return MENU_ERR_STACK_OVERFLOW;
|
||||||
|
}
|
||||||
|
stack->path[stack->top++] = id;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_stack_pop(MenuStack* stack, MenuNodeId* id) {
|
||||||
|
if (!stack) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (stack->top == 0) {
|
||||||
|
return MENU_ERR_STACK_UNDERFLOW;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
*id = stack->path[--stack->top];
|
||||||
|
} else {
|
||||||
|
stack->top--;
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_stack_peek(const MenuStack* stack, MenuNodeId* id) {
|
||||||
|
if (!stack || !id) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (stack->top == 0) {
|
||||||
|
return MENU_ERR_STACK_UNDERFLOW;
|
||||||
|
}
|
||||||
|
*id = stack->path[stack->top - 1];
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_stack_is_empty(const MenuStack* stack) {
|
||||||
|
return (stack == NULL) || (stack->top == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_stack_clear(MenuStack* stack) {
|
||||||
|
if (stack) {
|
||||||
|
stack->top = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/core/menu_stack.h
Normal file
21
src/core/menu_stack.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#ifndef MENU_STACK_H
|
||||||
|
#define MENU_STACK_H
|
||||||
|
|
||||||
|
#include "menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
MenuErrCode menu_stack_init(MenuStack* stack);
|
||||||
|
MenuErrCode menu_stack_push(MenuStack* stack, MenuNodeId id);
|
||||||
|
MenuErrCode menu_stack_pop(MenuStack* stack, MenuNodeId* id);
|
||||||
|
MenuErrCode menu_stack_peek(const MenuStack* stack, MenuNodeId* id);
|
||||||
|
bool menu_stack_is_empty(const MenuStack* stack);
|
||||||
|
void menu_stack_clear(MenuStack* stack);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_STACK_H
|
||||||
190
src/core/menu_types.h
Normal file
190
src/core/menu_types.h
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
#ifndef MENU_TYPES_H
|
||||||
|
#define MENU_TYPES_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "menu_config.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
struct MenuCoreCtx;
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
typedef uint16_t MenuNodeId;
|
||||||
|
typedef uint8_t MenuPermissionLevel;
|
||||||
|
|
||||||
|
// Role structure
|
||||||
|
typedef struct {
|
||||||
|
uint8_t id;
|
||||||
|
const char* name;
|
||||||
|
MenuPermissionLevel level;
|
||||||
|
} MenuRole;
|
||||||
|
|
||||||
|
// Error codes
|
||||||
|
typedef enum {
|
||||||
|
MENU_ERR_OK = 0,
|
||||||
|
MENU_ERR_FAIL,
|
||||||
|
MENU_ERR_INVALID_PARAM,
|
||||||
|
MENU_ERR_NO_MEM,
|
||||||
|
MENU_ERR_NOT_FOUND,
|
||||||
|
MENU_ERR_PERMISSION_DENIED,
|
||||||
|
MENU_ERR_QUEUE_FULL,
|
||||||
|
MENU_ERR_QUEUE_EMPTY,
|
||||||
|
MENU_ERR_STACK_OVERFLOW,
|
||||||
|
MENU_ERR_STACK_UNDERFLOW,
|
||||||
|
MENU_ERR_NOT_INIT,
|
||||||
|
MENU_ERR_CORRUPTED_DATA,
|
||||||
|
MENU_ERR_INVALID_NODE,
|
||||||
|
} MenuErrCode;
|
||||||
|
|
||||||
|
// Menu States
|
||||||
|
typedef enum {
|
||||||
|
MENU_STATE_INIT = 0,
|
||||||
|
MENU_STATE_NORMAL,
|
||||||
|
MENU_STATE_PARAM_EDIT,
|
||||||
|
MENU_STATE_CONFIRM,
|
||||||
|
MENU_STATE_ERROR,
|
||||||
|
} MenuState;
|
||||||
|
|
||||||
|
// Menu Events
|
||||||
|
typedef enum {
|
||||||
|
MENU_EVENT_NONE = 0,
|
||||||
|
MENU_EVENT_KEY_UP,
|
||||||
|
MENU_EVENT_KEY_DOWN,
|
||||||
|
MENU_EVENT_KEY_ENTER,
|
||||||
|
MENU_EVENT_KEY_ESC,
|
||||||
|
MENU_EVENT_TIMEOUT,
|
||||||
|
MENU_EVENT_RENDER, // Optimization: Render event
|
||||||
|
|
||||||
|
// Extended event types for more complex interactions
|
||||||
|
MENU_EVENT_PARAM_CHANGED, // Parameter value changed
|
||||||
|
MENU_EVENT_MENU_ENTER, // Entered a menu node
|
||||||
|
MENU_EVENT_MENU_EXIT, // Exited a menu node
|
||||||
|
MENU_EVENT_STATE_CHANGED, // Menu state changed
|
||||||
|
MENU_EVENT_ERROR, // Error occurred
|
||||||
|
MENU_EVENT_REFRESH, // Force refresh the menu
|
||||||
|
MENU_EVENT_CUSTOM_BASE = 20, // Custom events start from here
|
||||||
|
} MenuEventType;
|
||||||
|
|
||||||
|
// Callback function types
|
||||||
|
typedef MenuErrCode (*MenuCallback)(MenuNodeId node_id, struct MenuCoreCtx* ctx);
|
||||||
|
|
||||||
|
// Menu Node Structure
|
||||||
|
typedef struct MenuNode {
|
||||||
|
MenuNodeId id;
|
||||||
|
MenuNodeId parent_id;
|
||||||
|
const char* name; // Or language key
|
||||||
|
MenuCallback enter_cb;
|
||||||
|
MenuCallback exit_cb;
|
||||||
|
MenuCallback render_cb; // Optimization: Custom render callback per node
|
||||||
|
|
||||||
|
MenuNodeId first_child_id;
|
||||||
|
MenuNodeId next_sibling_id;
|
||||||
|
MenuNodeId prev_sibling_id;
|
||||||
|
MenuNodeId hash_next_id;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool is_registered : 1;
|
||||||
|
bool is_selected : 1;
|
||||||
|
bool is_visible : 1;
|
||||||
|
bool is_enabled : 1;
|
||||||
|
unsigned int reserved : 4;
|
||||||
|
} flags;
|
||||||
|
|
||||||
|
MenuPermissionLevel permission_level;
|
||||||
|
uint16_t param_id;
|
||||||
|
void* user_data; // Optimization: User context
|
||||||
|
} MenuNode;
|
||||||
|
|
||||||
|
// Navigation Stack
|
||||||
|
typedef struct {
|
||||||
|
MenuNodeId path[MENU_CONFIG_STACK_DEPTH];
|
||||||
|
uint8_t top;
|
||||||
|
} MenuStack;
|
||||||
|
|
||||||
|
// Event priority levels
|
||||||
|
typedef enum {
|
||||||
|
MENU_EVENT_PRIORITY_LOW = 0,
|
||||||
|
MENU_EVENT_PRIORITY_NORMAL,
|
||||||
|
MENU_EVENT_PRIORITY_HIGH,
|
||||||
|
MENU_EVENT_PRIORITY_URGENT,
|
||||||
|
} MenuEventPriority;
|
||||||
|
|
||||||
|
// Event structure
|
||||||
|
typedef struct {
|
||||||
|
MenuEventType type;
|
||||||
|
uint32_t param;
|
||||||
|
uint32_t timestamp;
|
||||||
|
MenuEventPriority priority;
|
||||||
|
} MenuEvent;
|
||||||
|
|
||||||
|
// Event Queue
|
||||||
|
typedef struct {
|
||||||
|
MenuEvent events[MENU_CONFIG_EVENT_QUEUE_LEN];
|
||||||
|
uint8_t head;
|
||||||
|
uint8_t tail;
|
||||||
|
uint8_t count;
|
||||||
|
} MenuEventQueue;
|
||||||
|
|
||||||
|
// Navigation Path (breadcrumbs for display)
|
||||||
|
typedef struct {
|
||||||
|
MenuNodeId nodes[MENU_CONFIG_STACK_DEPTH];
|
||||||
|
uint8_t depth;
|
||||||
|
} MenuNavPath;
|
||||||
|
|
||||||
|
typedef struct MenuPersistenceData {
|
||||||
|
MenuNodeId current_node_id;
|
||||||
|
MenuState current_state;
|
||||||
|
MenuNavPath nav_path;
|
||||||
|
MenuStack stack;
|
||||||
|
uint32_t timestamp;
|
||||||
|
uint8_t checksum;
|
||||||
|
} MenuPersistenceData;
|
||||||
|
|
||||||
|
// Core Context
|
||||||
|
typedef struct MenuCoreCtx {
|
||||||
|
MenuNode nodes[MENU_CONFIG_MAX_NODES];
|
||||||
|
uint16_t hash_table[MENU_CONFIG_HASH_TABLE_SIZE];
|
||||||
|
MenuStack stack;
|
||||||
|
MenuNavPath nav_path;
|
||||||
|
MenuEventQueue event_queue;
|
||||||
|
|
||||||
|
MenuNodeId current_node_id;
|
||||||
|
MenuState current_state;
|
||||||
|
uint32_t last_refresh_tick;
|
||||||
|
bool is_initialized;
|
||||||
|
|
||||||
|
MenuErrCode error_code;
|
||||||
|
const char* error_msg;
|
||||||
|
|
||||||
|
uint16_t node_count;
|
||||||
|
uint16_t free_node_count;
|
||||||
|
|
||||||
|
// Permission module data
|
||||||
|
struct {
|
||||||
|
uint8_t current_role_id;
|
||||||
|
MenuRole roles[8];
|
||||||
|
uint8_t role_count;
|
||||||
|
} permission;
|
||||||
|
|
||||||
|
// Persistence module data
|
||||||
|
struct {
|
||||||
|
bool dirty;
|
||||||
|
} persistence;
|
||||||
|
|
||||||
|
// Language module data
|
||||||
|
struct {
|
||||||
|
uint8_t current_lang_id;
|
||||||
|
} lang;
|
||||||
|
|
||||||
|
} MenuCoreCtx;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_TYPES_H
|
||||||
28
src/lang/menu_lang.c
Normal file
28
src/lang/menu_lang.c
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#include "menu_lang.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static MenuLangID g_curr_lang = MENU_LANG_EN;
|
||||||
|
|
||||||
|
void menu_lang_set(MenuLangID lang_id) {
|
||||||
|
if (lang_id < MENU_LANG_MAX) {
|
||||||
|
g_curr_lang = lang_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuLangID menu_lang_get(void) {
|
||||||
|
return g_curr_lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* menu_lang_get_str(const char* key) {
|
||||||
|
// Simple placeholder. In real implementation, look up in table.
|
||||||
|
// If key starts with '$', it's a key, else return as is.
|
||||||
|
if (!key) return "";
|
||||||
|
if (key[0] == '$') {
|
||||||
|
// Mock lookup
|
||||||
|
if (strcmp(key, "$MAIN") == 0) return (g_curr_lang == MENU_LANG_CN) ? "主菜单" : "Main Menu";
|
||||||
|
if (strcmp(key, "$SETTING") == 0) return (g_curr_lang == MENU_LANG_CN) ? "设置" : "Settings";
|
||||||
|
if (strcmp(key, "$INFO") == 0) return (g_curr_lang == MENU_LANG_CN) ? "信息" : "Info";
|
||||||
|
return key + 1; // Return key without $
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
27
src/lang/menu_lang.h
Normal file
27
src/lang/menu_lang.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#ifndef MENU_LANG_H
|
||||||
|
#define MENU_LANG_H
|
||||||
|
|
||||||
|
#include "../core/menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MENU_LANG_EN = 0,
|
||||||
|
MENU_LANG_CN,
|
||||||
|
MENU_LANG_MAX
|
||||||
|
} MenuLangID;
|
||||||
|
|
||||||
|
// Set current language
|
||||||
|
void menu_lang_set(MenuLangID lang_id);
|
||||||
|
MenuLangID menu_lang_get(void);
|
||||||
|
|
||||||
|
// Get string
|
||||||
|
const char* menu_lang_get_str(const char* key);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_LANG_H
|
||||||
96
src/param/menu_param.c
Normal file
96
src/param/menu_param.c
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#include "menu_param.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MENU_MAX_PARAMS 32
|
||||||
|
|
||||||
|
static MenuParam g_params[MENU_MAX_PARAMS];
|
||||||
|
static uint16_t g_param_count = 0;
|
||||||
|
|
||||||
|
MenuErrCode menu_param_init(void) {
|
||||||
|
memset(g_params, 0, sizeof(g_params));
|
||||||
|
g_param_count = 0;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_param_register(const MenuParam* param) {
|
||||||
|
if (!param) return MENU_ERR_INVALID_PARAM;
|
||||||
|
if (g_param_count >= MENU_MAX_PARAMS) return MENU_ERR_NO_MEM;
|
||||||
|
|
||||||
|
// Check ID conflict
|
||||||
|
for (int i = 0; i < g_param_count; i++) {
|
||||||
|
if (g_params[i].id == param->id) {
|
||||||
|
return MENU_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_params[g_param_count++] = *param;
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuParam* menu_param_get(uint16_t id) {
|
||||||
|
for (int i = 0; i < g_param_count; i++) {
|
||||||
|
if (g_params[i].id == id) {
|
||||||
|
return &g_params[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuErrCode menu_param_set_int(uint16_t id, int32_t val) {
|
||||||
|
const MenuParam* p = menu_param_get(id);
|
||||||
|
if (!p || p->read_only || !p->value_ptr) return MENU_ERR_INVALID_PARAM;
|
||||||
|
|
||||||
|
switch (p->type) {
|
||||||
|
case MENU_PARAM_TYPE_BOOL:
|
||||||
|
*(bool*)p->value_ptr = (val != 0);
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_INT8:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(int8_t*)p->value_ptr = (int8_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_UINT8:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(uint8_t*)p->value_ptr = (uint8_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_INT16:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(int16_t*)p->value_ptr = (int16_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_UINT16:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(uint16_t*)p->value_ptr = (uint16_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_INT32:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(int32_t*)p->value_ptr = (int32_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_UINT32:
|
||||||
|
if (val < p->meta.num.min || val > p->meta.num.max) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(uint32_t*)p->value_ptr = (uint32_t)val;
|
||||||
|
break;
|
||||||
|
case MENU_PARAM_TYPE_ENUM:
|
||||||
|
if (val < 0 || val >= p->meta.enum_meta.count) return MENU_ERR_INVALID_PARAM;
|
||||||
|
*(uint8_t*)p->value_ptr = (uint8_t)val; // Assume enum stored as uint8
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return MENU_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
return MENU_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t menu_param_get_int(uint16_t id) {
|
||||||
|
const MenuParam* p = menu_param_get(id);
|
||||||
|
if (!p || !p->value_ptr) return 0;
|
||||||
|
|
||||||
|
switch (p->type) {
|
||||||
|
case MENU_PARAM_TYPE_BOOL: return *(bool*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_INT8: return *(int8_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_UINT8: return *(uint8_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_INT16: return *(int16_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_UINT16: return *(uint16_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_INT32: return *(int32_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_UINT32: return *(uint32_t*)p->value_ptr;
|
||||||
|
case MENU_PARAM_TYPE_ENUM: return *(uint8_t*)p->value_ptr;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/param/menu_param.h
Normal file
63
src/param/menu_param.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#ifndef MENU_PARAM_H
|
||||||
|
#define MENU_PARAM_H
|
||||||
|
|
||||||
|
#include "../core/menu_types.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MENU_PARAM_TYPE_NONE = 0,
|
||||||
|
MENU_PARAM_TYPE_BOOL,
|
||||||
|
MENU_PARAM_TYPE_INT8,
|
||||||
|
MENU_PARAM_TYPE_UINT8,
|
||||||
|
MENU_PARAM_TYPE_INT16,
|
||||||
|
MENU_PARAM_TYPE_UINT16,
|
||||||
|
MENU_PARAM_TYPE_INT32,
|
||||||
|
MENU_PARAM_TYPE_UINT32,
|
||||||
|
MENU_PARAM_TYPE_FLOAT,
|
||||||
|
MENU_PARAM_TYPE_STRING,
|
||||||
|
MENU_PARAM_TYPE_ENUM,
|
||||||
|
} MenuParamType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t id;
|
||||||
|
MenuParamType type;
|
||||||
|
const char* name;
|
||||||
|
void* value_ptr; // Pointer to actual variable
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
int32_t min;
|
||||||
|
int32_t max;
|
||||||
|
int32_t step;
|
||||||
|
} num;
|
||||||
|
struct {
|
||||||
|
const char** options;
|
||||||
|
uint8_t count;
|
||||||
|
} enum_meta;
|
||||||
|
struct {
|
||||||
|
uint16_t max_len;
|
||||||
|
} str_meta;
|
||||||
|
} meta;
|
||||||
|
bool read_only;
|
||||||
|
} MenuParam;
|
||||||
|
|
||||||
|
// Initialize param system
|
||||||
|
MenuErrCode menu_param_init(void);
|
||||||
|
|
||||||
|
// Register a parameter
|
||||||
|
MenuErrCode menu_param_register(const MenuParam* param);
|
||||||
|
|
||||||
|
// Get parameter by ID
|
||||||
|
const MenuParam* menu_param_get(uint16_t id);
|
||||||
|
|
||||||
|
// Set value (helper)
|
||||||
|
MenuErrCode menu_param_set_int(uint16_t id, int32_t val);
|
||||||
|
int32_t menu_param_get_int(uint16_t id);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // MENU_PARAM_H
|
||||||
Reference in New Issue
Block a user