初始化版本

This commit is contained in:
冯佳
2025-12-23 08:29:44 +08:00
parent 0ec8a5b380
commit cc9f363ff8
22 changed files with 1370 additions and 50 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.cache/*
build/*

25
CMakeLists.txt Normal file
View 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)

115
api/menu_api.c Normal file
View File

@ -0,0 +1,115 @@
#include "menu_api.h"
#include "../src/core/menu_core.h"
#include "../src/core/menu_event.h"
#include "../src/core/menu_stack.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_post_event(MenuEventType type, uint32_t param) {
return menu_event_post(&g_menu_ctx.event_queue, type, param);
}
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(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;
}

51
api/menu_api.h Normal file
View File

@ -0,0 +1,51 @@
#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
);
// Event Handling
MenuErrCode menu_post_event(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);
#ifdef __cplusplus
}
#endif
#endif // MENU_API_H

119
examples/demo.c Normal file
View File

@ -0,0 +1,119 @@
#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 = 0; // 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)
}
// 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
// Simulation Loop
int steps = 0;
while (steps < 20) {
menu_main_loop(0);
// Simulate inputs
if (steps == 2) {
menu_port_log("User Press: ENTER");
menu_post_event(MENU_EVENT_KEY_ENTER, 0);
}
if (steps == 4) {
menu_port_log("User Press: DOWN");
menu_post_event(MENU_EVENT_KEY_DOWN, 0);
}
steps++;
// sleep
}
return 0;
}

21
port/menu_port.c Normal file
View 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
View 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
View 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

316
src/core/menu_core.c Normal file
View File

@ -0,0 +1,316 @@
#include "menu_core.h"
#include "menu_event.h"
#include "menu_stack.h"
#include "menu_hash.h"
#include "menu_param.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;
// Simple linear scan for now.
// Optimization: Could use hash table if implemented to return index/pointer
for (int i = 0; i < MENU_CONFIG_MAX_NODES; i++) {
if (ctx->nodes[i].flags.is_registered && ctx->nodes[i].id == id) {
return &ctx->nodes[i];
}
}
return NULL;
}
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);
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);
if (out_id) *out_id = new_node->id;
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
// TODO: Check permission
#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;
default:
break;
}
return MENU_ERR_OK;
}
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);
}
}
// Render
// Optimization: Add a render callback to the context or port
// For now we rely on the current node's render_cb
MenuNode* current = menu_core_get_node(ctx, ctx->current_node_id);
if (current && current->render_cb) {
current->render_cb(current->id, ctx);
}
}

31
src/core/menu_core.h Normal file
View File

@ -0,0 +1,31 @@
#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);
// 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);
#ifdef __cplusplus
}
#endif
#endif // MENU_CORE_H

48
src/core/menu_event.c Normal file
View File

@ -0,0 +1,48 @@
#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) {
if (!queue) return MENU_ERR_INVALID_PARAM;
if (queue->count >= MENU_CONFIG_EVENT_QUEUE_LEN) {
return MENU_ERR_QUEUE_FULL;
}
MenuEvent* evt = &queue->events[queue->tail];
evt->type = type;
evt->param = param;
// timestamp handling can be added here if we have system tick access,
// for now left as 0 or handled by caller if passed in param (which is just 32bit)
// or we can add a tick parameter to post_event.
// For simplicity, we assume timestamp is not critical for now or set elsewhere.
evt->timestamp = 0;
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
View 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);
MenuErrCode menu_event_get(MenuEventQueue* queue, MenuEvent* event);
bool menu_event_is_empty(const MenuEventQueue* queue);
#ifdef __cplusplus
}
#endif
#endif // MENU_EVENT_H

99
src/core/menu_hash.c Normal file
View File

@ -0,0 +1,99 @@
#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));
return MENU_ERR_OK;
}
MenuErrCode menu_hash_insert(MenuCoreCtx* ctx, MenuNodeId id) {
if (!ctx) return MENU_ERR_INVALID_PARAM;
uint16_t index = id % MENU_CONFIG_HASH_TABLE_SIZE;
MenuNodeId current = ctx->hash_table[index];
if (current == 0) {
ctx->hash_table[index] = id;
} else {
// Collision handling: append to the end of the chain
// Note: 0 is used as invalid ID/null pointer in this static array context
// This implies valid node IDs must start from 1.
// Find the node structure for 'current'
// This requires access to the node array which is in ctx
// but we need to find the node index from the ID if ID != index
// In this system, we usually assume ID corresponds to index if allocated sequentially,
// but since we support arbitrary IDs (maybe?), we need to be careful.
// HOWEVER, the design says "Static node pool", so usually we just iterate.
// BUT wait, if we are building a hash table, we want O(1) access.
// Let's assume ID is NOT the index in the array, but a unique handle.
// But we need to find the MenuNode* from ID to set hash_next_id.
// Since we are inside menu_hash_insert, we probably just added the node
// so we might know where it is, but this function takes ID.
// We need a way to look up the MenuNode* from ID to set the next pointer.
// If we can't find it efficiently, this hash insert is slow.
// Actually, looking at `MenuCoreCtx`, `nodes` is an array.
// Does `nodes[i].id` == i?
// If the user provides arbitrary IDs, we need to scan the array to find the node
// to set its `hash_next_id`.
// That defeats the purpose of the hash table if insertion is O(N).
// But insertion happens only at init. Lookup is frequent.
// Let's implement finding the node by scanning for insertion.
MenuNode* prev_node = NULL;
// Find the head of the chain
for (int i = 0; i < ctx->node_count; i++) {
if (ctx->nodes[i].id == current) {
prev_node = &ctx->nodes[i];
break;
}
}
if (!prev_node) return MENU_ERR_NOT_FOUND; // Should not happen
while (prev_node->hash_next_id != 0) {
MenuNodeId next_id = prev_node->hash_next_id;
bool found = false;
for (int i = 0; i < ctx->node_count; i++) {
if (ctx->nodes[i].id == next_id) {
prev_node = &ctx->nodes[i];
found = true;
break;
}
}
if (!found) return MENU_ERR_FAIL; // Broken chain
}
prev_node->hash_next_id = id;
}
return MENU_ERR_OK;
}
MenuNodeId menu_hash_find(MenuCoreCtx* ctx, MenuNodeId id) {
if (!ctx) return 0;
uint16_t index = id % MENU_CONFIG_HASH_TABLE_SIZE;
MenuNodeId current = ctx->hash_table[index];
while (current != 0) {
if (current == id) return current;
// Move to next
bool found = false;
for (int i = 0; i < ctx->node_count; i++) {
if (ctx->nodes[i].id == current) {
current = ctx->nodes[i].hash_next_id;
found = true;
break;
}
}
if (!found) break;
}
return 0; // Not found
}

18
src/core/menu_hash.h Normal file
View File

@ -0,0 +1,18 @@
#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);
MenuNodeId menu_hash_find(MenuCoreCtx* ctx, MenuNodeId id);
#ifdef __cplusplus
}
#endif
#endif // MENU_HASH_H

50
src/core/menu_stack.c Normal file
View 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
View 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

153
src/core/menu_types.h Normal file
View File

@ -0,0 +1,153 @@
#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;
// 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,
} 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
MENU_EVENT_CUSTOM_BASE = 10,
} 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 structure
typedef struct {
MenuEventType type;
uint32_t param;
uint32_t timestamp;
} 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;
// Core Context
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;
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;
} 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
View 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
View 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
View 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
View 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

View File

@ -1,50 +0,0 @@
def generate_menu_nodes_source(self):
"""生成菜单节点源文件"""
menu_src_file = os.path.join(self.output_dir, f"{self.source_prefix}nodes.c")
with open(menu_src_file, 'w', encoding='utf-8') as f:
f.write("/**\n")
f.write(" **********************************************************************************************************************\n")
f.write(" * @file menu_nodes.c\n")
f.write(" * @brief 电磁启动器菜单节点实现\n")
f.write(" * @author Code Generator\n")
f.write(" * @date 2025-12-22 12:00:00\n")
f.write(" **********************************************************************************************************************\n")
f.write(" */\n\n")
f.write("#include \"menu_nodes.h\"\n\n")
# 生成菜单节点数组
f.write("/* 菜单节点数组定义 */\n")
f.write("const MenuNode g_menu_nodes[] = {\n")
for i, node in enumerate(self.nodes):
f.write(" {\n")
f.write(f" .id = {node['id']},\n")
f.write(f" .parent_id = {node['parent_id']},\n")
f.write(f" .name_en = \"{node['name']['en']}",\n")
f.write(f" .name_cn = \"{node['name']['cn']}",\n")
f.write(f" .id_name = \"{node['id_name']}",\n")
f.write(f" .enter_cb = {node.get('enter_cb', 'NULL')},\n")
f.write(f" .display_cb = {node.get('display_cb', 'NULL')},\n")
f.write(f" .action_cb = {node.get('action_cb', 'NULL')},\n")
f.write(f" .visible_cb = {node.get('visible_cb', 'NULL')},\n")
f.write(f" .permission_level = {node.get('permission_level', 0)},\n")
f.write(f" .confirm_required = {str(node.get('confirm_required', False)).lower()},\n")
f.write(f" .bind_param_id = {node.get('bind_param_id', 0)},\n")
f.write(f" .menu_item_type = {node['c_code'].get('menu_item_type', 'SUB_MENU')}\n")
# 最后一个元素不需要逗号
if i == len(self.nodes) - 1:
f.write(" }\n")
else:
f.write(" },\n")
f.write("};
")
# 生成菜单节点数量
f.write("/* 菜单节点数量 */\n")
f.write("const uint32_t g_menu_node_count = sizeof(g_menu_nodes) / sizeof(g_menu_nodes[0]);\n\n")
print(f"菜单节点源文件生成成功: {menu_src_file}")