Преглед изворни кода

docs: 更新连续梯形图与语法检查说明

main
suyu пре 3 недеља
родитељ
комит
b8e10af580
10 измењених фајлова са 255 додато и 154 уклоњено
  1. +1
    -0
      .gitignore
  2. +1
    -0
      AGENTS.md
  3. +81
    -100
      docs/ai/handoff.md
  4. +23
    -20
      docs/architecture.md
  5. +22
    -20
      docs/工程格式说明.md
  6. +5
    -5
      docs/开发顺序.md
  7. +8
    -7
      docs/测试约定.md
  8. +2
    -2
      docs/用户使用/数量边界确认方案.md
  9. +63
    -0
      docs/用户使用/连续梯形图网格说明.md
  10. +49
    -0
      docs/用户使用/鼠标画线与删线说明.md

+ 1
- 0
.gitignore Прегледај датотеку

@@ -69,3 +69,4 @@ Desktop.ini
/代码
/other_version
/app_copy
当前窗口需求.md

+ 1
- 0
AGENTS.md Прегледај датотеку

@@ -28,6 +28,7 @@
- 应用配置文件:`docs/用户使用/应用配置说明.md`
- 用户运行程序导出:`docs/用户使用/用户运行程序导出说明.md`
- 数据监控与离线初始值:`docs/用户使用/数据监控与离线初始值说明.md`
- 鼠标画线与删线:`docs/用户使用/鼠标画线与删线说明.md`
- 工程 JSON 格式:`docs/工程格式说明.md`
- 开发阶段顺序:`docs/开发顺序.md`(阶段参考,当前状态以 handoff 为准)
- 测试约定:`docs/测试约定.md`


+ 81
- 100
docs/ai/handoff.md Прегледај датотеку

@@ -1,108 +1,89 @@
# 当前开发交接

> 更新日期:2026-08-26。本文件只保留当前工作区状态、最近验证和下一步,不记录历史开发流水账;稳定架构和业务细节分别以 `docs/architecture.md` 及对应专题文档为准
> 更新日期:2026-08-26。本文件只保留当前工作区、本轮验证和下一步

## 当前状态

- 分支:`main`
- 核心闭环可用:HMI 编辑、结构化梯形图、离线仿真、工程 JSON、运行监控、自由监控和 Modbus RTU 真机读写均已实现
- D 区 Float32/REAL 读写已接入:HMI 和自由监控可选 Int16/Float32,底层仍保存原始 16 位字,Float32 使用低字/高字连续双 D 编解码
- 真机本地梯形图只读推算已实现:HMI/自由监控继续读写 PLC,本地梯形图只读 PLC 缓存并在临时仓库推算轨迹
- 编辑态 M/D 数据监控已接入:监控面板可在编辑态修改虚拟 M/D,并将值作为下一次离线仿真的进程内初始值;运行输出不会污染初始值
- 数据监控在编辑态作为与 HMI 页面、控制逻辑并列的第三个编辑页签显示,运行监控窗口继续复用同一监控服务;离线写虚拟仓库,真机写 PLC 缓存
- 应用外部配置业务已经完成,固定读取可执行文件目录下的 `config/application.ini`
- 配置支持 6 个体验型数量上限、2 个新建 HMI 页面默认尺寸和 6 个 PLC 串口默认参数
- 配置启动时只读取一次,不热更新,不覆盖已有文件,不回写 PLC 配置窗口中的临时修改
- 当前没有代码阻塞,命令语输入和用户运行程序导出功能已接入,Release 功能测试和 Release 主程序构建均已通过

## 工作区状态

- 当前工作区干净;用户运行程序导出代码已提交为 `e8f7cc9 feat: 改为工程数据封装导出`,旧运行版兼容入口清理已提交为 `7a827a2 fix: 移除旧运行版兼容入口`,配套文档提交为 `e8f5e81` 和 `d324d23`
- 工程树的新建页面、新建控制逻辑及重命名入口已统一禁止提交空名称或纯空白名称,确认按钮会随输入有效性即时启停
- 新增 `ApplicationSettings`、严格 UTF-8 INI 加载器及独立 `application_settings_tests`
- 新增用户文档 `docs/用户使用/应用配置说明.md`,并同步架构、数量边界、测试约定和 `AGENTS.md` 索引
- `build/` 只保存本地构建和测试输出,不纳入 Git
- 未创建或修改发布包内的默认 INI;正式程序首次运行时由加载器按实际可执行文件目录创建
- 新增 `LogicCommandService`,支持首期 14 类命令解析、M/D 地址校验、指令补全数据和原子转换
- 梯形图条件区、横线、Gap 和输出槽支持双击命令输入;成功后复用现有表达式树和图元刷新,失败保留输入并标红
- 连续 `LD` 会创建下一个网络;连续 `AND/ANI` 追加当前网络;连续 `OR/ORI` 将累计条件整体并联且不额外占用主干列
- 命令生成节点直接标记为已配置,不新增命令文本持久化格式;运行、JSON 保存和真机本地推算继续使用结构化梯形图
- 新增 `docs/用户使用/命令语输入说明.md` 和命令解析、连续输入、横线/Gap 替换及原子失败测试
- 命令输入框失焦会自动结束未提交输入,场景刷新会清理旧选择;表达式渲染跳过零长度连接线
- 删除后保留的 `Gap` 会被横线和条件追加优先复用,连续横线插入会消耗目标后的 Gap,不会错误创建新网络
- 已有条件节点和输出节点支持双击进入命令输入;条件节点可用 `LD/LDI/LDP/LDF` 替换,输出节点可用 `OUT/SET/RST/MOV/ADD/SUB` 替换,类型不匹配时原子拒绝
- 双击已有节点时命令输入框会回填当前指令和 M/D 地址;补全弹窗改为左对齐的“指令 / 操作数 / 说明”三列表格,并扩大宽高,输入框在视口边缘自动避让
- 命令补全增加 `LD=`、`LD<>`、`LD<`、`LD<=`、`LD>`、`LD>=` D 值比较指令,格式为比较指令 + D 地址 + 有符号常量
- `MOV/ADD/SUB` 命令输入保留指令对话框允许的常量操作数;补全过滤只按第一段指令名匹配,输入空格后的 M/D 操作数时仍保留指令提示
- 双击命令输入只打开空输入框,不再自动弹出补全列表;用户开始输入指令名后才显示补全,并移除未实现的 `TON/CTU` 提示项
- 梯形图删除支持框选网络批量删除;混合选中时忽略断路占位,不再弹出“已经是断路”的误导提示
- 梯形图画布统一支持从网络内部或外部开始鼠标拖拽框选,Ctrl 继续追加选择
- 新增“文件 -> 导出用户运行程序”:导出前执行运行校验,将当前工程写入临时 JSON,复制已编译的编程器 exe 并把工程数据封装到 exe 尾部,再复制 Qt DLL 与平台插件到用户选择的目标目录
- 新增 `RuntimeProjectBundleService`,封装格式包含固定魔数、版本、长度和 SHA-256;导出使用本地进度窗口和原子文件写入,不再在导出时调用 PowerShell、qmake 或 MinGW
- `scripts/package_qt_app.ps1` 只负责维护者构建普通编程器发布包;发布包携带 Qt 运行库后可移动到任意目录导出
- 导出入口改为选择目标父目录并输入文件夹名称,最终生成“文件夹/同名 exe + 运行库”的完整目录,覆盖已有目录前需要确认
- 用户运行版启动后隐藏编程器编辑器,直接进入离线仿真;运行监控顶部提供离线/真机切换、PLC 配置、断开和退出程序操作
- 修复用户运行版未完成 PLC 首次读取时切换真机导致运行监控窗口隐藏、应用无可见窗口而退出的问题;现在保持离线运行并在运行监控窗口提示先配置 PLC 并完成首次读取

## 本轮变更结论

