770 lines
28 KiB
Markdown
770 lines
28 KiB
Markdown
# 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. 系统分层架构
|
||
<img src="https://lsky.bitnasdaq.vip/8CaeLa.png" style="display: block; margin: auto; max-width: 90%;">
|
||
**目录结构:**
|
||
```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
|
||
block:menu
|
||
a["LCD"]
|
||
end
|
||
|
||
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.2 按键驱动层
|
||
|
||
- 文件:`src/Drv/menu.c`、`src/Drv/menu.h`
|
||
- 职责:
|
||
- 基于静态菜单模型构建运行时菜单树
|
||
- 处理按键驱动的菜单状态迁移
|
||
- 执行菜单显示坐标计算与多级菜单渲染调度
|
||
### 4.3 多页面管理层设计
|
||
#### 4.3.1 页面管理器设计
|
||
主要是通过页面管理器对不同页面之间切换进行调度。
|
||
<img src="https://lsky.bitnasdaq.vip/cRml1o.png" style="display: block; margin: auto; max-width: 90%;">
|
||
**多页面目录结构设计:**
|
||
```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;
|
||
}
|
||
```
|
||
### 菜单页面设计
|
||
<img src="https://lsky.bitnasdaq.vip/uGb3n2.png" style="display: block; margin: auto; max-width: 90%;">
|
||
**设计思想:**
|
||
基于经典的 MVP 范式,彻底解耦**数据、视图、控制逻辑**,解决复杂 UI 与业务逻辑的协同维护问题。
|
||
- **Model(模型层)**:纯业务数据与状态管理,负责系统参数、设备状态、采集数据的读写、校验、存储,与 UI 完全无关。
|
||
- **View(视图层)**:纯渲染显示,仅根据 Model 的数据绘制菜单界面、焦点高亮、弹窗、动画,不处理任何业务逻辑。
|
||
- **Presenter(控制层)**:核心调度中枢,接收输入事件、更新 Model 数据、控制 View 刷新、处理菜单跳转逻辑,是 Model 与 View 的唯一桥梁。
|
||
<img src="https://lsky.bitnasdaq.vip/reEG2w.png" style="display: block; margin: auto; max-width: 90%;">
|
||
**菜单页面的目录设计:**
|
||
```txt
|
||
菜单页面的目录结构设计:
|
||
src/
|
||
└── Drv/
|
||
└── pages/
|
||
└── menu/ # 项目菜单逻辑根目录
|
||
├── model.c # 菜单模型层实现
|
||
├── model.h # 菜单模型层头文件
|
||
├── view.c # 菜单视图层实现
|
||
├── view.h # 菜单视图层头文件
|
||
├── presenter.c # 菜单控制层实现
|
||
├── presenter.h # 菜单控制层头文件
|
||
├── page.c # 菜单页面逻辑实现
|
||
└── page.h # 菜单页面逻辑头文件
|
||
```
|
||
当有外部的事件产生时,MVP 架构的数据流图如下:
|
||
<img src="https://lsky.bitnasdaq.vip/yK2625.png" style="display: block; margin: auto; max-width: 90%;">
|
||
### 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`(定义于 `display.h`,数据在 `display.c`)
|
||
- 运行时菜单项:`tagMenuItem`(定义于 `menu.c` 内)
|
||
- 全局控制:`g_tMenuCtrl`、`g_tDspCtrl`
|
||
|
||
关键关系:
|
||
|
||
- `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
|
||
[Menu_Route]
|
||
|
|
||
v
|
||
[Sleep 20ms]
|
||
|
|
||
v
|
||
[计数器累加]
|
||
|
|
||
v
|
||
[是否到刷新周期?] --否--> [Menu_Route]
|
||
|
|
||
+--是--> [Menu_Show_Proc] --> [Menu_Route]
|
||
```
|
||
|
||
### 6.2 菜单交互流程
|
||
|
||
- 输入来源:`Key_Read()`(含远程写入按键)
|
||
- 行为:
|
||
- 上/下:同级循环移动
|
||
- 左/ESC:回退上级或退回主层
|
||
- 右/确认:进入子级或执行叶子回调
|
||
- 渲染:
|
||
- `Menu_Show_Proc` 根据路径增量刷新或全量刷新
|
||
|
||
### 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
|
||
```
|
||
维护命令:
|
||
```bash
|
||
tasklist /FI "IMAGENAME eq DTU-HMI.exe" #查找程序 PID
|
||
taskkill /PID 66464 /F #根据 PID 关闭程序
|
||
```
|
||
## 10. 模块依赖关系(代码级)
|
||
|
||
- `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils`
|
||
- `menu.c` 依赖:`lcd`、`display`、`key`
|
||
- `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils`
|
||
- `lcd.c` 依赖:`ascii`
|
||
- `display.c` 提供:静态菜单表(被 `menu.c` 使用)
|
||
|
||
## 11. 已知风险与改进建议
|
||
|
||
- **并发一致性风险**:远程线程与主线程共享状态无锁访问。
|
||
- 建议:为显存快照与按键事件引入互斥保护或双缓冲。
|
||
- **协议缓冲鲁棒性**:当前异常数据采用清空缓冲策略,存在丢包窗口。
|
||
- 建议:增加更精细的帧边界恢复策略与统计日志。
|
||
- **可测试性边界**:部分逻辑仍与全局状态耦合较深。
|
||
- 建议:逐步引入接口注入(如 `TcpOps`、`delay_ms`)降低耦合。
|
||
|
||
## 12. 版本与维护
|
||
|
||
- 文档版本:v1.0
|
||
- 适配代码基线:当前 `DTU-HMI` 仓库主干实现
|
||
- 维护建议:
|
||
- 每次新增模块或调整主流程时同步更新本文档
|
||
- 测试策略更新需同步维护“第 9 章 测试架构”
|
||
|