366 lines
10 KiB
Markdown
366 lines
10 KiB
Markdown
# DTU-HMI
|
||
|
||
`DTU-HMI` 是一个纯 C 实现的 PC 端 DTU 人机界面模拟工程,用于在电脑上复现现场装置的菜单结构、按键交互、LCD 绘制与远程显示链路。当前代码已经从早期的过程式菜单实现演进为“页面管理器 + 菜单页 MVP 分层”结构,更适合作为后续页面扩展、联调和测试的基础。
|
||
|
||
## 项目特性
|
||
|
||
- 多级菜单树,支持父子层级跳转与同层循环导航
|
||
- 160×160 单色 LCD 模拟,支持边框、文字、反显等基础绘制
|
||
- 按键输入驱动菜单导航,支持确认、返回、上下左右等操作
|
||
- 基于 TCP 的 RemoDispBus 远程显示链路,默认端口 `7003`
|
||
- UTF-8 文本显示与菜单宽度自适应计算
|
||
- 基于 CTest 的单元测试体系,已覆盖菜单、页面管理、按键、LCD、TCP 回环等模块
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 构建
|
||
|
||
在仓库根目录执行:
|
||
|
||
```powershell
|
||
cmake -S . -B build
|
||
cmake --build build
|
||
```
|
||
|
||
生成产物:
|
||
|
||
- Windows: `build/DTU-HMI.exe`
|
||
|
||
### 运行
|
||
|
||
```powershell
|
||
.\build\DTU-HMI.exe
|
||
```
|
||
|
||
程序启动后会:
|
||
|
||
- 初始化页面管理器、LCD 和菜单页
|
||
- 启动 TCP 服务线程,监听 `7003`
|
||
- 进入主循环,周期性读取按键并刷新当前页面
|
||
|
||
### 运行测试
|
||
|
||
```powershell
|
||
cmake -S . -B build
|
||
cmake --build build
|
||
ctest --test-dir build -C Debug --output-on-failure
|
||
```
|
||
|
||
补充说明:
|
||
|
||
- 单配置生成器下即使没有单独的 `Debug` 目录,`ctest` 也可直接运行
|
||
- 多配置生成器下可先显式构建:`cmake --build build --config Debug`
|
||
|
||
---
|
||
|
||
## 环境要求
|
||
|
||
- CMake `3.10+`
|
||
- C99 编译器
|
||
- Windows 下默认适配 MSVC,并自动启用 `/utf-8`
|
||
- 非 Windows 平台默认链接 `pthread`
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```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/
|
||
│ │ ├── ascii.c/h
|
||
│ │ ├── 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/
|
||
│ ├── 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
|
||
```
|
||
|
||
---
|
||
|
||
## 当前架构
|
||
|
||
### 总体运行流
|
||
|
||
当前主程序入口在 `src/main.c`,主流程可以概括为:
|
||
|
||
```text
|
||
main
|
||
├─ PageManager_Init()
|
||
├─ Lcd_Init()
|
||
├─ PageManager_Register(MenuPage_GetInstance())
|
||
├─ PageManager_Navigate(PAGE_ID_MENU)
|
||
├─ StartTcpServerThread(... port 7003 ...)
|
||
└─ while (1)
|
||
├─ Key_Read()
|
||
├─ PageManager_DispatchEvent(...)
|
||
└─ PageManager_Loop()
|
||
```
|
||
|
||
因此,当前项目的交互入口已经不是旧版 `menu.c` 直接驱动,而是统一通过页面系统调度。
|
||
|
||
### 页面管理器
|
||
|
||
`src/Drv/pages/page_manager.c` 负责维护“页面注册表 + 页面栈”两套状态,提供:
|
||
|
||
- `PageManager_Register()`:注册页面
|
||
- `PageManager_Navigate()`:按 `page_id` 导航
|
||
- `PageManager_Push()` / `PageManager_Pop()`:管理页面栈
|
||
- `PageManager_DispatchEvent()`:把事件分发给当前栈顶页面
|
||
- `PageManager_Loop()`:周期驱动当前页面
|
||
|
||
当前 `page_t` 生命周期约定:
|
||
|
||
- 首次进入页面:`on_create -> on_enter`
|
||
- 切页离开:`on_exit`
|
||
- 非缓存页出栈:`on_exit -> on_destroy`
|
||
- 周期调度:`on_loop`
|
||
|
||
这使得项目后续可以继续扩展成多页面系统,而不是只服务于一个菜单模块。
|
||
|
||
### 菜单页 MVP 分层
|
||
|
||
当前菜单页位于 `src/Drv/pages/menu/`,职责拆分如下:
|
||
|
||
- `model.c`:菜单静态表、树构建、显示名修饰、层级索引与布局计算
|
||
- `presenter.c`:按键路由、路径重建、刷新策略判定
|
||
- `view.c`:边框、文字、高亮、整页刷新与局部刷新
|
||
- `page.c`:把菜单页接入 `PageManager` 生命周期
|
||
|
||
数据流可以理解为:
|
||
|
||
```text
|
||
按键输入
|
||
-> MenuPage_OnEvent
|
||
-> MenuPresenter_HandleInput
|
||
-> MenuNavigator_ProcessKey
|
||
-> 更新 tagMenuCtrl
|
||
-> MenuPresenter_Refresh
|
||
-> MenuView_* 渲染接口
|
||
-> LCD 渲染端口
|
||
```
|
||
|
||
---
|
||
|
||
## 菜单数据模型
|
||
|
||
### 静态定义
|
||
|
||
菜单原始数据定义在 `src/Drv/pages/menu/model.c` 的 `menuTab[]` 中,每一项使用 `tagMenuModel` 描述:
|
||
|
||
- `byClass`:层级,`0` 表示顶层
|
||
- `byName`:菜单显示名
|
||
- `byTip`:提示文本
|
||
- `byAttrib`:业务属性
|
||
- `wPassword`:权限或密码
|
||
- `wPara`:动作参数
|
||
- `pfnWinProc`:叶子节点动作入口
|
||
|
||
### 运行时节点
|
||
|
||
运行时菜单节点类型为 `MenuItem`,由三部分组成:
|
||
|
||
- `links`:父、子、前、后四向链路
|
||
- `menuDef`:静态业务定义副本
|
||
- `rect`:菜单框位置、子项数量、当前项序号
|
||
|
||
关键字段含义:
|
||
|
||
- `links.higher`:父节点
|
||
- `links.lower`:第一个子节点
|
||
- `links.before` / `links.behind`:同层前后节点
|
||
- `rect.wPos`:当前节点在本层中的 `0` 基序号
|
||
- `rect.wNum`:当前节点的直接子节点数量
|
||
- `rect.wSPosX/Y`、`rect.wEPosX/Y`:子菜单框坐标
|
||
|
||
### 初始化流程
|
||
|
||
`MenuModel_Init()` 的核心步骤如下:
|
||
|
||
1. 统计 0 级菜单数量
|
||
2. 将 `menuTab[]` 构建成运行时树
|
||
3. 为存在子菜单的项追加显示箭头 `\x10`
|
||
4. 计算每个节点的层内索引 `wPos` 和子节点数量 `wNum`
|
||
5. 计算各层菜单框的初始坐标
|
||
|
||
### 结构特点
|
||
|
||
- 同层链表最终会闭环,便于上下循环导航
|
||
- 子菜单框 X 坐标从父菜单右侧展开
|
||
- 子菜单 Y 坐标会根据当前项位置和屏幕边界做自适应修正
|
||
- 视图层只读取 `rect` 结果,不参与复杂布局推导
|
||
|
||
---
|
||
|
||
## 菜单导航与渲染
|
||
|
||
### 导航规则
|
||
|
||
导航逻辑集中在 `src/Drv/pages/menu/presenter.c`,主要按键语义如下:
|
||
|
||
- `KEY_U`:移动到同层前一个菜单
|
||
- `KEY_D`:移动到同层后一个菜单
|
||
- `KEY_R` / `KEY_ENT`:进入子菜单,或执行叶子节点动作
|
||
- `KEY_L` / `KEY_ESC`:返回上一级
|
||
|
||
Presenter 维护的核心状态包括:
|
||
|
||
- `ptCurrent`:当前选中项
|
||
- `ptRoute[]`:当前路径上的各层节点
|
||
- `ptCurBak`:上一次选中项
|
||
- `pt0Level`:当前顶层上下文
|
||
|
||
### 刷新策略
|
||
|
||
当前渲染不是每次都整页重画,而是根据状态差异选择不同路径:
|
||
|
||
- 首帧或跨顶层切换:`full_refresh`
|
||
- 同层同父切换:`update_selection_same_level`
|
||
- 进入更深层:`update_selection_new_level`
|
||
|
||
这种方式可以减少闪烁,也让 Presenter 和 View 的职责更清晰。
|
||
|
||
### 顶部状态栏
|
||
|
||
`src/Drv/pages/menu/def.h` 中定义了 `MenuMode`,用于顶部状态栏文案:
|
||
|
||
- `MODE_NONE`
|
||
- `MODE_OVERFLOW_PROTECTION`
|
||
- `MODE_LOCAL_FEEDER_SEGMENT`
|
||
- `MODE_LOCAL_FEEDER_CONTACT`
|
||
|
||
当前 `view.c` 已支持这些模式的显示,默认使用 `MODE_NONE`。
|
||
|
||
---
|
||
|
||
## 远程显示与网络
|
||
|
||
项目通过 `src/remoteDisplay.c` 与 `src/TCP/tcp.c` 提供远程显示基础能力。
|
||
|
||
当前主程序会在独立线程中启动 TCP 服务:
|
||
|
||
- 默认监听端口:`7003`
|
||
- 用途:同步 LCD 内容,并为远程显示或上位机交互预留链路
|
||
|
||
如果需要联调网络链路,建议重点查看:
|
||
|
||
- `src/main.c`
|
||
- `src/remoteDisplay.c`
|
||
- `src/TCP/tcp.c`
|
||
|
||
---
|
||
|
||
## UTF-8 与 LCD 显示
|
||
|
||
项目中的文本显示链路大致如下:
|
||
|
||
```text
|
||
UTF-8 文本
|
||
-> utf8.c 解析字符
|
||
-> lcd_text / ascii 绘制字符
|
||
-> lcd / lcd_draw 写入像素缓冲
|
||
-> renderer_lcd 输出到页面渲染端口
|
||
```
|
||
|
||
菜单宽度计算不是按字节长度,而是按显示宽度计算:
|
||
|
||
- ASCII 字符记宽度 `1`
|
||
- 多字节 UTF-8 字符记宽度 `2`
|
||
|
||
对应实现主要在:
|
||
|
||
- `MenuModel_Utf8LenCal()`
|
||
- `MenuModel_GetMenuMaxDisplayLen()`
|
||
|
||
这也是菜单框宽度能够同时适配中文和英文标题的基础。
|
||
|
||
---
|
||
|
||
## 调试
|
||
|
||
可通过 CMake 选项打开 `DEBUG` 宏:
|
||
|
||
```powershell
|
||
cmake -S . -B build -DENABLE_DEBUG=ON
|
||
cmake --build build
|
||
```
|
||
|
||
建议调试入口:
|
||
|
||
- 菜单树构建问题:看 `src/Drv/pages/menu/model.c`
|
||
- 页面切换与事件分发问题:看 `src/Drv/pages/page_manager.c`
|
||
- 高亮与刷新异常:看 `src/Drv/pages/menu/presenter.c`、`src/Drv/pages/menu/view.c`
|
||
- TCP 联调问题:看 `src/main.c`、`src/remoteDisplay.c`、`src/TCP/tcp.c`
|
||
|
||
---
|
||
|
||
## 测试说明
|
||
|
||
当前 `tests/CMakeLists.txt` 已注册以下测试:
|
||
|
||
- `test_smoke`:测试框架可用性
|
||
- `test_p0_remote_display`:远程显示协议/链路核心逻辑
|
||
- `test_p0_utf8_hz12_get`:字库查找
|
||
- `test_p1_key`:按键模块
|
||
- `test_p1_lcd_basic`:LCD 像素与基础绘制
|
||
- `test_p1_page_manager`:页面管理器生命周期、导航与事件分发
|
||
- `test_p1_menu`:菜单模型、视图初始化、页面入口绑定
|
||
- `test_p2_tcp_loopback`:TCP 本机回环集成测试
|
||
|
||
其中比较值得关注的是:
|
||
|
||
- `test_p1_menu.c`:覆盖菜单树构建、显示名修饰、`wPos/wNum` 统计、菜单页实例绑定
|
||
- `test_p1_page_manager.c`:覆盖注册、导航、`ESC` 兜底回退、缓存页与非缓存页行为
|
||
|
||
建议门禁:
|
||
|
||
- 提交前至少执行一次 `ctest --test-dir build -C Debug --output-on-failure`
|
||
- 修改纯逻辑代码时优先补充或更新对应单元测试
|
||
|
||
---
|
||
|
||
## 后续扩展建议
|
||
|
||
从当前代码结构看,比较自然的演进方向包括:
|
||
|
||
- 在 `pages/` 下继续新增业务页面,而不是把所有交互都塞进菜单页
|
||
- 为 `MenuMode` 和顶部状态栏接入真实业务状态源
|
||
- 补充 RemoDispBus 交互说明和上位机联调文档
|
||
- 继续加强 `Presenter` 和 `View` 的细粒度测试覆盖
|