- 默认配置文件包含 `[Config]`、`[ProjectLimits]`、`[HmiDefaults]` 和 `[PlcDefaults]` 四个分区,当前版本固定为 `1`
- 文件不存在时自动创建目录和默认文件,本次使用代码默认值;创建或读取失败时默认值启动,底部输出详细原因并弹一次警告
- 缺失普通字段使用默认值,未知字段忽略;重复字段、版本错误、非法 UTF-8、非整数、越界或不支持的串口枚举值会使整份配置回退
- 领域聚合校验、编辑服务、JSON 保存加载、运行前校验和 UI 使用同一份启动期 `ProjectLimitSettings`
- 用户调低上限后,超限工程在局部对象中解析失败,不替换当前工程、不加载部分内容,也不修改原 JSON
- 全工程 HMI 控件 2048、全工程网络 2048、M/D 地址、16 位数值、梯形图结构、表达式、Modbus 和文件容量继续使用代码硬限制
- `DefaultPageWidth/DefaultPageHeight` 只影响新建页面;已有页面继续使用工程 JSON 中的尺寸
- PLC 默认值初始化主窗口连接配置;不存在的默认 COM 端口仍按现有规则过滤,不会制造无效端口项
- 页面和控制逻辑名称在界面提交前统一去除首尾空白;服务层继续负责空名称、长度和唯一性校验
- 真机每次完整 PLC 轮询成功后,复制梯形图引用的 M/D 到独立临时虚拟仓库,再复用离线扫描规则计算轨迹
- 本地线圈、MOVE、ADD、SUB 只修改本轮临时仓库;前面网络的临时结果可供本轮后续网络使用,下一轮重新从 PLC 最新缓存开始
- 真机轨迹明确标记为“PLC 缓存 · 本地推算轨迹”,不读取、生成、下载或修改 PLC 内部程序和内部轨迹
- 真机梯形图区显示只读边界提示:本地输出不写入 PLC 程序或 M/D,同时说明 HMI 和自由监控仍可写 PLC
- 本地推算失败或 PLC 断线/通信故障时停止推算并退出真机运行;推算故障详情在退出后保留,下一次启动重新清理
- Float32 工程 JSON 固定保存 `dataType: "int16"` 或 `"float32"`;缺少字段直接拒绝加载,不增加旧结构兼容分支
- 离线仿真启动前复制编辑态确认的初始 M/D 快照,停止或返回编辑态时恢复初始快照;初始值只存在进程内,不写工程 JSON
- 普通梯形图比较、MOVE、ADD、SUB 继续按 Int16;普通整数指令可以读取 Float32 占用字,但不能写入其任一 D
- 自由监控完全重复点拒绝,部分重叠允许但提示;Float32 批量地址步长为 2,并显示 D 起始~结束范围
- Float32 写入使用一次写两个连续保持寄存器,真机缓存等待轮询读回;Float32 轮询分块不会拆开两个连续字
- 梯形图使用连续网格模型:每行固定 10 个 `LadderCell`,输出槽独立保存,竖线使用相邻行之间的 `VerticalConnection`
- 工程 JSON 固定为严格 `2.0`;旧 `1.0` 工程直接拒绝,不实现兼容迁移
- 软件逻辑执行器按列传播电源并处理竖线连通分量;离线仿真和真机本地推算的边界保持不变
- `当前窗口需求.md` 的第 1~6 项均已完成:连续网格渲染、统一布局、批量选择删除、编辑后光标前进、内嵌连续输入和逐段运行轨迹均已落地
- 本轮已修复运行轨迹快照重复投影、空行直接输出不补横线、断路输出仍可进入运行三项回归问题
- 梯形图复制粘贴已改为统一片段模型:单横线不再丢失或误复制整行,普通对象片段和明确整行片段使用不同选择语义
- 信捷式语法检查已经落地:`Ctrl+G` 会自动规整当前梯形图的无效横竖线,断路输出保留现场并定位到第 11 列;离线、真机和导出前检查全部已启用逻辑

## 画布渲染与布局

- `LogicEditorWidget` 已将网格、梯形图内容和选择覆盖层拆开,选择框统一位于 `z=100`,不再被相邻网格覆盖右边或下边
- 网格层按整行一次填充统一画布底色,触点、横线、`Gap` 和输出槽不再使用不同语义底色
- 条件区按常开、常闭、上升沿、下降沿和比较块绘制;输出区按普通/SET/RST 线圈及 MOV/ADD/SUB 指令块绘制,数据块显示完整操作数
- 输出区位于条件网格和右母线之间;左右母线使用同一画笔,并严格止于第一行顶部和最后一行底部
- 画布先生成统一 `RowLayout`;网络分组由相邻行之间是否存在竖线动态推导,绘制和命中测试共同使用布局表
- 左侧只显示三位视觉行号;每个网络首行上方预留独立注释带并使用绿色文字,支路行注释不重复显示

## 选择与删除

- 选择模式支持左键橡皮筋框选,触点、横线、输出块和竖线可跨行混选
- 普通拖动替换选择,`Ctrl+拖动`追加选择,`Ctrl+单击`切换单个对象;空格只作为编辑光标,不进入删除请求
- `LogicSelectionDeleteRequest` 将条件/横线网格、输出行和竖线 ID 一次提交给 `LogicEditorService::deleteSelection`
- 服务会先校验完整请求,再一次清空网格、输出和竖线;结构校验失败会恢复模型及工程修改状态
- 一次批量删除只生成一条撤销记录,单次撤销/重做恢复或重删全部对象
- 普通 `Delete` 删除选择集,整行删除快捷键为 `Shift+Delete`

## 复制和粘贴

- `LogicClipboardFragment` 统一保存网格对象片段或整行片段,主窗口不再分别维护条件、输出和行三套剪贴板
- 单击或框选横线、条件、输出和竖线时保存相对行列;未选位置透明,普通复制不把 `Gap` 作为覆盖内容
- 当前光标行和明确整行选择已经拆开;只有点击左侧行号才复制整行,多行整行选择要求视觉连续
- 整行片段保存完整 10 格、输出、网络注释和选中行内部的竖线,粘贴到当前行之后
- 单输出可明确替换输出槽;混合输出冲突、节点冲突、越过第 10 列、跨行不足、末行竖线和行数上限都会在修改前整次拒绝
- 所有新节点、行和竖线重新分配 ID;一次粘贴只生成一条撤销记录,失败不改变模型、修改状态或历史,成功后新对象保持选中

## 连续输入与光标前进

- `LogicEditorService` 统一提供“应用指令并返回下一光标”的编辑结果,工具栏、快捷键和命令语输入共享同一套业务规则
- 空逻辑上插入横线或触点时,服务在一个事务内创建首行并写入第 1 格;成功后逐格右移,第 10 格完成后进入输出槽
- 输出成功后跳到当前网络分组后面的第一个网格;当前分组已经位于末尾时,输出和追加完整空行在一个事务内完成,只生成一条撤销记录
- 完全空行直接写输出时自动生成 10 格 `Wire`;已有内容时只补最右有效内容后的尾部 `Gap`,中间的明确断点保持不变
- 输出事务会在修改前检查行数等边界;追加失败时不会留下已写输出、半行数据或额外撤销记录
- 画布内嵌 `QLineEdit` 和指令补全已恢复;双击网格或输出槽可连续输入,`Enter` 提交、`Escape` 取消,输入错误时保留编辑框并显示错误状态
- 连续输入提交条件后右移,走完第 10 列进入输出槽,输出确认后继续停在下一网络首格;滚动和窗口尺寸变化时输入框跟随目标位置

## 运行轨迹

