# 综合平台编程器架构 ## 目标与范围 项目是面向设备交付、调试和验证工程师的单工程、单 PLC、多 HMI 页面和多控制逻辑 Qt 桌面编程器,提供 HMI 编辑、结构化梯形图、离线仿真和 Modbus RTU 真机联机。 本项目不生成、编译或下载 PLC 程序,不读取 PLC 内部程序与网络轨迹,不实现完整 XDPPro 指令集,也不支持任意自由画线。当前梯形图模型不包含 T/C 触点、TON 或 CTU/CTD。真机运行时 PLC 内部程序仍是唯一设备控制源;HMI 和自由监控可以正常读写工程引用的 M/D,本地梯形图只读取 PLC 缓存并推算轨迹,不把线圈或数据指令结果写回 PLC。 ## 分层与依赖 ```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` 禁用编辑入口 - 运行监控窗口顶部始终提供“离线仿真/真机运行”模式选择;从一种运行模式切换到另一种时由 UI 自动经过编辑态,再执行目标模式的完整校验 - 主窗口不保留运行监控页签;编辑态主窗口提供与“HMI 页面”“控制逻辑”并列的“数据监控”编辑页签,运行态显示弹窗中的运行监控投影 - 离线时显示可交互 HMI、软件梯形图轨迹和自由监控;真机时同时显示可交互 HMI、自由监控和根据 PLC 缓存计算的“本地推算轨迹” - HMI 页面跳转、按钮和数值输入都通过同一份 `HmiNavigationService` 和活动寄存器仓库完成;离线写虚拟 M/D,真机写 PLC,仿真停止或通信不可用时禁止写入 - 运行监控只能通过界面内的返回编辑按钮请求 `MainWindow -> RuntimeModeService` 返回编辑态;运行期间系统窗口关闭路径被忽略,离线时先停止软件扫描,真机时保留 PLC 连接但撤销运行会话;应用退出时允许顶层窗口真正关闭 ## 工程模型与持久化 ```text Project ├── metadata ├── hmiPages[] + initialHmiPageId ├── alarmDefinitions[] ├── registerComments[] └── controlLogics[] └── rungs[] ├── cells[10] └── output └── verticalConnections[] ``` - 页面、逻辑、行、网格、节点和竖线使用稳定 ID;名称可修改,数组顺序决定视觉顺序和离线扫描顺序 - 梯形图是连续网格:`Gap/Wire/Node` 是独立格子对象,竖线连接相邻行列边界,插入/删除行会维护连接关系,删线即可拆分网络 - `ControlLogic::networkHeadIndex()` 是网络首行的唯一判定;每个网络只有首行的 `LadderRung::comment` 可以非空,支路行保存注释属于无效工程结构 - 当前编辑页面和当前逻辑是 UI 会话状态,不写入工程 - HMI 控件类型、默认值、绑定规则和运行值类型由无 Qt 依赖的 `hmi_control_registry` 统一描述 - HMI 数值控件通过 `RegisterDataType` 选择 `Int16`、`Int32`、`Float32` 或 `Float64`;底层仓库始终保存原始 16 位 D 字,统一编解码层按低地址低字顺序组合 1、2 或 4 个连续 D - 状态文本是独立只读控件:M 配置 OFF/ON 文本,D 配置覆盖全域的连续半开区间;离线读取虚拟仓库,真机读取 PLC 缓存,不可用时显示 `--` - HMI 控件根据控件描述符自动使用固定绑定区域:按钮/指示灯使用 M,数值显示/输入使用 D;新增控件从当前区域最大已用地址后继续递增,属性面板只保留地址编号输入,状态文本的 M/D 选择仍属于专用状态映射配置 - HMI 按钮保留原有四种 M 位操作,并可附加一个 M 位或 D 数值启用条件;条件不满足或读回失败时运行画面置灰且禁止操作,条件引用地址纳入 PLC 轮询 - 需要寄存器的 HMI 控件、报警条件和梯形图节点直接保存 M/D 地址;项目不再维护统一 `dataPoints` 表 - `registerComments` 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询 工程 JSON 格式固定为严格的 `4.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。旧 `1.0/2.0/3.0` 文件直接拒绝,不做迁移或双写。字段定义见 `docs/工程格式说明.md`。 校验分两级: - `validate()` 保证可保存结构合法,允许未绑定控件、待配置节点和未完成网络作为编辑草稿 - `validateForRunning()` 检查运行所需绑定和已启用逻辑;禁用草稿不阻止运行 数量的绝对硬上限和默认值集中在 `domain/project_limits.h`。启动时可由应用配置收紧的数量使用同一份 `ProjectLimitSettings`,由 `main.cpp` 注入领域校验、编辑服务、JSON 存储和 UI;任何一层都不能另存一套运行上限。最终业务口径见 `docs/用户使用/数量边界确认方案.md`。 ## 应用启动配置 `ApplicationSettingsLoader` 在 Qt 应用创建后、其他业务对象创建前读取可执行文件目录下的 `config/application.ini`。配置只读取一次,不热更新,也不写入工程 JSON。 ```text config/application.ini | ApplicationSettingsLoader | ApplicationSettings(启动后只读) |-- ProjectLimitSettings -> Domain / Editor Services / JsonProjectStorage / UI |-- HmiDefaultSettings -> HmiEditorService / PropertyPanelController `-- PlcDefaults -> MainWindow -> PlcConnectionDialog ``` - 文件不存在时创建默认文件,本次仍使用代码默认值;已有文件永不被自动覆盖 - 缺失的普通字段使用代码默认值,未知字段忽略,两者都记录到底部输出面板 - 重复字段、版本不兼容、非法 UTF-8、类型错误或越界值会使整份文件失效,所有配置回退为代码默认值 - 配置创建或加载严重失败时主窗口继续创建,启动诊断先进入底部输出面板,再异步弹出一次警告框 - HMI 宽高只决定新建页面的初始尺寸;已有页面继续使用工程 JSON 中保存的尺寸 - PLC 默认参数只用于本次进程第一次打开配置窗口时的初值;用户在窗口中的修改只保留在当前进程,不自动回写 INI 字段、范围和用户操作方式见 `docs/用户使用/应用配置说明.md`。 ## 用户运行程序导出 编程器通过“导出用户运行程序”把当前工程先原子写入临时 JSON,再将当前已编译的编程器 exe 复制为模板,把 JSON 追加到 exe 尾部的固定封装格式中,并复制 Qt 运行库和平台插件。封装格式包含魔数、版本、数据长度和 SHA-256,写入使用临时文件后原子改名。带有有效工程封装的 exe 启动时读取自身尾部工程数据,读取工程成功后直接隐藏编辑器并进入离线运行;运行监控顶部提供离线/真机切换、PLC 配置、断开和退出程序入口。普通 `integrated_platform.exe` 没有工程封装时按编程器入口启动。 `scripts/package_qt_app.ps1` 只用于维护者构建编程器发布包和复制 Qt 运行库,不参与工程导出。编程器发布包包含自身 exe 及 Qt 运行库后可以移动到任意目录,导出时不再依赖 PowerShell、qmake、MinGW 或项目源码目录。 ## 结构化梯形图 连续梯形图不再使用串并联表达式树。`LadderRung` 是按视觉顺序排列的一行,前 10 列固定保存 `LadderCell`,每格独立表示 `Gap`、`Wire` 或条件 `Node`;第 11 列是独立输出槽。行之间的网络关系由 `ControlLogic::verticalConnections` 保存,每根竖线只连接相邻两行的一个 `0~10` 列边界。长竖线由多个相邻连接对象组成,因此可以逐段画线、删线、拆分和恢复。 `LogicEditorService` 负责网格、竖线、行和网络注释的原子编辑:横向手势批量设置 `Wire / Gap`,纵向手势批量创建或删除相邻 `VerticalConnection`,插入行会拆分原连接,删除行只在上下同列连接都存在时合并。用户从任意支路编辑网络注释时,服务统一读写网络首行;竖线或粘贴合并网络时,单侧注释移动到新首行,相同注释去重,不同注释阻止整次合并。一次鼠标手势和一次工具栏操作都只产生一条撤销记录,失败时回滚整个编辑。`LogicEditorWidget` 只负责命中测试、绘制和将服务结果投影到画布,不直接修改工程容器。 梯形图复制使用服务层值对象 `LogicClipboardFragment`,不保存源模型指针。`GridObjects` 模式按相对行列保存明确选中的 `Wire / Node`、输出和竖线,未选位置不写入片段,也不把 `Gap` 当成待覆盖内容;`WholeRows` 模式只由左侧行号选择产生,保存完整 10 格、输出、注释和选中行内部的竖线。`LogicEditorService::pasteClipboard()` 先校验全部目标行列、对象类型、冲突和数量上限,再统一分配新 ID 并提交一次历史记录;任一目标非法时恢复模型和工程修改状态。当前光标行与明确整行选择是两份独立 UI 状态,不能互相回退。 单格编程通过 `LogicEditCursor -> LogicEditResult` 服务契约完成。服务在同一事务中写入触点、横线或输出并返回下一光标:条件区逐格右移,第 10 格后进入输出槽,输出后越过当前竖线连通分组;末尾输出会连同下一空行一起提交和撤销。工具栏竖线操作通过独立结果返回下一视觉行,F12 每成功连接一对相邻行后保持列边界不变并向下推进,末行停止且不自动创建新行;已经存在的连接只推进位置,不产生脏状态或空撤销记录。鼠标画线/删线拖动由 `LogicEditorWidget` 在前景层绘制临时预览,移动过程中不修改工程,释放时才调用范围编辑服务一次性提交;非法释放直接丢弃整次预览,合法释放只产生一条撤销记录。空行直接写输出时,输出事务把 10 个条件格全部补为 `Wire`;已有条件时只补最右有效内容到输出槽之间的尾部 `Gap`,不会跨过中间的明确断点。画布内嵌指令、工具栏动作和 QAction 快捷键只消费服务结果,不在 UI 重复计算推进规则。 网络不是额外的表达式对象,而是横向导通格、条件节点、输出槽以及竖线连通分量共同决定的电气关系。执行器按列传播电源,在每个列边界先合并竖线连通分量,再计算各行网格和输出;删除竖线会自然拆分网络。节点配置使用 `std::variant` 的独立类型,当前条件包括 M 常开/常闭、M 上升沿/下降沿和 D 比较,输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。 控制逻辑组可以暂时没有行。新增行会生成 10 个稳定网格 ID;鼠标横向拖动设置连续 `Wire`,竖向拖动创建多个相邻 `VerticalConnection`。插入行时原有竖线被拆成上下两段以保持连续;删除行时只有上下两段在同列都存在才合并,单边连接直接移除。所有编辑由 `LogicEditorService` 原子提交并进入撤销历史。 网络运行语义: - 空网络不执行 - 输出槽按所在行的实际导通状态执行;没有输出的行可以作为纯连接支路或编辑草稿存在,不会单独产生输出 - 直接接通的输出使用 10 个连续 `Wire` 网格显式表示恒真路径,每次扫描执行输出 - `Wire` 的逻辑值为真,但运行轨迹是否导通仍取决于上游电流 - `Gap` 表示明确断路,可以保存编辑草稿;执行器遇到 `Gap` 时该支路不导通,允许作为并联网络中的断路支路 `LogicEditorService::checkSyntax()` 在当前控制逻辑上执行信捷式“规整 LD + 语法检查”。服务按列计算从左母线向右的结构可达性和从所有输出向左的反向可达性;对于已经存在完整输出路径的网络,只保留能够参与完整路径的 `Wire` 和 `VerticalConnection`,没有输出的残线全部清除。规整不删除条件节点、输出或数据指令;若某个输出网络本身断路,则该网络不参与自动清理,保留用户现场并返回控制逻辑、网络、视觉行和第 11 列输出槽位置。一次检查对多条横竖线的修改只提交一条梯形图撤销记录。 `RuntimeModeService` 在离线启动和完成 PLC 首读后的真机启动前调用 `LogicEditorService::checkEnabledSyntax()`;用户运行程序导出执行同一检查。规整完成后,`ControlLogic::validateForRunning()` 继续使用和执行器相同的逐列边界与竖线连通关系做最终静态可达性检查。没有输出的自动追加空行可以继续保留;任何带输出的行都必须存在从左母线到第 10 列边界的结构路径。中间 `Gap` 只有在其他并联支路能够绕过时才允许运行,否则运行和导出都会停止。`LogicEditorService::checkDoubleCoils()` 作为单独动作检查重复 M 线圈并定位第二个输出,不混入普通语法检查或运行前阻断。 节点配置使用 `std::variant` 的独立类型。当前条件包括 M 常开/常闭触点、M 上升沿/下降沿触点和 D 与常量比较;输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。新增或恢复指令必须增加独立配置、校验、JSON、执行、编辑和测试分支,不能向通用节点堆叠无关字段。 `SoftwareLogicExecutor` 按 `controlLogics` 和视觉行顺序扫描已启用逻辑,竖线连通的相邻行先作为同一个网格组传播电源,再按视觉行顺序执行输出;前面输出的 M/D 写入在同一扫描周期对后续行可见。执行器只通过寄存器仓库读写 M/D,并仅为边沿触点保留跨扫描输入状态;扫描轨迹按逻辑 ID 分区,避免不同逻辑中的重复节点 ID 相互覆盖。 运行画布不改变网格背景。每格左边界使用 `cellInputPowerValues`,右边界使用 `cellPowerValues`,竖线使用 `verticalConnectionValues`,输出连接使用 `rungValues`,输出符号使用 `nodeValues`;选择和故障覆盖层的优先级高于普通运行轨迹。轨迹调用链始终向运行监控和控制器传递完整 `LogicTraceSnapshot`,只允许最终 `LogicEditorWidget` 根据当前逻辑 ID 执行一次 `forLogic()`,避免重复投影把扁平轨迹清空。 真机模式由 `OnlineLogicMonitorService` 复用同一执行器,但不会把 PLC 仓库直接交给执行器。每次完整 PLC 轮询后,服务把梯形图实际引用的 M/D 从 PLC 缓存复制到独立的临时 `VirtualRegisterRepository`,再执行一轮本地扫描。线圈、MOVE、ADD 和 SUB 只修改本轮临时值,所以前面网络的本地结果仍可供后面网络使用,但任何本地输出都不会写入 PLC;下一轮重新从最新 PLC 缓存开始。 ## 数据源与运行模式 运行界面只访问 `RegisterRepository`。`ActiveRegisterRepository` 根据模式切换实际数据源: ```text 编辑:UI -> Editor Services -> ProjectService -> Project └── ProjectStorage -> JSON 编辑态数据监控:UI -> RegisterMonitorService -> 虚拟 M/D 或 PLC 缓存 └── 离线初始值 VirtualRegisterRepository(进程内) 离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository ^ OfflineSimulationService(SoftwareLogicExecutor) 真机:运行监控弹窗 -> ActiveRegisterRepository -> PlcRegisterRepository(cache) ^ PLC <-> PlcCommunicationService <--------------------┘ PlcRegisterRepository(cache) --只读复制--> OnlineLogicMonitorService -> 临时 VirtualRegisterRepository -> SoftwareLogicExecutor -> 本地推算轨迹 ``` | 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 | | --- | --- | --- | --- | --- | | 编辑态 | 是 | 未连接 PLC 时使用虚拟 M/D;连接并完成首读后数据监控使用 PLC 缓存 | 停止 | 运行态先退出 | | 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 | | 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读,工程通过运行校验 | 离线和真机不能直接互切。进入真机不复制或下发离线值;本地轨迹从最新 PLC 缓存开始推算,只表示 PC 根据缓存计算出的结果,不是 PLC 内部真实轨迹。 自由监控是会话级诊断工具,不属于 `Project`,不写入 JSON。M 监控点固定为位值,D 监控点可选 `Int16`、`Int32`、`Float32` 或 `Float64`。批量添加的步长等于类型占用字数:Int16 为 1,Int32/Float32 为 2,Float64 为 4;类型列显示完整占用范围。完全重复点拒绝,部分重叠点允许添加但提示。读写统一使用连续字块接口;真机 Int16 使用单字写入,Int32、Float32 和 Float64 使用一次多保持寄存器写入,并继续受连接与首读门槛约束。 编辑态数据监控未连接 PLC 时写入虚拟 M/D,并同时更新当前进程内的离线初始值;连接 PLC 并完成首读后,读写直接使用 PLC 缓存,写入等待后续轮询确认且不更新离线初始值。PLC 已连接但尚未完成首读时只显示等待状态并禁止写入。启动离线仿真时,运行仓库先清空,再复制离线初始值;仿真扫描产生的线圈、MOVE、ADD 和 SUB 结果只写入本轮运行仓库。仿真停止或返回编辑态时丢弃本轮输出并恢复初始值。离线初始值和监控地址均不写入工程 JSON。运行态监控继续复用同一 `RegisterMonitorService`,离线写入虚拟仓库,真机写入 PLC 缓存并等待轮询确认。 ## Modbus RTU 通信 `PlcCommunicationService` 使用 `QModbusRtuSerialMaster` 异步连接、轮询和单点写入,UI 线程不等待串口响应。Modbus 地址使用从 `0` 开始的原始地址。 PLC 配置窗口的自动搜索使用独立 `PlcDiscoveryService`,不复用正式轮询主站,避免搜索失败污染连接状态、缓存和首读资格。搜索固定使用界面当前站号,优先当前串口参数,再异步遍历全部可用 PC 串口和项目允许的 60 组波特率、数据位、校验位及停止位组合;每组参数只读 `D0`,正常响应或合法 Modbus 异常响应都能证明端口、站号和串口帧匹配。找到后必须先释放搜索串口,再把参数交给原有正式连接流程;取消或全部失败时不保留搜索连接。 轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。Int32、Float32 和 Float64 的起始地址及占用字数作为连续范围元数据传入通信层;拆分单次最多 120 字的读块时会向前调整边界,保证一个 2 字或 4 字值不会被切到两个请求。监控表最多 64 行,但 PLC 的 256 上限按去重后的实际 16 位 M/D 地址计数,因此 64 个 Double 已占满 256 个 D 地址,若还有 HMI、报警或梯形图地址会在添加监控点前原子拒绝。横线、断路和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再同时应用新地址和多字范围。真机本地轨迹只在全部读块成功完成一轮后推算一次,不在单个读块更新时重复扫描。 PLC 缓存只保存最后一次成功读回值。写请求受理后不乐观修改缓存,界面等待后续轮询确认真实值。真机运行资格属于当前连接代次:必须完成完整首读,旧连接的异步回复不能污染重连后的状态。 通信状态包括 `Disconnected / Connecting / Connected / Recovering / Faulted`。连接失败或物理断线会关闭串口会话并进入 `Disconnected`;超时、协议或读写故障会停止正常轮询、保留最后有效缓存并进入 `Faulted`。所有通信故障都会撤销首读资格;若当时处于真机运行,运行模式服务立即返回编辑态。可恢复超时探测成功后进入 `Recovering` 并重新完整首读,但不会自动重新进入真机运行。 ## 扩展规则 - 新业务先确定所属领域模型和服务用例,再添加 UI;不得从 UI 直接改工程容器或串口状态 - 编辑操作必须在服务层校验并原子提交,失败不能污染撤销历史或留下半成品 - 新持久化字段必须同步领域校验、严格 JSON 往返、缺失/非法字段测试和格式说明;是否升级格式版本必须明确决定 - 新数量边界必须进入 `project_limits.h`、数量边界文档和上下界测试 - 新 HMI 类型优先扩展 `hmi_control_registry`,专用交互留在对应服务和图元 - 不重新引入数据点表、自由线段、UI 直连串口,也不得把真机本地梯形图的临时输出写入 PLC - 自动化测试覆盖领域、服务、JSON 存储和 Fake PLC 通信状态;UI 工作流在交付前人工验收,真实串口、接线和设备 RUN 联动必须现场验证