소스 검색

docs: 整理项目入口与数量边界文档

main
suyu 1 개월 전
부모
커밋
2000305681
5개의 변경된 파일289개의 추가작업 그리고 416개의 파일을 삭제
  1. +63
    -50
      AGENTS.md
  2. +41
    -208
      docs/ai/handoff.md
  3. +90
    -156
      docs/architecture.md
  4. +2
    -2
      docs/工程格式说明.md
  5. +93
    -0
      docs/用户使用/数量边界确认方案.md

+ 63
- 50
AGENTS.md 파일 보기

@@ -1,58 +1,71 @@
# 项目长期约定

- 项目作者:`suyu`。
- 项目作者:`suyu`
- 文件、终端输出和源代码统一使用 UTF-8

## 新会话阅读顺序

1. 先读 `docs/ai/handoff.md`,确认当前状态、未提交改动、验证结果和下一步
2. 涉及模块边界、数据流、运行模式或持久化时,再读 `docs/architecture.md`
3. 按任务读取下方索引中的业务文档;Markdown 没有答案时再查 `docs/pdf/`

三个入口文档各自只负责一类信息:

- `AGENTS.md`:长期有效的开发、构建和安全约束
- `docs/architecture.md`:稳定的分层、模块边界、核心模型和数据流
- `docs/ai/handoff.md`:当前进度、工作区状态、最近验证和下一步;更新时替换旧状态,不追加开发流水账

## 关键文件索引

- C++ 代码规范:`docs/C++代码规范.md`。
- 需求参考文档:`docs/0_综合平台编程器_修改.md`。
- 推荐开发顺序:`docs/开发顺序.md`。
- Qt 应用源码根目录:`app/`。
- 本机构建输出目录:`build/`(不纳入 Git)。
- Qt 一键构建运行脚本:`scripts/build_and_run_qt.ps1`。
- XDH-60T4-E 硬件与接线要点:`docs/XDH-60T4-E硬件与接线要点.md`。
- XDH-60T4-E 指令与 Modbus 要点:`docs/XDH-60T4-E指令与Modbus要点.md`。
- PLC 官方手册:`docs/pdf/`。
- 当前开发进度和未完成事项:`docs/ai/handoff.md`(创建后每次新会话优先阅读;不要把动态进度写入本文件)。
- 架构、模块边界和数据流:`docs/architecture.md`(创建后优先阅读)。
- 查阅资料时,先读取 UTF-8 编码的 Markdown 文档;只有 Markdown 中找不到所需信息时,才查阅 `docs/pdf/` 内的 PDF 手册。
- 当前交接:`docs/ai/handoff.md`
- 架构与数据流:`docs/architecture.md`
- C++ 规范:`docs/C++代码规范.md`
- 原始需求:`docs/0_综合平台编程器_修改.md`
- 数量边界最终规则:`docs/用户使用/数量边界确认方案.md`
- 工程 JSON 格式:`docs/工程格式说明.md`
- 开发阶段顺序:`docs/开发顺序.md`(阶段参考,当前状态以 handoff 为准)
- XDH-60T4-E 硬件接线:`docs/XDH-60T4-E硬件与接线要点.md`
- XDH-60T4-E 指令与 Modbus:`docs/XDH-60T4-E指令与Modbus要点.md`
- PLC 官方手册:`docs/pdf/`
- Qt 源码:`app/`
- 构建输出:`build/`(不纳入 Git)

## 技术栈与构建

- 使用 C++、Qt 5.15.2 和 Qt Widgets;UI 默认使用 Qt Designer 的 `.ui` 文件。
- 使用 qmake,不使用 CMake。
- Qt MinGW 套件根目录:`D:\Qt5.15.2\5.15.2\mingw81_64`
- 构建前在 PowerShell 7 中设置 Qt 工具链路径;MinGW 工具目录为常见安装位置,如实际安装位置不同,以本机路径为准:
```powershell
$env:QTDIR = 'D:\Qt5.15.2\5.15.2\mingw81_64'
$env:Path = "$env:QTDIR\bin;D:\Qt5.15.2\Tools\mingw810_64\bin;$env:Path"
qmake <项目文件>.pro
mingw32-make -j2
```
- 优先在独立构建目录执行 qmake 和 `mingw32-make`,不要将生成物混入源代码目录。
- 文件读写、终端查看和源代码统一使用 UTF-8。
- 一键构建并运行 Debug 程序:`pwsh -NoLogo -NoProfile -File .\scripts\build_and_run_qt.ps1`。
- 一键构建并运行 Release 程序:`pwsh -NoLogo -NoProfile -File .\scripts\build_and_run_qt.ps1 -Configuration Release`。
- 打开 ui 文件:` & 'D:\Qt5.15.2\5.15.2\mingw81_64\bin\designer.exe' '.\app\src\ui\main_window.ui'`
## Git 提交约定
- 每次提交只包含一个逻辑完整的改动;提交前必须完成对应的构建或测试验证。
- 提交信息使用 `类型: 简短说明` 格式,例如 `feat: 建立 Qt 工程基线`。
- 类型使用 `feat`、`fix`、`docs`、`test`、`refactor` 或 `chore`。
## 架构与运行模式
- 保持 UI、领域、服务和基础设施分层;业务逻辑不得堆入 `MainWindow` 或 Qt 槽函数。
- HMI 只绑定 M/D 寄存器仓库,不得直接读写串口;串口通信必须异步,不能阻塞 UI。
- M 区和 D 区的项目可用地址均为 `0~4000`;Modbus 代码使用从 `0` 开始的原始地址。
- 离线模式使用虚拟 M/D,并运行软件逻辑执行器。
- 真机模式使用 PLC 的真实 M/D,软件逻辑执行器必须停止;切换时先读取 PLC 数据,不自动写入离线虚拟值。
- 真机联动设备时 PLC 必须处于 RUN;STOP 仅用于安全通信验证。
## 注意
- 生成注释的文本结尾别加逗号 `。`。
- 你写完代码需要测试真机PLC可以直接连 COM3,9600 8e1配置。
- 离线仿真不会把PLC程序下载到真机PLC的,不做这个。
- 使用 C++17、Qt 5.15.2、Qt Widgets 和 qmake,不使用 CMake
- UI 默认使用 Qt Designer `.ui` 文件;不要把静态表单重新堆回 C++
- Qt MinGW 套件:`D:\Qt5.15.2\5.15.2\mingw81_64`
- Windows 命令优先使用 PowerShell 7;显式调用使用 `pwsh -NoLogo -NoProfile -Command`,脚本使用 `pwsh -NoLogo -NoProfile -File`
- 复杂 PowerShell 逻辑写入 `.ps1` 后执行,避免嵌套引号;排查兼容问题时先确认 `$PSVersionTable.PSVersion` 和 `$PSVersionTable.PSEdition`
- 在独立构建目录运行 qmake 和 `mingw32-make`,不要把生成物写进 `app/`
- Debug 构建运行:`pwsh -NoLogo -NoProfile -File .\scripts\build_and_run_qt.ps1`
- Release 构建运行:`pwsh -NoLogo -NoProfile -File .\scripts\build_and_run_qt.ps1 -Configuration Release`
- 打开主界面:`& 'D:\Qt5.15.2\5.15.2\mingw81_64\bin\designer.exe' '.\app\src\ui\main_window.ui'`
## 代码与架构约束
- 保持 `UI -> Services -> Domain` 依赖方向,基础设施实现服务契约;`domain` 不依赖 Qt、串口或文件系统
- 业务规则放领域或服务层,不得堆入 `MainWindow`、Qt 槽函数或图元绘制代码
- HMI 和运行界面只访问寄存器仓库,不直接访问串口;PLC 通信必须异步,不能阻塞 UI
- 编辑操作通过服务完成并保持原子性,失败时不得留下部分修改
- 数量边界统一定义在 `app/src/domain/project_limits.h`,修改时同步更新数量边界文档和边界测试
- M/D 项目地址范围固定为 `0~4000`;Modbus 使用从 `0` 开始的原始地址
- 离线模式使用虚拟 M/D 并运行软件逻辑执行器
- 真机模式使用 PLC 读回缓存并停止软件逻辑执行器;切换时先读 PLC,不复制离线值
- 本项目不生成、编译或下载 PLC 程序,不得把本地梯形图轨迹当成 PLC 内部程序轨迹
- 代码注释只解释不明显的约束,末尾不要加句号 `。`
## 验证与安全
- 代码改动必须构建相关测试;共享模型、持久化、运行模式或 UI 工作流改动还要扩大回归范围
- 提交前至少完成对应自动化测试、Release 构建和 `git diff --check`
- 涉及 PLC 通信或真机运行模式的代码,使用 `COM3 / 9600 / 8E1 / 站号 1` 做真实读写验证;先读取原值,测试后恢复并读回确认
- STOP 用于安全通信验证;带真实设备的联动测试必须在确认接线和地址后切到 RUN
- 纯文档、领域校验或离线执行器改动不要求连接 PLC,但要在 handoff 中说明未做真机测试的原因
- 离线仿真不会向 PLC 下载程序
## Git 提交
- 不覆盖或回退工作区中来源不明的改动,开始前先检查 `git status` 和相关 diff
- 每次提交只包含一个逻辑完整改动,提交前完成对应验证
- 提交信息使用 `类型: 简短说明`,类型限 `feat`、`fix`、`docs`、`test`、`refactor`、`chore`

+ 41
- 208
docs/ai/handoff.md 파일 보기

