Files
DTU-HMI/README.md

14 KiB
Raw Permalink Blame History

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.cmenuTab[] 中,每一项使用 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/Yrect.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.csrc/TCP/tcp.c 提供远程显示基础能力。

当前主程序会在独立线程中启动 TCP 服务:

  • 默认监听端口:7003
  • 用途:同步 LCD 内容,并为远程显示或上位机交互预留链路

如果需要联调网络链路,建议重点查看:

  • src/main.c
  • src/remoteDisplay.c
  • src/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.csrc/Drv/pages/menu/view.c
  • TCP 联调问题:看 src/main.csrc/remoteDisplay.csrc/TCP/tcp.c

测试说明

当前 tests/CMakeLists.txt 已注册以下测试:

  • test_smoke:测试框架可用性
  • test_p0_remote_display:远程显示协议/链路核心逻辑
  • test_p0_utf8_hz12_get:字库查找
  • test_p1_key:按键模块
  • test_p1_lcd_basicLCD 像素与基础绘制
  • test_p1_page_manager:页面管理器生命周期、导航与事件分发
  • test_p1_menu:菜单模型、视图初始化、页面入口绑定
  • test_p2_tcp_loopbackTCP 本机回环集成测试

其中比较值得关注的是:

  • test_p1_menu.c:覆盖菜单树构建、显示名修饰、wPos/wNum 统计、菜单页实例绑定
  • test_p1_page_manager.c:覆盖注册、导航、ESC 兜底回退、缓存页与非缓存页行为

建议门禁:

  • 提交前至少执行一次 ctest --test-dir build -C Debug --output-on-failure
  • 修改纯逻辑代码时优先补充或更新对应单元测试

后续扩展建议

从当前代码结构看,比较自然的演进方向包括:

  • pages/ 下继续新增业务页面,而不是把所有交互都塞进菜单页
  • MenuMode 和顶部状态栏接入真实业务状态源
  • 补充 RemoDispBus 交互说明和上位机联调文档
  • 继续加强 PresenterView 的细粒度测试覆盖