综合平台编程器项目的远程存储
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

25 KiB

综合平台编程器架构

目标与范围

项目是面向设备交付、调试和验证工程师的单工程、单 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 RTU
  • ui 只提交命令并投影模型、轨迹和错误,不直接读写串口或承载业务校验
  • main.cpp 只组合对象,不实现业务规则

主窗口静态布局由 main_window.ui 管理。UI 协调分为:

组件 职责
ProjectWorkspaceController 工程树、当前页面/逻辑会话选择和编辑对象刷新
PropertyPanelController HMI/梯形图选择、属性表单和编辑服务调用
RuntimePanelController 运行会话、顶层运行监控窗口、运行值刷新和仿真故障反馈
RuntimeMonitorWindow 独立顶层窗口的显示、隐藏、关闭请求和应用退出生命周期
RuntimeMonitorWidget 全应用唯一运行投影,组合可交互 HMI、梯形图轨迹和自由监控
MainWindow 窗口级动作、工程文件操作、控制器组合和模式切换

运行界面统一投影到独立顶层 RuntimeMonitorWindow 中的唯一 RuntimeMonitorWidget。普通编程器中的该窗口是工程师运行监控大屏;带 HMI 工程封装的用户运行版则复用同一投影,只保留 HMI 页面:

  • 进入离线或真机运行后,普通编程器弹出并最大化运行监控窗口;主编程器窗口保留在后面且不改变布局,运行期间由同一份 ModePolicy 禁用编辑入口
  • 普通编程器运行监控窗口顶部提供“离线仿真/真机运行”模式选择;用户运行版隐藏模式、配置和诊断控件,启动后自动连接真实 PLC
  • 主窗口不保留运行监控页签;编辑态主窗口提供与“HMI 页面”“控制逻辑”并列的“数据监控”编辑页签,运行态显示弹窗中的运行监控投影
  • 普通编程器离线时显示可交互 HMI、软件梯形图轨迹和自由监控;真机时同时显示可交互 HMI、自由监控和根据 PLC 缓存计算的“本地推算轨迹”;用户运行版只显示可交互 HMI
  • HMI 页面跳转、按钮和数值输入都通过同一份 HmiNavigationService 和活动寄存器仓库完成;离线写虚拟 M/D,真机写 PLC,仿真停止或通信不可用时禁止写入
  • 运行监控只能通过界面内的返回编辑按钮请求 MainWindow -> RuntimeModeService 返回编辑态;运行期间系统窗口关闭路径被忽略,离线时先停止软件扫描,真机时保留 PLC 连接但撤销运行会话;应用退出时允许顶层窗口真正关闭

工程模型与持久化

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 选择 Int16Int32Float32Float64;底层仓库始终保存原始 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。带工程封装的用户运行版不读取这份完整配置,而是使用下方独立的最小串口配置。

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

用户运行程序导出

编程器通过“导出用户运行程序”先复制当前工程的 HMI 页面、报警和页面导航所需数据,明确移除控制逻辑和地址备注,再原子写入临时 JSON。随后将当前已编译的编程器 exe 复制为模板,把裁剪后的 JSON 追加到 exe 尾部的固定封装格式中,并复制 Qt 运行库、平台插件和 config/runtime.ini。封装格式包含魔数、版本、数据长度和 SHA-256,写入使用临时文件后原子改名。带有有效工程封装的 exe 启动时读取自身尾部工程数据,隐藏编辑器,只显示 HMI 页面,并从 config/runtime.ini 自动连接真实 PLC;运行版不提供离线仿真、梯形图、本地逻辑推算、自由监控、串口配置或模式切换控件。普通 integrated_platform.exe 没有工程封装时按编程器入口启动。

config/runtime.ini 只允许 [PlcDefaults] 下的六个串口字段:PortNameServerAddressBaudRateDataBitsParityStopBits。文件不存在时自动创建代码默认值;字段错误时整份回退到默认串口参数。运行版的 HMI 读写直接使用 PLC 轮询缓存,运行版不启动离线执行器,也不执行本地梯形图扫描。串口被拔出后运行版按固定间隔重新打开串口;PLC 端口无响应触发通信超时恢复,恢复失败时同样重新建立连接并重新完成首读。

scripts/package_qt_app.ps1 只用于维护者构建编程器发布包和复制 Qt 运行库,不参与工程导出。编程器发布包包含自身 exe 及 Qt 运行库后可以移动到任意目录,导出时不再依赖 PowerShell、qmake、MinGW 或项目源码目录。

结构化梯形图

连续梯形图不再使用串并联表达式树。LadderRung 是按视觉顺序排列的一行,前 10 列固定保存 LadderCell,每格独立表示 GapWire 或条件 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 + 语法检查”。服务按列计算从左母线向右的结构可达性和从所有输出向左的反向可达性;对于已经存在完整输出路径的网络,只保留能够参与完整路径的 WireVerticalConnection,没有输出的残线全部清除。规整不删除条件节点、输出或数据指令;若某个输出网络本身断路,则该网络不参与自动清理,保留用户现场并返回控制逻辑、网络、视觉行和第 11 列输出槽位置。一次检查对多条横竖线的修改只提交一条梯形图撤销记录。

