# 综合平台编程器架构 ## 目标与范围 项目是面向设备交付、调试和验证工程师的单工程、单 PLC、多 HMI 页面和多控制逻辑 Qt 桌面编程器,提供 HMI 编辑、结构化梯形图、离线仿真和 Modbus RTU 真机联机。 本项目不生成、编译或下载 PLC 程序,不读取 PLC 内部程序与网络轨迹,不实现完整 XDPPro 指令集,也不支持任意自由画线。真机运行时 PLC 内部程序是唯一控制源,PC 只读写工程实际引用的 M/D 地址。 ## 分层与依赖 ```text app/src/ ├── domain/ 无 Qt 依赖的模型、值对象、校验和仓库接口 ├── services/ 编辑、工程、仿真、运行模式和通信契约 ├── infrastructure/ JSON、PLC 缓存、Qt Modbus RTU 实现 ├── ui/ Qt Widgets 视图、对话框和界面控制器 └── main.cpp 对象创建与依赖注入 ``` 生产代码依赖方向固定为: ```text UI -> Services -> Domain Infrastructure -> Services contracts + Domain main.cpp -> UI + Services + Infrastructure ``` - `domain` 不依赖 Qt、文件系统或串口 - `services` 执行业务用例和原子编辑,通过接口使用工程存储与 PLC 通信 - `infrastructure` 实现 JSON 工程存储、PLC 缓存和异步 Modbus RTU - `ui` 只提交命令并投影模型、轨迹和错误,不直接读写串口或承载业务校验 - `main.cpp` 只组合对象,不实现业务规则 主窗口静态布局由 `main_window.ui` 管理。UI 协调分为: | 组件 | 职责 | | --- | --- | | `ProjectWorkspaceController` | 工程树、当前页面/逻辑会话选择和编辑对象刷新 | | `PropertyPanelController` | HMI/梯形图选择、属性表单和编辑服务调用 | | `RuntimePanelController` | 运行会话、顶层运行监控窗口、运行值刷新和仿真故障反馈 | | `RuntimeMonitorWindow` | 独立顶层窗口的显示、隐藏、关闭请求和应用退出生命周期 | | `RuntimeMonitorWidget` | 全应用唯一运行投影,组合可交互 HMI、梯形图轨迹和自由监控 | | `MainWindow` | 窗口级动作、工程文件操作、控制器组合和模式切换 | 运行界面统一投影到独立顶层 `RuntimeMonitorWindow` 中的唯一 `RuntimeMonitorWidget`。该窗口是编程器的工程师运行监控大屏,不是旧的纯 HMI 操作员窗口: - 进入离线或真机运行后,弹出并最大化运行监控窗口;主编程器窗口保留在后面且不改变布局,运行期间由同一份 `ModePolicy` 禁用编辑入口 - 主窗口不保留运行监控页签,也不创建第二份监控控件;工程对象刷新和运行交互始终指向弹窗中的唯一实例 - 离线时显示可交互 HMI、软件梯形图轨迹和自由监控;真机时显示可交互 HMI 和自由监控,不能显示本地梯形图伪轨迹 - HMI 页面跳转、按钮和数值输入都通过同一份 `HmiNavigationService` 和活动寄存器仓库完成;离线写虚拟 M/D,真机写 PLC,仿真停止或通信不可用时禁止写入 - 运行监控的返回编辑按钮或窗口关闭按钮都请求 `MainWindow -> RuntimeModeService` 返回编辑态。离线时先停止软件扫描,真机时保留 PLC 连接但撤销运行会话;应用退出时允许顶层窗口真正关闭 ## 工程模型与持久化 ```text Project ├── metadata ├── hmiPages[] + initialHmiPageId ├── alarmDefinitions[] ├── registerComments[] └── controlLogics[] └── rungs[] ├── condition └── output ``` - 页面、逻辑和节点使用稳定 ID;名称可修改,数组顺序决定工程树顺序和离线扫描顺序 - 当前编辑页面和当前逻辑是 UI 会话状态,不写入工程 - HMI 控件类型、默认值、绑定规则和运行值类型由无 Qt 依赖的 `hmi_control_registry` 统一描述 - 需要寄存器的 HMI 控件、报警条件和梯形图节点直接保存 M/D 地址;项目不再维护统一 `dataPoints` 表 - `registerComments` 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询 工程 JSON 格式固定为严格的 `1.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。当前版本不读取旧 `dataPoints`、`stages/branches` 或缺少必填字段的同版本草稿。字段定义见 `docs/工程格式说明.md`。 校验分两级: - `validate()` 保证可保存结构合法,允许未绑定控件、待配置节点和未完成网络作为编辑草稿 - `validateForRunning()` 检查运行所需绑定和已启用逻辑;禁用草稿不阻止运行 数量常量集中在 `domain/project_limits.h`,最终业务口径见 `docs/用户使用/数量边界确认方案.md`。领域、服务、JSON 和 UI 入口必须使用同一组常量,不能各自维护数字。 ## 结构化梯形图 网络条件保存为规范化表达式树: ```text ConditionExpression ├── Node 触点、边沿或比较条件 ├── Wire 无地址横线,保存条件区列跨度 ├── Series 子表达式执行 AND └── Parallel 子表达式执行 OR ``` 输出是网络最右侧的单独节点。`LogicEditorService` 把串联、并联、旁路、替换和删除转换为表达式树操作,失败时回退整个编辑;`LogicEditorWidget` 只把树投影成正交网格。竖线由 `Parallel` 边界派生,网格坐标和自由线段不持久化,因此领域模型不会产生悬空线、环路或多输出路径。 单网络前 10 列为条件区,输出最多位于第 11 列;横线只能占条件区。第 11 个条件、过深表达式或过多并联行会在编辑和加载阶段被拒绝,具体数值以数量边界文档为准。 网络运行语义: - 空网络不执行 - 有条件但没有输出的已启用网络不能运行 - 有输出但没有条件的网络是恒真网络,每次扫描执行输出 - `Wire` 的逻辑值为真,但运行轨迹是否导通仍取决于上游电流 节点配置使用 `std::variant` 的独立类型。当前条件包括 M 触点与上下沿、T/C 触点和 D 比较;输出包括普通/置位/复位 M 线圈、TON、CTU/CTD、MOVE、ADD/SUB。新增指令必须增加独立配置、校验、JSON、执行、编辑和测试分支,不能向通用节点堆叠无关字段。 `SoftwareLogicExecutor` 按 `controlLogics` 和网络数组顺序扫描已启用逻辑,前面网络的 M/D 写入在同一扫描周期对后面网络可见。T/C 是离线执行器内部资源,不映射 PLC T/C 区,也不进入 Modbus 轮询;计数器 CV/PV 等显式 M/D 引用仍使用统一仓库。扫描轨迹按逻辑 ID 分区,避免不同逻辑中的重复节点 ID 相互覆盖。 ## 数据源与运行模式 运行界面只访问 `RegisterRepository`。`ActiveRegisterRepository` 根据模式切换实际数据源: ```text 编辑:UI -> Editor Services -> ProjectService -> Project └── ProjectStorage -> JSON 离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository ^ OfflineSimulationService(SoftwareLogicExecutor) 真机:运行监控弹窗 -> ActiveRegisterRepository -> PlcRegisterRepository(cache) ^ PLC <-> PlcCommunicationService <--------------------┘ ``` | 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 | | --- | --- | --- | --- | --- | | 编辑态 | 是 | 无运行数据源 | 停止 | 运行态先退出 | | 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 | | 真机运行 | 否 | PLC 读回缓存 | 停止 | PLC 已连接并完成本次首读 | 离线和真机不能直接互切。进入真机不复制或下发离线值,也不显示本地梯形图导通轨迹。 自由监控是会话级诊断工具,不属于 `Project`,不写入 JSON。它通过活动仓库读取和单点写入 M/D;真机写入仍必须经过连接与首读门槛。 ## Modbus RTU 通信 `PlcCommunicationService` 使用 `QModbusRtuSerialMaster` 异步连接、轮询和单点写入,UI 线程不等待串口响应。Modbus 地址使用从 `0` 开始的原始地址。 轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。T/C 内部资源、横线和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再应用新集合。 PLC 缓存只保存最后一次成功读回值。写请求受理后不乐观修改缓存,界面等待后续轮询确认真实值。真机运行资格属于当前连接代次:必须完成完整首读,旧连接的异步回复不能污染重连后的状态。 通信状态包括 `Disconnected / Connecting / Connected / Recovering / Faulted`。连接失败或物理断线会关闭串口会话并进入 `Disconnected`;超时、协议或读写故障会停止正常轮询、保留最后有效缓存并进入 `Faulted`。所有通信故障都会撤销首读资格;若当时处于真机运行,运行模式服务立即返回编辑态。可恢复超时探测成功后进入 `Recovering` 并重新完整首读,但不会自动重新进入真机运行。 ## 扩展规则 - 新业务先确定所属领域模型和服务用例,再添加 UI;不得从 UI 直接改工程容器或串口状态 - 编辑操作必须在服务层校验并原子提交,失败不能污染撤销历史或留下半成品 - 新持久化字段必须同步领域校验、严格 JSON 往返、缺失/非法字段测试和格式说明;是否升级格式版本必须明确决定 - 新数量边界必须进入 `project_limits.h`、数量边界文档和上下界测试 - 新 HMI 类型优先扩展 `hmi_control_registry`,专用交互留在对应服务和图元 - 不重新引入数据点表、自由线段、UI 直连串口或真机执行本地梯形图 - 自动化测试按风险覆盖领域、服务、存储、通信和 UI;真实串口、接线和设备 RUN 联动必须现场验证