项目是面向设备交付、调试和验证工程师的单工程、单 PLC、多 HMI 页面和多控制逻辑 Qt 桌面编程器,提供 HMI 编辑、结构化梯形图、离线仿真和 Modbus RTU 真机联机。
本项目不生成、编译或下载 PLC 程序,不读取 PLC 内部程序与网络轨迹,不实现完整 XDPPro 指令集,也不支持任意自由画线。当前梯形图模型不包含 T/C 触点、TON 或 CTU/CTD。真机运行时 PLC 内部程序仍是唯一设备控制源;HMI 和自由监控可以正常读写工程引用的 M/D,本地梯形图只读取 PLC 缓存并推算轨迹,不把线圈或数据指令结果写回 PLC。
app/src/
├── domain/ 无 Qt 依赖的模型、值对象、校验和仓库接口
├── services/ 编辑、工程、仿真、运行模式和通信契约
├── infrastructure/ JSON、PLC 缓存、Qt Modbus RTU 实现
├── ui/ Qt Widgets 视图、对话框和界面控制器
└── main.cpp 对象创建与依赖注入
生产代码依赖方向固定为:
UI -> Services -> Domain
Infrastructure -> Services contracts + Domain
main.cpp -> UI + Services + Infrastructure
domain 不依赖 Qt、文件系统或串口services 执行业务用例和原子编辑,通过接口使用工程存储与 PLC 通信infrastructure 实现应用配置与 JSON 加载、PLC 缓存和异步 Modbus RTUui 只提交命令并投影模型、轨迹和错误,不直接读写串口或承载业务校验main.cpp 只组合对象,不实现业务规则主窗口静态布局由 main_window.ui 管理。UI 协调分为:
| 组件 | 职责 |
|---|---|
ProjectWorkspaceController |
工程树、当前页面/逻辑会话选择和编辑对象刷新 |
PropertyPanelController |
HMI/梯形图选择、属性表单和编辑服务调用 |
RuntimePanelController |
运行会话、顶层运行监控窗口、运行值刷新和仿真故障反馈 |
RuntimeMonitorWindow |
独立顶层窗口的显示、隐藏、关闭请求和应用退出生命周期 |
RuntimeMonitorWidget |
全应用唯一运行投影,组合可交互 HMI、梯形图轨迹和自由监控 |
MainWindow |
窗口级动作、工程文件操作、控制器组合和模式切换 |
运行界面统一投影到独立顶层 RuntimeMonitorWindow 中的唯一 RuntimeMonitorWidget。该窗口是编程器的工程师运行监控大屏,不是旧的纯 HMI 操作员窗口:
ModePolicy 禁用编辑入口HmiNavigationService 和活动寄存器仓库完成;离线写虚拟 M/D,真机写 PLC,仿真停止或通信不可用时禁止写入MainWindow -> RuntimeModeService 返回编辑态;运行期间系统窗口关闭路径被忽略,离线时先停止软件扫描,真机时保留 PLC 连接但撤销运行会话;应用退出时允许顶层窗口真正关闭Project
├── metadata
├── hmiPages[] + initialHmiPageId
├── alarmDefinitions[]
├── registerComments[]
└── controlLogics[]
└── rungs[]
├── cells[10]
└── output
└── verticalConnections[]
Gap/Wire/Node 是独立格子对象,竖线连接相邻行列边界,插入/删除行会维护连接关系,删线即可拆分网络hmi_control_registry 统一描述RegisterDataType 选择 Int16 或 Float32;底层仓库始终保存原始 16 位 D 字,Float32Codec 负责低字/高字组合dataPoints 表registerComments 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询工程 JSON 格式固定为严格的 2.0。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。旧 1.0 文件直接拒绝,不做迁移或双写。字段定义见 docs/工程格式说明.md。
校验分两级:
validate() 保证可保存结构合法,允许未绑定控件、待配置节点和未完成网络作为编辑草稿validateForRunning() 检查运行所需绑定和已启用逻辑;禁用草稿不阻止运行数量的绝对硬上限和默认值集中在 domain/project_limits.h。启动时可由应用配置收紧的数量使用同一份 ProjectLimitSettings,由 main.cpp 注入领域校验、编辑服务、JSON 存储和 UI;任何一层都不能另存一套运行上限。最终业务口径见 docs/用户使用/数量边界确认方案.md。
ApplicationSettingsLoader 在 Qt 应用创建后、其他业务对象创建前读取可执行文件目录下的 config/application.ini。配置只读取一次,不热更新,也不写入工程 JSON。
config/application.ini
|
ApplicationSettingsLoader
|
ApplicationSettings(启动后只读)
|-- ProjectLimitSettings -> Domain / Editor Services / JsonProjectStorage / UI
|-- HmiDefaultSettings -> HmiEditorService / PropertyPanelController
`-- PlcDefaults -> MainWindow -> PlcConnectionDialog
字段、范围和用户操作方式见 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 格后进入输出槽,输出后越过当前竖线连通分组;末尾输出会连同下一空行一起提交和撤销。空行直接写输出时,输出事务把 10 个条件格全部补为 Wire;已有条件时只补最右有效内容到输出槽之间的尾部 Gap,不会跨过中间的明确断点。画布内嵌指令、工具栏动作和 QAction 快捷键只消费该结果,不在 UI 重复计算推进规则。
网络不是额外的表达式对象,而是横向导通格、条件节点、输出槽以及竖线连通分量共同决定的电气关系。执行器按列传播电源,在每个列边界先合并竖线连通分量,再计算各行网格和输出;删除竖线会自然拆分网络。节点配置使用 std::variant 的独立类型,当前条件包括 M 常开/常闭、M 上升沿/下降沿和 D 比较,输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。
控制逻辑组可以暂时没有行。新增行会生成 10 个稳定网格 ID;鼠标横向拖动设置连续 Wire,竖向拖动创建多个相邻 VerticalConnection。插入行时原有竖线被拆成上下两段以保持连续;删除行时只有上下两段在同列都存在才合并,单边连接直接移除。所有编辑由 LogicEditorService 原子提交并进入撤销历史。
网络运行语义:
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 根据模式切换实际数据源:
编辑:UI -> Editor Services -> ProjectService -> Project
└── ProjectStorage -> JSON
编辑态数据监控:UI -> RegisterMonitorService -> VirtualRegisterRepository
└── 离线初始值 VirtualRegisterRepository(进程内)
离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository
^
OfflineSimulationService(SoftwareLogicExecutor)
真机:运行监控弹窗 -> ActiveRegisterRepository -> PlcRegisterRepository(cache)
^
PLC <-> PlcCommunicationService <--------------------┘
PlcRegisterRepository(cache) --只读复制--> OnlineLogicMonitorService
-> 临时 VirtualRegisterRepository
-> SoftwareLogicExecutor -> 本地推算轨迹
| 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 |
|---|---|---|---|---|
| 编辑态 | 是 | 数据监控使用虚拟 M/D 初始值;其他编辑操作不启用运行数据源 | 停止 | 运行态先退出 |
| 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 |
| 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读,工程通过运行校验 |
离线和真机不能直接互切。进入真机不复制或下发离线值;本地轨迹从最新 PLC 缓存开始推算,只表示 PC 根据缓存计算出的结果,不是 PLC 内部真实轨迹。
自由监控是会话级诊断工具,不属于 Project,不写入 JSON。每个监控点可选 Int16 或 Float32,Float32 批量添加按 2 个 D 偏移并显示实际占用范围;完全重复点拒绝,部分重叠点允许添加但提示。它通过活动仓库读取 M/D 或成对读取 Float32,并通过单字或双字请求写入;真机写入仍必须经过连接与首读门槛。
编辑态数据监控写入虚拟 M/D 时,同时更新当前进程内的离线初始值仓库。启动离线仿真时,运行仓库先清空,再复制这份初始值;仿真扫描产生的线圈、MOVE、ADD 和 SUB 结果只写入本轮运行仓库。仿真停止或返回编辑态时丢弃本轮输出并恢复初始值。离线初始值和监控地址均不写入工程 JSON。运行态监控继续复用同一 RegisterMonitorService,离线写入虚拟仓库,真机写入 PLC 缓存并等待轮询确认。
PlcCommunicationService 使用 QModbusRtuSerialMaster 异步连接、轮询和单点写入,UI 线程不等待串口响应。Modbus 地址使用从 0 开始的原始地址。
PLC 配置窗口的自动搜索使用独立 PlcDiscoveryService,不复用正式轮询主站,避免搜索失败污染连接状态、缓存和首读资格。搜索固定使用界面当前站号,优先当前串口参数,再异步遍历全部可用 PC 串口和项目允许的 60 组波特率、数据位、校验位及停止位组合;每组参数只读 D0,正常响应或合法 Modbus 异常响应都能证明端口、站号和串口帧匹配。找到后必须先释放搜索串口,再把参数交给原有正式连接流程;取消或全部失败时不保留搜索连接。
轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。Float32 的起始地址同时作为成对读取元数据传入通信层;拆分 120 字上限时会调整边界,保证两个连续保持寄存器不会分到不同请求。横线、断路和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再应用新集合。真机本地轨迹只在全部读块成功完成一轮后推算一次,不在单个读块更新时重复扫描。
PLC 缓存只保存最后一次成功读回值。写请求受理后不乐观修改缓存,界面等待后续轮询确认真实值。真机运行资格属于当前连接代次:必须完成完整首读,旧连接的异步回复不能污染重连后的状态。
通信状态包括 Disconnected / Connecting / Connected / Recovering / Faulted。连接失败或物理断线会关闭串口会话并进入 Disconnected;超时、协议或读写故障会停止正常轮询、保留最后有效缓存并进入 Faulted。所有通信故障都会撤销首读资格;若当时处于真机运行,运行模式服务立即返回编辑态。可恢复超时探测成功后进入 Recovering 并重新完整首读,但不会自动重新进入真机运行。
project_limits.h、数量边界文档和上下界测试hmi_control_registry,专用交互留在对应服务和图元