- 运行态不再使用整格绿色背景;横线、触点左端、触点符号及右端、比较块输入输出线、输出连接线、输出符号和竖线分别读取对应轨迹值着色
- 未导通线路统一使用普通深色,导通线路统一使用监控绿色,故障红色优先;选择覆盖层继续位于最上层
- 竖线只在 `verticalConnectionValues` 明确导通时变绿,缺少轨迹值时保持深色;左右母线使用同一普通颜色
- 控制器和运行监控传递完整 `LogicTraceSnapshot`,只有最终画布按当前逻辑 ID 投影一次;不再因连续调用 `forLogic()` 清空轨迹
- 离线执行器回归覆盖正反转启动、自锁、互锁和停止状态,并校验触点左右端及右侧竖线的真实电源传播

## 语法检查与运行前校验

- `LogicEditorService::checkSyntax()` 同时执行线路规整和语法检查;只删除 `Wire` 和 `VerticalConnection`,不会删除触点、指令或输出
- 已有完整输出路径的网络只保留参与左母线到输出完整路径的线路;没有输出的悬空线路全部删除,一次规整只生成一条撤销记录
- 输出网络已经断路时保留整个网络现场,返回控制逻辑、网络、视觉行、第 11 列输出槽以及实际断开列;未配置指令返回自身行列
- 主窗口输出栏保存结构化错误位置,双击错误可切换控制逻辑、选中对应网格或输出槽并滚动到可见区域
- `ControlLogic::validateForRunning()` 按列传播结构可达性,并在每个边界合并 `VerticalConnection` 支路
- 带输出的行必须存在一条从左母线到输出槽的完整路径;不可达时错误包含控制逻辑、视觉行和断开列
- 没有输出的自动追加空行允许保留;并联网络中的单条 `Gap` 断路支路也允许保留,只要输出仍能从其他支路到达
- 离线入口、完成 PLC 首读后的真机入口和用户运行程序导出先共用服务层规整和语法检查,再执行领域运行校验;真机未首读时仍优先返回首读未完成
- “双线圈检查”是单独菜单动作,定位第二个重复 M 线圈输出;不混入普通语法检查,也不会在运行前自动阻断

## 最近验证

- 2026-08-26 将数据监控从停靠面板迁移为第三个编辑页签;Release 主程序重新执行 qmake 和 `mingw32-make -j2` 链接通过,Release All 套件和 `git diff --check` 通过
- 2026-08-25 执行 `run_qt_tests.ps1 -Configuration Release -Suite All`,12 个 Release 功能测试目标和性能测试全部通过
- 2026-08-25 执行 `run_qt_tests.ps1 -Configuration Release -Suite Functional`,包含命令输入测试的 12 个功能测试目标全部通过
- 2026-08-25 重新执行 qmake 和 `mingw32-make -j2`,Release 主程序 `build/release/release/integrated_platform.exe` 链接通过
- 2026-08-26 修复用户运行版未配置 PLC 切换真机的窗口生命周期问题,Release 主程序 qmake 和 `mingw32-make -j2` 编译通过,`git diff --check` 通过
- 2026-08-26 使用 `json/motor_forward_reverse.json` 重新生成 `build/package/motor_forward_reverse_fixed`,运行版启动后进程保持运行,确认补丁已编入用户运行版入口
- 2026-08-26 新增用户运行程序 exe 尾部工程封装,Release 主程序、封装回归测试和 Functional 套件通过;导出链路不再依赖 `package_qt_app.ps1`
- 2026-08-26 Release 主程序在独立 `build/release-final` 目录重新 qmake 和 `mingw32-make -j2` 链接通过;发布包复制到项目目录外后,工程封装 exe 实际启动并保持运行,确认不依赖源码目录
- 2026-08-26 删除旧命令行、旁路工程 JSON 和 qrc 资源入口;普通发布包脚本只构建编程器,新的发布包和工程封装 exe 均重新构建、启动验证通过
- 2026-08-25 修复用户运行程序导出资源错配:使用合法 `1.0` 工程夹具验证 qrc 指向 JSON、运行版 exe 启动不再出现 `illegal value`;Release 主程序构建、All 测试及普通/运行版打包均通过
- 2026-08-25 新增数据监控编辑页和离线初始值生命周期;Release Functional 测试、主程序 qmake/`mingw32-make -j2` 和新增快照回归通过
- 2026-08-25 修复命令输入失焦、零长度连线黑点、删除后 Gap 复用和连续横线插入;Release Functional 测试及主程序构建通过
- 2026-08-25 修复双击空网格时补全列表提前弹出;现在仅在用户开始输入后显示补全,并移除未实现的 TON/CTU 提示;Release Functional 测试和主程序构建通过
- 2026-08-25 修复已有节点双击无法进入命令输入;新增条件/输出节点替换回归测试,Release Functional 测试、主程序构建和 `git diff --check` 通过
- 2026-08-25 修复梯形图整网删除误报断路、网络内部无法启动框选;新增批量网络删除原子性测试,Release Functional 测试、主程序构建和 `git diff --check` 通过
- 2026-08-25 新增 Float32 编解码、HMI 双字读写、D3999/D4000 边界、HMI 重叠、自由监控步长/重叠/读写和 Float32 轮询元数据测试;All 套件全部通过
- `application_settings_tests` 覆盖首次创建与默认内容、完整加载、缺失和未知字段、重复字段、越界、非整数、版本不兼容、非法 UTF-8 及创建失败回退
- 新增离线初始值和监控写入回归测试:编辑态 M/D/Float32 写入进入初始仓库,仿真输出在停止和重启时被丢弃
- 领域和服务测试覆盖 HMI 页面/控件、报警、控制逻辑/网络动态上限与新建页面默认尺寸
- 工程管理测试覆盖调低页面上限后拒绝两页面工程,并确认当前工程状态保持不变
- 性能基准通过:代表性软件扫描约 `0.203 ms`,4001 个虚拟寄存器边界工作量约 `0.0454 ms`
- 重新执行 qmake 并完成 Release 主程序链接:`build/float32/release/integrated_platform.exe`
- 新增真机测试覆盖:本地临时输出不改变 PLC 源仓库、同一轮后续网络可见、完整轮询只触发一次推算、断线/通信故障停止推算并返回编辑态、推算故障详情保留
- 使用已有 `build/manual_plc_discovery_probe/release/manual_plc_discovery_probe.exe` 在 `COM3 / 9600 / 8E1 / 站号 1` 完成一次真实 D0 探测,结果找到 PLC;本轮未重复执行真实 M/D 写入恢复流程
- 本轮未做完整真机运行轨迹人工验收:改动的本地推算路径只读取 PLC 缓存、输出只写临时仓库,不改变 PLC 通信写入;本轮 UI 命令输入改动同样未连接真实 PLC,交付前仍建议现场连接真实 PLC 确认轨迹随完整轮询刷新

## 待完成

1. 本轮代码和文档已完成并提交;后续只需按现场计划继续做真实 PLC 验收
2. 现场使用真实 PLC 做一次 Float32 真机验收:读取 D10/D11 原值,写入一个有限 Float32,确认一次双寄存器请求,轮询读回显示正确,恢复原值并再次读回确认
3. 现场同时确认 HMI 写入仍正常、本地轨迹随每轮 PLC 缓存刷新、PLC 源 M/D 不被本地梯形图输出改变

## 下一会话起点

