diff --git a/CMakeLists.txt b/CMakeLists.txt index 77513d8..f30b5bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,14 +30,13 @@ add_executable(DTU-HMI src/thread_utils.c src/remoteDisplay.c src/Drv/menu/app/menu.c - src/Drv/menu/model/display.c - src/Drv/menu/model/menu_model.c - src/Drv/menu/view/menu_view.c - src/Drv/menu/presenter/menu_presenter.c - src/Drv/menu/model/menu_tree_builder.c - src/Drv/menu/view/menu_layout.c - src/Drv/menu/presenter/menu_navigator.c - src/Drv/menu/view/menu_renderer_lcd.c + src/Drv/pages/page_manager.c + src/Drv/pages/global/global_state.c + src/Drv/pages/global/renderer_lcd.c + src/Drv/pages/menu/page.c + src/Drv/pages/menu/model.c + src/Drv/pages/menu/presenter.c + src/Drv/pages/menu/view.c src/Drv/lcd/lcd.c src/Drv/lcd/lcd_draw.c src/Drv/lcd/lcd_text.c diff --git a/Testing/Temporary/CTestCostData.txt b/Testing/Temporary/CTestCostData.txt deleted file mode 100644 index ed97d53..0000000 --- a/Testing/Temporary/CTestCostData.txt +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/docs/系统架构设计文档.md b/docs/系统架构设计文档.md index 88ff0fb..21186dc 100644 --- a/docs/系统架构设计文档.md +++ b/docs/系统架构设计文档.md @@ -77,13 +77,535 @@ columns 1 - 处理按键驱动的菜单状态迁移与路径重建 - 执行菜单显示坐标计算与多级菜单渲染调度 -### 4.3 显示层 +### 4.3 多页面管理层设计 +**设计思想:** +基于经典的 MVP 范式,彻底解耦**数据、视图、控制逻辑**,解决复杂 UI 与业务逻辑的协同维护问题。 +- **Model(模型层)**:纯业务数据与状态管理,负责系统参数、设备状态、采集数据的读写、校验、存储,与 UI 完全无关。 +- **View(视图层)**:纯渲染显示,仅根据 Model 的数据绘制菜单界面、焦点高亮、弹窗、动画,不处理任何业务逻辑。 +- **Presenter(控制层)**:核心调度中枢,接收输入事件、更新 Model 数据、控制 View 刷新、处理菜单跳转逻辑,是 Model 与 View 的唯一桥梁。 +目录结构: +`src/Drv/pages`: 这个文件夹下面放不同的页面 +`src/Drv/pages/global`: 全局状态管理器,页面之间交互的中间件 +`src/Drv/pages/global/renderer_lcd.c`: 这个文件是抽象底层的lcd,给所有页面提供一个统一的调用接口。 +`src/Drv/pages/global/renderer_lcd.h`: 这个文件是抽象底层的lcd,给所有页面提供一个统一的调用接口。 +`src/Drv/pages/menu`: 项目的菜单逻辑 +`src/Drv/pages/menu/model`: 菜单的模型层 +`src/Drv/pages/menu/view`: 菜单的视图层 +`src/Drv/pages/menu/Presenter`: 菜单的控制层 +`src/Drv/pages/menu/page.c`: 菜单的页面逻辑 +`src/Drv/pages/menu/page.h`: 菜单的页面逻辑 -- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/menu/model/display.c` -- 职责: - - 管理 LCD 显存 `g_tCVsr` 与像素绘制 - - 提供 ASCII/UTF-8 字符显示能力 - - 提供静态菜单模型定义 `g_tMenuModelTab` +`src/Drv/pages/...`: 其他页面 + +整个多页面管理系统分为 5 个核心模块,从上到下形成完整的调度闭环,与底层 MVP 架构无缝衔接 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 全局状态管理器(Global Model) │ +│ (跨页面共享数据、持久化配置、系统全局状态、观察者通知) │ +├─────────────────────────────────────────────────────────────┤ +│ 页面管理器核心(Page Manager) │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ 页面栈(Page Stack 静态数组) ││ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ +│ │ │ 栈顶页面 │ │ 后台页面1 │ │ 后台页面2 │ ... ││ +│ │ │(前台显示) │ │(暂停状态) │ │(暂停状态) │ ││ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││ +│ └─────────────────────────────────────────────────────────┘│ +│ 生命周期调度器 | 事件分发器 | 页面跳转核心接口 │ +├─────────────────────────────────────────────────────────────┤ +│ 标准化页面抽象(Page 接口层) │ +│ 每个页面独立封装:生命周期钩子 + 专属MVP三元组 │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ Page 基础属性:页面ID、缓存标志、创建状态 ││ +│ │ 生命周期钩子:on_create/on_enter/on_exit/on_destroy ││ +│ │ 运行入口:on_event(事件处理)、on_loop(主循环) ││ +│ │ 专属MVP三元组:Model(私有) → Presenter → View ││ +│ └─────────────────────────────────────────────────────────┘│ +├─────────────────────────────────────────────────────────────┤ +│ 底层支撑层(与MVP架构无缝衔接) │ +│ 显示HAL | 输入HAL | 基础图形库 | 硬件驱动 │ +└─────────────────────────────────────────────────────────────┘ +``` +#### 4.3.1 标准化页面抽象(Page 接口层) +这是整个系统的基础单元,**每个独立业务页面对应一个 Page 实例,每个 Page 内部封装一套完全独立的 MVP 三元组**,对外仅暴露标准接口,页面管理器仅通过标准接口与页面交互,完全不关心页面内部的 MVP 实现,实现页面间的彻底解耦。 +```c +// 编译期静态配置(根据MCU资源调整) +#define MAX_PAGE_STACK_DEPTH 5 // 最大页面栈深度,防止栈溢出 +#define MAX_PAGE_COUNT 8 // 系统支持的最大页面总数 +#define EVENT_QUEUE_LENGTH 8 // 输入事件队列长度 + +// 页面唯一ID枚举(所有页面必须在此定义,用于页面查找、防重复入栈) +typedef enum { + PAGE_ID_NONE = 0, + PAGE_ID_HOME, // 首页(主页面,栈底常驻) + PAGE_ID_SETTINGS, // 设置页面 + PAGE_ID_TEMP_DETAIL, // 温度详情页面 + PAGE_ID_ABOUT, // 关于页面 + PAGE_ID_MAX +} page_id_t; + +// 事件处理结果枚举 +typedef enum { + EVENT_UNHANDLED = 0, // 事件未被消费,继续分发 + EVENT_HANDLED = 1 // 事件已被消费,终止分发 +} event_result_t; + +// 页面结构体前置声明 +typedef struct page_t page_t; + +``` +#### 4.3.2 页面标准接口与生命周期定义 +嵌入式场景下,生命周期钩子的设计必须严格对应资源的申请 / 释放时机,避免 RAM 浪费和低功耗异常,每个钩子的执行时机、职责完全固定,禁止越权操作。 +```c +// 页面标准接口结构体 +struct page_t { + // ===================== 页面基础属性 ===================== + page_id_t page_id; // 页面唯一ID,不可重复 + bool is_cached; // 缓存标志:true=出栈时不销毁,仅暂停;false=出栈时彻底销毁 + bool is_created; // 内部状态标志:页面是否已执行on_create,外部只读 + + // ===================== 生命周期钩子函数 ===================== + /** + * @brief 页面第一次创建时执行,仅执行1次 + * @note 职责:MVP三元组初始化、静态资源申请(字体/图标缓冲区)、回调绑定 + * @note 禁止:耗时操作、屏幕绘制、动态内存申请 + */ + void (*on_create)(page_t *page); + + /** + * @brief 页面进入前台(成为栈顶)时执行,每次进入都执行 + * @note 职责:从Model拉取最新数据、全量UI绘制、开启定时器/传感器、注册事件回调 + * @note 禁止:资源销毁、MVP初始化操作 + */ + void (*on_enter)(page_t *page); + + /** + * @brief 页面离开前台(被覆盖/出栈)时执行,每次离开都执行 + * @note 职责:关闭定时器/传感器、保存临时数据、注销事件回调、低功耗准备 + * @note 禁止:屏幕绘制、MVP初始化、耗时操作 + */ + void (*on_exit)(page_t *page); + + /** + * @brief 页面彻底销毁时执行,仅执行1次 + * @note 职责:释放所有申请的资源、MVP实例销毁、回调注销 + * @note 仅非缓存页面出栈时会触发,缓存页面不会触发 + */ + void (*on_destroy)(page_t *page); + + // ===================== 运行时入口 ===================== + /** + * @brief 事件处理入口,页面管理器仅将事件分发给栈顶页面的此函数 + * @note 职责:将事件转发给内部View,由View判断命中并转发给Presenter + * @return 事件是否被消费 + */ + event_result_t (*on_event)(page_t *page, input_event_t *event); + + /** + * @brief 主循环入口,页面管理器仅调用栈顶页面的此函数 + * @note 职责:执行页面Presenter的主循环逻辑、UI动画刷新、状态机轮询 + * @note 禁止:阻塞操作、耗时计算 + */ + void (*on_loop)(page_t *page); + + // ===================== 页面专属MVP三元组 ===================== + // 每个页面独立拥有,与其他页面完全隔离,页面管理器不直接访问 + void *presenter; // 页面对应的Presenter实例指针 + void *view; // 页面对应的View实例指针 + void *model; // 页面对应的私有Model实例(仅当前页面使用) +}; +``` +#### 4.3.3 页面 MVP 三元组的绑定规则 +- **完全隔离**:每个页面的 Model 仅管理当前页面的私有数据,禁止跨页面访问;View 仅负责当前页面的 UI 绘制;Presenter 仅调度当前页面的业务逻辑 +- **绑定时机**:MVP 三元组的初始化、绑定,必须在页面的`on_create`钩子中完成,保证页面创建时 MVP 已就绪 +- **销毁时机**:MVP 实例的资源释放,必须在页面的`on_destroy`钩子中完成,避免资源泄漏 +- **跨页面交互**:禁止页面 A 的 Presenter 直接调用页面 B 的任何接口,所有跨页面数据同步必须通过全局状态管理器完成 +#### 4.3.4 栈式页面管理器(核心调度中枢) +这是整个系统的核心,通过静态数组实现的栈结构管理所有页面,负责页面跳转的核心逻辑、生命周期的精准调度、输入事件的统一分发、主循环的统一调度。 +**页面管理器结构体定义** +```c +// 页面管理器结构体 +typedef struct { + page_t *page_stack[MAX_PAGE_STACK_DEPTH]; // 页面栈,静态数组实现 + int8_t stack_top; // 栈顶指针,初始值为-1(空栈) +} page_manager_t; + +// 全局页面管理器单例(整个系统仅一个实例) +extern page_manager_t g_page_manager; +``` +**基础接口:初始化与栈状态查询** +```c +// 全局页面管理器实例 +page_manager_t g_page_manager; + +/** + * @brief 页面管理器初始化,系统启动时调用1次 + */ +void page_manager_init(page_manager_t *manager) { + memset(manager, 0, sizeof(page_manager_t)); + manager->stack_top = -1; // 初始化为空栈 +} + +/** + * @brief 获取当前栈顶页面(前台显示页面) + * @return 栈顶页面指针,空栈返回NULL + */ +page_t* page_manager_get_top(page_manager_t *manager) { + if (manager->stack_top < 0) { + return NULL; + } + return manager->page_stack[manager->stack_top]; +} +``` +**页面入栈(Push,页面跳转)** +- 入参校验:检查栈是否已满、新页面是否合法、是否重复入栈 +- 触发旧栈顶页面的`on_exit`:当前页面被覆盖,执行暂停逻辑 +- 新页面入栈,栈顶指针 + 1 +- 若新页面未创建,触发`on_create`:执行 MVP 初始化,仅执行 1 次 +- 触发新页面的`on_enter`:新页面进入前台,执行 UI 绘制与业务启动 +- 新页面成为前台页面,接收事件与主循环调度 +```c +/** + * @brief 页面入栈(跳转到新页面) + * @param manager 页面管理器实例 + * @param new_page 待入栈的新页面实例 + * @return 0=成功,负数=错误码 + */ +int page_manager_push(page_manager_t *manager, page_t *new_page) { + // 入参合法性校验 + if (manager == NULL || new_page == NULL) { + return -1; + } + // 栈满校验,防止栈溢出 + if (manager->stack_top >= MAX_PAGE_STACK_DEPTH - 1) { + return -2; + } + // 防重复入栈:当前栈顶已是该页面,无需重复操作 + page_t *current_top = page_manager_get_top(manager); + if (current_top != NULL && current_top->page_id == new_page->page_id) { + return 0; + } + + // ===================== 生命周期执行 ===================== + // 1. 触发当前栈顶页面的on_exit(离开前台) + if (current_top != NULL && current_top->on_exit != NULL) { + current_top->on_exit(current_top); + } + + // 2. 新页面入栈 + manager->stack_top++; + manager->page_stack[manager->stack_top] = new_page; + + // 3. 页面首次创建,触发on_create(仅执行1次) + if (!new_page->is_created) { + if (new_page->on_create != NULL) { + new_page->on_create(new_page); + } + new_page->is_created = true; + } + + // 4. 触发新页面的on_enter(进入前台) + if (new_page->on_enter != NULL) { + new_page->on_enter(new_page); + } + + return 0; +} +``` +**页面出栈(Pop,页面返回)** +- 入参校验:检查栈是否为空、是否为栈底首页(禁止出栈) +- 触发当前栈顶页面的`on_exit`:当前页面离开前台,执行退出准备 +- 若当前页面为非缓存页面,触发`on_destroy`:彻底销毁页面,释放资源 +- 栈顶指针 - 1,上一个页面恢复为栈顶 +- 触发新栈顶页面的`on_enter`:页面恢复前台,执行数据刷新与 UI 重绘 +- 新栈顶页面成为前台页面,接收事件与主循环调度 +```c +/** + * @brief 页面出栈(返回上一页) + * @param manager 页面管理器实例 + * @return 0=成功,负数=错误码 + */ +int page_manager_pop(page_manager_t *manager) { + // 入参校验 + if (manager == NULL) { + return -1; + } + // 禁止空栈出栈、禁止栈底首页出栈(保证首页常驻) + if (manager->stack_top <= 0) { + return -2; + } + + // ===================== 生命周期执行 ===================== + // 1. 获取当前栈顶页面(待出栈页面) + page_t *current_page = manager->page_stack[manager->stack_top]; + + // 2. 触发当前页面的on_exit(离开前台) + if (current_page->on_exit != NULL) { + current_page->on_exit(current_page); + } + + // 3. 非缓存页面:触发on_destroy,彻底销毁,释放资源 + if (!current_page->is_cached) { + if (current_page->on_destroy != NULL) { + current_page->on_destroy(current_page); + } + current_page->is_created = false; + } + + // 4. 栈顶指针减1,页面出栈 + manager->stack_top--; + + // 5. 触发新栈顶页面的on_enter(恢复前台) + page_t *new_top = manager->page_stack[manager->stack_top]; + if (new_top->on_enter != NULL) { + new_top->on_enter(new_top); + } + + return 0; +} +``` + +**统一事件分发机制** +嵌入式多页面 GUI 的核心痛点之一是事件误响应,必须保证**仅当前前台页面(栈顶)可接收输入事件**,后台页面完全无法接收事件,避免出现「设置页面按键触发首页逻辑」的 bug +- 底层 Input HAL 在中断中采集输入事件(按键、触摸、编码器),投递到全局静态环形事件队列 +- 页面管理器在主循环中从事件队列取出事件 +- 获取当前栈顶前台页面,将事件分发给页面的`on_event`入口 +- 页面将事件转发给内部 View,View 判断事件命中,转发给 Presenter 处理 +- 若页面返回`EVENT_UNHANDLED`(事件未被消费),则执行全局事件处理(例如全局返回键、电源键 +```c +// 全局事件处理(示例:全局返回键) +static void global_event_handle(input_event_t *event) { + // 全局返回键:无论哪个页面,按返回键都执行出栈 + if (event->type == EVENT_TYPE_KEY_DOWN && event->key_id == KEY_ID_BACK) { + page_manager_pop(&g_page_manager); + } +} + +// 页面管理器事件分发逻辑(集成在主循环中) +static void page_manager_dispatch_event(page_manager_t *manager) { + input_event_t event; + // 从事件队列取出事件 + if (!input_event_get(&event)) { + return; + } + // 获取当前栈顶页面 + page_t *current_page = page_manager_get_top(manager); + if (current_page == NULL) { + return; + } + // 分发给当前页面 + event_result_t result = EVENT_UNHANDLED; + if (current_page->on_event != NULL) { + result = current_page->on_event(current_page, &event); + } + // 页面未消费,执行全局事件处理 + if (result == EVENT_UNHANDLED) { + global_event_handle(&event); + } +} +``` +#### 4.3.4 全局状态管理器(跨页面数据同步) +多页面场景下,必然存在跨页面数据共享的需求(例如设置页修改温度单位,首页、详情页都要同步更新),如果直接让页面间互相调用,会彻底破坏 MVP 的解耦原则,导致代码耦合混乱。 + +全局状态管理器的核心设计思路是:**实现一个全局单例的 Model(GlobalModel),作为跨页面数据的唯一可信源,通过观察者模式实现数据变化的同步通知,完全不破坏各页面 MVP 的独立性** +#### 4.3.5 完整页面实现示例 +以下是一个完整的页面实现,包含生命周期钩子、MVP 三元组绑定、页面跳转逻辑,可直接落地使用。 +**设置页面 MVP 定义** +```c +// ===================== 设置页面Model ===================== +typedef struct { + float temp_offset; // 温度校准偏移 + uint8_t temp_unit; // 温度单位 + bool backlight_auto; // 自动背光开关 + // 数据变化回调 + void (*on_data_change)(void *context); + void *context; +} settings_model_t; + +// ===================== 设置页面View接口 ===================== +typedef struct { + void (*update_temp_offset)(void *view, float offset); + void (*update_temp_unit)(void *view, uint8_t unit); + void (*update_backlight_state)(void *view, bool auto_on); + void (*draw)(void *view); +} settings_view_interface_t; + +// ===================== 设置页面Presenter ===================== +typedef struct { + page_t *page; + settings_model_t *model; + settings_view_interface_t *view; +} settings_presenter_t; +``` +**设置页面实例与生命周期实现** +```c +// 静态页面实例与MVP实例(静态内存,无动态分配) +static page_t settings_page; +static settings_model_t settings_model; +static settings_view_interface_t settings_view; +static settings_presenter_t settings_presenter; + +// ===================== 生命周期钩子实现 ===================== +// 页面创建:MVP初始化,仅执行1次 +static void settings_page_on_create(page_t *page) { + // 1. 初始化Model + memset(&settings_model, 0, sizeof(settings_model_t)); + // 从Flash读取持久化配置 + settings_model.temp_unit = global_model_get_temp_unit(); + settings_model.temp_offset = flash_read_temp_offset(); + settings_model.backlight_auto = flash_read_backlight_auto(); + // 绑定Model回调 + settings_model.context = &settings_presenter; + settings_model.on_data_change = settings_presenter_on_model_change; + + // 2. 初始化View接口 + memset(&settings_view, 0, sizeof(settings_view_interface_t)); + settings_view.update_temp_offset = settings_view_update_temp_offset; + settings_view.update_temp_unit = settings_view_update_temp_unit; + settings_view.update_backlight_state = settings_view_update_backlight_state; + settings_view.draw = settings_view_draw; + + // 3. 初始化Presenter,绑定MVP + memset(&settings_presenter, 0, sizeof(settings_presenter_t)); + settings_presenter.page = page; + settings_presenter.model = &settings_model; + settings_presenter.view = &settings_view; + + // 4. 绑定MVP到页面 + page->model = &settings_model; + page->view = &settings_view; + page->presenter = &settings_presenter; +} + +// 页面进入前台:刷新UI,开启定时器 +static void settings_page_on_enter(page_t *page) { + settings_presenter_t *presenter = (settings_presenter_t *)page->presenter; + // 从Model拉取数据,刷新UI + presenter->view->update_temp_offset(presenter->view, presenter->model->temp_offset); + presenter->view->update_temp_unit(presenter->view, presenter->model->temp_unit); + presenter->view->update_backlight_state(presenter->view, presenter->model->backlight_auto); + // 全量绘制页面 + presenter->view->draw(presenter->view); + // 开启背光定时器 + backlight_timer_start(); +} + +// 页面离开前台:保存数据,关闭定时器 +static void settings_page_on_exit(page_t *page) { + settings_model_t *model = (settings_model_t *)page->model; + // 关闭背光定时器 + backlight_timer_stop(); + // 保存配置到Flash + flash_write_temp_offset(model->temp_offset); + flash_write_backlight_auto(model->backlight_auto); + // 同步全局状态 + global_model_set_temp_unit(model->temp_unit); +} + +// 页面销毁:释放资源 +static void settings_page_on_destroy(page_t *page) { + memset(page->model, 0, sizeof(settings_model_t)); + memset(page->presenter, 0, sizeof(settings_presenter_t)); + page->model = NULL; + page->view = NULL; + page->presenter = NULL; +} + +// 事件处理入口 +static event_result_t settings_page_on_event(page_t *page, input_event_t *event) { + settings_view_interface_t *view = (settings_view_interface_t *)page->view; + // 转发事件给View处理 + return settings_view_handle_event(view, event); +} + +// 主循环入口 +static void settings_page_on_loop(page_t *page) { + settings_presenter_t *presenter = (settings_presenter_t *)page->presenter; + // 执行Presenter主循环逻辑(长按检测、动画刷新等) + settings_presenter_loop(presenter); +} + +// ===================== 页面实例获取接口 ===================== +page_t* settings_page_get_instance(void) { + memset(&settings_page, 0, sizeof(page_t)); + settings_page.page_id = PAGE_ID_SETTINGS; + settings_page.is_cached = true; // 缓存页面,出栈不销毁 + // 绑定生命周期钩子 + settings_page.on_create = settings_page_on_create; + settings_page.on_enter = settings_page_on_enter; + settings_page.on_exit = settings_page_on_exit; + settings_page.on_destroy = settings_page_on_destroy; + settings_page.on_event = settings_page_on_event; + settings_page.on_loop = settings_page_on_loop; + return &settings_page; +} +``` +**页面跳转调用示例** +```c +// 首页点击设置按钮,跳转到设置页面 +void home_presenter_on_settings_btn_click(home_presenter_t *presenter) { + page_t *settings_page = settings_page_get_instance(); + page_manager_push(&g_page_manager, settings_page); +} + +// 设置页面点击返回按钮,返回首页 +event_result_t settings_view_handle_event(settings_view_interface_t *view, input_event_t *event) { + if (event->type == EVENT_TYPE_TOUCH_DOWN) { + // 判断返回按钮点击 + if (is_point_in_rect(event->x, event->y, BACK_BTN_X, BACK_BTN_Y, BACK_BTN_W, BACK_BTN_H)) { + // 调用出栈接口,返回上一页 + page_manager_pop(&g_page_manager); + return EVENT_HANDLED; + } + } + return EVENT_UNHANDLED; +} +``` + +#### 4.3.6 当前项目重构落地规划(已执行) +为解决 `MenuProc_See_AppInfo`、`MenuProc_See_YC` 这类“页面入口被函数化”的扩展瓶颈,本项目按以下路径重构,并要求每个模块均可独立测试: + +1. **目录与职责重构(已完成)** + - 新增 `src/Drv/pages` 作为多页面管理根目录; + - 新增 `src/Drv/pages/global`,统一承载跨页面能力(`global_state`、`renderer_lcd`); + - 新增 `src/Drv/pages/menu/page.c`,将现有菜单 MVP 封装成标准页面; + - 新增示例业务页面 `src/Drv/pages/app_info`、`src/Drv/pages/yc`,用于验证页面跳转链路与生命周期。 + +2. **核心调度中枢(已完成)** + - 新增 `page.h` 定义标准页面抽象(ID、缓存策略、生命周期钩子、事件入口、循环入口、私有 MVP 指针); + - 新增 `page_manager.c`,实现: + - 静态页面注册(防重复、上限保护); + - 栈式 `push/pop` 生命周期调度; + - 栈顶独占事件分发; + - 全局返回键兜底(页面未消费时触发 `pop`); + - 栈深度查询接口(用于诊断与测试)。 + +3. **菜单页面化改造(已完成)** + - `MenuApp_Init` 改为: + - 初始化全局状态; + - 初始化并激活 `PageManager`; + - 注册 `Menu/AppInfo/YC` 页面; + - 默认入栈 `PAGE_ID_MENU`; + - `MenuApp_HandleInput` 改为统一输入事件分发; + - `MenuApp_Render` 改为调度当前栈顶页面 `on_loop`; + - `MenuProc_See_AppInfo`、`MenuProc_See_YC` 改为页面跳转入口(非业务实现函数); + - `MenuProc_See_YC` 跳转前通过 `GlobalModel` 同步当前菜单参数,实现跨页面上下文传递。 + +4. **可靠性约束(已完成)** + - 全部页面实例采用静态内存,避免运行期动态分配; + - 页面栈深度与页面注册数均有编译期上限; + - 生命周期调用顺序固定:`exit -> (destroy) -> enter`; + - 禁止后台页面收事件,仅栈顶可处理输入。 + +5. **单元可测性设计(目标 > 80%,已落地测试入口)** + - 新增 `test_p1_page_manager.c`:覆盖页面注册/入栈/出栈/全局返回事件/循环调度; + - 新增 `test_p1_menu_page_actions.c`:覆盖菜单页跳转 `AppInfo/YC` 与返回链路; + - 保留并复用原 `menu tree/layout/navigator/runtime startup` 测试,保障回归稳定。 + +6. **后续扩展规范(作为后续页面开发模板)** + - 新页面必须以 `src/Drv/pages//page.c` 实现标准生命周期; + - 页面间禁止直接互调,只允许通过 `GlobalModel` 同步共享状态; + - 菜单项 `pfnWinProc` 仅作为“页面路由入口”,不再承载页面业务实现。 ### 4.4 输入层 @@ -111,9 +633,9 @@ columns 1 ### 5.1 菜单模型与菜单树 -- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/menu/model/display.h`,数据在 `src/Drv/menu/model/display.c`) -- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/menu/common/menu_item_types.h`,实例由 `src/Drv/menu/app/menu.c` 管理) -- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/menu/common/menu_state_types.h`,实例为 `app/menu.c` 内部私有) +- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/pages/menu/model/display.h`,数据在 `src/Drv/pages/menu/model/menu_model.c`) +- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/pages/menu/view/menu_view.h`,实例由 `src/Drv/pages/menu/page.c` 管理) +- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/pages/menu/view/menu_view.h`,实例为 `src/Drv/pages/menu/page.c` 内部私有) 关键关系: @@ -173,18 +695,18 @@ columns 1 - `src/Drv/menu/app/menu.c` - 菜单应用 Facade(`MenuApp_*` 对外接口) - - 持有运行时私有状态(`s_menuCtrl`、`s_dspCtrl`、`s_menuItems`) -- `src/Drv/menu/presenter/menu_presenter.c` + - 负责 PageManager 初始化与 `MenuPage` 生命周期托管 +- `src/Drv/pages/menu/presenter/menu_presenter.c` - 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新 -- `src/Drv/menu/model/menu_model.c` +- `src/Drv/pages/menu/model/menu_model.c` - 模型初始化:树构建、菜单名修饰、初始状态建立 -- `src/Drv/menu/view/menu_view.c` +- `src/Drv/pages/menu/view/menu_view.c` - 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略 -- `src/Drv/menu/presenter/menu_navigator.c` +- `src/Drv/pages/menu/presenter/menu_navigator.c` - 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染) -- `src/Drv/menu/view/menu_layout.c` +- `src/Drv/pages/menu/view/menu_layout.c` - 菜单布局计算:宽度统计、层级矩形定位、越界回退策略 -- `src/Drv/menu/model/display.c` +- `src/Drv/pages/menu/model/menu_model.c`(包含静态菜单表数据) - 静态菜单模型表 `g_tMenuModelTab`(业务菜单定义) #### 6.4.2 运行时调用时序 @@ -313,12 +835,12 @@ ctest --test-dir build -C Debug --output-on-failure ## 10. 模块依赖关系(代码级) - `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils` -- `src/Drv/menu/app/menu.c` 依赖:`presenter/menu_presenter`、`model/display`、`key`、`lcd` -- `src/Drv/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator` -- `src/Drv/menu/view/menu_view.c` 依赖:`model/menu_layout`、`view/menu_render_port`、`lcd` +- `src/Drv/menu/app/menu.c` 依赖:`pages/page_manager`、`pages/menu/page`、`key` +- `src/Drv/pages/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator` +- `src/Drv/pages/menu/view/menu_view.c` 依赖:`view/menu_layout`、`view/menu_render_port`、`lcd` - `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils` - `lcd.c` 依赖:`ascii` -- `src/Drv/menu/model/display.c` 提供:静态菜单表(被 menu 模块使用) +- `src/Drv/pages/menu/model/menu_model.c` 提供:静态菜单表(被 `MenuPage` 使用) ## 11. 已知风险与改进建议 diff --git a/docs/绘图/.$HMI 图像绘制.drawio.bkp b/docs/绘图/.$HMI 图像绘制.drawio.bkp new file mode 100644 index 0000000..0b3e6ae --- /dev/null +++ b/docs/绘图/.$HMI 图像绘制.drawio.bkp @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/绘图/.$HMI 图像绘制.drawio.dtmp b/docs/绘图/.$HMI 图像绘制.drawio.dtmp new file mode 100644 index 0000000..78274f3 --- /dev/null +++ b/docs/绘图/.$HMI 图像绘制.drawio.dtmp @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/绘图/HMI 图像绘制.drawio b/docs/绘图/HMI 图像绘制.drawio new file mode 100644 index 0000000..5b31e8f --- /dev/null +++ b/docs/绘图/HMI 图像绘制.drawio @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/include/types.h b/include/types.h index e512275..5806a9c 100644 --- a/include/types.h +++ b/include/types.h @@ -1,6 +1,8 @@ #ifndef __TYPES__H__ #define __TYPES__H__ +#include + #define uint8_t unsigned char #define uint16_t unsigned short #define uint32_t unsigned int @@ -15,4 +17,18 @@ typedef int (*FUNCPTR) ( ); +// 调试模式断言 +#ifdef DEBUG +#define ASSERT(expr) \ + do { \ + if (expr) { \ + printf("Assertion failed: %s, file %s, line %d\n", \ + #expr, __FILE__, __LINE__); \ + } \ + } while (0) +#else +#define ASSERT(expr) ((void)0) // 发布模式禁用断言 +#endif + + #endif diff --git a/src/Drv/menu/app/menu.c b/src/Drv/menu/app/menu.c index 119e142..5a9ae87 100644 --- a/src/Drv/menu/app/menu.c +++ b/src/Drv/menu/app/menu.c @@ -1,191 +1,54 @@ #include "menu.h" -#include -#include +#include "../../pages/menu/page.h" -#include "../model/display.h" -#include "../../key.h" -#include "../view/menu_layout.h" -#include "../presenter/menu_presenter.h" -#include "../view/menu_render_port.h" -#include "../common/menu_state_types.h" - -/* ------------------------------------------------------------------------- - * 运行态状态收口(App 层私有) - * - * 设计意图: - * 1) 避免全局变量四处散落,统一由 app/menu.c 托管生命周期; - * 2) 外部模块只能经由 MenuApp_* 接口访问,降低耦合; - * 3) Presenter/Model/View 依赖都在初始化时一次性装配。 - * - * 变量职责: - * - s_dspCtrl: - * 显示刷新控制状态(例如首帧标记 bFirst)。 - * - s_menuCtrl: - * 导航与路由控制状态(当前节点、各级 route、0 级入口等)。 - * - s_menuItems[300]: - * 运行时菜单节点池(树构建后常驻内存),由 Model 初始化、View 读取布局/渲染。 - * - g_menuPresenter: - * MVP 调度中枢实例,封装输入处理与刷新协同。 - * - g_menuLayoutConfig: - * 布局参数快照(屏幕尺寸、行高、边距、菜单宽度等),初始化时写入一次。 - * ------------------------------------------------------------------------- */ -static tagDspCtrl s_dspCtrl; -static tagMenuCtrl s_menuCtrl; -static tagMenuItem s_menuItems[300]; - -static MenuPresenter g_menuPresenter; -static MenuLayoutConfig g_menuLayoutConfig; - -void MenuApp_Render(void) +void MenuProc_See_AppInfo(void) { - /* 统一渲染入口:由 Presenter 决定全量/增量刷新策略。 */ - MenuPresenter_Refresh(&g_menuPresenter); + MenuPage_TriggerCurrentAction(); } -void MenuApp_HandleInput(uint8_t keyVal) +void MenuProc_See_YC(void) { - /* 单次输入事件入口:按键解释与状态迁移都在 Presenter 内完成。 */ - MenuPresenter_HandleInput(&g_menuPresenter, keyVal); + MenuPage_TriggerCurrentAction(); } -void MenuApp_PollInput(void) -{ - /* 轮询输入设备(键盘/按键),再复用事件处理入口。 */ - MenuApp_HandleInput(Key_Read()); -} +void MenuProc_Set_Value(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Set_Soft(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_Time(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_RevEvent(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_ManualWave(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_Input(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_Set(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_Soft(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_YX_SetCommType(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_YX_SetWidth(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_YX_SetTwin(void) { MenuPage_TriggerCurrentAction(); } -void MenuApp_Init(void) -{ - const MenuRenderPort *renderPort = MenuRenderPort_Lcd(); - uint16_t lcdSizeX; - uint16_t lcdSizeY; - - /* 1) 初始化底层渲染设备(LCD/模拟器具体实现由 renderPort 决定)。 */ - renderPort->init(); - - /* 2) 通过渲染端口查询设备能力,避免 app 层直接依赖 lcd.h 宏。 */ - lcdSizeX = renderPort->get_size_x(); - lcdSizeY = renderPort->get_size_y(); - - /* 3) 组装布局配置(后续由 View 布局计算与渲染阶段复用)。 */ - g_menuLayoutConfig.lcdSizeX = lcdSizeX; - g_menuLayoutConfig.lcdSizeY = lcdSizeY; - g_menuLayoutConfig.menuYMin = MENU_YMIN; - g_menuLayoutConfig.menuYMax = MENU_YMAX_FROM_LCD(lcdSizeY); - g_menuLayoutConfig.lineHeight = LINE_HEIGHT; - g_menuLayoutConfig.menuWidth = MENU_WITDTH; - g_menuLayoutConfig.menuXAdd = MENU_XADD; - g_menuLayoutConfig.menuYAdd = MENU_YADD; - - /* 4) 装配 Presenter:注入控制状态、菜单池、渲染端口与布局配置。 */ - MenuPresenter_Setup(&g_menuPresenter, - &s_dspCtrl, - &s_menuCtrl, - s_menuItems, - renderPort, - &g_menuLayoutConfig); - - /* 5) 初始化模型数据并建立初始路由/布局。 */ - MenuPresenter_InitModel(&g_menuPresenter, g_tMenuModelTab, MENU_MAX_ITEM); -} - -const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count) -{ - /* 诊断/测试只读访问接口: - * - 返回内部菜单池首地址; - * - 若 count 非空,同步返回数组容量。 */ - if (count != NULL) - { - *count = (uint16_t)(sizeof(s_menuItems) / sizeof(s_menuItems[0])); - } - return s_menuItems; -} - -void MenuProc_See_AppInfo() -{ - printf("MenuProc_See_AppInfo\n"); -} - -void MenuProc_See_YC() -{ - printf("MenuProc_See_YC\n"); -} -void MenuProc_Set_Value() -{ - printf("MenuProc_Set_Value\n"); -} -void MenuProc_Set_Soft() -{ - printf("MenuProc_Set_Soft\n"); -} -void MenuProc_Cfg_Time() -{ - printf("MenuProc_Cfg_Time\n"); -} -void MenuProc_Cfg_RevEvent() -{ - printf("MenuProc_Cfg_RevEvent\n"); -} -void MenuProc_Cfg_ManualWave() -{ - printf("MenuProc_Cfg_ManualWave\n"); -} -void MenuProc_See_Input() -{ - printf("MenuProc_See_Input\n"); -} - -void MenuProc_See_Set() -{ - printf("MenuProc_See_Set\n"); -} - -void MenuProc_See_Soft() -{ - printf("MenuProc_See_Soft\n"); -} -void MenuProc_YX_SetCommType() -{ - printf("MenuProc_YX_SetCommType\n"); -} - -void MenuProc_YX_SetWidth() -{ - printf("MenuProc_YX_SetWidth\n"); -} - -void MenuProc_YX_SetTwin() -{ - printf("MenuProc_YX_SetTwin\n"); -} - -/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */ -void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); } -void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); } -void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); } -void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); } -void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); } -void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); } -void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); } -void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); } -void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); } -void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); } -void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); } -void MenuProc_See_VersionBoard(void) { printf("MenuProc_See_VersionBoard\n"); } -void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); } -void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); } -void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); } -void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); } -void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); } -void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); } -void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); } -void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); } -void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); } -void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); } -void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); } -void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); } -void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); } -void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); } -void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); } -void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); } +void Menu_NonPfunc(void) {} +void MenuProc_AllInf_Default(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Para_Default(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Set_Default(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Resume_Soft(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_CellDef(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_ShowAnaType(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Dbg_XuYX(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Dbg_XuYC(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Dbg_XuEvent(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Dbg_Relay(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_VersionBoard(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_ClrRec(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_ComPara(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_EditIP(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_EditSntp(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_Cfg_CellConf(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecSOE(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecAct(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecOpt(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecAlm(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecStart(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecYK(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecChk(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecRun(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_See_RecFault(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_YC_SetSqValue(void) { MenuPage_TriggerCurrentAction(); } +void MenuProc_YC_SetAdjCoe(void) { MenuPage_TriggerCurrentAction(); } diff --git a/src/Drv/menu/app/menu.h b/src/Drv/menu/app/menu.h index 0706dfc..af56e97 100644 --- a/src/Drv/menu/app/menu.h +++ b/src/Drv/menu/app/menu.h @@ -10,14 +10,6 @@ * - 结构采用 MVP:Model(状态) / View(渲染) / Presenter(调度) */ -#include "../common/menu_item_types.h" - -void MenuApp_Init(void); -void MenuApp_HandleInput(uint8_t keyVal); -void MenuApp_PollInput(void); -void MenuApp_Render(void); -const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count); - /* 非功能键处理 */ void Menu_NonPfunc(); void MenuProc_AllInf_Default(); diff --git a/src/Drv/menu/common/menu_item_types.h b/src/Drv/menu/common/menu_item_types.h deleted file mode 100644 index c88adc8..0000000 --- a/src/Drv/menu/common/menu_item_types.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef MENU_ITEM_TYPES_H -#define MENU_ITEM_TYPES_H - -#include "types.h" - -typedef struct MenuItem MenuItem; -typedef MenuItem *tagPMenuItem; -typedef MenuItem tagMenuItem; - -typedef struct -{ - MenuItem *higher; - MenuItem *lower; - MenuItem *before; - MenuItem *behind; -} MenuLinks; - -typedef struct -{ - uint16_t wSPosX; - uint16_t wSPosY; - uint16_t wEPosX; - uint16_t wEPosY; -} MenuRect; - -struct MenuItem -{ - /* 导航关系 */ - MenuItem *ptHigher; - MenuItem *ptLower; - MenuItem *ptBefore; - MenuItem *ptBehind; - - /* 菜单元数据 */ - uint8_t byClass; - uint8_t byName[50]; - uint8_t byTip[50]; - uint8_t byAttrib; - uint16_t wPassword; - uint16_t wPara; - FUNCPTR pfnWinProc; - - /* 视图与布局 */ - uint16_t wPos; - uint16_t wNum; - uint16_t wSPosX; - uint16_t wSPosY; - uint16_t wEPosX; - uint16_t wEPosY; -}; - -#endif diff --git a/src/Drv/menu/common/menu_state_types.h b/src/Drv/menu/common/menu_state_types.h deleted file mode 100644 index 5544904..0000000 --- a/src/Drv/menu/common/menu_state_types.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef MENU_STATE_TYPES_H -#define MENU_STATE_TYPES_H - -#include "menu_item_types.h" - -typedef struct -{ - /* 当前菜单显示只依赖首帧刷新标记 */ - unsigned bFirst : 1; -} tagDspCtrl; - -typedef struct -{ - uint8_t by0LevelNum; - - tagPMenuItem ptHead; - tagPMenuItem ptCurrent; - tagPMenuItem ptRoute[4]; - tagPMenuItem ptCurBak; - tagPMenuItem pt0Level; -} tagMenuCtrl; - -#endif diff --git a/src/Drv/menu/model/display.c b/src/Drv/menu/model/display.c deleted file mode 100644 index 16e4ab1..0000000 --- a/src/Drv/menu/model/display.c +++ /dev/null @@ -1,147 +0,0 @@ -#include "../app/menu.h" -#include "display.h" - -//========================================== // -// 字符串代号常量定义 -enum _STR_FLAG_ { - EN_STR_FLAG_FUNSET = 0, // 投入,退出 - EN_STR_FLAG_OKCANCEL, - EN_VALUE_FLAG_SLOTTYPE, // 板件类型 - EN_VALUE_FLAG_YESNO, // 是否 - EN_VALUE_FLAG_CHN_NUM, // 中文数字 - EN_VALUE_FLAG_YXSET, // 遥信设置 - EN_VALUE_FLAG_YKSTEP, // 遥控步骤 - EN_VALUE_FLAG_YKACTION, // 遥控操作类型 - EN_VALUE_FLAG_CHK, // 自检信息 - EN_VALUE_FLAG_BEUSED, // 是否使用 - EN_VALUE_FLAG_PORTTYPE, // 端口类型 - EN_VALUE_FLAG_MAPNAME, // 转发表名称 - EN_VALUE_FLAG_RECTITLE, // 记录类型表 - EN_VALUE_FLAG_BHINFO, // 保护动作/复归 - EN_VALUE_FLAG_BHSET, // 保护动作/复归 - EN_VALUE_FLAG_OKCANCEL, // 确定/取消 - EN_VALUE_FLAG_FILETYPE, // 文件类型 - EN_VALUE_FLAG_ROLE, // 备份角色 - EN_VALUE_FLAG_COMMSTATE, // 通讯状态 - EN_VALUE_FLAG_ALARM, // 告警状态 - EN_VALUE_FLAG_YXTYPE, // 遥信类型 - EN_STR_FLAG_MENUALAUTO, - EN_STR_FLAG_LINKBREAK, - EN_STR_FLAG_DONEUNDONE, - EN_STR_FLAG_ACTBACK, - EN_STR_FLAG_RESUMEBREAK, - EN_STR_FLAG_USESTOP, - EN_STR_FLAG_BAUDRATE, - EN_STR_FLAG_VERIFY, - EN_STR_FLAG_PROTOL, - EN_STR_FLAG_RECORD, - EN_STR_FLAG_MESSAGE, - EN_STR_FLAG_COM, - EN_STR_FLAG_MODE, - EN_STR_FLAG_COMBAUD, - EN_STR_FLAG_DATABIT, - EN_STR_FLAG_STOPBIT, - EN_STR_FLAG_COMTYPE, - EN_STR_FLAG_SAMEV, - EN_STR_FLAG_PT, - EN_STR_FLAG_CT, - EN_STR_FLAG_TRANSTYPE, - EN_STR_FLAG_LINEPHS, - EN_STR_FLAG_RETOUT, - EN_STR_FLAG_YXDI, - EN_STR_FLAG_YKPUL, - EN_STR_FLAG_WAVE, - EN_STR_FLAG_MEMTYPE, - EN_STR_FLAG_CHANNEL, - EN_STR_FLAG_POWERTYPE, - EN_STR_FLAG_ZEROTYPE, - EN_STR_FLAG_YCCOMMTYPE, - EN_STR_FLAG_YXCOMMTYPE, - EN_STR_FLAG_CTRLWORD, - EN_STR_FLAG_BUSNAME, - EN_STR_FLAG_UNITSHOWMODE, - EN_STR_FLAG_BAYMODE, - EN_STR_FLAG_BASEANA, - EN_STR_FLAG_BASEPHASE, - EN_STR_FLAG_BAYCODE, - EN_STR_FLAG_SET_NUM, - EN_STR_FLAG_DCTYPE, - EN_STR_FLAG_DCINTYPE, - EN_STR_FLAG_DWINTYPE, - EN_STR_FLAG_PTCT, - EN_STR_FLAG_HARMOTYPE, - EN_STR_FLAG_YXSIN_DI, //设置双点遥信 - EN_STR_FLAG_SHOWANATYPE, - EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性 - EN_STR_FLAG_WIRDIAGRAMTYPE // 增加主接线图类型识别.modified by zhanggl 111010 - -} enumStrType; - -/* 静态菜单表 -注意:该表定义时顺序不能乱,需要从 0 级开始,一级一经按顺序写入 -*/ -const tagMenuModel g_tMenuModelTab[] = -{ - { 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, - { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, - { 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, - { 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input }, - { 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value }, - { 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, - { 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, - { 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft }, - { 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set }, - { 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, - { 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, - { 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft }, - { 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue }, - { 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe }, - { 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType }, - { 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth }, - { 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin }, - { 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time }, - { 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent }, - { 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave }, - { 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec }, - { 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara }, - { 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP }, - { 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp }, - { 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE }, - { 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct }, - { 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt }, - { 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm }, - { 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart }, - { 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK }, - { 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk }, - { 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun }, - { 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault }, - { 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, - { 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf }, - { 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default }, - { 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default }, - { 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default }, - { 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft }, - { 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef }, - { 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType }, - { 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, - { 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX }, - { 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, - { 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, - { 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, - { 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, - { 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, - { 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay }, - { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, - { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, -}; -const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel); diff --git a/src/Drv/menu/model/display.h b/src/Drv/menu/model/display.h deleted file mode 100644 index 8e6f847..0000000 --- a/src/Drv/menu/model/display.h +++ /dev/null @@ -1,180 +0,0 @@ -#ifndef __DISPLAY__H__ -#define __DISPLAY__H__ - -#include "types.h" - - -/* 静态菜定义需要的属性 */ -typedef struct -{ - uint8_t byClass; // 菜单分级标志; - uint8_t byName[50]; // 菜单字符串; - uint8_t byTip[50]; // 菜单提示文本; - uint8_t byAttrib; // 菜单属性,设置菜单特殊显示效果; - uint16_t wPassword; // 访问密码,0x0000表示没有密码; - uint16_t wPara; // 菜单执行函数参数; - FUNCPTR pfnWinProc; // 界面执行函数指针; -}tagMenuModel,*tagPMenuModel; - -extern const tagMenuModel g_tMenuModelTab[]; -extern const uint32_t MENU_MAX_ITEM; - - - - -//菜单密码设置 -//============================================================================== - -#define CN_COP_PWD (321) // 厂家密码:可修改保护元件配置、查看内存等 -#define CN_USER_PWD (700) // 用户超级密码:可用于修改普通密码、定值,预设等 -#define CN_SUPER_PWD (620) - - -enum _REV_TYPE_ // 复归类型 -{ - EN_REV_FREE, // 非强制复归 - EN_REV_FORCE // 强制复归 -}; - -enum _SET_SIDE_TYPE_ // 定值类别 -{ - EN_SIDE_START = 0, // 备用侧 - EN_SIDE_BASIC, // 基本信息 - EN_SIDE_DEVINF, // 装置参数 - EN_SIDE_COP, // 内部定值 - EN_SIDE_MATRIX, // 内部定值 出口矩阵 - EN_SIDE_ALL, // 全侧 - EN_SIDE_HIGH, // 高压侧 - EN_SIDE_MED1, // 中压侧 - EN_SIDE_MED2, // 中压侧 - EN_SIDE_LOW1, // 低压1侧 - EN_SIDE_LOW2, // 低压2侧 - EN_SIDE_LK, // 电抗器 - EN_SIDE_Z, // Z变 - EN_SIDE_DEF, // 自定义 - EN_SIDE_NONE = 0xFF // 不存在 -}; - -// ============================================================================ -// Const_Soft.h 常量固定部分 -// ============================================================================ -// 软压板类型 -enum _SOFT_TYPE_NUMBER -{ - EN_SOFT_PRO = 0, // 保护功能软压板 - EN_SOFT_GOOSE, // GOOSE软压板 - EN_SOFT_MU, // MU软压板 - EN_SOFT_SWITCH, // 刀闸强制软压板 - EN_SOFT_BAK, // 备用软压板 -//----------------------------------------------// - EN_SOFT_TYPE_END // 软压板类型总数目,不可改动 -}; - -//============================================================================ -//实时数据类型标志 -//============================================================================ -enum _MEA_TYPE_{ - //保护交流量 - EN_MEA_RLY, // 保护交流量1 - EN_MEA_RLY2, // 保护交流量2 - EN_MEA_RLY3, // 保护交流量3 - EN_MEA_ANA, // 保护测量量1 - EN_MEA_ANA2, // 保护测量量2 - EN_MEA_ANA3, // 保护测量量3 - //遥测 - EN_MEA_AC, // 遥测交流量 - EN_MEA_DC, // 保护直流量 - EN_MEA_SYN, // 保护谐波量 - EN_MEA_POWER, // 保护电能量 - EN_MEA_DD, // 电度量 - EN_MEA_JLYC, - EN_MEA_GEAR, // 保护档位量 - EN_MEA_TQ, // 保护同期量 - EN_MEA_INPUT1, // 保护交流量1 - EN_MEA_INPUT2, // 保护交流量2 - EN_MEA_INPUT3, // 保护交流量3 - EN_INPUT_RLY_ALL, //开入加遥信 - EN_INPUT_RLY_FAULT, - EN_INPUT_RLY_OTHER, - EN_INPUT_BS_ALL, - EN_INPUT_BS_FAULT, - EN_INPUT_BS_OTHER, - - EN_MEA_ADJ, // 遥测微调系数 - EN_MEA_YX, // 遥信状态 - EN_OUTPUT_TRIP, // 保护出口调试 - EN_OUTPUT_SIGN, // 信号出口调试 - EN_MEA_LS, // 联锁信号.zhanggl - EN_MEA_SCRLY // 顺控配置信号.zhanggl -}; - - -enum _INDEX_VALUE_TYPE_{ - - EN_SOFT_SET, - EN_FIGURE_SET -}; - -//============================================================================== -// 菜单常量表相关常量 - -//============================================================================ -//交流量菜单对应wParameter执行菜单参数(应用于菜单常量表) -//============================================================================ -enum _ANA_PARA_ { - EN_ANA_0 = 1, - EN_ANA_1 -}; - -//============================================================================ -//开入量菜单对应wParameter执行菜单参数(应用于菜单常量表) -//============================================================================ -enum _INPUT_PARA_ { - EN_INPUT_0 = 1, - EN_INPUT_1 -}; - -enum _ANA_TYPE_ -{ - EN_TYPE_DIF_CURRENT=0 , - EN_TYPE_UNIT_CURRENT , - EN_TYPE_UNIT_VOLTAGE - -}; - -enum _NO_USER_PASSWORD_{ - EN_NO_USER_PWD = 0x55 -}; -enum _FACTORY_PASSWORD_{ - EN_FACTORY_PASSWORD = 0x55 -}; - -// 记录类型 -enum _REC_TYPE_ -{ - EN_ACT_REC = 0, // 事件记录 - EN_ALM_REC, // 告警记录 - EN_CHK_REC, // 自检记录 - EN_SOE_REC, // SOE记录 - EN_COS_REC, // COS记录 - EN_LOCK_REC, // 瞬时闭锁记录 - EN_OVER_REC, // 越限记录 - EN_START_REC, // 启动记录 - EN_RUN_REC, // 运行记录 - EN_INPUT_REC, // 开入变位记录 - EN_ONOFF_REC, // 保护投退记录 - EN_OPT_REC, // 保护操作记录 - EN_YK_REC, // 保护遥控记录 - EN_SC_REC, // 装置顺控记录 - EN_SCSTEPINFO_REC, // 装置顺控单步记录 - EN_FAULT_REC, // 事故报告记录 - EN_ACTWAVE_REC, // 动作录波信息记录 - EN_STARTWAVE_REC, // 启动录波信息记录 - EN_HANDWAVE_REC, // 手动录波信息记录 - EN_FAULT_NO, // 故障序号 - EN_ALL_REC = 0xFF, // 所有记录 - EN_NO_REC = 0xFFFF // 无效记录 -}; - - -#endif diff --git a/src/Drv/menu/model/menu_model.c b/src/Drv/menu/model/menu_model.c deleted file mode 100644 index 3c07b2d..0000000 --- a/src/Drv/menu/model/menu_model.c +++ /dev/null @@ -1,52 +0,0 @@ -#include "menu_model.h" - -#include - -#include "menu_tree_builder.h" - -/* 初始化阶段一次性修饰菜单名称:对有子项的节点追加 '\x10' 指示符。 */ -static void MenuModel_DecorateDisplayNames(tagMenuItem *items, uint32_t maxItem) -{ - for (uint32_t i = 0; i < maxItem; i++) - { - uint16_t nameCapacity = (uint16_t)sizeof(items[i].byName); - uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1); - uint16_t len = 0; - if (items[i].ptLower == NULL) - { - continue; - } - while ((len < maxTextLenWithoutNull) && (items[i].byName[len] != '\0')) - { - len++; - } - if ((len == 0) || (items[i].byName[len - 1] != '\x10')) - { - if (len < maxTextLenWithoutNull) - { - items[i].byName[len] = '\x10'; - items[i].byName[len + 1] = '\0'; - } - } - } -} - -void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl, - tagMenuItem *menuItems, - const tagMenuModel *modelTab, - uint32_t modelCount) -{ - MenuTree_0LevelNumCal(menuCtrl, modelTab, modelCount); - MenuTree_MainCreate(menuItems, modelTab, modelCount); - MenuModel_DecorateDisplayNames(menuItems, modelCount); - - menuCtrl->ptHead = &menuItems[0]; - - menuCtrl->ptRoute[0] = &menuItems[0]; - menuCtrl->pt0Level = menuCtrl->ptRoute[0]; - menuCtrl->ptCurrent = menuCtrl->ptHead->ptLower; - menuCtrl->ptCurBak = menuCtrl->ptCurrent; - menuCtrl->ptRoute[1] = menuCtrl->ptCurrent; - menuCtrl->ptRoute[2] = menuCtrl->ptCurrent; - menuCtrl->ptRoute[3] = menuCtrl->ptCurrent; -} diff --git a/src/Drv/menu/model/menu_model.h b/src/Drv/menu/model/menu_model.h deleted file mode 100644 index be67a22..0000000 --- a/src/Drv/menu/model/menu_model.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef MENU_MODEL_H -#define MENU_MODEL_H - -#include "display.h" -#include "../common/menu_state_types.h" - -void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl, - tagMenuItem *menuItems, - const tagMenuModel *modelTab, - uint32_t modelCount); - -#endif diff --git a/src/Drv/menu/model/menu_tree_builder.c b/src/Drv/menu/model/menu_tree_builder.c deleted file mode 100644 index f551b62..0000000 --- a/src/Drv/menu/model/menu_tree_builder.c +++ /dev/null @@ -1,119 +0,0 @@ -#include - -#include "menu_tree_builder.h" - -void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem) -{ - uint8_t by0LevelNum = 0; - uint32_t wLoop; - - for (wLoop = 0; wLoop < maxItem; wLoop++) - { - if (modelTab[wLoop].byClass == 0) - { - by0LevelNum++; - } - } - menuCtrl->by0LevelNum = by0LevelNum; -} - -void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem) -{ - tagPMenuItem ptFirst[4]; - tagPMenuItem ptLast[4]; - tagPMenuItem ptCurrent; - tagPMenuItem ptNextNode; - uint32_t wLoop1; - uint8_t byCurClass; - uint8_t byNextClass; - uint8_t wLoop2; - - ptFirst[0] = &menuItems[0]; - ptLast[0] = ptFirst[0]; - - ptCurrent = &menuItems[0]; - ptCurrent->wPos = 0; - ptCurrent->wNum = 0; - ptCurrent->wSPosX = 0; - ptCurrent->wSPosY = 0; - ptCurrent->wEPosX = 0; - ptCurrent->wEPosY = 0; - - ptCurrent->ptHigher = NULL; - ptCurrent->ptLower = NULL; - ptCurrent->ptBefore = NULL; - ptCurrent->ptBehind = NULL; - - ptCurrent->byClass = modelTab[0].byClass; - ptCurrent->byAttrib = modelTab[0].byAttrib; - ptCurrent->wPassword = modelTab[0].wPassword; - ptCurrent->wPara = modelTab[0].wPara; - ptCurrent->pfnWinProc = modelTab[0].pfnWinProc; - memcpy(ptCurrent->byName, modelTab[0].byName, 50); - memcpy(ptCurrent->byTip, modelTab[0].byTip, 50); - - for (wLoop1 = 1; wLoop1 < maxItem; wLoop1++) - { - ptNextNode = &menuItems[wLoop1]; - - ptNextNode->wPos = 0; - ptNextNode->wNum = 0; - ptNextNode->wSPosX = 0; - ptNextNode->wSPosY = 0; - ptNextNode->wEPosX = 0; - ptNextNode->wEPosY = 0; - - ptNextNode->ptLower = NULL; - ptNextNode->ptBehind = NULL; - ptNextNode->ptBefore = NULL; - ptNextNode->ptHigher = NULL; - - ptNextNode->byClass = modelTab[wLoop1].byClass; - ptNextNode->byAttrib = modelTab[wLoop1].byAttrib; - ptNextNode->wPassword = modelTab[wLoop1].wPassword; - ptNextNode->wPara = modelTab[wLoop1].wPara; - ptNextNode->pfnWinProc = modelTab[wLoop1].pfnWinProc; - memcpy(ptNextNode->byName, modelTab[wLoop1].byName, 50); - memcpy(ptNextNode->byTip, modelTab[wLoop1].byTip, 50); - - byCurClass = ptCurrent->byClass; - byNextClass = ptNextNode->byClass; - - if (byCurClass < byNextClass) - { - ptCurrent->ptLower = ptNextNode; - ptNextNode->ptHigher = ptCurrent; - ptFirst[byNextClass] = ptNextNode; - ptLast[byNextClass] = ptNextNode; - } - else if (byCurClass == byNextClass) - { - ptNextNode->ptBefore = ptCurrent; - ptNextNode->ptHigher = ptCurrent->ptHigher; - ptCurrent->ptBehind = ptNextNode; - ptLast[byNextClass] = ptNextNode; - } - else if (byCurClass > byNextClass) - { - ptNextNode->ptBefore = ptLast[byNextClass]; - ptNextNode->ptHigher = ptFirst[byNextClass]->ptHigher; - ptLast[byNextClass]->ptBehind = ptNextNode; - ptLast[byNextClass] = ptNextNode; - - for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--) - { - ptLast[wLoop2]->ptBehind = ptFirst[wLoop2]; - ptFirst[wLoop2]->ptBefore = ptLast[wLoop2]; - } - } - - ptCurrent = ptNextNode; - } - - byCurClass = ptCurrent->byClass; - for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++) - { - ptLast[wLoop1]->ptBehind = ptFirst[wLoop1]; - ptFirst[wLoop1]->ptBefore = ptLast[wLoop1]; - } -} diff --git a/src/Drv/menu/model/menu_tree_builder.h b/src/Drv/menu/model/menu_tree_builder.h deleted file mode 100644 index 66c2400..0000000 --- a/src/Drv/menu/model/menu_tree_builder.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef MENU_TREE_BUILDER_H -#define MENU_TREE_BUILDER_H - -#include "display.h" -#include "../common/menu_state_types.h" - -void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem); -void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem); - -#endif diff --git a/src/Drv/menu/presenter/menu_nav_types.h b/src/Drv/menu/presenter/menu_nav_types.h deleted file mode 100644 index 810d3a3..0000000 --- a/src/Drv/menu/presenter/menu_nav_types.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MENU_NAV_TYPES_H -#define MENU_NAV_TYPES_H - -#include "../common/menu_item_types.h" - -typedef struct -{ - tagPMenuItem ptHead; - tagPMenuItem ptCurrent; - tagPMenuItem ptRoute[4]; - tagPMenuItem ptCurBak; - tagPMenuItem pt0Level; -} MenuNavState; - -#endif diff --git a/src/Drv/menu/presenter/menu_navigator.c b/src/Drv/menu/presenter/menu_navigator.c deleted file mode 100644 index 9d7d60d..0000000 --- a/src/Drv/menu/presenter/menu_navigator.c +++ /dev/null @@ -1,111 +0,0 @@ -#include - -#include "menu_navigator.h" - -#include "../../key.h" - -MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal) -{ - MenuNavResult result; - tagPMenuItem ptCurrent; - tagPMenuItem ptHead; - tagPMenuItem *ptRoute; - - result.needRefresh = 0; - result.skipRenderThisRound = 0; - - ptCurrent = navState->ptCurrent; - ptHead = navState->ptHead; - ptRoute = navState->ptRoute; - - switch (keyVal) - { - case KEY_F1: - case KEY_F2: - break; - case KEY_U: - ptCurrent = ptCurrent->ptBefore; - result.needRefresh = 1; - break; - case KEY_D: - ptCurrent = ptCurrent->ptBehind; - result.needRefresh = 1; - break; - case KEY_L: - ptCurrent = ptRoute[ptCurrent->byClass - 1]; - if (ptCurrent->byClass == 0) - { - if (ptCurrent->wPos == 1) - { - ptCurrent = ptCurrent->ptBefore->ptBefore; - } - else - { - ptCurrent = ptCurrent->ptBefore; - } - ptCurrent = ptCurrent->ptLower; - } - result.needRefresh = 1; - break; - case KEY_R: - case KEY_ENT: - if (ptCurrent->ptLower != NULL) - { - ptCurrent = ptCurrent->ptLower; - result.needRefresh = 1; - } - else if (ptCurrent->pfnWinProc != NULL) - { - ptCurrent->pfnWinProc(); - } - break; - case KEY_ESC: - if (ptCurrent->byClass == 1) - { - navState->pt0Level = ptHead; - navState->ptRoute[0] = ptHead; - navState->ptCurrent = ptHead->ptLower; - navState->ptCurBak = navState->ptCurrent; - navState->ptRoute[1] = navState->ptCurrent; - result.skipRenderThisRound = 1; - return result; - } - ptCurrent = ptRoute[ptCurrent->byClass - 1]; - result.needRefresh = 1; - break; - default: - break; - } - - navState->ptCurrent = ptCurrent; - return result; -} - -void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem) -{ - tagPMenuItem ptIndex; - - if (navState->ptCurBak == navState->ptCurrent) - { - return; - } - - ptIndex = navState->ptCurrent; - for (uint32_t index = 0; index < maxItem; index++) - { - if (ptIndex->ptHigher == NULL) - { - ptIndex = ptIndex->ptBefore; - } - else - { - ptIndex = ptIndex->ptHigher; - navState->ptRoute[ptIndex->byClass] = ptIndex; - } - if (ptIndex->byClass == 0) - { - break; - } - } - navState->ptRoute[navState->ptCurrent->byClass] = navState->ptCurrent; -} diff --git a/src/Drv/menu/presenter/menu_navigator.h b/src/Drv/menu/presenter/menu_navigator.h deleted file mode 100644 index 09e8ce3..0000000 --- a/src/Drv/menu/presenter/menu_navigator.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MENU_NAVIGATOR_H -#define MENU_NAVIGATOR_H - -#include "menu_nav_types.h" - -typedef struct -{ - uint8_t needRefresh; - uint8_t skipRenderThisRound; -} MenuNavResult; - -MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal); -void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem); - -#endif diff --git a/src/Drv/menu/presenter/menu_presenter.c b/src/Drv/menu/presenter/menu_presenter.c deleted file mode 100644 index fa8d155..0000000 --- a/src/Drv/menu/presenter/menu_presenter.c +++ /dev/null @@ -1,176 +0,0 @@ -#include "menu_presenter.h" - -#include "../model/menu_model.h" -#include "menu_navigator.h" - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_FillNavState - * 作用: 将“运行时控制结构 tagMenuCtrl”中的导航相关字段复制到 - * “导航临时结构 MenuNavState”。 - * - * 设计原因: - * - MenuNavigator_* 仅关心导航字段,不应直接操作完整控制结构; - * - 通过拷贝到轻量结构,可在 Presenter 内控制读写边界。 - * ------------------------------------------------------------------------- */ -static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav) -{ - /* 根节点指针:用于 ESC 回到顶层时复位。 */ - nav->ptHead = ctrl->ptHead; - /* 当前焦点节点:导航计算的主要输入。 */ - nav->ptCurrent = ctrl->ptCurrent; - /* 路径第 0 级:顶层菜单路径缓存。 */ - nav->ptRoute[0] = ctrl->ptRoute[0]; - /* 路径第 1 级:一级菜单路径缓存。 */ - nav->ptRoute[1] = ctrl->ptRoute[1]; - /* 路径第 2 级:二级菜单路径缓存。 */ - nav->ptRoute[2] = ctrl->ptRoute[2]; - /* 路径第 3 级:三级菜单路径缓存。 */ - nav->ptRoute[3] = ctrl->ptRoute[3]; - /* 上一次焦点备份:用于判断是否发生焦点迁移。 */ - nav->ptCurBak = ctrl->ptCurBak; - /* 当前可见的 0 级入口:用于判断是否需要全屏刷新。 */ - nav->pt0Level = ctrl->pt0Level; -} - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_ApplyNavState - * 作用: 将导航临时结构 MenuNavState 的结果回写到 tagMenuCtrl。 - * - * 说明: - * - FillNavState + Navigator + ApplyNavState 构成一次输入处理闭环; - * - 这样 Presenter 明确“读输入状态 -> 计算 -> 写回状态”三个阶段。 - * ------------------------------------------------------------------------- */ -static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav) -{ - /* 回写根节点(通常不变,但保持对称拷贝便于维护)。 */ - ctrl->ptHead = nav->ptHead; - /* 回写当前焦点。 */ - ctrl->ptCurrent = nav->ptCurrent; - /* 回写 0 级路径缓存。 */ - ctrl->ptRoute[0] = nav->ptRoute[0]; - /* 回写 1 级路径缓存。 */ - ctrl->ptRoute[1] = nav->ptRoute[1]; - /* 回写 2 级路径缓存。 */ - ctrl->ptRoute[2] = nav->ptRoute[2]; - /* 回写 3 级路径缓存。 */ - ctrl->ptRoute[3] = nav->ptRoute[3]; - /* 回写焦点备份。 */ - ctrl->ptCurBak = nav->ptCurBak; - /* 回写 0 级入口指针。 */ - ctrl->pt0Level = nav->pt0Level; -} - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_Setup - * 作用: 完成 Presenter 的依赖注入和 View 绑定。 - * - * 输入: - * - dspCtrl/menuCtrl/menuItems: 运行态状态与数据池 - * - renderPort: 渲染端口抽象(LCD/模拟器实现) - * - layoutConfig: 布局参数 - * ------------------------------------------------------------------------- */ -void MenuPresenter_Setup(MenuPresenter *presenter, - tagDspCtrl *dspCtrl, - tagMenuCtrl *menuCtrl, - tagMenuItem *menuItems, - const MenuRenderPort *renderPort, - const MenuLayoutConfig *layoutConfig) -{ - /* 保存显示控制状态句柄。 */ - presenter->dspCtrl = dspCtrl; - /* 保存菜单控制状态句柄。 */ - presenter->menuCtrl = menuCtrl; - /* 保存菜单节点池句柄。 */ - presenter->menuItems = menuItems; - /* 保存布局配置句柄。 */ - presenter->layoutConfig = layoutConfig; - /* 初始化 View,并注入具体渲染端口。 */ - MenuView_Init(&presenter->view, renderPort); -} - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_InitModel - * 作用: 启动时初始化模型与布局。 - * - * 步骤: - * 1) 设置首帧标志,确保首次进入能触发完整绘制; - * 2) 通过 Model 完成菜单树构建与初始路由; - * 3) 通知 View 计算每个节点的布局坐标。 - * ------------------------------------------------------------------------- */ -void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount) -{ - /* 标记“首帧未绘制”,后续输入循环会先触发一次刷新。 */ - presenter->dspCtrl->bFirst = 1; - /* 构建运行时菜单树并初始化控制状态。 */ - MenuModel_Bootstrap(presenter->menuCtrl, - presenter->menuItems, - modelTab, - modelCount); - /* 根据模型结果计算显示矩形位置。 */ - MenuView_Layout(&presenter->view, presenter->menuCtrl, presenter->layoutConfig); -} - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_Refresh - * 作用: 统一刷新入口,把“当前状态”交给 View 决定具体渲染策略。 - * ------------------------------------------------------------------------- */ -void MenuPresenter_Refresh(MenuPresenter *presenter) -{ - /* View 根据 bFirst / ptCurBak / route 差异决定增量或全量刷新。 */ - MenuView_RenderByState(&presenter->view, presenter->menuCtrl, presenter->dspCtrl); -} - -/* ------------------------------------------------------------------------- - * 函数: MenuPresenter_HandleInput - * 作用: 处理一次输入事件(按键),驱动导航状态迁移与视图刷新。 - * - * 核心流程: - * 1) 首帧短路:先渲染再返回; - * 2) 拷贝导航状态 -> 调用导航器 -> 回写结果; - * 3) 按导航结果决定是否跳过渲染、是否重建路径并刷新。 - * ------------------------------------------------------------------------- */ -void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal) -{ - /* 本轮输入使用的导航状态快照。 */ - MenuNavState navState; - /* 导航器输出:是否需要刷新/是否跳过本轮渲染。 */ - MenuNavResult navResult; - - /* 若仍是首帧阶段:优先做一次渲染,不消费导航逻辑。 */ - if (presenter->dspCtrl->bFirst) - { - /* 执行首帧刷新。 */ - MenuPresenter_Refresh(presenter); - /* 首帧路径到此结束。 */ - return; - } - - /* 从控制结构提取导航子集,作为导航器输入。 */ - MenuPresenter_FillNavState(presenter->menuCtrl, &navState); - /* 基于按键执行导航状态机,得到状态变化与渲染建议。 */ - navResult = MenuNavigator_ProcessKey(&navState, keyVal); - /* 将导航器结果回写到运行时控制结构。 */ - MenuPresenter_ApplyNavState(presenter->menuCtrl, &navState); - - /* 某些按键(如特定 ESC 场景)要求本轮跳过渲染。 */ - if (navResult.skipRenderThisRound) - { - /* 按导航器要求直接返回。 */ - return; - } - - /* 只有导航器明确要求刷新时,才进行路径重建与渲染。 */ - if (navResult.needRefresh) - { - /* 用于“自底向上”重建 route[] 的临时状态。 */ - MenuNavState rebuildState; - /* 以最新控制状态为基准生成重建输入。 */ - MenuPresenter_FillNavState(presenter->menuCtrl, &rebuildState); - /* 重建各级 route 缓存,保证后续渲染路径一致。 */ - MenuNavigator_RebuildRoute(&rebuildState, MENU_MAX_ITEM); - /* 回写重建后的 route 与相关导航状态。 */ - MenuPresenter_ApplyNavState(presenter->menuCtrl, &rebuildState); - /* 触发刷新,让界面体现本次输入结果。 */ - MenuPresenter_Refresh(presenter); - } -} diff --git a/src/Drv/menu/presenter/menu_presenter.h b/src/Drv/menu/presenter/menu_presenter.h deleted file mode 100644 index a8e1616..0000000 --- a/src/Drv/menu/presenter/menu_presenter.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef MENU_PRESENTER_H -#define MENU_PRESENTER_H - -#include "../model/display.h" -#include "../view/menu_layout.h" -#include "../view/menu_render_port.h" -#include "../common/menu_state_types.h" -#include "../view/menu_view.h" - -typedef struct -{ - tagDspCtrl *dspCtrl; - tagMenuCtrl *menuCtrl; - tagMenuItem *menuItems; - MenuView view; - const MenuLayoutConfig *layoutConfig; -} MenuPresenter; - -void MenuPresenter_Setup(MenuPresenter *presenter, - tagDspCtrl *dspCtrl, - tagMenuCtrl *menuCtrl, - tagMenuItem *menuItems, - const MenuRenderPort *renderPort, - const MenuLayoutConfig *layoutConfig); - -void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount); -void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal); -void MenuPresenter_Refresh(MenuPresenter *presenter); - -#endif diff --git a/src/Drv/menu/view/menu_layout.c b/src/Drv/menu/view/menu_layout.c deleted file mode 100644 index 941da7b..0000000 --- a/src/Drv/menu/view/menu_layout.c +++ /dev/null @@ -1,447 +0,0 @@ -#include -#include "menu_layout.h" -#include "utf8.h" - -/****************************************************************************** - * 模块: menu_layout.c - * 职责: 根据菜单树结构计算各级菜单在 LCD 上的显示矩形区域 - * - * 设计要点: - * 1) 采用“先算当前层,再递归/级联算下一层”的方式布点; - * 2) 每级菜单都维护: - * - ptFirst[level]: 当前层循环链表的起点; - * - ptIndex[level]: 当前层遍历游标; - * - byMenuNum[level]: 当前层项数量(或当前位置计数); - * 3) 对 Y 方向越界提供多级回退策略(向上翻转/贴顶重排),尽量保证菜单可见。 - *****************************************************************************/ - -/* 读取菜单节点导航指针集合(本文件局部辅助,避免 core 泄漏行为函数)。 */ -static MenuLinks MenuLayout_GetLinks(const MenuItem *item) -{ - MenuLinks links; - links.higher = item->ptHigher; - links.lower = item->ptLower; - links.before = item->ptBefore; - links.behind = item->ptBehind; - return links; -} - -/* 读取菜单节点矩形(本文件局部辅助)。 */ -static MenuRect MenuLayout_GetRect(const MenuItem *item) -{ - MenuRect rect; - rect.wSPosX = item->wSPosX; - rect.wSPosY = item->wSPosY; - rect.wEPosX = item->wEPosX; - rect.wEPosY = item->wEPosY; - return rect; -} - -/* 给菜单项写入矩形坐标的轻量封装,避免到处重复构造 MenuRect。 */ -static void MenuLayout_SetRect(MenuItem *item, uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey) -{ - item->wSPosX = sx; - item->wSPosY = sy; - item->wEPosX = ex; - item->wEPosY = ey; -} - -/* 将理论挂接行压缩映射到可视区,避免超出可显示行数。 */ -static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint8_t itemNum, uint8_t maxNum) -{ - if ((itemNum > maxNum) && (menuPos >= maxNum)) - { - return (uint16_t)(maxNum - (itemNum - menuPos)); - } - return menuPos; -} - -/* 子菜单 X 方向统一规则:从父框右边展开。 */ -static void MenuLayout_CalcSubMenuX(const MenuRect *parentRect, - uint8_t maxLen, - const MenuLayoutConfig *config, - uint16_t *startX, - uint16_t *endX) -{ - *startX = parentRect->wEPosX; - *endX = (uint16_t)(*startX + maxLen * config->menuWidth + config->menuXAdd); -} - -/* 尝试将菜单框整体翻转到上方显示;成功返回 1,失败返回 0。 */ -static uint8_t MenuLayout_TryFlipUp(uint16_t *startY, - uint16_t *endY, - uint8_t itemNum, - uint16_t flipMinY, - const MenuLayoutConfig *config) -{ - uint16_t candidateEndY; - uint16_t temp; - - candidateEndY = (uint16_t)(*startY - (itemNum - 1) * config->lineHeight - config->menuYAdd); - if ((candidateEndY > flipMinY) && (candidateEndY < config->menuYMax)) - { - temp = *startY; - *startY = candidateEndY; - *endY = (uint16_t)(temp + config->lineHeight); - return 1; - } - return 0; -} - -/* Sub2 兜底:回退到顶层起始 Y,并在底部做截断。 */ -static void MenuLayout_FallbackClampToTop(const tagPMenuItem *ptIndex, - uint8_t itemNum, - uint16_t *startY, - uint16_t *endY, - const MenuLayoutConfig *config) -{ - *startY = ptIndex[0]->wSPosY; - *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); - if (*endY > config->menuYMax) - { - *endY = config->menuYMax; - } -} - -/* Sub1 兜底:从顶层起始 Y 逐行上探,直到菜单框不越界。 */ -static void MenuLayout_FallbackProbeUp(const tagPMenuItem *ptIndex, - uint8_t itemNum, - uint16_t *startY, - uint16_t *endY, - const MenuLayoutConfig *config) -{ - *startY = ptIndex[0]->wSPosY; - *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); - for (uint16_t i = 1; i < (uint16_t)(ptIndex[0]->wSPosY / config->lineHeight + 1); i++) - { - if (*endY > config->menuYMax) - { - *startY = (uint16_t)(ptIndex[0]->wSPosY - config->lineHeight * i); - if (*startY < config->lineHeight) - { - *startY = 0; - } - *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); - } - else - { - break; - } - } -} - -/* 计算单个菜单项“显示宽度”: - * - 中文等多字节字符按 2 列计宽,ASCII 按 1 列; - * - 若该项有下级菜单且名称末尾未带特殊标记 '\x10',额外预留 1 列用于层级指示。 */ -static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item) -{ - uint8_t displayLen; - displayLen = MenuLayout_Utf8LenCal((uint8_t *)item->byName); - if (item->ptLower != NULL) - { - uint8_t byteLen = 0; - while ((byteLen < 50) && (item->byName[byteLen] != '\0')) - { - byteLen++; - } - if ((byteLen == 0) || (item->byName[byteLen - 1] != '\x10')) - { - displayLen += 1; - } - } - return displayLen; -} - -/* UTF-8 显示长度统计: - * utf8_next() 每次返回一个 Unicode 字符占用的字节数 n, - * 约定 n>1 视为“宽字符”(占 2 列),否则占 1 列。 */ -uint8_t MenuLayout_Utf8LenCal(uint8_t *str) -{ - uint8_t strLen = 0; - uint32_t unicode; - uint8_t index = 0; - uint8_t n = 0; - - while (str[index] != '\0') - { - n = utf8_next(str + index, &unicode); - strLen += (n > 1) ? 2 : 1; - index += n; - } - return strLen; -} - -/****************************************************************************** - * 函数: MenuLayout_CharLenCal - * 作用: - * 1) 统计 bylevel 下一层(bylevel+1)菜单项总数; - * 2) 计算该层中最长显示宽度(用于决定菜单框宽度); - * 3) 为该层每个节点写 wPos(从 1 开始的位置序号); - * 4) 把“下一层项数”写回父节点 wNum。 - * - * 变量字典: - * - bylevel: 输入为父层级,函数内部会自增到“子层级”; - * - byMenuNum[level]: level 层累计项数(同时作为 wPos 的来源); - * - ptFirst[level]: level 层循环链表的起点节点; - * - ptIndex[level]: level 层当前遍历节点; - * - byMaxLen: 本层所有项显示宽度的最大值(单位: 字符列)。 - *****************************************************************************/ -uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) -{ - uint8_t displayLen; - uint8_t byMaxLen = 0; - MenuLinks links; - - /* 进入下一层,从父节点的 lower 作为该层环形链表起点。 */ - ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower; - bylevel = bylevel + 1; - ptIndex[bylevel] = ptFirst[bylevel]; - - byMenuNum[bylevel] = 1; - ptIndex[bylevel]->wPos = 1; - - /* 固定上限 300 作为保护,避免链表异常导致死循环。 */ - for (uint16_t wLoop = 0; wLoop < 300; wLoop++) - { - links = MenuLayout_GetLinks(ptIndex[bylevel]); - displayLen = MenuLayout_ItemDisplayLen(ptIndex[bylevel]); - - if (byMaxLen < displayLen) - { - byMaxLen = displayLen; - } - - ptIndex[bylevel] = links.behind; - if (ptIndex[bylevel] == ptFirst[bylevel]) - { - break; - } - byMenuNum[bylevel]++; - ptIndex[bylevel]->wPos = byMenuNum[bylevel]; - } - - /* 父节点记录子项数量,供分页/显示逻辑复用。 */ - ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel]; - return byMaxLen; -} - -/****************************************************************************** - * 函数: MenuLayout_Sub2PosCal - * 作用: 计算二级子菜单(及其同层兄弟)的显示矩形 - * 策略: - * - 先按“父项对齐”得到理想 Y; - * - 若底部越界,尝试向上翻转; - * - 仍不满足则使用贴近顶层菜单的兜底摆放。 - * - * 变量字典: - * - byMaxNum: 当前配置下可完整显示的最大行数; - * - byMenuPos: 当前菜单相对父菜单的挂接行(理论位置); - * - byItemNum: 当前层或下一层项数(代码中按阶段复用); - * - wSPosY/wEPosY: 菜单框起止 Y 坐标(含间距边界); - * - parentRect: 父菜单矩形,用于对子菜单做相对定位。 - * - * 关键公式: - * - X 起点 = parentRect.wEPosX(总是从父框右侧展开); - * - X 终点 = X 起点 + byMaxLen * menuWidth + menuXAdd; - * - Y 起点 = parentRect.wSPosY + (byMenuPos - 1) * lineHeight; - * - Y 终点 = Y 起点 + 子项数 * lineHeight + menuYAdd。 - *****************************************************************************/ -void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config) -{ - uint16_t wSPosY; - uint16_t wEPosY; - uint8_t byMaxLen; - uint8_t byMaxNum; - uint16_t byMenuPos; - uint8_t byItemNum; - uint8_t byMenuNum[4]; - MenuLinks links; - MenuRect parentRect; - - /* 当前可完整容纳的最大行数(预留边框/间距)。 */ - byMaxNum = (config->menuYMax - config->menuYMin - 6) / config->lineHeight; - ptIndex[bylevel] = ptFirst[bylevel]; - for (uint16_t wLoop = 0; wLoop < 300; wLoop++) - { - links = MenuLayout_GetLinks(ptIndex[bylevel]); - if (links.lower != NULL) - { - byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex); - parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]); - MenuLayout_CalcSubMenuX(&parentRect, - byMaxLen, - config, - &ptIndex[bylevel]->wSPosX, - &ptIndex[bylevel]->wEPosX); - - /* byMenuPos: 当前项在同层中的理论挂接位置(必要时压缩映射)。 */ - byMenuPos = ptIndex[bylevel]->wPos; - byItemNum = byMenuNum[bylevel]; - byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum); - - wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight; - byItemNum = byMenuNum[bylevel + 1]; - wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd; - - if (wEPosY < config->menuYMax) - { - /* 理想位置不越界,直接应用。 */ - MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); - } - else - { - /* 底部越界:先尝试“向上翻转”放置整个菜单框。 */ - if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->menuYMin, config)) - { - /* 仍不满足:回退到顶层菜单起始 Y,再做截断兜底。 */ - MenuLayout_FallbackClampToTop(ptIndex, byItemNum, &wSPosY, &wEPosY, config); - } - MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); - } - } - ptIndex[bylevel] = links.behind; - if (ptIndex[bylevel] == ptFirst[bylevel]) - { - break; - } - } -} - -/****************************************************************************** - * 函数: MenuLayout_Sub1PosCal - * 作用: 计算一级子菜单(通常是主菜单下拉)的位置,并联动计算二级子菜单 - * 说明: 与 Sub2 逻辑相近,但在兜底时会逐步向上平移,直到菜单框可容纳。 - * - * 变量字典: - * - byMaxNum: 屏幕当前可显示的最大菜单行数; - * - byMenuPos: 当前节点理论挂接行(可能被映射压缩); - * - byItemNum: 当前下拉框条目数; - * - wTemp: 用于“翻转布局”时交换起止 Y; - * - byMenuNum[4]: 分层统计数组,索引与菜单层级一一对应。 - * - * 与 Sub2 的核心差异: - * - Sub1 兜底策略增加“逐行上探”,尝试找到不越界的最靠下位置, - * 视觉上更适合主菜单下拉场景。 - *****************************************************************************/ -void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config) -{ - uint16_t wSPosY; - uint16_t wEPosY; - uint8_t byMenuNum[4]; - uint8_t byMaxLen; - uint8_t byMaxNum; - uint16_t byMenuPos; - uint8_t byItemNum; - MenuLinks links; - MenuRect parentRect; - - byMaxNum = config->menuYMax / config->lineHeight; - ptIndex[bylevel] = ptFirst[bylevel]; - for (uint16_t wLoop = 0; wLoop < 300; wLoop++) - { - links = MenuLayout_GetLinks(ptIndex[bylevel]); - if (links.lower != NULL) - { - byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex); - parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]); - MenuLayout_CalcSubMenuX(&parentRect, - byMaxLen, - config, - &ptIndex[bylevel]->wSPosX, - &ptIndex[bylevel]->wEPosX); - - byMenuPos = ptIndex[bylevel]->wPos; - byItemNum = byMenuNum[bylevel]; - byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum); - - wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight; - byItemNum = byMenuNum[bylevel + 1]; - wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd; - - if (wEPosY < config->menuYMax) - { - MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); - } - else - { - /* 底部越界:优先尝试一次整体翻转到上方。 */ - if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->lineHeight, config)) - { - /* 二次兜底:从顶层菜单起始 Y 往上逐行试探,直到不越界。 */ - MenuLayout_FallbackProbeUp(ptIndex, byItemNum, &wSPosY, &wEPosY, config); - } - MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); - } - /* 一级菜单定位完成后,继续计算二级菜单。 */ - MenuLayout_Sub2PosCal(2, ptFirst, ptIndex, config); - } - ptIndex[bylevel] = links.behind; - if (ptIndex[bylevel] == ptFirst[bylevel]) - { - break; - } - } -} - -/****************************************************************************** - * 函数: MenuLayout_PositionCal - * 作用: 入口函数,计算从 0 级到 2 级菜单的显示坐标 - * 流程: - * 1) 先给 0 级菜单按屏宽等分; - * 2) 每个 0 级项若有下级,则计算其下拉框; - * 3) 再级联计算 1/2 级子菜单。 - * - * 变量字典: - * - by0LevelNum: 顶层菜单数量; - * - byInterval: 顶层菜单等分后的列宽; - * - byMenuNum[0]: 当前顶层节点在同层中的序号(从 1 开始); - * - byMenuNum[1]: 顶层节点的直接子项数量(由 CharLenCal 写入); - * - ptFirst/ptIndex: 各层遍历上下文,在本函数中作为全流程共享状态。 - *****************************************************************************/ -void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config) -{ - tagPMenuItem ptFirst[4]; - tagPMenuItem ptIndex[4]; - uint8_t byMenuNum[4]; - uint8_t byMaxLen; - uint8_t byInterval; - MenuLinks links; - - /* 输入保护:头指针/配置为空,或 0 级菜单数量为 0 时直接返回。 */ - if ((ptMenuHead == NULL) || (config == NULL) || (by0LevelNum == 0)) - { - return; - } - - ptFirst[0] = ptMenuHead; - ptIndex[0] = ptFirst[0]; - byMenuNum[0] = 1; - ptIndex[0]->wPos = 1; - /* 顶层菜单按等分宽度布局。 */ - byInterval = config->lcdSizeX / by0LevelNum; - - for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++) - { - links = MenuLayout_GetLinks(ptIndex[0]); - if (links.lower != NULL) - { - /* 先统计该分支子项宽高需求,再写入 0 级菜单框。 */ - byMaxLen = MenuLayout_CharLenCal(0, byMenuNum, ptFirst, ptIndex); - MenuLayout_SetRect( - ptIndex[0], - (byMenuNum[0] - 1) * byInterval, - config->lcdSizeY - config->lineHeight - byMenuNum[1] * config->lineHeight - config->menuYAdd, - (byMenuNum[0] - 1) * byInterval + byMaxLen * config->menuWidth + config->menuXAdd, - config->lcdSizeY - config->lineHeight); - /* 0 级完成后继续展开 1/2 级。 */ - MenuLayout_Sub1PosCal(1, ptFirst, ptIndex, config); - } - ptIndex[0] = links.behind; - if (ptIndex[0] == ptFirst[0]) - { - break; - } - byMenuNum[0]++; - ptIndex[0]->wPos = byMenuNum[0]; - } -} diff --git a/src/Drv/menu/view/menu_layout.h b/src/Drv/menu/view/menu_layout.h deleted file mode 100644 index 9887687..0000000 --- a/src/Drv/menu/view/menu_layout.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef MENU_LAYOUT_H -#define MENU_LAYOUT_H - -#include "../common/menu_item_types.h" - -#define CN_HEIGHT 12 -#define CN_ROWSPACE 2 -#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) -#define MENU_XADD 4 -#define MENU_YADD 4 -#define MENU_WITDTH 7 - -typedef struct -{ - uint16_t lcdSizeX; - uint16_t lcdSizeY; - uint16_t menuYMin; - uint16_t menuYMax; - uint16_t lineHeight; - uint16_t menuWidth; - uint16_t menuXAdd; - uint16_t menuYAdd; -} MenuLayoutConfig; - -#define MENU_YMIN 0 -#define MENU_YMAX_FROM_LCD(ySize) ((uint16_t)((ySize) - 12U)) - -uint8_t MenuLayout_Utf8LenCal(uint8_t *str); -uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex); -void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config); -void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config); -void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config); - -#endif diff --git a/src/Drv/menu/view/menu_renderer_lcd.c b/src/Drv/menu/view/menu_renderer_lcd.c deleted file mode 100644 index fa58876..0000000 --- a/src/Drv/menu/view/menu_renderer_lcd.c +++ /dev/null @@ -1,45 +0,0 @@ -#include "menu_render_port.h" - -#include "lcd.h" -#include "lcd_draw.h" -#include "lcd_text.h" - -static unsigned short MenuRenderPort_LcdSizeX(void) -{ - return LCD_SIZE_X; -} - -static unsigned short MenuRenderPort_LcdSizeY(void) -{ - return LCD_SIZE_Y; -} - -static unsigned char MenuRenderPort_LcdColorFont(void) -{ - return LCD_FONT; -} - -static unsigned char MenuRenderPort_LcdColorBack(void) -{ - return LCD_BACK; -} - -static const MenuRenderPort g_lcd_port = { - .init = Lcd_Init, - .get_size_x = MenuRenderPort_LcdSizeX, - .get_size_y = MenuRenderPort_LcdSizeY, - .get_color_font = MenuRenderPort_LcdColorFont, - .get_color_back = MenuRenderPort_LcdColorBack, - .fill_rect = Lcd_FillRect, - .line_h = Lcd_LineH, - .line_v = Lcd_LineV, - .line = Lcd_Line, - .set_pixel = Lcd_SetPixel, - .invert = Lcd_Invert, - .show_str = Lcd_ShowStr, -}; - -const MenuRenderPort *MenuRenderPort_Lcd(void) -{ - return &g_lcd_port; -} diff --git a/src/Drv/menu/view/menu_view.c b/src/Drv/menu/view/menu_view.c deleted file mode 100644 index d2e1f20..0000000 --- a/src/Drv/menu/view/menu_view.c +++ /dev/null @@ -1,114 +0,0 @@ -#include "menu_view.h" - -static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width) -{ - uint8_t fontColor = view->port->get_color_font(); - view->port->line_h(16, 144, yStart, width, fontColor); - view->port->line(8, yStart - 8, 16, yStart, width, fontColor); - view->port->line(144, yStart, 152, yStart - 8, width, fontColor); -} - -static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY) -{ - uint8_t backColor = view->port->get_color_back(); - uint8_t fontColor = view->port->get_color_font(); - for (uint16_t y = topY; y < bottomY; y++) - { - for (uint16_t x = leftX; x < rightX; x++) - { - view->port->set_pixel(x, y, backColor); - } - } - - view->port->line_h(leftX, rightX, topY, 1, fontColor); - view->port->line_v(topY, bottomY, leftX, 1, fontColor); - view->port->line_h(leftX, rightX + 1, bottomY, 1, fontColor); - view->port->line_v(topY, bottomY, rightX, 1, fontColor); -} - -static void MenuView_ShowOtherLevel(MenuView *view, const tagMenuCtrl *menuCtrl, uint8_t level) -{ - tagPMenuItem ptIndex = menuCtrl->ptRoute[level]; - tagPMenuItem ptRoute; - uint16_t wPosX; - uint16_t wPosY; - - MenuView_DrawBoundaryBox(view, ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY); - - ptRoute = menuCtrl->ptRoute[level + 1]; - ptIndex = ptRoute; - wPosX = menuCtrl->ptRoute[level]->wSPosX + 4; - for (uint16_t index = 0; index < menuCtrl->ptRoute[level]->wNum; index++) - { - wPosY = menuCtrl->ptRoute[level]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3; - view->port->show_str(wPosX, wPosY, ptIndex->byName); - if (ptRoute == ptIndex) - { - view->port->invert(menuCtrl->ptRoute[level]->wSPosX + 2, wPosY - 1, menuCtrl->ptRoute[level]->wEPosX - 2, wPosY + 14); - } - ptIndex = ptIndex->ptBehind; - } -} - -static void MenuView_ShowTopLevel(MenuView *view) -{ - uint16_t lcdSizeX = view->port->get_size_x(); - view->port->fill_rect(0, 0, lcdSizeX - 1, 32, view->port->get_color_back()); - MenuView_DrawMeitou(view, 16, 2); - view->port->show_str(16, 20, (uint8_t *)"当前模式: 无模式"); -} - -void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort) -{ - view->port = renderPort; -} - -void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config) -{ - (void)view; - MenuLayout_PositionCal(menuCtrl->ptHead, menuCtrl->by0LevelNum, config); -} - -void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl) -{ - uint8_t needFullRefresh = 0; - uint8_t backColor = view->port->get_color_back(); - uint16_t lcdSizeX = view->port->get_size_x(); - uint16_t menuYMax = MENU_YMAX_FROM_LCD(view->port->get_size_y()); - - if (dspCtrl->bFirst) - { - dspCtrl->bFirst = 0; - needFullRefresh = 1; - } - - if (menuCtrl->pt0Level != menuCtrl->ptRoute[0]) - { - needFullRefresh = 1; - view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor); - menuCtrl->pt0Level = menuCtrl->ptRoute[0]; - menuCtrl->ptCurBak = menuCtrl->ptCurrent; - } - else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent) - { - if (menuCtrl->ptCurrent->byClass >= menuCtrl->ptCurBak->byClass) - { - MenuView_ShowOtherLevel(view, menuCtrl, menuCtrl->ptCurrent->byClass - 1); - } - else - { - view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor); - needFullRefresh = 1; - } - menuCtrl->ptCurBak = menuCtrl->ptCurrent; - } - - if (needFullRefresh > 0) - { - MenuView_ShowTopLevel(view); - for (uint8_t index = 0; index < menuCtrl->ptCurrent->byClass; index++) - { - MenuView_ShowOtherLevel(view, menuCtrl, index); - } - } -} diff --git a/src/Drv/menu/view/menu_view.h b/src/Drv/menu/view/menu_view.h deleted file mode 100644 index 61490a1..0000000 --- a/src/Drv/menu/view/menu_view.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MENU_VIEW_H -#define MENU_VIEW_H - -#include "menu_layout.h" -#include "menu_render_port.h" -#include "../common/menu_state_types.h" - -typedef struct -{ - const MenuRenderPort *port; -} MenuView; - -void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort); -void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config); -void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl); - -#endif diff --git a/src/Drv/pages/global/global_state.c b/src/Drv/pages/global/global_state.c new file mode 100644 index 0000000..b0c494b --- /dev/null +++ b/src/Drv/pages/global/global_state.c @@ -0,0 +1,85 @@ +#include + +#include "global_state.h" + +static GlobalModel s_globalModel; + +static void GlobalModel_CopyText(uint8_t *dst, uint16_t dstSize, const uint8_t *src) +{ + uint16_t i; + if ((dst == NULL) || (dstSize == 0)) + { + return; + } + + if (src == NULL) + { + dst[0] = '\0'; + return; + } + + for (i = 0; i < (uint16_t)(dstSize - 1); i++) + { + dst[i] = src[i]; + if (src[i] == '\0') + { + return; + } + } + dst[dstSize - 1] = '\0'; +} + +void GlobalModel_Init(void) +{ + memset(&s_globalModel, 0, sizeof(s_globalModel)); +} + +GlobalModel *GlobalModel_Instance(void) +{ + return &s_globalModel; +} + +void GlobalModel_SetActiveYcType(uint16_t ycType) +{ + s_globalModel.activeYcType = ycType; +} + +uint16_t GlobalModel_GetActiveYcType(void) +{ + return s_globalModel.activeYcType; +} + +void GlobalModel_SetMenuAction(uint16_t actionId, + const uint8_t *menuName, + const uint8_t *menuTip, + uint16_t menuPara) +{ + s_globalModel.activeActionId = actionId; + s_globalModel.activeMenuPara = menuPara; + GlobalModel_CopyText(s_globalModel.activeMenuName, + (uint16_t)sizeof(s_globalModel.activeMenuName), + menuName); + GlobalModel_CopyText(s_globalModel.activeMenuTip, + (uint16_t)sizeof(s_globalModel.activeMenuTip), + menuTip); +} + +uint16_t GlobalModel_GetMenuActionId(void) +{ + return s_globalModel.activeActionId; +} + +uint16_t GlobalModel_GetMenuActionPara(void) +{ + return s_globalModel.activeMenuPara; +} + +const uint8_t *GlobalModel_GetMenuActionName(void) +{ + return s_globalModel.activeMenuName; +} + +const uint8_t *GlobalModel_GetMenuActionTip(void) +{ + return s_globalModel.activeMenuTip; +} diff --git a/src/Drv/pages/global/global_state.h b/src/Drv/pages/global/global_state.h new file mode 100644 index 0000000..35d7860 --- /dev/null +++ b/src/Drv/pages/global/global_state.h @@ -0,0 +1,71 @@ +#ifndef PAGE_GLOBAL_STATE_H +#define PAGE_GLOBAL_STATE_H + +#include "types.h" + +typedef enum +{ + MENU_ACTION_NONE = 0, + MENU_ACTION_SET_VALUE, + MENU_ACTION_SET_SOFT, + MENU_ACTION_CFG_TIME, + MENU_ACTION_CFG_REV_EVENT, + MENU_ACTION_CFG_MANUAL_WAVE, + MENU_ACTION_SEE_INPUT, + MENU_ACTION_SEE_SET, + MENU_ACTION_SEE_SOFT, + MENU_ACTION_YX_SET_COMM_TYPE, + MENU_ACTION_YX_SET_WIDTH, + MENU_ACTION_YX_SET_TWIN, + MENU_ACTION_ALLINF_DEFAULT, + MENU_ACTION_PARA_DEFAULT, + MENU_ACTION_SET_DEFAULT, + MENU_ACTION_RESUME_SOFT, + MENU_ACTION_CFG_CELL_DEF, + MENU_ACTION_CFG_SHOW_ANA_TYPE, + MENU_ACTION_DBG_XU_YX, + MENU_ACTION_DBG_XU_YC, + MENU_ACTION_DBG_XU_EVENT, + MENU_ACTION_DBG_RELAY, + MENU_ACTION_SEE_VERSION_BOARD, + MENU_ACTION_CFG_CLR_REC, + MENU_ACTION_CFG_COM_PARA, + MENU_ACTION_CFG_EDIT_IP, + MENU_ACTION_CFG_EDIT_SNTP, + MENU_ACTION_CFG_CELL_CONF, + MENU_ACTION_SEE_REC_SOE, + MENU_ACTION_SEE_REC_ACT, + MENU_ACTION_SEE_REC_OPT, + MENU_ACTION_SEE_REC_ALM, + MENU_ACTION_SEE_REC_START, + MENU_ACTION_SEE_REC_YK, + MENU_ACTION_SEE_REC_CHK, + MENU_ACTION_SEE_REC_RUN, + MENU_ACTION_SEE_REC_FAULT, + MENU_ACTION_YC_SET_SQ_VALUE, + MENU_ACTION_YC_SET_ADJ_COE +} menu_action_id_t; + +typedef struct +{ + uint16_t activeYcType; + uint16_t activeMenuPara; + uint16_t activeActionId; + uint8_t activeMenuName[50]; + uint8_t activeMenuTip[50]; +} GlobalModel; + +void GlobalModel_Init(void); +GlobalModel *GlobalModel_Instance(void); +void GlobalModel_SetActiveYcType(uint16_t ycType); +uint16_t GlobalModel_GetActiveYcType(void); +void GlobalModel_SetMenuAction(uint16_t actionId, + const uint8_t *menuName, + const uint8_t *menuTip, + uint16_t menuPara); +uint16_t GlobalModel_GetMenuActionId(void); +uint16_t GlobalModel_GetMenuActionPara(void); +const uint8_t *GlobalModel_GetMenuActionName(void); +const uint8_t *GlobalModel_GetMenuActionTip(void); + +#endif diff --git a/src/Drv/pages/global/renderer_lcd.c b/src/Drv/pages/global/renderer_lcd.c new file mode 100644 index 0000000..9952fcc --- /dev/null +++ b/src/Drv/pages/global/renderer_lcd.c @@ -0,0 +1,44 @@ +#include "renderer_lcd.h" + +#include "lcd.h" +#include "lcd_draw.h" +#include "lcd_text.h" + +static unsigned short PageRenderer_LcdSizeX(void) +{ + return LCD_SIZE_X; +} + +static unsigned short PageRenderer_LcdSizeY(void) +{ + return LCD_SIZE_Y; +} + +static unsigned char PageRenderer_LcdColorFont(void) +{ + return LCD_FONT; +} + +static unsigned char PageRenderer_LcdColorBack(void) +{ + return LCD_BACK; +} + +static const PageRenderPort g_lcd_port = { + .get_size_x = PageRenderer_LcdSizeX, + .get_size_y = PageRenderer_LcdSizeY, + .get_color_font = PageRenderer_LcdColorFont, + .get_color_back = PageRenderer_LcdColorBack, + .fill_rect = Lcd_FillRect, + .line_h = Lcd_LineH, + .line_v = Lcd_LineV, + .line = Lcd_Line, + .set_pixel = Lcd_SetPixel, + .invert = Lcd_Invert, + .show_str = Lcd_ShowStr, +}; + +const PageRenderPort *PageRenderer_Lcd(void) +{ + return &g_lcd_port; +} diff --git a/src/Drv/menu/view/menu_render_port.h b/src/Drv/pages/global/renderer_lcd.h similarity index 88% rename from src/Drv/menu/view/menu_render_port.h rename to src/Drv/pages/global/renderer_lcd.h index 253b751..c914986 100644 --- a/src/Drv/menu/view/menu_render_port.h +++ b/src/Drv/pages/global/renderer_lcd.h @@ -1,9 +1,8 @@ -#ifndef MENU_RENDER_PORT_H -#define MENU_RENDER_PORT_H +#ifndef PAGE_RENDERER_LCD_H +#define PAGE_RENDERER_LCD_H typedef struct { - void (*init)(void); unsigned short (*get_size_x)(void); unsigned short (*get_size_y)(void); unsigned char (*get_color_font)(void); @@ -15,8 +14,8 @@ typedef struct signed char (*set_pixel)(unsigned short x, unsigned short y, unsigned char color); signed char (*invert)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y); signed char (*show_str)(unsigned short x, unsigned short y, unsigned char *text); -} MenuRenderPort; +} PageRenderPort; -const MenuRenderPort *MenuRenderPort_Lcd(void); +const PageRenderPort *PageRenderer_Lcd(void); #endif diff --git a/src/Drv/pages/menu/def.h b/src/Drv/pages/menu/def.h new file mode 100644 index 0000000..6c04b9d --- /dev/null +++ b/src/Drv/pages/menu/def.h @@ -0,0 +1,145 @@ +#ifndef MENU_def_H +#define MENU_def_H + +/* layout constants are unified into menu_view */ +#define MENU_SIZE_Y (160) +#define MENU_SIZE_X (160) +#define CN_HEIGHT 12 +#define CN_ROWSPACE 2 +#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) +#define MENU_XADD 4 +#define MENU_YADD 4 +#define MENU_WITDTH 7 +#define MENU_YMIN 0 +#define MENU_YMAX (MENU_SIZE_Y - CN_HEIGHT) + + +/* ------------------------------------------------------------------------- + * 枚举名: MenuMode + * 功能: + * 定义菜单页顶部状态栏显示所使用的运行模式。 + * + * 枚举值说明: + * MODE_NONE - 无模式/默认模式 + * MODE_OVERFLOW_PROTECTION - 过流保护模式 + * MODE_LOCAL_FEEDER_SEGMENT - 就地馈线分段模式 + * MODE_LOCAL_FEEDER_CONTACT - 就地馈线联络模式 + * ------------------------------------------------------------------------- */ +typedef enum +{ + MODE_NONE = 0, + MODE_OVERFLOW_PROTECTION = 1, + MODE_LOCAL_FEEDER_SEGMENT = 2, + MODE_LOCAL_FEEDER_CONTACT = 3 +} MenuMode; + +typedef struct MenuItem MenuItem; +typedef MenuItem *tagPMenuItem; +typedef MenuItem tagMenuItem; + +/* ------------------------------------------------------------------------- + * 结构体名: tagMenuModel + * 功能: + * 菜单项的静态业务定义数据(名称、属性、参数与动作入口)。 + * + * 字段说明: + * byClass - 菜单层级(0 表示根层,其它值表示子层级深度) + * byName - 菜单显示名称(UTF-8 字节数组) + * byTip - 菜单提示信息(UTF-8 字节数组) + * byAttrib - 菜单属性标志(读写权限/类型等) + * wPassword - 菜单访问密码或权限码 + * wPara - 菜单动作参数(业务扩展字段) + * pfnWinProc - 菜单动作回调函数(叶子节点触发) + * ------------------------------------------------------------------------- */ +typedef struct tagMenuModel +{ + uint8_t byClass; + uint8_t byName[50]; + uint8_t byTip[50]; + uint8_t byAttrib; + uint16_t wPassword; + uint16_t wPara; + FUNCPTR pfnWinProc; +} tagMenuModel; + +/* ------------------------------------------------------------------------- + * 结构体名: MenuLinks + * 功能: + * 菜单运行时节点的四向链路关系,描述父子与同层前后关系。 + * + * 字段说明: + * higher - 指向父节点 + * lower - 指向第一个子节点 + * before - 指向同层前一个节点 + * behind - 指向同层后一个节点 + * ------------------------------------------------------------------------- */ +typedef struct +{ + MenuItem *higher; + MenuItem *lower; + MenuItem *before; + MenuItem *behind; +} MenuLinks; + +/* ------------------------------------------------------------------------- + * 结构体名: MenuRect + * 功能: + * 菜单项/菜单框的布局计算结果,供 View 层绘制与反显定位使用。 + * + * 字段说明: + * wPos - 当前项在本层中的行号(从 0 开始) + * wNum - 当前层可见项数量 + * wSPosX - 绘制区域左上角 X + * wSPosY - 绘制区域左上角 Y + * wEPosX - 绘制区域右下角 X + * wEPosY - 绘制区域右下角 Y + * ------------------------------------------------------------------------- */ +typedef struct +{ + uint16_t wPos; /* 当前项在本层中的行号(从 0 开始) */ + uint16_t wNum; + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; +} MenuRect; + +/* ------------------------------------------------------------------------- + * 结构体名: MenuItem + * 功能: + * 菜单运行时节点,聚合链路关系、业务定义与布局结果。 + * + * 字段说明: + * links - 节点拓扑关系(父子/同层) + * menuDef - 节点业务定义(名称、属性、回调等) + * rect - 节点绘制位置与层级布局信息 + * ------------------------------------------------------------------------- */ +struct MenuItem +{ + MenuLinks links; + tagMenuModel menuDef; + MenuRect rect; +}; + +/* ------------------------------------------------------------------------- + * 结构体名: tagMenuCtrl + * 功能: + * 菜单导航与渲染过程中的运行时控制上下文(Presenter 持有)。 + * + * 字段说明: + * ptHead - 菜单根节点 + * ptCurrent - 当前选中节点 + * ptRoute - 当前路径路由(按层级保存各级节点,固定深度 4) + * ptCurBak - 上一次选中节点(用于差分刷新) + * pt0Level - 当前顶层节点缓存(用于判断是否需要整页刷新) + * ------------------------------------------------------------------------- */ +typedef struct +{ + tagPMenuItem ptHead; + tagPMenuItem ptCurrent; + tagPMenuItem ptRoute[4]; + tagPMenuItem ptCurBak; + tagPMenuItem pt0Level; +} tagMenuCtrl; + +#endif \ No newline at end of file diff --git a/src/Drv/pages/menu/display.h b/src/Drv/pages/menu/display.h new file mode 100644 index 0000000..06b8440 --- /dev/null +++ b/src/Drv/pages/menu/display.h @@ -0,0 +1,138 @@ +#ifndef __DISPLAY__H__ +#define __DISPLAY__H__ + +#include "types.h" + +#define CN_COP_PWD (321) +#define CN_USER_PWD (700) +#define CN_SUPER_PWD (620) + +enum _REV_TYPE_ +{ + EN_REV_FREE, + EN_REV_FORCE +}; + +enum _SET_SIDE_TYPE_ +{ + EN_SIDE_START = 0, + EN_SIDE_BASIC, + EN_SIDE_DEVINF, + EN_SIDE_COP, + EN_SIDE_MATRIX, + EN_SIDE_ALL, + EN_SIDE_HIGH, + EN_SIDE_MED1, + EN_SIDE_MED2, + EN_SIDE_LOW1, + EN_SIDE_LOW2, + EN_SIDE_LK, + EN_SIDE_Z, + EN_SIDE_DEF, + EN_SIDE_NONE = 0xFF +}; + +enum _SOFT_TYPE_NUMBER +{ + EN_SOFT_PRO = 0, + EN_SOFT_GOOSE, + EN_SOFT_MU, + EN_SOFT_SWITCH, + EN_SOFT_BAK, + EN_SOFT_TYPE_END +}; + +enum _MEA_TYPE_ +{ + EN_MEA_RLY, + EN_MEA_RLY2, + EN_MEA_RLY3, + EN_MEA_ANA, + EN_MEA_ANA2, + EN_MEA_ANA3, + EN_MEA_AC, + EN_MEA_DC, + EN_MEA_SYN, + EN_MEA_POWER, + EN_MEA_DD, + EN_MEA_JLYC, + EN_MEA_GEAR, + EN_MEA_TQ, + EN_MEA_INPUT1, + EN_MEA_INPUT2, + EN_MEA_INPUT3, + EN_INPUT_RLY_ALL, + EN_INPUT_RLY_FAULT, + EN_INPUT_RLY_OTHER, + EN_INPUT_BS_ALL, + EN_INPUT_BS_FAULT, + EN_INPUT_BS_OTHER, + EN_MEA_ADJ, + EN_MEA_YX, + EN_OUTPUT_TRIP, + EN_OUTPUT_SIGN, + EN_MEA_LS, + EN_MEA_SCRLY +}; + +enum _INDEX_VALUE_TYPE_ +{ + EN_SOFT_SET, + EN_FIGURE_SET +}; + +enum _ANA_PARA_ +{ + EN_ANA_0 = 1, + EN_ANA_1 +}; + +enum _INPUT_PARA_ +{ + EN_INPUT_0 = 1, + EN_INPUT_1 +}; + +enum _ANA_TYPE_ +{ + EN_TYPE_DIF_CURRENT = 0, + EN_TYPE_UNIT_CURRENT, + EN_TYPE_UNIT_VOLTAGE +}; + +enum _NO_USER_PASSWORD_ +{ + EN_NO_USER_PWD = 0x55 +}; +enum _FACTORY_PASSWORD_ +{ + EN_FACTORY_PASSWORD = 0x55 +}; + +enum _REC_TYPE_ +{ + EN_ACT_REC = 0, + EN_ALM_REC, + EN_CHK_REC, + EN_SOE_REC, + EN_COS_REC, + EN_LOCK_REC, + EN_OVER_REC, + EN_START_REC, + EN_RUN_REC, + EN_INPUT_REC, + EN_ONOFF_REC, + EN_OPT_REC, + EN_YK_REC, + EN_SC_REC, + EN_SCSTEPINFO_REC, + EN_FAULT_REC, + EN_ACTWAVE_REC, + EN_STARTWAVE_REC, + EN_HANDWAVE_REC, + EN_FAULT_NO, + EN_ALL_REC = 0xFF, + EN_NO_REC = 0xFFFF +}; + +#endif diff --git a/src/Drv/pages/menu/menu_model.h b/src/Drv/pages/menu/menu_model.h new file mode 100644 index 0000000..1fd0ace --- /dev/null +++ b/src/Drv/pages/menu/menu_model.h @@ -0,0 +1,69 @@ +#ifndef MENU_MODEL_H +#define MENU_MODEL_H + +#include "display.h" + +typedef struct tagMenuModel +{ + uint8_t byClass; + uint8_t byName[50]; + uint8_t byTip[50]; + uint8_t byAttrib; + uint16_t wPassword; + uint16_t wPara; + FUNCPTR pfnWinProc; +} tagMenuModel; + +typedef struct MenuItem MenuItem; +typedef MenuItem *tagPMenuItem; +typedef MenuItem tagMenuItem; + +typedef struct +{ + MenuItem *higher; + MenuItem *lower; + MenuItem *before; + MenuItem *behind; +} MenuLinks; + +typedef struct +{ + uint16_t wPos; + uint16_t wNum; + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; +} MenuRect; + +struct MenuItem +{ + MenuLinks links; + tagMenuModel menuDef; + MenuRect rect; +}; + + + +typedef struct +{ + uint8_t by0LevelNum; + tagPMenuItem ptHead; + tagPMenuItem ptCurrent; + tagPMenuItem ptRoute[4]; + tagPMenuItem ptCurBak; + tagPMenuItem pt0Level; +} tagMenuCtrl; + +typedef struct menu_model_t menu_model_t; + +struct menu_model_t +{ + uint16_t maxItem; + tagMenuItem menuItems[300]; + uint16_t by0LevelNum; +}; + +void MenuModel_Init(menu_model_t *model); + +#endif diff --git a/src/Drv/pages/menu/model.c b/src/Drv/pages/menu/model.c new file mode 100644 index 0000000..e8e39bc --- /dev/null +++ b/src/Drv/pages/menu/model.c @@ -0,0 +1,891 @@ +#include "model.h" + +#include + +#include "../../menu/app/menu.h" +#include "utf8.h" + +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_Utf8LenCal + * 功能: + * 计算 UTF-8 字符串在当前菜单显示规则下的“显示宽度”。 + * + * 参数: + * str - 待计算的 UTF-8 字符串 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `str` 非空且为 `\0` 结尾。 + * - 依赖 `utf8_next()` 逐字符解析 UTF-8 编码;若输入非法,结果由底层解析行为决定。 + * + * 说明: + * - 当前显示规则约定: + * 1) ASCII/单字节字符宽度记为 1 + * 2) 多字节 UTF-8 字符(如中文)宽度记为 2 + * - 本函数返回的是“显示占位宽度”,不是原始字节长度。 + * - 该结果会被菜单布局函数用于计算菜单框宽度与字符串显示长度。 + * + * 返回值: + * - UTF-8 字符串的显示宽度 + * ------------------------------------------------------------------------- */ +uint8_t MenuModel_Utf8LenCal(uint8_t *str) +{ + uint8_t strLen = 0; + uint32_t unicode; + uint8_t index = 0; + uint8_t n = 0; + + while (str[index] != '\0') + { + n = utf8_next(str + index, &unicode); + strLen += (n > 1) ? 2 : 1; + index += n; + } + return strLen; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuLayout_ItemDisplayLen + * 功能: + * 补充右箭头并计算单个菜单项最终用于布局的显示宽度。 + * + * 参数: + * item - 菜单项节点指针 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `item` 有效。 + * - 菜单名称最大扫描长度固定为 50,与 `byName` 缓冲区容量保持一致。 + * + * 说明: + * - 先通过 `MenuModel_Utf8LenCal()` 计算菜单名称本身的显示宽度。 + * - 若该菜单项存在子节点(`links.lower != NULL`),则该项在界面上通常会显示 + * 一个“可下钻”装饰符(例如 `\x10` 右箭头)。 + * - 如果当前名称末尾尚未包含该装饰符,则额外为布局宽度加 1, + * 以保证菜单框宽度计算时为装饰符预留空间。 + * - 本函数只做宽度估算,不修改菜单项内容。 + * + * 返回值: + * - 该菜单项的显示宽度 + * ------------------------------------------------------------------------- */ +static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item) +{ + uint8_t displayLen; + displayLen = MenuModel_Utf8LenCal((uint8_t *)item->menuDef.byName); + if (item->links.lower != NULL) + { + uint8_t byteLen = 0; + while ((byteLen < 50) && (item->menuDef.byName[byteLen] != '\0')) + { + byteLen++; + } + if ((byteLen == 0) || (item->menuDef.byName[byteLen - 1] != '\x10')) + { + displayLen += 1; + } + } + return displayLen; +} +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_GetMenuMaxDisplayLen + * 功能: + * 统计当前层级菜单项的最大显示宽度,并返回当前层级菜单项中的最大显示宽度。 + * + * 参数: + * ptFirst - 当前层的首节点指针 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `ptFirst` 有效。 + * + * 说明: + * - 本函数只负责遍历下一层菜单项,并调用 `MenuLayout_ItemDisplayLen()` + * 计算每个节点的显示宽度。 + * + * 返回值: + * - 当前层级菜单项中的最大显示宽度 + * ------------------------------------------------------------------------- */ +uint8_t MenuModel_GetMenuMaxDisplayLen(tagPMenuItem ptFirst) +{ + uint8_t displayLen; + uint8_t byMaxLen = 0; + tagPMenuItem ptIndex = ptFirst; + + for (uint16_t index = 0; index < MENU_MODEL_MAX_ITEM; index++) + { + displayLen = MenuLayout_ItemDisplayLen(ptIndex); + + if (byMaxLen < displayLen) + { + byMaxLen = displayLen; + } + + ptIndex = ptIndex->links.behind; + if (ptIndex == ptFirst) + { + break; + } + } + return byMaxLen; +} +/* ------------------------------------------------------------------------- + * 函数名: MenuLayout_MapMenuPos + * 功能: + * 根据当前菜单项在本层中的原始序号,以及当前层可显示的最大行数, + * 计算该菜单项在菜单框中的实际显示行号。 + * + * 参数: + * menuPos - 当前菜单项在本层中的原始位置序号(从 0 开始) + * itemNum - 当前层菜单项总数 + * maxNum - 当前菜单框在可视区域内最多可容纳的显示行数 + * + * 边界处理: + * - 本函数不做参数合法性校验,调用方需保证 `menuPos / itemNum / maxNum` 有效。 + * - 若 `itemNum <= maxNum`,说明当前层菜单可完整显示,直接返回原始序号。 + * - 若 `itemNum > maxNum` 且 `menuPos < maxNum`,说明当前项仍落在可直接显示区域, + * 直接返回原始序号。 + * + * 说明: + * - 当当前层菜单项总数未超过窗口容量时,菜单项在框内的显示行号与其原始序号一致。 + * - 当菜单项总数超过窗口容量时,需要把靠后的菜单项映射到有限的可视行范围内。 + * - 当前实现策略为: + * 1) 若当前项仍位于前 `maxNum - 1` 个可显示位置内,则保持不变 + * 2) 若当前项已落到窗口容量之外,则将其映射到靠近窗口底部的可视区域 + * - 该函数的作用是为后续 Y 坐标计算提供“菜单框内部显示行号”, + * 避免菜单项过多时直接按原始序号计算导致超出可显示范围。 + * + * 返回值: + * - 当前菜单项在菜单框中的实际显示行号 + * ------------------------------------------------------------------------- */ +static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint16_t itemNum, uint8_t maxNum) +{ + if ((itemNum > maxNum) && (menuPos >= maxNum)) + { + return (uint16_t)(maxNum - (itemNum - menuPos)); + } + return menuPos; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuLayout_TryFlipUp + * 功能: + * 当子菜单默认向下展开可能越界时,尝试将菜单框改为“向上展开”。 + * + * 参数: + * startY - 菜单框起始 Y 坐标(输入/输出) + * endY - 菜单框结束 Y 坐标(输入/输出) + * itemNum - 当前子菜单项数量 + * flipMinY - 允许上翻后的最小 Y 边界 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `startY / endY` 有效。 + * - 若根据 `itemNum` 推导出的上翻后起点小于等于 `flipMinY`, + * 则认为顶部空间不足,上翻失败。 + * - 若候选起点超出 `MENU_YMAX`,则视为异常坐标,上翻失败。 + * + * 说明: + * - 本函数的目标是在“尽量保持父子菜单行对齐”的前提下, + * 把原本会向下越界的子菜单框整体上移。 + * - 计算步骤: + * 1) 以当前 `startY` 为锚点,反推出上翻后的候选顶部 `candidateEndY` + * 2) 判断该候选顶部是否落在允许范围 `(flipMinY, MENU_YMAX)` 内 + * 3) 若可上翻,则将 `startY` 更新为候选顶部 + * 4) 同时使用旧 `startY` 反推出新的 `endY` + * - 返回成功后,调用方可直接使用更新后的 `startY / endY`; + * 若失败,则由调用方继续执行 clamp/probe 等后备布局策略。 + * + * 返回值: + * - 1 : 上翻成功 + * - 0 : 上翻失败 + * ------------------------------------------------------------------------- */ +static uint8_t MenuLayout_TryFlipUp(uint16_t *startY, uint16_t *endY, uint16_t itemNum, uint16_t flipMinY) +{ + uint16_t candidateEndY; + uint16_t temp; + + candidateEndY = (uint16_t)(*startY - (itemNum - 1) * LINE_HEIGHT - MENU_YADD); + if ((candidateEndY > flipMinY) && (candidateEndY < MENU_YMAX)) + { + temp = *startY; + *startY = candidateEndY; + *endY = (uint16_t)(temp + LINE_HEIGHT); + return 1; + } + return 0; +} +/* ------------------------------------------------------------------------- + * 函数名: MenuLayout_FallbackProbeUp + * 功能: + * 当子菜单无法直接放入可视区域时,按“逐行向上试探”的方式寻找可容纳位置。 + * + * 参数: + * ptIndex - 当前菜单路径节点数组(输入) + * itemNum - 当前子菜单项数量 + * startY - 菜单框起始 Y 坐标(输出) + * endY - 菜单框结束 Y 坐标(输出) + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `ptIndex / startY / endY` 有效。 + * - 默认要求 `ptIndex[0]` 已指向 0 级菜单节点,且其 `rect.wSPosY` 已完成布局计算。 + * - 当试探上移后 `startY` 小于 1 行高度时,直接钳制为 0,避免出现负坐标。 + * - 若多次上移后仍无法完全放入 `MENU_YMAX` 范围,本函数保留最后一次试探结果, + * 由调用方或后续绘制逻辑继续处理。 + * + * 说明: + * - 该函数是介于“直接上翻失败”和“强制贴顶”之间的一种更平滑兜底策略。 + * - 处理步骤: + * 1) 先以 0 级菜单顶部 `ptIndex[0]->rect.wSPosY` 作为初始起点 + * 2) 按 `itemNum * LINE_HEIGHT + MENU_YADD` 计算当前理论底部 + * 3) 若底部仍超出 `MENU_YMAX`,则每次向上移动 1 个 `LINE_HEIGHT` + * 4) 每移动一次都重新计算 `endY`,直到菜单框进入可显示区域或试探结束 + * - 这种策略比直接贴顶更保守,尽量保留与原始父菜单位置的相对关系, + * 同时逐步寻找一个更自然的显示位置。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuLayout_FallbackProbeUp(uint16_t w0LevelSPosY, uint16_t itemNum, uint16_t *startY, uint16_t *endY) +{ + *startY = w0LevelSPosY; + *endY = (uint16_t)(*startY + itemNum * LINE_HEIGHT + MENU_YADD); + for (uint16_t i = 1; i < (uint16_t)(w0LevelSPosY / LINE_HEIGHT + 1); i++) + { + if (*endY > MENU_YMAX) + { + *startY = (uint16_t)(w0LevelSPosY - LINE_HEIGHT * i); + if (*startY < LINE_HEIGHT) + { + *startY = 0; + } + *endY = (uint16_t)(*startY + itemNum * LINE_HEIGHT + MENU_YADD); + } + else + { + break; + } + } +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_SubPosCal + * 功能: + * 以递归方式计算从当前层首节点开始的所有子菜单框位置, + * 完成当前层以及更深层级菜单的矩形布局。 + * + * 参数: + * ptFirst - 当前层首节点指针 + * ptParent - 当前层所属父菜单框节点 + * w0LevelSPosY - 0 级菜单框顶部 Y 坐标,用于越界时的回退布局基准 + * + * 边界处理: + * - 若 `ptFirst == NULL`,说明当前层不存在有效菜单链,直接返回。 + * - 本函数不对 `ptParent` 做空指针校验,调用方需保证其有效, + * 且其 `rect` 已完成布局计算。 + * - 当前层遍历仍使用 `MENU_MODEL_MAX_ITEM` 作为保护上限, + * 防止异常链表导致死循环。 + * - 当子菜单框底部超出 `MENU_YMAX` 时,优先尝试上翻; + * 若上翻失败,则退化到 `MenuLayout_FallbackProbeUp()` 继续寻找可显示位置。 + * + * 说明: + * - 本函数是子菜单布局的递归核心,处理流程如下: + * 1) 以 `ptFirst` 为起点遍历当前层所有节点 + * 2) 对每个存在子菜单的节点,先取其下一层首节点 `ptNextFirst` + * 3) 通过 `MenuModel_GetMenuMaxDisplayLen()` 计算该下一层菜单的最大显示宽度 + * 4) 根据父菜单框右边界,计算当前子菜单框的 X 坐标 + * 5) 根据当前节点在本层中的 0 基序号 `wPos`、父层项数 `wNum`, + * 计算当前子菜单框的理论 Y 坐标 + * 6) 若理论底部越界,则尝试上翻或执行回退探测 + * 7) 当前节点对应子菜单框位置确定后, + * 再递归处理其更深一层的子菜单布局 + * - `ptParent->rect.wNum` 表示父节点直接子节点总数, + * 用于把当前节点的原始层内 0 基序号映射到菜单框可显示行号。 + * - `w0LevelSPosY` 作为递归过程中统一的顶部参考, + * 保证深层菜单在空间不足时仍能回退到稳定的显示区域。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_SubPosCal(tagPMenuItem ptFirst, tagPMenuItem ptParent, uint16_t w0LevelSPosY) +{ + uint16_t wSPosY; + uint16_t wEPosY; + uint8_t byMaxLen; + uint8_t byMaxNum; + uint16_t byMenuPos; + uint16_t byItemNum; + + tagPMenuItem ptNextFirst; + tagPMenuItem ptIndex; + + + if (ptFirst == NULL) + { + return; + } + + byMaxNum = (MENU_YMAX - MENU_YMIN) / LINE_HEIGHT; + ptIndex = ptFirst; + for (uint16_t wLoop = 0; wLoop < MENU_MODEL_MAX_ITEM; wLoop++) + { + if (ptIndex->links.lower != NULL) + { + ptNextFirst = ptIndex->links.lower; /* 保存下一层的第一个节点 */ + + byMaxLen = MenuModel_GetMenuMaxDisplayLen(ptNextFirst); /* 获取下一层菜单项中的最大显示宽度 */ + + /* 计算子菜单框的 X 坐标 */ + ptIndex->rect.wSPosX = ptParent->rect.wEPosX; /* 子菜单框左边界紧贴父菜单框右边界 */ + /* 子菜单框右边界由最大显示宽度 * 单字符宽度 + 额外边距推导 */ + ptIndex->rect.wEPosX = (uint16_t)(ptIndex->rect.wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD); + + + /* 计算子菜单框的 Y 坐标 */ + byMenuPos = ptIndex->rect.wPos; /* 当前菜单项的 0 基序号 */ + byItemNum = ptParent->rect.wNum; /* 父菜单项的项数 */ + byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum); + + wSPosY = ptParent->rect.wSPosY + byMenuPos * LINE_HEIGHT; + byItemNum = ptIndex->rect.wNum; + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; + + if (wEPosY < MENU_YMAX) + { + ptIndex->rect.wSPosY = wSPosY; + ptIndex->rect.wEPosY = wEPosY; + } + else + { + if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, MENU_YMIN)) + { + MenuLayout_FallbackProbeUp(w0LevelSPosY, byItemNum, &wSPosY, &wEPosY); + } + ptIndex->rect.wSPosY = wSPosY; + ptIndex->rect.wEPosY = wEPosY; + } + MenuView_SubPosCal(ptNextFirst, ptIndex, w0LevelSPosY); + } + ptIndex = ptIndex->links.behind; + if (ptIndex == ptFirst) + { + break; + } + } +} +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_PositionCal + * 功能: + * 计算整棵菜单树的初始显示位置,完成 0 级菜单以及后续子菜单层级的矩形布局。 + * + * 参数: + * ptMenuHead - 菜单头节点(0 级起点) + * by0LevelNum - 0 级菜单数量 + * + * 边界处理: + * - 若 ptMenuHead 为 NULL、config 为 NULL 或 by0LevelNum 为 0,则直接返回。 + * - 若 by0LevelNum 超过本函数内部固定工作数组容量 4,则直接返回。 + * - 当 0 级菜单数量大于 LCD 宽度时,byInterval 可能被截断为 0; + * 当前通过断言暴露异常输入,并保持返回保护。 + * + * 说明: + * - 本函数是菜单布局计算入口: + * 1) 先建立 0 级菜单的遍历起点、当前位置与 0 基序号 + * 2) 根据 lcdSizeX / by0LevelNum 计算 0 级菜单的水平分布间隔 + * 3) 逐个遍历 0 级菜单项 + * 4) 对存在子菜单的节点,计算其子菜单框体矩形 + * 5) 递归调用 MenuView_Sub1PosCal() 继续完成更深层布局 + * - 本函数只计算 rect,不负责实际绘制。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuModel_PositionCal(tagPMenuItem ptMenuHead, uint16_t by0LevelNum) +{ + tagPMenuItem ptFirst = ptMenuHead; + tagPMenuItem ptIndex = ptMenuHead; + uint8_t byMaxLen; + uint8_t byInterval; + tagPMenuItem ptNextFirst; + + ASSERT((ptMenuHead == NULL) || (by0LevelNum == 0)); + if ((ptMenuHead == NULL) || (by0LevelNum == 0)) + { + return; + } + + byInterval = MENU_SIZE_X / by0LevelNum; + + for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++) + { + if (ptIndex->links.lower != NULL) + { + ptNextFirst = ptIndex->links.lower; /* 保存下一层的第一个节点 */ + byMaxLen = MenuModel_GetMenuMaxDisplayLen(ptNextFirst); + ptIndex->rect.wSPosX = ptIndex->rect.wPos * byInterval; + ptIndex->rect.wSPosY = MENU_SIZE_Y - LINE_HEIGHT - ptIndex->rect.wNum * LINE_HEIGHT - MENU_YADD; + ptIndex->rect.wEPosX = ptIndex->rect.wPos * byInterval + byMaxLen * MENU_WITDTH + MENU_XADD; + ptIndex->rect.wEPosY = MENU_SIZE_Y - LINE_HEIGHT; + + MenuView_SubPosCal(ptNextFirst, ptIndex, ptIndex->rect.wSPosY); + } + ptIndex = ptIndex->links.behind; + if (ptIndex == ptFirst) + { + break; + } + } +} + +enum _STR_FLAG_ { + EN_STR_FLAG_FUNSET = 0, + EN_STR_FLAG_OKCANCEL, + EN_VALUE_FLAG_SLOTTYPE, + EN_VALUE_FLAG_YESNO, + EN_VALUE_FLAG_CHN_NUM, + EN_VALUE_FLAG_YXSET, + EN_VALUE_FLAG_YKSTEP, + EN_VALUE_FLAG_YKACTION, + EN_VALUE_FLAG_CHK, + EN_VALUE_FLAG_BEUSED, + EN_VALUE_FLAG_PORTTYPE, + EN_VALUE_FLAG_MAPNAME, + EN_VALUE_FLAG_RECTITLE, + EN_VALUE_FLAG_BHINFO, + EN_VALUE_FLAG_BHSET, + EN_VALUE_FLAG_OKCANCEL, + EN_VALUE_FLAG_FILETYPE, + EN_VALUE_FLAG_ROLE, + EN_VALUE_FLAG_COMMSTATE, + EN_VALUE_FLAG_ALARM, + EN_VALUE_FLAG_YXTYPE, + EN_STR_FLAG_MENUALAUTO, + EN_STR_FLAG_LINKBREAK, + EN_STR_FLAG_DONEUNDONE, + EN_STR_FLAG_ACTBACK, + EN_STR_FLAG_RESUMEBREAK, + EN_STR_FLAG_USESTOP, + EN_STR_FLAG_BAUDRATE, + EN_STR_FLAG_VERIFY, + EN_STR_FLAG_PROTOL, + EN_STR_FLAG_RECORD, + EN_STR_FLAG_MESSAGE, + EN_STR_FLAG_COM, + EN_STR_FLAG_MODE, + EN_STR_FLAG_COMBAUD, + EN_STR_FLAG_DATABIT, + EN_STR_FLAG_STOPBIT, + EN_STR_FLAG_COMTYPE, + EN_STR_FLAG_SAMEV, + EN_STR_FLAG_PT, + EN_STR_FLAG_CT, + EN_STR_FLAG_TRANSTYPE, + EN_STR_FLAG_LINEPHS, + EN_STR_FLAG_RETOUT, + EN_STR_FLAG_YXDI, + EN_STR_FLAG_YKPUL, + EN_STR_FLAG_WAVE, + EN_STR_FLAG_MEMTYPE, + EN_STR_FLAG_CHANNEL, + EN_STR_FLAG_POWERTYPE, + EN_STR_FLAG_ZEROTYPE, + EN_STR_FLAG_YCCOMMTYPE, + EN_STR_FLAG_YXCOMMTYPE, + EN_STR_FLAG_CTRLWORD, + EN_STR_FLAG_BUSNAME, + EN_STR_FLAG_UNITSHOWMODE, + EN_STR_FLAG_BAYMODE, + EN_STR_FLAG_BASEANA, + EN_STR_FLAG_BASEPHASE, + EN_STR_FLAG_BAYCODE, + EN_STR_FLAG_SET_NUM, + EN_STR_FLAG_DCTYPE, + EN_STR_FLAG_DCINTYPE, + EN_STR_FLAG_DWINTYPE, + EN_STR_FLAG_PTCT, + EN_STR_FLAG_HARMOTYPE, + EN_STR_FLAG_YXSIN_DI, + EN_STR_FLAG_SHOWANATYPE, + EN_STR_FLAG_ANAPOLARTYPE, + EN_STR_FLAG_WIRDIAGRAMTYPE +} enumStrType; + +const tagMenuModel menuTab[] = +{ + { 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, + { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, + { 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, + { 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input }, + { 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value }, + { 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, + { 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, + { 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft }, + { 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set }, + { 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, + { 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, + { 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft }, + { 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue }, + { 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe }, + { 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType }, + { 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth }, + { 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin }, + { 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time }, + { 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent }, + { 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave }, + { 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec }, + { 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara }, + { 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP }, + { 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp }, + { 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE }, + { 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct }, + { 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt }, + { 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm }, + { 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart }, + { 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK }, + { 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk }, + { 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun }, + { 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault }, + { 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, + { 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf }, + { 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default }, + { 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default }, + { 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default }, + { 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft }, + { 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef }, + { 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType }, + { 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX }, + { 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, + { 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, + { 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay }, + { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, + { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, +}; + +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_CountTopLevel + * 功能: + * 统计静态菜单定义表中 0 级菜单项的数量,用于后续顶部菜单布局计算。 + * + * 参数: + * maxItem - 需要参与统计的菜单项总数 + * + * 边界处理: + * - 本函数默认 `maxItem` 不超过 `menuTab` 实际长度,调用方需保证该前提成立。 + * - 当 `maxItem == 0` 时,for 循环不进入,函数返回 0。 + * + * 说明: + * - 在当前菜单模型中,`byClass == 0` 表示顶层菜单节点。 + * - 本函数通过顺序扫描 `menuTab[0 ~ maxItem-1]`, + * 统计所有 0 级节点数量,并将结果返回给布局初始化流程。 + * - 返回值通常会被用于计算顶层菜单在 LCD 上的水平分布间隔。 + * + * 返回值: + * - 静态菜单表中的 0 级菜单数量 + * ------------------------------------------------------------------------- */ +uint16_t MenuModel_CountTopLevel(uint16_t maxItem) +{ + uint8_t by0LevelNum = 0; + uint32_t wLoop; + + for (wLoop = 0; wLoop < maxItem; wLoop++) + { + if (menuTab[wLoop].byClass == 0) + { + by0LevelNum++; + } + } + return by0LevelNum; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_BuildTree + * 功能: + * 根据按层级顺序排列的静态菜单定义表,构建菜单运行时树,并建立 + * `higher / lower / before / behind` 四类链路关系。 + * + * 参数: + * menuItems - 运行时菜单节点数组(输出) + * modelTab - 静态菜单定义表(输入) + * modelCount - 菜单项总数量 + * + * 边界处理: + * - 本函数不做 menuItems/modelTab 空指针校验,调用方需保证输入有效。 + * - 默认要求 modelCount > 0,且 `modelTab` 已按菜单显示顺序正确排列: + * 同层节点连续,子节点紧跟在父节点之后。 + * - 当前函数内部使用固定深度数组 `ptFirst[4] / ptLast[4]`, + * 因此默认支持的最大层级索引为 3。 + * + * 说明: + * - 构建流程: + * 1) 先初始化 `menuItems[0]`,作为整棵树的起点 + * 2) 使用 `ptFirst[level] / ptLast[level]` 记录每一层当前的首节点和尾节点 + * 3) 依次读取后续静态定义项,复制 `menuDef` 到运行时节点 + * 4) 比较“当前节点层级”和“下一个节点层级”,决定链路挂接方式: + * a) byCurClass < byNextClass + * 表示进入更深一层,`ptNextNode` 是 `ptCurrent` 的第一个子节点: + * - `ptCurrent->links.lower = ptNextNode` + * - `ptNextNode->links.higher = ptCurrent` + * - 该层的 `ptFirst/ptLast` 都初始化为 `ptNextNode` + * b) byCurClass == byNextClass + * 表示接入同层兄弟节点: + * - `ptNextNode->links.before = ptCurrent` + * - `ptCurrent->links.behind = ptNextNode` + * - `ptNextNode->links.higher` 继承当前节点的父节点 + * - 更新该层 `ptLast` + * c) byCurClass > byNextClass + * 表示从更深层回退到某个上层后,再接入该层的新兄弟节点: + * - `ptNextNode->links.before = ptLast[byNextClass]` + * - `ptNextNode->links.higher = ptFirst[byNextClass]->links.higher` + * 即:新节点的父节点与该层第一个节点保持一致 + * - 将原尾节点的 `behind` 指向新节点,并更新该层 `ptLast` + * - 对回退过程中已经结束的更深层链表做首尾闭环 + * 5) 全部节点处理完成后,再将 `0 ~ byCurClass` 各层链表统一闭环 + * - 构建结果: + * `higher` - 父节点 + * `lower` - 第一个子节点 + * `before` - 同层前一个节点 + * `behind` - 同层后一个节点 + * - 同层链表最终被组织为环形结构,便于菜单上下移动时循环导航。 + * - 本函数只负责树与链表关系构建,不负责菜单位置计算和显示名称装饰。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuModel_BuildTree(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t modelCount) +{ + tagPMenuItem ptFirst[MENU_MODEL_MAX_LEVEL]; /* 每层第一个节点 */ + tagPMenuItem ptLast[MENU_MODEL_MAX_LEVEL]; /* 每层最后一个节点 */ + tagPMenuItem ptCurrent; /* 当前节点 */ + tagPMenuItem ptNextNode; /* 下一个节点 */ + + uint8_t byCurClass; /* 当前节点层级 */ + uint8_t byNextClass; /* 下一个节点层级 */ + + /* 初始化第 0 个节点,作为整棵树的起点 */ + memset(&menuItems[0], 0, sizeof(menuItems[0])); + /* 初始化第 0 个节点的业务字段 */ + memcpy(&menuItems[0].menuDef, &modelTab[0], sizeof(menuItems[0].menuDef)); + + /* 初始化第 0 个节点的链路 */ + ptFirst[0] = &menuItems[0]; + ptLast[0] = ptFirst[0]; + ptCurrent = &menuItems[0]; + + /* 构建菜单树 */ + for (uint16_t index = 1; index < modelCount; index++) + { + /* 初始化第 index 个节点的业务字段 */ + ptNextNode = &menuItems[index]; + memset(ptNextNode, 0, sizeof(*ptNextNode)); + memcpy(&ptNextNode->menuDef, &modelTab[index], sizeof(ptNextNode->menuDef)); + + byCurClass = ptCurrent->menuDef.byClass; + byNextClass = ptNextNode->menuDef.byClass; + + if (byCurClass < byNextClass) /* 当前节点层级小于下一个节点层级 */ + { + ptCurrent->links.lower = ptNextNode; + ptNextNode->links.higher = ptCurrent; + /* 更新该层 first/last */ + ptFirst[byNextClass] = ptNextNode; + ptLast[byNextClass] = ptNextNode; + } + else if (byCurClass == byNextClass) /* 当前节点层级等于下一个节点层级 */ + { + ptNextNode->links.before = ptCurrent; + ptNextNode->links.higher = ptCurrent->links.higher; + ptCurrent->links.behind = ptNextNode; + /* 更新该层 last */ + ptLast[byNextClass] = ptNextNode; + } + else /* 当前节点层级大于下一个节点层级,回退层级*/ + { + ptNextNode->links.before = ptLast[byNextClass]; + ptNextNode->links.higher = ptFirst[byNextClass]->links.higher; + /* 更新该层 last 的 behind */ + ptLast[byNextClass]->links.behind = ptNextNode; + ptLast[byNextClass] = ptNextNode; + + /* 形成回环链表 */ + for (uint16_t index2 = byCurClass; index2 > byNextClass; index2--) + { + ptLast[index2]->links.behind = ptFirst[index2]; + ptFirst[index2]->links.before = ptLast[index2]; + } + } + /* 更新当前节点 */ + ptCurrent = ptNextNode; + } + /* 形成回环链表 */ + byCurClass = ptCurrent->menuDef.byClass; + for (uint16_t index = 0; index <= byCurClass; index++) + { + ptLast[index]->links.behind = ptFirst[index]; + ptFirst[index]->links.before = ptLast[index]; + } +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_DecorateDisplayNames + * 功能: + * 为存在子菜单的菜单项名称追加显示装饰符,提示该节点可继续进入下一级。 + * + * 参数: + * menuItems - 菜单项数组首地址 + * modelCount - 菜单项总数量 + * + * 边界处理: + * - 本函数不做 menuItems 空指针校验,调用方需保证数组有效。 + * - 若菜单项没有子节点(links.lower == NULL),直接跳过,不追加装饰符。 + * - 通过 `sizeof(byName) - 1` 预留字符串结尾 `\0`,避免越界写入。 + * - 若名称已以 `\x10` 结尾,则不重复追加。 + * + * 说明: + * - `\x10` 在当前菜单显示逻辑中作为“右箭头/可下钻”标记, + * 用于提示用户该项后面还有子菜单。 + * - 处理流程: + * 1) 遍历所有菜单项 + * 2) 判断是否存在子菜单 + * 3) 统计当前名称长度(不超过缓冲区上限) + * 4) 若末尾尚未追加装饰符,则写入 `\x10` 和字符串结束符 `\0` + * - 该函数只修改显示名称,不改变菜单树链路关系和业务属性。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuModel_DecorateDisplayNames(tagMenuItem *menuItems, uint32_t modelCount) +{ + for (uint32_t i = 0; i < modelCount; i++) + { + uint16_t nameCapacity = (uint16_t)sizeof(menuItems[i].menuDef.byName); + uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1); + uint16_t len = 0; + /* 如果没有子菜单,直接跳过 */ + if (menuItems[i].links.lower == NULL) + { + continue; + } + /* 计算名称长度 */ + while ((len < maxTextLenWithoutNull) && (menuItems[i].menuDef.byName[len] != '\0')) + { + len++; + } + /* 补充右箭头 */ + if ((len == 0) || (menuItems[i].menuDef.byName[len - 1] != '\x10')) + { + if (len < maxTextLenWithoutNull) + { + menuItems[i].menuDef.byName[len] = '\x10'; + menuItems[i].menuDef.byName[len + 1] = '\0'; + } + } + } +} +/* ------------------------------------------------------------------------- + * 函数名: MenuModel_IndexMenuItems + * 功能: + * 统计整棵菜单树中所有菜单项的 `wPos` 与 `wNum`。 + * + * 参数: + * menuItems - 菜单项数组 + * modelCount - 菜单项总数 + * + * 边界处理: + * - 若 `menuItems == NULL` 或 `modelCount == 0`,则直接返回。 + * + * 说明: + * - 本函数分两部分初始化: + * 1) 先遍历 0 级同层链表,为所有顶层节点写入 `rect.wPos` + * 2) 再遍历每个节点: + * a) 若无子节点,则其 `rect.wNum = 0` + * b) 若有子节点,则沿子层 `behind` 链统计子节点总数 + * c) 同时为该组子节点写入各自的 `rect.wPos` + * d) 最后把子节点总数写回当前父节点的 `rect.wNum` + * - 因此: + * `wPos` 表示“当前节点在本层中的 0 基序号” + * `wNum` 表示“当前节点拥有的直接子节点总数” + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuModel_IndexMenuItems(tagMenuItem *menuItems, uint32_t modelCount) +{ + tagPMenuItem current; + tagPMenuItem parent; + tagPMenuItem child; + uint16_t count; + + if ((menuItems == NULL) || (modelCount == 0)) + { + return; + } + + /* 1. 记录 0 级节点的位置和子节点数量 */ + current = &menuItems[0]; + count = 0; + for (uint16_t index = 0; index < modelCount; index++) + { + current->rect.wPos = count; + current = current->links.behind; + if ((current == NULL) || (current == &menuItems[0])) + { + break; + } + count++; + } + + /* 2. 记录其他层级的位置和子节点数量 */ + for (uint32_t i = 0; i < modelCount; i++) + { + parent = &menuItems[i]; + current = parent->links.lower; + /* a. 如果当前节点没有子节点,说明是最深一层,记录当前节点子节点数量为 0 */ + if (current == NULL) + { + parent->rect.wNum = 0; + continue; + } + + /*b. 若有子节点,则沿子层 `behind` 链统计子节点总数 */ + child = current; + count = 0; + + for (uint16_t index = 0; index < MENU_MODEL_MAX_ITEM; index++) + { + /*c. 为该组子节点写入各自的 `rect.wPos` */ + child->rect.wPos = count; + count++; + + child = child->links.behind; + if ((child == NULL) || (child == current)) + { + break; + } + } + /* d. 最后把子节点总数写回当前父节点的 `rect.wNum` */ + parent->rect.wNum = count; + } +} + + +void MenuModel_Init(menu_model_t *model) +{ + uint16_t by0LevelNum; + model->maxItem = (uint16_t)(sizeof(menuTab) / sizeof(tagMenuModel)); + memset(model->menuItems, 0, sizeof(model->menuItems)); + + by0LevelNum = MenuModel_CountTopLevel(model->maxItem); + MenuModel_BuildTree(model->menuItems, menuTab, model->maxItem); + MenuModel_DecorateDisplayNames(model->menuItems, model->maxItem); + MenuModel_IndexMenuItems(model->menuItems, model->maxItem); + MenuModel_PositionCal(&model->menuItems[0], by0LevelNum); +} \ No newline at end of file diff --git a/src/Drv/pages/menu/model.h b/src/Drv/pages/menu/model.h new file mode 100644 index 0000000..f4c12f5 --- /dev/null +++ b/src/Drv/pages/menu/model.h @@ -0,0 +1,36 @@ +#ifndef MENU_MODEL_H +#define MENU_MODEL_H + +#include "display.h" +#include "def.h" + +#define MENU_MODEL_MAX_ITEM 300 /* 最大菜单项数 */ +#define MENU_MODEL_MAX_LEVEL 4 /* 最大菜单层级 */ + +typedef struct menu_model_t menu_model_t; + +struct menu_model_t +{ + uint16_t maxItem; + tagMenuItem menuItems[MENU_MODEL_MAX_ITEM]; +}; + + + +void MenuModel_Init(menu_model_t *model); + + + +/* whitebox test hooks */ +#ifdef UNIT_TEST +void MenuModel_IndexMenuItems(tagMenuItem *menuItems, uint32_t modelCount); +void MenuModel_PositionCal(tagPMenuItem ptMenuHead, uint16_t by0LevelNum); +uint8_t MenuModel_Utf8LenCal(uint8_t *str); +uint8_t MenuModel_GetMenuMaxDisplayLen(tagPMenuItem ptFirst); +uint16_t MenuModel_CountTopLevel(uint16_t maxItem); +void MenuModel_BuildTree(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t modelCount); +void MenuModel_DecorateDisplayNames(tagMenuItem *menuItems, uint32_t modelCount); +#endif + + +#endif diff --git a/src/Drv/pages/menu/page.c b/src/Drv/pages/menu/page.c new file mode 100644 index 0000000..34556b3 --- /dev/null +++ b/src/Drv/pages/menu/page.c @@ -0,0 +1,255 @@ +#include +#include + +#include "page.h" + +#include "display.h" +#include "presenter.h" + + +/* ------------------------------------------------------------------------- + * 模块内静态对象说明: + * s_model - 菜单页 Model 实例(菜单数据与运行时结构) + * s_view - 菜单页 View 实例(布局与渲染能力) + * s_presenter - 菜单页 Presenter 实例(输入处理与状态驱动) + * s_menuPage - 页面管理器可注册的 page_t 描述对象 + * + * 说明: + * - 以上对象均为文件内静态单例,生命周期覆盖进程运行期。 + * - 通过 MenuPage_GetInstance() 暴露 s_menuPage 给 PageManager 注册。 + * ------------------------------------------------------------------------- */ +static menu_model_t s_model; +static menu_view_t s_view; +static menu_presenter_t s_presenter; +static page_t s_menuPage; + + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnEnter + * 功能: + * 页面进入回调:将菜单渲染标记为“首帧全量刷新”,并立即触发一次刷新。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 本函数不依赖 page 内容,统一转为 (void)page 消除未使用告警。 + * + * 说明: + * - 通过 dspCtrl.bFirst = 1 告知 Presenter 下一次刷新走首帧路径。 + * - 进入页面后立即刷新,确保界面可见状态与内部状态同步。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPage_OnEnter(page_t *page) +{ + (void)page; + s_presenter.dspCtrl.bFirst = 1; + MenuPresenter_Refresh(&s_presenter); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnExit + * 功能: + * 页面退出回调占位点;当前版本无额外退出动作。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 (void)page 防止未使用参数告警。 + * + * 说明: + * - 预留给后续扩展(如停止定时任务、冻结动画、保存瞬时 UI 状态等)。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPage_OnExit(page_t *page) +{ + (void)page; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnDestroy + * 功能: + * 页面销毁回调:清空菜单页内部三层对象(Model/View/Presenter)状态。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 memset 全量清零静态对象,避免残留状态影响后续重建。 + * + * 说明: + * - 与 is_cached 策略配合:当页面被标记为非缓存并弹栈销毁时,该函数用于复位。 + * - page_t 元信息不在此函数复位,由页面生命周期创建阶段重新赋值。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPage_OnDestroy(page_t *page) +{ + (void)page; + memset(&s_model, 0, sizeof(s_model)); + memset(&s_view, 0, sizeof(s_view)); + memset(&s_presenter, 0, sizeof(s_presenter)); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnEvent + * 功能: + * 页面事件回调:校验输入事件后,将按键转交 Presenter 处理。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * event - 输入事件指针 + * + * 边界处理: + * - event 为 NULL 时返回 EVENT_UNHANDLED。 + * - 仅处理 PAGE_EVENT_KEY 事件类型,其它类型返回 EVENT_UNHANDLED。 + * - keyVal 为 0 视为无效按键,返回 EVENT_UNHANDLED。 + * + * 说明: + * - 事件有效时调用 MenuPresenter_HandleInput() 执行业务输入流转。 + * - 返回 EVENT_HANDLED,表示该事件已被菜单页消费,不再交给上层页面逻辑。 + * + * 返回值: + * - EVENT_HANDLED : 事件已处理 + * - EVENT_UNHANDLED : 事件无效或不属于本页处理范围 + * ------------------------------------------------------------------------- */ +static event_result_t MenuPage_OnEvent(page_t *page, input_event_t *event) +{ + (void)page; + if ((event == NULL) || (event->type != PAGE_EVENT_KEY) || (event->keyVal == 0)) + { + return EVENT_UNHANDLED; + } + + MenuPresenter_HandleInput(&s_presenter, event->keyVal); + return EVENT_HANDLED; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnLoop + * 功能: + * 页面循环回调:周期性驱动 Presenter 执行刷新逻辑。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 (void)page 防止未使用参数告警。 + * + * 说明: + * - 实际刷新策略(全量/增量)由 Presenter 内部状态控制。 + * - 该函数通常由 PageManager_Loop() 在主循环节拍中调用。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPage_OnLoop(page_t *page) +{ + (void)page; + MenuPresenter_Refresh(&s_presenter); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnCreate + * 功能: + * 页面创建回调:按 Model -> View -> Presenter 顺序完成菜单页运行时装配, + * 并初始化 page_t 描述对象字段。 + * + * 参数: + * page - 当前页面对象指针(由 PageManager 传入) + * + * 边界处理: + * - 先清零 s_menuPage,再统一重建其元信息与回调绑定,避免脏状态遗留。 + * - 假定 page 非空且来自 PageManager 生命周期调用链。 + * + * 说明: + * - 初始化顺序固定: + * 1) MenuModel_Init(&s_model) + * 2) MenuView_Init(&s_view) + * 3) MenuPresenter_Init(&s_presenter, &s_model, &s_view) + * - 将 model/presenter/view 回填到 page 与 s_menuPage,便于调试与统一访问。 + * - s_menuPage 作为静态页面实例,对外由 MenuPage_GetInstance() 返回。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPage_OnCreate(page_t *page) +{ + /* 1) model init */ + MenuModel_Init(&s_model); + + /* 2) view init */ + MenuView_Init(&s_view); + + /* 3) presenter setup + runtime build */ + MenuPresenter_Init(&s_presenter, &s_model, &s_view); + + + page->model = &s_model; + page->presenter = &s_presenter; + page->view = &s_view; + + s_menuPage.presenter = &s_presenter; + s_menuPage.view = &s_view; + s_menuPage.model = &s_model; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_GetInstance + * 功能: + * 获取菜单页静态实例指针,供 PageManager_Register() 注册使用。 + * + * 参数: + * 无 + * + * 边界处理: + * - 返回文件内静态对象地址,无空指针分支。 + * + * 说明: + * - 该函数仅暴露页面入口,不负责初始化;初始化由 on_create 生命周期完成。 + * + * 返回值: + * - 指向静态页面对象 s_menuPage 的 page_t* 指针 + * ------------------------------------------------------------------------- */ +page_t *MenuPage_GetInstance(void) +{ + /* 确保在注册到 PageManager 前,页面生命周期回调已就绪 */ + memset(&s_menuPage, 0, sizeof(s_menuPage)); + s_menuPage.page_id = PAGE_ID_MENU; + s_menuPage.is_cached = 1; + s_menuPage.on_create = MenuPage_OnCreate; + s_menuPage.on_enter = MenuPage_OnEnter; + s_menuPage.on_exit = MenuPage_OnExit; + s_menuPage.on_destroy = MenuPage_OnDestroy; + s_menuPage.on_event = MenuPage_OnEvent; + s_menuPage.on_loop = MenuPage_OnLoop; + return &s_menuPage; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_TriggerCurrentAction + * 功能: + * 触发当前菜单项动作的占位接口(当前版本仅输出调试日志)。 + * + * 参数: + * 无 + * + * 边界处理: + * - 当前实现不依赖外部输入,不涉及参数校验。 + * + * 说明: + * - 现阶段用于保留动作触发扩展点,后续可接入真实业务动作执行链路。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuPage_TriggerCurrentAction(void) +{ + printf("MenuPage_TriggerCurrentAction\n"); +} diff --git a/src/Drv/pages/menu/page.h b/src/Drv/pages/menu/page.h new file mode 100644 index 0000000..41d1653 --- /dev/null +++ b/src/Drv/pages/menu/page.h @@ -0,0 +1,10 @@ +#ifndef MENU_PAGE_H +#define MENU_PAGE_H + +#include "view.h" +#include "../page.h" + +page_t *MenuPage_GetInstance(void); +void MenuPage_TriggerCurrentAction(void); + +#endif diff --git a/src/Drv/pages/menu/presenter.c b/src/Drv/pages/menu/presenter.c new file mode 100644 index 0000000..b5cd522 --- /dev/null +++ b/src/Drv/pages/menu/presenter.c @@ -0,0 +1,272 @@ +#include "presenter.h" + +#include +#include + +#include "../../key.h" + +typedef struct +{ + tagPMenuItem ptHead; + tagPMenuItem ptCurrent; + tagPMenuItem ptRoute[4]; + tagPMenuItem ptCurBak; + tagPMenuItem pt0Level; +} MenuNavState; + +typedef struct +{ + uint8_t needRefresh; + uint8_t skipRenderThisRound; +} MenuNavResult; + +static MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal) +{ + MenuNavResult result; + tagPMenuItem ptCurrent; + tagPMenuItem ptHead; + tagPMenuItem *ptRoute; + + result.needRefresh = 0; + result.skipRenderThisRound = 0; + + ptCurrent = navState->ptCurrent; + ptHead = navState->ptHead; + ptRoute = navState->ptRoute; + + switch (keyVal) + { + case KEY_F1: + case KEY_F2: + break; + case KEY_U: + ptCurrent = ptCurrent->links.before; + result.needRefresh = 1; + break; + case KEY_D: + ptCurrent = ptCurrent->links.behind; + result.needRefresh = 1; + break; + case KEY_L: + ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1]; + if (ptCurrent->menuDef.byClass == 0) + { + if (ptCurrent->rect.wPos == 1) + { + ptCurrent = ptCurrent->links.before->links.before; + } + else + { + ptCurrent = ptCurrent->links.before; + } + ptCurrent = ptCurrent->links.lower; + } + result.needRefresh = 1; + break; + case KEY_R: + case KEY_ENT: + if (ptCurrent->links.lower != NULL) + { + ptCurrent = ptCurrent->links.lower; + result.needRefresh = 1; + } + else if (ptCurrent->menuDef.pfnWinProc != NULL) + { + ptCurrent->menuDef.pfnWinProc(); + } + break; + case KEY_ESC: + if (ptCurrent->menuDef.byClass == 1) + { + navState->pt0Level = ptHead; + navState->ptRoute[0] = ptHead; + navState->ptCurrent = ptHead->links.lower; + navState->ptCurBak = navState->ptCurrent; + navState->ptRoute[1] = navState->ptCurrent; + result.skipRenderThisRound = 1; + return result; + } + ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1]; + result.needRefresh = 1; + break; + default: + break; + } + + navState->ptCurrent = ptCurrent; + return result; +} + +static void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem) +{ + tagPMenuItem ptIndex; + + if (navState->ptCurBak == navState->ptCurrent) + { + return; + } + + ptIndex = navState->ptCurrent; + for (uint32_t index = 0; index < maxItem; index++) + { + if (ptIndex->links.higher == NULL) + { + ptIndex = ptIndex->links.before; + } + else + { + ptIndex = ptIndex->links.higher; + navState->ptRoute[ptIndex->menuDef.byClass] = ptIndex; + } + if (ptIndex->menuDef.byClass == 0) + { + break; + } + } + navState->ptRoute[navState->ptCurrent->menuDef.byClass] = navState->ptCurrent; +} + +static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav) +{ + nav->ptHead = ctrl->ptHead; + nav->ptCurrent = ctrl->ptCurrent; + nav->ptRoute[0] = ctrl->ptRoute[0]; + nav->ptRoute[1] = ctrl->ptRoute[1]; + nav->ptRoute[2] = ctrl->ptRoute[2]; + nav->ptRoute[3] = ctrl->ptRoute[3]; + nav->ptCurBak = ctrl->ptCurBak; + nav->pt0Level = ctrl->pt0Level; +} + +static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav) +{ + ctrl->ptHead = nav->ptHead; + ctrl->ptCurrent = nav->ptCurrent; + ctrl->ptRoute[0] = nav->ptRoute[0]; + ctrl->ptRoute[1] = nav->ptRoute[1]; + ctrl->ptRoute[2] = nav->ptRoute[2]; + ctrl->ptRoute[3] = nav->ptRoute[3]; + ctrl->ptCurBak = nav->ptCurBak; + ctrl->pt0Level = nav->pt0Level; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPresenter_RenderByState + * 功能: + * 根据当前菜单状态差异选择渲染策略(整页刷新 / 同层局部刷新 / 单层补绘)。 + * + * 参数: + * presenter - 菜单 Presenter 实例,内部持有 menuCtrl 与 view 接口 + * + * 边界处理: + * - 本函数不做空指针校验,默认由调用链保证 presenter/view 有效。 + * - 当菜单路由发生跨 0 级变化时,强制走整页刷新,避免多层残影。 + * - 仅在“同层且同父”场景使用局部反显更新,其他变化按层级关系降级处理。 + * + * 说明: + * - 该函数是 Presenter 的渲染决策核心,不直接参与菜单导航计算。 + * - 决策顺序如下: + * 1) 若 pt0Level 与 route[0] 不一致: + * 说明当前顶层上下文已切换,先同步 pt0Level/ptCurBak,再 full_refresh。 + * 2) 否则若 ptCurBak 与 ptCurrent 不一致: + * a) 同层 + 同父:调用 update_selection_same_level,执行“旧选中恢复 + 新选中高亮” + * b) 新层级 >= 旧层级:调用 show_other_level,仅补绘受影响层 + * c) 其他(通常是回退到更高层):调用 full_refresh,保证界面一致性 + * 3) 渲染后统一将 ptCurBak 更新为 ptCurrent,作为下一轮差异比较基准。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPresenter_RenderByState(MenuPresenter *presenter) +{ + tagMenuCtrl *menuCtrl = &presenter->menuCtrl; + MenuView *view = presenter->view; + + if (menuCtrl->pt0Level != menuCtrl->ptRoute[0]) + { + menuCtrl->pt0Level = menuCtrl->ptRoute[0]; + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + presenter->view->full_refresh(presenter->view, menuCtrl, MODE_NONE); + } + else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent) + { + if ((menuCtrl->ptCurrent->menuDef.byClass == menuCtrl->ptCurBak->menuDef.byClass) && + (menuCtrl->ptCurrent->menuDef.byClass > 0) && + (menuCtrl->ptCurrent->links.higher == menuCtrl->ptCurBak->links.higher)) + { + view->update_selection_same_level(view, menuCtrl->ptCurBak, menuCtrl->ptCurrent); + } + else if (menuCtrl->ptCurrent->menuDef.byClass >= menuCtrl->ptCurBak->menuDef.byClass) + { + view->update_selection_new_level(view, menuCtrl->ptRoute[menuCtrl->ptCurrent->menuDef.byClass - 1], menuCtrl->ptRoute[menuCtrl->ptCurrent->menuDef.byClass]); + } + else + { + presenter->view->full_refresh(presenter->view, menuCtrl, MODE_NONE); + } + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + } +} + +void MenuPresenter_Init(MenuPresenter *presenter, menu_model_t *model, menu_view_t *view) +{ + presenter->dspCtrl.bFirst = 0; + memset(&presenter->menuCtrl, 0, sizeof(presenter->menuCtrl)); + presenter->model = model; + presenter->view = view; + + + presenter->dspCtrl.bFirst = 1; + presenter->menuCtrl.ptHead = &presenter->model->menuItems[0]; + presenter->menuCtrl.pt0Level = &presenter->model->menuItems[0]; + presenter->menuCtrl.ptCurrent = presenter->menuCtrl.ptHead->links.lower; + presenter->menuCtrl.ptCurBak = presenter->menuCtrl.ptCurrent; + + presenter->menuCtrl.ptRoute[0] = &presenter->model->menuItems[0]; + presenter->menuCtrl.ptRoute[1] = presenter->menuCtrl.ptCurrent; + presenter->menuCtrl.ptRoute[2] = presenter->menuCtrl.ptCurrent; + presenter->menuCtrl.ptRoute[3] = presenter->menuCtrl.ptCurrent; +} + +void MenuPresenter_Refresh(MenuPresenter *presenter) +{ + if (presenter->dspCtrl.bFirst) + { + presenter->dspCtrl.bFirst = 0; + presenter->view->full_refresh(presenter->view, &presenter->menuCtrl, MODE_NONE); + } + else + { + MenuPresenter_RenderByState(presenter); + } +} + +void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal) +{ + MenuNavState navState; + MenuNavResult navResult; + + if (presenter->dspCtrl.bFirst) + { + MenuPresenter_Refresh(presenter); + return; + } + + MenuPresenter_FillNavState(&presenter->menuCtrl, &navState); + navResult = MenuNavigator_ProcessKey(&navState, keyVal); + MenuPresenter_ApplyNavState(&presenter->menuCtrl, &navState); + + if (navResult.skipRenderThisRound) + { + return; + } + + if (navResult.needRefresh) + { + MenuNavState rebuildState; + MenuPresenter_FillNavState(&presenter->menuCtrl, &rebuildState); + MenuNavigator_RebuildRoute(&rebuildState, presenter->model->maxItem); + MenuPresenter_ApplyNavState(&presenter->menuCtrl, &rebuildState); + MenuPresenter_Refresh(presenter); + } +} diff --git a/src/Drv/pages/menu/presenter.h b/src/Drv/pages/menu/presenter.h new file mode 100644 index 0000000..32899e1 --- /dev/null +++ b/src/Drv/pages/menu/presenter.h @@ -0,0 +1,33 @@ +#ifndef MENU_PRESENTER_H +#define MENU_PRESENTER_H + +#include "display.h" +#include "model.h" +#include "view.h" + +typedef struct menu_presenter_t menu_presenter_t; + +typedef struct +{ + uint8_t bFirst; +} tagDspCtrl; + + +struct menu_presenter_t +{ + tagDspCtrl dspCtrl; + tagMenuCtrl menuCtrl; + menu_model_t *model; + menu_view_t *view; +}; + +typedef menu_presenter_t MenuPresenter; + +void MenuPresenter_Init(MenuPresenter *presenter, + menu_model_t *model, + menu_view_t *view); + +void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal); +void MenuPresenter_Refresh(MenuPresenter *presenter); + +#endif diff --git a/src/Drv/pages/menu/view.c b/src/Drv/pages/menu/view.c new file mode 100644 index 0000000..5b15a6a --- /dev/null +++ b/src/Drv/pages/menu/view.c @@ -0,0 +1,313 @@ +#include +#include +#include "view.h" + + +static const PageRenderPort *s_port = NULL; +/* ------------------------------------------------------------------------- + * 函数名: MenuView_DrawMeitou + * 功能: + * 绘制菜单标题装饰线(“眉头”样式):中间横线 + 左右两端斜线。 + * + * 参数: + * view - 菜单视图对象,提供底层渲染端口 + * yStart - 装饰线基准 Y 坐标(横线所在行) + * width - 线宽参数(传递给 line_h/line) + * + * 边界处理: + * - 本函数不做空指针判定,调用方需保证 view 与 s_port 有效。 + * - 坐标范围合法性由上层布局计算保证。 + * + * 说明: + * - 颜色统一使用字体前景色(get_color_font),确保装饰与文字风格一致。 + * - 绘制顺序: + * 1) 中间横线(x:16~144) + * 2) 左斜线(8, yStart-8 -> 16, yStart) + * 3) 右斜线(144, yStart -> 152, yStart-8) + * - 该函数用于视觉分隔与层次强调,不改变菜单状态数据。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width) +{ + uint8_t fontColor = s_port->get_color_font(); + s_port->line_h(16, 144, yStart, width, fontColor); + s_port->line(8, yStart - 8, 16, yStart, width, fontColor); + s_port->line(144, yStart, 152, yStart - 8, width, fontColor); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_DrawBoundaryBox + * 功能: + * 绘制矩形边界框:先清空内部区域,再绘制四条边线。 + * + * 参数: + * view - 菜单视图对象,提供像素与线段绘制接口 + * leftX - 左边界 X + * topY - 上边界 Y + * rightX - 右边界 X + * bottomY - 下边界 Y + * + * 边界处理: + * - 本函数不校验 view/port 空指针,调用方需保证有效。 + * - 默认要求 leftX <= rightX、topY <= bottomY; + * 若参数异常,像素循环或画线结果由底层驱动行为决定。 + * + * 说明: + * - 背景填充:逐像素将内部区域置为背景色(get_color_back), + * 避免残影或旧内容叠加。 + * - 边框绘制:统一使用字体前景色(get_color_font),绘制上/左/下/右四边。 + * - 下边框使用 rightX + 1,目的是与其它边界在视觉上闭合对齐。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY) +{ + uint8_t backColor = s_port->get_color_back(); + uint8_t fontColor = s_port->get_color_font(); + for (uint16_t y = topY; y < bottomY; y++) + { + for (uint16_t x = leftX; x < rightX; x++) + { + s_port->set_pixel(x, y, backColor); + } + } + + s_port->line_h(leftX, rightX, topY, 1, fontColor); + s_port->line_v(topY, bottomY, leftX, 1, fontColor); + s_port->line_h(leftX, rightX + 1, bottomY, 1, fontColor); + s_port->line_v(topY, bottomY, rightX, 1, fontColor); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_UpdateSelectionNewLevel + * 功能: + * 在“进入新层级”场景下,重绘目标层级窗口内容(边框 + 文本) + * 并对当前选中项执行反显高亮。 + * + * 参数: + * view - 菜单视图对象,提供底层渲染接口 + * currentLevel - 当前层级的“层级框拥有者”节点,提供窗口矩形与数量信息 + * nextLevel - 当前层级内的选中节点(也是该层列表起始遍历节点) + * + * 边界处理: + * - 本函数不做空指针判定,调用方需保证 view/currentLevel/nextLevel 有效。 + * - 默认要求 currentLevel 与 nextLevel 来自同一层级链路,且 behind 链可遍历。 + * - 当 currentLevel->rect.wNum 为 0 时,文本循环不执行, + * 但边框仍会被绘制。 + * + * 说明: + * - 绘制步骤: + * 1) 使用 currentLevel->rect 绘制层级边框区域 + * 2) 从 nextLevel 起,按 wNum 数量沿 behind 链逐项绘制名称 + * 3) 基于 nextLevel 的 wPos 计算行区域并执行 invert 高亮 + * - 坐标策略: + * wPosX = currentLevel->rect.wSPosX + 4(文本左内边距) + * wPosY = currentLevel->rect.wSPosY + item->rect.wPos * LINE_HEIGHT + 3 + * - 该函数对应“新层级内容补绘”路径,与同层移动的局部反显函数职责分离。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ + + static void MenuView_UpdateSelectionNewLevel(MenuView *view, const tagPMenuItem currentLevel, const tagPMenuItem nextLevel) + { + tagPMenuItem ptIndex; + + uint16_t wPosX; + uint16_t wPosY; + /* 1. 绘制当前层级的边界框 */ + MenuView_DrawBoundaryBox(view, currentLevel->rect.wSPosX, currentLevel->rect.wSPosY, currentLevel->rect.wEPosX, currentLevel->rect.wEPosY); + /*2. 获取当前层级选中菜单项,并计算X起始坐标*/ + ptIndex = nextLevel; + /* 计算当前菜单项的 X 坐标,对于下一层级起始 X 坐标都是一致的 */ + wPosX = currentLevel->rect.wSPosX + 4; + + /* 3. 绘制当前层级的所有菜单项 */ + for (uint16_t index = 0; index < currentLevel->rect.wNum; index++) + { + wPosY = currentLevel->rect.wSPosY + ptIndex->rect.wPos * LINE_HEIGHT + 3; + s_port->show_str(wPosX, wPosY, ptIndex->menuDef.byName); + /* 移动到下一个菜单项 */ + ptIndex = ptIndex->links.behind; + } + + /* 4. 绘制当前选中项的反显 */ + wPosY = currentLevel->rect.wSPosY + nextLevel->rect.wPos * LINE_HEIGHT + 3; + s_port->invert(currentLevel->rect.wSPosX + 2, wPosY - 1, currentLevel->rect.wEPosX - 2, wPosY + LINE_HEIGHT); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_ShowTopLevel + * 功能: + * 绘制菜单页顶部区域(0 级标题栏):清空标题背景、绘制装饰线, + * 并按传入模式显示顶部提示文案。 + * + * 参数: + * view - 菜单视图对象,提供渲染端口能力 + * mode - 当前需要显示的运行模式(MenuMode) + * + * 边界处理: + * - 本函数不做 view/port 空指针校验,调用方需保证对象已正确初始化。 + * - 模式值若不在已定义枚举分支内,走默认分支显示“当前模式: 无模式”。 + * + * 说明: + * - 先清空顶部区域(y: 0~32),避免旧文案残留。 + * - 再调用 MenuView_DrawMeitou() 绘制标题装饰线,保持页面视觉一致性。 + * - 模式文案映射: + * MODE_OVERFLOW_PROTECTION -> "当前模式: 过流保护" + * MODE_LOCAL_FEEDER_SEGMENT -> "就地馈线: 分段模式" + * MODE_LOCAL_FEEDER_CONTACT -> "就地馈线: 联络模式" + * 其他/未设置 -> "当前模式: 无模式" + * - 本函数只负责“顶部区域”渲染,不处理菜单列表区内容刷新。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_ShowTopLevel(MenuView *view, MenuMode mode) +{ + uint16_t lcdSizeX = s_port->get_size_x(); + s_port->fill_rect(0, 0, lcdSizeX - 1, 32, s_port->get_color_back()); + MenuView_DrawMeitou(view, 16, 2); + /* 在 0 级菜单标题栏下方显示当前运行模式提示文字 */ + if (mode == MODE_OVERFLOW_PROTECTION) + { + s_port->show_str(16, 20, (uint8_t *)"当前模式: 过流保护"); + } + else if (mode == MODE_LOCAL_FEEDER_SEGMENT) + { + s_port->show_str(16, 20, (uint8_t *)"就地馈线: 分段模式"); + } + else if (mode == MODE_LOCAL_FEEDER_CONTACT) + { + s_port->show_str(16, 20, (uint8_t *)"就地馈线: 联络模式"); + } + else + { + s_port->show_str(16, 20, (uint8_t *)"当前模式: 无模式"); + } +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_UpdateSelectionSameLevel + * 功能: + * 在“同层同父节点”移动场景下执行局部刷新,仅更新旧/新选中行的反显状态, + * 避免整块区域重绘带来的闪烁。 + * + * 参数: + * view - 菜单视图对象,提供底层 invert 绘制能力 + * oldItem - 变更前的选中菜单项(旧焦点) + * newItem - 变更后的选中菜单项(新焦点) + * + * 边界处理: + * - 本函数不做空指针与父链校验,调用方需保证: + * 1) view/s_port 有效 + * 2) oldItem/newItem 非空 + * 3) oldItem->links.higher 非空且与 newItem 属于同一父节点 + * - 若上述前置条件不满足,坐标计算与反显结果将不可预期。 + * + * 说明: + * - 该函数是 Presenter 在“同层同父移动”场景(典型 U/D)的优化渲染路径。 + * - 通过两次 invert 完成“旧选中恢复 + 新选中高亮”: + * 1) 对旧行区域 invert:撤销旧高亮 + * 2) 对新行区域 invert:施加新高亮 + * - 坐标基准来自 oldItem 的父节点(levelRectOwner = oldItem->links.higher): + * posY = levelRectOwner->rect.wSPosY + (item->rect.wPos - 1) * LINE_HEIGHT + 3 + * - X 方向使用边框内缩(+2/-2),避免覆盖边框线条,保持视觉完整性。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_UpdateSelectionSameLevel(MenuView *view, const tagPMenuItem oldItem, const tagPMenuItem newItem) +{ + tagPMenuItem levelRectOwner = oldItem->links.higher; + + uint16_t leftX = (uint16_t)(levelRectOwner->rect.wSPosX + 2); + uint16_t rightX = (uint16_t)(levelRectOwner->rect.wEPosX - 2); + uint16_t oldPosY = (uint16_t)(levelRectOwner->rect.wSPosY + oldItem->rect.wPos * LINE_HEIGHT + 3); + uint16_t newPosY = (uint16_t)(levelRectOwner->rect.wSPosY + newItem->rect.wPos * LINE_HEIGHT + 3); + + /* 旧选中行反显恢复 + 新选中行反显高亮 */ + s_port->invert(leftX, (uint16_t)(oldPosY - 1), rightX, (uint16_t)(oldPosY + LINE_HEIGHT)); + s_port->invert(leftX, (uint16_t)(newPosY - 1), rightX, (uint16_t)(newPosY + LINE_HEIGHT)); +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuView_FullRefresh + * 功能: + * 执行菜单页整页刷新:清空工作区、重绘顶部标题区域、按当前层级路径重绘各级菜单框。 + * + * 参数: + * view - 菜单视图对象,提供布局参数与绘制能力 + * menuCtrl - 菜单运行时控制结构,提供当前层级与路由链信息 + * mode - 顶部标题显示使用的运行模式 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 view/menuCtrl/s_port 均有效。 + * - 当 byClass 为 0 时,for 循环不进入,仅刷新顶部区域。 + * - 默认要求 ptRoute[index] 与 ptRoute[index + 1] 在有效层级范围内可访问。 + * + * 说明: + * - 刷新步骤: + * 1) 使用背景色清空菜单工作区域(MENU_YMIN ~ menuYMax) + * 2) 调用 MenuView_ShowTopLevel() 重绘顶部模式文案与装饰 + * 3) 按当前菜单层级(byClass)逐层调用 MenuView_UpdateSelectionNewLevel() + * 重绘各层边框、文本和选中态 + * - 该函数用于“整页一致性恢复”场景(如跨层回退、顶层切换等)。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuView_FullRefresh(MenuView *view, const tagMenuCtrl *menuCtrl, MenuMode mode) +{ + uint8_t backColor = s_port->get_color_back(); + uint16_t lcdSizeX = s_port->get_size_x(); + + s_port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, MENU_YMAX, backColor); + + MenuView_ShowTopLevel(view, mode); + for (uint8_t index = 0; index < menuCtrl->ptCurrent->menuDef.byClass; index++) + { + MenuView_UpdateSelectionNewLevel(view, menuCtrl->ptRoute[index], menuCtrl->ptRoute[index + 1]); + } +} +/* ------------------------------------------------------------------------- + * 函数名: MenuView_Init + * 功能: + * 初始化菜单视图对象 MenuView,准备渲染端口引用与布局参数, + * 并设置当前模式状态的默认值以及渲染入口函数指针。 + * + * 参数: + * view - 菜单视图实例地址(由调用方分配,函数内填充其字段) + * + * 边界处理: + * - 本函数不校验 view 空指针,调用方需保证 view 非空。 + * - 渲染端口的物理 init 不在视图内执行; + * 由入口(main)统一完成屏幕初始化(符合 PageRenderPort 共享策略)。 + * + * 说明: + * - 初始化内容包含: + * 1) s_port = PageRenderer_Lcd():绑定 LCD 渲染端口表指针 + * 4) 设置当前模式为 MODE_NONE,避免进入绘制前读取未定义值 + * 5) 视图层仅保留绘制原子动作,刷新策略由 Presenter 调度 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuView_Init(MenuView *view) +{ + s_port = PageRenderer_Lcd(); + view->update_selection_new_level = MenuView_UpdateSelectionNewLevel; + view->update_selection_same_level = MenuView_UpdateSelectionSameLevel; + view->full_refresh = MenuView_FullRefresh; +} + +#ifdef UNIT_TEST +const PageRenderPort *MenuView_GetPortForTest(void) +{ + return s_port; +} +#endif + diff --git a/src/Drv/pages/menu/view.h b/src/Drv/pages/menu/view.h new file mode 100644 index 0000000..88620a4 --- /dev/null +++ b/src/Drv/pages/menu/view.h @@ -0,0 +1,44 @@ +#ifndef MENU_VIEW_H +#define MENU_VIEW_H + +#include "types.h" +#include "display.h" +#include "def.h" +#include "../global/renderer_lcd.h" + + + + +typedef struct menu_view_t menu_view_t; + +/* ------------------------------------------------------------------------- + * 结构体名: menu_view_t + * 功能: + * 菜单页面 View 层对象,封装对外绘制接口函数指针。 + * + * 字段说明: + * update_selection_new_level - 新层级切换时的层级窗口重绘与高亮更新 + * update_selection_same_level - 同层移动时的局部反显更新(降低闪烁) + * full_refresh - 整页刷新入口(顶部 + 多层菜单内容) + * + * 说明: + * - Presenter 通过该结构体调用 View 能力,避免直接依赖 view.c 内部实现细节。 + * - 函数指针在 MenuView_Init() 中完成绑定。 + * ------------------------------------------------------------------------- */ +struct menu_view_t +{ + void (*update_selection_new_level)(struct menu_view_t *self, const tagPMenuItem currentLevel, const tagPMenuItem nextLevel); + void (*update_selection_same_level)(struct menu_view_t *self, const tagPMenuItem oldItem, const tagPMenuItem newItem); + void (*full_refresh)(struct menu_view_t *self, const tagMenuCtrl *menuCtrl, MenuMode mode); +}; + +typedef menu_view_t MenuView; + + +void MenuView_Init(MenuView *view); + +#ifdef UNIT_TEST +const PageRenderPort *MenuView_GetPortForTest(void); +#endif + +#endif diff --git a/src/Drv/pages/page.h b/src/Drv/pages/page.h new file mode 100644 index 0000000..e435e9b --- /dev/null +++ b/src/Drv/pages/page.h @@ -0,0 +1,125 @@ +#ifndef PAGE_H +#define PAGE_H + +#include "types.h" +/* ------------------------------------------------------------------------- + * 枚举名: page_id_t + * 作用: + * 定义系统内可导航页面的逻辑标识(Page ID)。 + * + * 取值说明: + * PAGE_ID_NONE - 无效页面 ID / 未初始化占位值 + * PAGE_ID_MENU - 菜单页 ID(当前主运行页) + * PAGE_ID_APP_INFO - 预留页面 ID(当前版本可注册与否由上层决定) + * PAGE_ID_MAX - 上界哨兵,不可作为有效页面 ID 使用 + * + * 使用约束: + * - PageManager_Register() 会校验 page_id,PAGE_ID_NONE 与 >= PAGE_ID_MAX + * 均视为非法值。 + * ------------------------------------------------------------------------- */ +typedef enum +{ + PAGE_ID_NONE = 0, + PAGE_ID_MENU = 1, + PAGE_ID_APP_INFO = 2, /* 预留ID:当前版本未注册运行 */ + PAGE_ID_MAX +} page_id_t; + +/* ------------------------------------------------------------------------- + * 枚举名: page_event_type_t + * 作用: + * 定义页面层可分发的事件类型。 + * + * 当前约定: + * PAGE_EVENT_KEY - 按键输入事件 + * + * 扩展说明: + * - 后续可按需扩展触摸、定时器、通信消息等事件类型。 + * ------------------------------------------------------------------------- */ +typedef enum +{ + PAGE_EVENT_KEY = 1 +} page_event_type_t; + +/* ------------------------------------------------------------------------- + * 结构体名: input_event_t + * 作用: + * 页面事件分发的数据载体,由 PageManager_DispatchEvent() 传入页面 on_event。 + * + * 字段说明: + * type - 事件类型,取值来自 page_event_type_t + * keyVal - 按键值(当 type 为 PAGE_EVENT_KEY 时有效) + * + * 使用约束: + * - 调用方应保证 type/keyVal 与事件来源一致; + * - 页面 on_event 可基于该结构返回 EVENT_HANDLED / EVENT_UNHANDLED。 + * ------------------------------------------------------------------------- */ +typedef struct +{ + uint8_t type; + uint8_t keyVal; +} input_event_t; + +/* ------------------------------------------------------------------------- + * 枚举名: event_result_t + * 作用: + * 定义页面事件处理结果,用于控制事件链是否继续兜底处理。 + * + * 取值说明: + * EVENT_UNHANDLED - 页面未消费事件;允许交由管理器执行全局兜底逻辑 + * EVENT_HANDLED - 页面已消费事件;事件链终止 + * ------------------------------------------------------------------------- */ +typedef enum +{ + EVENT_UNHANDLED = 0, + EVENT_HANDLED = 1 +} event_result_t; + +/* 前置声明:支持在回调签名中使用 page_t* */ +typedef struct page_t page_t; + +/* ------------------------------------------------------------------------- + * 结构体名: page_t + * 作用: + * 页面抽象基元,描述“一个可被 PageManager 管理的页面实例”。 + * + * 字段分组: + * 1) 基础元信息 + * page_id - 页面逻辑标识 + * is_cached - 是否缓存页(1: Pop 后不销毁;0: Pop 后可销毁) + * is_created - 是否已执行过 on_create(由管理器维护) + * + * 2) 生命周期与事件回调 + * on_create - 首次创建时调用 + * on_enter - 页面进入前台时调用 + * on_exit - 页面离开前台时调用 + * on_destroy - 页面销毁时调用(常用于非缓存页) + * on_event - 输入事件处理回调 + * on_loop - 周期循环回调 + * + * 3) 三层对象挂载指针(MVP) + * presenter / view / model - 页面内部层对象地址(可为空,按页面实现决定) + * + * 生命周期约定(由 PageManager 驱动): + * - Push 到新页面时:旧页 on_exit -> 新页(必要时 on_create)-> 新页 on_enter + * - Pop 回退时:当前页 on_exit ->(非缓存页可 on_destroy)-> 新栈顶 on_enter + * ------------------------------------------------------------------------- */ +struct page_t +{ + page_id_t page_id; + uint8_t is_cached; + uint8_t is_created; + + void (*on_create)(page_t *page); + void (*on_enter)(page_t *page); + void (*on_exit)(page_t *page); + void (*on_destroy)(page_t *page); + event_result_t (*on_event)(page_t *page, input_event_t *event); + void (*on_loop)(page_t *page); + + void *presenter; + void *view; + void *model; +}; + +#endif diff --git a/src/Drv/pages/page_manager.c b/src/Drv/pages/page_manager.c new file mode 100644 index 0000000..96ed57a --- /dev/null +++ b/src/Drv/pages/page_manager.c @@ -0,0 +1,448 @@ +#include +#include + +#include "page_manager.h" +#include "../key.h" + +/* 默认全局页面管理器实例(便于主流程直接使用) */ +static page_manager_t s_pageManager; + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_GlobalEventHandle + * 功能: + * 页面管理器的全局事件兜底处理入口,用于处理未被页面消费的通用按键逻辑。 + * + * 参数: + * event - 输入事件指针 + * + * 边界处理: + * - 若 event 为 NULL,函数直接返回。 + * - 若事件类型不是 PAGE_EVENT_KEY,函数直接返回,不做处理。 + * - 仅当 keyVal 为 KEY_ESC 时触发返回上一页,其它按键忽略。 + * + * 说明: + * - 该函数通常由 PageManager_DispatchEvent 在页面返回 + * EVENT_UNHANDLED 时调用。 + * - 当前全局策略为“ESC = 返回”:内部通过 PageManager_Pop() + * 执行页面栈回退。 + * - 设计目标是在不侵入页面私有逻辑的前提下,提供统一系统级快捷行为。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void PageManager_GlobalEventHandle(input_event_t *event) +{ + ASSERT((event == NULL) || (event->type != PAGE_EVENT_KEY)); + if ((event == NULL) || (event->type != PAGE_EVENT_KEY)) + { + return; + } + + if (event->keyVal == KEY_ESC) + { + (void)PageManager_Pop(); + } +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Init + * 功能: + * 初始化单例页面管理器 s_pageManager,使其进入“空栈、可注册”的初始状态。 + * + * 参数: + * 无 + * + * 边界处理: + * - 本函数无外部入参,不涉及空指针校验。 + * - 通过 memset 全量清零,确保历史运行残留状态(栈、注册表、计数)被彻底复位。 + * + * 说明: + * - stack_top 采用 -1 表示页面栈为空,这是后续 Push/GetTop/Depth 的统一判定基准。 + * - 该函数通常在系统启动阶段调用一次;若重复调用,将清空当前页面管理上下文。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void PageManager_Init(void) +{ + memset(&s_pageManager, 0, sizeof(s_pageManager)); + s_pageManager.stack_top = -1; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Register + * 功能: + * 将页面对象注册到页面管理器的注册表中,供后续按 page_id 查找与导航。 + * + * 参数: + * page - 待注册页面指针 + * + * 边界处理: + * - 若 page 为 NULL,返回 PAGE_MANAGER_ERR_NULL_PARAM,不执行注册。 + * - 若 page_id 非法(PAGE_ID_NONE 或超出 PAGE_ID_MAX),返回 PAGE_MANAGER_ERR_INVALID_ID。 + * - 若注册表已满(page_count >= MAX_PAGE_COUNT),返回 PAGE_MANAGER_ERR_REGISTRY_FULL。 + * + * 说明: + * - 若检测到相同 page_id 已存在,不新增条目,而是覆盖旧指针(热更新语义)。 + * - 新页面按 page_count 尾插,成功后 page_count 自增 1。 + * + * 返回值: + * - PAGE_MANAGER_OK : 注册成功(含覆盖更新成功) + * - PAGE_MANAGER_ERR_NULL_PARAM : 参数无效 + * - PAGE_MANAGER_ERR_INVALID_ID : page_id 非法 + * - PAGE_MANAGER_ERR_REGISTRY_FULL : 注册表容量不足 + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_Register(page_t *page) +{ + ASSERT(page == NULL); + if (page == NULL) + { + return PAGE_MANAGER_ERR_NULL_PARAM; + } + ASSERT((page->page_id == PAGE_ID_NONE) || (page->page_id >= PAGE_ID_MAX)); + if ((page->page_id == PAGE_ID_NONE) || (page->page_id >= PAGE_ID_MAX)) + { + return PAGE_MANAGER_ERR_INVALID_ID; + } + + for (uint8_t i = 0; i < s_pageManager.page_count; i++) + { + /* 支持重复注册:同 page_id 视为更新页面指针 */ + if (s_pageManager.page_registry[i]->page_id == page->page_id) + { + s_pageManager.page_registry[i] = page; + return PAGE_MANAGER_OK; + } + } + + ASSERT(s_pageManager.page_count >= MAX_PAGE_COUNT); + if (s_pageManager.page_count >= MAX_PAGE_COUNT) + { + return PAGE_MANAGER_ERR_REGISTRY_FULL; + } + + s_pageManager.page_registry[s_pageManager.page_count] = page; + s_pageManager.page_count++; + return PAGE_MANAGER_OK; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Find + * 功能: + * 在管理器的页面注册表中按 page_id 线性查找,返回对应的 page_t 指针。 + * + * 参数: + * pageId - 待查找的页面标识 + * + * 边界处理: + * - 若注册表为空(page_count 为 0),循环不执行,返回 NULL。 + * - 若不存在匹配的 page_id,返回 NULL。 + * + * 说明: + * - 查找顺序与注册顺序一致;同 id 在 Register 中已保证唯一或覆盖更新。 + * - 常与 PageManager_Navigate 配合:先 Find 再 Push,上层也可直接 Find 获取页面对象。 + * + * 返回值: + * - 命中时返回注册表中的 page_t 指针 + * - 未命中返回 NULL + * ------------------------------------------------------------------------- */ +page_t *PageManager_Find(page_id_t pageId) +{ + for (uint8_t i = 0; i < s_pageManager.page_count; i++) + { + if (s_pageManager.page_registry[i]->page_id == pageId) + { + return s_pageManager.page_registry[i]; + } + } + return NULL; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_GetTop + * 功能: + * 返回当前页面栈栈顶元素,即当前“前台/可见”页面对应的 page_t 指针。 + * + * 参数: + * 无 + * + * 边界处理: + * - stack_top < 0 表示空栈,返回 NULL。 + * - 正常栈顶索引由 Push/Pop 维护,假定栈槽位与 stack_top 一致。 + * + * 说明: + * - DispatchEvent、Loop、Navigate 等逻辑均以栈顶页为“当前页”。 + * - 不修改栈状态,仅查询。 + * + * 返回值: + * - 非空栈时返回栈顶 page_t 指针 + * - 空栈时返回 NULL + * ------------------------------------------------------------------------- */ +page_t *PageManager_GetTop(void) +{ + if (s_pageManager.stack_top < 0) + { + return NULL; + } + return s_pageManager.page_stack[s_pageManager.stack_top]; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Push + * 功能: + * 将指定页面压入页面栈并切换为当前页:必要时触发旧页 on_exit、新页 on_create/on_enter。 + * + * 参数: + * newPage - 要入栈并成为栈顶的目标页面指针 + * + * 边界处理: + * - 若 newPage 为 NULL,返回 PAGE_MANAGER_ERR_NULL_PARAM。 + * - 若栈已满(stack_top >= MAX_PAGE_STACK_DEPTH - 1),返回 PAGE_MANAGER_ERR_STACK_FULL。 + * - 若当前栈顶与 newPage 的 page_id 相同,视为重复导航,直接返回 PAGE_MANAGER_OK, + * 不再次调用 on_exit/on_enter(幂等)。 + * + * 说明: + * - 切换顺序:旧栈顶 on_exit(若存在)-> 新页入栈 -> + * 若 is_created 为 0 则 on_create 并置 is_created,再 on_enter。 + * - 页面“创建一次”语义由 is_created 控制;非缓存页在 Pop 时可销毁并重置 is_created。 + * + * 返回值: + * - PAGE_MANAGER_OK : 成功(含幂等重复入同一逻辑页) + * - PAGE_MANAGER_ERR_NULL_PARAM : 参数无效 + * - PAGE_MANAGER_ERR_STACK_FULL : 栈满 + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_Push(page_t *newPage) +{ + page_t *currentTop; + ASSERT(newPage == NULL); + if (newPage == NULL) + { + return PAGE_MANAGER_ERR_NULL_PARAM; + } + ASSERT(s_pageManager.stack_top >= (MAX_PAGE_STACK_DEPTH - 1)); + if (s_pageManager.stack_top >= (MAX_PAGE_STACK_DEPTH - 1)) + { + return PAGE_MANAGER_ERR_STACK_FULL; + } + + currentTop = PageManager_GetTop(); + ASSERT((currentTop != NULL) && (currentTop->page_id == newPage->page_id)); + if ((currentTop != NULL) && (currentTop->page_id == newPage->page_id)) + { + /* 重复导航到当前页面:按幂等处理 */ + return PAGE_MANAGER_OK; + } + ASSERT((currentTop != NULL) && (currentTop->on_exit != NULL)); + if ((currentTop != NULL) && (currentTop->on_exit != NULL)) + { + currentTop->on_exit(currentTop); + } + + s_pageManager.stack_top++; + s_pageManager.page_stack[s_pageManager.stack_top] = newPage; + + if (!newPage->is_created) + { + /* 页面只创建一次(除非在 Pop 时被销毁并重置) */ + if (newPage->on_create != NULL) + { + newPage->on_create(newPage); + } + newPage->is_created = 1; + } + + if (newPage->on_enter != NULL) + { + newPage->on_enter(newPage); + } + return PAGE_MANAGER_OK; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Pop + * 功能: + * 弹出当前栈顶页面并回到下一层:触发当前页 on_exit,非缓存页可 on_destroy, + * 再对新栈顶调用 on_enter。 + * + * 参数: + * 无 + * + * 边界处理: + * - 若 stack_top <= 0(仅余底页或空栈),不允许继续弹出, + * 返回 PAGE_MANAGER_ERR_STACK_BOTTOM。 + * - 当前页指针为 NULL 时跳过对应回调(防御性,正常不应出现)。 + * + * 说明: + * - 设计为至少保留栈底一页:不能无限 Pop 到空栈,故 stack_top <= 0 即失败。 + * - 非缓存页(is_cached == 0)在 Pop 后会 on_destroy 且 is_created 清零,下次 Push 会重新 on_create。 + * - 缓存页仅 on_exit,不销毁,便于保留状态。 + * - 全局 ESC 兜底通过本函数回退上一页。 + * + * 返回值: + * - PAGE_MANAGER_OK : 出栈成功,新栈顶已 on_enter(若实现) + * - PAGE_MANAGER_ERR_STACK_BOTTOM : 无法弹出(已到底或空栈) + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_Pop(void) +{ + page_t *currentPage; + page_t *newTop; + + ASSERT(s_pageManager.stack_top <= 0); + if (s_pageManager.stack_top <= 0) + { + return PAGE_MANAGER_ERR_STACK_BOTTOM; + } + + currentPage = s_pageManager.page_stack[s_pageManager.stack_top]; + ASSERT((currentPage != NULL) && (currentPage->on_exit != NULL)); + if ((currentPage != NULL) && (currentPage->on_exit != NULL)) + { + currentPage->on_exit(currentPage); + } + + if ((currentPage != NULL) && !currentPage->is_cached) + { + /* 非缓存页离开即销毁,下次进入会重新 on_create */ + if (currentPage->on_destroy != NULL) + { + currentPage->on_destroy(currentPage); + } + currentPage->is_created = 0; + } + + s_pageManager.page_stack[s_pageManager.stack_top] = NULL; + s_pageManager.stack_top--; + + newTop = s_pageManager.page_stack[s_pageManager.stack_top]; + ASSERT((newTop != NULL) && (newTop->on_enter != NULL)); + if ((newTop != NULL) && (newTop->on_enter != NULL)) + { + newTop->on_enter(newTop); + } + return PAGE_MANAGER_OK; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Navigate + * 功能: + * 根据目标 page_id 执行页面导航:先在注册表中查找页面,再将其压入页面栈。 + * + * 参数: + * pageId - 目标页面标识 + * + * 边界处理: + * - 若注册表中未找到对应 pageId,返回 PAGE_MANAGER_ERR_NOT_FOUND, + * 不执行页面切换。 + * - 目标页找到后,Push 过程中的边界条件(如栈满、重复入栈等) + * 由 PageManager_Push 统一处理并返回对应错误码。 + * + * 说明: + * - 本函数是“按 ID 导航”的统一入口,避免上层直接操作 page_t 指针。 + * - 导航成功后将触发标准生命周期链路: + * 旧页 on_exit -> 新页(必要时 on_create)-> 新页 on_enter。 + * + * 返回值: + * - PAGE_MANAGER_OK : 导航成功 + * - PAGE_MANAGER_ERR_NOT_FOUND : 未找到目标页面 + * - 其他 : 透传 PageManager_Push 的返回码(如栈满等) + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_Navigate(page_id_t pageId) +{ + page_t *target; + target = PageManager_Find(pageId); + ASSERT(target == NULL); + if (target == NULL) + { + return PAGE_MANAGER_ERR_NOT_FOUND; + } + return PageManager_Push(target); +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_DispatchEvent + * 功能: + * 将输入事件分发到当前栈顶页面,并在页面未处理时执行全局兜底处理。 + * + * 参数: + * event - 待分发的输入事件指针(键值、事件类型等) + * + * 边界处理: + * - 若 event 为 NULL,返回 PAGE_MANAGER_ERR_NULL_PARAM,不进行任何处理。 + * - 若当前页面栈为空(GetTop 返回 NULL),返回 PAGE_MANAGER_ERR_NO_ACTIVE_PAGE。 + * - 若栈顶页面未实现 on_event 回调,则视为未处理并进入兜底判定流程。 + * + * 说明: + * - 分发优先级:页面私有事件处理 > 管理器全局兜底处理。 + * - top->on_event 返回 EVENT_UNHANDLED 时,才调用 + * PageManager_GlobalEventHandle(event)。 + * - 该机制允许页面覆盖默认按键行为,同时保留统一的系统级快捷逻辑 + * (例如 ESC 返回上页)。 + * + * 返回值: + * - PAGE_MANAGER_OK : 分发流程执行完成 + * - PAGE_MANAGER_ERR_NULL_PARAM : 事件参数为空 + * - PAGE_MANAGER_ERR_NO_ACTIVE_PAGE : 当前无活动页面 + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_DispatchEvent(input_event_t *event) +{ + page_t *top; + event_result_t result; + + ASSERT(event == NULL); + if (event == NULL) + { + return PAGE_MANAGER_ERR_NULL_PARAM; + } + + top = PageManager_GetTop(); + ASSERT(top == NULL); + if (top == NULL) + { + return PAGE_MANAGER_ERR_NO_ACTIVE_PAGE; + } + + result = EVENT_UNHANDLED; + if (top->on_event != NULL) + { + result = top->on_event(top, event); + } + + /* 页面未处理时交给管理器处理全局快捷行为 */ + if (result == EVENT_UNHANDLED) + { + PageManager_GlobalEventHandle(event); + } + return PAGE_MANAGER_OK; +} + +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Loop + * 功能: + * 驱动当前栈顶页面的循环回调(on_loop),用于执行周期性任务。 + * + * 参数: + * 无 + * + * 边界处理: + * - 若当前页面栈为空(GetTop 返回 NULL),函数直接返回。 + * - 若栈顶页面未实现 on_loop 回调,函数直接返回。 + * - 通过双重判空避免空指针调用导致崩溃。 + * + * 说明: + * - 本函数只处理“当前可见页面”(栈顶页),不遍历其它页面。 + * - 典型调用位置是主循环中的固定节拍点(例如每 N ms 调用一次)。 + * - 页面切换、事件分发不在本函数处理范围内,分别由 Navigate/Push/Pop、 + * DispatchEvent 负责。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void PageManager_Loop(void) +{ + page_t *top = PageManager_GetTop(); + ASSERT((top == NULL) || (top->on_loop == NULL)); + if ((top != NULL) && (top->on_loop != NULL)) + { + top->on_loop(top); + } +} diff --git a/src/Drv/pages/page_manager.h b/src/Drv/pages/page_manager.h new file mode 100644 index 0000000..2b04869 --- /dev/null +++ b/src/Drv/pages/page_manager.h @@ -0,0 +1,77 @@ +#ifndef PAGE_MANAGER_H +#define PAGE_MANAGER_H + +#include "page.h" + +/* ------------------------------------------------------------------------- + * 结构体名: page_manager_t + * 作用: + * 页面管理器运行时上下文,维护“页面栈 + 页面注册表”两套核心状态。 + * + * 设计说明: + * - page_stack: + * 运行时导航栈,表示当前页面跳转路径。 + * 栈顶元素是当前可见/可交互页面。 + * - stack_top: + * 栈顶索引。约定 -1 表示空栈。 + * 当前栈深可由 (stack_top + 1) 推导。 + * - page_registry: + * 可导航页面的注册表,按 page_id 查找目标页面时使用。 + * - page_count: + * 当前注册表有效页面数量,范围 [0, MAX_PAGE_COUNT]。 + * + * 约束关系: + * - page_stack 最大深度由 MAX_PAGE_STACK_DEPTH 限制; + * - page_registry 最大容量由 MAX_PAGE_COUNT 限制; + * - 调用方需通过 PageManager_Init 初始化后再使用。 + * ------------------------------------------------------------------------- */ + #define MAX_PAGE_STACK_DEPTH 5 + #define MAX_PAGE_COUNT 16 + +/* ------------------------------------------------------------------------- + * 枚举名: page_manager_result_t + * 作用: + * 定义 PageManager 相关接口的统一返回码,避免使用无语义的裸整数。 + * + * 取值说明: + * PAGE_MANAGER_OK - 操作成功 + * PAGE_MANAGER_ERR_NULL_PARAM - 传入参数为空 + * PAGE_MANAGER_ERR_INVALID_ID - 页面 ID 非法 + * PAGE_MANAGER_ERR_NOT_FOUND - 目标页面未注册/未找到 + * PAGE_MANAGER_ERR_STACK_FULL - 页面栈已满,无法继续压栈 + * PAGE_MANAGER_ERR_STACK_BOTTOM - 页面栈已到栈底,无法继续弹栈 + * PAGE_MANAGER_ERR_REGISTRY_FULL - 页面注册表已满 + * PAGE_MANAGER_ERR_NO_ACTIVE_PAGE - 当前无活动页面可供分发/循环驱动 + * ------------------------------------------------------------------------- */ +typedef enum +{ + PAGE_MANAGER_OK = 0, + PAGE_MANAGER_ERR_NULL_PARAM = -1, + PAGE_MANAGER_ERR_INVALID_ID = -2, + PAGE_MANAGER_ERR_NOT_FOUND = -3, + PAGE_MANAGER_ERR_STACK_FULL = -4, + PAGE_MANAGER_ERR_STACK_BOTTOM = -5, + PAGE_MANAGER_ERR_REGISTRY_FULL = -6, + PAGE_MANAGER_ERR_NO_ACTIVE_PAGE = -7 +} page_manager_result_t; + + typedef struct +{ + page_t *page_stack[MAX_PAGE_STACK_DEPTH]; /* 页面导航栈 */ + int8_t stack_top; /* 栈顶索引(-1 表示空栈) */ + page_t *page_registry[MAX_PAGE_COUNT]; /* 页面注册表 */ + uint8_t page_count; /* 注册页面数量 */ +} page_manager_t; + +void PageManager_Init(void); +page_manager_result_t PageManager_Register(page_t *page); +page_manager_result_t PageManager_Navigate(page_id_t pageId); +page_manager_result_t PageManager_DispatchEvent(input_event_t *event); + +page_t *PageManager_GetTop(void); +page_manager_result_t PageManager_Push(page_t *newPage); +page_manager_result_t PageManager_Pop(void); +page_t *PageManager_Find(page_id_t pageId); +void PageManager_Loop(void); + +#endif diff --git a/src/main.c b/src/main.c index dc798b7..2eb1c95 100644 --- a/src/main.c +++ b/src/main.c @@ -30,10 +30,12 @@ static int getch(void) } #endif -#include "Drv/menu/app/menu.h" +#include "Drv/pages/page_manager.h" +#include "Drv/pages/menu/page.h" #include "TCP/tcp.h" #include "remoteDisplay.h" #include "Drv/key.h" +#include "Drv/lcd/lcd.h" #include "thread_utils.h" /* ---------------------------------------------------------------------------- @@ -42,6 +44,7 @@ static int getch(void) int main(void) { uint8_t count = 0; + input_event_t event; #ifdef _WIN32 /* Windows:将控制台代码页设为 UTF-8,避免中文乱码(如“监听”“退出”等) */ @@ -61,9 +64,12 @@ int main(void) server_arg.p_server_sock = &server_sock; server_arg.p_quit = &server_quit; - printf("开始初始化菜单树...\n"); + printf("开始初始化页面管理器与菜单页面...\n"); - MenuApp_Init(); /* 初始化菜单应用(Model + View + Presenter) */ + PageManager_Init(); + Lcd_Init(); /* 初始化屏幕显存:由入口统一完成 */ + (void)PageManager_Register(MenuPage_GetInstance()); + (void)PageManager_Navigate(PAGE_ID_MENU); Key_Init(); /* 初始化按键 */ printf("PC 端 HMI 菜单模拟启动(TCP 服务在单独线程,端口 7003)。\n"); /* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */ @@ -73,13 +79,19 @@ int main(void) } while(1) { - MenuApp_PollInput(); + uint8_t keyVal = Key_Read(); + if (keyVal != KEY_NONE) + { + event.type = PAGE_EVENT_KEY; + event.keyVal = keyVal; + PageManager_DispatchEvent(&event); + } Sleep(20); count++; if(count > 50) { count = 0; - MenuApp_Render(); + PageManager_Loop(); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6b8dadd..603f35c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,7 +7,6 @@ set(DTU_TEST_COMMON_SOURCES "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" "${CMAKE_SOURCE_DIR}/src/Drv/key.c" "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c" "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" "${CMAKE_SOURCE_DIR}/src/thread_utils.c" "${CMAKE_SOURCE_DIR}/src/remoteDisplay.c" @@ -70,11 +69,10 @@ add_dtu_test( # ------------------------------------------------------------ # P1:业务核心计算/状态流转测试 # ------------------------------------------------------------ -add_dtu_test( - test_p1_key - test_p1_key.c - "${CMAKE_SOURCE_DIR}/src/Drv/key.c" -) + +# ------------------------------------------------------------ +# P1:lcd基本测试 +# ------------------------------------------------------------ add_dtu_test( test_p1_lcd_basic test_p1_lcd_basic.c @@ -83,40 +81,40 @@ add_dtu_test( "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" ) +# ------------------------------------------------------------ +# P1:菜单管理器测试 +# ------------------------------------------------------------ +add_dtu_test( + test_p1_page_manager + test_p1_page_manager.c + "${CMAKE_SOURCE_DIR}/src/Drv/pages/page_manager.c" +) +# ------------------------------------------------------------ +# P1:菜单测试 +# ------------------------------------------------------------ add_dtu_test( test_p1_menu test_p1_menu.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" -) -add_dtu_test( - test_p1_menu_nav_legacy - test_p1_menu_nav_legacy.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" -) -add_dtu_test( - test_p1_menu_navigator - test_p1_menu_navigator.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" -) -add_dtu_test( - test_p1_menu_tree_builder - test_p1_menu_tree_builder.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" -) -add_dtu_test( - test_p1_menu_layout - test_p1_menu_layout.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/model.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/view.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/presenter.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/page.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/global/renderer_lcd.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" ) - +target_compile_definitions(test_p1_menu PRIVATE UNIT_TEST) +# ------------------------------------------------------------ +# P1:key测试 +# ------------------------------------------------------------ +add_dtu_test( + test_p1_key + test_p1_key.c + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" +) # ------------------------------------------------------------ # P2:集成测试(网络回环等) # ------------------------------------------------------------ @@ -126,21 +124,3 @@ add_dtu_test( "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" "${CMAKE_SOURCE_DIR}/src/thread_utils.c" ) -add_dtu_test( - test_p2_menu_runtime_startup - test_p2_menu_runtime_startup.c - "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_model.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_view.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_presenter.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_renderer_lcd.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" - "${CMAKE_SOURCE_DIR}/src/Drv/key.c" -) diff --git a/tests/test_p1_menu.c b/tests/test_p1_menu.c index db5111b..7420d80 100644 --- a/tests/test_p1_menu.c +++ b/tests/test_p1_menu.c @@ -2,46 +2,199 @@ #include "test_common.h" -#include "../src/Drv/menu/view/menu_layout.h" -#include "../src/Drv/menu/model/menu_tree_builder.h" - -static int noop_proc(void) -{ - return 0; -} +#include "../src/Drv/pages/menu/view.h" +#include "../src/Drv/pages/menu/model.h" +#include "../src/Drv/pages/menu/page.h" +/* ------------------------------------------------------------------------- + * 函数名: main + * 功能: + * 覆盖菜单模块的白盒/黑盒关键能力,验证: + * 1) Model 初始化与辅助函数 + * 2) 菜单树构建与显示名称装饰 + * 3) View 初始化后的端口与接口绑定 + * 4) MenuPage 生命周期入口是否完整绑定 + * + * 参数: + * 无 + * + * 返回值: + * - 0 : 测试通过 + * ------------------------------------------------------------------------- */ int main(void) { - tagMenuCtrl ctrl; - tagMenuItem items[4]; - uint8_t menu_num[4] = {0}; + /* 用于 MenuView_CharLenCal / Position 相关白盒测试的临时数组 */ tagPMenuItem first[4] = {0}; tagPMenuItem index[4] = {0}; uint8_t max_len; - const tagMenuModel model[4] = { - {0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {1, "设置", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {2, "子项", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {1, "查看", "", 0, 0, 0, (FUNCPTR)noop_proc}, + /* 页面实例:用于验证 MenuPage_GetInstance 生命周期回调绑定 */ + page_t *menuPage; + + /* 真实菜单模型对象:用于测试 MenuModel_Init 及其派生结果 */ + menu_model_t menuModel; + + /* 视图对象:用于验证 MenuView_Init 的接口与渲染端口初始化 */ + MenuView view; + + /* 自定义最小菜单树:用于单独测试 BuildTree / DecorateDisplayNames */ + tagMenuItem customItems[4]; + tagMenuItem customItems2[7]; + tagMenuModel customModelTab[4] = + { + {0, "Root", "", 0, 0x0000, 0, NULL}, + {1, "ChildA", "", 0, 0x0000, 0, NULL}, + {2, "Leaf", "", 0, 0x0000, 0, NULL}, + {1, "ChildB", "", 0, 0x0000, 0, NULL} + }; + tagMenuModel customModelTab2[7] = + { + {0, "RootA", "", 0, 0x0000, 0, NULL}, + {1, "A1", "", 0, 0x0000, 0, NULL}, + {2, "A1a", "", 0, 0x0000, 0, NULL}, + {1, "A2", "", 0, 0x0000, 0, NULL}, + {0, "RootB", "", 0, 0x0000, 0, NULL}, + {1, "B1", "", 0, 0x0000, 0, NULL}, + {0, "RootC", "", 0, 0x0000, 0, NULL} }; - memset(&ctrl, 0, sizeof(ctrl)); - memset(items, 0, sizeof(items)); + /* --------------------------------------------------------------------- + * Case 1: + * 测试 MenuModel_Init / MenuModel_CountTopLevel + * - 初始化真实菜单模型 + * - 验证 0 级菜单数量统计结果有效 + * --------------------------------------------------------------------- */ + memset(&menuModel, 0, sizeof(menuModel)); + MenuModel_Init(&menuModel); - MenuTree_0LevelNumCal(&ctrl, model, 4); - ASSERT_EQ_INT(1, ctrl.by0LevelNum); - MenuTree_MainCreate(items, model, 4); + /* --------------------------------------------------------------------- + * Case 2: + * 测试 MenuView_Utf8LenCal + * - 验证英文与中文字符串长度计算逻辑 + * --------------------------------------------------------------------- */ + ASSERT_EQ_INT(3, MenuModel_Utf8LenCal((uint8_t *)"ABC")); + ASSERT_EQ_INT(2, MenuModel_Utf8LenCal((uint8_t *)"你")); - ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC")); - ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你")); - - first[0] = &items[0]; - index[0] = &items[0]; - max_len = MenuLayout_CharLenCal(0, menu_num, first, index); + /* --------------------------------------------------------------------- + * Case 3: + * 测试 MenuModel_IndexMenuItems / MenuModel_GetMenuMaxDisplayLen + * - 使用真实菜单模型验证序号初始化、子节点数量统计与子菜单宽度统计 + * --------------------------------------------------------------------- */ + first[0] = &menuModel.menuItems[0]; + index[0] = &menuModel.menuItems[0]; + first[1] = menuModel.menuItems[0].links.lower; + max_len = MenuModel_GetMenuMaxDisplayLen(first[1]); ASSERT_TRUE(max_len > 0); - ASSERT_TRUE(menu_num[1] > 0); - ASSERT_STREQ("设置", (const char *)items[1].byName); + ASSERT_EQ_INT(0, menuModel.menuItems[0].rect.wPos); + ASSERT_EQ_INT(0, first[1]->rect.wPos); + ASSERT_TRUE(menuModel.menuItems[0].rect.wNum > 0); + if (first[1]->links.lower == NULL) + { + ASSERT_EQ_INT(0, first[1]->rect.wNum); + } + else + { + ASSERT_TRUE(first[1]->rect.wNum > 0); + } + + /* --------------------------------------------------------------------- + * Case 4: + * 测试 MenuModel_DecorateDisplayNames 的真实初始化结果 + * - 验证存在子菜单的项名称非空,并被正确用于显示 + * --------------------------------------------------------------------- */ + /* 有子菜单的项会被装饰追加 '\x10'(右箭头标记) */ + ASSERT_TRUE(((const char *)menuModel.menuItems[1].menuDef.byName)[0] != '\0'); + + /* --------------------------------------------------------------------- + * Case 5: + * 测试 MenuModel_BuildTree + * - 使用自定义 4 节点菜单表验证父子、同层、闭环链路关系 + * --------------------------------------------------------------------- */ + memset(customItems, 0, sizeof(customItems)); + + MenuModel_BuildTree(customItems, customModelTab, 4); + MenuModel_IndexMenuItems(customItems, 4); + ASSERT_TRUE(customItems[0].links.lower == &customItems[1]); + ASSERT_TRUE(customItems[1].links.higher == &customItems[0]); + ASSERT_TRUE(customItems[1].links.lower == &customItems[2]); + ASSERT_TRUE(customItems[2].links.higher == &customItems[1]); + ASSERT_TRUE(customItems[1].links.behind == &customItems[3]); + ASSERT_TRUE(customItems[3].links.before == &customItems[1]); + ASSERT_TRUE(customItems[3].links.higher == &customItems[0]); + ASSERT_TRUE(customItems[3].links.behind == &customItems[1]); /* 同层闭环 */ + ASSERT_EQ_INT(0, customItems[0].rect.wPos); + ASSERT_EQ_INT(2, customItems[0].rect.wNum); + ASSERT_EQ_INT(0, customItems[1].rect.wPos); + ASSERT_EQ_INT(1, customItems[1].rect.wNum); + ASSERT_EQ_INT(1, customItems[3].rect.wPos); + ASSERT_EQ_INT(0, customItems[3].rect.wNum); + ASSERT_EQ_INT(0, customItems[2].rect.wPos); + ASSERT_EQ_INT(0, customItems[2].rect.wNum); + + /* --------------------------------------------------------------------- + * Case 5.1: + * 强化测试 MenuModel_IndexMenuItems + * - 覆盖多顶层、多兄弟、跨层回退后的 wPos / wNum 统计 + * --------------------------------------------------------------------- */ + memset(customItems2, 0, sizeof(customItems2)); + MenuModel_BuildTree(customItems2, customModelTab2, 7); + MenuModel_IndexMenuItems(customItems2, 7); + ASSERT_EQ_INT(0, customItems2[0].rect.wPos); + ASSERT_EQ_INT(2, customItems2[0].rect.wNum); + ASSERT_EQ_INT(0, customItems2[1].rect.wPos); + ASSERT_EQ_INT(1, customItems2[1].rect.wNum); + ASSERT_EQ_INT(0, customItems2[2].rect.wPos); + ASSERT_EQ_INT(0, customItems2[2].rect.wNum); + ASSERT_EQ_INT(1, customItems2[3].rect.wPos); + ASSERT_EQ_INT(0, customItems2[3].rect.wNum); + ASSERT_EQ_INT(1, customItems2[4].rect.wPos); + ASSERT_EQ_INT(1, customItems2[4].rect.wNum); + ASSERT_EQ_INT(0, customItems2[5].rect.wPos); + ASSERT_EQ_INT(0, customItems2[5].rect.wNum); + ASSERT_EQ_INT(2, customItems2[6].rect.wPos); + ASSERT_EQ_INT(0, customItems2[6].rect.wNum); + + /* --------------------------------------------------------------------- + * Case 6: + * 测试 MenuModel_DecorateDisplayNames + * - 验证有子节点的项追加 '\x10' + * - 验证叶子节点名称不被追加装饰符 + * --------------------------------------------------------------------- */ + MenuModel_DecorateDisplayNames(customItems, 4); + ASSERT_TRUE(customItems[0].menuDef.byName[4] == '\x10'); + ASSERT_TRUE(customItems[0].menuDef.byName[5] == '\0'); + ASSERT_TRUE(customItems[1].menuDef.byName[6] == '\x10'); + ASSERT_TRUE(customItems[1].menuDef.byName[7] == '\0'); + ASSERT_TRUE(customItems[2].menuDef.byName[4] == '\0'); /* 叶子节点不追加 */ + + /* --------------------------------------------------------------------- + * Case 7: + * 测试 MenuView_Init / MenuView_GetPortForTest + * - 验证 View 初始化后渲染端口、布局参数、接口函数指针均可用 + * --------------------------------------------------------------------- */ + /* View 初始化后,渲染端口派生数据和对外接口必须可用 */ + memset(&view, 0, sizeof(view)); + ASSERT_TRUE(MenuView_GetPortForTest() == NULL); + MenuView_Init(&view); + ASSERT_TRUE(MenuView_GetPortForTest() != NULL); + ASSERT_TRUE(view.update_selection_new_level != NULL); + ASSERT_TRUE(view.update_selection_same_level != NULL); + ASSERT_TRUE(view.full_refresh != NULL); + + /* --------------------------------------------------------------------- + * Case 8: + * 测试 MenuPage_GetInstance + * - 验证页面生命周期入口已经完整绑定 + * --------------------------------------------------------------------- */ + /* 页面生命周期入口必须完整绑定,避免注册后生命周期不执行 */ + menuPage = MenuPage_GetInstance(); + ASSERT_TRUE(menuPage != NULL); + ASSERT_TRUE(menuPage->on_create != NULL); + ASSERT_TRUE(menuPage->on_enter != NULL); + ASSERT_TRUE(menuPage->on_exit != NULL); + ASSERT_TRUE(menuPage->on_destroy != NULL); + ASSERT_TRUE(menuPage->on_event != NULL); + ASSERT_TRUE(menuPage->on_loop != NULL); return 0; } diff --git a/tests/test_p1_menu_layout.c b/tests/test_p1_menu_layout.c deleted file mode 100644 index e4272eb..0000000 --- a/tests/test_p1_menu_layout.c +++ /dev/null @@ -1,47 +0,0 @@ -#include "test_common.h" - -#include - -#include "../src/Drv/lcd/lcd.h" -#include "../src/Drv/menu/app/menu.h" -#include "../src/Drv/menu/view/menu_layout.h" - -int main(void) -{ - tagMenuItem root; - tagMenuItem child; - MenuLayoutConfig config = { - LCD_SIZE_X, - LCD_SIZE_Y, - MENU_YMIN, - MENU_YMAX_FROM_LCD(LCD_SIZE_Y), - LINE_HEIGHT, - MENU_WITDTH, - MENU_XADD, - MENU_YADD}; - - memset(&root, 0, sizeof(root)); - memset(&child, 0, sizeof(child)); - - memcpy(root.byName, "Root", 5); - memcpy(child.byName, "Child", 6); - root.byClass = 0; - child.byClass = 1; - - root.ptLower = &child; - root.ptBehind = &root; - root.ptBefore = &root; - child.ptHigher = &root; - child.ptBehind = &child; - child.ptBefore = &child; - - ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC")); - ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你")); - - MenuLayout_PositionCal(&root, 1, &config); - ASSERT_TRUE(root.wEPosX > root.wSPosX); - ASSERT_TRUE(root.wEPosY > root.wSPosY); - ASSERT_TRUE(root.wEPosY <= LCD_SIZE_Y); - - return 0; -} diff --git a/tests/test_p1_menu_nav_legacy.c b/tests/test_p1_menu_nav_legacy.c deleted file mode 100644 index 6424976..0000000 --- a/tests/test_p1_menu_nav_legacy.c +++ /dev/null @@ -1,93 +0,0 @@ -#include - -#include "test_common.h" - -#include "../src/Drv/key.h" -#include "../src/Drv/menu/presenter/menu_navigator.h" - -static int g_exec_count = 0; - -static int on_exec(void) -{ - g_exec_count++; - return 0; -} - -static void build_legacy_like_tree(MenuNavState *nav, tagMenuItem *root, tagMenuItem *m1, tagMenuItem *m2, tagMenuItem *m1_sub) -{ - memset(nav, 0, sizeof(*nav)); - memset(root, 0, sizeof(*root)); - memset(m1, 0, sizeof(*m1)); - memset(m2, 0, sizeof(*m2)); - memset(m1_sub, 0, sizeof(*m1_sub)); - - root->byClass = 0; - root->ptLower = m1; - root->ptBefore = root; - root->ptBehind = root; - root->wPos = 1; - - m1->byClass = 1; - m1->wPos = 1; - m1->ptHigher = root; - m1->ptBefore = m2; - m1->ptBehind = m2; - m1->ptLower = m1_sub; - m1->pfnWinProc = on_exec; - - m2->byClass = 1; - m2->wPos = 2; - m2->ptHigher = root; - m2->ptBefore = m1; - m2->ptBehind = m1; - m2->pfnWinProc = on_exec; - - m1_sub->byClass = 2; - m1_sub->wPos = 1; - m1_sub->ptHigher = m1; - m1_sub->ptBefore = m1_sub; - m1_sub->ptBehind = m1_sub; - m1_sub->pfnWinProc = on_exec; - - nav->ptHead = root; - nav->ptCurrent = m1; - nav->ptCurBak = m1; - nav->ptRoute[0] = root; - nav->ptRoute[1] = m1; -} - -int main(void) -{ - MenuNavState nav; - tagMenuItem root; - tagMenuItem m1; - tagMenuItem m2; - tagMenuItem m1_sub; - MenuNavResult result; - - build_legacy_like_tree(&nav, &root, &m1, &m2, &m1_sub); - - result = MenuNavigator_ProcessKey(&nav, KEY_D); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &m2); - - result = MenuNavigator_ProcessKey(&nav, KEY_U); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &m1); - - result = MenuNavigator_ProcessKey(&nav, KEY_ENT); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &m1_sub); - - result = MenuNavigator_ProcessKey(&nav, KEY_ESC); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &m1); - - g_exec_count = 0; - nav.ptCurrent = &m2; - result = MenuNavigator_ProcessKey(&nav, KEY_ENT); - ASSERT_EQ_INT(0, result.needRefresh); - ASSERT_EQ_INT(1, g_exec_count); - - return 0; -} diff --git a/tests/test_p1_menu_navigator.c b/tests/test_p1_menu_navigator.c deleted file mode 100644 index f06e302..0000000 --- a/tests/test_p1_menu_navigator.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "test_common.h" - -#include - -#include "../src/Drv/key.h" -#include "../src/Drv/menu/presenter/menu_navigator.h" - -static int g_exec_count = 0; - -static int on_exec(void) -{ - g_exec_count++; - return 0; -} - -static void build_two_level(MenuNavState *nav, tagMenuItem *root, tagMenuItem *child_a, tagMenuItem *child_b) -{ - memset(nav, 0, sizeof(*nav)); - memset(root, 0, sizeof(*root)); - memset(child_a, 0, sizeof(*child_a)); - memset(child_b, 0, sizeof(*child_b)); - - root->byClass = 0; - root->wPos = 1; - root->ptLower = child_a; - root->ptBefore = root; - root->ptBehind = root; - - child_a->byClass = 1; - child_a->wPos = 1; - child_a->ptHigher = root; - child_a->ptBefore = child_b; - child_a->ptBehind = child_b; - child_a->pfnWinProc = on_exec; - - child_b->byClass = 1; - child_b->wPos = 2; - child_b->ptHigher = root; - child_b->ptBefore = child_a; - child_b->ptBehind = child_a; - child_b->pfnWinProc = on_exec; - - nav->ptHead = root; - nav->ptCurrent = child_a; - nav->ptCurBak = child_a; - nav->ptRoute[0] = root; - nav->ptRoute[1] = child_a; -} - -int main(void) -{ - MenuNavState nav; - tagMenuItem root; - tagMenuItem a; - tagMenuItem b; - MenuNavResult result; - - build_two_level(&nav, &root, &a, &b); - - result = MenuNavigator_ProcessKey(&nav, KEY_D); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &b); - - result = MenuNavigator_ProcessKey(&nav, KEY_U); - ASSERT_EQ_INT(1, result.needRefresh); - ASSERT_TRUE(nav.ptCurrent == &a); - - g_exec_count = 0; - result = MenuNavigator_ProcessKey(&nav, KEY_ENT); - ASSERT_EQ_INT(0, result.needRefresh); - ASSERT_EQ_INT(1, g_exec_count); - - result = MenuNavigator_ProcessKey(&nav, KEY_ESC); - ASSERT_EQ_INT(1, result.skipRenderThisRound); - ASSERT_TRUE(nav.ptCurrent == root.ptLower); - ASSERT_TRUE(nav.ptRoute[0] == &root); - - return 0; -} diff --git a/tests/test_p1_menu_tree_builder.c b/tests/test_p1_menu_tree_builder.c deleted file mode 100644 index 0a0d955..0000000 --- a/tests/test_p1_menu_tree_builder.c +++ /dev/null @@ -1,37 +0,0 @@ -#include "test_common.h" - -#include - -#include "../src/Drv/menu/model/menu_tree_builder.h" - -static int noop_proc(void) -{ - return 0; -} - -int main(void) -{ - tagMenuCtrl ctrl; - tagMenuItem items[4]; - const tagMenuModel model[4] = { - {0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {1, "A", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {1, "B", "", 0, 0, 0, (FUNCPTR)noop_proc}, - {2, "C", "", 0, 0, 0, (FUNCPTR)noop_proc}, - }; - - memset(&ctrl, 0, sizeof(ctrl)); - memset(items, 0, sizeof(items)); - - MenuTree_0LevelNumCal(&ctrl, model, 4); - ASSERT_EQ_INT(1, ctrl.by0LevelNum); - - MenuTree_MainCreate(items, model, 4); - ASSERT_TRUE(items[0].ptLower == &items[1]); - ASSERT_TRUE(items[1].ptBehind == &items[2]); - ASSERT_TRUE(items[2].ptBefore == &items[1]); - ASSERT_TRUE(items[2].ptLower == &items[3]); - ASSERT_TRUE(items[3].ptHigher == &items[2]); - - return 0; -} diff --git a/tests/test_p1_page_manager.c b/tests/test_p1_page_manager.c new file mode 100644 index 0000000..d3ec6f0 --- /dev/null +++ b/tests/test_p1_page_manager.c @@ -0,0 +1,292 @@ +#include + +#include "test_common.h" + +#include "../src/Drv/key.h" +#include "../src/Drv/pages/page_manager.h" + +/* ------------------------------------------------------------------------- + * 结构体名: test_page_ctx_t + * 功能: + * 测试页面私有上下文,用于统计各生命周期回调与事件处理的触发次数。 + * + * 字段说明: + * createCount - on_create 被调用次数 + * enterCount - on_enter 被调用次数 + * exitCount - on_exit 被调用次数 + * destroyCount - on_destroy 被调用次数 + * loopCount - on_loop 被调用次数 + * eventResult - on_event 预设返回值(用于控制是否触发全局兜底) + * ------------------------------------------------------------------------- */ +typedef struct +{ + int createCount; + int enterCount; + int exitCount; + int destroyCount; + int loopCount; + event_result_t eventResult; +} test_page_ctx_t; + +static test_page_ctx_t g_ctx_a; +static test_page_ctx_t g_ctx_b; + +/* A 页生命周期/事件回调桩函数:将调用痕迹写入 page->model 对应的计数器 */ +static void on_create_a(page_t *page) { ((test_page_ctx_t *)page->model)->createCount++; } +static void on_enter_a(page_t *page) { ((test_page_ctx_t *)page->model)->enterCount++; } +static void on_exit_a(page_t *page) { ((test_page_ctx_t *)page->model)->exitCount++; } +static void on_destroy_a(page_t *page) { ((test_page_ctx_t *)page->model)->destroyCount++; } +static event_result_t on_event_a(page_t *page, input_event_t *event) +{ + (void)event; + return ((test_page_ctx_t *)page->model)->eventResult; +} +static void on_loop_a(page_t *page) { ((test_page_ctx_t *)page->model)->loopCount++; } + +/* B 页生命周期/事件回调桩函数:用于验证多页切换与销毁链路 */ +static void on_create_b(page_t *page) { ((test_page_ctx_t *)page->model)->createCount++; } +static void on_enter_b(page_t *page) { ((test_page_ctx_t *)page->model)->enterCount++; } +static void on_exit_b(page_t *page) { ((test_page_ctx_t *)page->model)->exitCount++; } +static void on_destroy_b(page_t *page) { ((test_page_ctx_t *)page->model)->destroyCount++; } +static event_result_t on_event_b(page_t *page, input_event_t *event) +{ + (void)event; + return ((test_page_ctx_t *)page->model)->eventResult; +} +static void on_loop_b(page_t *page) { ((test_page_ctx_t *)page->model)->loopCount++; } + +static void setup_page(page_t *page, + page_id_t pageId, + uint8_t isCached, + test_page_ctx_t *ctx, + void (*on_create)(page_t *), + void (*on_enter)(page_t *), + void (*on_exit)(page_t *), + void (*on_destroy)(page_t *), + event_result_t (*on_event)(page_t *, input_event_t *), + void (*on_loop)(page_t *)) +{ + memset(page, 0, sizeof(*page)); + page->page_id = pageId; + page->is_cached = isCached; + page->on_create = on_create; + page->on_enter = on_enter; + page->on_exit = on_exit; + page->on_destroy = on_destroy; + page->on_event = on_event; + page->on_loop = on_loop; + page->model = ctx; +} + +/* ------------------------------------------------------------------------- + * 函数名: main + * 功能: + * 覆盖 PageManager 的核心行为链路,验证注册、导航、事件分发(含 ESC 兜底回退)、 + * 生命周期回调顺序以及循环驱动是否符合预期。 + * + * 参数: + * 无 + * + * 边界处理: + * - 通过 memset 将页面对象和上下文清零,避免脏数据影响断言。 + * - 使用 ASSERT_TRUE 先校验 GetTop 非空,再访问 page_id,防止空指针解引用。 + * + * 说明: + * - pageA 设为缓存页(is_cached = 1),用于验证回退后可继续进入而不销毁。 + * - pageB 设为非缓存页(is_cached = 0),用于验证 ESC 回退时触发 on_destroy。 + * - 将 pageB.on_event 设为 EVENT_UNHANDLED,确保 DispatchEvent 走全局兜底, + * 从而触发 KEY_ESC -> PageManager_Pop() 的系统行为。 + * - 关键验证点: + * 1) Navigate 到 A:A create/enter 各一次; + * 2) Navigate 到 B:A exit 一次,B create/enter 各一次; + * 3) ESC 事件:B exit + destroy,栈顶回到 A,A enter 再次触发; + * 4) Loop:仅驱动当前栈顶 A 的 on_loop。 + * + * 返回值: + * - 0:测试通过 + * ------------------------------------------------------------------------- */ +int main(void) +{ + page_t pageA; + page_t pageB; + page_t pageA2; + page_t pageB2; + page_t pageA3; + input_event_t event; + page_t invalidNonePage; + page_t invalidMaxPage; + + /* ===== Case 1: Init 后的默认行为与空栈边界 ===== */ + memset(&g_ctx_a, 0, sizeof(g_ctx_a)); + memset(&g_ctx_b, 0, sizeof(g_ctx_b)); + PageManager_Init(); + ASSERT_TRUE(PageManager_GetTop() == NULL); + ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == NULL); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_NOT_FOUND, PageManager_Navigate(PAGE_ID_MENU)); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_BOTTOM, PageManager_Pop()); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_Register(NULL)); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_Push(NULL)); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_DispatchEvent(NULL)); + PageManager_Loop(); + + /* ===== Case 2: Register 参数边界与重复注册覆盖 ===== */ + setup_page(&invalidNonePage, + PAGE_ID_NONE, + 1, + &g_ctx_a, + on_create_a, + on_enter_a, + on_exit_a, + on_destroy_a, + on_event_a, + on_loop_a); + setup_page(&invalidMaxPage, + PAGE_ID_MAX, + 1, + &g_ctx_b, + on_create_b, + on_enter_b, + on_exit_b, + on_destroy_b, + on_event_b, + on_loop_b); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_INVALID_ID, PageManager_Register(&invalidNonePage)); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_INVALID_ID, PageManager_Register(&invalidMaxPage)); + + setup_page(&pageA, + PAGE_ID_MENU, + 1, + &g_ctx_a, + on_create_a, + on_enter_a, + on_exit_a, + on_destroy_a, + on_event_a, + on_loop_a); + setup_page(&pageB, + PAGE_ID_APP_INFO, + 0, + &g_ctx_b, + on_create_b, + on_enter_b, + on_exit_b, + on_destroy_b, + on_event_b, + on_loop_b); + setup_page(&pageA2, + PAGE_ID_MENU, + 1, + &g_ctx_a, + on_create_a, + on_enter_a, + on_exit_a, + on_destroy_a, + on_event_a, + on_loop_a); + setup_page(&pageB2, + PAGE_ID_APP_INFO, + 0, + &g_ctx_b, + on_create_b, + on_enter_b, + on_exit_b, + on_destroy_b, + on_event_b, + on_loop_b); + setup_page(&pageA3, + PAGE_ID_MENU, + 1, + &g_ctx_a, + on_create_a, + on_enter_a, + on_exit_a, + on_destroy_a, + on_event_a, + on_loop_a); + + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageA)); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageB)); + ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == &pageA); + ASSERT_TRUE(PageManager_Find(PAGE_ID_APP_INFO) == &pageB); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageA2)); /* 重复 ID 覆盖 */ + ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == &pageA2); + + /* ===== Case 3: Navigate + 生命周期 + GetTop ===== */ + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_MENU)); + ASSERT_EQ_INT(1, g_ctx_a.createCount); + ASSERT_EQ_INT(1, g_ctx_a.enterCount); + ASSERT_TRUE(PageManager_GetTop() != NULL); + ASSERT_EQ_INT(PAGE_ID_MENU, PageManager_GetTop()->page_id); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_MENU)); /* 幂等导航到当前页 */ + ASSERT_EQ_INT(1, g_ctx_a.enterCount); /* 不应重复 enter */ + + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_APP_INFO)); + ASSERT_EQ_INT(1, g_ctx_a.exitCount); + ASSERT_EQ_INT(1, g_ctx_b.createCount); + ASSERT_EQ_INT(1, g_ctx_b.enterCount); + ASSERT_TRUE(PageManager_GetTop() != NULL); + ASSERT_EQ_INT(PAGE_ID_APP_INFO, PageManager_GetTop()->page_id); + + /* ===== Case 4: DispatchEvent handled/unhandled 分支 ===== */ + g_ctx_b.eventResult = EVENT_HANDLED; + event.type = PAGE_EVENT_KEY; + event.keyVal = KEY_ESC; + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event)); + ASSERT_TRUE(PageManager_GetTop() == &pageB); /* handled 时不走全局 Pop */ + ASSERT_EQ_INT(0, g_ctx_b.exitCount); + ASSERT_EQ_INT(0, g_ctx_b.destroyCount); + + g_ctx_b.eventResult = EVENT_UNHANDLED; + event.type = PAGE_EVENT_KEY; + event.keyVal = KEY_ESC; + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event)); + ASSERT_TRUE(PageManager_GetTop() == &pageA2); + ASSERT_EQ_INT(PAGE_ID_MENU, PageManager_GetTop()->page_id); + ASSERT_EQ_INT(1, g_ctx_b.exitCount); + ASSERT_EQ_INT(1, g_ctx_b.destroyCount); + ASSERT_EQ_INT(2, g_ctx_a.enterCount); + + /* 未处理但非键盘事件,不应触发全局行为 */ + event.type = 0; + event.keyVal = KEY_ESC; + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event)); + ASSERT_TRUE(PageManager_GetTop() == &pageA2); + + /* ===== Case 5: Loop 仅驱动栈顶页 ===== */ + PageManager_Loop(); + ASSERT_EQ_INT(1, g_ctx_a.loopCount); + ASSERT_EQ_INT(0, g_ctx_b.loopCount); + + /* ===== Case 6: Push/Pop 边界(含栈满) ===== */ + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageB2)); + ASSERT_EQ_INT(2, g_ctx_b.createCount); /* 上次被 destroy,重新 push 应再 create */ + ASSERT_EQ_INT(2, g_ctx_b.enterCount); + ASSERT_TRUE(PageManager_GetTop() == &pageB2); + + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageA3)); + ASSERT_TRUE(PageManager_GetTop() == &pageA3); + + /* 当前栈深推进到 4(A2 -> B2 -> A3 -> B) */ + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageB)); + ASSERT_TRUE(PageManager_GetTop() == &pageB); + + /* 再 Push 一次达到栈深 5(上限) */ + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageA3)); + ASSERT_TRUE(PageManager_GetTop() == &pageA3); + + /* 栈满后继续 Push 应失败 */ + ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_FULL, PageManager_Push(&pageA2)); + + /* 连续 Pop 到底页,最后一次应失败 */ + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop()); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop()); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop()); + ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop()); + ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_BOTTOM, PageManager_Pop()); + ASSERT_TRUE(PageManager_GetTop() == &pageA2); + + /* ===== Case 7: Navigate 未注册页 ===== */ + ASSERT_EQ_INT(PAGE_MANAGER_ERR_NOT_FOUND, PageManager_Navigate(PAGE_ID_NONE)); + + return 0; +} diff --git a/tests/test_p2_menu_runtime_startup.c b/tests/test_p2_menu_runtime_startup.c deleted file mode 100644 index 28e08c9..0000000 --- a/tests/test_p2_menu_runtime_startup.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "test_common.h" - -#include "../src/Drv/key.h" -#include "../src/Drv/menu/app/menu.h" - -int main(void) -{ - int decorated_found = 0; - uint16_t itemCount = 0; - const tagMenuItem *menuItems; - - MenuApp_Init(); - Key_Init(); - menuItems = MenuApp_GetMenuItems(&itemCount); - - for (uint16_t i = 0; i < itemCount; i++) - { - if (menuItems[i].ptLower != NULL) - { - uint8_t len = 0; - while ((len < 50) && (menuItems[i].byName[len] != '\0')) - { - len++; - } - ASSERT_TRUE(len > 0); - ASSERT_EQ_INT('\x10', menuItems[i].byName[len - 1]); - decorated_found = 1; - break; - } - } - ASSERT_TRUE(decorated_found == 1); - - /* 首次路由应仅触发首帧绘制,不应崩溃 */ - MenuApp_PollInput(); - - /* 二次刷新路径也不应崩溃 */ - MenuApp_Render(); - return 0; -}