| @@ -57,12 +57,15 @@ Thumbs.db | |||
| Desktop.ini | |||
| /scripts/* | |||
| !/scripts/build_and_run_qt.ps1 | |||
| !/scripts/measure_cpp_code.ps1 | |||
| !/scripts/package_qt_app.ps1 | |||
| !/scripts/run_qt_tests.ps1 | |||
| /docs/images | |||
| /docs/类图Mermaid代码.md | |||
| /other | |||
| 设计方案书.doc | |||
| /学习 | |||
| /json | |||
| /代码 | |||
| /other_version | |||
| /app_copy | |||
| @@ -42,8 +42,9 @@ | |||
| - 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` | |||
| - 项目编译运行:`pwsh -NoLogo -NoProfile -File .\scripts\build_and_run_qt.ps1` | |||
| - 测试编译运行:`pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release` | |||
| - Qt 项目打包:`pwsh -NoLogo -NoProfile -File .\scripts\package_qt_app.ps1` | |||
| - 打开主界面:`& 'D:\Qt5.15.2\5.15.2\mingw81_64\bin\designer.exe' '.\app\src\ui\main_window.ui'` | |||
| ## 代码与架构约束 | |||
| @@ -1,47 +1,68 @@ | |||
| # 当前开发交接 | |||
| > 更新日期:2026-08-24。本文件只保留当前工作区状态、最近验证和下一步,不记录历史开发流水账;稳定架构和业务细节分别以 `docs/architecture.md` 及对应专题文档为准。 | |||
| > 更新日期:2026-08-25。本文件只保留当前工作区状态、最近验证和下一步,不记录历史开发流水账;稳定架构和业务细节分别以 `docs/architecture.md` 及对应专题文档为准。 | |||
| ## 当前状态 | |||
| - 分支:`main` | |||
| - 核心闭环仍可用:HMI 编辑、结构化梯形图、离线仿真、工程 JSON、运行监控、自由监控和 Modbus RTU 真机读写均已实现 | |||
| - 当前梯形图条件只包含 M 常开/常闭触点、M 上升沿/下降沿触点和 D 与常量比较;输出只包含普通/置位/复位 M 线圈、MOVE、ADD 和 SUB | |||
| - T/C 触点、TON、CTU、CTD 已从领域模型、JSON、编辑服务、离线执行器、属性面板、工具栏、图标和自动化测试中整条移除,不是仅隐藏界面按钮 | |||
| - 工程格式仍为严格的 `1.0`;包含 `timerContact`、`counterContact`、`ton` 或 `counter` 节点的工程现在会因节点类型不受支持而加载失败,不做自动迁移 | |||
| - 当前没有代码阻塞,下一步是完成人工界面检查并整理提交 | |||
| - 核心闭环可用:HMI 编辑、结构化梯形图、离线仿真、工程 JSON、运行监控、自由监控和 Modbus RTU 真机读写均已实现 | |||
| - 梯形图条件区已经统一为显式模型:可见横线保存为 `Wire`,明确断路保存为 `Gap`,输出固定在第 11 列 | |||
| - 工程 JSON 继续严格使用 `1.0`,不增加版本迁移或早期同版本草稿兼容分支 | |||
| - HMI 进度条控件已经完整移除,编辑器、运行投影和工程 JSON 均不再支持该类型 | |||
| - 返回编辑态后会拒绝退出前排队的离线扫描轨迹,梯形图不会重新显示旧的绿色导通状态 | |||
| - 新建工程名称为空或全是空白时会禁用确认按钮,取消输入时继续安全保留当前工程,不再静默接受无效名称 | |||
| - 第一次保存和工程另存为会用当前工程名称预填 `.json` 文件名,并兼容 Windows 非法字符与保留设备名 | |||
| - PLC 通信配置支持刷新当前可用串口及按当前站号自动搜索:搜索异步遍历可用串口和全部受支持串口参数,找到后自动回填并进入正式连接,搜索可随时取消 | |||
| - HMI/梯形图复制粘贴、数量边界收紧和全局对话框标题栏调整仍保留在当前未提交工作区 | |||
| - 当前没有代码阻塞,Release 功能测试、性能测试和主程序构建均已通过 | |||
| ## 工作区状态 | |||
| - 删除定时器/计数器能力的 21 个 `app` 源码和测试文件仍处于未提交修改状态,属于当前任务成果,不能回退 | |||
| - `app/src/services/offline_simulation_service.cpp` 只修正了“重启会话时清空 T/C 状态”的过时注释,没有行为改动 | |||
| - `docs/用户使用/软件使用帮助.md` 处于已删除状态,本轮按现有工作区保留删除,不恢复旧内容 | |||
| - 本轮已同步入口文档、工程格式、数量边界、开发顺序、测试约定和 XDH 指令摘要;各文档只保留自身负责的信息 | |||
| - 当前未提交功能改动包含 HMI/梯形图复制粘贴、数量边界收紧、对话框标题栏调整、显式 `Wire / Gap` 梯形图连接模型、离线轨迹退出时序修复、HMI 进度条控件移除,以及 PLC 自动搜索 | |||
| - PLC 自动搜索新增服务层搜索契约、独立 Qt Modbus RTU 探测实现、配置对话框进度/取消交互、主窗口依赖注入、候选覆盖测试和离屏对话框流程测试 | |||
| - Git 忽略的 `json/` 本地示例工程已同步当前格式;综合测试工程删除了进度条对象和关联说明,电机正反转工程无需内容调整 | |||
| - `.gitignore`、`AGENTS.md`、未跟踪脚本和测试大纲文档属于工作区已有内容,没有被本轮回退 | |||
| - `build/` 只保存本地构建和测试输出,不纳入 Git | |||
| ## 本轮变更结论 | |||
| - `LogicNodeConfig` 不再包含 Timer/Counter 地址、触点或输出配置,运行前也不再做 T/C 资源引用检查 | |||
| - `SoftwareLogicExecutor` 只保留 M/D 读写、边沿触点跨扫描状态、线圈和字操作执行;轨迹不再保存定时器或计数器状态 | |||
| - JSON 读写只接受 `contact`、`edgeContact`、`coil`、`compare`、`move` 和 `arithmetic` | |||
| - 梯形图工具栏、指令对话框、属性面板、绘制和图标工厂均不再提供 T/C、TON、CTU、CTD 入口 | |||
| - 自动化测试已删除对应领域、JSON、编辑和离线执行断言,同时保留当前指令、结构化梯形图、运行模式和 PLC 轮询回归 | |||
| - 条件区每一段可见、可选择和可删除横线都持久化为 `Wire(columnSpan)`;没有横线的位置使用 `Gap(columnSpan)` 明确占位 | |||
| - 有输出的网络必须由 `Node / Wire / Gap` 显式占满前 10 列;直接添加输出生成 `Wire(10) + Output`,一个触点后添加输出生成 `Node + Wire(9) + Output` | |||
| - 删除触点会在原位置留下单格 `Gap`;删除横线只断开选中的网格,支持在 `Gap` 上补横线或原位插入触点 | |||
| - 并联各支路必须显式等宽,短支路补线保存为真实 `Wire`;母线、节点和输出自身端子、并联竖线仍由结构派生 | |||
| - `Gap` 可以保存草稿,但运行校验会拒绝;执行器不再把 `condition=null + output` 解释为隐式恒真网络 | |||
| - JSON 继续严格读写 `formatVersion: "1.0"`,新增 `kind: "gap"` 表达式;早期依赖隐式输出连线或隐式并联补线的同版本草稿不会自动迁移 | |||
| - `RuntimePanelController` 只在仍处于离线运行且执行器状态为 `Running` 时接收扫描轨迹;返回编辑态后的过期排队回调会被丢弃 | |||
| - HMI 控件枚举、注册描述、专用配置模型、编辑服务比较/默认值、JSON 字段、画布绘制、属性表单、添加动作和工具栏图标中的进度条代码均已删除 | |||
| - 严格 `1.0` 工程只接受当前注册表中的 7 类 HMI 控件;包含已移除控件类型的工程会按不支持类型拒绝加载,不做同版本迁移 | |||
| - HMI 编辑和工程管理测试样例已移除该控件,并保留未知 HMI 类型加载失败的通用回归覆盖 | |||
| - 自动搜索固定使用界面当前站号,按优先级遍历每个可用 PC 串口的 60 组受支持串口参数;不穷举 `1~247` 站号 | |||
| - 每组参数只读 `D0`,不写 PLC;正常回复或合法 Modbus 异常回复都视为参数匹配,找到后先释放搜索串口再交给原正式连接流程 | |||
| - 搜索期间配置表单和确认按钮禁用,自动搜索按钮切换为停止命令;取消、无串口和全部失败都有明确结果,找到后自动回填端口、波特率、数据位、校验位和停止位 | |||
| - 端口下拉框右侧提供刷新按钮;窗口打开和手动刷新时都只保留系统当前实际存在的串口,不再把已经拔掉的上次配置端口重新加入列表 | |||
| ## 最近验证 | |||
| - 2026-08-24 重新构建并通过 9 个 Release 功能测试目标:Domain、Service、JSON 和 Fake PLC 全部通过 | |||
| - 独立 Release 性能目标通过:100 个控制逻辑扫描约 `0.123 ms`,4001 个 M 寄存器完整读写约 `0.048 ms`;CSV 位于 `build/tests/release/performance_tests/benchmark.csv` | |||
| - Qt Release 主程序已构建并链接成功:`build/release-docs/release/integrated_platform.exe` | |||
| - 本轮未连接真实 PLC:改动没有触及 Modbus 通信、真机运行模式或地址映射;继续沿用已有 COM3 安全读写和 RUN 联动验收结果 | |||
| - 2026-08-25 PLC 自动搜索完成后,11 个 Release 功能测试目标全部通过;`plc_connection_dialog_tests` 覆盖开始、取消、进度、参数回填和端口刷新,`plc_runtime_tests` 覆盖 180 组候选优先级、去重和边界 | |||
| - 2026-08-24 修复新建工程空名称静默失败后,10 个 Release 功能测试目标全部通过,Release 主程序重新编译并链接成功 | |||
| - 2026-08-24 删除 HMI 进度条后重新 qmake、构建并通过 10 个 Release 功能测试目标:Domain、Alarm、HMI 编辑、梯形图编辑、离线仿真、工程管理、寄存器监控、运行模式、运行面板控制器和 PLC Runtime 全部通过 | |||
| - 新增真实 `RuntimePanelController + LogicEditorWidget` 无界面回归测试,确认退出前排队的 `scanCompleted` 在编辑态不会恢复绿色轨迹 | |||
| - 独立 Release 性能目标通过:100 个控制逻辑扫描约 `0.207 ms`,4001 个 M 寄存器完整读写约 `0.048 ms`;CSV 位于 `build/tests/release/performance_tests/benchmark.csv` | |||
| - Qt Release 主程序重新构建并链接成功:`build/release/release/integrated_platform.exe` | |||
| - 工程管理测试覆盖 `Gap` 严格 `1.0` 往返、`Wire(10)` 恒真输出、拒绝加载 `condition=null + output` 的隐式连线草稿,以及拒绝未知 HMI 控件类型 | |||
| - `json/full_hmi_ladder_test.json` 和 `json/motor_forward_reverse.json` 均已使用当前 `JsonProjectStorage` 实际加载成功,并通过领域校验 | |||
| - 2026-08-25 使用新搜索器按站号 `1` 对本机 `COM1/COM2/COM3` 完成 180 组参数的只读 `D0` 尝试,设备未返回 Modbus 响应并正确报告未找到;本次没有写寄存器,不需要恢复原值,也不能作为成功联机证据 | |||
| ## 待完成 | |||
| 1. 人工打开主界面,确认梯形图工具栏、属性面板和指令对话框不再出现 T/C、TON、CTU、CTD,当前保留指令仍能正常添加和编辑 | |||
| 2. 如需兼容历史 `1.0` 工程中的定时器/计数器节点,单独确定格式升级或迁移方案;当前实现按不兼容并拒绝加载处理 | |||
| 3. 提交前再次确认 `git status --short` 和提交范围,将代码删除与配套文档作为一个逻辑完整改动提交 | |||
| 1. 人工确认 HMI 工具栏和属性面板不再出现进度条入口或空白属性行,再操作直接输出、触点后输出、横线选中、单格断线、断路补线、断路插入触点、删除触点和保存重载,并确认离线仿真返回编辑态后不保留绿色轨迹 | |||
| 2. 需要继续使用的早期隐式连线 `1.0` 草稿应在当前编辑器中重新保存为显式 `Wire / Gap` 结构,本项目不提供自动迁移 | |||
| 3. 在确认 PLC 已上电、RS-485 接线正确、COM2 已配置为 Modbus RTU 从站且站号正确后,人工验收自动搜索能找到配置并自动完成正式连接 | |||
| 4. 提交时区分本轮显式连接模型、PLC 自动搜索与工作区其他已有改动,避免混入无关未跟踪文件 | |||
| ## 下一会话起点 | |||
| 1. 先运行 `git status --short`,保留当前源码、测试、文档修改和使用帮助删除 | |||
| 2. 涉及梯形图能力时以 `LogicNodeConfig` 当前六类配置为准;Qt 的 `QTimer` 只是轮询或界面刷新机制,不代表梯形图定时器指令 | |||
| 3. 修改代码后运行受影响测试、独立性能目标、Release 构建和 `git diff --check`;涉及真机通信时再按 `AGENTS.md` 做 COM3 读写及恢复验证 | |||
| 1. 先运行 `git status --short`,保留工作区全部现有修改,不回退来源不明的文件 | |||
| 2. PLC 设备条件就绪后先在配置窗口保持正确站号,点击自动搜索,确认进度、停止、未找到提示,以及找到后参数回填和正式连接 | |||
| 3. 人工验收 HMI 时确认“更多控件”菜单只保留页面跳转、报警列表和报警配置;验收梯形图时重点确认选中有线网格仍显示横线,删除后只有目标格变为断路,补线后恢复运行资格 | |||
| 4. 修改代码后运行 `pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release -Suite All`、Release 主程序构建和 `git diff --check` | |||
| @@ -70,7 +70,7 @@ Project | |||
| - 需要寄存器的 HMI 控件、报警条件和梯形图节点直接保存 M/D 地址;项目不再维护统一 `dataPoints` 表 | |||
| - `registerComments` 只是 M/D 地址元数据,不保存当前值或引用位置,也不会让地址自动进入 PLC 轮询 | |||
| 工程 JSON 格式固定为严格的 `1.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。当前版本不读取旧 `dataPoints`、`stages/branches` 或缺少必填字段的同版本草稿。字段定义见 `docs/工程格式说明.md`。 | |||
| 工程 JSON 格式固定为严格的 `1.0`。加载顺序为 JSON 解析、版本检查、字段解析、领域校验,全部成功后才能替换当前工程;保存使用原子提交。当前版本不读取旧 `dataPoints`、`stages/branches`,也不迁移缺少显式横线、断路或其他必填结构的早期同版本草稿。字段定义见 `docs/工程格式说明.md`。 | |||
| 校验分两级: | |||
| @@ -87,22 +87,24 @@ Project | |||
| ConditionExpression | |||
| ├── Node 触点、边沿或比较条件 | |||
| ├── Wire 无地址横线,保存条件区列跨度 | |||
| ├── Gap 明确断开的空白网格,保存条件区列跨度 | |||
| ├── Series 子表达式执行 AND | |||
| └── Parallel 子表达式执行 OR | |||
| ``` | |||
| 输出是网络最右侧的单独节点。`LogicEditorService` 把串联、并联、旁路、替换和删除转换为表达式树操作,失败时回退整个编辑;`LogicEditorWidget` 只把树投影成正交网格。竖线由 `Parallel` 边界派生,网格坐标和自由线段不持久化,因此领域模型不会产生悬空线、环路或多输出路径。 | |||
| 单网络前 10 列为条件区,输出最多位于第 11 列;横线只能占条件区。第 11 个条件、过深表达式或过多并联行会在编辑和加载阶段被拒绝,具体数值以数量边界文档为准。 | |||
| 单网络前 10 列为条件区,第 11 列固定为输出位置;存在输出时,前 10 列必须由 `Node / Wire / Gap` 显式占满。横线和断路只能占条件区。第 11 个条件、未占满或超过 10 列的输出网络、过深表达式或过多并联行会在编辑和加载阶段被拒绝,具体数值以数量边界文档为准。 | |||
| 控制逻辑组可以暂时没有网络。新建工程和新建控制逻辑都保持 `rungs=[]`,用户第一次添加条件、横线或输出时,编辑服务在同一个原子操作中创建网络;删除最后一个网络后也允许回到空逻辑。画布中的空网格只表示可编辑位置,不绘制真实横线;模型中的 `Wire`、实际节点、输出连接,以及并联短支路到右侧汇合点的必要结构连接,才投影为梯形图导线。并联短支路的结构连接由 UI 从 `Parallel` 宽度派生,不额外写入 `Wire`。连续追加单格横线占满十个条件列后,下一次无选中目标的追加会在同一撤销记录中创建下一个网络。 | |||
| 控制逻辑组可以暂时没有网络。新建工程和新建控制逻辑都保持 `rungs=[]`,用户第一次添加条件、横线或输出时,编辑服务在同一个原子操作中创建网络;删除最后一个网络后也允许回到空逻辑。条件区中每一段可见、可选择和可删除的横线都必须是持久化 `Wire`;没有横线的位置由 `Gap(columnSpan)` 明确占位。直接添加输出会创建 `Wire(10) + Output`,一个触点后添加输出会创建 `Node + Wire(9) + Output`。删除触点或横线只把对应网格替换为 `Gap`,不会让左右两侧自动重新接通;在该网格重新添加横线或触点后才恢复连接。并联各支路必须用真实 `Wire` 或 `Gap` 补到相同列宽。左右母线、节点和输出图元自身的端子线、并联竖线仍由结构派生,不保存为自由像素线。连续追加单格横线占满十个条件列后,下一次无选中目标的追加会在同一撤销记录中创建下一个网络。 | |||
| 网络运行语义: | |||
| - 空网络不执行 | |||
| - 有条件但没有输出的已启用网络不能运行 | |||
| - 有输出但没有条件的网络是恒真网络,每次扫描执行输出 | |||
| - 直接接通的输出使用 `Wire(10)` 显式表示恒真路径,每次扫描执行输出 | |||
| - `Wire` 的逻辑值为真,但运行轨迹是否导通仍取决于上游电流 | |||
| - `Gap` 表示明确断路,可以保存编辑草稿,但任何包含 `Gap` 的启用网络都不能进入运行态 | |||
| 节点配置使用 `std::variant` 的独立类型。当前条件包括 M 常开/常闭触点、M 上升沿/下降沿触点和 D 与常量比较;输出包括普通/置位/复位 M 线圈、MOVE、ADD 和 SUB。新增或恢复指令必须增加独立配置、校验、JSON、执行、编辑和测试分支,不能向通用节点堆叠无关字段。 | |||
| @@ -139,7 +141,9 @@ PLC <-> PlcCommunicationService <--------------------┘ | |||
| `PlcCommunicationService` 使用 `QModbusRtuSerialMaster` 异步连接、轮询和单点写入,UI 线程不等待串口响应。Modbus 地址使用从 `0` 开始的原始地址。 | |||
| 轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。横线和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再应用新集合。 | |||
| PLC 配置窗口的自动搜索使用独立 `PlcDiscoveryService`,不复用正式轮询主站,避免搜索失败污染连接状态、缓存和首读资格。搜索固定使用界面当前站号,优先当前串口参数,再异步遍历全部可用 PC 串口和项目允许的 60 组波特率、数据位、校验位及停止位组合;每组参数只读 `D0`,正常响应或合法 Modbus 异常响应都能证明端口、站号和串口帧匹配。找到后必须先释放搜索串口,再把参数交给原有正式连接流程;取消或全部失败时不保留搜索连接。 | |||
| 轮询集合由 HMI、报警、梯形图中的显式 M/D 引用和自由监控地址合并,按区域去重并合并相邻地址。横线、断路和注释不进入轮询。动态修改监控地址时,当前请求按旧快照解析,空闲后再应用新集合。 | |||
| PLC 缓存只保存最后一次成功读回值。写请求受理后不乐观修改缓存,界面等待后续轮询确认真实值。真机运行资格属于当前连接代次:必须完成完整首读,旧连接的异步回复不能污染重连后的状态。 | |||
| @@ -61,7 +61,8 @@ | |||
| | `label` | 必须为 `null` | 无 | | |||
| | `pageJump` | 必须为 `null` | `targetPageId` | | |||
| | `alarmList` | 必须为 `null` | 无 | | |||
| | `progressBar` | D 地址或 `null` 草稿 | `minimumValue`、`maximumValue`、`showValue` | | |||
| `type` 只接受表中列出的控件类型。其他类型按严格 `1.0` 规则直接拒绝加载,不做兼容迁移。 | |||
| 按钮操作字符串为 `setOn`、`setOff`、`toggle` 或 `momentaryOn`。PageJump 使用目标页面稳定 ID,不使用名称或数组位置: | |||
| @@ -81,11 +82,6 @@ | |||
| AlarmList 只负责显示项目级当前报警,不保存独立触发逻辑,也不直接绑定单个寄存器。编辑界面的标题输入上限为 12 个字符。多个页面放置 AlarmList 时共享同一组当前报警和确认状态。运行态没有报警时控件隐藏;有报警时按当前页的实际记录数收缩高度,每页最多显示 5 条。超过 5 条时通过表头翻页按钮查看,记录不会被丢弃。标题和报警文本按单行绘制,空间不足时显示省略号。报警恢复后对应记录立即移除,最后一条报警恢复后控件再次隐藏。 | |||
| ProgressBar 使用绑定 D 地址的有符号 16 位字值,并将其限制在 `minimumValue` 到 | |||
| `maximumValue` 的范围后映射为百分比。`minimumValue` 和 `maximumValue` 必须是有符号 16 位整数, | |||
| 且最小值严格小于最大值;`showValue` 为布尔值,决定是否在进度条上显示百分比。ProgressBar | |||
| 是只读控件,不产生 PLC 写请求。 | |||
| ## 报警定义 | |||
| ```json | |||
| @@ -144,15 +140,23 @@ ProgressBar 使用绑定 D 地址的有符号 16 位字值,并将其限制在 | |||
| `enabled` 决定离线执行器是否扫描该逻辑。所有启用逻辑按 `controlLogics` 数组顺序执行;禁用逻辑仍要求 ID、名称、网络和表达式结构合法,但允许保留未完成节点作为草稿。 | |||
| 控制逻辑初始可以没有网络,`rungs` 为空不是错误。编辑器第一次添加条件、横线或输出时才创建网络;删除最后一个网络后可以再次保存为空逻辑。空网格属于界面编辑辅助,不写入 `Wire`,也不绘制真实梯形图导线。 | |||
| 控制逻辑初始可以没有网络,`rungs` 为空不是错误。编辑器第一次添加条件、横线或输出时才创建网络;删除最后一个网络后可以再次保存为空逻辑。没有输出的空网络草稿允许 `condition` 和 `output` 同时为 `null`。存在输出时,`condition` 不能为空,并且必须显式占满前 10 列。 | |||
| 网络、`Node / Wire / Series / Parallel` 条件表达式、M 触点、M 边沿、D 比较、M 线圈和字操作指令字段沿用当前结构化梯形图模型。横线保存为恒真的 `Wire` 叶子: | |||
| 网络、`Node / Wire / Gap / Series / Parallel` 条件表达式、M 触点、M 边沿、D 比较、M 线圈和字操作指令字段沿用当前结构化梯形图模型。条件区中每一段可见、可编辑横线都保存为恒真的 `Wire` 叶子: | |||
| ```json | |||
| {"id": "wire-1", "kind": "wire", "columnSpan": 2} | |||
| ``` | |||
| `columnSpan` 是必填整数,范围为 `1~10`。它表示横线占用的条件区逻辑网格列数,不是像素长度。单个网络前 10 列是条件区,第 11 列固定放输出指令。竖线不保存为独立对象,而是由 `Parallel` 表达式的支路边界派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下: | |||
| 明确断开的连续空白网格保存为 `Gap` 叶子: | |||
| ```json | |||
| {"id": "gap-1", "kind": "gap", "columnSpan": 1} | |||
| ``` | |||
| `Wire` 和 `Gap` 的 `columnSpan` 都是必填整数,范围为 `1~10`,表示占用的条件区逻辑网格列数,不是像素长度。删除一格横线或触点时,该格改为 `Gap(1)`;多格横线只断开被删除的格,其余部分仍是 `Wire`。`Gap` 可以保存和重新加载,但运行校验必须拒绝包含 `Gap` 的启用网络。 | |||
| 单个网络前 10 列是条件区,第 11 列固定放输出指令。存在输出时,前 10 列必须由 `Node / Wire / Gap` 精确占满;直接添加输出保存为 `Wire(10) + Output`,一个触点后添加输出保存为 `Node + Wire(9) + Output`。并联支路必须显式等宽,短支路的补线也保存为真实 `Wire`。左右母线、节点和输出自身端子、并联竖线不保存为独立对象,而是由表达式结构派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线和断路不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下: | |||
| ```json | |||
| {"type": "contact", "address": {"area": "M", "index": 0}, "mode": "normallyOpen"} | |||
| @@ -172,4 +176,6 @@ MOVE、ADD 和 SUB 只能把结果写入 D 地址,源操作数和算术左右 | |||
| 保存顺序为领域校验、JSON 序列化、`QSaveFile` 原子提交。加载顺序为 JSON 语法、`formatVersion`、必填字段和类型、领域关系校验,全部成功后才替换当前工程。 | |||
| 工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑包含输出指令;禁用草稿逻辑不阻止运行。只有输出而没有条件的网络表示左侧电源线直接接通,运行时按恒真条件执行。 | |||
| 工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑包含输出指令,并且条件区中不存在 `Gap`;禁用草稿逻辑不阻止运行。左侧电源线直接接通的输出必须保存为 `Wire(10) + Output`,不能省略 `condition`。 | |||
| 当前实现只读写本文定义的严格 `1.0`。早期同为 `1.0`、但依赖隐式输出连线或隐式并联补线的草稿不会自动迁移,结构不符合当前领域校验时直接加载失败。 | |||
| @@ -6,11 +6,11 @@ | |||
| | 类型 | 证明内容 | 执行方式 | | |||
| | --- | --- | --- | | |||
| | 功能自动化测试 | 领域规则、服务用例、JSON 工程、离线执行和 Fake PLC 状态机正确 | 提交前自动执行 | | |||
| | 功能自动化测试 | 领域规则、服务用例、JSON 工程、离线执行、运行面板时序和 Fake PLC 状态机正确 | 提交前自动执行 | | |||
| | 性能测试 | 代表性工程扫描和边界规模寄存器访问性能 | 独立执行 | | |||
| | 真实 PLC 实测 | 串口、Modbus RTU、PLC 响应、接线和现场联动有效 | 人工执行并记录 | | |||
| 不保留 Qt UI 自动化测试。界面是否可操作、布局是否正常和完整工作流由交付前人工验收确认;领域规则和业务结果由更稳定的 Domain、Service、JSON 和 Fake PLC 测试负责。 | |||
| 不使用自动化测试验证界面布局、视觉效果和真实鼠标操作,这些内容由交付前人工验收确认。跨运行模式的 Qt 排队信号无法只靠服务测试证明,因此保留一个无界面运行的控制器测试,专门验证退出离线仿真后不会恢复旧梯形图轨迹;PLC 自动搜索还保留一个离屏对话框流程测试,验证开始、取消、进度和找到后自动提交参数。 | |||
| ## 功能自动化测试 | |||
| @@ -24,6 +24,8 @@ | |||
| | `project_management_tests` | JSON 往返、非法文件和工程保存状态 | | |||
| | `register_monitor_service_tests` | 监视地址、读写和活动仓库 | | |||
| | `runtime_mode_service_tests` | 编辑、离线和真机状态转换 | | |||
| | `runtime_panel_controller_tests` | 离线扫描排队回调和编辑态轨迹清理 | | |||
| | `plc_connection_dialog_tests` | PLC 端口刷新及自动搜索开始、取消、进度和参数回填 | | |||
| | `plc_runtime_tests` | PLC 缓存、轮询、错误恢复和 Fake gateway | | |||
| 同一规则只在成本最低、失败定位最清楚的层验证: | |||
| @@ -41,7 +43,7 @@ | |||
| pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release | |||
| ``` | |||
| `app/tests/tests.pro` 只聚合上述 9 个功能测试目标。测试构建输出放在 `build/`,不写入 `app/`。 | |||
| `app/tests/tests.pro` 只聚合上述 11 个功能测试目标。测试构建输出放在 `build/`,不写入 `app/`。 | |||
| ## 性能测试 | |||
| @@ -85,7 +87,7 @@ pwsh -NoLogo -NoProfile -File .\scripts\run_qt_tests.ps1 -Configuration Release | |||
| ## 提交前检查 | |||
| 1. 运行 9 个 Release 功能测试目标 | |||
| 1. 运行 11 个 Release 功能测试目标 | |||
| 2. 单独运行 `performance_tests` 并保留基准输出 | |||
| 3. 构建 Qt Release 主程序 | |||
| 4. 按风险决定是否重新执行真实 PLC 实测;未重测时引用已有验收记录并说明原因 | |||
| @@ -10,36 +10,39 @@ | |||
| | PLC 软元件 | D 地址 | 0 | 4000 | 拒绝保存、加载或连接使用 | 项目需求已经明确限定 | | |||
| | 数值 | D 字、比较值、阈值、普通常量 | -32768 | 32767 | 编辑和加载时拒绝越界值,运算溢出时压到边界值 | D 是有符号 16 位数据 | | |||
| | 工程文件 | 单个 JSON 文件大小 | 0 | 16 MiB | 读取 JSON 前直接拒绝 | 防止超大文件一次性占满内存 | | |||
| | HMI | 页面数量 | 0 | 128 | 第 129 个页面不能创建或加载 | 控制工程树和运行页面的内存开销 | | |||
| | HMI | 单页控件数量 | 0 | 512 | 第 513 个控件不能创建或加载 | 保证编辑画布和运行刷新可控 | | |||
| | HMI | 页面数量 | 0 | 32 | 第 33 个页面不能创建或加载 | 控制工程树和运行页面的内存开销 | | |||
| | HMI | 单页控件数量 | 0 | 128 | 第 129 个控件不能创建或加载 | 保证编辑画布和运行刷新可控 | | |||
| | HMI | 全工程控件数量 | 0 | 2048 | 第 2049 个控件不能创建或加载 | 限制工程快照、JSON 和跨页面总刷新工作量 | | |||
| | HMI | 页面宽度 | 320 | 1600 | 拒绝保存、加载或编辑 | 当前工具只面向本机 HMI 编辑,限制在可合理查看的桌面页面范围内 | | |||
| | HMI | 页面高度 | 200 | 800 | 拒绝保存、加载或编辑 | 当前工具只面向本机 HMI 编辑,限制在可合理查看的桌面页面范围内 | | |||
| | HMI | 控件宽度、高度 | 1 | 8192 | 拒绝保存或加载;控件还必须完整放在页面内 | 防止异常尺寸和越界绘制 | | |||
| | HMI | 单控件扩展属性 | 0 对 | 64 对 | 第 65 对属性不能加载或保存 | 防止属性对象无限增长 | | |||
| | HMI | 字号 | 6 | 72 | 拒绝保存、加载或编辑 | 控制文字可读性和控件布局开销 | | |||
| | HMI | 控件属性文本输入长度 | 0 字符 | 12 个字符 | 编辑界面限制继续输入 | 保持 HMI 控件属性文本紧凑,避免编辑和运行显示溢出 | | |||
| | 报警 | 报警定义数量 | 0 | 512 | 第 513 条报警不能创建或加载 | 报警每个刷新周期都要判断 | | |||
| | 报警 | 报警定义数量 | 0 | 256 | 第 257 条报警不能创建或加载 | 报警每个刷新周期都要判断 | | |||
| | 报警 | 报警列表标题输入长度 | 0 字符 | 12 个字符 | 编辑界面限制继续输入 | 标题需要在表头为翻页控件保留空间,显示不下时使用省略号 | | |||
| | 报警 | 单条报警文本输入长度 | 1 字符 | 20 个字符 | 编辑界面限制继续输入 | 报警信息按单行显示并在宽度不足时使用省略号 | | |||
| | 报警 | 单页同时可见行数 | 1 行 | 5 行 | 超出后通过表头按钮翻页,活动记录不丢失 | 控制默认控件高度并避免少量报警产生大块留白 | | |||
| | 注释 | 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 | 超出后编辑操作回退,加载失败 | 防止并联支路纵向无限扩展 | | |||
| | 控制逻辑 | 控制逻辑数量 | 0 | 32 | 第 33 组不能创建或加载 | 控制工程树和扫描工作量 | | |||
| | 控制逻辑 | 每组逻辑的网络数量 | 0 | 256 | 第 257 个网络不能创建或加载 | 每次软件扫描都要执行这些网络 | | |||
| | 控制逻辑 | 全工程网络数量 | 0 | 2048 | 第 2049 个网络不能创建或加载 | 限制离线扫描、编辑快照和工程文件总工作量 | | |||
| | 单个网络 | 条件表达式节点和叶子总数 | 0 | 1024 | 超出后编辑操作回退,JSON 加载失败 | 防止单个网络过度复杂 | | |||
| | 单个网络 | 表达式嵌套深度 | 1 层 | 12 层 | 第 13 层在解析时直接拒绝 | 控制递归求值和画布布局深度 | | |||
| | 单个容器 | 串联或并联子项数量 | 2 | 64 | 第 65 项不能加入或加载 | 控制单层表达式规模 | | |||
| | 单个网络 | 条件区列数 | 0(空网络草稿) | 10 | 第 11 个条件不能加入,加载失败;存在输出时不足 10 列也失败 | 输出网络前 10 列必须由条件、横线或断路显式占满 | | |||
| | 单个网络 | 总列数 | 0(空网络草稿) | 11 | 第 11 列固定放输出指令 | 运行网络固定为前 10 列条件区加第 11 列输出 | | |||
| | 单个网络 | 逻辑网格行数 | 1 | 64 | 超出后编辑操作回退,加载失败 | 防止并联支路纵向无限扩展 | | |||
| | 横线 | 单段横线跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | 横线只能占用条件区,不能占用输出列 | | |||
| | 断路 | 单段空白跨度 | 1 列 | 10 列 | 拒绝编辑或加载 | `Gap` 明确占用条件区空白网格,可保存但不能运行 | | |||
| | 字符串 | ID、格式版本、页面跳转目标 ID | 0 字节 | 128 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的 ID 仍必须非空 | ID 只用于稳定识别,不需要长文本 | | |||
| | 字符串 | 工程名、页面名、控件文本、报警文本、网络名称 | 0 字节 | 4096 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的字段仍必须非空 | 防止异常工程数据无限增长;HMI 控件文本、报警标题和报警文本编辑界面分别使用上方的 12/12/20 字符输入限制 | | |||
| | 字符串 | 工程名、页面名、控件文本、报警文本、网络名称 | 0 字节 | 256 个 UTF-8 字节 | 拒绝保存或加载;业务要求非空的字段仍必须非空 | 防止名称和显示文本异常增长;HMI 控件文本、报警标题和报警文本编辑界面分别使用上方的 12/12/20 字符输入限制 | | |||
| | 注释 | 软元件注释 | 1 字节 | 64 个 UTF-8 字节 | 拒绝编辑、保存或加载;禁止回车和换行 | 注释绘制在梯形图节点下方,必须保持紧凑单行 | | |||
| | 注释 | 梯形图网络注释 | 0 字节 | 128 个 UTF-8 字节 | 拒绝编辑、保存或加载;禁止回车和换行 | 注释绘制在网络标题下方,换行会挤占梯形图区域 | | |||
| | 字符串 | HMI 属性名称 | 1 字节 | 128 个 UTF-8 字节 | 拒绝保存或加载 | 属性名是短标识 | | |||
| | 字符串 | HMI 属性值 | 0 字节 | 4096 个 UTF-8 字节 | 拒绝保存或加载 | 属性值可能保存显示配置,但不能无限增长 | | |||
| | 字符串 | HMI 属性值 | 0 字节 | 1024 个 UTF-8 字节 | 拒绝保存或加载 | 属性值可能保存显示配置,但不能无限增长 | | |||
| | 自由监控 | 去重后的监控地址 | 0 | 64 | 第 65 个监控地址不能加入 | 这是用户临时监控区,不应拖慢 PLC 轮询 | | |||
| | 编辑历史 | 可撤销记录 | 0 | 100 | 新操作加入后自动删除最旧记录 | 历史记录会保存工程快照,占用内存 | | |||
| | 编辑历史 | 可撤销记录 | 0 | 30 | 新操作加入后自动删除最旧记录 | 历史记录会保存工程快照,占用内存 | | |||
| | 输出面板 | 日志条数 | 0 | 1000 | 加新日志前自动删除最旧一条 | 日志只用于查看近期状态 | | |||
| | PLC 通信 | 站号 | 1 | 247 | 不允许发起连接 | 248~254 虽然信捷手册可表示,但属于 Modbus 保留地址,本项目不使用 | | |||
| | PLC 通信 | 波特率 | 9600 | 115200 | 只接受 9600、19200、38400、57600、115200 | 只开放本项目已经验证的串口档位 | | |||
| @@ -49,28 +52,28 @@ | |||
| | 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 | 256 | 拒绝连接或拒绝更新轮询表 | 限制每轮通信总工作量 | | |||
| | PLC 通信 | 总轮询读块 | 2(无配置时保底读取 M0、D0) | 8 | 拒绝更新轮询表 | 地址太分散时,即使总数不大也会产生大量报文 | | |||
| | PLC 写入 | 同时未完成的写请求 | 0 | 1 | 前一笔没完成时拒绝新写入 | 串行写可以避免请求堆积和写入顺序混乱 | | |||
| 补充说明:空工程允许 0 个 HMI 页面;一旦已经创建页面,用户在编辑器里不能把最后一个页面删除。校验方式只能选无校验、奇校验或偶校验,常用真机配置仍是 `COM3、9600、8E1`。 | |||
| 补充说明:空工程允许 0 个 HMI 页面;一旦已经创建页面,用户在编辑器里不能把最后一个页面删除。校验方式只能选无校验、奇校验或偶校验,常用真机配置仍是 `COM3、9600、8E1`。PLC 自动搜索固定使用界面当前站号,遍历全部可用 PC 串口和上述允许参数的 60 种组合,不自动穷举 `1~247` 站号。 | |||
| 梯形图网络只放一个线圈或其他输出指令也是合法的。它表示条件一直成立,软件每次扫描都会执行这个输出;普通线圈会一直导通,MOVE、ADD/SUB 等指令会每次扫描执行。只有条件却没有输出仍属于未完成网络,不能进入运行态。 | |||
| 用户可以直接添加线圈或其他输出指令,但模型会自动在输出前生成 10 格真实横线,即 `Wire(10) + Output`。它表示条件一直成立,软件每次扫描都会执行这个输出;普通线圈会一直导通,MOVE、ADD/SUB 等指令会每次扫描执行。只有条件却没有输出,或条件区存在断路 `Gap`,都属于未完成网络,不能进入运行态。 | |||
| ## 2. 不需要再单独限制的项目 | |||
| | 项目 | 最终结论 | 原因 | | |||
| | --- | --- | --- | | |||
| | 当前同时触发的报警条数 | 不另设上限 | 它天然不会超过 512 条报警定义 | | |||
| | 页面跳转目标数量 | 不另设上限 | 目标只能来自最多 128 个现有页面 | | |||
| | PLC 缓存中的 M/D 数量 | 不另设上限 | 缓存由 `0~4000` 地址范围和 1024 个轮询地址共同限制 | | |||
| | 当前同时触发的报警条数 | 不另设上限 | 它天然不会超过 256 条报警定义 | | |||
| | 页面跳转目标数量 | 不另设上限 | 目标只能来自最多 32 个现有页面 | | |||
| | PLC 缓存中的 M/D 数量 | 不另设上限 | 缓存由 `0~4000` 地址范围和 256 个轮询地址共同限制 | | |||
| | 单次按钮点击、数值输入、保存和加载次数 | 不限制 | 每次操作完成后不会持续积累同类对象 | | |||
| | 软件连续运行时间 | 不限制 | 运行时间本身不是内存集合,也不是 PLC 资源 | | |||
| | 离线仿真的累计扫描次数 | 不限制 | 只保存一个 64 位计数,不保存每次扫描的完整副本 | | |||
| | 工程文件路径长度 | 不在业务层另设上限 | 交给 Windows 和 Qt 的文件接口处理,工程内容仍受 16 MiB 限制 | | |||
| | 可用 COM 端口列表数量 | 不另设上限 | 列表来自操作系统,项目不持久化累积这些端口 | | |||
| | 报警阈值、进度条范围、普通 HMI 数值的额外业务范围 | 不再加第二套范围 | 统一使用 D 的 `-32768~32767`,进度条只额外要求最小值小于最大值 | | |||
| | 加减法参与次数 | 不单独限制 | 执行量已经受 128 组逻辑、每组 1024 个网络和单网络复杂度限制 | | |||
| | 报警阈值和普通 HMI 数值的额外业务范围 | 不再加第二套范围 | 统一使用 D 的 `-32768~32767` | | |||
| | 加减法参与次数 | 不单独限制 | 执行量已经受 32 组逻辑、每组 256 个网络和单网络复杂度限制 | | |||
| ## 3. 这些数字从哪里来 | |||
| @@ -0,0 +1,56 @@ | |||
| [CmdletBinding()] | |||
| param( | |||
| [ValidateSet('Debug', 'Release')] | |||
| [string]$Configuration = 'Debug' | |||
| ) | |||
| Set-StrictMode -Version Latest | |||
| $ErrorActionPreference = 'Stop' | |||
| $projectRoot = Split-Path -Parent $PSScriptRoot | |||
| $qtRoot = 'D:\Qt5.15.2\5.15.2\mingw81_64' | |||
| $minGwBin = 'D:\Qt5.15.2\Tools\mingw810_64\bin' | |||
| $qmakePath = Join-Path $qtRoot 'bin\qmake.exe' | |||
| $makePath = Join-Path $minGwBin 'mingw32-make.exe' | |||
| $projectFile = Join-Path $projectRoot 'app\integrated_platform.pro' | |||
| $configurationName = $Configuration.ToLowerInvariant() | |||
| $buildDirectory = Join-Path $projectRoot "build\$configurationName" | |||
| $executablePath = Join-Path $buildDirectory "$configurationName\integrated_platform.exe" | |||
| foreach ($requiredPath in @($qmakePath, $makePath, $projectFile)) { | |||
| if (-not (Test-Path -LiteralPath $requiredPath)) { | |||
| throw "未找到必要文件:$requiredPath" | |||
| } | |||
| } | |||
| New-Item -ItemType Directory -Path $buildDirectory -Force | Out-Null | |||
| $originalPath = $env:Path | |||
| $env:Path = "$qtRoot\bin;$minGwBin;$env:Path" | |||
| Push-Location $buildDirectory | |||
| try { | |||
| & $qmakePath $projectFile "CONFIG+=$configurationName" "CONFIG-=$(if ($configurationName -eq 'debug') { 'release' } else { 'debug' })" | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "qmake 执行失败,退出码:$LASTEXITCODE" | |||
| } | |||
| & $makePath -j2 | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "mingw32-make 执行失败,退出码:$LASTEXITCODE" | |||
| } | |||
| if (-not (Test-Path -LiteralPath $executablePath)) { | |||
| throw "未生成可执行文件:$executablePath" | |||
| } | |||
| Write-Host "构建完成,正在启动:$executablePath" | |||
| & $executablePath | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "程序退出异常,退出码:$LASTEXITCODE" | |||
| } | |||
| } | |||
| finally { | |||
| Pop-Location | |||
| $env:Path = $originalPath | |||
| } | |||
| @@ -0,0 +1,174 @@ | |||
| [CmdletBinding()] | |||
| param( | |||
| [Parameter()] | |||
| [string]$SourcePath = (Join-Path $PSScriptRoot '..\app\src'), | |||
| [Parameter()] | |||
| [switch]$ShowFiles | |||
| ) | |||
| Set-StrictMode -Version Latest | |||
| $ErrorActionPreference = 'Stop' | |||
| function Get-CodeLineStatistics { | |||
| param( | |||
| [Parameter(Mandatory)] | |||
| [string]$FilePath | |||
| ) | |||
| $statistics = [ordered]@{ | |||
| TotalLines = 0 | |||
| BlankLines = 0 | |||
| CommentOnlyLines = 0 | |||
| PreprocessorLines = 0 | |||
| CodeLines = 0 | |||
| } | |||
| $inBlockComment = $false | |||
| $inRawString = $false | |||
| $rawStringTerminator = '' | |||
| foreach ($line in [System.IO.File]::ReadLines($FilePath, [System.Text.UTF8Encoding]::new($false))) { | |||
| $statistics.TotalLines++ | |||
| $index = 0 | |||
| $code = [System.Text.StringBuilder]::new() | |||
| $containsComment = $inBlockComment | |||
| while ($index -lt $line.Length) { | |||
| if ($inRawString) { | |||
| $terminatorIndex = $line.IndexOf($rawStringTerminator, $index, [System.StringComparison]::Ordinal) | |||
| if ($terminatorIndex -lt 0) { | |||
| [void]$code.Append($line.Substring($index)) | |||
| $index = $line.Length | |||
| continue | |||
| } | |||
| [void]$code.Append($line.Substring($index, $terminatorIndex + $rawStringTerminator.Length - $index)) | |||
| $index = $terminatorIndex + $rawStringTerminator.Length | |||
| $inRawString = $false | |||
| continue | |||
| } | |||
| if ($inBlockComment) { | |||
| $containsComment = $true | |||
| $commentEnd = $line.IndexOf('*/', $index, [System.StringComparison]::Ordinal) | |||
| if ($commentEnd -lt 0) { | |||
| $index = $line.Length | |||
| continue | |||
| } | |||
| $index = $commentEnd + 2 | |||
| $inBlockComment = $false | |||
| continue | |||
| } | |||
| $current = $line[$index] | |||
| $next = if ($index + 1 -lt $line.Length) { $line[$index + 1] } else { [char]0 } | |||
| if ($current -eq '/' -and $next -eq '/') { | |||
| $containsComment = $true | |||
| break | |||
| } | |||
| if ($current -eq '/' -and $next -eq '*') { | |||
| $containsComment = $true | |||
| $inBlockComment = $true | |||
| $index += 2 | |||
| continue | |||
| } | |||
| if ($current -eq 'R' -and $next -eq '"') { | |||
| $delimiterStart = $index + 2 | |||
| $openParenthesis = $line.IndexOf('(', $delimiterStart) | |||
| if ($openParenthesis -ge 0 -and $openParenthesis - $delimiterStart -le 16) { | |||
| $delimiter = $line.Substring($delimiterStart, $openParenthesis - $delimiterStart) | |||
| if ($delimiter -notmatch '[\s\\()]') { | |||
| $rawStringTerminator = ')' + $delimiter + '"' | |||
| [void]$code.Append($line.Substring($index, $openParenthesis - $index + 1)) | |||
| $index = $openParenthesis + 1 | |||
| $inRawString = $true | |||
| continue | |||
| } | |||
| } | |||
| } | |||
| if ($current -eq '"' -or $current -eq "'") { | |||
| $quote = $current | |||
| [void]$code.Append($quote) | |||
| $index++ | |||
| while ($index -lt $line.Length) { | |||
| $stringCharacter = $line[$index] | |||
| [void]$code.Append($stringCharacter) | |||
| $index++ | |||
| if ($stringCharacter -eq '\\' -and $index -lt $line.Length) { | |||
| [void]$code.Append($line[$index]) | |||
| $index++ | |||
| continue | |||
| } | |||
| if ($stringCharacter -eq $quote) { | |||
| break | |||
| } | |||
| } | |||
| continue | |||
| } | |||
| [void]$code.Append($current) | |||
| $index++ | |||
| } | |||
| $remaining = $code.ToString().Trim() | |||
| if ([string]::IsNullOrWhiteSpace($line)) { | |||
| $statistics.BlankLines++ | |||
| } | |||
| elseif ($remaining.StartsWith('#')) { | |||
| $statistics.PreprocessorLines++ | |||
| } | |||
| elseif (-not [string]::IsNullOrWhiteSpace($remaining)) { | |||
| $statistics.CodeLines++ | |||
| } | |||
| elseif ($containsComment) { | |||
| $statistics.CommentOnlyLines++ | |||
| } | |||
| else { | |||
| $statistics.BlankLines++ | |||
| } | |||
| } | |||
| return [pscustomobject]$statistics | |||
| } | |||
| $resolvedSourcePath = (Resolve-Path -LiteralPath $SourcePath).Path | |||
| $files = Get-ChildItem -LiteralPath $resolvedSourcePath -Recurse -File -Filter '*.cpp' | Sort-Object FullName | |||
| if ($files.Count -eq 0) { | |||
| throw "未在 '$resolvedSourcePath' 找到 .cpp 文件" | |||
| } | |||
| $fileResults = foreach ($file in $files) { | |||
| $statistics = Get-CodeLineStatistics -FilePath $file.FullName | |||
| [pscustomobject]@{ | |||
| File = $file.FullName.Substring($resolvedSourcePath.Length).TrimStart('\', '/') | |||
| CodeLines = $statistics.CodeLines | |||
| TotalLines = $statistics.TotalLines | |||
| BlankLines = $statistics.BlankLines | |||
| CommentOnlyLines = $statistics.CommentOnlyLines | |||
| PreprocessorLines = $statistics.PreprocessorLines | |||
| } | |||
| } | |||
| $total = [pscustomobject]@{ | |||
| File = '合计' | |||
| CodeLines = [int]($fileResults | Measure-Object -Property CodeLines -Sum).Sum | |||
| TotalLines = [int]($fileResults | Measure-Object -Property TotalLines -Sum).Sum | |||
| BlankLines = [int]($fileResults | Measure-Object -Property BlankLines -Sum).Sum | |||
| CommentOnlyLines = [int]($fileResults | Measure-Object -Property CommentOnlyLines -Sum).Sum | |||
| PreprocessorLines = [int]($fileResults | Measure-Object -Property PreprocessorLines -Sum).Sum | |||
| } | |||
| Write-Host "扫描目录: $resolvedSourcePath" | |||
| Write-Host "统计范围: $($files.Count) 个 .cpp 文件,不统计 .h;移除注释及 # 预处理指令后统计代码行" | |||
| Write-Host "总行数: $($total.TotalLines)" | |||
| Write-Host "纯 C++ 代码: $($total.CodeLines) 行" | |||
| Write-Host "空白行: $($total.BlankLines) | 纯注释行: $($total.CommentOnlyLines) | 预处理行: $($total.PreprocessorLines)" | |||
| if ($ShowFiles) { | |||
| $fileResults | Format-Table File, CodeLines, TotalLines, BlankLines, CommentOnlyLines, PreprocessorLines -AutoSize | |||
| } | |||
| @@ -0,0 +1,122 @@ | |||
| [CmdletBinding()] | |||
| param( | |||
| [string]$OutputName = 'integrated_platform-win64' | |||
| ) | |||
| Set-StrictMode -Version Latest | |||
| $ErrorActionPreference = 'Stop' | |||
| $projectRoot = Split-Path -Parent $PSScriptRoot | |||
| $qtRoot = if ($env:QTDIR) { $env:QTDIR } else { 'D:\Qt5.15.2\5.15.2\mingw81_64' } | |||
| $mingwBin = 'D:\Qt5.15.2\Tools\mingw810_64\bin' | |||
| $qmakePath = Join-Path $qtRoot 'bin\qmake.exe' | |||
| $makePath = Join-Path $mingwBin 'mingw32-make.exe' | |||
| $pluginDirectory = Join-Path $qtRoot 'plugins' | |||
| $platformPlugin = Join-Path $pluginDirectory 'platforms\qwindows.dll' | |||
| $stylePlugin = Join-Path $pluginDirectory 'styles\qwindowsvistastyle.dll' | |||
| $projectFile = Join-Path $projectRoot 'app\integrated_platform.pro' | |||
| $buildDirectory = Join-Path $projectRoot 'build\package-build\release' | |||
| $executablePath = Join-Path $buildDirectory 'release\integrated_platform.exe' | |||
| $packageRoot = Join-Path $projectRoot 'build\package' | |||
| foreach ($requiredPath in @($qmakePath, $makePath, $platformPlugin, $stylePlugin, $projectFile)) { | |||
| if (-not (Test-Path -LiteralPath $requiredPath)) { | |||
| throw "未找到必要文件:$requiredPath" | |||
| } | |||
| } | |||
| if ([string]::IsNullOrWhiteSpace($OutputName) -or | |||
| $OutputName -in @('.', '..') -or | |||
| $OutputName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0 -or | |||
| [System.IO.Path]::GetFileName($OutputName) -ne $OutputName) { | |||
| throw "输出名称不是有效的 Windows 文件名:$OutputName" | |||
| } | |||
| $packageDirectory = Join-Path $packageRoot $OutputName | |||
| $archivePath = Join-Path $packageRoot "$OutputName.zip" | |||
| $resolvedProjectRoot = [System.IO.Path]::GetFullPath($projectRoot).TrimEnd('\') | |||
| $resolvedPackageRoot = [System.IO.Path]::GetFullPath($packageRoot).TrimEnd('\') | |||
| $resolvedPackageDirectory = [System.IO.Path]::GetFullPath($packageDirectory) | |||
| if (-not $resolvedPackageRoot.StartsWith("$resolvedProjectRoot\build\", [System.StringComparison]::OrdinalIgnoreCase) -or | |||
| [System.IO.Path]::GetDirectoryName($resolvedPackageDirectory) -ne $resolvedPackageRoot) { | |||
| throw "打包目录必须是 build\package 的直接子目录:$resolvedPackageDirectory" | |||
| } | |||
| New-Item -ItemType Directory -Path $buildDirectory -Force | Out-Null | |||
| New-Item -ItemType Directory -Path $packageRoot -Force | Out-Null | |||
| $originalPath = $env:Path | |||
| $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | |||
| try { | |||
| Push-Location $buildDirectory | |||
| try { | |||
| & $qmakePath $projectFile 'CONFIG+=release' 'CONFIG-=debug' | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "qmake 执行失败,退出码:$LASTEXITCODE" | |||
| } | |||
| & $makePath -j2 | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "mingw32-make 执行失败,退出码:$LASTEXITCODE" | |||
| } | |||
| } | |||
| finally { | |||
| Pop-Location | |||
| } | |||
| if (-not (Test-Path -LiteralPath $executablePath)) { | |||
| throw "未生成可执行文件:$executablePath" | |||
| } | |||
| if (Test-Path -LiteralPath $packageDirectory) { | |||
| Remove-Item -LiteralPath $packageDirectory -Recurse -Force | |||
| } | |||
| if (Test-Path -LiteralPath $archivePath) { | |||
| Remove-Item -LiteralPath $archivePath -Force | |||
| } | |||
| New-Item -ItemType Directory -Path $packageDirectory | Out-Null | |||
| Copy-Item -LiteralPath $executablePath -Destination $packageDirectory | |||
| $packagedExecutable = Join-Path $packageDirectory 'integrated_platform.exe' | |||
| $runtimeFiles = @( | |||
| 'Qt5Core.dll', 'Qt5Gui.dll', 'Qt5Network.dll', | |||
| 'Qt5SerialBus.dll', 'Qt5SerialPort.dll', 'Qt5Widgets.dll', | |||
| 'libgcc_s_seh-1.dll', 'libstdc++-6.dll', 'libwinpthread-1.dll' | |||
| ) | |||
| foreach ($runtimeFile in $runtimeFiles) { | |||
| $runtimeSource = Join-Path $(if ($runtimeFile -like 'Qt5*') { "$qtRoot\bin" } else { $mingwBin }) $runtimeFile | |||
| if (-not (Test-Path -LiteralPath $runtimeSource)) { | |||
| throw "未找到运行库:$runtimeSource" | |||
| } | |||
| Copy-Item -LiteralPath $runtimeSource -Destination $packageDirectory | |||
| } | |||
| $packagedPlatformDirectory = Join-Path $packageDirectory 'platforms' | |||
| $packagedStyleDirectory = Join-Path $packageDirectory 'styles' | |||
| New-Item -ItemType Directory -Path $packagedPlatformDirectory | Out-Null | |||
| New-Item -ItemType Directory -Path $packagedStyleDirectory | Out-Null | |||
| Copy-Item -LiteralPath $platformPlugin -Destination $packagedPlatformDirectory | |||
| Copy-Item -LiteralPath $stylePlugin -Destination $packagedStyleDirectory | |||
| foreach ($requiredOutput in @( | |||
| $packagedExecutable, | |||
| (Join-Path $packageDirectory 'Qt5Core.dll'), | |||
| (Join-Path $packageDirectory 'Qt5SerialBus.dll'), | |||
| (Join-Path $packagedPlatformDirectory 'qwindows.dll') | |||
| )) { | |||
| if (-not (Test-Path -LiteralPath $requiredOutput)) { | |||
| throw "打包结果缺少必要文件:$requiredOutput" | |||
| } | |||
| } | |||
| Compress-Archive -LiteralPath $packageDirectory -DestinationPath $archivePath -CompressionLevel Optimal | |||
| Write-Host "打包完成" | |||
| Write-Host "运行目录:$packageDirectory" | |||
| Write-Host "压缩包:$archivePath" | |||
| } | |||
| finally { | |||
| $env:Path = $originalPath | |||
| } | |||
| @@ -34,6 +34,7 @@ $functionalTargets = @( | |||
| 'register_monitor_service_tests', | |||
| 'runtime_mode_service_tests', | |||
| 'runtime_panel_controller_tests', | |||
| 'plc_connection_dialog_tests', | |||
| 'plc_runtime_tests' | |||
| ) | |||
| $performanceTargets = @('performance_tests') | |||