RuntimeModeService 在普通编程器的离线启动和完成 PLC 首读后的真机启动前调用 LogicEditorService::checkEnabledSyntax();HMI 专用运行版没有控制逻辑,不执行本地逻辑语法检查和扫描。普通编程器用户运行程序导出只校验裁剪后的 HMI 工程,规整完成后,ControlLogic::validateForRunning() 继续使用和执行器相同的逐列边界与竖线连通关系做最终静态可达性检查。没有输出的自动追加空行可以继续保留;任何带输出的行都必须存在从左母线到第 10 列边界的结构路径。中间 Gap 只有在其他并联支路能够绕过时才允许运行,否则普通编程器运行会停止。LogicEditorService::checkDoubleCoils() 作为单独动作检查重复 M 线圈并定位第二个输出,不混入普通语法检查或运行前阻断。

节点配置使用 std::variant 的独立类型。当前条件包括 M 常开/常闭触点、M 上升沿/下降沿触点和 D 与常量比较;输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。新增或恢复指令必须增加独立配置、校验、JSON、执行、编辑和测试分支,不能向通用节点堆叠无关字段。

SoftwareLogicExecutorcontrolLogics 和视觉行顺序扫描已启用逻辑,竖线连通的相邻行先作为同一个网格组传播电源,再按视觉行顺序执行输出;前面输出的 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 缓存开始。

数据源与运行模式

运行界面只访问 RegisterRepositoryActiveRegisterRepository 根据模式切换实际数据源:

编辑: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 -> 本地推算轨迹
模式 允许工程编辑 寄存器来源 软件逻辑执行器 进入条件
编辑态 数据监控由用户选择虚拟 M/D 或 PLC 缓存;连接 PLC 不自动切换 停止 运行态先退出
离线运行 虚拟 M/D 运行 工程通过运行校验
真机运行 HMI 使用 PLC 读回缓存;轨迹使用缓存副本 本地只读推算 PLC 已连接并完成本次首读,工程通过运行校验

离线和真机不能直接互切。进入真机不复制或下发离线值;本地轨迹从最新 PLC 缓存开始推算,只表示 PC 根据缓存计算出的结果,不是 PLC 内部真实轨迹。

自由监控是会话级诊断工具,不属于 Project,不写入 JSON。M 监控点固定为位值,D 监控点可选 Int16Int32Float32Float64。批量添加的步长等于类型占用字数:Int16 为 1,Int32/Float32 为 2,Float64 为 4;类型列显示完整占用范围。完全重复点拒绝,部分重叠点允许添加但提示。读写统一使用连续字块接口;真机 Int16 使用单字写入,Int32、Float32 和 Float64 使用一次多保持寄存器写入,并继续受连接与首读门槛约束。

编辑态数据监控默认选择离线 M/D,用户可在界面明确切换离线 M/D 和真机 PLC M/D;PLC 连接成功不会自动切换。离线写入虚拟 M/D 并同时更新当前进程内的离线初始值;真机只有在 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 并重新完整首读,但不会自动重新进入真机运行。

用户运行版按故障层级使用唯一恢复路径:本地 USB 串口消失进入 Disconnected 后,每 3 秒尝试一次完整串口重连;本地串口仍打开但 PLC 无响应或通信超时进入 Faulted 后,只由通信服务保留串口并每 2 秒发送轻量读取探测,界面层不得再次关闭和重开串口。一次故障周期只提示一次,错误分类或文字变化不产生新弹窗;只有状态恢复为 Connected 且重新完成全部轮询块首读后,才结束本轮故障并允许下一次故障提示。运行版恢复期间保持 HMI 页面和最后有效显示,但禁止写入。

扩展规则

  • 新业务先确定所属领域模型和服务用例,再添加 UI;不得从 UI 直接改工程容器或串口状态
  • 编辑操作必须在服务层校验并原子提交,失败不能污染撤销历史或留下半成品
  • 新持久化字段必须同步领域校验、严格 JSON 往返、缺失/非法字段测试和格式说明;是否升级格式版本必须明确决定
  • 新数量边界必须进入 project_limits.h、数量边界文档和上下界测试
  • 新 HMI 类型优先扩展 hmi_control_registry,专用交互留在对应服务和图元
  • 不重新引入数据点表、自由线段、UI 直连串口,也不得把真机本地梯形图的临时输出写入 PLC
  • 自动化测试覆盖领域、服务、JSON 存储和 Fake PLC 通信状态;UI 工作流在交付前人工验收,真实串口、接线和设备 RUN 联动必须现场验证