From df94630210ce84fdd6c5b13a596d215832f6be45 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 2 Apr 2026 17:13:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=20APPinfo=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=9F=BA=E7=A1=80=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 + README.md | 59 +- docs/系统架构设计文档.md | 1283 ++++++++++++-------------- docs/绘图/.$HMI 图像绘制.drawio.dtmp | 506 ---------- docs/绘图/HMI 图像绘制.drawio | 330 ++++++- include/types.h | 7 + src/Drv/lcd/lcd_draw.c | 13 +- src/Drv/lcd/lcd_draw.h | 2 +- src/Drv/lcd/lcd_text.c | 2 +- src/Drv/lcd/lcd_text.h | 3 + src/Drv/menu/app/menu.c | 27 +- src/Drv/pages/AppInfo/def.h | 9 + src/Drv/pages/AppInfo/model.c | 15 + src/Drv/pages/AppInfo/model.h | 15 + src/Drv/pages/AppInfo/page.c | 218 +++++ src/Drv/pages/AppInfo/page.h | 9 + src/Drv/pages/AppInfo/presenter.c | 90 ++ src/Drv/pages/AppInfo/presenter.h | 18 + src/Drv/pages/AppInfo/view.c | 97 ++ src/Drv/pages/AppInfo/view.h | 17 + src/Drv/pages/global/renderer_lcd.c | 67 +- src/Drv/pages/global/renderer_lcd.h | 29 +- src/Drv/pages/menu/page.c | 81 +- src/Drv/pages/menu/page.h | 2 - src/Drv/pages/menu/presenter.c | 435 ++++++--- src/Drv/pages/menu/presenter.h | 34 +- src/Drv/pages/menu/view.c | 2 +- src/Drv/pages/page.h | 4 +- src/Drv/pages/page_manager.c | 88 +- src/Drv/pages/page_manager.h | 1 + src/main.c | 2 + tests/CMakeLists.txt | 1 + 32 files changed, 2031 insertions(+), 1439 deletions(-) delete mode 100644 docs/绘图/.$HMI 图像绘制.drawio.dtmp create mode 100644 src/Drv/pages/AppInfo/def.h create mode 100644 src/Drv/pages/AppInfo/model.c create mode 100644 src/Drv/pages/AppInfo/model.h create mode 100644 src/Drv/pages/AppInfo/page.c create mode 100644 src/Drv/pages/AppInfo/page.h create mode 100644 src/Drv/pages/AppInfo/presenter.c create mode 100644 src/Drv/pages/AppInfo/presenter.h create mode 100644 src/Drv/pages/AppInfo/view.c create mode 100644 src/Drv/pages/AppInfo/view.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f30b5bc..9e73a61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,10 @@ add_executable(DTU-HMI src/Drv/pages/menu/model.c src/Drv/pages/menu/presenter.c src/Drv/pages/menu/view.c + src/Drv/pages/AppInfo/page.c + src/Drv/pages/AppInfo/model.c + src/Drv/pages/AppInfo/presenter.c + src/Drv/pages/AppInfo/view.c src/Drv/lcd/lcd.c src/Drv/lcd/lcd_draw.c src/Drv/lcd/lcd_text.c diff --git a/README.md b/README.md index a6a1004..a47f398 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,31 @@ - 基于 CTest 的单元测试体系,已覆盖菜单、页面管理、按键、LCD、TCP 回环等模块 --- +```txt +┌─────────────────────────────────────────────────────────────┐ +│ 应用层 (main.c) │ +│ 系统初始化 / 主循环调度 / 生命周期管理 │ +├─────────────────────────────────────────────────────────────┤ +│ 多页面管理层 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Page Manager (栈式调度) │ Global Model (跨页面共享) │ │ +│ └───────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ MVP 业务层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Menu Page │ │ AppInfoPage │ │ YC Page │ │ +│ │ Model/ │ │ Model/ │ │ Model/ │ │ +│ │ Presenter/ │ │ Presenter/ │ │ Presenter/ │ │ +│ │ View │ │ View │ │ View │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 驱动抽象层 │ +│ LCD 驱动 │ 按键驱动 │ 布局计算 │ 渲染端口 │ +├─────────────────────────────────────────────────────────────┤ +│ 底层基础设施 │ +│ TCP/Socket 封装 │ 线程工具 │ UTF-8 处理 │ +└─────────────────────────────────────────────────────────────┘ +``` ## 快速开始 @@ -70,38 +95,38 @@ ctest --test-dir build -C Debug --output-on-failure DTU-HMI/ ├── CMakeLists.txt ├── README.md -├── include/ -│ └── types.h +├── include/ # 公共头文件 +│ └── types.h # 类型定义 ├── src/ -│ ├── main.c +│ ├── main.c # ⭐ 应用入口,主循环调度 │ ├── common/ │ │ └── utf8.c/h │ ├── remoteDisplay.c/h │ ├── thread_utils.c/h │ ├── TCP/ │ │ └── tcp.c/h -│ └── Drv/ -│ ├── key.c/h +│ └── Drv/ # 📦 驱动层 +│ ├── key.c/h # 按键输入抽象 │ ├── menu/app/ │ │ └── menu.c/h -│ ├── lcd/ -│ │ ├── ascii.c/h +│ ├── lcd/ # LCD 显示驱动 +│ │ ├── ascii.c/h # ASCII 字库 │ │ ├── lcd.c/h │ │ ├── lcd_draw.c/h │ │ └── lcd_text.c/h -│ └── pages/ -│ ├── page.h -│ ├── page_manager.c/h -│ ├── global/ -│ │ ├── global_state.c/h -│ │ └── renderer_lcd.c/h -│ └── menu/ +│ └── pages/ # ⭐ 新版多页面系统根目录 +│ ├── page.h # 标准页面抽象定义 +│ ├── page_manager.c/h # 页面栈调度器 +│ ├── global/ # 🌍 全局状态管理 +│ │ ├── global_state.c/h # 跨页面共享数据 +│ │ └── renderer_lcd.c/h # LCD 渲染 HAL 接口 +│ └── menu/ # 📋 Menu 页面的 MVP 实现 │ ├── def.h │ ├── model.c/h │ ├── presenter.c/h -│ ├── view.c/h -│ └── page.c/h -└── tests/ +│ ├── view.c/h # 菜单渲染绘制 +│ └── page.c/h # 页面生命周期注册 +└── tests/ # 🧪 测试目录 ├── CMakeLists.txt ├── test_p0_remote_display.c ├── test_p0_utf8_hz12_get.c diff --git a/docs/系统架构设计文档.md b/docs/系统架构设计文档.md index 21186dc..4f4d357 100644 --- a/docs/系统架构设计文档.md +++ b/docs/系统架构设计文档.md @@ -1,37 +1,100 @@ # DTU-HMI 系统架构设计文档 ## 1. 文档目的 - 本文档用于描述 `DTU-HMI` 工程的整体架构、核心模块职责、关键数据流、线程与通信模型、构建与测试体系,作为后续开发、联调、测试与维护的统一基线。 - ## 2. 系统概述 - `DTU-HMI` 是一个基于纯 C 实现的 PC 端 HMI 菜单逻辑模拟系统,目标是复现现场 DTU 设备的人机界面行为。 系统支持本地菜单交互与远程显示协议(RemoDispBus)通信,主要包含: - - 菜单树构建与路由(多级菜单、同级循环导航) - LCD 显存与绘制(160x160 单色像素缓冲) - 按键输入(本地/远程按键注入) - TCP 服务(远程显示数据交互) - 跨平台线程与网络适配(Windows/Linux) - ## 3. 架构目标与设计原则 - - **可移植性**:通过 `tcp.c`、`thread_utils.c` 封装平台差异。 - **可维护性**:按模块职责划分(菜单/显示/网络/线程/输入)。 + - 系统进行严格的分层设计 + - 分布规划: + - 应用层 + - 服务层 + - 设备驱动层 (LCD 屏幕,TCP/IP) + - 分层规律 + - 单向依赖 + - 禁止跨层调用 + - 职责单一 + - 接口标准化 + - 全局变量零容忍 + - 实时性高的数据采用**观察者模式**进行处理 + - 实时性低的数据通过**消息队列**的方式处理 - **可测试性**:优先抽取并覆盖纯逻辑函数,逐步推进集成测试。 - **低耦合高内聚**:上层业务通过明确接口调用下层能力。 - +- **错误检查**:对可预见的错误进行检查 + - **断言检查**:增加调试编译条件下的断言检查,快速定位一般情况下的错误。 + - **错误码处理** ## 4. 系统分层架构 + +**目录结构:** +```text +DTU-HMI/ +├── CMakeLists.txt +├── README.md +├── include/ # 公共头文件 +│ └── types.h # 类型定义 +├── src/ +│ ├── main.c # ⭐ 应用入口,主循环调度 +│ ├── common/ +│ │ └── utf8.c/h +│ ├── remoteDisplay.c/h +│ ├── thread_utils.c/h +│ ├── TCP/ +│ │ └── tcp.c/h +│ └── Drv/ # 📦 驱动层 +│ ├── key.c/h # 按键输入抽象 +│ ├── menu/app/ +│ │ └── menu.c/h +│ ├── lcd/ # LCD 显示驱动 +│ │ ├── ascii.c/h # ASCII 字库 +│ │ ├── lcd.c/h +│ │ ├── lcd_draw.c/h +│ │ └── lcd_text.c/h +│ └── pages/ # ⭐ 新版多页面系统根目录 +│ ├── page.h # 标准页面抽象定义 +│ ├── page_manager.c/h # 页面栈调度器 +│ ├── global/ # 🌍 全局状态管理 +│ │ ├── global_state.c/h # 跨页面共享数据 +│ │ └── renderer_lcd.c/h # LCD 渲染 HAL 接口 +│ └── menu/ # 📋 Menu 页面的 MVP 实现 +│ ├── def.h +│ ├── model.c/h +│ ├── presenter.c/h +│ ├── view.c/h # 菜单渲染绘制 +│ └── page.c/h # 页面生命周期注册 +└── tests/ # 🧪 测试目录 + ├── CMakeLists.txt + ├── test_p0_remote_display.c + ├── test_p0_utf8_hz12_get.c + ├── test_p1_key.c + ├── test_p1_lcd_basic.c + ├── test_p1_menu.c + ├── test_p1_page_manager.c + └── test_p2_tcp_loopback.c +``` +### 4.1 LCD 驱动层 +- 文件夹:`src/Drv/lcd` +- 职责:提供屏幕的显示 ```mermaid block-beta columns 1 - a["菜单"] + block:menu + a["LCD"] + end + block:textdraw columns 2 - a1["文字显示"] a2["图像显示"] + a1["文字显示"] a2["图像显示"] end + block:draw columns 2 block:text_group @@ -47,6 +110,7 @@ columns 1 end b1["utf8_next"] end + block:draw_group columns 4 c1["Lcd_LineH"] @@ -54,588 +118,512 @@ columns 1 c3["Lcd_Invert"] c4["Lcd_FillRect"] end + end - block:lcd + +block:lcd lcda["Lcd_Init"] lcdb["Lcd_SetPixel"] lcdc["Lcd_GetPixel"] - end +end ``` -### 4.1 应用层 +### 4.2 按键驱动层 -- 文件:`src/main.c` +- 文件:`src/Drv/menu.c`、`src/Drv/menu.h` - 职责: - - 系统初始化(菜单、按键、TCP 线程) - - 主循环调度(菜单路由、周期显示刷新) - - 生命周期管理(线程退出、网络清理) - -### 4.2 菜单业务层 - -- 文件:`src/Drv/menu/`、`src/Drv/menu/app/menu.h` -- 职责: - - 采用 MVP 分层组织菜单模块(`Model / Presenter / View`) - - 基于静态菜单模型构建运行时菜单树 - - 处理按键驱动的菜单状态迁移与路径重建 - - 执行菜单显示坐标计算与多级菜单渲染调度 - + - 基于静态菜单模型构建运行时菜单树 + - 处理按键驱动的菜单状态迁移 + - 执行菜单显示坐标计算与多级菜单渲染调度 ### 4.3 多页面管理层设计 +#### 4.3.1 页面管理器设计 +主要是通过页面管理器对不同页面之间切换进行调度。 + +**多页面目录结构设计:** +```txt +src/ +└── Drv/ + └── pages/ # 所有页面模块根目录 + ├── page_manager.c # 页面管理实现 + ├── page_manager.h # 页面管理头文件 + ├── global/ # 全局状态管理器 + 页面交互中间件 + │ ├── renderer_lcd.c # 抽象LCD底层,统一渲染接口 + │ └── renderer_lcd.h # 抽象LCD底层,统一渲染接口 + ├── menu/ # 菜单页面(MVP架构) + │ ├── model.c + │ ├── model.h + │ ├── view.c + │ ├── view.h + │ ├── presenter.c + │ ├── presenter.h + │ ├── page.c + │ └── page.h + └── (other pages) # 其他页面模块 +``` +###### 数据结构设计 +```c +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; + +/* 页面导航栈,记录着页面进入的顺序 */ +page_stack = [page_id0, page_id1, page_id2, page_id3] +page_registry /*存储被页面管理器的页面*/ +``` +##### 函数设计 +页面管理器 Page Manager 主要函数: +* `void PageManager_Init(void)` +* `int PageManager_Register(page_t *page)`:将页面对象注册到页面管理器的注册表中,供后续按 page_id 查找与导航。 +* `int PageManager_Navigate(page_id_t pageId)`: 根据目标 page_id 执行页面导航:先在注册表中查找页面,再将其压入页面栈。 + 1. 在`page_registry`中找到对应的页面 `pageId` + 2. 找到`page_stack` 的栈顶页面 `currentTop = PageManager_GetTop()` + 3. 栈顶页面执行退出 `currentTop->on_exit(currentTop)` + 4. 将 `pageId` 放到 `page_stack` 顶部 + 5. 如果页面没有创建则执行创建页面 `newPage->on_create(newPage)` + 6. 执行页面进入 `newPage->on_enter(newPage)` +* `void PageManager_Loop(void)`: 驱动当前栈顶页面的循环回调(on_loop),用于执行周期性任务。 +* `void PageManager_DispatchEvent(input_event_t *event)`: 将输入事件分发到当前栈顶页面,并在页面未处理时执行全局兜底处理。 +* `static void PageManager_GlobalEventHandle(input_event_t *event)`: 页面管理器的全局事件兜底处理入口,用于处理未被页面消费的通用按键逻辑。 +* `page_t *PageManager_Find(page_id_t pageId)`: 在管理器的页面注册表中按 page_id 线性查找,返回对应的 page_t 指针。 +* `page_t *PageManager_GetTop(void)`: 返回当前页面栈栈顶元素,即当前“前台/可见”页面对应的 page_t 指针。 +* `int PageManager_Push(page_t *newPage)`: 将指定页面压入页面栈并切换为当前页:必要时触发旧页 on_exit、新页 on_create/on_enter。 +* `int PageManager_Pop(void)`: 弹出当前栈顶页面并回到下一层:触发当前页 on_exit,非缓存页可 on_destroy, +#### 4.3.2 标准化页面抽象(Page 接口层) +这是整个系统的基础单元,**每个独立业务页面对应一个 Page 实例,每个 Page 内部封装一套完全独立的 MVP 三元组**,对外仅暴露标准接口,页面管理器仅通过标准接口与页面交互,完全不关心页面内部的 MVP 实现,实现页面间的彻底解耦。 +```c +/* ------------------------------------------------------------------------- + * 枚举名: 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; +}; + + + +**菜单页面首次进入流程图:** +```txt +[系统启动] + | + v +MenuPage_GetInstance() + | + v +返回 &s_menuPage 给 PageManager_Register(...) + | + v +PageManager_Navigate(PAGE_ID_MENU) + | + v +PageManager_Push(s_menuPage) + | + +--> (首次进入) MenuPage_OnCreate(page) + | | + | +--> MenuModel_Init(&s_model) // 1. 模型初始化 + | +--> MenuView_Init(&s_view) // 2. 视图初始化 + | +--> MenuPresenter_Init(...) // 3. 主持器初始化 + | +--> page/s_menuPage 绑定 model/view/presenter + | + +--> MenuPage_OnEnter(page) + | + +--> s_presenter.dspCtrl.bFirst = 1 + +--> MenuPresenter_Refresh(&s_presenter) // 首帧刷新 +``` +#### 4.3.3 页面标准接口与生命周期定义 +嵌入式场景下,生命周期钩子的设计必须严格对应资源的申请 / 释放时机,避免 RAM 浪费和低功耗异常,每个钩子的执行时机、职责完全固定,禁止越权操作。 +```c +/* ------------------------------------------------------------------------- + * 模块内静态对象说明: + * 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) +{ + memset(&s_menuPage, 0, sizeof(s_menuPage)); + /* 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; + + 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; +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_GetInstance + * 功能: + * 获取菜单页静态实例指针,供 PageManager_Register() 注册使用。 + * + * 参数: + * 无 + * + * 边界处理: + * - 返回文件内静态对象地址,无空指针分支。 + * + * 说明: + * - 该函数仅暴露页面入口,不负责初始化;初始化由 on_create 生命周期完成。 + * + * 返回值: + * - 指向静态页面对象 s_menuPage 的 page_t* 指针 + * ------------------------------------------------------------------------- */ +page_t *MenuPage_GetInstance(void) +{ + return &s_menuPage; +} +``` +### 菜单页面设计 + **设计思想:** 基于经典的 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/pages/...`: 其他页面 - -整个多页面管理系统分为 5 个核心模块,从上到下形成完整的调度闭环,与底层 MVP 架构无缝衔接 + +**菜单页面的目录设计:** +```txt +菜单页面的目录结构设计: +src/ +└── Drv/ + └── pages/ + └── menu/ # 项目菜单逻辑根目录 + ├── model.c # 菜单模型层实现 + ├── model.h # 菜单模型层头文件 + ├── view.c # 菜单视图层实现 + ├── view.h # 菜单视图层头文件 + ├── presenter.c # 菜单控制层实现 + ├── presenter.h # 菜单控制层头文件 + ├── page.c # 菜单页面逻辑实现 + └── page.h # 菜单页面逻辑头文件 ``` -┌─────────────────────────────────────────────────────────────┐ -│ 全局状态管理器(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` 仅作为“页面路由入口”,不再承载页面业务实现。 - +当有外部的事件产生时,MVP 架构的数据流图如下: + ### 4.4 输入层 - - 文件:`src/Drv/key.c`、`src/Drv/key.h` - 职责: - - 提供按键读写状态控制(消费式读取) - - 接收远程模块写入的按键事件并供菜单模块读取 - + - 提供按键读写状态控制(消费式读取) + - 接收远程模块写入的按键事件并供菜单模块读取 ### 4.5 远程显示通信层 - 文件:`src/remoteDisplay.c`、`src/remoteDisplay.h` - 职责: - - 实现 RemoDispBus 协议解析与回复 - - 提供 TCP 服务器线程入口与启动逻辑 - - 处理保活、初始化、按键下发、显存上传等命令 + - 实现 RemoDispBus 协议解析与回复 + - 提供 TCP 服务器线程入口与启动逻辑 + - 处理保活、初始化、按键下发、显存上传等命令 ### 4.6 平台适配层 - 文件:`src/TCP/tcp.c`、`src/thread_utils.c` - 职责: - - 提供跨平台 socket 与线程封装 - - 隔离 Windows/Linux API 差异 + - 提供跨平台 socket 与线程封装 + - 隔离 Windows/Linux API 差异 ## 5. 核心数据结构设计 ### 5.1 菜单模型与菜单树 -- 静态菜单模型:`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` 内部私有) +- 静态菜单模型:`tagMenuModel`(定义于 `display.h`,数据在 `display.c`) +- 运行时菜单项:`tagMenuItem`(定义于 `menu.c` 内) +- 全局控制:`g_tMenuCtrl`、`g_tDspCtrl` 关键关系: @@ -646,16 +634,16 @@ event_result_t settings_view_handle_event(settings_view_interface_t *view, input ### 5.2 显示控制结构 - `tagScreenControl g_tCVsr`: - - 显存缓冲 `pwbyLCDMemory` - - 前景/背景色 - - ASCII 与汉字字体参数 + - 显存缓冲 `pwbyLCDMemory` + - 前景/背景色 + - ASCII 与汉字字体参数 ### 5.3 远程按键结构 - `tagRKeyCtrl g_tRemoteKey`: - - `byKeyValid`:是否有新按键 - - `byKeyValue`:按键值 - - `bUseRkey`:远程按键开关(当前实现中初始化为启用) + - `byKeyValid`:是否有新按键 + - `byKeyValue`:按键值 + - `bUseRkey`:远程按键开关(当前实现中初始化为启用) ## 6. 关键业务流程 @@ -665,7 +653,7 @@ event_result_t settings_view_handle_event(settings_view_interface_t *view, input [系统初始化] | v -[MenuApp_PollInput] +[Menu_Route] | v [Sleep 20ms] @@ -674,98 +662,20 @@ event_result_t settings_view_handle_event(settings_view_interface_t *view, input [计数器累加] | v -[是否到刷新周期?] --否--> [MenuApp_PollInput] +[是否到刷新周期?] --否--> [Menu_Route] | - +--是--> [MenuApp_Render] --> [MenuApp_PollInput] + +--是--> [Menu_Show_Proc] --> [Menu_Route] ``` ### 6.2 菜单交互流程 - 输入来源:`Key_Read()`(含远程写入按键) - 行为: - - 上/下:同级循环移动 - - 左/ESC:回退上级或退回主层 - - 右/确认:进入子级或执行叶子回调 + - 上/下:同级循环移动 + - 左/ESC:回退上级或退回主层 + - 右/确认:进入子级或执行叶子回调 - 渲染: - - `MenuApp_Render` 调用 Presenter/View,根据路径进行增量刷新或全量刷新 - -### 6.4 当前 Menu 详细设计(MVP) - -#### 6.4.1 模块拆分 - -- `src/Drv/menu/app/menu.c` - - 菜单应用 Facade(`MenuApp_*` 对外接口) - - 负责 PageManager 初始化与 `MenuPage` 生命周期托管 -- `src/Drv/pages/menu/presenter/menu_presenter.c` - - 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新 -- `src/Drv/pages/menu/model/menu_model.c` - - 模型初始化:树构建、菜单名修饰、初始状态建立 -- `src/Drv/pages/menu/view/menu_view.c` - - 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略 -- `src/Drv/pages/menu/presenter/menu_navigator.c` - - 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染) -- `src/Drv/pages/menu/view/menu_layout.c` - - 菜单布局计算:宽度统计、层级矩形定位、越界回退策略 -- `src/Drv/pages/menu/model/menu_model.c`(包含静态菜单表数据) - - 静态菜单模型表 `g_tMenuModelTab`(业务菜单定义) - -#### 6.4.2 运行时调用时序 - -```text -MenuApp_Init - -> MenuPresenter_Setup - -> MenuPresenter_InitModel - -> MenuModel_Bootstrap - -> MenuTree_0LevelNumCal - -> MenuTree_MainCreate - -> MenuView_Layout - -> MenuLayout_PositionCal - -主循环: - MenuApp_PollInput - -> Key_Read - -> MenuApp_HandleInput(key) - -> MenuPresenter_HandleInput - -> MenuNavigator_ProcessKey - -> (needRefresh) MenuNavigator_RebuildRoute - -> MenuPresenter_Refresh - -> MenuView_RenderByState -``` - -#### 6.4.3 关键状态数据 - -- `tagMenuItem` - - 菜单节点实体,包含树关系(`ptHigher/ptLower/ptBefore/ptBehind`)与显示矩形 -- `tagMenuCtrl` - - 导航核心状态(`ptCurrent`、`ptRoute[4]`、`ptCurBak`、`pt0Level` 等) -- `tagDspCtrl` - - 显示控制状态(当前主要使用首帧标记 `bFirst`) -- `MenuNavState / MenuNavResult` - - Presenter 与 Navigator 之间的状态快照与处理结果 - -#### 6.4.4 对外数据接口(当前基线) - -- `void MenuApp_Init(void)` - - 初始化菜单应用(Presenter/Model/View) -- `void MenuApp_HandleInput(uint8_t keyVal)` - - 注入输入事件并驱动导航状态变更 -- `void MenuApp_PollInput(void)` - - 从 `Key_Read()` 读取按键并转发到 `MenuApp_HandleInput` -- `void MenuApp_Render(void)` - - 主动触发一次渲染(用于周期刷新或外部强制重绘) -- `const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count)` - - 只读导出当前菜单项数组(调试/测试使用) - -#### 6.4.5 内部接口边界(模块间) - -- Model -> View - - 仅通过控制状态与布局结果共享,不直接调用绘制原语 -- Presenter -> Model - - 仅在初始化阶段触发 `MenuModel_Bootstrap` -- Presenter -> View - - 通过 `MenuPresenter_Refresh` 驱动渲染,不暴露底层 LCD 细节 -- Presenter -> Navigator - - 通过 `MenuNavState` 快照交互,避免导航器直接操作外部全局变量 + - `Menu_Show_Proc` 根据路径增量刷新或全量刷新 ### 6.3 远程显示协议流程 @@ -793,9 +703,9 @@ MenuApp_Init ## 7. 并发与线程模型 - 主线程: - - 负责菜单路由与本地显示刷新 + - 负责菜单路由与本地显示刷新 - TCP 服务器线程: - - 监听连接、解析协议、处理远程请求 + - 监听连接、解析协议、处理远程请求 共享状态: @@ -810,81 +720,50 @@ MenuApp_Init - 构建系统:CMake(`C_STANDARD 99`) - 可执行目标:`DTU-HMI` - 平台链接: - - Windows:`ws2_32` - - Linux/macOS:`pthread` + - Windows:`ws2_32` + - Linux/macOS:`pthread` - 可选调试:`ENABLE_DEBUG=ON` 自动定义 `DEBUG` 宏 ## 9. 测试架构 - 测试目录:`tests/` - - 框架:`ctest + 自定义断言宏` - 分层策略: - - P0:纯逻辑单元测试(协议解析、UTF-8 解析、字库查找) - - P1:状态/计算单测(按键、菜单、LCD 基础像素操作) - - P2:集成测试(TCP 回环) - + - P0:纯逻辑单元测试(协议解析、UTF-8 解析、字库查找) + - P1:状态/计算单测(按键、菜单、LCD 基础像素操作) + - P2:集成测试(TCP 回环) 建议执行命令: - ```bash cmake -S . -B build cmake --build build ctest --test-dir build -C Debug --output-on-failure ``` - +维护命令: +```bash +tasklist /FI "IMAGENAME eq DTU-HMI.exe" #查找程序 PID +taskkill /PID 66464 /F #根据 PID 关闭程序 +``` ## 10. 模块依赖关系(代码级) - `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils` -- `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` +- `menu.c` 依赖:`lcd`、`display`、`key` - `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils` - `lcd.c` 依赖:`ascii` -- `src/Drv/pages/menu/model/menu_model.c` 提供:静态菜单表(被 `MenuPage` 使用) +- `display.c` 提供:静态菜单表(被 `menu.c` 使用) ## 11. 已知风险与改进建议 - **并发一致性风险**:远程线程与主线程共享状态无锁访问。 - - 建议:为显存快照与按键事件引入互斥保护或双缓冲。 + - 建议:为显存快照与按键事件引入互斥保护或双缓冲。 - **协议缓冲鲁棒性**:当前异常数据采用清空缓冲策略,存在丢包窗口。 - - 建议:增加更精细的帧边界恢复策略与统计日志。 + - 建议:增加更精细的帧边界恢复策略与统计日志。 - **可测试性边界**:部分逻辑仍与全局状态耦合较深。 - - 建议:逐步引入接口注入(如 `TcpOps`、`delay_ms`)降低耦合。 + - 建议:逐步引入接口注入(如 `TcpOps`、`delay_ms`)降低耦合。 ## 12. 版本与维护 - 文档版本:v1.0 - 适配代码基线:当前 `DTU-HMI` 仓库主干实现 - 维护建议: - - 每次新增模块或调整主流程时同步更新本文档 - - 测试策略更新需同步维护“第 9 章 测试架构” - -## 13. 菜单重构故障复盘(2026-03) - -### 13.1 现象 - -- 菜单分层重构后,程序在启动阶段(当前入口为 `MenuApp_Init`)出现访问冲突,表现为“运行即崩溃”。 - -### 13.2 根因 - -- 根因位于菜单树构建模块 `MenuTree_MainCreate`。 -- 在“层级回退(`byCurClass > byNextClass`)”分支中,缺少对中间层级链表的及时收口(首尾成环)处理。 -- 后续 `Menu_PositionCal` 在遍历同级链表时访问到异常节点,导致崩溃。 - -### 13.3 修复措施 - -- 恢复并对齐原稳定逻辑: - - 当层级回退时,立即对回退区间层级执行首尾成环收口。 - - 每轮处理后更新 `ptCurrent = ptNextNode`,保证状态推进一致。 - - 循环结束后按当前实际层级执行最终收口。 - -### 13.4 回归防线 - -- 新增启动路径集成回归用例:`tests/test_p2_menu_runtime_startup.c`。 -- 覆盖最易回归的启动路径: - - `MenuApp_Init()` - - `Key_Init()` - - 首次 `MenuApp_PollInput()` - - `MenuApp_Render()` -- 该用例用于防止“菜单树可编译但启动崩溃”的问题再次进入主干。 + - 每次新增模块或调整主流程时同步更新本文档 + - 测试策略更新需同步维护“第 9 章 测试架构” diff --git a/docs/绘图/.$HMI 图像绘制.drawio.dtmp b/docs/绘图/.$HMI 图像绘制.drawio.dtmp deleted file mode 100644 index 78274f3..0000000 --- a/docs/绘图/.$HMI 图像绘制.drawio.dtmp +++ /dev/null @@ -1,506 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/绘图/HMI 图像绘制.drawio b/docs/绘图/HMI 图像绘制.drawio index 5b31e8f..9550148 100644 --- a/docs/绘图/HMI 图像绘制.drawio +++ b/docs/绘图/HMI 图像绘制.drawio @@ -1,4 +1,4 @@ - + @@ -437,7 +437,7 @@ - + @@ -503,4 +503,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/include/types.h b/include/types.h index 5806a9c..6bfe0a5 100644 --- a/include/types.h +++ b/include/types.h @@ -30,5 +30,12 @@ typedef int (*FUNCPTR) ( ); #define ASSERT(expr) ((void)0) // 发布模式禁用断言 #endif +#ifdef DEBUG +#define LOG(format, ...) \ + printf(format, ##__VA_ARGS__) +#else +#define LOG(format, ...) ((void)0) // 发布模式禁用断言 +#endif + #endif diff --git a/src/Drv/lcd/lcd_draw.c b/src/Drv/lcd/lcd_draw.c index 36ecc96..b543295 100644 --- a/src/Drv/lcd/lcd_draw.c +++ b/src/Drv/lcd/lcd_draw.c @@ -52,10 +52,13 @@ static int8_t Lcd_ColorCheck(uint32_t color) * - LCD_ERR_OUT_OF_RANGE: 坐标越界或顺序非法 * - LCD_ERR_INVALID_COLOR: 颜色非法 * ------------------------------------------------------------------------- */ -int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color) +int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint8_t color) { + ASSERT((right_x >= LCD_SIZE_X) || (bottom_y >= LCD_SIZE_Y)); if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR; + ASSERT((right_x >= LCD_SIZE_X) || (bottom_y >= LCD_SIZE_Y)); if ((right_x < left_x) || (bottom_y < top_y)) return LCD_ERR_OUT_OF_RANGE; + ASSERT((right_x >= LCD_SIZE_X) || (bottom_y >= LCD_SIZE_Y)); if ((right_x >= LCD_SIZE_X) || (bottom_y >= LCD_SIZE_Y)) return LCD_ERR_OUT_OF_RANGE; for (uint16_t y = top_y; y <= bottom_y; y++) { @@ -109,9 +112,13 @@ int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wW M_GuiSwap(wXEnd, wXStart); } wYEnd32 = (uint32_t)wYStart + (uint32_t)wWidth; + ASSERT((wXStart >= LCD_SIZE_X) || (wXEnd > LCD_SIZE_X)); if (wXStart >= LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; + ASSERT((wXEnd > LCD_SIZE_X) || (wYStart >= LCD_SIZE_Y)); if (wXEnd > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; /* 半开区间,end 可等于边界 */ + ASSERT((wYStart >= LCD_SIZE_Y) || (wYEnd32 > LCD_SIZE_Y)); if (wYStart >= LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; + ASSERT((wYEnd32 > LCD_SIZE_Y) || (wXStart >= LCD_SIZE_X)); if (wYEnd32 > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; /* 半开区间,end 可等于边界 */ wYEnd = (uint16_t)wYEnd32; @@ -157,9 +164,13 @@ int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wW M_GuiSwap(wYEnd, wYStart); } wXEnd32 = (uint32_t)wXStart + (uint32_t)wWidth; + ASSERT((wYStart >= LCD_SIZE_Y) || (wXStart >= LCD_SIZE_X)); if (wYStart >= LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; + ASSERT((wYEnd > LCD_SIZE_Y) || (wXStart >= LCD_SIZE_X)); if (wYEnd > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; /* 半开区间,end 可等于边界 */ + ASSERT((wXStart >= LCD_SIZE_X) || (wXEnd32 > LCD_SIZE_X)); if (wXStart >= LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; + ASSERT((wXEnd32 > LCD_SIZE_X) || (wYStart >= LCD_SIZE_Y)); if (wXEnd32 > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; /* 半开区间,end 可等于边界 */ wXEnd = (uint16_t)wXEnd32; diff --git a/src/Drv/lcd/lcd_draw.h b/src/Drv/lcd/lcd_draw.h index d64c437..93980c2 100644 --- a/src/Drv/lcd/lcd_draw.h +++ b/src/Drv/lcd/lcd_draw.h @@ -5,7 +5,7 @@ -int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color); +int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint8_t color); int8_t Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend); int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color); int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color); diff --git a/src/Drv/lcd/lcd_text.c b/src/Drv/lcd/lcd_text.c index da5323c..71b26cf 100644 --- a/src/Drv/lcd/lcd_text.c +++ b/src/Drv/lcd/lcd_text.c @@ -21,7 +21,7 @@ #include "ascii.h" #include "lcd_draw.h" -static textConfig text_cfg = { +textConfig text_cfg = { .wGBFontWidth = 13, .wGBFontHeight = 12, .wASCIIFontWidth = 7, diff --git a/src/Drv/lcd/lcd_text.h b/src/Drv/lcd/lcd_text.h index 60f9dbb..4703e0a 100644 --- a/src/Drv/lcd/lcd_text.h +++ b/src/Drv/lcd/lcd_text.h @@ -13,6 +13,9 @@ typedef struct { uint16_t rowSpace; } textConfig; + +extern textConfig text_cfg; + int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString); int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString); diff --git a/src/Drv/menu/app/menu.c b/src/Drv/menu/app/menu.c index 5a9ae87..5de124f 100644 --- a/src/Drv/menu/app/menu.c +++ b/src/Drv/menu/app/menu.c @@ -1,12 +1,35 @@ #include "menu.h" -#include "../../pages/menu/page.h" +#include "../../pages/page_manager.h" void MenuProc_See_AppInfo(void) { - MenuPage_TriggerCurrentAction(); + (void)PageManager_Navigate(PAGE_ID_APP_INFO); } +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_TriggerCurrentAction + * 功能: + * 触发当前菜单项动作的占位接口(当前版本仅输出调试日志)。 + * + * 参数: + * 无 + * + * 边界处理: + * - 当前实现不依赖外部输入,不涉及参数校验。 + * + * 说明: + * - 现阶段用于保留动作触发扩展点,后续可接入真实业务动作执行链路。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ + void MenuPage_TriggerCurrentAction(void) + { + printf("MenuPage_TriggerCurrentAction\n"); + } + + void MenuProc_See_YC(void) { MenuPage_TriggerCurrentAction(); diff --git a/src/Drv/pages/AppInfo/def.h b/src/Drv/pages/AppInfo/def.h new file mode 100644 index 0000000..513be67 --- /dev/null +++ b/src/Drv/pages/AppInfo/def.h @@ -0,0 +1,9 @@ +#ifndef APPINFO_def_H +#define APPINFO_def_H + +#define CN_HEIGHT 12 +#define CN_ROWSPACE 2 +#define APPINFO_WITDTH 7 + + +#endif \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/model.c b/src/Drv/pages/AppInfo/model.c new file mode 100644 index 0000000..1508677 --- /dev/null +++ b/src/Drv/pages/AppInfo/model.c @@ -0,0 +1,15 @@ +#include "Drv/pages/AppInfo/model.h" + +#include + +void AppInfoModel_Init(appinfo_model_t *model) +{ + if (model == NULL) + { + return; + } + + memset(model, 0, sizeof(*model)); + /* 默认顶栏标题,与菜单「装置信息」条目一致 */ + (void)memcpy(model->topName, "装置信息", sizeof("装置信息")); +} diff --git a/src/Drv/pages/AppInfo/model.h b/src/Drv/pages/AppInfo/model.h new file mode 100644 index 0000000..3628074 --- /dev/null +++ b/src/Drv/pages/AppInfo/model.h @@ -0,0 +1,15 @@ +#ifndef APPINFO_MODEL_H +#define APPINFO_MODEL_H + +#include "types.h" + +typedef struct appinfo_model_t appinfo_model_t; + +struct appinfo_model_t +{ + uint8_t topName[32]; +}; + +void AppInfoModel_Init(appinfo_model_t *model); + +#endif \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/page.c b/src/Drv/pages/AppInfo/page.c new file mode 100644 index 0000000..fd7284e --- /dev/null +++ b/src/Drv/pages/AppInfo/page.c @@ -0,0 +1,218 @@ +#include +#include + +#include "Drv/pages/AppInfo/page.h" +#include "Drv/pages/AppInfo/model.h" +#include "Drv/pages/AppInfo/presenter.h" +#include "Drv/pages/AppInfo/view.h" +#include "Drv/pages/global/renderer_lcd.h" + +/* ------------------------------------------------------------------------- + * 模块内静态对象说明: + * s_model - AppInfo页 Model 实例(AppInfo数据与运行时结构) + * s_view - AppInfo页 View 实例(布局与渲染能力) + * s_presenter - AppInfo页 Presenter 实例(输入处理与状态驱动) + * s_page - 页面管理器可注册的 page_t 描述对象 + * + * 说明: + * - 以上对象均为文件内静态单例,生命周期覆盖进程运行期。 + * - 通过 AppInfoPage_GetInstance() 暴露 s_page 给 PageManager 注册。 + * ------------------------------------------------------------------------- */ +static appinfo_model_t s_model; +static appinfo_view_t s_view; +static appinfo_presenter_t s_presenter; +static page_t s_page; +static const PageRenderPort *s_port = NULL; +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_OnExit + * 功能: + * 页面退出回调占位点;当前版本无额外退出动作。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 (void)page 防止未使用参数告警。 + * + * 说明: + * - 预留给后续扩展(如停止定时任务、冻结动画、保存瞬时 UI 状态等)。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void Page_OnExit(page_t *page) +{ + s_port->clear_screen(); + (void)page; +} + +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_OnDestroy + * 功能: + * 页面销毁回调:清空菜单页内部三层对象(Model/View/Presenter)状态。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 memset 全量清零静态对象,避免残留状态影响后续重建。 + * + * 说明: + * - 与 is_cached 策略配合:当页面被标记为非缓存并弹栈销毁时,该函数用于复位。 + * - page_t 元信息不在此函数复位,由页面生命周期创建阶段重新赋值。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void Page_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)); +} + +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_OnEvent + * 功能: + * 页面事件回调:校验输入事件后,将按键转交 Presenter 处理。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * event - 输入事件指针 + * + * 边界处理: + * - event 为 NULL 时返回 EVENT_UNHANDLED。 + * - 仅处理 PAGE_EVENT_KEY 事件类型,其它类型返回 EVENT_UNHANDLED。 + * - keyVal 为 0 视为无效按键,返回 EVENT_UNHANDLED。 + * + * 说明: + * - 事件有效时调用 Presenter 对外输入接口执行业务流转。 + * - 返回 EVENT_HANDLED,表示该事件已被菜单页消费,不再交给上层页面逻辑。 + * + * 返回值: + * - EVENT_HANDLED : 事件已处理 + * - EVENT_UNHANDLED : 事件无效或不属于本页处理范围 + * ------------------------------------------------------------------------- */ +static event_result_t Page_OnEvent(page_t *page, input_event_t *event) +{ + (void)page; + if ((event == NULL) || (event->type != PAGE_EVENT_KEY) || (event->keyVal == 0)) + { + return EVENT_UNHANDLED; + } + + s_presenter.handle_input(&s_presenter, event->keyVal); + return EVENT_HANDLED; +} + +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_OnLoop + * 功能: + * 页面循环回调:周期性驱动 Presenter 执行刷新逻辑。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 使用 (void)page 防止未使用参数告警。 + * + * 说明: + * - 实际刷新策略(全量/增量)由 Presenter 内部状态控制。 + * - 该函数通常由 PageManager_Loop() 在主循环节拍中调用。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void Page_OnLoop(page_t *page) +{ + (void)page; + s_presenter.refresh(&s_presenter); +} + +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_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 Page_OnCreate(page_t *page) +{ + /* 1) model init */ + AppInfoModel_Init(&s_model); + + /* 2) view init */ + AppInfoView_Init(&s_view); + + /* 3) presenter setup + runtime build */ + AppInfoPresenter_Init(&s_presenter, &s_model, &s_view); + + + page->model = &s_model; + page->presenter = &s_presenter; + page->view = &s_view; + + s_page.presenter = &s_presenter; + s_page.view = &s_view; + s_page.model = &s_model; + s_port = PageRenderer_Lcd(); +} + +/* ------------------------------------------------------------------------- + * 函数名: AppInfoPage_OnEnter + * 功能: + * 页面进入回调:将菜单渲染标记为“首帧全量刷新”,并立即触发一次刷新。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 本函数不依赖 page 内容,统一转为 (void)page 消除未使用告警。 + * + * 说明: + * - 通过 refresh(..., 1) 告知 Presenter 下一次刷新走首帧路径。 + * - 进入页面后立即刷新,确保界面可见状态与内部状态同步。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ + static void Page_OnEnter(page_t *page) + { + (void)page; + /* 首帧全量刷新 */ + LOG("AppInfoPage_OnEnter\n"); + s_presenter.refresh(&s_presenter); + } + +page_t *AppInfoPage_GetInstance(void) +{ + /* 确保在注册到 PageManager 前,页面生命周期回调已就绪 */ + memset(&s_page, 0, sizeof(s_page)); + s_page.page_id = PAGE_ID_APP_INFO; + s_page.is_cached = 1; + s_page.on_create = Page_OnCreate; + s_page.on_enter = Page_OnEnter; + s_page.on_exit = Page_OnExit; + s_page.on_destroy = Page_OnDestroy; + s_page.on_event = Page_OnEvent; + s_page.on_loop = Page_OnLoop; + return &s_page; +} diff --git a/src/Drv/pages/AppInfo/page.h b/src/Drv/pages/AppInfo/page.h new file mode 100644 index 0000000..53c5a45 --- /dev/null +++ b/src/Drv/pages/AppInfo/page.h @@ -0,0 +1,9 @@ +#ifndef APPINFO_PAGE_H +#define APPINFO_PAGE_H + +#include "Drv/pages/page.h" + +page_t *AppInfoPage_GetInstance(void); +void AppInfoPage_TriggerCurrentAction(void); + +#endif \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/presenter.c b/src/Drv/pages/AppInfo/presenter.c new file mode 100644 index 0000000..c0aecb5 --- /dev/null +++ b/src/Drv/pages/AppInfo/presenter.c @@ -0,0 +1,90 @@ +#include "Drv/pages/AppInfo/presenter.h" +#include "Drv/pages/page_manager.h" +#include "Drv/key.h" +#include + +static void AppInfoPresenter_HandleInput(appinfo_presenter_t *presenter, uint8_t keyVal) +{ + switch (keyVal) + { + case KEY_ESC: + (void)PageManager_Pop(); + break; + default: break; + } +} + +/* ------------------------------------------------------------------------- + * 函数名:MenuPresenter_Refresh + * 功能: + * Presenter 层的对外刷新接口,根据是否为首帧渲染选择不同的刷新策略。 + * 本函数作为 menu_presenter_t::refresh 回调被调用,负责协调 View 层完成界面更新。 + * + * 参数: + * presenter - 菜单 Presenter 实例指针,内部持有 menuCtrl 上下文和 view 接口引用 + * isFirstFrame - 是否为首帧标志: + * 1 - 首帧渲染:需要完整初始化并刷新整个菜单界面 + * 0 - 非首帧渲染:根据当前状态差异进行增量或局部刷新 + * + * 刷新策略: + * 首帧模式 (isFirstFrame == 1): + * - 调用 View 层的 full_refresh() 接口,完整重绘整个菜单界面。 + * - 此时 menuCtrl 中的导航路径、选中项等状态已完成初始化,可安全进行全量渲染。 + * - MODE_NONE 表示不进行特殊模式过滤(如调试模式、特定层级过滤等)。 + * + * 非首帧模式 (isFirstFrame == 0): + * - 调用 MenuPresenter_RenderByState() 进行智能状态对比刷新: + * 1) 比较 pt0Level 与 ptRoute[0],判断顶层上下文是否切换 + * 2) 比较 ptCurBak 与 ptCurrent,判断选中项是否变化 + * 3) 根据层级关系和父节点关联选择最优渲染策略: + * - 同层同父:局部反显更新(旧选中恢复 + 新选中高亮) + * - 新层级 >= 旧层级:补绘受影响层级 + * - 回退到更高层:整页刷新保证一致性 + * - 此策略可避免不必要的重绘,提升界面响应性能。 + * + * 调用关系: + * - 被 MenuPresenter_HandleInput() 在导航状态变化后调用(isFirstFrame = 0) + * - 被外部初始化完成后首次调用(isFirstFrame = 1) + * - 通过 presenter->refresh 函数指针绑定,符合 Presenter 的接口抽象。 + * + * 边界处理: + * - 本函数假设 presenter 指针有效,不做空指针校验(由调用方保证)。 + * - view 接口的 full_refresh() 和内部渲染逻辑负责具体的绘制安全校验。 + * ------------------------------------------------------------------------- */ + static void AppInfoPresenter_Refresh(appinfo_presenter_t *presenter) + { + presenter->view->show_top_name(presenter->model->topName); + } +/* ------------------------------------------------------------------------- + * 函数名: MenuPresenter_Init + * 功能: + * 初始化菜单 Presenter 实例,完成依赖绑定、对外接口挂接以及初始导航状态装配。 + * + * 参数: + * presenter - 待初始化的 Presenter 实例 + * model - 菜单 Model 实例,提供菜单树与运行时数据 + * view - 菜单 View 实例,提供刷新与绘制能力 + * + * 边界处理: + * - 若 `presenter`、`model` 或 `view` 任一为空,则直接返回。 + * + * 说明: + + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ + void AppInfoPresenter_Init(appinfo_presenter_t *presenter, appinfo_model_t *model, appinfo_view_t *view) + { + ASSERT((presenter == NULL) || (model == NULL) || (view == NULL)); + if ((presenter == NULL) || (model == NULL) || (view == NULL)) + { + return; + } + + memset(presenter, 0, sizeof(*presenter)); + presenter->model = model; + presenter->view = view; + presenter->handle_input = AppInfoPresenter_HandleInput; + presenter->refresh = AppInfoPresenter_Refresh; + } \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/presenter.h b/src/Drv/pages/AppInfo/presenter.h new file mode 100644 index 0000000..c121712 --- /dev/null +++ b/src/Drv/pages/AppInfo/presenter.h @@ -0,0 +1,18 @@ +#ifndef APPINFO_PRESENTER_H +#define APPINFO_PRESENTER_H + +#include "Drv/pages/AppInfo/model.h" +#include "Drv/pages/AppInfo/view.h" + +typedef struct appinfo_presenter_t appinfo_presenter_t; + +struct appinfo_presenter_t +{ + appinfo_model_t *model; + appinfo_view_t *view; + void (*handle_input)(appinfo_presenter_t *presenter, uint8_t keyVal); + void (*refresh)(appinfo_presenter_t *presenter); +}; + +void AppInfoPresenter_Init(appinfo_presenter_t *presenter, appinfo_model_t *model, appinfo_view_t *view); +#endif \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/view.c b/src/Drv/pages/AppInfo/view.c new file mode 100644 index 0000000..895f36f --- /dev/null +++ b/src/Drv/pages/AppInfo/view.c @@ -0,0 +1,97 @@ + +#include "Drv/pages/AppInfo/view.h" +#include + +/* MSVC 对含多字节/中文格式串的静态分析可能误报 C4474/C4996。 + * 本文件内的格式化输出均为受控 buffer,并用于显示文本渲染。 */ +#if defined(_MSC_VER) +#pragma warning(disable : 4474) +#pragma warning(disable : 4996) +#endif + +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 AppInfoView_DrawMeitou(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); +} + +void AppInfoView_ShowInfoPage(uint16_t wPageNum, uint16_t wPageMax ) +{ + /* show_str 期望的是 '\0' 结尾的 UTF-8 字符串 */ + char pbyTips[64]; + uint16_t y = s_port->get_size_y() - 16; + + /* 格式化字符串(用于 LCD 文本显示) */ +#if defined(_MSC_VER) + (void)sprintf_s(pbyTips, sizeof(pbyTips), "第%4u 页 , 共%4u 页", + (unsigned)wPageNum, (unsigned)wPageMax); +#else + (void)snprintf(pbyTips, sizeof(pbyTips), "第%4u 页 , 共%4u 页", + (unsigned)wPageNum, (unsigned)wPageMax); +#endif + + s_port->show_str(6, y, (uint8_t *)pbyTips); +} + +void AppInfoView_ShowTerminalInfo(void) +{ + uint8_t *context[] = { + "终端类型 : F30", + "终端型号 : F30", + "软件版本 : SV0.010", + "硬件版本 : HW0.010", + "软件校验 : 4454", + "程序日期 : 2024.08.27" + }; + s_port->show_str(32, 26, "馈线自动化终端"); + for(uint8_t index = 0; index < 6; index++ ) + { + s_port->show_str(6, 26 + (index + 1) * (s_port->get_ascii_height() + s_port->get_row_space()), context[index]); + } +} + +void AppInfoView_ShowTopName(uint8_t *name) +{ + uint16_t wLen = s_port->get_utf8_len(name) * s_port->get_ascii_width(); + + s_port->fill_rect(0, 0, s_port->get_size_x() - 1, s_port->get_size_y() - 1, s_port->get_color_back()); + /* 显示顶部名称,居中显示 */ + s_port->show_str((s_port->get_size_x() - wLen) / 2, 3, name); + + AppInfoView_DrawMeitou(18, 2); + AppInfoView_ShowInfoPage(10, 1); + AppInfoView_ShowTerminalInfo(); +} +void AppInfoView_Init(appinfo_view_t *view) +{ + s_port = PageRenderer_Lcd(); + view->show_top_name = AppInfoView_ShowTopName; +} \ No newline at end of file diff --git a/src/Drv/pages/AppInfo/view.h b/src/Drv/pages/AppInfo/view.h new file mode 100644 index 0000000..9e94a11 --- /dev/null +++ b/src/Drv/pages/AppInfo/view.h @@ -0,0 +1,17 @@ +#ifndef APPINFO_VIEW_H +#define APPINFO_VIEW_H + +#include "types.h" +#include "Drv/pages/AppInfo/def.h" +#include "Drv/pages/global/renderer_lcd.h" + +typedef struct appinfo_view_t appinfo_view_t; + +struct appinfo_view_t +{ + void (*show_top_name)(uint8_t *name); +}; + +void AppInfoView_Init(appinfo_view_t *view); + +#endif \ No newline at end of file diff --git a/src/Drv/pages/global/renderer_lcd.c b/src/Drv/pages/global/renderer_lcd.c index 9952fcc..609bd97 100644 --- a/src/Drv/pages/global/renderer_lcd.c +++ b/src/Drv/pages/global/renderer_lcd.c @@ -4,31 +4,87 @@ #include "lcd_draw.h" #include "lcd_text.h" -static unsigned short PageRenderer_LcdSizeX(void) +static uint16_t PageRenderer_LcdSizeX(void) { return LCD_SIZE_X; } -static unsigned short PageRenderer_LcdSizeY(void) +static uint16_t PageRenderer_LcdSizeY(void) { return LCD_SIZE_Y; } -static unsigned char PageRenderer_LcdColorFont(void) +static uint8_t PageRenderer_LcdColorFont(void) { return LCD_FONT; } -static unsigned char PageRenderer_LcdColorBack(void) +static uint8_t PageRenderer_LcdColorBack(void) { return LCD_BACK; } - +static uint16_t PageRenderer_LcdGetASCIIWidth(void) +{ + return text_cfg.wASCIIFontWidth; +} +static uint16_t PageRenderer_LcdGetASCIIHeight(void) +{ + return text_cfg.wASCIIFontHeight; +} +/* ------------------------------------------------------------------------- + * 函数名: PageRenderer_LcdGetUtf8Len + * 功能: + * 计算 UTF-8 字符串在当前菜单显示规则下的“显示宽度”。 + * + * 参数: + * str - 待计算的 UTF-8 字符串 + * + * 边界处理: + * - 本函数不做空指针校验,调用方需保证 `str` 非空且为 `\0` 结尾。 + * - 依赖 `utf8_next()` 逐字符解析 UTF-8 编码;若输入非法,结果由底层解析行为决定。 + * + * 说明: + * - 当前显示规则约定: + * 1) ASCII/单字节字符宽度记为 1 + * 2) 多字节 UTF-8 字符(如中文)宽度记为 2 + * - 本函数返回的是“显示占位宽度”,不是原始字节长度。 + * - 该结果会被菜单布局函数用于计算菜单框宽度与字符串显示长度。 + * + * 返回值: + * - UTF-8 字符串的显示宽度 + * ------------------------------------------------------------------------- */ + static uint16_t PageRenderer_LcdGetUtf8Len(uint8_t *str) + { + uint16_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; +} +static uint16_t PageRenderer_LcdGetRowSpace(void) +{ + return text_cfg.rowSpace; +} +static void PageRenderer_ClearScreen(void) +{ + Lcd_FillRect(0, 0, LCD_SIZE_X - 1, LCD_SIZE_Y - 1, LCD_BACK); +} static const PageRenderPort g_lcd_port = { .get_size_x = PageRenderer_LcdSizeX, .get_size_y = PageRenderer_LcdSizeY, + .get_ascii_width = PageRenderer_LcdGetASCIIWidth, + .get_ascii_height = PageRenderer_LcdGetASCIIHeight, .get_color_font = PageRenderer_LcdColorFont, .get_color_back = PageRenderer_LcdColorBack, + .get_utf8_len = PageRenderer_LcdGetUtf8Len, + .get_row_space = PageRenderer_LcdGetRowSpace, .fill_rect = Lcd_FillRect, .line_h = Lcd_LineH, .line_v = Lcd_LineV, @@ -36,6 +92,7 @@ static const PageRenderPort g_lcd_port = { .set_pixel = Lcd_SetPixel, .invert = Lcd_Invert, .show_str = Lcd_ShowStr, + .clear_screen = PageRenderer_ClearScreen, }; const PageRenderPort *PageRenderer_Lcd(void) diff --git a/src/Drv/pages/global/renderer_lcd.h b/src/Drv/pages/global/renderer_lcd.h index c914986..703b184 100644 --- a/src/Drv/pages/global/renderer_lcd.h +++ b/src/Drv/pages/global/renderer_lcd.h @@ -1,19 +1,26 @@ #ifndef PAGE_RENDERER_LCD_H #define PAGE_RENDERER_LCD_H +#include "types.h" + typedef struct { - unsigned short (*get_size_x)(void); - unsigned short (*get_size_y)(void); - unsigned char (*get_color_font)(void); - unsigned char (*get_color_back)(void); - signed char (*fill_rect)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y, unsigned int color); - signed char (*line_h)(unsigned short x_start, unsigned short x_end, unsigned short y, unsigned short width, unsigned char color); - signed char (*line_v)(unsigned short y_start, unsigned short y_end, unsigned short x, unsigned short width, unsigned char color); - signed char (*line)(unsigned short x_start, unsigned short y_start, unsigned short x_end, unsigned short y_end, unsigned short width, unsigned char color); - 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); + uint16_t (*get_size_x)(void); + uint16_t (*get_size_y)(void); + uint8_t (*get_color_font)(void); + uint8_t (*get_color_back)(void); + uint16_t (*get_ascii_width)(void); + uint16_t (*get_ascii_height)(void); + uint16_t (*get_utf8_len)(uint8_t *str); + uint16_t (*get_row_space)(void); + int8_t (*fill_rect)(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint8_t color); + int8_t (*line_h)(uint16_t x_start, uint16_t x_end, uint16_t y, uint16_t width, uint8_t color); + int8_t (*line_v)(uint16_t y_start, uint16_t y_end, uint16_t x, uint16_t width, uint8_t color); + int8_t (*line)(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t width, uint8_t color); + int8_t (*set_pixel)(uint16_t x, uint16_t y, uint8_t color); + int8_t (*invert)(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y); + int8_t (*show_str)(uint16_t x, uint16_t y, uint8_t *text); + int8_t (*clear_screen)(void); } PageRenderPort; const PageRenderPort *PageRenderer_Lcd(void); diff --git a/src/Drv/pages/menu/page.c b/src/Drv/pages/menu/page.c index 34556b3..e2ffdb7 100644 --- a/src/Drv/pages/menu/page.c +++ b/src/Drv/pages/menu/page.c @@ -23,32 +23,6 @@ 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 * 功能: @@ -112,7 +86,7 @@ static void MenuPage_OnDestroy(page_t *page) * - keyVal 为 0 视为无效按键,返回 EVENT_UNHANDLED。 * * 说明: - * - 事件有效时调用 MenuPresenter_HandleInput() 执行业务输入流转。 + * - 事件有效时调用 Presenter 对外输入接口执行业务流转。 * - 返回 EVENT_HANDLED,表示该事件已被菜单页消费,不再交给上层页面逻辑。 * * 返回值: @@ -127,7 +101,7 @@ static event_result_t MenuPage_OnEvent(page_t *page, input_event_t *event) return EVENT_UNHANDLED; } - MenuPresenter_HandleInput(&s_presenter, event->keyVal); + s_presenter.handle_input(&s_presenter, event->keyVal); return EVENT_HANDLED; } @@ -152,7 +126,7 @@ static event_result_t MenuPage_OnEvent(page_t *page, input_event_t *event) static void MenuPage_OnLoop(page_t *page) { (void)page; - MenuPresenter_Refresh(&s_presenter); + s_presenter.refresh(&s_presenter, 0); } /* ------------------------------------------------------------------------- @@ -200,6 +174,31 @@ static void MenuPage_OnCreate(page_t *page) s_menuPage.model = &s_model; } +/* ------------------------------------------------------------------------- + * 函数名: MenuPage_OnEnter + * 功能: + * 页面进入回调:将菜单渲染标记为“首帧全量刷新”,并立即触发一次刷新。 + * + * 参数: + * page - 当前页面对象指针(本实现未直接使用) + * + * 边界处理: + * - 本函数不依赖 page 内容,统一转为 (void)page 消除未使用告警。 + * + * 说明: + * - 通过 refresh(..., 1) 告知 Presenter 下一次刷新走首帧路径。 + * - 进入页面后立即刷新,确保界面可见状态与内部状态同步。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ + static void MenuPage_OnEnter(page_t *page) + { + (void)page; + /* 首帧全量刷新 */ + s_presenter.refresh(&s_presenter, 1); + } + /* ------------------------------------------------------------------------- * 函数名: MenuPage_GetInstance * 功能: @@ -230,26 +229,4 @@ page_t *MenuPage_GetInstance(void) 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"); -} +} \ No newline at end of file diff --git a/src/Drv/pages/menu/page.h b/src/Drv/pages/menu/page.h index 41d1653..2c7a7b3 100644 --- a/src/Drv/pages/menu/page.h +++ b/src/Drv/pages/menu/page.h @@ -1,10 +1,8 @@ #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 index b5cd522..b5c602e 100644 --- a/src/Drv/pages/menu/presenter.c +++ b/src/Drv/pages/menu/presenter.c @@ -5,149 +5,256 @@ #include "../../key.h" -typedef struct +/* ------------------------------------------------------------------------- + * 枚举名:MenuNavResult + * 功能: + * 菜单导航操作后的刷新结果状态指示器。用于 `MenuNavigator_ProcessKey()` + * 函数返回值,标识按键操作是否导致菜单界面需要重新绘制。 + * + * 成员说明: + * MENU_NAV_NO_REFRESH (0) - 无刷新: + * 表示当前按键操作未引起菜单焦点或层级状态的变化, + * 或变化不足以触发界面重绘(例如 F1/F2 功能键、无效按键等)。 + * 调用方收到此值后不应执行额外的刷新操作。 + * + * MENU_NAV_REFRESH (1) - 需刷新: + * 表示当前按键操作导致菜单选中项或层级发生变化, + * 需要重建导航路径并触发界面刷新以反映新的状态。 + * + * 使用场景: + * - `MenuNavigator_ProcessKey()` 根据按键类型和当前菜单状态计算是否需要刷新 + * - `MenuPresenter_HandleInput()` 根据返回值决定是否调用路由重建和刷新接口 + * - 返回值为 MENU_NAV_REFRESH 时,会触发: + * 1) MenuNavigator_RebuildRoute() 重建当前导航路径 ptRoute[] + * 2) Presenter->refresh() 执行界面增量或整页刷新 + * + * 设计意图: + * - 通过返回值区分"需刷新"和"无需刷新"两种状态,避免不必要的界面重绘 + * - 将导航计算与渲染决策分离,符合关注点分离原则 + * ------------------------------------------------------------------------- */ +typedef enum { - tagPMenuItem ptHead; - tagPMenuItem ptCurrent; - tagPMenuItem ptRoute[4]; - tagPMenuItem ptCurBak; - tagPMenuItem pt0Level; -} MenuNavState; - -typedef struct -{ - uint8_t needRefresh; - uint8_t skipRenderThisRound; + MENU_NAV_NO_REFRESH = 0, ///< 无需刷新:按键未引起菜单焦点/层级变化(如 F1/F2、无效按键) + MENU_NAV_REFRESH = 1 ///< 需要刷新:按键导致选中项或层级改变,需重建路径并触发界面更新 } MenuNavResult; -static MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal) +/* ------------------------------------------------------------------------- + * 函数名:MenuNavigator_ProcessKey + * 功能: + * 处理菜单导航按键,根据当前选中项和输入按键更新导航状态,并返回是否需要刷新界面。 + * 本函数是菜单导航的核心逻辑,负责解析各类按键(方向键、确认键、返回键等)并执行 + * 相应的菜单层级跳转和选中项变更操作。 + * + * 参数: + * menuCtrl - 菜单运行时控制上下文指针,包含当前选中项、路径数组、各级菜单节点引用等状态 + * keyVal - 输入按键值(来自 KEY_F1, KEY_F2, KEY_U, KEY_D, KEY_L, KEY_R, KEY_ENT, KEY_ESC 等) + * + * 返回值: + * MenuNavResult - 导航结果枚举: + * MENU_NAV_NO_REFRESH (0) - 无需刷新:按键未引起状态变化(如 F1/F2、未知按键) + * MENU_NAV_REFRESH (1) - 需要刷新:选中项或层级发生变化,需触发界面更新 + * + * 按键处理逻辑: + * KEY_F1 / KEY_F2: + * 功能键,当前不执行任何导航操作,仅占用一个 case 分支。 + * + * KEY_U (Up - 上移): + * 将选中项移动到前一项(ptCurrent->links.before),标记需要刷新界面。 + * + * KEY_D (Down - 下移): + * 将选中项移动到后一项(ptCurrent->links.behind),标记需要刷新界面。 + * + * KEY_L (Left - 左移/返回上级): + * 向左移动或返回上一级菜单: + * 1) 根据当前层级从 ptRoute 数组获取对应层级的节点 + * 2) 若为 0 级菜单(byClass == 0): + * - wPos == 1 时:向前跳过两个节点后下探到子级 + * - wPos != 1 时:向前一个节点后下探到子级 + * 3) 标记需要刷新界面。 + * + * KEY_R / KEY_ENT (Right/Enter - 右移/确认): + * 向右移动或进入子菜单: + * a) 若当前项有子菜单(links.lower != NULL): + * 下探到子菜单节点,标记需要刷新。 + * b) 否则若当前项绑定了窗口回调函数(pfnWinProc != NULL): + * 直接执行该回调函数(如打开弹窗、启动新界面等)。 + * + * KEY_ESC (Escape - 返回/退出): + * 返回或退出菜单: + * a) 若当前在 1 级菜单(byClass == 1): + * 重置导航状态到初始根节点,快速返回顶层。 + * b) 否则: + * 向上回退一级(ptRoute[byClass - 1]),标记需要刷新。 + * + * default (未知按键): + * 直接返回 MENU_NAV_NO_REFRESH,不执行任何操作。 + * + * 边界处理: + * - 本函数假设 menuCtrl 指针有效,不做空指针校验(由调用方保证)。 + * - 在访问 ptCurrent->links.before/behind/lower/higher 等指针前,依赖菜单树结构的正确性。 + * - 对于子菜单为空或回调函数为 NULL 的情况有相应判断保护。 + * + * 实现说明: + * - 使用局部变量 ptCurrent、ptHead、ptRoute 缓存 menuCtrl 中的状态引用,避免多次解引用。 + * - needRefresh 标志用于累积本次按键操作是否需要刷新界面(默认 0,遇到有效导航操作后置为 1)。 + * - 函数末尾统一设置 menuCtrl->ptCurrent = ptCurrent 更新选中项,并根据 needRefresh 返回结果。 + * ------------------------------------------------------------------------- */ +static MenuNavResult MenuNavigator_ProcessKey(tagMenuCtrl *menuCtrl, 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; + uint8_t needRefresh = 0; /* < 刷新标志:1 表示需要刷新界面,0 表示无需刷新 */ + tagPMenuItem ptCurrent; /* < 当前选中菜单项指针(局部副本) */ + tagPMenuItem ptHead; /* < 菜单树根节点指针(局部副本) */ + tagPMenuItem *ptRoute; /* < 导航路径数组指针(局部副本,用于快速访问各级菜单节点) */ + /* 缓存 menuCtrl 中的状态引用,避免多次解引用 */ + ptCurrent = menuCtrl->ptCurrent; + ptHead = menuCtrl->ptHead; + ptRoute = menuCtrl->ptRoute; switch (keyVal) { case KEY_F1: case KEY_F2: + /* 功能键:当前不执行任何导航操作 */ break; + case KEY_U: + /* Up - 向上移动选中项到前一项 */ ptCurrent = ptCurrent->links.before; - result.needRefresh = 1; + needRefresh = 1; break; + case KEY_D: + /* Down - 向下移动选中项到后一项 */ ptCurrent = ptCurrent->links.behind; - result.needRefresh = 1; + needRefresh = 1; break; + case KEY_L: + /* Left 返回上一级菜单 */ + /* 由于 ptCurrent 不能为 0 级菜单,byClass - 1 必定大于 0 */ ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1]; + /* 若当前项为 0 级菜单,则直接下探到子级 */ if (ptCurrent->menuDef.byClass == 0) { - if (ptCurrent->rect.wPos == 1) + /*当前的逻辑是,每次都回到第一个0级菜单,然后下探到子级*/ + /* 0 级菜单的特殊处理:根据 wPos 决定向前跳过的节点数 */ + if (ptCurrent->rect.wPos == 0) { + /* 只有两个 0级菜单,跳两次又回到自己 */ ptCurrent = ptCurrent->links.before->links.before; } - else + else /* 若 wPos != 1,则向前跳过一个节点 */ { ptCurrent = ptCurrent->links.before; } ptCurrent = ptCurrent->links.lower; } - result.needRefresh = 1; + needRefresh = 1; break; + case KEY_R: case KEY_ENT: + /* Right/Enter - 向右移动或进入子菜单 */ if (ptCurrent->links.lower != NULL) { + /* 有子菜单:下探到子级节点 */ ptCurrent = ptCurrent->links.lower; - result.needRefresh = 1; + needRefresh = 1; } else if (ptCurrent->menuDef.pfnWinProc != NULL) { + /* 无子菜单但有回调函数:执行窗口回调(如打开弹窗) */ ptCurrent->menuDef.pfnWinProc(); } break; + case KEY_ESC: + /* Escape - 返回或退出菜单 */ 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; + /* 在 1 级菜单:快速重置到根节点状态 */ + /* 强制整页刷新:避免 invert 反显状态残留叠加 */ + menuCtrl->pt0Level = NULL; + menuCtrl->ptRoute[0] = ptHead; + menuCtrl->ptCurrent = ptHead->links.lower; + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + menuCtrl->ptRoute[1] = menuCtrl->ptCurrent; + return MENU_NAV_REFRESH; } + /* 在其他层级:向上回退一级 */ ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1]; - result.needRefresh = 1; + needRefresh = 1; break; + default: - break; + /* 未知按键:不执行任何操作,返回无需刷新 */ + return MENU_NAV_NO_REFRESH; } - navState->ptCurrent = ptCurrent; - return result; + /* 更新选中项状态 */ + menuCtrl->ptCurrent = ptCurrent; + /* 根据刷新标志返回结果 */ + return needRefresh ? MENU_NAV_REFRESH : MENU_NAV_NO_REFRESH; } -static void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem) +/* ------------------------------------------------------------------------- + * 函数名: MenuNavigator_RebuildRoute + * 功能: + * 根据当前选中菜单项,向上回溯父链并重建当前菜单路径 `ptRoute[]`。 + * + * 参数: + * menuCtrl - 菜单运行时控制上下文,内部保存当前选中项与路径数组 + * maxItem - 最大回溯次数上限,用于防止异常链路导致死循环 + * + * 边界处理: + * - 本函数不做空指针校验,默认由调用链保证 `menuCtrl` 有效。 + * - 若 `ptCurBak == ptCurrent`,说明当前选中项未发生变化,直接返回。 + * - 使用 `maxItem` 作为保护上限,避免异常菜单链表造成无限回溯。 + * + * 说明: + * - 重建流程如下: + * 1) 从当前选中项 `ptCurrent` 出发 + * 2) 若当前节点没有父节点,说明到达 0 级菜单链,沿 `before` 回退到根上下文 + * 3) 若存在父节点,则沿 `higher` 向上回溯,并把对应层级节点写入 `ptRoute[level]` + * 4) 当回溯到 `byClass == 0` 的节点时结束 + * 5) 最后把当前选中节点重新写回 `ptRoute[当前层级]` + * - 该函数的作用是让 `ptRoute[]` 始终准确反映“从顶层到当前焦点”的完整路径, + * 供后续 View 刷新和层级定位使用。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuNavigator_RebuildRoute(tagMenuCtrl *menuCtrl, uint32_t maxItem) { tagPMenuItem ptIndex; - if (navState->ptCurBak == navState->ptCurrent) + /* 若当前选中项未发生变化,则直接返回 */ + if (menuCtrl->ptCurBak == menuCtrl->ptCurrent) { return; } - ptIndex = navState->ptCurrent; + ptIndex = menuCtrl->ptCurrent; for (uint32_t index = 0; index < maxItem; index++) { + /* 若当前节点没有父节点,则沿 `before` 回退到根上下文 */ if (ptIndex->links.higher == NULL) { ptIndex = ptIndex->links.before; } - else + else /* 若存在父节点,则沿 `higher` 向上回溯,并把对应层级节点写入 `ptRoute[level]` */ { ptIndex = ptIndex->links.higher; - navState->ptRoute[ptIndex->menuDef.byClass] = ptIndex; + menuCtrl->ptRoute[ptIndex->menuDef.byClass] = ptIndex; } + /* 当回溯到 `byClass == 0` 的节点时结束 */ 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; + /* 最后把当前选中节点重新写回 `ptRoute[当前层级]` */ + menuCtrl->ptRoute[menuCtrl->ptCurrent->menuDef.byClass] = menuCtrl->ptCurrent; } /* ------------------------------------------------------------------------- @@ -177,7 +284,7 @@ static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *n * 返回值: * - 无 * ------------------------------------------------------------------------- */ -static void MenuPresenter_RenderByState(MenuPresenter *presenter) +static void MenuPresenter_RenderByState(menu_presenter_t *presenter) { tagMenuCtrl *menuCtrl = &presenter->menuCtrl; MenuView *view = presenter->view; @@ -208,15 +315,150 @@ static void MenuPresenter_RenderByState(MenuPresenter *presenter) } } -void MenuPresenter_Init(MenuPresenter *presenter, menu_model_t *model, menu_view_t *view) +/* ------------------------------------------------------------------------- + * 函数名:MenuPresenter_Refresh + * 功能: + * Presenter 层的对外刷新接口,根据是否为首帧渲染选择不同的刷新策略。 + * 本函数作为 menu_presenter_t::refresh 回调被调用,负责协调 View 层完成界面更新。 + * + * 参数: + * presenter - 菜单 Presenter 实例指针,内部持有 menuCtrl 上下文和 view 接口引用 + * isFirstFrame - 是否为首帧标志: + * 1 - 首帧渲染:需要完整初始化并刷新整个菜单界面 + * 0 - 非首帧渲染:根据当前状态差异进行增量或局部刷新 + * + * 刷新策略: + * 首帧模式 (isFirstFrame == 1): + * - 调用 View 层的 full_refresh() 接口,完整重绘整个菜单界面。 + * - 此时 menuCtrl 中的导航路径、选中项等状态已完成初始化,可安全进行全量渲染。 + * - MODE_NONE 表示不进行特殊模式过滤(如调试模式、特定层级过滤等)。 + * + * 非首帧模式 (isFirstFrame == 0): + * - 调用 MenuPresenter_RenderByState() 进行智能状态对比刷新: + * 1) 比较 pt0Level 与 ptRoute[0],判断顶层上下文是否切换 + * 2) 比较 ptCurBak 与 ptCurrent,判断选中项是否变化 + * 3) 根据层级关系和父节点关联选择最优渲染策略: + * - 同层同父:局部反显更新(旧选中恢复 + 新选中高亮) + * - 新层级 >= 旧层级:补绘受影响层级 + * - 回退到更高层:整页刷新保证一致性 + * - 此策略可避免不必要的重绘,提升界面响应性能。 + * + * 调用关系: + * - 被 MenuPresenter_HandleInput() 在导航状态变化后调用(isFirstFrame = 0) + * - 被外部初始化完成后首次调用(isFirstFrame = 1) + * - 通过 presenter->refresh 函数指针绑定,符合 Presenter 的接口抽象。 + * + * 边界处理: + * - 本函数假设 presenter 指针有效,不做空指针校验(由调用方保证)。 + * - view 接口的 full_refresh() 和内部渲染逻辑负责具体的绘制安全校验。 + * ------------------------------------------------------------------------- */ +static void MenuPresenter_Refresh(menu_presenter_t *presenter, uint8_t isFirstFrame) { - presenter->dspCtrl.bFirst = 0; + if (isFirstFrame) + { + /* 首帧模式:完整重绘整个菜单界面 */ + presenter->view->full_refresh(presenter->view, &presenter->menuCtrl, MODE_NONE); + } + else + { + /* 非首帧模式:根据状态差异进行智能增量刷新 */ + MenuPresenter_RenderByState(presenter); + } +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPresenter_HandleInput + * 功能: + * 处理菜单页输入按键,驱动导航状态更新,并在需要时触发路由重建与界面刷新。 + * + * 参数: + * presenter - 菜单 Presenter 实例,内部持有导航控制状态与 Model/View 依赖 + * keyVal - 当前输入按键值 + * + * 边界处理: + * - 若 `presenter == NULL`,则直接返回。 + * - 若 `keyVal == KEY_NONE`,则直接返回。 + * - 若按键不引起状态变化,`MenuNavigator_ProcessKey()` 会返回 + * `MENU_NAV_NO_REFRESH`,本函数直接结束,不触发额外刷新。 + * + * 说明: + * - 处理流程如下: + * 1) 调用 `MenuNavigator_ProcessKey()`,根据按键更新 `menuCtrl` 中的当前选中项 + * 与必要的路径状态 + * 2) 若返回值为 `MENU_NAV_REFRESH`,说明本次输入导致菜单焦点或层级发生变化 + * 3) 调用 `MenuNavigator_RebuildRoute()` 重建当前路径 `ptRoute[]` + * 4) 调用 Presenter 对外刷新接口 `refresh(..., 0)`,执行增量或整页刷新判定 + * - 本函数本身不直接绘制界面,而是负责把“输入 -> 导航状态变化 -> 刷新请求” + * 串联起来。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +static void MenuPresenter_HandleInput(menu_presenter_t *presenter, uint8_t keyVal) +{ + MenuNavResult navResult; + + ASSERT(presenter == NULL); + if (presenter == NULL) + { + return; + } + + if (keyVal == KEY_NONE) + { + return; + } + + navResult = MenuNavigator_ProcessKey(&presenter->menuCtrl, keyVal); + + if (navResult == MENU_NAV_REFRESH) + { + MenuNavigator_RebuildRoute(&presenter->menuCtrl, presenter->model->maxItem); + presenter->refresh(presenter, 0); + } +} + +/* ------------------------------------------------------------------------- + * 函数名: MenuPresenter_Init + * 功能: + * 初始化菜单 Presenter 实例,完成依赖绑定、对外接口挂接以及初始导航状态装配。 + * + * 参数: + * presenter - 待初始化的 Presenter 实例 + * model - 菜单 Model 实例,提供菜单树与运行时数据 + * view - 菜单 View 实例,提供刷新与绘制能力 + * + * 边界处理: + * - 若 `presenter`、`model` 或 `view` 任一为空,则直接返回。 + * + * 说明: + * - 初始化步骤如下: + * 1) 清零 `menuCtrl`,避免历史状态残留 + * 2) 绑定 `model` / `view` 依赖 + * 3) 绑定 Presenter 对外接口 `handle_input` / `refresh` + * 4) 将 `ptHead` 指向菜单树根节点 + * 5) 将 `ptCurrent` 初始化为根节点的首个子菜单 + * 6) 初始化 `ptCurBak` 与 `ptRoute[0..3]`,建立首帧导航路径 + * - 该函数只负责 Presenter 状态装配,不触发实际刷新。 + * + * 返回值: + * - 无 + * ------------------------------------------------------------------------- */ +void MenuPresenter_Init(menu_presenter_t *presenter, menu_model_t *model, menu_view_t *view) +{ + ASSERT((presenter == NULL) || (model == NULL) || (view == NULL)); + if ((presenter == NULL) || (model == NULL) || (view == NULL)) + { + return; + } + memset(&presenter->menuCtrl, 0, sizeof(presenter->menuCtrl)); presenter->model = model; presenter->view = view; + presenter->handle_input = MenuPresenter_HandleInput; + presenter->refresh = MenuPresenter_Refresh; - 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; @@ -227,46 +469,3 @@ void MenuPresenter_Init(MenuPresenter *presenter, menu_model_t *model, menu_view 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 index 32899e1..0f4f520 100644 --- a/src/Drv/pages/menu/presenter.h +++ b/src/Drv/pages/menu/presenter.h @@ -7,27 +7,37 @@ typedef struct menu_presenter_t menu_presenter_t; -typedef struct -{ - uint8_t bFirst; -} tagDspCtrl; - +/* ------------------------------------------------------------------------- + * 结构体名: menu_presenter_t + * 功能: + * 菜单页面 Presenter 层对象,负责衔接输入事件、菜单导航状态与 View 刷新行为。 + * + * 字段说明: + * menuCtrl - 菜单运行时控制上下文,保存当前选中项、路径与刷新相关状态 + * model - 菜单 Model 实例,提供菜单树与运行时数据访问 + * view - 菜单 View 实例,提供界面刷新与绘制能力 + * handle_input - Presenter 对外输入接口,负责处理按键并驱动导航状态变化 + * refresh - Presenter 对外刷新接口,负责按当前状态触发界面刷新 + * + * 说明: + * - Presenter 位于 Model 与 View 之间: + * 1) 从页面层接收输入事件 + * 2) 更新 `menuCtrl` 中的导航状态 + * 3) 根据状态变化调用 View 接口完成局部或整页刷新 + * - 函数指针在 `MenuPresenter_Init()` 中完成绑定。 + * ------------------------------------------------------------------------- */ struct menu_presenter_t { - tagDspCtrl dspCtrl; tagMenuCtrl menuCtrl; menu_model_t *model; menu_view_t *view; + void (*handle_input)(struct menu_presenter_t *self, uint8_t keyVal); + void (*refresh)(struct menu_presenter_t *self, uint8_t isFirstFrame); }; -typedef menu_presenter_t MenuPresenter; - -void MenuPresenter_Init(MenuPresenter *presenter, +void MenuPresenter_Init(menu_presenter_t *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 index 5b15a6a..b107ee7 100644 --- a/src/Drv/pages/menu/view.c +++ b/src/Drv/pages/menu/view.c @@ -265,7 +265,7 @@ static void MenuView_FullRefresh(MenuView *view, const tagMenuCtrl *menuCtrl, Me 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); + s_port->fill_rect(0, MENU_YMIN, (uint16_t)(lcdSizeX - 1), MENU_YMAX, backColor); MenuView_ShowTopLevel(view, mode); for (uint8_t index = 0; index < menuCtrl->ptCurrent->menuDef.byClass; index++) diff --git a/src/Drv/pages/page.h b/src/Drv/pages/page.h index e435e9b..4759e0d 100644 --- a/src/Drv/pages/page.h +++ b/src/Drv/pages/page.h @@ -10,7 +10,7 @@ * 取值说明: * PAGE_ID_NONE - 无效页面 ID / 未初始化占位值 * PAGE_ID_MENU - 菜单页 ID(当前主运行页) - * PAGE_ID_APP_INFO - 预留页面 ID(当前版本可注册与否由上层决定) + * PAGE_ID_APP_INFO - 装置信息页 ID(在 main 中与菜单页一并注册) * PAGE_ID_MAX - 上界哨兵,不可作为有效页面 ID 使用 * * 使用约束: @@ -21,7 +21,7 @@ typedef enum { PAGE_ID_NONE = 0, PAGE_ID_MENU = 1, - PAGE_ID_APP_INFO = 2, /* 预留ID:当前版本未注册运行 */ + PAGE_ID_APP_INFO = 2, /* 装置信息页:AppInfoPage_GetInstance */ PAGE_ID_MAX } page_id_t; diff --git a/src/Drv/pages/page_manager.c b/src/Drv/pages/page_manager.c index 96ed57a..987ca94 100644 --- a/src/Drv/pages/page_manager.c +++ b/src/Drv/pages/page_manager.c @@ -232,12 +232,13 @@ page_manager_result_t PageManager_Push(page_t *newPage) /* 重复导航到当前页面:按幂等处理 */ return PAGE_MANAGER_OK; } - ASSERT((currentTop != NULL) && (currentTop->on_exit != NULL)); + /* 调用旧页的 on_exit 回调 */ if ((currentTop != NULL) && (currentTop->on_exit != NULL)) { currentTop->on_exit(currentTop); } + /* 将新页压入栈 */ s_pageManager.stack_top++; s_pageManager.page_stack[s_pageManager.stack_top] = newPage; @@ -294,7 +295,6 @@ page_manager_result_t PageManager_Pop(void) } 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); @@ -314,14 +314,12 @@ page_manager_result_t PageManager_Pop(void) 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 * 功能: @@ -358,6 +356,88 @@ page_manager_result_t PageManager_Navigate(page_id_t pageId) return PageManager_Push(target); } +/* ------------------------------------------------------------------------- + * 函数名: PageManager_Replace + * 功能: + * 页面替换接口:用目标页面替换当前栈顶页面,不增加栈深。 + * + * 参数: + * pageId - 目标页面标识 + * + * 边界处理: + * - 若目标页未注册,返回 PAGE_MANAGER_ERR_NOT_FOUND。 + * - 若当前无活动页(空栈),退化为 Push 目标页。 + * - 若目标页与当前栈顶同一 page_id,按幂等处理返回 PAGE_MANAGER_OK。 + * + * 说明: + * - 生命周期顺序: + * 1) 当前页 on_exit + * 2) 若当前页非缓存且实现 on_destroy,则销毁并清 is_created + * 3) 栈顶指针替换为目标页 + * 4) 目标页必要时 on_create(首次)+ on_enter + * + * 返回值: + * - PAGE_MANAGER_OK : 替换成功 + * - PAGE_MANAGER_ERR_NOT_FOUND : 未找到目标页面 + * - PAGE_MANAGER_ERR_STACK_FULL : 空栈退化 Push 且栈满(透传) + * ------------------------------------------------------------------------- */ +page_manager_result_t PageManager_Replace(page_id_t pageId) +{ + page_t *target; + page_t *currentTop; + + target = PageManager_Find(pageId); + ASSERT(target == NULL); + if (target == NULL) + { + return PAGE_MANAGER_ERR_NOT_FOUND; + } + + currentTop = PageManager_GetTop(); + if (currentTop == NULL) + { + /* 空栈场景:替换退化为压栈导航 */ + return PageManager_Push(target); + } + + if (currentTop->page_id == target->page_id) + { + return PAGE_MANAGER_OK; + } + + if (currentTop->on_exit != NULL) + { + currentTop->on_exit(currentTop); + } + + if (!currentTop->is_cached) + { + if (currentTop->on_destroy != NULL) + { + currentTop->on_destroy(currentTop); + } + currentTop->is_created = 0; + } + + s_pageManager.page_stack[s_pageManager.stack_top] = target; + + if (!target->is_created) + { + if (target->on_create != NULL) + { + target->on_create(target); + } + target->is_created = 1; + } + + if (target->on_enter != NULL) + { + target->on_enter(target); + } + + return PAGE_MANAGER_OK; +} + /* ------------------------------------------------------------------------- * 函数名: PageManager_DispatchEvent * 功能: diff --git a/src/Drv/pages/page_manager.h b/src/Drv/pages/page_manager.h index 2b04869..656edf3 100644 --- a/src/Drv/pages/page_manager.h +++ b/src/Drv/pages/page_manager.h @@ -66,6 +66,7 @@ typedef enum 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_Replace(page_id_t pageId); page_manager_result_t PageManager_DispatchEvent(input_event_t *event); page_t *PageManager_GetTop(void); diff --git a/src/main.c b/src/main.c index 2eb1c95..2a25586 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,7 @@ static int getch(void) #endif #include "Drv/pages/page_manager.h" +#include "Drv/pages/AppInfo/page.h" #include "Drv/pages/menu/page.h" #include "TCP/tcp.h" #include "remoteDisplay.h" @@ -69,6 +70,7 @@ int main(void) PageManager_Init(); Lcd_Init(); /* 初始化屏幕显存:由入口统一完成 */ (void)PageManager_Register(MenuPage_GetInstance()); + (void)PageManager_Register(AppInfoPage_GetInstance()); (void)PageManager_Navigate(PAGE_ID_MENU); Key_Init(); /* 初始化按键 */ printf("PC 端 HMI 菜单模拟启动(TCP 服务在单独线程,端口 7003)。\n"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 603f35c..0346f3c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -96,6 +96,7 @@ add_dtu_test( test_p1_menu test_p1_menu.c "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" + "${CMAKE_SOURCE_DIR}/src/Drv/pages/page_manager.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"