diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eead513 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cache/* +build/* + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e65e5b3 --- /dev/null +++ b/CMakeLists.txt @@ -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) diff --git a/api/menu_api.c b/api/menu_api.c new file mode 100644 index 0000000..e84744d --- /dev/null +++ b/api/menu_api.c @@ -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 + +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; iparam_id = param_id; + return MENU_ERR_OK; +} diff --git a/api/menu_api.h b/api/menu_api.h new file mode 100644 index 0000000..d88c31b --- /dev/null +++ b/api/menu_api.h @@ -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 diff --git a/examples/demo.c b/examples/demo.c new file mode 100644 index 0000000..1855fbf --- /dev/null +++ b/examples/demo.c @@ -0,0 +1,119 @@ +#include "menu_api.h" +#include "menu_param.h" +#include "menu_lang.h" +#include "menu_port.h" +#include +#include + +// 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; +} diff --git a/port/menu_port.c b/port/menu_port.c new file mode 100644 index 0000000..c68f6e2 --- /dev/null +++ b/port/menu_port.c @@ -0,0 +1,21 @@ +#include "menu_port.h" +#include +#include +#include + +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"); +} diff --git a/port/menu_port.h b/port/menu_port.h new file mode 100644 index 0000000..6eb273e --- /dev/null +++ b/port/menu_port.h @@ -0,0 +1,18 @@ +#ifndef MENU_PORT_H +#define MENU_PORT_H + +#include + +#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 diff --git a/src/core/menu_config.h b/src/core/menu_config.h new file mode 100644 index 0000000..0f01b8b --- /dev/null +++ b/src/core/menu_config.h @@ -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 diff --git a/src/core/menu_core.c b/src/core/menu_core.c new file mode 100644 index 0000000..86b83dd --- /dev/null +++ b/src/core/menu_core.c @@ -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 + +// 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; inav_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; inodes[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); + } +} diff --git a/src/core/menu_core.h b/src/core/menu_core.h new file mode 100644 index 0000000..157ad5a --- /dev/null +++ b/src/core/menu_core.h @@ -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 diff --git a/src/core/menu_event.c b/src/core/menu_event.c new file mode 100644 index 0000000..3661b04 --- /dev/null +++ b/src/core/menu_event.c @@ -0,0 +1,48 @@ +#include "menu_event.h" +#include + +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); +} diff --git a/src/core/menu_event.h b/src/core/menu_event.h new file mode 100644 index 0000000..0b6c689 --- /dev/null +++ b/src/core/menu_event.h @@ -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 diff --git a/src/core/menu_hash.c b/src/core/menu_hash.c new file mode 100644 index 0000000..a8878b4 --- /dev/null +++ b/src/core/menu_hash.c @@ -0,0 +1,99 @@ +#include "menu_hash.h" +#include "menu_core.h" +#include + +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 +} diff --git a/src/core/menu_hash.h b/src/core/menu_hash.h new file mode 100644 index 0000000..b0a1e8c --- /dev/null +++ b/src/core/menu_hash.h @@ -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 diff --git a/src/core/menu_stack.c b/src/core/menu_stack.c new file mode 100644 index 0000000..dec67d2 --- /dev/null +++ b/src/core/menu_stack.c @@ -0,0 +1,50 @@ +#include "menu_stack.h" +#include + +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; + } +} diff --git a/src/core/menu_stack.h b/src/core/menu_stack.h new file mode 100644 index 0000000..23366f3 --- /dev/null +++ b/src/core/menu_stack.h @@ -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 diff --git a/src/core/menu_types.h b/src/core/menu_types.h new file mode 100644 index 0000000..a08b4c6 --- /dev/null +++ b/src/core/menu_types.h @@ -0,0 +1,153 @@ +#ifndef MENU_TYPES_H +#define MENU_TYPES_H + +#include +#include +#include +#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 diff --git a/src/lang/menu_lang.c b/src/lang/menu_lang.c new file mode 100644 index 0000000..fb62669 --- /dev/null +++ b/src/lang/menu_lang.c @@ -0,0 +1,28 @@ +#include "menu_lang.h" +#include + +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; +} diff --git a/src/lang/menu_lang.h b/src/lang/menu_lang.h new file mode 100644 index 0000000..7c5883e --- /dev/null +++ b/src/lang/menu_lang.h @@ -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 diff --git a/src/param/menu_param.c b/src/param/menu_param.c new file mode 100644 index 0000000..3d04ea8 --- /dev/null +++ b/src/param/menu_param.c @@ -0,0 +1,96 @@ +#include "menu_param.h" +#include + +#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; + } +} diff --git a/src/param/menu_param.h b/src/param/menu_param.h new file mode 100644 index 0000000..b59fd5b --- /dev/null +++ b/src/param/menu_param.h @@ -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 diff --git a/tool/code_generator.py b/tool/code_generator.py deleted file mode 100644 index b5a0ed3..0000000 --- a/tool/code_generator.py +++ /dev/null @@ -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}") \ No newline at end of file