@@ -1,221 +1,54 @@
# Current Handoff
# 当前开发交接

- Goal: 完成综合平台编程器的结构化梯形图、HMI、离线仿真和 Modbus RTU 真机链路
- Branch: `main`
- Current status: 第 1~10 步的软件实现与自动化测试已完成,待连接 XDH-60T4-E 进行真机安全验证和整理最终交付资料
> 更新日期:2026-08-19。本文件只保留当前状态和下一步;完成事项合并成结论,不追加按日期排列的开发日志。

## 当前能力
## 当前状态

- 梯形图条件使用递归 `Node / Wire / Series / Parallel` 表达式树,可表示 `A OR (B AND C)`、嵌套串并联和无地址横线
- 编辑器使用固定行列网格和正交连线,支持当前节点或横线后串联、单节点并联、连续范围横线旁路、删除节点或网络以及设置输出线圈
- 触点、横线和竖线连接段都可以选择;横线可由触点原位替换,竖线映射到对应并联支路,内部表达式容器不作为通用选择对象暴露
- `F11/F12` 分别插入横线和建立连续范围旁路,`Shift+F11/Shift+F12` 分别删除所选横线和竖线支路
- 执行器递归求值并生成节点、表达式、网络轨迹,离线运行时画布显示导通路径和故障位置
- HMI 和梯形图直接配置 M/D 地址,不依赖预先创建的数据点
- HMI 控件描述注册表统一维护现有 8 种控件的稳定名称、默认值、绑定规则和运行值类型
- HMI ProgressBar 使用 D 区 Word 值和可配置范围显示百分比,支持越界钳制、JSON 往返和运行态只读展示
- `ActiveRegisterRepository` 在离线虚拟仓库与 PLC 缓存仓库之间切换
- `PlcCommunicationService` 使用 Qt SerialBus 异步连接、轮询和单点写入,UI 不阻塞
- 真机连接收集 HMI、报警定义和梯形图实际引用地址,按 M/D 分区并合并相邻地址读取
- 真机模式要求首次读取完成,不复制离线值,不运行 PC 软件逻辑执行器
- 通信错误停止轮询并保留最后一次有效缓存,状态栏和日志显示错误
- 通信故障立即撤销当前连接的首读资格;真机运行中发生故障会自动返回编辑态,重新进入真机前必须重新连接并完成新一轮首读
- PLC 故障态可直接“重新配置”,连接前自动清理旧串口会话,不再要求用户先手动点击断开
- PLC 日志区分连接阶段串口无法打开与已连接后的本地串口意外中断;后者自动转为已断开并禁用重复断开操作
- 主窗口静态控件已迁入 `main_window.ui`,PLC 参数由独立配置对话框收集后再发起连接
- 主窗口界面协调已拆为 `ProjectWorkspaceController`、`PropertyPanelController` 和
`RuntimePanelController`,分别负责工程树与会话选择、属性编辑、运行监控与仿真反馈;
`MainWindow` 保留窗口级动作、工程文件操作和跨区域模式切换
- 已增加运行监控工作台:离线仿真同时显示 HMI、梯形图轨迹和自由监控,真机运行只显示 HMI 和自由监控
- 自由监控支持 `M0~M4000`、`D0~D4000` 地址输入、连续批量添加、去重、删除、清空和最多 64 项限制
- 自由监控读取活动寄存器仓库,离线使用虚拟 M/D,真机使用 PLC 缓存,通信故障保留并标记最后有效值
- HMI 按钮支持 TouchWin 同类的置 ON、置 OFF、取反和瞬时 ON,默认瞬时 ON
- 工程支持多个有序 HMI 页面和多个有序控制逻辑,工程树可选择切换、新增、重命名、删除、启停和上下移动
- 主窗口使用会话态当前页面 ID、当前逻辑 ID,所有编辑入口不再固定操作第一项
- 工程持久化初始 HMI 页面;运行态由 `HmiNavigationService` 从初始页启动并处理本地 PageJump 导航
- HMI 已开放不绑定寄存器的 `Label` 和独立 `PageJump` 创建入口,属性区按控件类型显示绑定、按钮操作或目标页面
- HMI 已增加 AlarmList 和报警配置入口,支持 M ON、D 高限、D 低限,统一显示当前报警和确认状态;运行态无报警时隐藏,报警恢复后立即移除
- 离线执行器按工程顺序扫描所有启用逻辑,禁用草稿不阻止运行;轨迹按逻辑 ID 隔离,运行工作台可切换当前投影
- 梯形图已支持上升沿、下降沿、T/C 常开/常闭触点、非保持 TON、CTU/CTD、MOVE 和 ADD/SUB;边沿与计数都按上升沿扫描语义执行,TON 按 `steady_clock` 计算实际 ET
- T/C 是 `T0~T4000`、`C0~C4000` 的离线仿真内部资源;C 计数器的 CV 保存到 D 地址,PV 支持常量或 D 地址,复位使用 M 地址;T/C 资源本身不读写 M/D,也不进入 Modbus 轮询,同一资源只允许一个已启用驱动
- ADD/SUB 使用 `int32_t` 中间值并将结果饱和到 `-32768~32767`;越界只在运行轨迹的 `WordTraceValue::overflow` 标记,不停止扫描
- 工程已支持 M/D 地址注释和 `LadderRung` 网络注释;地址注释只保存地址与文本,不混入当前值和引用状态
- HMI 运行按钮已区分悬停、按下、释放和禁止写入外观,运行态不再显示编辑选中框,可写按钮使用手型光标
- PLC 轮询集合已扩展为 HMI、报警定义、梯形图和自由监控地址的并集,连接后可动态增删监控地址
- 已在真实 `COM3 / 9600 / 8E1 / 站号 1` 上完成只读联机验证,项目通信服务可完成 M/D 首次读取并进入真机运行前置状态
- 分支:`main`
- 核心软件闭环已经可用:HMI 编辑、结构化梯形图、离线仿真、工程 JSON、自由监控和 Modbus RTU 真机读写均已实现
- 当前工作区有未提交改动,主要是全项目数量边界、梯形图最大 11 列、无条件输出语义及配套测试和文档;开始工作前先看 `git status` 和相关 diff,不要重置
- `docs/二次开发/` 是已有未跟踪资料,不能因当前任务删除
- 当前没有代码阻塞;下一里程碑是实际设备 RUN 联动和交付整理

## 关键决策
## 已实现能力

- 工程格式继续使用 `1.0`,当前文件保存 HMI、报警定义和梯形图表达式树,不保留 `dataPoints` 结构及兼容代码
- 工程 `1.0` 直接要求 `alarmDefinitions`,不为缺少该字段的旧草稿增加迁移或默认值
- AlarmList 不直接保存报警逻辑,项目级 `AlarmService` 统一管理运行状态;当前不设置提示、警告、故障等级
- 当前 `1.0` 直接要求 `initialHmiPageId` 和 PageJump 的 `targetPageId`,开发阶段不为同版本旧草稿补默认值或迁移分支
- 页面跳转保存目标页面稳定 ID,不保存名称或数组索引;编辑器当前页面和当前逻辑属于会话状态,不写入工程
- 按钮操作是工程 `1.0` 的必填字段,不兼容或迁移缺少该字段的旧按钮数据
- 不读取旧的 `stages/branches` 结构,不添加旧格式迁移和兼容层
- 梯形图编辑结构而不是自由线段,避免悬空线、环路和多个输出路径
- 横线是保存 `columnSpan` 的恒真 `Wire` 叶子,竖线由 `Parallel` 边界派生,不保存竖线或像素坐标
- 网格仅作为编辑投影,不进入工程格式;服务层把连续范围操作原子转换为规范化表达式树
- 条件节点继续使用 `std::variant` 的独立配置类型,不向通用节点添加无关字段
- HMI、梯形图和通信共享 `M0~M4000`、`D0~D4000` 地址范围
- 已删除统一数据点表、主窗口数据点 Dock 和属性区数据点下拉框;M/D 注释作为独立的工程级地址元数据实现
- PLC 读写使用从 `0` 开始的原始 Modbus 地址
- HMI 写请求不乐观修改 PLC 缓存,由下一次轮询确认真实值
- 真机运行时 PLC 内部程序是控制源,软件逻辑执行器保持停止
- 真机运行不显示本地梯形图导通轨迹,避免把本地工程误表示为 PLC 内部程序的真实执行状态
- 自由监控表属于当前调试会话,不进入工程 `1.0` JSON,不重新引入数据点表
- 工程 `1.0` 直接要求顶层 `registerComments` 和每个网络的 `comment` 字段,不增加旧工程迁移或缺省兼容
- 50 ms Qt 定时器只调度离线扫描;每个 TON 不创建独立 Qt 定时器,真机模式继续停止整个软件逻辑执行器
- 项目梯形图不下载到 PLC;真机模式停止本地软件逻辑执行器,只轮询新指令实际引用的 M/D 地址
- `Wire` 不绑定寄存器且不进入 M/D 轮询;离线轨迹使用上游输入电流决定横线是否显示导通
- 工程格式为严格的 `1.0`,保存多 HMI 页面、初始页、报警定义、M/D 注释、多控制逻辑和结构化梯形图;不读取旧 `dataPoints` 或 `stages/branches` 格式
- HMI 现有 8 类控件:Button、Indicator、NumericDisplay、NumericInput、Label、PageJump、AlarmList、ProgressBar
- 梯形图使用 `Node / Wire / Series / Parallel` 表达式树和正交网格,不保存自由线段;前 10 列为条件区,输出最多位于第 11 列
- 梯形图支持 M 触点与上下沿、T/C 触点、普通/置位/复位线圈、TON、CTU/CTD、MOVE、ADD/SUB
- 只有输出、没有条件的网络按恒真执行;只有条件、没有输出的启用网络不能运行;空网络扫描时跳过
- 离线执行器按控制逻辑和网络顺序扫描,使用虚拟 M/D,并生成按逻辑 ID 隔离的运行轨迹
- 真机模式只使用 PLC 读回缓存,停止本地逻辑执行器,不下载本地梯形图,也不显示伪造的 PLC 梯形图轨迹
- 自由监控属于当前会话,支持 M/D 批量添加、去重、读取和单点写入,不写入工程 JSON
- PLC 通信使用异步 Qt Modbus RTU;轮询地址来自 HMI、报警、梯形图和自由监控的引用并集,写入后等待轮询确认,不乐观修改缓存
- 真机进入要求当前连接完成首次读取;通信故障撤销首读资格并退出真机运行,恢复通信后仍停留在编辑态
- 所有数量上限和超限处理见 `docs/用户使用/数量边界确认方案.md`,核心常量集中在 `app/src/domain/project_limits.h`

