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"