Files
DTU-HMI/docs/系统架构设计文档.md

891 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DTU-HMI 系统架构设计文档
## 1. 文档目的
本文档用于描述 `DTU-HMI` 工程的整体架构、核心模块职责、关键数据流、线程与通信模型、构建与测试体系,作为后续开发、联调、测试与维护的统一基线。
## 2. 系统概述
`DTU-HMI` 是一个基于纯 C 实现的 PC 端 HMI 菜单逻辑模拟系统,目标是复现现场 DTU 设备的人机界面行为。
系统支持本地菜单交互与远程显示协议RemoDispBus通信主要包含
- 菜单树构建与路由(多级菜单、同级循环导航)
- LCD 显存与绘制160x160 单色像素缓冲)
- 按键输入(本地/远程按键注入)
- TCP 服务(远程显示数据交互)
- 跨平台线程与网络适配Windows/Linux
## 3. 架构目标与设计原则
- **可移植性**:通过 `tcp.c``thread_utils.c` 封装平台差异。
- **可维护性**:按模块职责划分(菜单/显示/网络/线程/输入)。
- **可测试性**:优先抽取并覆盖纯逻辑函数,逐步推进集成测试。
- **低耦合高内聚**:上层业务通过明确接口调用下层能力。
## 4. 系统分层架构
```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 应用层
- 文件:`src/main.c`
- 职责:
- 系统初始化菜单、按键、TCP 线程)
- 主循环调度(菜单路由、周期显示刷新)
- 生命周期管理(线程退出、网络清理)
### 4.2 菜单业务层
- 文件:`src/Drv/menu/``src/Drv/menu/app/menu.h`
- 职责:
- 采用 MVP 分层组织菜单模块(`Model / Presenter / View`
- 基于静态菜单模型构建运行时菜单树
- 处理按键驱动的菜单状态迁移与路径重建
- 执行菜单显示坐标计算与多级菜单渲染调度
### 4.3 多页面管理层设计
**设计思想:**
基于经典的 MVP 范式,彻底解耦**数据、视图、控制逻辑**,解决复杂 UI 与业务逻辑的协同维护问题。
- **Model模型层**:纯业务数据与状态管理,负责系统参数、设备状态、采集数据的读写、校验、存储,与 UI 完全无关。
- **View视图层**:纯渲染显示,仅根据 Model 的数据绘制菜单界面、焦点高亮、弹窗、动画,不处理任何业务逻辑。
- **Presenter控制层**:核心调度中枢,接收输入事件、更新 Model 数据、控制 View 刷新、处理菜单跳转逻辑,是 Model 与 View 的唯一桥梁。
目录结构:
`src/Drv/pages`: 这个文件夹下面放不同的页面
`src/Drv/pages/global`: 全局状态管理器,页面之间交互的中间件
`src/Drv/pages/global/renderer_lcd.c`: 这个文件是抽象底层的lcd给所有页面提供一个统一的调用接口。
`src/Drv/pages/global/renderer_lcd.h`: 这个文件是抽象底层的lcd给所有页面提供一个统一的调用接口。
`src/Drv/pages/menu`: 项目的菜单逻辑
`src/Drv/pages/menu/model`: 菜单的模型层
`src/Drv/pages/menu/view`: 菜单的视图层
`src/Drv/pages/menu/Presenter`: 菜单的控制层
`src/Drv/pages/menu/page.c`: 菜单的页面逻辑
`src/Drv/pages/menu/page.h`: 菜单的页面逻辑
`src/Drv/pages/...`: 其他页面
整个多页面管理系统分为 5 个核心模块,从上到下形成完整的调度闭环,与底层 MVP 架构无缝衔接
```
┌─────────────────────────────────────────────────────────────┐
│ 全局状态管理器Global Model
│ (跨页面共享数据、持久化配置、系统全局状态、观察者通知) │
├─────────────────────────────────────────────────────────────┤
│ 页面管理器核心Page Manager
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 页面栈Page Stack 静态数组) ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ 栈顶页面 │ │ 后台页面1 │ │ 后台页面2 │ ... ││
│ │ │(前台显示) │ │(暂停状态) │ │(暂停状态) │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ 生命周期调度器 | 事件分发器 | 页面跳转核心接口 │
├─────────────────────────────────────────────────────────────┤
│ 标准化页面抽象Page 接口层) │
│ 每个页面独立封装:生命周期钩子 + 专属MVP三元组 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Page 基础属性页面ID、缓存标志、创建状态 ││
│ │ 生命周期钩子on_create/on_enter/on_exit/on_destroy ││
│ │ 运行入口on_event事件处理、on_loop主循环 ││
│ │ 专属MVP三元组Model(私有) → Presenter → View ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ 底层支撑层与MVP架构无缝衔接
│ 显示HAL | 输入HAL | 基础图形库 | 硬件驱动 │
└─────────────────────────────────────────────────────────────┘
```
#### 4.3.1 标准化页面抽象Page 接口层)
这是整个系统的基础单元,**每个独立业务页面对应一个 Page 实例,每个 Page 内部封装一套完全独立的 MVP 三元组**,对外仅暴露标准接口,页面管理器仅通过标准接口与页面交互,完全不关心页面内部的 MVP 实现,实现页面间的彻底解耦。
```c
// 编译期静态配置根据MCU资源调整
#define MAX_PAGE_STACK_DEPTH 5 // 最大页面栈深度,防止栈溢出
#define MAX_PAGE_COUNT 8 // 系统支持的最大页面总数
#define EVENT_QUEUE_LENGTH 8 // 输入事件队列长度
// 页面唯一ID枚举所有页面必须在此定义用于页面查找、防重复入栈
typedef enum {
PAGE_ID_NONE = 0,
PAGE_ID_HOME, // 首页(主页面,栈底常驻)
PAGE_ID_SETTINGS, // 设置页面
PAGE_ID_TEMP_DETAIL, // 温度详情页面
PAGE_ID_ABOUT, // 关于页面
PAGE_ID_MAX
} page_id_t;
// 事件处理结果枚举
typedef enum {
EVENT_UNHANDLED = 0, // 事件未被消费,继续分发
EVENT_HANDLED = 1 // 事件已被消费,终止分发
} event_result_t;
// 页面结构体前置声明
typedef struct page_t page_t;
```
#### 4.3.2 页面标准接口与生命周期定义
嵌入式场景下,生命周期钩子的设计必须严格对应资源的申请 / 释放时机,避免 RAM 浪费和低功耗异常,每个钩子的执行时机、职责完全固定,禁止越权操作。
```c
// 页面标准接口结构体
struct page_t {
// ===================== 页面基础属性 =====================
page_id_t page_id; // 页面唯一ID不可重复
bool is_cached; // 缓存标志true=出栈时不销毁仅暂停false=出栈时彻底销毁
bool is_created; // 内部状态标志页面是否已执行on_create外部只读
// ===================== 生命周期钩子函数 =====================
/**
* @brief 页面第一次创建时执行仅执行1次
* @note 职责MVP三元组初始化、静态资源申请字体/图标缓冲区)、回调绑定
* @note 禁止:耗时操作、屏幕绘制、动态内存申请
*/
void (*on_create)(page_t *page);
/**
* @brief 页面进入前台(成为栈顶)时执行,每次进入都执行
* @note 职责从Model拉取最新数据、全量UI绘制、开启定时器/传感器、注册事件回调
* @note 禁止资源销毁、MVP初始化操作
*/
void (*on_enter)(page_t *page);
/**
* @brief 页面离开前台(被覆盖/出栈)时执行,每次离开都执行
* @note 职责:关闭定时器/传感器、保存临时数据、注销事件回调、低功耗准备
* @note 禁止屏幕绘制、MVP初始化、耗时操作
*/
void (*on_exit)(page_t *page);
/**
* @brief 页面彻底销毁时执行仅执行1次
* @note 职责释放所有申请的资源、MVP实例销毁、回调注销
* @note 仅非缓存页面出栈时会触发,缓存页面不会触发
*/
void (*on_destroy)(page_t *page);
// ===================== 运行时入口 =====================
/**
* @brief 事件处理入口,页面管理器仅将事件分发给栈顶页面的此函数
* @note 职责将事件转发给内部View由View判断命中并转发给Presenter
* @return 事件是否被消费
*/
event_result_t (*on_event)(page_t *page, input_event_t *event);
/**
* @brief 主循环入口,页面管理器仅调用栈顶页面的此函数
* @note 职责执行页面Presenter的主循环逻辑、UI动画刷新、状态机轮询
* @note 禁止:阻塞操作、耗时计算
*/
void (*on_loop)(page_t *page);
// ===================== 页面专属MVP三元组 =====================
// 每个页面独立拥有,与其他页面完全隔离,页面管理器不直接访问
void *presenter; // 页面对应的Presenter实例指针
void *view; // 页面对应的View实例指针
void *model; // 页面对应的私有Model实例仅当前页面使用
};
```
#### 4.3.3 页面 MVP 三元组的绑定规则
- **完全隔离**:每个页面的 Model 仅管理当前页面的私有数据禁止跨页面访问View 仅负责当前页面的 UI 绘制Presenter 仅调度当前页面的业务逻辑
- **绑定时机**MVP 三元组的初始化、绑定,必须在页面的`on_create`钩子中完成,保证页面创建时 MVP 已就绪
- **销毁时机**MVP 实例的资源释放,必须在页面的`on_destroy`钩子中完成,避免资源泄漏
- **跨页面交互**:禁止页面 A 的 Presenter 直接调用页面 B 的任何接口,所有跨页面数据同步必须通过全局状态管理器完成
#### 4.3.4 栈式页面管理器(核心调度中枢)
这是整个系统的核心,通过静态数组实现的栈结构管理所有页面,负责页面跳转的核心逻辑、生命周期的精准调度、输入事件的统一分发、主循环的统一调度。
**页面管理器结构体定义**
```c
// 页面管理器结构体
typedef struct {
page_t *page_stack[MAX_PAGE_STACK_DEPTH]; // 页面栈,静态数组实现
int8_t stack_top; // 栈顶指针,初始值为-1空栈
} page_manager_t;
// 全局页面管理器单例(整个系统仅一个实例)
extern page_manager_t g_page_manager;
```
**基础接口:初始化与栈状态查询**
```c
// 全局页面管理器实例
page_manager_t g_page_manager;
/**
* @brief 页面管理器初始化系统启动时调用1次
*/
void page_manager_init(page_manager_t *manager) {
memset(manager, 0, sizeof(page_manager_t));
manager->stack_top = -1; // 初始化为空栈
}
/**
* @brief 获取当前栈顶页面(前台显示页面)
* @return 栈顶页面指针空栈返回NULL
*/
page_t* page_manager_get_top(page_manager_t *manager) {
if (manager->stack_top < 0) {
return NULL;
}
return manager->page_stack[manager->stack_top];
}
```
**页面入栈Push页面跳转**
- 入参校验:检查栈是否已满、新页面是否合法、是否重复入栈
- 触发旧栈顶页面的`on_exit`:当前页面被覆盖,执行暂停逻辑
- 新页面入栈,栈顶指针 + 1
- 若新页面未创建,触发`on_create`:执行 MVP 初始化,仅执行 1 次
- 触发新页面的`on_enter`:新页面进入前台,执行 UI 绘制与业务启动
- 新页面成为前台页面,接收事件与主循环调度
```c
/**
* @brief 页面入栈(跳转到新页面)
* @param manager 页面管理器实例
* @param new_page 待入栈的新页面实例
* @return 0=成功,负数=错误码
*/
int page_manager_push(page_manager_t *manager, page_t *new_page) {
// 入参合法性校验
if (manager == NULL || new_page == NULL) {
return -1;
}
// 栈满校验,防止栈溢出
if (manager->stack_top >= MAX_PAGE_STACK_DEPTH - 1) {
return -2;
}
// 防重复入栈:当前栈顶已是该页面,无需重复操作
page_t *current_top = page_manager_get_top(manager);
if (current_top != NULL && current_top->page_id == new_page->page_id) {
return 0;
}
// ===================== 生命周期执行 =====================
// 1. 触发当前栈顶页面的on_exit离开前台
if (current_top != NULL && current_top->on_exit != NULL) {
current_top->on_exit(current_top);
}
// 2. 新页面入栈
manager->stack_top++;
manager->page_stack[manager->stack_top] = new_page;
// 3. 页面首次创建触发on_create仅执行1次
if (!new_page->is_created) {
if (new_page->on_create != NULL) {
new_page->on_create(new_page);
}
new_page->is_created = true;
}
// 4. 触发新页面的on_enter进入前台
if (new_page->on_enter != NULL) {
new_page->on_enter(new_page);
}
return 0;
}
```
**页面出栈Pop页面返回**
- 入参校验:检查栈是否为空、是否为栈底首页(禁止出栈)
- 触发当前栈顶页面的`on_exit`:当前页面离开前台,执行退出准备
- 若当前页面为非缓存页面,触发`on_destroy`:彻底销毁页面,释放资源
- 栈顶指针 - 1上一个页面恢复为栈顶
- 触发新栈顶页面的`on_enter`:页面恢复前台,执行数据刷新与 UI 重绘
- 新栈顶页面成为前台页面,接收事件与主循环调度
```c
/**
* @brief 页面出栈(返回上一页)
* @param manager 页面管理器实例
* @return 0=成功,负数=错误码
*/
int page_manager_pop(page_manager_t *manager) {
// 入参校验
if (manager == NULL) {
return -1;
}
// 禁止空栈出栈、禁止栈底首页出栈(保证首页常驻)
if (manager->stack_top <= 0) {
return -2;
}
// ===================== 生命周期执行 =====================
// 1. 获取当前栈顶页面(待出栈页面)
page_t *current_page = manager->page_stack[manager->stack_top];
// 2. 触发当前页面的on_exit离开前台
if (current_page->on_exit != NULL) {
current_page->on_exit(current_page);
}
// 3. 非缓存页面触发on_destroy彻底销毁释放资源
if (!current_page->is_cached) {
if (current_page->on_destroy != NULL) {
current_page->on_destroy(current_page);
}
current_page->is_created = false;
}
// 4. 栈顶指针减1页面出栈
manager->stack_top--;
// 5. 触发新栈顶页面的on_enter恢复前台
page_t *new_top = manager->page_stack[manager->stack_top];
if (new_top->on_enter != NULL) {
new_top->on_enter(new_top);
}
return 0;
}
```
**统一事件分发机制**
嵌入式多页面 GUI 的核心痛点之一是事件误响应,必须保证**仅当前前台页面(栈顶)可接收输入事件**,后台页面完全无法接收事件,避免出现「设置页面按键触发首页逻辑」的 bug
- 底层 Input HAL 在中断中采集输入事件(按键、触摸、编码器),投递到全局静态环形事件队列
- 页面管理器在主循环中从事件队列取出事件
- 获取当前栈顶前台页面,将事件分发给页面的`on_event`入口
- 页面将事件转发给内部 ViewView 判断事件命中,转发给 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 的解耦原则,导致代码耦合混乱。
全局状态管理器的核心设计思路是:**实现一个全局单例的 ModelGlobalModel作为跨页面数据的唯一可信源通过观察者模式实现数据变化的同步通知完全不破坏各页面 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_name>/page.c` 实现标准生命周期;
- 页面间禁止直接互调,只允许通过 `GlobalModel` 同步共享状态;
- 菜单项 `pfnWinProc` 仅作为“页面路由入口”,不再承载页面业务实现。
### 4.4 输入层
- 文件:`src/Drv/key.c``src/Drv/key.h`
- 职责:
- 提供按键读写状态控制(消费式读取)
- 接收远程模块写入的按键事件并供菜单模块读取
### 4.5 远程显示通信层
- 文件:`src/remoteDisplay.c``src/remoteDisplay.h`
- 职责:
- 实现 RemoDispBus 协议解析与回复
- 提供 TCP 服务器线程入口与启动逻辑
- 处理保活、初始化、按键下发、显存上传等命令
### 4.6 平台适配层
- 文件:`src/TCP/tcp.c``src/thread_utils.c`
- 职责:
- 提供跨平台 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` 内部私有)
关键关系:
- `ptHigher` / `ptLower`:父子层级关系
- `ptBefore` / `ptBehind`:同级双向关系(首尾成环)
- `ptRoute[]`当前路径缓存0~3 级)
### 5.2 显示控制结构
- `tagScreenControl g_tCVsr`
- 显存缓冲 `pwbyLCDMemory`
- 前景/背景色
- ASCII 与汉字字体参数
### 5.3 远程按键结构
- `tagRKeyCtrl g_tRemoteKey`
- `byKeyValid`:是否有新按键
- `byKeyValue`:按键值
- `bUseRkey`:远程按键开关(当前实现中初始化为启用)
## 6. 关键业务流程
### 6.1 主循环流程
```text
[系统初始化]
|
v
[MenuApp_PollInput]
|
v
[Sleep 20ms]
|
v
[计数器累加]
|
v
[是否到刷新周期?] --否--> [MenuApp_PollInput]
|
+--是--> [MenuApp_Render] --> [MenuApp_PollInput]
```
### 6.2 菜单交互流程
- 输入来源:`Key_Read()`(含远程写入按键)
- 行为:
- 上/下:同级循环移动
- 左/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` 快照交互,避免导航器直接操作外部全局变量
### 6.3 远程显示协议流程
```text
[Accept客户端]
|
v
[接收缓冲区累积]
|
v
[parse_frame 校验解析]
|
+--成功--------> [按 cmd 分发] --> [send_reply 回包] --> [接收缓冲区累积]
|
+--失败/不完整--> [接收缓冲区累积]
```
命令语义RemoDispBus
- `CMD_INIT`:返回 LCD 宽高与显存尺寸
- `CMD_LCDMEM`:返回显存数据(支持起始地址)
- `CMD_KEY`:注入远程按键到 `g_tRemoteKey`
- `CMD_KEEPLIVE`:保活响应
## 7. 并发与线程模型
- 主线程:
- 负责菜单路由与本地显示刷新
- TCP 服务器线程:
- 监听连接、解析协议、处理远程请求
共享状态:
- `g_tCVsr.pwbyLCDMemory`(远程读取 + 本地写入)
- `g_tRemoteKey`(远程写入 + 菜单读取)
当前实现未使用锁机制,依赖业务访问模式降低冲突风险。
后续若并发复杂度提升,建议引入细粒度互斥或无锁缓冲策略。
## 8. 构建与运行架构
- 构建系统CMake`C_STANDARD 99`
- 可执行目标:`DTU-HMI`
- 平台链接:
- Windows`ws2_32`
- Linux/macOS`pthread`
- 可选调试:`ENABLE_DEBUG=ON` 自动定义 `DEBUG`
## 9. 测试架构
测试目录:`tests/`
- 框架:`ctest + 自定义断言宏`
- 分层策略:
- P0纯逻辑单元测试协议解析、UTF-8 解析、字库查找)
- P1状态/计算单测按键、菜单、LCD 基础像素操作)
- P2集成测试TCP 回环)
建议执行命令:
```bash
cmake -S . -B build
cmake --build build
ctest --test-dir build -C Debug --output-on-failure
```
## 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`
- `remoteDisplay.c` 依赖:`lcd``key``tcp``thread_utils`
- `lcd.c` 依赖:`ascii`
- `src/Drv/pages/menu/model/menu_model.c` 提供:静态菜单表(被 `MenuPage` 使用)
## 11. 已知风险与改进建议
- **并发一致性风险**:远程线程与主线程共享状态无锁访问。
- 建议:为显存快照与按键事件引入互斥保护或双缓冲。
- **协议缓冲鲁棒性**:当前异常数据采用清空缓冲策略,存在丢包窗口。
- 建议:增加更精细的帧边界恢复策略与统计日志。
- **可测试性边界**:部分逻辑仍与全局状态耦合较深。
- 建议:逐步引入接口注入(如 `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()`
- 该用例用于防止“菜单树可编译但启动崩溃”的问题再次进入主干。