## 自动化验证
## 当前未提交改动

以下测试均已完成干净构建并通过:
- 对页面、控件、报警、逻辑、表达式、字符串、JSON、通信参数、轮询规模、日志和撤销历史补齐统一数量限制
- 单网络条件区从原 256 列收紧为 10 列,输出最多占第 11 列,横线跨度最多 10 列,表达式深度最多 20 层
- 编辑服务在第 11 个条件加入时原子回退;JSON 加载和领域校验不能绕过边界
- 无条件输出已经贯通领域校验、编辑服务、JSON 往返、离线执行器、轨迹和画布提示
- `AGENTS.md`、本 handoff 和 `docs/architecture.md` 已按长期规则、动态状态和稳定架构重新分工精简

- `domain_tests`
- `logic_editor_service_tests`
- `offline_simulation_service_tests`
- `project_management_tests`
- `hmi_editor_service_tests`
- `runtime_mode_service_tests`
- `plc_runtime_tests`
- `main_window_tests`
- `register_monitor_service_tests`
- `alarm_service_tests`
## 最近验证

`main_window_tests` 还覆盖 PLC 配置参数往返、独立配置弹窗入口,以及主窗口不再内嵌串口参数控件。
- 当前数量边界和无条件输出改动已通过:`domain_tests`、`logic_editor_service_tests`、`offline_simulation_service_tests`、`project_management_tests`、`main_window_tests`
- Qt Release 主程序已重新构建并链接成功
- `git diff --check` 通过;`runtime_mode_service_tests.cpp` 仅有既存 CRLF 转 LF 提示
- 真实 PLC 已使用 `COM3 / 9600 / 8E1 / 站号 1` 验证首次读取、断线恢复和重新首读
- 2026-08-19 已读取 `D4000=0`,写入并读回 `1`,随后恢复并读回 `0`;没有下载 PLC 程序
- 当前无条件输出改动只影响领域校验和离线执行器,真机模式不执行该逻辑,因此没有为此重复写 PLC

本轮边沿、TON 和注释实现增加了:上下沿一次扫描脉冲、不同逻辑同名边沿状态隔离、TON 到时与断开复位、多个 T 独立计时、网络顺序可见性、运行重启状态清理、T 引用完整性、M/D 与网络注释 JSON 往返、严格新字段缺失失败,以及 T 和纯注释地址不进入 PLC 轮询集合的覆盖。
## 待完成

本轮多页面和多逻辑实现增加了领域、服务、存储和主窗口集成覆盖:初始页约束、页面引用删除保护、页面/逻辑名称唯一性和顺序、禁用草稿逻辑、多逻辑扫描与轨迹隔离、严格 JSON `1.0` 必填字段、工程树选择切换、Label/PageJump 属性可见性、运行态页面跳转和逻辑轨迹选择。
1. 在确认接线、地址和负载安全后,将 PLC 切到 RUN,完成 HMI 与实际设备的完整联动测试
2. 整理最终演示工程、构建说明和验收记录
3. 复核当前未提交 diff,并按逻辑拆分提交;不要把 `docs/二次开发/` 等来源不明改动混入提交

2026-08-13 真机排查发现 `QModbusRtuSerialMaster::connectDevice()` 在当前 Windows
串口驱动上会同步触发 `ConnectedState`。旧实现随后再次写入 `Connecting`,导致已经发出的
读取响应因服务状态不是 `Connected` 而被丢弃,首次读取标志永远不能完成。现已把
`Connecting` 状态设置移动到 Qt 连接调用之前,并在真实 PLC 上验证首轮 `M0~M2`、`D0`
读取约 288 ms 完成,缓存值为 `M0=1`、`M1=1`、`M2=0`、`D0=0`。主窗口现在会显示
连接、首读完成和具体中文通信故障,生产代码中的业务错误提示已统一为中文,JSON 字段和协议标记保持不变。
## 下一会话起点

PLC 连接初始化会连续通知首读重置、正在连接和已连接。旧主窗口把每次通知都排队处理,
队列执行时读取到的已经是同一个最终状态,因此输出区会连续追加三条相同的“正在首读”。
现已在主窗口合并同一事件循环内的 PLC 状态刷新,并拒绝连续追加完全相同的输出消息。
真实 `COM3` 主窗口联调确认“正在首读”和“首次读取完成”各输出一次。

本轮已完整删除统一数据点业务,包括领域模型、服务、主窗口 Dock、属性区下拉框、JSON 字段和独立测试工程。当前开发阶段不保留 `dataPoints` 旧结构的迁移或兼容逻辑。主窗口测试明确验证已删除控件不存在,PLC 轮询测试改为验证从 HMI 实际绑定收集地址。

本轮梯形图编辑重构还验证了:固定网格渲染、水平/垂直正交布线、工具栏直接建立并联支路,以及连续范围 `A AND ((B AND C) OR D)` 的结构变换和非连续范围拒绝。

本轮信捷风格布线增加了结构化横线和可选竖线连接段。自动化覆盖横线跨度边界、JSON 往返、连续范围旁路和非连续范围拒绝、触点原位替换横线、删除后的表达式归一化与撤销,以及 `F11/F12/Shift+F11/Shift+F12` 主窗口交互。横线逻辑值恒真,但只有上游输入导通时才显示运行电流;竖线仍由 `Parallel` 派生,不引入自由线段或 PLC 地址。

本轮自由监控与运行工作台实现还验证了:地址字符串解析和边界、连续范围展开、去重和数量
上限、活动仓库切换、离线工作台三区域同时可见、真机工作台只显示 HMI 与自由监控、离线
监控读取虚拟 M/D,以及真机运行期间新增地址立即进入 PLC 聚合轮询集合。PLC 动态轮询在
读取请求空闲后应用新集合,连接首读完成后不会因新增监控地址而回退。

运行工作台的 HMI 和梯形图是独立于编辑画布的模型投影。此前同一页面或逻辑 ID 在编辑态
发生变化后,运行投影的 `setPageId()` / `setLogicId()` 会因 ID 未变化而跳过重建,导致新建
HMI 控件在离线运行工作台中显示为空白。现在每次同步运行对象都会显式重建两个投影,主窗口
测试会保留一个已配置 HMI 控件,并断言离线运行视图实际包含该控件,而不再只检查区域可见。

Qt Release 应用已完成干净构建,并执行了启动与正常退出冒烟验证。`git diff --check` 无空白错误。

本轮修正 PLC 故障恢复状态链。通信服务发生错误时会清除当前连接的首次读取资格,运行模式
服务同时校验 PLC 必须处于 `Connected` 且当前连接已完成首读;真机运行期间发生故障会自动
返回编辑态,避免使用旧连接的首读结果重新进入真机运行。故障状态下主窗口开放“PLC 重新
配置”,用户确认后自动断开并清理旧会话,再发起新连接。旧连接未完成的异步回复使用连接代次
隔离,不能污染重连后的新会话。

PLC 错误分类已拆到独立基础设施模块。连接阶段无法打开串口时直接报告端口不存在或被占用,
保持未连接状态;已连接后收到 `ConnectionError` 或意外进入 `UnconnectedState` 时统一报告本地
串口连接已中断,自动清理并转为未连接状态,不再依赖错误瞬间的 Windows COM 口枚举结果。

PLC 断开状态现已与底层 Qt Modbus 会话保持一致。连接中断类错误会停止轮询、隔离旧回复并
主动关闭 `QModbusRtuSerialMaster`;再次连接前还会防御性清理残留的底层连接状态,避免界面已
禁用“断开 PLC”但重连仍提示连接已经启动。串口无法打开时,输出区只通过 PLC 状态刷新记录
一次故障,连接操作不再重复追加同一错误,但仍保留警告对话框。

PLC 未响应或通信超时且本地串口仍打开时,服务停止正常轮询,每次探测失败后等待 2 秒再发送
一次单地址只读探测,不重复输出相同的超时日志。探测成功后进入 `Recovering` 状态,重新读取全部轮询地址;
完整首读完成后才恢复 `Connected`。故障和恢复过程始终停留在编辑态,不自动回到真机运行,
“断开 PLC”继续用于释放本地串口会话。