1. 先运行 `git status --short`,确认工作区状态后再开始下一项改动
2. 若继续改配置字段,同步更新默认文件内容、严格加载器、配置测试和 `docs/用户使用/应用配置说明.md`
3. 提交前重新运行 Release 测试、主程序构建和 `git diff --check`
4. 命令输入尚未做完整人工 UI 验收,交付前需在编辑器中双击已有触点、条件网格、横线、Gap 和输出槽,确认替换、输入框位置、补全、错误标红和连续光标移动
- `domain_tests` Release 通过,覆盖断路输出拒绝和竖线并联支路绕过断点
- `logic_editor_service_tests` Release 通过,除既有编辑、复制和输出事务外,覆盖无效横竖线规整、有效并联旁路保留、断路网络保留、一次撤销及独立双线圈检查
- `runtime_mode_service_tests` Release 通过,覆盖运行前先规整独立残线、同一断路详情阻止离线和完成首读后的真机运行
- `runtime_panel_controller_tests` Release 通过,覆盖完整执行器轨迹、画布布局、框选、连续输入、复制粘贴,以及语法错误的条件格和第 11 列输出槽定位
- 网格背景像素回归通过,触点、横线、`Gap` 和输出槽的非线路区域采样颜色完全一致
- `offline_simulation_service_tests` Release 通过,覆盖正反转启动、自锁、互锁、停止及分段电源轨迹
- 13 个 Release Functional 测试目标全量通过
- 主程序已重新构建到 `build/release/release/integrated_platform.exe`,离屏启动 3 秒正常保持 Qt 事件循环
- `git diff --check` 通过;只有工作区既有 `runtime_mode_service_tests.cpp` 的 CRLF/LF 转换提示,没有空白错误
- 尚未连接真实 PLC;本轮修改的是编辑事务、运行前静态校验和离线/本地轨迹投影,不涉及设备写入,因此未做 COM3 真机验证
- 真机模式仍只读取 PLC 缓存并在本地推算梯形图轨迹,不生成、下载或写入 PLC 程序

## 下一步

- 人工桌面检查常开/常闭/沿触点、SET/RST、比较、MOV/ADD/SUB 的视觉比例,以及不同系统缩放下的框选边框和内嵌输入框位置
- 有真实接线和已确认寄存器地址后,再单独验证真机读回轨迹;验证过程仍不得向 PLC 下载本地梯形图

+ 23
- 20
docs/architecture.md Прегледај датотеку

@@ -60,18 +60,20 @@ Project
├── registerComments[]
└── controlLogics[]
└── rungs[]
├── condition
├── cells[10]
└── output
└── verticalConnections[]
```

- 页面、逻辑和节点使用稳定 ID;名称可修改,数组顺序决定工程树顺序和离线扫描顺序
- 页面、逻辑、行、网格、节点和竖线使用稳定 ID;名称可修改,数组顺序决定视觉顺序和离线扫描顺序
- 梯形图是连续网格:`Gap/Wire/Node` 是独立格子对象,竖线连接相邻行列边界,插入/删除行会维护连接关系,删线即可拆分网络
- 当前编辑页面和当前逻辑是 UI 会话状态,不写入工程
- HMI 控件类型、默认值、绑定规则和运行值类型由无 Qt 依赖的 `hmi_control_registry` 统一描述
- HMI 数值控件通过 `RegisterDataType` 选择 `Int16` 或 `Float32`;底层仓库始终保存原始 16 位 D 字,`Float32Codec` 负责低字/高字组合
- 需要寄存器的 HMI 控件、报警条件和梯形图节点直接保存 M/D 地址;项目不再维护统一 `dataPoints` 表
- `registerComments` 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询

工程 JSON 格式固定为严格的 `1.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。当前版本不读取旧 `dataPoints`、`stages/branches`,也不迁移缺少显式横线、断路或其他必填结构的早期同版本草稿。字段定义见 `docs/工程格式说明.md`。
工程 JSON 格式固定为严格的 `2.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。旧 `1.0` 文件直接拒绝,不做迁移或双写。字段定义见 `docs/工程格式说明.md`。

校验分两级:

@@ -112,34 +114,35 @@ ApplicationSettings(启动后只读)

## 结构化梯形图

网络条件保存为规范化表达式树:
连续梯形图不再使用串并联表达式树。`LadderRung` 是按视觉顺序排列的一行,前 10 列固定保存 `LadderCell`,每格独立表示 `Gap`、`Wire` 或条件 `Node`;第 11 列是独立输出槽。行之间的网络关系由 `ControlLogic::verticalConnections` 保存,每根竖线只连接相邻两行的一个 `0~10` 列边界。长竖线由多个相邻连接对象组成,因此可以逐段画线、删线、拆分和恢复。

```text
ConditionExpression
├── Node 触点、边沿或比较条件
├── Wire 无地址横线,保存条件区列跨度
├── Gap 明确断开的空白网格,保存条件区列跨度
├── Series 子表达式执行 AND
└── Parallel 子表达式执行 OR
```
`LogicEditorService` 负责网格、竖线和行的原子编辑:横向手势批量设置 `Wire / Gap`,纵向手势批量创建或删除相邻 `VerticalConnection`,插入行会拆分原连接,删除行只在上下同列连接都存在时合并。一次鼠标手势和一次工具栏操作都只产生一条撤销记录,失败时回滚整个编辑。`LogicEditorWidget` 只负责命中测试、绘制和将服务结果投影到画布,不直接修改工程容器。

梯形图复制使用服务层值对象 `LogicClipboardFragment`,不保存源模型指针。`GridObjects` 模式按相对行列保存明确选中的 `Wire / Node`、输出和竖线,未选位置不写入片段,也不把 `Gap` 当成待覆盖内容;`WholeRows` 模式只由左侧行号选择产生,保存完整 10 格、输出、注释和选中行内部的竖线。`LogicEditorService::pasteClipboard()` 先校验全部目标行列、对象类型、冲突和数量上限,再统一分配新 ID 并提交一次历史记录;任一目标非法时恢复模型和工程修改状态。当前光标行与明确整行选择是两份独立 UI 状态,不能互相回退。

输出是网络最右侧的单独节点。`LogicEditorService` 把串联、并联、旁路、替换和删除转换为表达式树操作,失败时回退整个编辑;`LogicEditorWidget` 只把树投影成正交网格。竖线由 `Parallel` 边界派生,网格坐标和自由线段不持久化,因此领域模型不会产生悬空线、环路或多输出路径
单格编程通过 `LogicEditCursor -> LogicEditResult` 服务契约完成。服务在同一事务中写入触点、横线或输出并返回下一光标:条件区逐格右移,第 10 格后进入输出槽,输出后越过当前竖线连通分组;末尾输出会连同下一空行一起提交和撤销。空行直接写输出时,输出事务把 10 个条件格全部补为 `Wire`;已有条件时只补最右有效内容到输出槽之间的尾部 `Gap`,不会跨过中间的明确断点。画布内嵌指令、工具栏动作和 QAction 快捷键只消费该结果,不在 UI 重复计算推进规则。

单网络前 10 列为条件区,第 11 列固定为输出位置;存在输出时,前 10 列必须由 `Node / Wire / Gap` 显式占满。横线和断路只能占条件区。第 11 个条件、未占满或超过 10 列的输出网络、过深表达式或过多并联行会在编辑和加载阶段被拒绝,具体数值以数量边界文档为准
网络不是额外的表达式对象,而是横向导通格、条件节点、输出槽以及竖线连通分量共同决定的电气关系。执行器按列传播电源,在每个列边界先合并竖线连通分量,再计算各行网格和输出;删除竖线会自然拆分网络。节点配置使用 `std::variant` 的独立类型,当前条件包括 M 常开/常闭、M 上升沿/下降沿和 D 比较,输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。

控制逻辑组可以暂时没有网络。新建工程和新建控制逻辑都保持 `rungs=[]`,用户第一次添加条件、横线或输出时,编辑服务在同一个原子操作中创建网络;删除最后一个网络后也允许回到空逻辑。条件区中每一段可见、可选择和可删除的横线都必须是持久化 `Wire`;没有横线的位置由 `Gap(columnSpan)` 明确占位。直接添加输出会创建 `Wire(10) + Output`,一个触点后添加输出会创建 `Node + Wire(9) + Output`。删除触点或横线只把对应网格替换为 `Gap`,不会让左右两侧自动重新接通;在该网格重新添加横线或触点后才恢复连接。并联各支路必须用真实 `Wire` 或 `Gap` 补到相同列宽。左右母线、节点和输出图元自身的端子线、并联竖线仍由结构派生,不保存为自由像素线。连续追加单格横线占满十个条件列后,下一次无选中目标的追加会在同一撤销记录中创建下一个网络
控制逻辑组可以暂时没有行。新增行会生成 10 个稳定网格 ID;鼠标横向拖动设置连续 `Wire`,竖向拖动创建多个相邻 `VerticalConnection`。插入行时原有竖线被拆成上下两段以保持连续;删除行时只有上下两段在同列都存在才合并,单边连接直接移除。所有编辑由 `LogicEditorService` 原子提交并进入撤销历史

网络运行语义:

- 空网络不执行
- 有条件但没有输出的已启用网络不能运行
- 直接接通的输出使用 `Wire(10)` 显式表示恒真路径,每次扫描执行输出
- 输出槽按所在行的实际导通状态执行;没有输出的行可以作为纯连接支路或编辑草稿存在,不会单独产生输出
- 直接接通的输出使用 10 个连续 `Wire` 网格显式表示恒真路径,每次扫描执行输出
- `Wire` 的逻辑值为真,但运行轨迹是否导通仍取决于上游电流
- `Gap` 表示明确断路,可以保存编辑草稿,但任何包含 `Gap` 的启用网络都不能进入运行态
- `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 相互覆盖。
`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 缓存开始。

@@ -171,7 +174,7 @@ PLC <-> PlcCommunicationService <--------------------┘
| --- | --- | --- | --- | --- |
| 编辑态 | 是 | 数据监控使用虚拟 M/D 初始值;其他编辑操作不启用运行数据源 | 停止 | 运行态先退出 |
| 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 |
| 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读 |
| 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读,工程通过运行校验 |

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



+ 22
- 20
docs/工程格式说明.md Прегледај датотеку

@@ -1,14 +1,14 @@
# 工程 JSON 1.0 格式说明
# 工程 JSON 2.0 格式说明

## 格式策略

当前开发阶段只有一个有效工程格式:`1.0`。代码直接读写本文描述的结构,不为早期开发草稿增加同版本默认值、字段推断、迁移分支或双写逻辑。缺少必填字段、字段类型错误、引用不存在或领域校验失败时,加载必须失败且不得替换当前工程。
当前只有一个有效工程格式:`2.0`。代码直接读写本文描述的结构,不为 `1.0` 或早期开发草稿增加迁移分支、字段推断或双写逻辑;旧版本加载会明确失败。缺少必填字段、字段类型错误、引用不存在或领域校验失败时,加载必须失败且不得替换当前工程。

## 顶层结构

```json
{
"formatVersion": "1.0",
"formatVersion": "2.0",
"id": "project-1",
"name": "包装线",
"hmiPages": [],
@@ -62,7 +62,7 @@
| `pageJump` | 必须为 `null` | `targetPageId` |
| `alarmList` | 必须为 `null` | 无 |

`type` 只接受表中列出的控件类型。其他类型按严格 `1.0` 规则直接拒绝加载,不做兼容迁移。
`type` 只接受表中列出的控件类型。其他类型按严格 `2.0` 规则直接拒绝加载,不做兼容迁移。

数值显示和数值输入控件必须额外保存 `dataType`,值只能是 `int16` 或 `float32`:

@@ -78,7 +78,7 @@
}
```

`int16` 只占起始 D 一个字,沿用现有有符号 16 位逻辑。`float32` 使用 IEEE-754 单精度,占用起始 D 和下一个 D,低地址保存低字、高地址保存高字;起始地址只能是 `D0~D3999`。数值控件之间允许相同起始地址和相同类型重复绑定,部分重叠或类型冲突会被拒绝。工程格式仍固定为严格 `1.0`,数值控件缺少 `dataType` 时加载失败。
`int16` 只占起始 D 一个字,沿用现有有符号 16 位逻辑。`float32` 使用 IEEE-754 单精度,占用起始 D 和下一个 D,低地址保存低字、高地址保存高字;起始地址只能是 `D0~D3999`。数值控件之间允许相同起始地址和相同类型重复绑定,部分重叠或类型冲突会被拒绝。工程格式固定为严格 `2.0`,数值控件缺少 `dataType` 时加载失败。

按钮操作字符串为 `setOn`、`setOff`、`toggle` 或 `momentaryOn`。PageJump 使用目标页面稳定 ID,不使用名称或数组位置:

@@ -131,14 +131,20 @@ AlarmList 只负责显示项目级当前报警,不保存独立触发逻辑,
}
```

