重构显示逻辑为 MVP 架构,进行显示模块的解耦

This commit is contained in:
2026-03-24 19:52:22 +08:00
parent a4bf0962b2
commit 0690d6a00e
42 changed files with 2207 additions and 1417 deletions

View File

@@ -24,26 +24,40 @@
## 4. 系统分层架构
```text
+----------------------+
| AppLayer |
| main.c |
+----------+-----------+
|
+--------------+--------------+
| |
v v
+---------+----------+ +---------+------------------+
| MenuLayer | | RemoteDisplayLayer |
| menu.c | | remoteDisplay.c |
+----+-----------+---+ +-----+-----------+----------+
| | | |
v v v v
+------+-----+ +--+---------+ +---+----+ +---+------------------+
|DisplayLayer| |InputLayer | |Input | |PlatformLayer |
|lcd/Ascii/ | |key.c | |key.c | |tcp.c + thread_utils.c |
|display.c | +------------+ +-------+ +------------------------+
+------------+
```mermaid
block-beta
columns 1
a["菜单"]
block:textdraw
columns 2
a1["文字显示"] a2["图像显示"]
end
block:draw
columns 2
block:text_group
columns 2
block:left_text
columns 1
b["Lcd_ShowStr"]
block:showstr
columns 2
s1["Lcd_Pub_Ascii"]
s2["Lcd_Pub_UTF8"]
end
end
b1["utf8_next"]
end
block:draw_group
columns 4
c1["Lcd_LineH"]
c2["Lcd_LineV"]
c3["Lcd_Invert"]
c4["Lcd_FillRect"]
end
end
block:lcd
lcda["Lcd_Init"] lcdb["Lcd_SetPixel"] lcdc["Lcd_GetPixel"]
end
```
### 4.1 应用层
@@ -56,15 +70,16 @@
### 4.2 菜单业务层
- 文件:`src/Drv/menu.c``src/Drv/menu.h`
- 文件:`src/Drv/menu/``src/Drv/menu/app/menu.h`
- 职责:
- 采用 MVP 分层组织菜单模块(`Model / Presenter / View`
- 基于静态菜单模型构建运行时菜单树
- 处理按键驱动的菜单状态迁移
- 处理按键驱动的菜单状态迁移与路径重建
- 执行菜单显示坐标计算与多级菜单渲染调度
### 4.3 显示层
- 文件:`src/Drv/lcd/lcd.c``src/Drv/lcd/lcd_draw.c``src/Drv/lcd/lcd_text.c``src/Drv/lcd/text_codec.c``src/Drv/lcd/ascii.c``src/Drv/display.c`
- 文件:`src/Drv/lcd/lcd.c``src/Drv/lcd/lcd_draw.c``src/Drv/lcd/lcd_text.c``src/Drv/lcd/text_codec.c``src/Drv/lcd/ascii.c``src/Drv/menu/model/display.c`
- 职责:
- 管理 LCD 显存 `g_tCVsr` 与像素绘制
- 提供 ASCII/UTF-8 字符显示能力
@@ -96,9 +111,9 @@
### 5.1 菜单模型与菜单树
- 静态菜单模型:`tagMenuModel`(定义于 `display.h`,数据在 `display.c`
- 运行时菜单项:`tagMenuItem`(定义于 `menu.c`
- 全局控制:`g_tMenuCtrl``g_tDspCtrl`
- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/menu/model/display.h`,数据在 `src/Drv/menu/model/display.c`
- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/menu/common/menu_item_types.h`,实例由 `src/Drv/menu/app/menu.c` 管理
- 运行时控制:`tagMenuCtrl``tagDspCtrl`(定义于 `src/Drv/menu/common/menu_state_types.h`,实例为 `app/menu.c` 内部私有)
关键关系:
@@ -128,7 +143,7 @@
[系统初始化]
|
v
[Menu_Route]
[MenuApp_PollInput]
|
v
[Sleep 20ms]
@@ -137,9 +152,9 @@
[计数器累加]
|
v
[是否到刷新周期?] --否--> [Menu_Route]
[是否到刷新周期?] --否--> [MenuApp_PollInput]
|
+--是--> [Menu_Show_Proc] --> [Menu_Route]
+--是--> [MenuApp_Render] --> [MenuApp_PollInput]
```
### 6.2 菜单交互流程
@@ -150,7 +165,85 @@
- 左/ESC回退上级或退回主层
- 右/确认:进入子级或执行叶子回调
- 渲染:
- `Menu_Show_Proc` 根据路径增量刷新或全量刷新
- `MenuApp_Render` 调用 Presenter/View根据路径进行增量刷新或全量刷新
### 6.4 当前 Menu 详细设计MVP
#### 6.4.1 模块拆分
- `src/Drv/menu/app/menu.c`
- 菜单应用 Facade`MenuApp_*` 对外接口)
- 持有运行时私有状态(`s_menuCtrl``s_dspCtrl``s_menuItems`
- `src/Drv/menu/presenter/menu_presenter.c`
- 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新
- `src/Drv/menu/model/menu_model.c`
- 模型初始化:树构建、菜单名修饰、初始状态建立
- `src/Drv/menu/view/menu_view.c`
- 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略
- `src/Drv/menu/presenter/menu_navigator.c`
- 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染)
- `src/Drv/menu/view/menu_layout.c`
- 菜单布局计算:宽度统计、层级矩形定位、越界回退策略
- `src/Drv/menu/model/display.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` 快照交互,避免导航器直接操作外部全局变量
### 6.3 远程显示协议流程
@@ -220,10 +313,12 @@ ctest --test-dir build -C Debug --output-on-failure
## 10. 模块依赖关系(代码级)
- `main.c` 依赖:`menu``key``remoteDisplay``tcp``thread_utils`
- `menu.c` 依赖:`lcd``display``key`
- `src/Drv/menu/app/menu.c` 依赖:`presenter/menu_presenter``model/display``key``lcd`
- `src/Drv/menu/presenter/menu_presenter.c` 依赖:`model/menu_model``view/menu_view``presenter/menu_navigator`
- `src/Drv/menu/view/menu_view.c` 依赖:`model/menu_layout``view/menu_render_port``lcd`
- `remoteDisplay.c` 依赖:`lcd``key``tcp``thread_utils`
- `lcd.c` 依赖:`ascii`
- `display.c` 提供:静态菜单表(被 `menu.c` 使用)
- `src/Drv/menu/model/display.c` 提供:静态菜单表(被 menu 模块使用)
## 11. 已知风险与改进建议
@@ -242,3 +337,32 @@ ctest --test-dir build -C Debug --output-on-failure
- 每次新增模块或调整主流程时同步更新本文档
- 测试策略更新需同步维护“第 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()`
- 该用例用于防止“菜单树可编译但启动崩溃”的问题再次进入主干。