# 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` 的细粒度测试覆盖