注释不保存寄存器当前值,也不自动把地址加入 Modbus 轮询。网络注释属于 `LadderRung`,与网络 ID、名称、条件和输出并列保存。网络注释允许为空;非空时只能使用单行文本,不能包含回车或换行,且不能超过 128 个 UTF-8 字节:
注释不保存寄存器当前值,也不自动把地址加入 Modbus 轮询。网络注释属于 `LadderRung`,与行 ID、名称、连续网格和输出并列保存。网络注释允许为空;非空时只能使用单行文本,不能包含回车或换行,且不能超过 128 个 UTF-8 字节:

```json
{
"id": "rung-1",
"name": "启动网络",
"comment": "启动按钮接通后延时启动",
"condition": null,
"cells": [
{"id":"cell-0","kind":"gap"}, {"id":"cell-1","kind":"gap"},
{"id":"cell-2","kind":"gap"}, {"id":"cell-3","kind":"gap"},
{"id":"cell-4","kind":"gap"}, {"id":"cell-5","kind":"gap"},
{"id":"cell-6","kind":"gap"}, {"id":"cell-7","kind":"gap"},
{"id":"cell-8","kind":"gap"}, {"id":"cell-9","kind":"gap"}
],
"output": null
}
```
@@ -154,25 +160,21 @@ AlarmList 只负责显示项目级当前报警,不保存独立触发逻辑,
}
```

`enabled` 决定离线执行器是否扫描该逻辑。所有启用逻辑按 `controlLogics` 数组顺序执行;禁用逻辑仍要求 ID、名称、网络和表达式结构合法,但允许保留未完成节点作为草稿。
`enabled` 决定离线执行器是否扫描该逻辑。所有启用逻辑按 `controlLogics` 数组顺序执行;禁用逻辑仍要求 ID、名称、行和连续网格结构合法,但允许保留未完成节点作为草稿。

控制逻辑初始可以没有网络,`rungs` 为空不是错误。编辑器第一次添加条件、横线或输出时才创建网络;删除最后一个网络后可以再次保存为空逻辑。没有输出的空网络草稿允许 `condition` 和 `output` 同时为 `null`。存在输出时,`condition` 不能为空,并且必须显式占满前 10 列
控制逻辑初始可以没有行,`rungs` 为空不是错误。编辑器第一次添加条件、横线或输出时才创建行;删除最后一行后可以再次保存为空逻辑。没有输出的空行草稿允许保存 10 个 `Gap` 网格。输出行也允许暂存未完成的条件路径;运行时 `Gap` 会按断路处理,只有实际导通的输出槽才执行

网络、`Node / Wire / Gap / Series / Parallel` 条件表达式、M 触点、M 边沿、D 比较、M 线圈和字操作指令字段沿用当前结构化梯形图模型。条件区中每一段可见、可编辑横线都保存为恒真的 `Wire` 叶子
2.0 使用连续网格模型。每条 `LadderRung` 固定保存 10 个 `cells`,每格的 `kind` 是 `gap`、`wire` 或 `node`;条件节点直接嵌在 `node` 格中。行不再是互相隔离的网络容器,行间连接由控制逻辑的 `verticalConnections` 保存。长竖线由多个相邻连接对象组成,插入行会拆分原连接,删除行只在上下两段同列存在时合并。2.0 不再把 `ConditionExpression` 写入工程 JSON,也不读取旧 `condition` 字段

```json
{"id": "wire-1", "kind": "wire", "columnSpan": 2}
{"id":"vertical-1","upperRungId":"rung-1","lowerRungId":"rung-2","columnBoundary":2}
```

明确断开的连续空白网格保存为 `Gap` 叶子:
每行 `cells` 必须严格为 10 个。鼠标拖动会逐格设置横线或空白,删除横线只恢复目标格;竖线连接的行在执行器中共享列边界电源。

```json
{"id": "gap-1", "kind": "gap", "columnSpan": 1}
```

`Wire` 和 `Gap` 的 `columnSpan` 都是必填整数,范围为 `1~10`,表示占用的条件区逻辑网格列数,不是像素长度。删除一格横线或触点时,该格改为 `Gap(1)`;多格横线只断开被删除的格,其余部分仍是 `Wire`。`Gap` 可以保存和重新加载,但运行校验必须拒绝包含 `Gap` 的启用网络。
每个 `cells` 元素只描述一个固定列,例如 `{"id":"cell-1","kind":"wire"}`、`{"id":"cell-2","kind":"gap"}`,或者带 `node` 对象的 `kind":"node"`。横线和空白不使用 `columnSpan`;每个格子固定占用一列。`Gap` 表示明确断路,可以保存和重新加载;执行器遇到 `Gap` 时该行电源在此处断开,允许作为未完成草稿或并联网络中的断路支路。