2026-08-14 使用真实 `COM3 / 9600 / 8E1 / 站号 1` 完成只读恢复测试。连续 5 次读取
`M0~M2、D0` 的完整首读耗时为 `304 / 304 / 288 / 271 / 287 ms`,平均约 `291 ms`;串口
进入已连接状态平均约 `51 ms`。真机运行时拔掉 PLC 侧 RS-485 A/B 后,服务进入 `Faulted`,
运行模式同步退回 `Editing`;接回线后约 `7.05 s` 进入 `Recovering`,再用约 `224 ms` 完成
完整首读并恢复 `Connected`,最终仍保持 `Editing`。测试结束后再次首读耗时 `288 ms`,确认
恢复探针已释放 COM3。底层 Qt SerialBus 单次复测中,连接状态约 `58 ms`,读取 `M0~M2`
到 `96 ms`,继续读取 `D0` 到 `128 ms`,单次读响应约 `32~38 ms`;生产服务完整首读的主要
额外耗时来自 `200 ms` 正常轮询节拍。本次测试全程没有写入 PLC,读回值为
`M0=0、M1=0、M2=0、D0=0`。

本轮新增自动化测试覆盖故障自动退回编辑态、首读资格撤销、故障后禁止重新进入真机、故障态
直接重连清理旧会话、主窗口重新配置入口和五类核心通信错误。`plc_runtime_tests`、
`runtime_mode_service_tests`、`main_window_tests` 均通过,Qt Release 工程完成干净构建。

本轮参照 TouchWin 按钮业务新增四种位操作:置 ON、置 OFF、取反、瞬时 ON。按钮操作
使用领域枚举并写入工程 `1.0` JSON;运行服务按 Pressed/Released 事件生成写值,瞬时 ON
按下写 1、释放写 0,其他模式释放不重复写入。属性面板只在选中按钮时显示操作模式,新建
按钮默认瞬时 ON。相关服务、离线闭环、JSON 往返与主窗口测试已覆盖。

HMI 图元现已分离编辑选择与运行输入状态。运行按钮悬停时改变底色,按下时按钮面下移并
加深,释放后回弹;禁止写入时显示灰色且不接收鼠标写操作。运行态关闭图元选择,避免点击
后出现编辑选中框。主窗口测试使用真实鼠标按下和释放事件验证瞬时按钮写入、视觉状态切换、
手型光标、禁止写入和无运行选中框。

本轮新增最小报警闭环。项目级报警定义支持 M 位为 ON、D 值大于等于阈值、D 值小于等于
阈值;AlarmService 只维护当前报警的发生时间和确认状态,报警恢复后立即移除,多个 AlarmList 共享状态。
报警配置和列表展示分别由独立 Qt Designer 对话框与 HMI 图元完成,主窗口只协调服务。报警
地址已进入 PLC 聚合轮询。工程版本保持 `1.0`,`alarmDefinitions` 为必填字段,不增加兼容分支。

2026-08-17 使用 `COM3 / 9600 / 8E1 / 站号 1` 完成报警功能后的真机只读冒烟,成功读取
`M0~M2 = OFF`、`D0 = 0`,测试结束后正常关闭串口。本次没有执行任何 PLC 写操作。

本轮新增 CTU/CTD、MOVE、ADD/SUB 离线逻辑和编辑配置。领域校验限制 C/T 资源及 M/D 地址
范围,C 计数器要求唯一驱动和已绑定 C 触点;计数按上升沿执行,CV 通过 D 地址读写,CTU
复位为 0、CTD 复位装载 PV,ADD/SUB 越界采用有符号 16 位饱和并记录溢出轨迹。JSON 保存
加载、离线执行、主窗口入口和真机轮询地址覆盖已通过,项目 Release 构建通过。真机仍只验证
M/D 通信,不执行或下载这些本地梯形图指令。

本轮在不修改领域、服务和通信规则的前提下拆分主窗口三个界面控制器。`main_window.cpp` 从
约 2200 行降至约 1300 行,原工程树、属性面板和运行刷新实现已从主窗口移除,没有保留重复
死代码。Qt Release 生产工程和 `main_window_tests` 均使用独立构建目录重新构建并通过,
`git diff --check` 无空白错误。

2026-08-17 使用 `COM3 / 9600 / 8E1 / 站号 1` 完成本轮重构后的真机只读复核,成功读取
`M0~M2 = OFF`、`D0 = 0`,测试结束后正常关闭串口,全程没有执行 PLC 写操作。

本轮建立无 Qt 依赖的 HMI 控件描述注册表,将 Button、Indicator、NumericDisplay、
NumericInput、Label、PageJump 和 AlarmList 的工程稳定类型名、界面名称、默认 ID 前缀、
默认文字、默认尺寸、绑定规则和运行值类型集中维护。领域校验、编辑创建、运行读取、JSON
编解码和属性面板已改为查询注册表,原有重复类型辅助函数和分支表已删除;专用绘制与交互
行为保持原模块边界。注册表完整性测试覆盖枚举和稳定名称双向查询、名称唯一性、默认尺寸、
M/D 绑定与运行值匹配及未知类型拒绝。10 个自动化测试工程全部重新构建并通过,Qt Release
主工程完成独立构建,`git diff --check` 无空白错误。

本轮新增 HMI ProgressBar 控件。进度条使用 D 区 Word 绑定,保存最小值、最大值和是否显示百分比,
运行态将读回值钳制到配置范围后绘制水平填充,不接收鼠标写入。属性面板支持范围编辑,JSON
严格要求 `minimumValue`、`maximumValue` 和 `showValue`,领域测试覆盖无效范围、越界映射和
M/D 绑定边界;主窗口测试覆盖创建、属性编辑、运行绘制和只读交互。

此前的 7 种控件已升级为 8 种控件,ProgressBar 的稳定类型名为 `progressBar`,默认范围为
`0~100`,默认尺寸为 `240x36`。

2026-08-17 使用 `COM3 / 9600 / 8E1 / 站号 1` 完成本轮注册表重构后的真机只读复核,
成功读取 `M0~M2 = OFF`、`D0 = 0`,测试结束后正常关闭串口,全程没有执行 PLC 写操作。

2026-08-18 完成本轮信捷风格横线/竖线功能后的生产通信服务只读回归。`COM3 / 9600 / 8E1 / 站号 1` 在约 288 ms 完成 `M0~M2` 和 `D0` 首次读取,结果为 `M0=0`、`M1=0`、`M2=0`、`D0=0`;真机模式前置状态切换成功,并在约 407 ms 正常断开。探针只调用读取、模式切换和断开接口,没有执行 PLC 写入或程序下载。Qt Release 主程序也已完成最新构建、窗口启动和退出码 0 的正常关闭冒烟。

## 待完成事项

1. 在 PLC STOP 状态确认 COM2 参数和 RS-485 A/B 接线
2. 用安全测试地址读取 `M0`、`D0`
3. 写入 `M0`、`D0` 并由 PLC 监控和下一次轮询确认
4. 真机复核电脑侧 USB 转串口拔除、PLC 侧 A/B 断开、超时和重新连接日志及恢复流程
5. PLC 切换 RUN 后执行完整 HMI 与设备联动测试
6. 整理测试记录、构建说明和演示工程

## 建议下一步

运行 Release 应用,配置实际 COM 口、站号、波特率和校验方式,在 PLC STOP 状态完成 `M0`、`D0` 首次安全读写。未确认设备程序与负载安全前不要在 RUN 状态写控制地址。
1. 运行 `git status --short`,确认当前未提交和未跟踪文件仍在
2. 根据任务读取 `docs/architecture.md`、数量边界文档或硬件/Modbus 要点,不再从旧开发日志推断现状
3. 修改代码后运行受影响测试和 Release 构建;涉及真机通信时按 `AGENTS.md` 做 COM3 读写及恢复验证

+ 90
- 156
docs/architecture.md 파일 보기

@@ -1,26 +1,23 @@
# 综合平台编程器架构

## 目标与边界
## 目标与范围

项目采用 UI、领域、服务和基础设施分层。HMI 编辑、结构化梯形图、离线仿真和真机联机共用工程模型,UI 不直接依赖文件系统、串口或 Modbus 实现
项目是单工程、单 PLC、多 HMI 页面和多控制逻辑的 Qt 桌面编程器,提供 HMI 编辑、结构化梯形图、离线仿真和 Modbus RTU 真机联机

当前目标是完成单工程、单 PLC、多 HMI 页面和多控制逻辑的基础设备控制闭环,不生成、编译或下载 PLC 程序,不实现 XDPPro 的完整 POU/指令集,也不提供任意自由画线
本项目不生成、编译或下载 PLC 程序,不读取 PLC 内部程序与网络轨迹,不实现完整 XDPPro 指令集,也不支持任意自由画线。真机运行时 PLC 内部程序是唯一控制源,PC 只读写工程实际引用的 M/D 地址

## 目录与依赖
## 分层与依赖

```text
app/
├── integrated_platform.pro qmake 工程入口
├── src/
│ ├── main.cpp 对象创建与依赖注入
│ ├── ui/ Qt Widgets 视图和界面控制器
│ ├── domain/ 工程、HMI、梯形图和寄存器模型
│ ├── services/ 编辑、仿真、运行模式和通信契约
│ └── infrastructure/ JSON、PLC 缓存和 Qt Modbus RTU 实现
└── tests/ 单元测试与集成测试工程
app/src/
├── domain/ 无 Qt 依赖的模型、值对象、校验和仓库接口
├── services/ 编辑、工程、仿真、运行模式和通信契约
├── infrastructure/ JSON、PLC 缓存、Qt Modbus RTU 实现
├── ui/ Qt Widgets 视图、对话框和界面控制器
└── main.cpp 对象创建与依赖注入
```

