# DTU-HMI `DTU-HMI` 是一个纯 C 实现的 PC 端 DTU 人机界面模拟工程,用于在电脑上复现现场装置的菜单结构、按键交互、LCD 绘制与远程显示链路。当前代码已经从早期的过程式菜单实现演进为“页面管理器 + 菜单页 MVP 分层”结构,更适合作为后续页面扩展、联调和测试的基础。 ## 项目特性 - 多级菜单树,支持父子层级跳转与同层循环导航 - 160×160 单色 LCD 模拟,支持边框、文字、反显等基础绘制 - 按键输入驱动菜单导航,支持确认、返回、上下左右等操作 - 基于 TCP 的 RemoDispBus 远程显示链路,默认端口 `7003` - UTF-8 文本显示与菜单宽度自适应计算 - 基于 CTest 的单元测试体系,已覆盖菜单、页面管理、按键、LCD、TCP 回环等模块 --- ```txt ┌─────────────────────────────────────────────────────────────┐ │ 应用层 (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 处理 │ └─────────────────────────────────────────────────────────────┘ ``` ## 快速开始 ### 构建 在仓库根目录执行: ```powershell cmake -S . -B build cmake --build build ``` 生成产物: - Windows: `build/DTU-HMI.exe` ### 运行 ```powershell .\build\DTU-HMI.exe ``` 程序启动后会: - 初始化页面管理器、LCD 和菜单页 - 启动 TCP 服务线程,监听 `7003` - 进入主循环,周期性读取按键并刷新当前页面 ### 运行测试 ```powershell # 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` --- ## 目录结构 ```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 ``` --- ## 当前架构 ### 总体运行流 当前主程序入口在 `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` 的细粒度测试覆盖