单个网络前 10 列是条件区,第 11 列固定放输出指令。存在输出时,前 10 列必须由 `Node / Wire / Gap` 精确占满;直接添加输出保存为 `Wire(10) + Output`,一个触点后添加输出保存为 `Node + Wire(9) + Output`。并联支路必须显式等宽,短支路的补线也保存为真实 `Wire`。左右母线、节点和输出自身端子、并联竖线不保存为独立对象,而是由表达式结构派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线和断路不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下:
单个行前 10 列是条件区,第 11 列固定放输出指令。空白格可作为编辑草稿,输出行中的断路会让该支路不导通。左右母线、格子端子和竖线均按网格模型绘制,画布像素坐标和运行轨迹不进入 JSON;横线和断路不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下:

```json
{"type": "contact", "address": {"area": "M", "index": 0}, "mode": "normallyOpen"}
@@ -192,6 +194,6 @@ MOVE、ADD 和 SUB 只能把结果写入 D 地址,源操作数和算术左右

保存顺序为领域校验、JSON 序列化、`QSaveFile` 原子提交。加载顺序为 JSON 语法、`formatVersion`、必填字段和类型、领域关系校验,全部成功后才替换当前工程。

工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑包含输出指令,并且条件区中不存在 `Gap`;禁用草稿逻辑不阻止运行。左侧电源线直接接通的输出必须保存为 `Wire(10) + Output`,不能省略 `condition`
工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑中的节点已配置;`Gap` 仍表示断路并会让该支路不导通,不会被当成隐含横线。禁用草稿逻辑不阻止运行。左侧电源线直接接通的输出必须保存为 10 个 `Wire` 网格加输出槽

当前实现只读写本文定义的严格 `1.0`。早期同为 `1.0`、但依赖隐式输出连线或隐式并联补线的草稿不会自动迁移,结构不符合当前领域校验时直接加载失败
当前实现只读写本文定义的严格 `2.0`。`1.0` 或其他旧工程不会自动迁移,加载时直接返回不支持的格式版本

+ 5
- 5
docs/开发顺序.md Прегледај датотеку

@@ -4,7 +4,7 @@

## 当前进度

第 1~11 步的软件实现和现场验证已完成,工程格式继续使用 `1.0`,不保留旧梯形图结构的兼容代码。当前重点是第 12 步的交付资料和最终演示整理。
第 1~11 步的软件实现和现场验证已完成,工程格式统一为连续网格 `2.0`,不保留旧梯形图结构的兼容代码。当前重点是第 12 步的交付资料和最终演示整理。

## 1. 建立开发基线

@@ -57,9 +57,9 @@

## 7. 实现控制逻辑编辑器

- 使用 `Node / Wire / Series / Parallel` 表达式树建立梯形图逻辑模型,支持嵌套串并联关系和无地址横线
- 支持节点前后串联插入、与节点并联、与整个支路并联、删除和输出线圈配置
- 支持选择横线后用触点原位替换;`F11/F12` 插入横线或为连续范围建立竖线旁路,`Shift+F11/Shift+F12` 删除对应横线或并联支路
- 使用连续 10 列 `LadderCell` 和独立 `VerticalConnection` 建立梯形图网格,横线、竖线和条件节点分别持久化
- 支持节点按列插入、并联支路、输出槽配置、行上方/下方插入和行删除
- 支持鼠标横向连续画线、纵向连续连接、独立删线和网络拆分;`F11/F12` 与 `Shift+F11/Shift+F12` 分别操作当前选中的网格或竖线
- 实现 M 常开/常闭触点、M 上升沿/下降沿触点、D 值与常量比较、普通/置位/复位 M 线圈以及 MOVE、ADD/SUB。
- 上升沿和下降沿按一次扫描脉冲执行;ADD/SUB 以有符号 16 位结果饱和处理溢出,不能让一次越界中止扫描。
- 梯形图节点只引用项目统一的 M/D 地址;新增或恢复指令时,必须同步领域模型、编辑器、JSON、执行器和测试。
@@ -67,7 +67,7 @@
- 新增节点类型时使用独立配置类型,不向通用节点结构持续堆叠无关字段。
- 编辑器只生成逻辑模型,不直接修改 HMI 或 PLC。
- 工程可包含多个有序控制逻辑,离线执行器按顺序扫描全部启用项,运行工作台只切换当前轨迹投影。
- 画布按表达式结构自动布线,横线作为结构叶子保存列跨度,竖线由并联结构派生;不允许节点自由拖动,也不保存自由像素线段。
- 画布按固定网格绘制左右母线和输出槽,行间网络由竖线连通分量决定;不允许节点自由拖动,也不保存自由像素线段。

完成标准:能够表达 `A OR (B AND C)` 等嵌套逻辑,并配置启动、停止和状态保持逻辑。



+ 8
- 7
docs/测试约定.md Прегледај датотеку

@@ -10,22 +10,23 @@
| 性能测试 | 代表性工程扫描和边界规模寄存器访问性能 | 独立执行 |
| 真实 PLC 实测 | 串口、Modbus RTU、PLC 响应、接线和现场联动有效 | 人工执行并记录 |

不使用自动化测试验证界面布局、视觉效果和真实鼠标操作,这些内容由交付前人工验收确认。跨运行模式的 Qt 排队信号无法只靠服务测试证明,因此保留一个无界面运行的控制器测试,专门验证退出离线仿真后不会恢复旧梯形图轨迹;PLC 自动搜索还保留一个离屏对话框流程测试,验证开始、取消、进度和找到后自动提交参数。
自动化测试验证画布命中、拖动选择、对象与整行剪贴板、连续光标和轨迹字段到图元的投影,不以像素截图代替人工视觉验收;符号比例、颜色观感和不同系统缩放仍由交付前人工检查。跨运行模式的 Qt 排队信号无法只靠服务测试证明,因此保留离屏控制器测试验证退出离线仿真后不会恢复旧梯形图轨迹;PLC 自动搜索还保留离屏对话框流程测试,验证开始、取消、进度和找到后自动提交参数。

## 功能自动化测试

| 测试目标 | 主要契约 |
| --- | --- |
| `domain_tests` | 地址、模型、数量边界、Float32 编解码和运行前校验 |
| `domain_tests` | 地址、模型、数量边界、Float32 编解码和带竖线并联的输出路径可达性校验 |
| `application_settings_tests` | 默认配置创建、严格 INI 校验、缺失与未知字段、版本和整份回退 |
| `alarm_service_tests` | 报警定义和运行记录生命周期 |
| `hmi_editor_service_tests` | HMI 编辑、原子删除、历史、导航和 Float32 数值控件读写 |
| `logic_editor_service_tests` | 结构化梯形图编辑、归一化和历史 |
| `logic_editor_service_tests` | 连续网格编辑、输出自动补尾线、网格与整行片段复制粘贴、冲突回滚和历史 |
| `offline_simulation_service_tests` | 梯形图扫描语义、离线初始 M/D 快照、真机缓存副本推算、只读隔离、跨扫描状态和故障处理 |
| `project_management_tests` | JSON 往返、非法文件和工程保存状态 |
| `register_monitor_service_tests` | 监视地址、Int16/Float32 读写、离线初始值捕获、批量步长、重叠提示和活动仓库 |
| `runtime_mode_service_tests` | 编辑、离线、真机只读轨迹和仓库切换 |
| `runtime_panel_controller_tests` | 本地扫描排队回调和编辑态轨迹清理 |
| `runtime_project_bundle_tests` | 用户运行程序封装的写入、读取、校验和损坏拒绝 |
| `runtime_mode_service_tests` | 编辑、离线、真机只读轨迹、仓库切换和断路工程启动拦截 |
| `runtime_panel_controller_tests` | 完整执行器快照经过控制器和运行窗口后的轨迹投影、排队回调、画布框选、对象/行号剪贴板和连续输入 |
| `plc_connection_dialog_tests` | PLC 端口刷新及自动搜索开始、取消、进度和参数回填 |
| `plc_runtime_tests` | PLC 缓存、完整轮询通知、成对轮询边界、错误恢复和 Fake gateway |

@@ -44,7 +45,7 @@
pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release
```