生产代码保持以下依赖方向:
生产代码依赖方向固定为:

```text
UI -> Services -> Domain
@@ -28,183 +25,120 @@ Infrastructure -> Services contracts + Domain
main.cpp -> UI + Services + Infrastructure
```

- `domain` 不依赖 Qt、串口和文件系统
- `services` 编排用例,通过接口使用工程存储、寄存器和 PLC 通信
- `infrastructure` 实现 JSON 工程文件、PLC 缓存和异步 Modbus RTU
- `ui` 只提交操作并展示模型、运行轨迹和错误状态
- `main.cpp` 负责组合依赖,不承载业务规则
- `domain` 不依赖 Qt、文件系统或串口
- `services` 执行业务用例和原子编辑,通过接口使用工程存储与 PLC 通信
- `infrastructure` 实现 JSON 工程存储、PLC 缓存和异步 Modbus RTU
- `ui` 只提交命令并投影模型、轨迹和错误,不直接读写串口或承载业务校验
- `main.cpp` 只组合对象,不实现业务规则

主窗口的菜单、工具栏和属性表单由 Qt Designer 的
`main_window.ui` 定义。界面交互按职责由三个 UI 控制器协调:
主窗口静态布局由 `main_window.ui` 管理。UI 协调分为:

- `ProjectWorkspaceController` 负责工程树、页面/逻辑会话选择和工程对象刷新
- `PropertyPanelController` 负责 HMI/梯形图选择状态、属性面板和编辑器信号
- `RuntimePanelController` 负责运行监控工作台、运行值刷新和离线执行器反馈

`MainWindow` 负责组合这些控制器、处理窗口级菜单和跨区域模式切换,不重复实现工程校验或寄存器业务规则。真实 PLC 的串口与轮询参数由独立的 `PlcConnectionDialog` 收集。

## 核心领域模型

| 文件 | 职责 |
| 组件 | 职责 |
| --- | --- |
| `register_address.*` | 定义 `M0~M4000`、`D0~D4000` 地址和值域规则 |
| `register_repository.*` | 定义 M/D 统一读写接口和离线内存实现 |
| `active_register_repository.*` | 在虚拟仓库与 PLC 缓存仓库之间切换当前数据源 |
| `register_monitor_model.*` | 保存当前调试会话的自由监控 M/D 地址集合 |
| `alarm_model.*` | 定义项目级 M ON、D 高限和 D 低限报警条件 |
| `hmi_model.*` | 定义页面、控件、布局和寄存器绑定 |
| `hmi_control_registry.*` | 集中描述 HMI 控件的稳定名称、默认值、绑定规则和运行值类型 |
| `control_logic_model.*` | 定义梯形图表达式树、边沿/触点/比较/TON 节点和输出线圈 |
| `project_model.*` | 聚合 HMI 页面、报警定义和控制逻辑并执行工程校验 |
| `runtime_state.*` | 定义编辑、离线运行和真机运行状态机 |

工程文件格式保持 `1.0`。当前 `1.0` 直接保存有序 `hmiPages`、`initialHmiPageId`、有序 `alarmDefinitions`、工程级 `registerComments`、有序 `controlLogics` 和梯形图表达式树,不读取其他结构,也不包含迁移和兼容代码。加载必须先完成 JSON 解析、版本检查和领域校验,成功后才能替换当前工程;保存使用原子提交。完整字段定义见 `docs/工程格式说明.md`。
| `ProjectWorkspaceController` | 工程树、当前页面/逻辑会话选择和编辑对象刷新 |
| `PropertyPanelController` | HMI/梯形图选择、属性表单和编辑服务调用 |
| `RuntimePanelController` | 运行工作台、运行值刷新、仿真轨迹和故障反馈 |
| `MainWindow` | 窗口级动作、工程文件操作、控制器组合和模式切换 |

`HmiPage.id` 和 `ControlLogic.id` 是稳定内部标识,名称可修改,数组顺序分别决定工程树页面顺序和离线逻辑扫描顺序。`MainWindow` 保存 `current_hmi_page_id_` 与 `current_logic_id_` 作为编辑会话选择,`ProjectWorkspaceController` 负责将它们同步到工程树和编辑画布,它们不进入 JSON。`Project.initialHmiPageId` 是运行态每次启动 HMI 时使用的持久化初始页,与编辑器当前正在查看的页面相互独立。
## 工程模型与持久化

`Project::validate()` 允许未绑定控件、待配置节点和未完成网络作为编辑草稿保存。`Project::validateForRunning()` 额外拒绝未绑定 HMI、待配置节点、空条件和缺少输出的网络,并检查已启用逻辑中的 T 触点必须有唯一的 TON 驱动。
```text
Project
├── metadata
├── hmiPages[] + initialHmiPageId
├── alarmDefinitions[]
├── registerComments[]
└── controlLogics[]
└── rungs[]
├── condition
└── output
```

HMI 控件公共描述由无 Qt 依赖的 `hmi_control_registry` 模块统一提供。当前 8 种控件的工程稳定类型名、
界面名称、默认 ID 前缀、默认文字、默认尺寸、M/D 绑定规则和运行值类型只在注册表中定义一次。
领域校验、编辑服务、运行服务、JSON 存储和属性面板共同查询该描述;控件绘制、按钮写入、
PageJump 导航、AlarmList 展示和 ProgressBar 范围计算等专用行为仍留在各自模块,不进入公共描述表。
- 页面、逻辑和节点使用稳定 ID;名称可修改,数组顺序决定工程树顺序和离线扫描顺序
- 当前编辑页面和当前逻辑是 UI 会话状态,不写入工程
- HMI 控件类型、默认值、绑定规则和运行值类型由无 Qt 依赖的 `hmi_control_registry` 统一描述
- 需要寄存器的 HMI 控件、报警条件和梯形图节点直接保存 M/D 地址;项目不再维护统一 `dataPoints` 表
- `registerComments` 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询

