14 KiB
DTU-HMI
DTU-HMI 是一个纯 C 实现的 PC 端 DTU 人机界面模拟工程,用于在电脑上复现现场装置的菜单结构、按键交互、LCD 绘制与远程显示链路。当前代码已经从早期的过程式菜单实现演进为“页面管理器 + 菜单页 MVP 分层”结构,更适合作为后续页面扩展、联调和测试的基础。
项目特性
- 多级菜单树,支持父子层级跳转与同层循环导航
- 160×160 单色 LCD 模拟,支持边框、文字、反显等基础绘制
- 按键输入驱动菜单导航,支持确认、返回、上下左右等操作
- 基于 TCP 的 RemoDispBus 远程显示链路,默认端口
7003 - UTF-8 文本显示与菜单宽度自适应计算
- 基于 CTest 的单元测试体系,已覆盖菜单、页面管理、按键、LCD、TCP 回环等模块
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (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 处理 │
└─────────────────────────────────────────────────────────────┘
快速开始
构建
在仓库根目录执行:
cmake -S . -B build
cmake --build build
生成产物:
- Windows:
build/DTU-HMI.exe
运行
.\build\DTU-HMI.exe
程序启动后会:
- 初始化页面管理器、LCD 和菜单页
- 启动 TCP 服务线程,监听
7003 - 进入主循环,周期性读取按键并刷新当前页面
运行测试
# 1. 配置项目(生成构建文件)
cmake -S . -B build
# 2. 编译项目
cmake --build build
# 3. 测试
ctest --test-dir build -C Debug --output-on-failure
# 4. 运行编译结果
./\build\Debug\DTU-HMI.exe
补充说明:
- 单配置生成器下即使没有单独的
Debug目录,ctest也可直接运行 - 多配置生成器下可先显式构建:
cmake --build build --config Debug
环境要求
- CMake
3.10+ - C99 编译器
- Windows 下默认适配 MSVC,并自动启用
/utf-8 - 非 Windows 平台默认链接
pthread
目录结构
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
当前架构
总体运行流
当前主程序入口在 src/main.c,主流程可以概括为:
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生命周期
数据流可以理解为:
按键输入
-> 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() 的核心步骤如下:
- 统计 0 级菜单数量
- 将
menuTab[]构建成运行时树 - 为存在子菜单的项追加显示箭头
\x10 - 计算每个节点的层内索引
wPos和子节点数量wNum - 计算各层菜单框的初始坐标
结构特点
- 同层链表最终会闭环,便于上下循环导航
- 子菜单框 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_NONEMODE_OVERFLOW_PROTECTIONMODE_LOCAL_FEEDER_SEGMENTMODE_LOCAL_FEEDER_CONTACT
当前 view.c 已支持这些模式的显示,默认使用 MODE_NONE。
远程显示与网络
项目通过 src/remoteDisplay.c 与 src/TCP/tcp.c 提供远程显示基础能力。
当前主程序会在独立线程中启动 TCP 服务:
- 默认监听端口:
7003 - 用途:同步 LCD 内容,并为远程显示或上位机交互预留链路
如果需要联调网络链路,建议重点查看:
src/main.csrc/remoteDisplay.csrc/TCP/tcp.c
UTF-8 与 LCD 显示
项目中的文本显示链路大致如下:
UTF-8 文本
-> utf8.c 解析字符
-> lcd_text / ascii 绘制字符
-> lcd / lcd_draw 写入像素缓冲
-> renderer_lcd 输出到页面渲染端口
菜单宽度计算不是按字节长度,而是按显示宽度计算:
- ASCII 字符记宽度
1 - 多字节 UTF-8 字符记宽度
2
对应实现主要在:
MenuModel_Utf8LenCal()MenuModel_GetMenuMaxDisplayLen()
这也是菜单框宽度能够同时适配中文和英文标题的基础。
调试
可通过 CMake 选项打开 DEBUG 宏:
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的细粒度测试覆盖