`app/tests/tests.pro` 只聚合上述 12 个功能测试目标。测试构建输出放在 `build/`,不写入 `app/`。
`app/tests/tests.pro` 只聚合上述 13 个功能测试目标。测试构建输出放在 `build/`,不写入 `app/`。

## 性能测试

@@ -88,7 +89,7 @@ pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release

## 提交前检查

1. 运行 12 个 Release 功能测试目标
1. 运行 13 个 Release 功能测试目标
2. 单独运行 `performance_tests` 并保留基准输出
3. 构建 Qt Release 主程序
4. 按风险决定是否重新执行真实 PLC 实测;未重测时引用已有验收记录并说明原因


+ 2
- 2
docs/用户使用/数量边界确认方案.md Прегледај датотеку

@@ -49,7 +49,7 @@ HMI 页面宽度 `320~1600`、高度 `200~800` 仍是结构安全硬范围;`De
| 单个网络 | 总列数 | 0(空网络草稿) | 11 | 第 11 列固定放输出指令 | 运行网络固定为前 10 列条件区加第 11 列输出 |
| 单个网络 | 逻辑网格行数 | 1 | 64 | 超出后编辑操作回退,加载失败 | 防止并联支路纵向无限扩展 |
| 横线 | 单段横线跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | 横线只能占用条件区,不能占用输出列 |
| 断路 | 单段空白跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | `Gap` 明确占用条件区空白网格,可保存但不能运行 |
| 断路 | 单段空白跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | `Gap` 明确占用条件区空白网格;带输出的路径不可达时拒绝运行 |
| 字符串 | ID、格式版本、页面跳转目标 ID | 0 字节 | 128 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的 ID 仍必须非空 | ID 只用于稳定识别,不需要长文本 |
| 字符串 | 工程名、页面名、控件文本、报警文本、网络名称 | 0 字节 | 256 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的字段仍必须非空 | 防止名称和显示文本异常增长;HMI 控件文本、报警标题和报警文本编辑界面分别使用上方的 12/12/20 字符输入限制 |
| 注释 | 软元件注释 | 1 字节 | 64 个 UTF-8 字节 | 拒绝编辑、保存或加载;禁止回车和换行 | 注释绘制在梯形图节点下方,必须保持紧凑单行 |
@@ -73,7 +73,7 @@ HMI 页面宽度 `320~1600`、高度 `200~800` 仍是结构安全硬范围;`De

补充说明:空工程允许 0 个 HMI 页面;一旦已经创建页面,用户在编辑器里不能把最后一个页面删除。校验方式只能选无校验、奇校验或偶校验,常用真机配置仍是 `COM3、9600、8E1`。PLC 自动搜索固定使用界面当前站号,遍历全部可用 PC 串口和上述允许参数的 60 种组合,不自动穷举 `1~247` 站号。

用户可以直接添加线圈或其他输出指令,但模型会自动在输出前生成 10 格真实横线,即 `Wire(10) + Output`。它表示条件一直成立,软件每次扫描都会执行这个输出;普通线圈会一直导通,MOVE、ADD/SUB 等指令会每次扫描执行。只有条件却没有输出,或条件区存在断路 `Gap`,都属于未完成网络,不能进入运行态
用户可以直接添加线圈或其他输出指令,完全空行会自动在输出前生成 10 格真实横线,即 `Wire(10) + Output`。它表示条件一直成立,软件每次扫描都会执行这个输出;普通线圈会一直导通,MOVE、ADD/SUB 等指令会每次扫描执行。已有内容的行只补最右有效内容后的尾部横线,不跨过中间 `Gap`。`Gap` 本身不等于整个工程不能运行:没有输出的空白草稿和并联网络中的断路支路允许保留;只有带输出的行不存在任何左母线到输出槽的完整路径时,运行校验才会拒绝

## 2. 不需要再单独限制的项目



+ 63
- 0
docs/用户使用/连续梯形图网格说明.md Прегледај датотеку

@@ -0,0 +1,63 @@
# 连续梯形图网格说明

## 画布

控制逻辑使用一张连续画布。每行有 10 个条件格,第 11 列是输出槽;相邻行之间没有网络卡片。横线、空白、条件节点、输出和竖线都可以单独命中和编辑。

网格背景、梯形图线路和选择框由三个独立图层绘制。触点、横线、空白 `Gap` 和输出槽使用同一个画布底色,不再用格子背景区分数据类型;网格线保持统一浅色。常开、常闭、上升沿、下降沿、比较、普通线圈、SET/RST、MOV、ADD 和 SUB 使用各自的梯形图符号或指令块,数据指令会显示完整操作数。选择框始终位于最上层,四边使用同一颜色和线型。

左右母线使用相同颜色,只从第一条视觉行延伸到最后一条视觉行,不进入画布底部留白。输出槽位于条件区和右母线之间。

## 画线和删线

- 选择“鼠标画线”后,在同一行横向拖动,把经过的空白格设为横线;条件节点不会被横线覆盖
- 在同一列边界上下拖动,在经过的相邻行之间建立竖线;长竖线由多段相邻连接组成
- 选择“鼠标删线”后执行相同手势,横向手势恢复空白格,竖向手势删除连接段
- 工具栏“横线”按钮作用于当前条件光标;控制逻辑还没有行时,会一次完成“创建首行 + 写入第 1 格”
- 每次成功插入横线或触点后,光标自动右移一格;第 10 格完成后进入输出槽
- 每次完整拖动只产生一条撤销记录

## 连续输入

双击条件格或输出槽会在画布内打开指令输入框,不再弹出独立输入对话框。输入框提供 LD、LDI、LDP、LDF、比较、AND/ANI、OR/ORI、OUT、SET、RST、MOV、ADD 和 SUB 补全;`Enter` 提交,`Esc` 取消,滚动画布时输入框会跟随当前目标格。

条件指令提交后进入右侧一格,第 10 格后进入输出槽。输出确认后会越过当前竖线连通的整个网络分组,进入下一网络首格;当前分组已经在末尾时,服务会自动追加一条带 10 个空白格的新行。输出和新增行属于同一次服务事务,只生成一条撤销记录;若行数达到上限,输出和新增行都会失败,不会留下半成品。