HMI 按钮操作作为工程 `1.0` 的强类型字段保存,支持 `置 ON`、`置 OFF`、`取反` 和
`瞬时 ON`。新建按钮默认使用瞬时 ON;按钮按下和释放事件由 UI 转交运行服务解释,UI
不直接决定写入值。当前版本不迁移缺少按钮操作字段的旧工程文件。
工程 JSON 格式固定为严格的 `1.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。当前版本不读取旧 `dataPoints`、`stages/branches` 或缺少必填字段的同版本草稿。字段定义见 `docs/工程格式说明.md`。

`Label` 是不绑定 M/D 的固定文本控件。`PageJump` 是独立控件,使用强类型 `targetPageId` 引用目标页面稳定 ID,同样不绑定 M/D。编辑态点击 PageJump 只执行选择和拖动;离线或真机运行态点击后由 `HmiNavigationService` 在本地切换运行页面,不发出 PLC 写请求,因此 PLC 通信写权限不可用时也不影响页面导航。被 PageJump 引用的页面不能删除,初始页必须先切换后才能删除,工程至少保留一个页面。
校验分两级:

`AlarmList` 是不绑定单个寄存器的运行状态投影。项目级 `AlarmDefinition` 只支持 M 位为 ON、D 值大于等于阈值和 D 值小于等于阈值三种条件,不设置提示、警告、故障等级。`AlarmEditorService` 负责定义增删改,`AlarmService` 统一读取活动寄存器仓库并维护当前报警的发生时间和确认状态,所有页面上的 AlarmList 共享这份状态。报警条件恢复后立即删除对应运行记录;运行态没有报警时 AlarmList 隐藏,出现报警时显示并置顶。返回编辑态时清空运行记录,工程 JSON 只保存报警定义。
- `validate()` 保证可保存结构合法,允许未绑定控件、待配置节点和未完成网络作为编辑草稿
- `validateForRunning()` 检查运行所需绑定和已启用逻辑;禁用草稿不阻止运行

`ProgressBar` 绑定 D 区字地址,以 `minimumValue` 和 `maximumValue` 将读回值钳制并映射到
`0~100%`。进度条配置还保存 `showValue`,最小值必须严格小于最大值;控件只读,不产生 PLC
写请求。运行值读取沿用 `HmiRuntimeService` 的 Word 读取契约,绘制层只接收运行值和领域配置。
数量常量集中在 `domain/project_limits.h`,最终业务口径见 `docs/用户使用/数量边界确认方案.md`。领域、服务、JSON 和 UI 入口必须使用同一组常量,不能各自维护数字。

## 结构化梯形图

网络条件采用递归表达式
网络条件保存为规范化表达式树

```text
ConditionExpression
├── Node 单个触点或比较条件
├── Wire 恒真的横线叶子,保存网格列跨度
├── Series 子表达式依次串联,执行 AND
└── Parallel 子表达式并联,执行 OR
├── Node 触点、边沿或比较条件
├── Wire 无地址横线,保存条件区列跨度
├── Series 子表达式执行 AND
└── Parallel 子表达式执行 OR
```

例如 `A OR (B AND C)` 表达为:

```text
Parallel
├── Node(A)
└── Series
├── Node(B)
└── Node(C)
```

`LogicEditorService` 支持在当前节点或横线后串联插入,并为单个叶子或同一网络中的连续逻辑范围建立并联支路。例如选择 `B、C` 后建立并联会得到 `A AND ((B AND C) OR D)`。横线使用 `Wire` 叶子保存 `1~256` 列跨度;围绕连续范围建立竖线时,服务层原子创建一个同跨度的横线旁路和 `Parallel` 容器。服务层负责校验范围连续性和子表达式完整性,拒绝跨网络、非连续或截断内部子表达式的选择。删除后会归一化空容器和只有一个子项的容器,防止无效嵌套进入执行器。

`LogicEditorWidget` 将表达式树投影到固定行列网格,串联节点横向占列,并联分支纵向占行,所有连接只使用水平线和竖线。触点、横线和竖线连接段都可以在画布选择;竖线连接段映射到对应的并联子表达式,删除竖线会删除该支路并归一化结构。内部 `Series / Parallel` 容器仍不作为通用 UI 对象暴露。竖线来自 `Parallel` 边界,不保存为独立领域对象;网格和像素坐标只是编辑投影,不持久化自由线段,因此不会产生悬空线、环路和多输出路径。

工具栏中的常开、常闭、上升沿、下降沿、T 触点和比较条件在没有选择时追加到当前网络末尾,选中一个条件节点时插入到该节点后;选中横线后使用条件动作会原位替换横线。`F11` 在所选条件或横线后插入横线,没有选择时追加;`F12` 为所选连续范围建立横线旁路和竖线连接;`Shift+F11` 和 `Shift+F12` 分别删除所选横线和竖线支路。并联支路按钮继续用于新增可配置条件分支。新增节点保持待配置状态,并自动选中供属性区继续编辑。

条件节点使用 `std::variant` 保存独立配置类型,当前包含 M 常开/常闭触点、M 上升沿/下降沿触点、T 常开/常闭触点、C 常开/常闭触点和 D 值与常量比较。输出支持普通、置位、复位 M 线圈、非保持 TON、CTU/CTD、MOVE 以及 ADD/SUB。边沿节点在每个扫描周期比较当前 M 值与该逻辑节点自己的上一扫描值,只产生一个扫描周期的脉冲。T 地址是 `T0~T4000` 的离线资源,C 地址是 `C0~C4000` 的离线计数器资源,两者都不是 M/D 地址。新增节点类型时应增加独立配置类型及校验、存储和执行分支,不能向通用节点堆叠无关字段。

CTU/CTD 的 C 编号只标识软件执行器中的计数器实例;计数当前值 CV 显式保存到 D 地址,PV 支持常量或 D 地址,复位输入使用 M 地址。CTU 和 CTD 都只在网络条件的上升沿执行一次计数,C 触点读取对应计数器的完成状态。C 资源在已启用逻辑中必须只有一个计数指令驱动,C 触点必须能够找到该驱动,因此不会把 C 误当作需要 Modbus 读取的外部寄存器。

## 执行与运行反馈
输出是网络最右侧的单独节点。`LogicEditorService` 把串联、并联、旁路、替换和删除转换为表达式树操作,失败时回退整个编辑;`LogicEditorWidget` 只把树投影成正交网格。竖线由 `Parallel` 边界派生,网格坐标和自由线段不持久化,因此领域模型不会产生悬空线、环路或多输出路径。

`SoftwareLogicExecutor` 按 `controlLogics` 数组顺序扫描所有已启用逻辑,再按网络顺序递归求值 `Node / Wire / Series / Parallel`。`Wire` 的逻辑值恒真,但导通轨迹等于其输入电流,因此上游断开时横线不会错误显示为导通。禁用逻辑仅保留结构校验,未完成的禁用草稿不会阻止离线运行。普通线圈每周期写入网络结果,置位和复位线圈只在网络成立时写入;前面逻辑或网络的写入对后面逻辑和网络在同一扫描周期立即可见
单网络前 10 列为条件区,输出最多位于第 11 列;横线只能占条件区。第 11 个条件、过深表达式或过多并联行会在编辑和加载阶段被拒绝,具体数值以数量边界文档为准。

上升沿和下降沿是标准的一次扫描脉冲,运行状态按逻辑 ID 和节点 ID 隔离。TON 是正常非保持接通延时:输入接通后记录 `steady_clock` 起点,`ET` 使用实际经过的毫秒数,达到 `PT` 后 `Q` 为真;输入断开立即清零 `ET/Q`。TON 的输出状态保存在离线执行器的 T 资源表中,T 触点只读取对应 TON 的 `Q`。CTU 计数值在 `0~32767` 范围内递增,达到 PV 后保持完成;CTD 复位时装载 PV,在 `0~32767` 范围内递减,达到 0 后保持完成。ADD/SUB 使用有符号 16 位 D 值,先以更宽的中间类型计算,超出 `-32768~32767` 时饱和到边界并在运行轨迹标记 `overflow`,不会因单次算术溢出停止整个扫描。MOVE、ADD/SUB 只有网络条件成立时才写入目标 D,并在同一扫描周期对后续网络可见。软件不为每个 TON 创建 Qt `QTimer`,现有 50 ms `QTimer` 只负责扫描调度,因此不会与 Qt 事件循环或 PLC 轮询形成额外定时器线程。
网络运行语义:

每次扫描生成按逻辑 ID 分区的 `LogicTraceSnapshot`,记录节点、表达式、网络和线圈的导通状态,避免不同逻辑中重复的 `rung-1`、`contact-1` 相互覆盖。运行工作台的逻辑下拉框只切换当前轨迹投影,所有已启用逻辑仍持续执行。逻辑画布以绿色显示导通节点和路径;执行错误携带逻辑、网络和节点上下文,画布选择并以红色突出故障位置。
- 空网络不执行
- 有条件但没有输出的已启用网络不能运行
- 有输出但没有条件的网络是恒真网络,每次扫描执行输出
- `Wire` 的逻辑值为真,但运行轨迹是否导通仍取决于上游电流

`OfflineSimulationService` 以 50 ms 目标周期驱动扫描,启动时复制逻辑快照、清空虚拟 M/D 并重置边沿和 T 运行状态。故障或停止后清理这些运行状态;重新启动不会继承上次边沿历史或 TON 的 ET/Q。该周期只提供调度精度,不提供硬实时保证。
节点配置使用 `std::variant` 的独立类型。当前条件包括 M 触点与上下沿、T/C 触点和 D 比较;输出包括普通/置位/复位 M 线圈、TON、CTU/CTD、MOVE、ADD/SUB。新增指令必须增加独立配置、校验、JSON、执行、编辑和测试分支,不能向通用节点堆叠无关字段。

## M/D 地址使用
`SoftwareLogicExecutor` 按 `controlLogics` 和网络数组顺序扫描已启用逻辑,前面网络的 M/D 写入在同一扫描周期对后面网络可见。T/C 是离线执行器内部资源,不映射 PLC T/C 区,也不进入 Modbus 轮询;计数器 CV/PV 等显式 M/D 引用仍使用统一仓库。扫描轨迹按逻辑 ID 分区,避免不同逻辑中的重复节点 ID 相互覆盖。

HMI 控件和梯形图节点直接绑定 `M0~M4000`、`D0~D4000` 地址,不要求预先创建数据点。地址是程序和通信的真实标识,属性面板只编辑区域与地址。
## 数据源与运行模式

M/D 是外部地址类型:M 保存位输入、复位信号和线圈结果,D 保存字操作数、MOVE/ADD/SUB 目标以及计数器 CV。T/C 是软件逻辑资源类型,只在离线模式维护运行状态,不在工程中映射为 PLC 的 T/C 区寄存器;`Wire` 只是无地址的逻辑结构,不进入 M/D 引用收集和 PLC 轮询。项目不把梯形图下载到 PLC,因此真机模式只通信访问节点实际引用的 M/D 地址。
运行界面只访问 `RegisterRepository`。`ActiveRegisterRepository` 根据模式切换实际数据源:

工程级 `registerComments` 只保存 M/D 地址和注释文本,作为地址元数据供编辑器和注释对话框使用,不保存当前值、引用位置或缓存状态,也不会因为存在注释而加入 PLC 轮询。`LadderRung.comment` 保存单个网络的说明文本。两者在 JSON 中分开建模,不能重新引入混合“名称、注释、当前值、引用位置”的数据点表。

## 自由监控与运行工作台

自由监控是当前调试会话中的寄存器诊断工具,不属于工程模型,也不写入工程 JSON。用户可输入
`M0~M4000` 或 `D0~D4000`,并按连续个数批量添加地址。监控表最多保留 64 个去重地址,M
值显示为 `ON/OFF`,D 值按带符号 16 位十进制显示。

`RegisterMonitorService` 负责地址解析、范围展开、去重、增删和通过活动寄存器仓库读取值。
`FreeMonitorWidget` 只投影服务结果,不直接访问虚拟仓库、PLC 缓存或串口。监控地址在离线和
真机运行期间均可增删,因为它们属于调试操作而不是工程结构编辑。

主窗口在进入运行模式后自动打开运行监控工作台:

| 模式 | 同时显示 | 统一数据源 |
| --- | --- | --- |
| 离线运行 | 可导航 HMI、当前所选逻辑轨迹、自由监控 | 虚拟 M/D |
| 真机运行 | HMI、自由监控 | PLC 缓存 |

真机模式不显示本地梯形图导通轨迹。当前项目不能读取 PLC 内部程序和网络执行轨迹,本地
梯形图也不保证与 PLC 中的程序一致,因此不得把本地条件推演伪装成 PLC 实际运行状态。

## 寄存器与 Modbus RTU

`HmiRuntimeService` 和运行界面只访问 `RegisterRepository`。`ActiveRegisterRepository` 在两种数据源之间切换:

- 离线运行使用 `VirtualRegisterRepository`
- 真机运行使用 `PlcRegisterRepository` 的最近一次有效缓存
```text
编辑:UI -> Editor Services -> ProjectService -> Project
└── ProjectStorage -> JSON

