From 66ce714bf994c65da2047dfe03723181f3a4ded0 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Mon, 24 Aug 2026 15:48:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E4=B8=8E=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/domain/alarm_model.cpp | 2 +- app/src/domain/control_logic_model.cpp | 16 +-- app/src/domain/hmi_model.cpp | 8 +- app/src/domain/project_limits.h | 28 ++-- app/src/domain/project_model.cpp | 30 ++++- .../infrastructure/json_project_storage.cpp | 6 +- .../plc_communication_service.cpp | 4 +- app/src/services/alarm_editor_service.cpp | 2 +- app/src/services/editor_history.h | 4 +- app/src/services/hmi_editor_service.cpp | 30 ++++- app/src/services/logic_editor_service.cpp | 66 ++++++--- app/src/services/runtime_mode_service.cpp | 2 +- app/tests/domain_tests.cpp | 61 ++++++++- app/tests/hmi_editor_service_tests.cpp | 82 +++++++++++- app/tests/logic_editor_service_tests.cpp | 126 +++++++++++++++++- app/tests/plc_runtime_tests.cpp | 10 +- app/tests/project_management_tests.cpp | 4 +- 17 files changed, 403 insertions(+), 78 deletions(-) diff --git a/app/src/domain/alarm_model.cpp b/app/src/domain/alarm_model.cpp index 58ce351..2f47164 100644 --- a/app/src/domain/alarm_model.cpp +++ b/app/src/domain/alarm_model.cpp @@ -33,7 +33,7 @@ bool AlarmDefinition::validate(std::string *error) const } if (message.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "报警文本不能超过 4096 个 UTF-8 字节"); + setError(error, "报警文本不能超过 256 个 UTF-8 字节"); return false; } if (!address.isValid()) diff --git a/app/src/domain/control_logic_model.cpp b/app/src/domain/control_logic_model.cpp index 8e48e46..9549c3f 100644 --- a/app/src/domain/control_logic_model.cpp +++ b/app/src/domain/control_logic_model.cpp @@ -221,13 +221,13 @@ bool validateConditionExpression( { if (depth > ProjectLimits::kMaximumExpressionDepth) { - setError(error, "条件表达式最多嵌套 20 层"); + setError(error, "条件表达式最多嵌套 12 层"); return false; } ++*node_count; if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung) { - setError(error, "单个网络最多包含 4096 个条件表达式节点和叶子"); + setError(error, "单个网络最多包含 1024 个条件表达式节点和叶子"); return false; } if (expression.id.empty()) @@ -282,7 +282,7 @@ bool validateConditionExpression( } if (expression.children.size() > ProjectLimits::kMaximumExpressionChildren) { - setError(error, "单个串联或并联容器最多包含 256 个子表达式"); + setError(error, "单个串联或并联容器最多包含 64 个子表达式"); return false; } @@ -319,7 +319,7 @@ bool validateConditionExpression( total_columns = std::max(total_columns, child_columns); if (total_rows > ProjectLimits::kMaximumLogicRows - child_rows) { - setError(error, "单个网络的逻辑总行数最多为 256 行"); + setError(error, "单个网络的逻辑总行数最多为 64 行"); return false; } total_rows += child_rows; @@ -328,7 +328,7 @@ bool validateConditionExpression( if (total_columns > ProjectLimits::kMaximumConditionColumns || total_rows > ProjectLimits::kMaximumLogicRows) { - setError(error, "单个网络最多使用 10 列条件、1 列输出和 256 行逻辑网格"); + setError(error, "单个网络最多使用 10 列条件、1 列输出和 64 行逻辑网格"); return false; } *columns = total_columns; @@ -680,7 +680,7 @@ bool LadderRung::validateStructure(std::string *error) const } if (name.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "梯形图网络名称不能超过 4096 个 UTF-8 字节"); + setError(error, "梯形图网络名称不能超过 256 个 UTF-8 字节"); return false; } if (containsLineBreak(comment)) @@ -755,12 +755,12 @@ bool ControlLogic::validateStructure(std::string *error) const } if (name.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); + setError(error, "控制逻辑名称不能超过 256 个 UTF-8 字节"); return false; } if (rungs.size() > ProjectLimits::kMaximumRungsPerLogic) { - setError(error, "单组控制逻辑最多包含 1024 个网络"); + setError(error, "单组控制逻辑最多包含 256 个网络"); return false; } if (hasDuplicateId(rungs)) diff --git a/app/src/domain/hmi_model.cpp b/app/src/domain/hmi_model.cpp index 50d77ef..7a5fe57 100644 --- a/app/src/domain/hmi_model.cpp +++ b/app/src/domain/hmi_model.cpp @@ -126,7 +126,7 @@ bool HmiControl::validate(std::string *error) const } if (text.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "HMI 控件文本不能超过 4096 个 UTF-8 字节"); + setError(error, "HMI 控件文本不能超过 256 个 UTF-8 字节"); return false; } if (bounds.width <= 0 || bounds.height <= 0 @@ -155,7 +155,7 @@ bool HmiControl::validate(std::string *error) const } if (property.second.size() > ProjectLimits::kMaximumPropertyValueBytes) { - setError(error, "HMI 控件属性值不能超过 4096 个 UTF-8 字节"); + setError(error, "HMI 控件属性值不能超过 1024 个 UTF-8 字节"); return false; } if (!validateAppearanceProperty(property.first, property.second, error)) @@ -268,7 +268,7 @@ bool HmiPage::validate(std::string *error) const } if (name.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); + setError(error, "HMI 页面名称不能超过 256 个 UTF-8 字节"); return false; } if (width < ProjectLimits::kMinimumHmiPageWidth @@ -283,7 +283,7 @@ bool HmiPage::validate(std::string *error) const } if (controls.size() > ProjectLimits::kMaximumHmiControlsPerPage) { - setError(error, "单个 HMI 页面最多包含 512 个控件"); + setError(error, "单个 HMI 页面最多包含 128 个控件"); return false; } diff --git a/app/src/domain/project_limits.h b/app/src/domain/project_limits.h index 43158a3..1bfb683 100644 --- a/app/src/domain/project_limits.h +++ b/app/src/domain/project_limits.h @@ -7,30 +7,32 @@ namespace ProjectLimits { constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; // 一个工程文件最大 16 MiB,防止异常大文件占满内存 -constexpr std::size_t kMaximumHmiPages = 128U; // 一个工程最多放 128 个 HMI 页面 -constexpr std::size_t kMaximumHmiControlsPerPage = 512U; // 一个 HMI 页面最多放 512 个控件 -constexpr std::size_t kMaximumAlarmDefinitions = 512U; // 一个工程最多配置 512 条报警 +constexpr std::size_t kMaximumHmiPages = 32U; // 一个工程最多放 32 个 HMI 页面 +constexpr std::size_t kMaximumHmiControlsPerPage = 128U; // 一个 HMI 页面最多放 128 个控件 +constexpr std::size_t kMaximumHmiControlsPerProject = 2048U; // 一个工程最多保存 2048 个 HMI 控件 +constexpr std::size_t kMaximumAlarmDefinitions = 256U; // 一个工程最多配置 256 条报警 constexpr std::size_t kMaximumRegisterComments = 8002U; // M0~M4000 和 D0~D4000 最多各写一条注释 -constexpr std::size_t kMaximumControlLogics = 128U; // 一个工程最多放 128 组梯形图控制逻辑 -constexpr std::size_t kMaximumRungsPerLogic = 1024U; // 一组控制逻辑最多放 1024 个网络 +constexpr std::size_t kMaximumControlLogics = 32U; // 一个工程最多放 32 组梯形图控制逻辑 +constexpr std::size_t kMaximumRungsPerLogic = 256U; // 一组控制逻辑最多放 256 个网络 +constexpr std::size_t kMaximumRungsPerProject = 2048U; // 一个工程最多保存 2048 个网络 -constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U; // 一个网络里的触点、横线和内部结构最多共 4096 个 -constexpr std::size_t kMaximumExpressionDepth = 20U; // 并联套并联时最多套 20 层,防止结构无限变复杂 -constexpr std::size_t kMaximumExpressionChildren = 256U; // 一个串联组或并联组最多放 256 个子项 +constexpr std::size_t kMaximumExpressionNodesPerRung = 1024U; // 一个网络里的触点、横线和内部结构最多共 1024 个 +constexpr std::size_t kMaximumExpressionDepth = 12U; // 并联套并联时最多套 12 层,防止结构无限变复杂 +constexpr std::size_t kMaximumExpressionChildren = 64U; // 一个串联组或并联组最多放 64 个子项 constexpr int kMaximumConditionColumns = 10; // 一个网络前面的条件区最多 10 列 constexpr int kMaximumLadderColumns = 11; // 一个网络总共 11 列,最后 1 列专门放输出 -constexpr int kMaximumLogicRows = 256; // 一个网络上下最多 256 行并联支路 +constexpr int kMaximumLogicRows = 64; // 一个网络上下最多 64 行并联支路 static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); // 确保总列数始终等于条件列加输出列 constexpr std::size_t kMaximumHmiProperties = 64U; // 一个 HMI 控件最多保存 64 对扩展属性 constexpr std::size_t kMaximumIdBytes = 128U; // ID 最多 128 个 UTF-8 字节,够用来做稳定标识 -constexpr std::size_t kMaximumTextBytes = 4096U; // 工程名称等普通文本最多 4096 个 UTF-8 字节 +constexpr std::size_t kMaximumTextBytes = 256U; // 工程名称等普通文本最多 256 个 UTF-8 字节 constexpr std::size_t kMaximumHmiControlTextCharacters = 12U; // HMI 控件显示文字最多输入 12 个字符 constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; // 报警列表标题最多输入 12 个字符 constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; // 一条报警内容最多输入 20 个字符 constexpr std::size_t kMaximumPropertyKeyBytes = 128U; // HMI 属性名最多 128 个 UTF-8 字节 -constexpr std::size_t kMaximumPropertyValueBytes = 4096U; // HMI 属性值最多 4096 个 UTF-8 字节 +constexpr std::size_t kMaximumPropertyValueBytes = 1024U; // HMI 属性值最多 1024 个 UTF-8 字节 constexpr std::size_t kMaximumRegisterCommentBytes = 64U; // M/D 注释最多 64 个 UTF-8 字节,保证节点下方放得下 constexpr std::size_t kMaximumRungCommentBytes = 128U; // 网络注释最多 128 个 UTF-8 字节,并且保持单行显示 @@ -47,8 +49,8 @@ constexpr int kMaximumHmiFontPointSize = 72; // HMI 字体最大 72 磅,防止 constexpr int kAlarmDefaultFontPointReduction = 2; // 报警列表默认比普通文字小 2 磅,给表头留空间 constexpr int kMaximumVisibleAlarmRows = 5; // 报警列表默认最多同时显示 5 行,更多内容通过翻页查看 -constexpr std::size_t kMaximumPollAddresses = 1024U; // PLC 一轮轮询最多读取 1024 个去重后的地址 -constexpr std::size_t kMaximumPollBlocks = 64U; // PLC 一轮轮询最多拆成 64 个读块 +constexpr std::size_t kMaximumPollAddresses = 256U; // PLC 一轮轮询最多读取 256 个去重后的地址 +constexpr std::size_t kMaximumPollBlocks = 8U; // PLC 一轮轮询最多拆成 8 个读块 constexpr int kMaximumModbusReadCount = 120; // 一条 Modbus 读指令最多读 120 个数据项 constexpr int kMinimumPlcServerAddress = 1; // Modbus 站号从 1 开始 diff --git a/app/src/domain/project_model.cpp b/app/src/domain/project_model.cpp index a85f664..1aaaed7 100644 --- a/app/src/domain/project_model.cpp +++ b/app/src/domain/project_model.cpp @@ -125,7 +125,7 @@ bool Project::validate(std::string *error) const } if (metadata.name.size() > ProjectLimits::kMaximumTextBytes) { - setError(error, "工程名称不能超过 4096 个 UTF-8 字节"); + setError(error, "工程名称不能超过 256 个 UTF-8 字节"); return false; } if (initialHmiPageId.size() > ProjectLimits::kMaximumIdBytes) @@ -135,12 +135,12 @@ bool Project::validate(std::string *error) const } if (hmiPages.size() > ProjectLimits::kMaximumHmiPages) { - setError(error, "单个工程最多包含 128 个 HMI 页面"); + setError(error, "单个工程最多包含 32 个 HMI 页面"); return false; } if (alarmDefinitions.size() > ProjectLimits::kMaximumAlarmDefinitions) { - setError(error, "单个工程最多包含 512 条报警定义"); + setError(error, "单个工程最多包含 256 条报警定义"); return false; } if (registerComments.size() > ProjectLimits::kMaximumRegisterComments) @@ -150,7 +150,27 @@ bool Project::validate(std::string *error) const } if (controlLogics.size() > ProjectLimits::kMaximumControlLogics) { - setError(error, "单个工程最多包含 128 组控制逻辑"); + setError(error, "单个工程最多包含 32 组控制逻辑"); + return false; + } + std::size_t total_hmi_controls = 0U; + for (const HmiPage &page : hmiPages) + { + total_hmi_controls += page.controls.size(); + } + if (total_hmi_controls > ProjectLimits::kMaximumHmiControlsPerProject) + { + setError(error, "单个工程最多包含 2048 个 HMI 控件"); + return false; + } + std::size_t total_rungs = 0U; + for (const ControlLogic &logic : controlLogics) + { + total_rungs += logic.rungs.size(); + } + if (total_rungs > ProjectLimits::kMaximumRungsPerProject) + { + setError(error, "单个工程最多包含 2048 个梯形图网络"); return false; } if (containsDuplicateId(hmiPages)) @@ -307,7 +327,7 @@ bool Project::validate(std::string *error) const poll_addresses.end()); if (poll_addresses.size() > ProjectLimits::kMaximumPollAddresses) { - setError(error, "工程中需要 PLC 轮询的去重 M/D 地址最多为 1024 个"); + setError(error, "工程中需要 PLC 轮询的去重 M/D 地址最多为 256 个"); return false; } return true; diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index 037f784..119ac08 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -631,7 +631,7 @@ bool parseProperties( { return state->fail( ProjectStorageError::InvalidField, - "HMI 控件属性名称最多 128 字节,属性值最多 4096 字节"); + "HMI 控件属性名称最多 128 字节,属性值最多 1024 字节"); } properties->emplace(key, value); } @@ -1353,14 +1353,14 @@ bool parseConditionExpression( { return state->fail( ProjectStorageError::InvalidField, - context + " 的嵌套深度超过 20 层"); + context + " 的嵌套深度超过 12 层"); } ++*node_count; if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung) { return state->fail( ProjectStorageError::InvalidField, - context + " 的表达式节点总数超过 4096 个"); + context + " 的表达式节点总数超过 1024 个"); } std::string kind; if (!readString( diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index 9ff6ff0..c8a216f 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -72,7 +72,7 @@ bool normalizePollAddresses( std::unique(normalized->begin(), normalized->end()), normalized->end()); if (normalized->size() > ProjectLimits::kMaximumPollAddresses) { - *error = "PLC 轮询的去重 M/D 地址最多为 1024 个"; + *error = "PLC 轮询的去重 M/D 地址最多为 256 个"; return false; } return true; @@ -269,7 +269,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( } if (pollBlockCount(normalized) > ProjectLimits::kMaximumPollBlocks) { - return {false, "PLC 轮询地址拆分后最多允许 64 个读块"}; + return {false, "PLC 轮询地址拆分后最多允许 8 个读块"}; } // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 if (pending_reply_ != nullptr) diff --git a/app/src/services/alarm_editor_service.cpp b/app/src/services/alarm_editor_service.cpp index d973125..479a0ed 100644 --- a/app/src/services/alarm_editor_service.cpp +++ b/app/src/services/alarm_editor_service.cpp @@ -39,7 +39,7 @@ AlarmEditorResult AlarmEditorService::addDefinition( { if (definitions().size() >= ProjectLimits::kMaximumAlarmDefinitions) { - return failure(AlarmEditorError::InvalidDefinition, "单个工程最多包含 512 条报警定义"); + return failure(AlarmEditorError::InvalidDefinition, "单个工程最多包含 256 条报警定义"); } AlarmDefinition candidate = definition; candidate.id = makeUniqueId(); diff --git a/app/src/services/editor_history.h b/app/src/services/editor_history.h index cd7d97e..87c5f56 100644 --- a/app/src/services/editor_history.h +++ b/app/src/services/editor_history.h @@ -16,8 +16,8 @@ template class EditorHistory final { public: - // 单个编辑服务最多保留 100 条撤销和重做历史 - static constexpr std::size_t kMaximumEntries = 100U; + // 单个编辑服务最多保留 30 条撤销和重做历史 + static constexpr std::size_t kMaximumEntries = 30U; template /** diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 180767e..a22f525 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -51,6 +51,16 @@ std::string makeUniquePageId(const Project &project) } } +std::size_t totalControlCount(const Project &project) +{ + std::size_t total = 0U; + for (const HmiPage &page : project.hmiPages) + { + total += page.controls.size(); + } + return total; +} + } // namespace HmiEditorService::HmiEditorService(ProjectService &project_service) @@ -213,11 +223,11 @@ HmiEditorResult HmiEditorService::addPage(const std::string &name) const Project ¤t = project_service_.project(); if (current.hmiPages.size() >= ProjectLimits::kMaximumHmiPages) { - return failure(HmiEditorError::InvalidPage, "单个工程最多包含 128 个 HMI 页面"); + return failure(HmiEditorError::InvalidPage, "单个工程最多包含 32 个 HMI 页面"); } if (name.size() > ProjectLimits::kMaximumTextBytes) { - return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); + return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); } const bool duplicate = std::any_of( current.hmiPages.cbegin(), current.hmiPages.cend(), @@ -284,7 +294,7 @@ HmiEditorResult HmiEditorService::renamePage( } if (name.size() > ProjectLimits::kMaximumTextBytes) { - return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); + return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); } const Project ¤t = project_service_.project(); const auto existing = std::find_if( @@ -425,7 +435,12 @@ HmiEditorResult HmiEditorService::addControl( } if (page->controls.size() >= ProjectLimits::kMaximumHmiControlsPerPage) { - return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 512 个控件"); + return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 128 个控件"); + } + if (totalControlCount(project_service_.project()) + >= ProjectLimits::kMaximumHmiControlsPerProject) + { + return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); } const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); if (descriptor == nullptr) @@ -620,7 +635,12 @@ HmiEditorResult HmiEditorService::pasteControls( || existing_page->controls.size() + controls.size() > ProjectLimits::kMaximumHmiControlsPerPage) { - return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 512 个控件"); + return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 128 个控件"); + } + if (totalControlCount(project_service_.project()) + controls.size() + > ProjectLimits::kMaximumHmiControlsPerProject) + { + return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); } // 让整组控件尽量保留相对位置,靠近页面边界时自动向页面内收拢 diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index 1691a79..2cdc2b3 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -45,6 +45,25 @@ std::string makeUniqueLogicId(const Project &project) } } +std::size_t totalRungCount(const Project &project) +{ + std::size_t total = 0U; + for (const ControlLogic &logic : project.controlLogics) + { + total += logic.rungs.size(); + } + return total; +} + +std::string rungLimitMessage(const Project &project, const ControlLogic &logic) +{ + return logic.rungs.size() >= ProjectLimits::kMaximumRungsPerLogic + ? "单组控制逻辑最多包含 256 个网络" + : totalRungCount(project) >= ProjectLimits::kMaximumRungsPerProject + ? "单个工程最多包含 2048 个梯形图网络" + : std::string{}; +} + LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) { return {id, config, false}; @@ -897,11 +916,11 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name) const Project ¤t = project_service_.project(); if (current.controlLogics.size() >= ProjectLimits::kMaximumControlLogics) { - return failure(LogicEditorError::InvalidOperation, "单个工程最多包含 128 组控制逻辑"); + return failure(LogicEditorError::InvalidOperation, "单个工程最多包含 32 组控制逻辑"); } if (name.size() > ProjectLimits::kMaximumTextBytes) { - return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); + return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 256 个 UTF-8 字节"); } const bool duplicate = std::any_of( current.controlLogics.cbegin(), current.controlLogics.cend(), @@ -930,7 +949,7 @@ LogicEditorResult LogicEditorService::renameLogic( } if (name.size() > ProjectLimits::kMaximumTextBytes) { - return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节"); + return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 256 个 UTF-8 字节"); } const Project ¤t = project_service_.project(); const auto logic = std::find_if( @@ -1050,9 +1069,11 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) { return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); } - if (logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = rungLimitMessage( + project_service_.project(), *logic); + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } LadderRung rung; rung.id = makeUniqueRungId(*logic); @@ -1142,10 +1163,11 @@ LogicEditorResult LogicEditorService::appendCondition( { return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); } - if (create_rung - && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = create_rung + ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } if (!isConditionConfig(config)) { @@ -1416,10 +1438,11 @@ LogicEditorResult LogicEditorService::appendWire( // 无选中目标的连续追加达到十列后,原子切换到下一个网络 create_rung = true; } - if (create_rung - && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = create_rung + ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } const std::string target_rung_id = create_rung ? makeUniqueRungId(*logic) : rung_id; @@ -2149,10 +2172,11 @@ LogicEditorResult LogicEditorService::setOutput( ? "未找到梯形图网络" : "梯形图输出必须使用有效的输出指令"); } - if (rung_id.empty() - && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = rung_id.empty() + ? rungLimitMessage(project_service_.project(), *logic) : std::string{}; + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } const bool create_rung = rung_id.empty(); const std::string target_rung_id = create_rung @@ -2282,10 +2306,12 @@ LogicEditorResult LogicEditorService::pasteConditionNodes( { return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); } - if (rung_id.empty() - && existing_logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = rung_id.empty() + ? rungLimitMessage(project_service_.project(), *existing_logic) + : std::string{}; + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } const HistoryState before = captureState(); @@ -2406,9 +2432,11 @@ LogicEditorResult LogicEditorService::pasteRung( { return failure(LogicEditorError::InvalidOperation, source_error); } - if (existing_logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) + const std::string limit_message = rungLimitMessage( + project_service_.project(), *existing_logic); + if (!limit_message.empty()) { - return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); + return failure(LogicEditorError::InvalidOperation, limit_message); } const HistoryState before = captureState(); diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index 46885dd..e9c71c8 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -271,7 +271,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); if (addresses.size() > ProjectLimits::kMaximumPollAddresses) { - return {false, "PLC 轮询的去重 M/D 地址最多为 1024 个"}; + return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"}; } return plc_gateway_->setPollAddresses(addresses); } diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 383e1a9..953215e 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -356,10 +356,38 @@ void testQuantityBoundaries() 400, {}}); } - require(project.validate(), "an HMI page count of 128 must be accepted"); + require(project.validate(), "an HMI page count at the configured limit must be accepted"); project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}}); require(!project.validate(), "an HMI page count of 129 must be rejected"); + project = makeValidProject(); + project.hmiPages.clear(); + project.initialHmiPageId.clear(); + for (std::size_t page_index = 0U; page_index < 17U; ++page_index) + { + HmiPage page{ + "bulk-page-" + std::to_string(page_index), + "Bulk page " + std::to_string(page_index), + 800, + 400, + {}}; + for (std::size_t control_index = 0U; + control_index < ProjectLimits::kMaximumHmiControlsPerPage; + ++control_index) + { + HmiControl label; + label.id = "label-" + std::to_string(control_index); + label.type = HmiControlType::Label; + label.bounds = {0, 0, 1, 1}; + label.text = "label"; + page.controls.push_back(std::move(label)); + } + project.hmiPages.push_back(std::move(page)); + } + project.initialHmiPageId = project.hmiPages.front().id; + require(!project.validate(), + "an HMI control count over the project limit must be rejected"); + project = makeValidProject(); project.hmiPages.front().controls.clear(); for (std::size_t index = 0U; @@ -373,7 +401,7 @@ void testQuantityBoundaries() label.text = "label"; project.hmiPages.front().controls.push_back(std::move(label)); } - require(project.validate(), "a page control count of 512 must be accepted"); + require(project.validate(), "a page control count at the configured limit must be accepted"); HmiControl extra_label; extra_label.id = "label-over"; extra_label.type = HmiControlType::Label; @@ -382,6 +410,31 @@ void testQuantityBoundaries() project.hmiPages.front().controls.push_back(std::move(extra_label)); require(!project.validate(), "a page control count of 513 must be rejected"); + project = makeValidProject(); + project.controlLogics.clear(); + for (std::size_t logic_index = 0U; logic_index < 9U; ++logic_index) + { + ControlLogic logic{ + "bulk-logic-" + std::to_string(logic_index), + "Bulk logic " + std::to_string(logic_index), + {}, + true}; + for (std::size_t rung_index = 0U; + rung_index < ProjectLimits::kMaximumRungsPerLogic; + ++rung_index) + { + logic.rungs.push_back({ + "rung-" + std::to_string(rung_index), + "Rung " + std::to_string(rung_index), + {}, + std::nullopt, + {}}); + } + project.controlLogics.push_back(std::move(logic)); + } + require(!project.validate(), + "a ladder rung count over the project limit must be rejected"); + project = makeValidProject(); project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight; @@ -427,12 +480,12 @@ void testQuantityBoundaries() ConditionExpression maximum_depth = makeNested( static_cast(ProjectLimits::kMaximumExpressionDepth), &next_address); require(maximum_depth.validate(), - "an expression depth of 20 must be accepted"); + "an expression depth at the configured limit must be accepted"); ConditionExpression excessive_depth = makeNested( static_cast(ProjectLimits::kMaximumExpressionDepth) + 1, &next_address); require(!excessive_depth.validate(), - "an expression depth of 21 must be rejected"); + "an expression depth above the configured limit must be rejected"); (void)leaf; } diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index f1e4871..263bfd2 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -2,6 +2,7 @@ #include "domain/register_repository.h" #include "domain/virtual_register_repository.h" #include "services/hmi_editor_service.h" +#include "services/editor_history.h" #include "services/hmi_runtime_service.h" #include "services/hmi_navigation_service.h" #include "services/project_service.h" @@ -218,8 +219,8 @@ void testHistoryAndAtomicBatchDelete() { ++undo_count; } - require(undo_count == 100, - "HMI history must retain exactly the configured 100 most recent steps"); + require(undo_count == static_cast(EditorHistory::kMaximumEntries), + "HMI history must retain exactly the configured most recent steps"); } void testBatchPasteControls() @@ -262,6 +263,82 @@ void testBatchPasteControls() "batch control paste must be one undoable operation"); } +void testRuntimeValidationAndPasteBoundaries() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor(project_service); + const std::string page_id = editor.ensureDefaultPage().id; + + HmiControl label; + label.id = "label-source"; + label.type = HmiControlType::Label; + label.bounds = {760, 360, 32, 32}; + label.text = "边界"; + require(editor.pasteControls(page_id, {label}, 20, 20).succeeded, + "a paste near the page edge must be clamped into the page"); + const HmiPage *page = editor.findPage(page_id); + require(page != nullptr && page->controls.size() == 1U, + "edge paste must add exactly one control"); + const HmiControl &edge_copy = page->controls.front(); + require(edge_copy.bounds.x + edge_copy.bounds.width <= page->width + && edge_copy.bounds.y + edge_copy.bounds.height <= page->height + && edge_copy.bounds.x >= 0 && edge_copy.bounds.y >= 0, + "edge paste must keep the copied control inside page bounds"); + + require(!editor.pasteControls(page_id, {}).succeeded, + "an empty HMI clipboard must be rejected"); + HmiControl oversized = label; + oversized.bounds = {0, 0, page->width + 1, 32}; + require(!editor.pasteControls(page_id, {oversized}).succeeded + && editor.findPage(page_id)->controls.size() == 1U, + "a copied control wider than the target page must fail atomically"); + HmiControl unsupported = label; + unsupported.type = HmiControlType::Count; + require(!editor.pasteControls(page_id, {unsupported}).succeeded + && editor.findPage(page_id)->controls.size() == 1U, + "an unsupported copied HMI type must fail atomically"); + + std::vector too_many(513U, label); + require(!editor.pasteControls(page_id, too_many).succeeded + && editor.findPage(page_id)->controls.size() == 1U, + "a paste batch above the page limit must be rejected before mutation"); + + VirtualRegisterRepository repository; + HmiRuntimeService runtime(repository); + HmiControl unbound_indicator; + unbound_indicator.type = HmiControlType::Indicator; + require(runtime.readControl(unbound_indicator).error + == HmiRuntimeError::MissingBinding, + "an unbound runtime indicator must report MissingBinding"); + HmiControl wrong_area_indicator = unbound_indicator; + wrong_area_indicator.binding = RegisterAddress{RegisterArea::D, 0}; + require(runtime.readControl(wrong_area_indicator).error + == HmiRuntimeError::InvalidBinding, + "an indicator bound to D must report InvalidBinding"); + HmiControl invalid_indicator = unbound_indicator; + invalid_indicator.binding = RegisterAddress{RegisterArea::M, 4001}; + require(runtime.readControl(invalid_indicator).error + == HmiRuntimeError::InvalidBinding, + "an indicator with an out-of-range address must report InvalidBinding"); + require(runtime.readControl(label).error + == HmiRuntimeError::UnsupportedControl, + "a static label must not be treated as a register runtime control"); + + HmiControl wrong_button = unbound_indicator; + wrong_button.type = HmiControlType::Button; + wrong_button.binding = RegisterAddress{RegisterArea::D, 0}; + require(runtime.operateButton(wrong_button, HmiButtonEvent::Pressed).error + == HmiRuntimeError::InvalidBinding, + "a button bound to D must reject runtime writes"); + HmiControl wrong_numeric = unbound_indicator; + wrong_numeric.type = HmiControlType::NumericInput; + wrong_numeric.binding = RegisterAddress{RegisterArea::M, 0}; + require(runtime.writeNumericInput(wrong_numeric, 1).error + == HmiRuntimeError::InvalidBinding, + "a numeric input bound to M must reject runtime writes"); +} + void testAppearanceEditing() { TestProjectStorage storage; @@ -417,6 +494,7 @@ int main() testControlEditing(); testHistoryAndAtomicBatchDelete(); testBatchPasteControls(); + testRuntimeValidationAndPasteBoundaries(); testAppearanceEditing(); testRuntimeUsesRegisterRepository(); testPageLifecycleAndNavigation(); diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index 0b6d35e..1536c10 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -1,6 +1,7 @@ #include "domain/project_storage.h" #include "domain/project_limits.h" #include "services/logic_editor_service.h" +#include "services/editor_history.h" #include "services/project_service.h" #include "support/test_support.h" @@ -1003,8 +1004,8 @@ void testHistoryAndAtomicBatchDelete() { ++undo_count; } - require(undo_count == 100, - "logic history must retain exactly the configured 100 most recent steps"); + require(undo_count == static_cast(EditorHistory::kMaximumEntries), + "logic history must retain exactly the configured most recent steps"); require(service.redo().succeeded, "logic redo must be available after an undo"); @@ -1110,6 +1111,126 @@ void testBatchPasteNodesAndRung() "whole rung paste must regenerate ids and preserve output configuration"); } +void testConditionPasteTargetsAndValidation() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + LogicEditorService service(project_service); + const std::string logic_id = service.ensureDefaultLogic().id; + + LogicNode source; + source.id = "copied-condition"; + source.config = contact(90); + source.configured = true; + + const std::string empty_column_rung = makeEmptyRung(service, logic_id); + LogicConditionPasteTarget empty_column; + empty_column.kind = LogicConditionPasteTargetKind::EmptyColumn; + empty_column.column = 2; + require(service.pasteConditionNodes( + logic_id, empty_column_rung, {source}, empty_column).succeeded, + "condition paste must support an empty grid column"); + require(conditionColumns(*service.findRung( + logic_id, empty_column_rung)->condition) == 3, + "empty-column paste must preserve the requested column offset"); + + const std::string branch_rung = makeEmptyRung(service, logic_id); + const LogicEditorResult branch_source = service.appendCondition( + logic_id, branch_rung, contact(1)); + const LogicEditorResult branch_source_two = service.appendCondition( + logic_id, branch_rung, contact(2)); + const LogicEditorResult branch_source_three = service.appendCondition( + logic_id, branch_rung, contact(3)); + require(branch_source.succeeded && branch_source_two.succeeded + && branch_source_three.succeeded, + "branch paste setup must add a source range"); + const LogicEditorResult branch = service.addParallelBranch( + logic_id, branch_rung, + {branch_source.id, branch_source_two.id, branch_source_three.id}, + contact(4)); + require(branch.succeeded, "branch paste setup must create a parallel branch"); + LogicConditionPasteTarget branch_target; + branch_target.kind = LogicConditionPasteTargetKind::BranchEmptyColumn; + branch_target.expressionId = branch.id; + branch_target.column = 2; + require(service.pasteConditionNodes( + logic_id, branch_rung, {source}, branch_target).succeeded, + "condition paste must support a visible empty cell in a branch"); + + const std::string after_node_rung = makeEmptyRung(service, logic_id); + const LogicEditorResult after_source = service.appendCondition( + logic_id, after_node_rung, contact(3)); + require(after_source.succeeded, "after-node paste setup must add a source node"); + LogicConditionPasteTarget after_target; + after_target.kind = LogicConditionPasteTargetKind::AfterNode; + after_target.expressionId = after_source.id; + require(service.pasteConditionNodes( + logic_id, after_node_rung, {source}, after_target).succeeded, + "condition paste must support insertion after a selected node"); + require(conditionNodes(*service.findRung( + logic_id, after_node_rung)->condition) == 2, + "after-node paste must add exactly one condition"); + + const std::string replace_wire_rung = makeEmptyRung(service, logic_id); + const LogicEditorResult wire = service.appendWire( + logic_id, replace_wire_rung, 2); + require(wire.succeeded, "replace-wire paste setup must add a wire"); + LogicConditionPasteTarget replace_wire; + replace_wire.kind = LogicConditionPasteTargetKind::ReplaceWire; + replace_wire.expressionId = wire.id; + require(service.pasteConditionNodes( + logic_id, replace_wire_rung, {source}, replace_wire).succeeded, + "condition paste must replace an entire wire expression"); + require(conditionNodes(*service.findRung( + logic_id, replace_wire_rung)->condition) == 1, + "whole-wire paste must create one condition node"); + + const std::string replace_cell_rung = makeEmptyRung(service, logic_id); + const LogicEditorResult cell_wire = service.appendWire( + logic_id, replace_cell_rung, 3); + require(cell_wire.succeeded, "replace-cell paste setup must add a wire"); + LogicConditionPasteTarget replace_cell; + replace_cell.kind = LogicConditionPasteTargetKind::ReplaceWireColumn; + replace_cell.expressionId = cell_wire.id; + replace_cell.column = 1; + require(service.pasteConditionNodes( + logic_id, replace_cell_rung, {source}, replace_cell).succeeded, + "condition paste must replace one selected wire cell"); + require(conditionColumns(*service.findRung( + logic_id, replace_cell_rung)->condition) == 3, + "wire-cell paste must preserve the original network width"); + + require(!service.pasteConditionNodes(logic_id, after_node_rung, {}).succeeded, + "an empty condition clipboard must be rejected"); + LogicNode output = source; + output.config = CoilNodeConfig{RegisterAddress{RegisterArea::M, 91}, CoilMode::Normal}; + require(!service.pasteConditionNodes( + logic_id, after_node_rung, {output}).succeeded, + "an output instruction must not be pasted into the condition area"); + require(!service.pasteConditionNodes( + "missing-logic", after_node_rung, {source}).succeeded, + "condition paste must reject an unknown logic"); + require(!service.pasteConditionNodes( + logic_id, "missing-rung", {source}).succeeded, + "condition paste must reject an unknown rung"); + + const LadderRung *before_invalid = service.findRung( + logic_id, after_node_rung); + require(before_invalid != nullptr && before_invalid->condition.has_value(), + "invalid paste setup must retain its target network"); + const int node_count_before_invalid = conditionNodes(*before_invalid->condition); + LogicConditionPasteTarget invalid_target; + invalid_target.kind = LogicConditionPasteTargetKind::AfterNode; + invalid_target.expressionId = "missing-expression"; + service.clearHistory(); + require(!service.pasteConditionNodes( + logic_id, after_node_rung, {source}, invalid_target).succeeded + && conditionNodes(*service.findRung( + logic_id, after_node_rung)->condition) == node_count_before_invalid + && !service.canUndo(), + "an invalid paste target must roll back without recording history"); +} + } // namespace int main() @@ -1134,6 +1255,7 @@ int main() testEdgeNodesAndRungComments(); testHistoryAndAtomicBatchDelete(); testBatchPasteNodesAndRung(); + testConditionPasteTargetsAndValidation(); } catch (const std::exception &error) { diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp index 57c0600..d7f8e8c 100644 --- a/app/tests/plc_runtime_tests.cpp +++ b/app/tests/plc_runtime_tests.cpp @@ -342,15 +342,17 @@ void testPlcPollQuantityBoundaries() } too_many_addresses.push_back({RegisterArea::D, 0}); require(!service.setPollAddresses(too_many_addresses).succeeded, - "more than 1024 poll addresses must be rejected"); + "more than the configured poll address limit must be rejected"); std::vector too_many_blocks; - for (int index = 0; index < 65; ++index) + for (std::size_t index = 0; + index < ProjectLimits::kMaximumPollBlocks + 1U; + ++index) { - too_many_blocks.push_back({RegisterArea::M, index * 2}); + too_many_blocks.push_back({RegisterArea::M, static_cast(index * 2U)}); } require(!service.setPollAddresses(too_many_blocks).succeeded, - "more than 64 Modbus read blocks must be rejected"); + "more than the configured Modbus read block limit must be rejected"); std::vector maximum_contiguous_block; for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index) diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index b75dd5d..dbcdbb7 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -719,7 +719,7 @@ void testQuantityFileLimits() storage.load(too_many_pages_path.toStdString()); require(!too_many_pages_result.succeeded && too_many_pages_result.error == ProjectStorageError::InvalidField, - "a JSON page array over 128 items must be rejected"); + "a JSON page array over the configured limit must be rejected"); service.editProject() = makeExampleProject(); require(service.saveAs(valid_path.toStdString()).succeeded, @@ -766,7 +766,7 @@ void testQuantityFileLimits() const ProjectLoadResult deep_result = storage.load(deep_path.toStdString()); require(!deep_result.succeeded && deep_result.error == ProjectStorageError::InvalidField, - "a condition expression deeper than 20 levels must be rejected while parsing"); + "a condition expression deeper than the configured limit must be rejected while parsing"); } void testHmiPageJsonBoundaries()