完全空白的行直接输入线圈或其他输出时,软件会把输出前 10 格全部补成横线,形成 `Wire(10) + Output`。行内已经有触点或横线时,只自动补齐最右有效内容后面的尾部空格;位于有效内容中间的 `Gap` 是明确断路,不会被输出操作偷偷接通。补线、输出和必要的新行仍属于同一个可撤销事务。

工具栏按钮、F11 横线快捷键和画布指令输入都使用服务返回的同一下一光标,不各自计算右移位置。

## 行和网络

行是视觉顺序,不再是禁止跨越的网络边界。用户通过竖线把上下行连接成一个电气网络,删除竖线后立即拆分。

画布左侧只显示从 `000` 开始的视觉行号,不显示行名称。每个网络首行上方都有独立注释带,网络注释使用绿色文字;通过竖线连接的后续支路行不重复显示注释。绘制、点击命中、框选和滚动共同使用同一份行布局坐标。

选择模式下,普通单击选择一个网格或对象;按住左键拖动可框选多个触点、横线、输出块和竖线。`Ctrl+拖动`追加对象,`Ctrl+单击`追加或取消单个对象。“并联支路”只使用当前选中的连续条件作为范围,不会默认把整行全部并联。

普通 `Delete` 会把选中的节点、横线格、输出块和竖线作为一个选择集提交,一次删除只产生一条撤销记录;请求中任一对象失效时整次不修改。删除整行使用 `Shift+Delete`,单击列边界、空网格或空输出槽后按 `Delete` 不会删除整行。

插入行会保持原竖线连续:原来的上行到下行连接拆成“上行到新行”和“新行到下行”。删除行时,只有上下两段在同一列都存在才会合并为新的相邻连接,避免删除行后意外接通原本断开的网络。

## 复制和粘贴

- 单击横线、触点、输出或竖线后执行复制,只复制当前对象,不会因为光标位于某一行就退化成整行复制
- 框选或按住 `Ctrl` 追加选择时,横线、触点、输出和竖线按相对行列位置组成一个网格片段;未选中的格子是透明空洞,粘贴时不清空目标内容
- 普通对象片段不复制 `Gap`。需要制造断点时继续使用删线或 `Delete`
- 只有单击左侧行号才进入整行选择。整行复制保存 10 个条件格、输出、网络注释和选中行之间的竖线;多行整行复制要求视觉上连续
- 普通片段从当前网格、输出槽或列边界开始粘贴。单独输出只能粘贴到输出槽,单独竖线只能粘贴到存在相邻下行的列边界
- 单独复制的输出允许明确替换目标输出;混合片段不会静默覆盖已有输出,横线或节点也不会覆盖目标节点
- 片段越过第 10 个条件格、跨行目标不足、竖线落在末行、目标节点冲突或达到行数上限时,整次粘贴失败,不截断也不留下部分修改
- 整行粘贴插入当前行之后;粘贴产生的新节点、行和竖线使用新 ID。一次粘贴只生成一条撤销记录,成功后新对象保持选中

## 运行轨迹

运行态保持网格背景不变,只给实际电流路径着色。横线按左右半段显示,触点左端取网格输入电源、触点符号和右端取网格输出电源,竖线取对应连接段电源,输出连接线取行末电源,输出符号取输出节点状态。未导通使用统一深色,导通使用监控绿,故障红色优先;选择框仍位于所有轨迹图层之上。左右母线始终使用相同的普通深色,不参与运行绿色投影。

进入离线运行、完成 PLC 首读后进入真机运行、导出用户运行程序之前,软件都会检查每个输出是否能沿触点、横线和竖线连接回左母线。没有输出的自动追加空行允许保留;并联网络中的单条断路支路也可以保留,只要输出仍有其他完整路径。若输出不可达,界面会提示具体控制逻辑、视觉行和断开列,并保持在编辑态。

运行控制器始终传递完整轨迹快照,画布只按当前控制逻辑投影一次。这样离线扫描和基于 PLC 缓存的真机本地推算都会显示逐段轨迹,不会因为重复筛选而变成全深色。

## 工程文件

连续网格使用严格的工程 JSON `2.0`。旧 `1.0` 工程不自动迁移,加载时会提示格式版本不受支持。

+ 49
- 0
docs/用户使用/鼠标画线与删线说明.md Прегледај датотеку

@@ -0,0 +1,49 @@
# 鼠标画线与删线

控制逻辑画布采用连续 10 列网格。工具栏的“鼠标画线”和“鼠标删线”是互斥模式,运行态会自动退出编辑模式。

## 鼠标画线

- 同一行左右拖动:按网格吸附,将经过的 `Gap` 变为 `Wire`;已有条件节点不会被覆盖
- 从同一列边界上下拖动:在经过的相邻行之间创建 `VerticalConnection`;拖过多行会生成多个相邻连接段
- 工具栏“横线”按钮使用当前条件光标并在成功后右移;空逻辑会原子创建首行并写入第 1 格
- 已有行但没有明确光标时仍会提示先选择,避免误改其他网络
- 画线可以从第 0 列左边界到第 10 列右边界,边界竖线和横线分别独立保存
- 一次完整拖动只生成一条撤销记录,撤销会恢复整次手势

## 鼠标删线

- 同一行左右拖动:将经过的 `Wire` 网格恢复为 `Gap`,不会删除条件节点
- 点击竖线或沿竖线拖动:删除命中的连接段;网络会立即拆分,不会留下隐含连接
- 删除单段长竖线只影响该段,其余相邻段保持不变

## 选择和批量删除

- 退出“鼠标画线/鼠标删线”后进入选择模式,按住左键拖动会显示橡皮筋框
- 框中的触点、横线格、输出块和竖线进入同一个选择集;空白格不会加入删除集合
- 普通拖动替换原选择,`Ctrl+拖动`追加选择,`Ctrl+单击`追加或取消单个对象
- 选中边框由画布最高图层统一绘制,不与网格线共用边框
- 按 `Delete` 后,整个选择集只调用一次编辑服务并生成一条撤销记录;任一对象无效时不会留下部分删除

## 行和网络

- “新建网络”在末尾追加一行
- “上方插入行”和“下方插入行”以当前选中行作为参照;原有竖线会拆成上下两段保持连续
- “删除行”会移除当前行;只有上下两段在同一列都存在时才合并,否则保持断开
- `Delete` 只删除明确选中的节点、横线格、输出块或竖线,不会把网格边界、空格或空输出槽误当成整行;整行使用 `Shift+Delete`
- 网络由横线、条件节点、输出槽和竖线连通关系共同决定,行本身不再隔离网络

## 语法检查与规整

- 点击“语法检查”或按 `Ctrl+G`,会先规整当前控制逻辑,再检查指令配置和输出路径
- 对于能够正常到达输出的网络,不属于任何“左母线到输出”完整路径的 `Wire` 和 `VerticalConnection` 会直接删除
- 没有输出的悬空横线、悬空竖线和残留支路也会直接删除;一次检查产生的全部规整改动只占一条撤销记录
- 规整不会删除触点、比较指令、输出线圈或数据指令,也不会自动补线
- 如果某个输出网络已经断路,该网络保持原样并报告控制逻辑、网络、行和列;断路输出定位在第 11 列
- 双击输出栏中的语法错误可以切换到对应控制逻辑并选中错误位置
- 进入离线运行、完成 PLC 首读后的真机运行以及导出用户运行程序前,都会用同一规则检查全部已启用控制逻辑;存在语法错误时停止后续操作
- 双线圈不属于普通语法检查范围,继续作为独立检查项处理

## 数据与限制

每行固定持久化 10 个 `cells`,竖线持久化为相邻行之间的 `verticalConnections`。工程格式为严格 `2.0`,旧 `1.0` 文件直接拒绝,不写兼容迁移代码。画布像素、鼠标轨迹和悬空自由线段不会写入工程文件。

Loading…
Откажи
Сачувај