`PlcCommunicationService` 使用 `QModbusRtuSerialMaster` 异步连接、读取和写入。轮询集合由
HMI 引用、报警定义、梯形图中的 M/D 引用和自由监控地址合并而成,去重后按区域合并相邻地址。
T/C 触点、TON、CTU/CTD 的 C 资源、横线和工程级注释不会进入该集合;只有计数器 CV、PV 的 D 引用、复位 M 以及 MOVE/ADD/SUB 的 M/D 条件和操作数会进入集合。M 使用线圈,D 使用保持寄存器;所有 Modbus 地址均为从 `0` 开始的原始地址。
离线:Runtime UI -> ActiveRegisterRepository -> VirtualRegisterRepository
^
OfflineSimulationService(SoftwareLogicExecutor)

PLC 连接后可以动态增删自由监控地址。存在读取请求时,新的轮询集合在当前响应处理完成后
应用,旧响应仍按发出时的读取块解析。连接级首次读取完成后,动态增加地址不会撤销真机
运行资格;新增地址自身在首次读回前显示“等待读取”。如果连接首次读取尚未完成,新的完整
轮询集合会共同参与首次读取判定。
真机:Runtime UI -> ActiveRegisterRepository -> PlcRegisterRepository(cache)
^
PLC <-> PlcCommunicationService <------------------┘
```

HMI 写入只表示异步请求已受理,不乐观修改 PLC 缓存,实际值由后续读回确认。协议错误或断线会停止轮询、保留最后一次有效值并进入故障状态,错误通过运行模式服务交给 UI 展示。
PLC 未响应或通信超时且本地串口仍然打开时,通信服务停止正常高频轮询,每次探测失败后等待
2 秒,再只读探测一次当前轮询集合的首个地址。探测失败不重复打印同一超时日志;探测成功后进入恢复状态,
重新读取全部轮询地址并重建当前连接的首次读取资格,完整首读完成后才恢复为已连接状态。
| 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 |
| --- | --- | --- | --- | --- |
| 编辑态 | 是 | 无运行数据源 | 停止 | 运行态先退出 |
| 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 |
| 真机运行 | 否 | PLC 读回缓存 | 停止 | PLC 已连接并完成本次首读 |

通信故障会立即撤销当前连接的首次读取资格。若故障发生在真机运行态,运行模式服务自动返回
编辑态,PLC 最后一次有效缓存仍保留用于日志和故障诊断,但不能作为再次进入真机运行态的依据。
重新进入真机运行必须等待当前串口会话恢复或重新连接 PLC,并重新完成首次读取。
离线和真机不能直接互切。进入真机不复制或下发离线值,也不显示本地梯形图导通轨迹。

通信超时后的自动探测只恢复通信和缓存,不自动恢复真机运行。接线或 PLC 供电恢复后,应用
完成新的首次读取并继续停留在编辑态,由用户手动决定是否重新进入真机运行。“断开 PLC”
在超时和恢复过程中保持可用,用于关闭并释放仍然打开的本地串口会话。
自由监控是会话级诊断工具,不属于 `Project`,不写入 JSON。它通过活动仓库读取和单点写入 M/D;真机写入仍必须经过连接与首读门槛。

通信服务根据 Qt Modbus 错误和串口是否曾成功打开区分连接阶段失败与运行中连接中断。连接阶段
无法打开串口时直接保持未连接状态;已经连接后收到 `ConnectionError` 或意外进入
`UnconnectedState` 时,统一报告本地串口连接中断,自动清理会话并进入未连接状态。错误日志
独立保留,不会为了显示故障而继续把串口会话表示为已连接。
## Modbus RTU 通信

## 运行模式边界
`PlcCommunicationService` 使用 `QModbusRtuSerialMaster` 异步连接、轮询和单点写入,UI 线程不等待串口响应。Modbus 地址使用从 `0` 开始的原始地址。

| 模式 | 允许编辑 | 寄存器来源 | 软件逻辑执行器 | 进入约束 |
| --- | --- | --- | --- | --- |
| 编辑态 | 是 | 无运行数据源 | 停止 | 运行态必须先退出 |
| 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 |
| 真机运行 | 否 | PLC 读回缓存 | 停止 | PLC 已连接且首次读取完成 |
轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。T/C 内部资源、横线和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再应用新集合。

离线运行与真机运行不得直接互切。进入真机模式不复制或下发离线值;PLC 内部程序是控制源,PC 端软件逻辑执行器必须停止
PLC 缓存只保存最后一次成功读回值。写请求受理后不乐观修改缓存,界面等待后续轮询确认真实值。真机运行资格属于当前连接代次:必须完成完整首读,旧连接的异步回复不能污染重连后的状态。

## 测试边界
通信状态包括 `Disconnected / Connecting / Connected / Recovering / Faulted`。连接失败或物理断线会关闭串口会话并进入 `Disconnected`;超时、协议或读写故障会停止正常轮询、保留最后有效缓存并进入 `Faulted`。所有通信故障都会撤销首读资格;若当时处于真机运行,运行模式服务立即返回编辑态。可恢复超时探测成功后进入 `Recovering` 并重新完整首读,但不会自动重新进入真机运行。

| 优先级 | 测试对象 | 重点内容 |
| --- | --- | --- |
| 高 | 领域模型 | 地址边界、表达式校验与归一化、工程唯一性 |
| 高 | 编辑服务 | 嵌套串并联插入、删除和地址配置 |
| 高 | 执行与模式服务 | 递归求值、轨迹、离线闭环、仓库切换和首次读取约束 |
| 高 | 工程存储 | `1.0` 往返、缺失字段、非法结构和不支持版本 |
| 中 | PLC 通信 | 缓存有效性、地址集合、异步读写和错误反馈 |
| 中 | UI | 编辑、运行切换、选择和错误定位工作流 |
| 冒烟 | 真机 | STOP 状态安全读写 M0/D0,随后 RUN 联动 |
## 扩展规则

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

+ 2
- 2
docs/工程格式说明.md 파일 보기

@@ -137,7 +137,7 @@ ProgressBar 使用绑定 D 地址的有符号 16 位字值,并将其限制在
{"id": "wire-1", "kind": "wire", "columnSpan": 2}
```

`columnSpan` 是必填整数,范围为 `1~256`。它表示横线占用的逻辑网格列数,不是像素长度。竖线不保存为独立对象,而是由 `Parallel` 表达式的支路边界派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下:
`columnSpan` 是必填整数,范围为 `1~10`。它表示横线占用的条件区逻辑网格列数,不是像素长度。单个网络前 10 列是条件区,第 11 列固定放输出指令。竖线不保存为独立对象,而是由 `Parallel` 表达式的支路边界派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下:

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

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

工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑完整;禁用草稿逻辑不阻止运行。
工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑包含输出指令;禁用草稿逻辑不阻止运行。只有输出而没有条件的网络表示左侧电源线直接接通,运行时按恒真条件执行。

+ 93
- 0
docs/用户使用/数量边界确认方案.md 파일 보기

@@ -0,0 +1,93 @@
# 数量边界确认方案

这份表就是本项目最终采用的数量规则。表里的数字是确定值,不是建议值。达到上限还能正常使用,再多一个就会被软件拒绝;只有日志这类临时信息会自动删除最旧内容。

## 1. 必须做硬限制的项目

| 类别 | 项目 | 最小值 | 最大值 | 超出后怎么处理 | 为什么必须限制 |
| --- | --- | ---: | ---: | --- | --- |
| PLC 软元件 | M 地址 | 0 | 4000 | 拒绝保存、加载或连接使用 | 项目需求已经明确限定 |
| PLC 软元件 | D 地址 | 0 | 4000 | 拒绝保存、加载或连接使用 | 项目需求已经明确限定 |
| PLC 软元件 | T 定时器编号 | 0 | 4000 | 拒绝保存或运行 | 和本项目 M/D 范围保持一致,低于信捷硬件范围 |
| PLC 软元件 | C 计数器编号 | 0 | 4000 | 拒绝保存或运行 | 和本项目 M/D 范围保持一致,低于信捷硬件范围 |
| 数值 | D 字、比较值、阈值、普通常量 | -32768 | 32767 | 编辑和加载时拒绝越界值,运算溢出时压到边界值 | D 是有符号 16 位数据 |
| 计数器 | 常量预设值 PV | 0 | 32767 | 负数或更大值直接拒绝 | 16 位普通计数不能使用负预设值 |
| 计数器 | D 地址提供的 PV、当前值 CV | 0 | 32767 | 读到负数按 0 使用,超过范围按 32767 使用 | 计数语义不能出现负数量 |
| 定时器 | TON 预设时间 | 1 ms | 86400000 ms | 拒绝保存或运行 | 0 没有定时意义,最大值固定为 24 小时 |
| 工程文件 | 单个 JSON 文件大小 | 0 | 16 MiB | 读取 JSON 前直接拒绝 | 防止超大文件一次性占满内存 |
| HMI | 页面数量 | 0 | 128 | 第 129 个页面不能创建或加载 | 控制工程树和运行页面的内存开销 |
| HMI | 单页控件数量 | 0 | 512 | 第 513 个控件不能创建或加载 | 保证编辑画布和运行刷新可控 |
| HMI | 页面宽度、高度 | 1 | 8192 | 拒绝保存或加载 | 防止异常尺寸撑大画布 |
| HMI | 控件宽度、高度 | 1 | 8192 | 拒绝保存或加载;控件还必须完整放在页面内 | 防止异常尺寸和越界绘制 |
| HMI | 单控件扩展属性 | 0 对 | 64 对 | 第 65 对属性不能加载或保存 | 防止属性对象无限增长 |
| 报警 | 报警定义数量 | 0 | 512 | 第 513 条报警不能创建或加载 | 报警每个刷新周期都要判断 |
| 注释 | M/D 软元件注释数量 | 0 | 8002 | 超出后拒绝保存或加载 | M0~M4000 和 D0~D4000 每个地址最多一条,共 8002 条 |
| 控制逻辑 | 控制逻辑数量 | 0 | 128 | 第 129 组不能创建或加载 | 控制工程树和扫描工作量 |
| 控制逻辑 | 每组逻辑的网络数量 | 0 | 1024 | 第 1025 个网络不能创建或加载 | 每次软件扫描都要执行这些网络 |
| 单个网络 | 条件表达式节点和叶子总数 | 0 | 4096 | 超出后编辑操作回退,JSON 加载失败 | 防止单个网络过度复杂 |
| 单个网络 | 表达式嵌套深度 | 1 层 | 20 层 | 第 21 层在解析时直接拒绝 | 10 列条件区下,第 20 层是规范化串并联结构能够达到的最深边界 |
| 单个容器 | 串联或并联子项数量 | 2 | 256 | 第 257 项不能加入或加载 | 控制单层表达式规模 |
| 单个网络 | 条件区列数 | 0 | 10 | 第 11 个条件不能加入,加载失败 | 0 列表示无条件输出;最多 10 列保证网络容易查看 |
| 单个网络 | 总列数 | 1(无条件线圈) | 11 | 第 11 列固定放输出指令 | 普通网络前 10 列放条件;无条件线圈可以直接从左侧电源线接通 |
| 单个网络 | 逻辑网格行数 | 1 | 256 | 超出后编辑操作回退,加载失败 | 防止并联支路纵向无限扩展 |
| 横线 | 单段横线跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | 横线只能占用条件区,不能占用输出列 |
| 字符串 | ID、格式版本、页面跳转目标 ID | 0 字节 | 128 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的 ID 仍必须非空 | ID 只用于稳定识别,不需要长文本 |
| 字符串 | 工程名、页面名、控件文本、报警文本、注释 | 0 字节 | 4096 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的字段仍必须非空 | 给中文说明留足空间,同时防止异常长文本 |
| 字符串 | HMI 属性名称 | 1 字节 | 128 个 UTF-8 字节 | 拒绝保存或加载 | 属性名是短标识 |
| 字符串 | HMI 属性值 | 0 字节 | 4096 个 UTF-8 字节 | 拒绝保存或加载 | 属性值可能保存显示配置,但不能无限增长 |
| 自由监控 | 去重后的监控地址 | 0 | 64 | 第 65 个监控地址不能加入 | 这是用户临时监控区,不应拖慢 PLC 轮询 |
| 编辑历史 | 可撤销记录 | 0 | 100 | 新操作加入后自动删除最旧记录 | 历史记录会保存工程快照,占用内存 |
| 输出面板 | 日志条数 | 0 | 1000 | 加新日志前自动删除最旧一条 | 日志只用于查看近期状态 |
| PLC 通信 | 站号 | 1 | 247 | 不允许发起连接 | 248~254 虽然信捷手册可表示,但属于 Modbus 保留地址,本项目不使用 |
| PLC 通信 | 波特率 | 9600 | 115200 | 只接受 9600、19200、38400、57600、115200 | 只开放本项目已经验证的串口档位 |
| PLC 通信 | 数据位 | 7 | 8 | 只接受 7 或 8 | 串口帧格式必须明确 |
| PLC 通信 | 停止位 | 1 | 2 | 只接受 1 或 2 | 串口帧格式必须明确 |
| PLC 通信 | 响应超时 | 100 ms | 30000 ms | 不允许发起连接 | 太短容易误判,太长会让故障等待时间不可接受 |
| PLC 通信 | 失败重试次数 | 0 | 5 | 不允许发起连接 | 采用信捷手册给出的 Modbus 重试范围 |
| PLC 通信 | 轮询周期 | 50 ms | 10000 ms | 不允许发起连接 | 太快会压满串口,太慢会让画面失去实时性 |
| PLC 通信 | 单个 Modbus 读块 | 1 个 | 120 个 | 自动拆成多个读块 | 信捷 RTU 数据区最多 252 字节,120 个字安全留有余量 |
| PLC 通信 | 去重后的总轮询地址 | 0 | 1024 | 拒绝连接或拒绝更新轮询表 | 限制每轮通信总工作量 |
| PLC 通信 | 总轮询读块 | 2(无配置时保底读取 M0、D0) | 64 | 拒绝更新轮询表 | 地址太分散时,即使总数不大也会产生大量报文 |
| PLC 写入 | 同时未完成的写请求 | 0 | 1 | 前一笔没完成时拒绝新写入 | 串行写可以避免请求堆积和写入顺序混乱 |

补充说明:空工程允许 0 个 HMI 页面;一旦已经创建页面,用户在编辑器里不能把最后一个页面删除。校验方式只能选无校验、奇校验或偶校验,常用真机配置仍是 `COM3、9600、8E1`。

梯形图网络只放一个线圈或其他输出指令也是合法的。它表示条件一直成立,软件每次扫描都会执行这个输出;普通线圈会一直导通,MOVE、ADD/SUB 等指令会每次扫描执行。只有条件却没有输出仍属于未完成网络,不能进入运行态。

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

| 项目 | 最终结论 | 原因 |
| --- | --- | --- |
| 当前同时触发的报警条数 | 不另设上限 | 它天然不会超过 512 条报警定义 |
| 页面跳转目标数量 | 不另设上限 | 目标只能来自最多 128 个现有页面 |
| PLC 缓存中的 M/D 数量 | 不另设上限 | 缓存由 `0~4000` 地址范围和 1024 个轮询地址共同限制 |
| 单次按钮点击、数值输入、保存和加载次数 | 不限制 | 每次操作完成后不会持续积累同类对象 |
| 软件连续运行时间 | 不限制 | 运行时间本身不是内存集合,也不是 PLC 资源 |
| 离线仿真的累计扫描次数 | 不限制 | 只保存一个 64 位计数,不保存每次扫描的完整副本 |
| 工程文件路径长度 | 不在业务层另设上限 | 交给 Windows 和 Qt 的文件接口处理,工程内容仍受 16 MiB 限制 |
| 可用 COM 端口列表数量 | 不另设上限 | 列表来自操作系统,项目不持久化累积这些端口 |
| 报警阈值、进度条范围、普通 HMI 数值的额外业务范围 | 不再加第二套范围 | 统一使用 D 的 `-32768~32767`,进度条只额外要求最小值小于最大值 |
| 加减法参与次数 | 不单独限制 | 执行量已经受 128 组逻辑、每组 1024 个网络和单网络复杂度限制 |

## 3. 这些数字从哪里来

- `docs/pdf/0_综合平台编程器_修改.pdf` 第 2 页明确要求 M 区和 D 区使用 `0~4000`
- `XD、XL系列可编程控制器用户手册(基本指令篇)` PDF 第 272 页的 XDH/XLH 表中,实际 M/D 资源达到 `0~20479`,T/C 达到 `0~4095`。本项目仍统一收紧为 `0~4000`,不会超出 XDH-60T4-E 的资源范围
- 同一手册 PDF 第 28 页说明 D 是有符号 16 位寄存器,16 位普通计数范围为 `1~32767`。本项目允许 PV 为 0,表示尚未设定或立即完成,不允许负数
- 同一手册 PDF 第 274 页说明 Modbus RTU 数据区最多 252 字节,站号最大可到 254。本项目单读块固定为 120 个,站号固定为 `1~247`
- 同一手册 PDF 第 311 页给出 Modbus 重试次数 `0~5`、回复超时 `0~65535`。本项目采用重试 `0~5`,并把超时进一步收紧为 `100~30000 ms`,避免无限等待
- 页面、控件、报警、逻辑、JSON 大小、日志等数字在信捷手册里没有规定,所以按本软件的内存、绘制、扫描和通信成本给出固定安全预算

## 4. 用户实际会看到什么

- 正常编辑时,达到页面、控件、报警、逻辑或网络上限后,软件会直接提示具体上限,不会先添加再留下坏工程
- 手工改 JSON 绕过界面也没用,加载时会重新检查文件大小、数组数量、字符串长度、页面尺寸和表达式深度
- PLC 参数不只在对话框里限制,真正连接前还会再检查一次
- 超过 1000 条输出日志时,只删除最旧日志,不影响工程和 PLC 数据
- PLC 写请求正在等待回复时,新写请求会被拒绝,等前一笔完成后再操作即可

## 5. 本次验收结果

- Release 主程序已经重新构建通过,数量边界相关的 10 组自动化测试全部通过
- 2026-08-19 使用真实 PLC 在 `COM3、9600、8E1、站号 1` 下完成读写验证:先读取 `D4000=0`,写入 `1` 后读回 `1`,再恢复并读回 `0`
- 真机验证没有下载 PLC 程序,测试结束后 `D4000` 已恢复原值

불러오는 중...
취소
저장