Pārlūkot izejas kodu

feat: 统一项目数量边界与梯形图运行语义

main
suyu pirms 1 mēnesi
vecāks
revīzija
6a2b79b477
26 mainītis faili ar 1226 papildinājumiem un 123 dzēšanām
  1. +1
    -0
      app/integrated_platform.pro
  2. +12
    -0
      app/src/domain/alarm_model.cpp
  3. +173
    -52
      app/src/domain/control_logic_model.cpp
  4. +4
    -2
      app/src/domain/control_logic_model.h
  5. +54
    -4
      app/src/domain/hmi_model.cpp
  6. +52
    -0
      app/src/domain/project_limits.h
  7. +94
    -0
      app/src/domain/project_model.cpp
  8. +147
    -34
      app/src/infrastructure/json_project_storage.cpp
  9. +101
    -9
      app/src/infrastructure/plc_communication_service.cpp
  10. +3
    -1
      app/src/infrastructure/plc_communication_service.h
  11. +5
    -0
      app/src/services/alarm_editor_service.cpp
  12. +17
    -0
      app/src/services/hmi_editor_service.cpp
  13. +72
    -0
      app/src/services/logic_editor_service.cpp
  14. +59
    -1
      app/src/services/plc_communication_gateway.h
  15. +13
    -4
      app/src/services/runtime_mode_service.cpp
  16. +1
    -1
      app/src/services/runtime_mode_service.h
  17. +13
    -8
      app/src/services/software_logic_executor.cpp
  18. +126
    -2
      app/tests/domain_tests.cpp
  19. +1
    -0
      app/tests/domain_tests.pro
  20. +51
    -0
      app/tests/logic_editor_service_tests.cpp
  21. +23
    -0
      app/tests/offline_simulation_service_tests.cpp
  22. +69
    -1
      app/tests/plc_runtime_tests.cpp
  23. +6
    -3
      app/tests/plc_runtime_tests.pro
  24. +126
    -0
      app/tests/project_management_tests.cpp
  25. +1
    -0
      app/tests/project_management_tests.pro
  26. +2
    -1
      app/tests/runtime_mode_service_tests.cpp

+ 1
- 0
app/integrated_platform.pro Parādīt failu

@@ -76,6 +76,7 @@ HEADERS += \
src/domain/hmi_model.h \
src/domain/hmi_control_registry.h \
src/domain/control_logic_model.h \
src/domain/project_limits.h \
src/domain/project_model.h \
src/domain/runtime_state.h \
src/domain/project_storage.h \


+ 12
- 0
app/src/domain/alarm_model.cpp Parādīt failu

@@ -1,5 +1,7 @@
#include "alarm_model.h"

#include "project_limits.h"

namespace {

void setError(std::string *error, const std::string &message)
@@ -19,11 +21,21 @@ bool AlarmDefinition::validate(std::string *error) const
setError(error, "报警定义 ID 不能为空");
return false;
}
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "报警定义 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (message.empty())
{
setError(error, "报警文本不能为空");
return false;
}
if (message.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "报警文本不能超过 4096 个 UTF-8 字节");
return false;
}
if (!address.isValid())
{
setError(error, "报警定义使用了无效地址");


+ 173
- 52
app/src/domain/control_logic_model.cpp Parādīt failu

@@ -1,5 +1,7 @@
#include "control_logic_model.h"

#include "project_limits.h"

#include <algorithm>
#include <map>
#include <type_traits>
@@ -161,6 +163,12 @@ bool validateConfig(const CounterNodeConfig &config, std::string *error)
{
return false;
}
if (config.preset.kind == WordOperandKind::Constant
&& config.preset.constant < 0)
{
setError(error, "计数器常量预设值必须在 0~32767 范围内");
return false;
}
if (!config.resetAddress.isValid()
|| config.resetAddress.area() != RegisterArea::M)
{
@@ -272,6 +280,131 @@ void appendValidAddress(
}
}

bool validateConditionExpression(
const ConditionExpression &expression,
std::size_t depth,
std::size_t *node_count,
int *columns,
int *rows,
std::string *error)
{
if (depth > ProjectLimits::kMaximumExpressionDepth)
{
setError(error, "条件表达式最多嵌套 20 层");
return false;
}
++*node_count;
if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung)
{
setError(error, "单个网络最多包含 4096 个条件表达式节点和叶子");
return false;
}
if (expression.id.empty())
{
setError(error, "条件表达式 ID 不能为空");
return false;
}
if (expression.id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "条件表达式 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (expression.kind == ConditionExpressionKind::Node)
{
if (!expression.node.has_value() || expression.wire.has_value()
|| !expression.children.empty() || !expression.node->isCondition())
{
setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式");
return false;
}
*columns = 1;
*rows = 1;
return expression.node->validate(error);
}
if (expression.kind == ConditionExpressionKind::Wire)
{
if (expression.node.has_value() || !expression.wire.has_value()
|| !expression.children.empty())
{
setError(error, "横线叶节点必须包含横线配置且不能包含逻辑节点或子表达式");
return false;
}
if (!expression.wire->validate(error))
{
return false;
}
*columns = expression.wire->columnSpan;
*rows = 1;
return true;
}
if (expression.kind != ConditionExpressionKind::Series
&& expression.kind != ConditionExpressionKind::Parallel)
{
setError(error, "条件表达式使用了不支持的类型");
return false;
}
if (expression.node.has_value() || expression.wire.has_value()
|| expression.children.size() < 2U)
{
setError(error, "串联和并联表达式至少需要两个子表达式");
return false;
}
if (expression.children.size() > ProjectLimits::kMaximumExpressionChildren)
{
setError(error, "单个串联或并联容器最多包含 256 个子表达式");
return false;
}

int total_columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1;
int total_rows = expression.kind == ConditionExpressionKind::Parallel ? 0 : 1;
for (const ConditionExpression &child : expression.children)
{
if (child.kind == expression.kind)
{
setError(error, "同类型嵌套表达式必须先完成归一化");
return false;
}
int child_columns = 0;
int child_rows = 0;
if (!validateConditionExpression(
child, depth + 1U, node_count,
&child_columns, &child_rows, error))
{
return false;
}
if (expression.kind == ConditionExpressionKind::Series)
{
if (total_columns
> ProjectLimits::kMaximumConditionColumns - child_columns)
{
setError(error, "单个网络的条件区最多为 10 列,第 11 列固定用于输出指令");
return false;
}
total_columns += child_columns;
total_rows = std::max(total_rows, child_rows);
}
else
{
total_columns = std::max(total_columns, child_columns);
if (total_rows > ProjectLimits::kMaximumLogicRows - child_rows)
{
setError(error, "单个网络的逻辑总行数最多为 256 行");
return false;
}
total_rows += child_rows;
}
}
if (total_columns > ProjectLimits::kMaximumConditionColumns
|| total_rows > ProjectLimits::kMaximumLogicRows)
{
setError(error, "单个网络最多使用 10 列条件、1 列输出和 256 行逻辑网格");
return false;
}
*columns = total_columns;
*rows = total_rows;
return true;
}

} // namespace

TimerAddress::TimerAddress(int index)
@@ -444,6 +577,11 @@ bool LogicNode::validate(std::string *error) const
setError(error, "逻辑节点 ID 不能为空");
return false;
}
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "逻辑节点 ID 不能超过 128 个 UTF-8 字节");
return false;
}
return std::visit(
[error](const auto &config) { return validateConfig(config, error); },
config);
@@ -485,7 +623,7 @@ bool WireSegment::validate(std::string *error) const
{
if (columnSpan < kMinimumColumnSpan || columnSpan > kMaximumColumnSpan)
{
setError(error, "横线跨度必须在 1~256 列范围内");
setError(error, "横线跨度必须在 1~10 列范围内");
return false;
}
return true;
@@ -503,54 +641,11 @@ ConditionExpression ConditionExpression::fromWire(

bool ConditionExpression::validate(std::string *error) const
{
if (id.empty())
{
setError(error, "条件表达式 ID 不能为空");
return false;
}
if (kind == ConditionExpressionKind::Node)
{
if (!node.has_value() || wire.has_value()
|| !children.empty() || !node->isCondition())
{
setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式");
return false;
}
return node->validate(error);
}
if (kind == ConditionExpressionKind::Wire)
{
if (node.has_value() || !wire.has_value() || !children.empty())
{
setError(error, "横线叶节点必须包含横线配置且不能包含逻辑节点或子表达式");
return false;
}
return wire->validate(error);
}
if (kind != ConditionExpressionKind::Series
&& kind != ConditionExpressionKind::Parallel)
{
setError(error, "条件表达式使用了不支持的类型");
return false;
}
if (node.has_value() || wire.has_value() || children.size() < 2U)
{
setError(error, "串联和并联表达式至少需要两个子表达式");
return false;
}
for (const ConditionExpression &child : children)
{
if (child.kind == kind)
{
setError(error, "同类型嵌套表达式必须先完成归一化");
return false;
}
if (!child.validate(error))
{
return false;
}
}
return true;
std::size_t node_count = 0U;
int columns = 0;
int rows = 0;
return validateConditionExpression(
*this, 1U, &node_count, &columns, &rows, error);
}

bool ConditionExpression::validateForRunning(std::string *error) const
@@ -701,12 +796,12 @@ bool LadderRung::validateForRunning(std::string *error) const
{
return true;
}
if (!condition.has_value() || !output.has_value())
if (!output.has_value())
{
setError(error, "未完成的梯形图网络必须同时包含条件和输出节点");
setError(error, "梯形图网络必须包含输出指令");
return false;
}
if (!condition->validateForRunning(error))
if (condition.has_value() && !condition->validateForRunning(error))
{
return false;
}
@@ -725,6 +820,17 @@ bool LadderRung::validateStructure(std::string *error) const
setError(error, "梯形图网络 ID 和名称不能为空");
return false;
}
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "梯形图网络 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (name.size() > ProjectLimits::kMaximumTextBytes
|| comment.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "梯形图网络名称和注释不能超过 4096 个 UTF-8 字节");
return false;
}
std::vector<std::string> node_ids;
if (condition.has_value())
{
@@ -780,6 +886,21 @@ bool ControlLogic::validateStructure(std::string *error) const
setError(error, "控制逻辑 ID 和名称不能为空");
return false;
}
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "控制逻辑 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "控制逻辑名称不能超过 4096 个 UTF-8 字节");
return false;
}
if (rungs.size() > ProjectLimits::kMaximumRungsPerLogic)
{
setError(error, "单组控制逻辑最多包含 1024 个网络");
return false;
}
if (hasDuplicateId(rungs))
{
setError(error, "同一控制逻辑内的网络 ID 必须唯一");


+ 4
- 2
app/src/domain/control_logic_model.h Parādīt failu

@@ -1,5 +1,6 @@
#pragma once

#include "project_limits.h"
#include "register_address.h"

#include <cstdint>
@@ -212,7 +213,8 @@ enum class ConditionExpressionKind
struct WireSegment
{
static constexpr int kMinimumColumnSpan = 1;
static constexpr int kMaximumColumnSpan = 256;
static constexpr int kMaximumColumnSpan =
ProjectLimits::kMaximumConditionColumns;

int columnSpan = 1;

@@ -249,7 +251,7 @@ void collectConditionNodes(
void collectConditionExpressionIds(
const ConditionExpression &expression, std::vector<std::string> *ids);

// 一个网络包含一棵结构化条件表达式,输出指令固定在最右侧
// 条件表达式为空时表示恒真网络,输出指令固定在最右侧
struct LadderRung
{
std::string id;


+ 54
- 4
app/src/domain/hmi_model.cpp Parādīt failu

@@ -1,6 +1,7 @@
#include "hmi_model.h"

#include "hmi_control_registry.h"
#include "project_limits.h"

#include <algorithm>

@@ -47,9 +48,26 @@ bool HmiControl::validate(std::string *error) const
setError(error, "HMI 控件 ID 不能为空");
return false;
}
if (bounds.width <= 0 || bounds.height <= 0)
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "HMI 控件的宽度和高度必须大于 0");
setError(error, "HMI 控件 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (text.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "HMI 控件文本不能超过 4096 个 UTF-8 字节");
return false;
}
if (bounds.width <= 0 || bounds.height <= 0
|| bounds.width > ProjectLimits::kMaximumHmiControlWidth
|| bounds.height > ProjectLimits::kMaximumHmiControlHeight)
{
setError(error, "HMI 控件宽度和高度必须在 1~8192 范围内");
return false;
}
if (properties.size() > ProjectLimits::kMaximumHmiProperties)
{
setError(error, "单个 HMI 控件最多保存 64 对扩展属性");
return false;
}
for (const auto &property : properties)
@@ -59,6 +77,16 @@ bool HmiControl::validate(std::string *error) const
setError(error, "HMI 控件属性名称不能为空");
return false;
}
if (property.first.size() > ProjectLimits::kMaximumPropertyKeyBytes)
{
setError(error, "HMI 控件属性名称不能超过 128 个 UTF-8 字节");
return false;
}
if (property.second.size() > ProjectLimits::kMaximumPropertyValueBytes)
{
setError(error, "HMI 控件属性值不能超过 4096 个 UTF-8 字节");
return false;
}
}
const std::optional<RegisterArea> binding_area =
hmiBindingArea(descriptor->bindingKind);
@@ -92,6 +120,11 @@ bool HmiControl::validate(std::string *error) const
setError(error, "页面跳转控件缺少跳转配置");
return false;
}
if (pageJump->targetPageId.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "页面跳转目标 ID 不能超过 128 个 UTF-8 字节");
return false;
}
}
else if (pageJump.has_value())
{
@@ -153,9 +186,26 @@ bool HmiPage::validate(std::string *error) const
setError(error, "HMI 页面 ID 和名称不能为空");
return false;
}
if (width <= 0 || height <= 0)
if (id.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "HMI 页面 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "HMI 页面名称不能超过 4096 个 UTF-8 字节");
return false;
}
if (width <= 0 || height <= 0
|| width > ProjectLimits::kMaximumHmiPageWidth
|| height > ProjectLimits::kMaximumHmiPageHeight)
{
setError(error, "HMI 页面宽度和高度必须在 1~8192 范围内");
return false;
}
if (controls.size() > ProjectLimits::kMaximumHmiControlsPerPage)
{
setError(error, "HMI 页面尺寸必须大于 0");
setError(error, "单个 HMI 页面最多包含 512 个控件");
return false;
}



+ 52
- 0
app/src/domain/project_limits.h Parādīt failu

@@ -0,0 +1,52 @@
#pragma once

#include <cstddef>

namespace ProjectLimits {

constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U;

constexpr std::size_t kMaximumHmiPages = 128U;
constexpr std::size_t kMaximumHmiControlsPerPage = 512U;
constexpr std::size_t kMaximumAlarmDefinitions = 512U;
constexpr std::size_t kMaximumRegisterComments = 8002U;
constexpr std::size_t kMaximumControlLogics = 128U;
constexpr std::size_t kMaximumRungsPerLogic = 1024U;

constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U;
constexpr std::size_t kMaximumExpressionDepth = 20U;
constexpr std::size_t kMaximumExpressionChildren = 256U;
constexpr int kMaximumConditionColumns = 10;
constexpr int kMaximumLadderColumns = 11;
constexpr int kMaximumLogicRows = 256;

static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1);

constexpr std::size_t kMaximumHmiProperties = 64U;
constexpr std::size_t kMaximumIdBytes = 128U;
constexpr std::size_t kMaximumTextBytes = 4096U;
constexpr std::size_t kMaximumPropertyKeyBytes = 128U;
constexpr std::size_t kMaximumPropertyValueBytes = 4096U;

constexpr int kMaximumHmiPageWidth = 8192;
constexpr int kMaximumHmiPageHeight = 8192;
constexpr int kMaximumHmiControlWidth = 8192;
constexpr int kMaximumHmiControlHeight = 8192;

constexpr std::size_t kMaximumPollAddresses = 1024U;
constexpr std::size_t kMaximumPollBlocks = 64U;
constexpr int kMaximumModbusReadCount = 120;

constexpr int kMinimumPlcServerAddress = 1;
constexpr int kMaximumPlcServerAddress = 247;
constexpr int kMinimumResponseTimeoutMs = 100;
constexpr int kMaximumResponseTimeoutMs = 30000;
constexpr int kMinimumRetries = 0;
constexpr int kMaximumRetries = 5;
constexpr int kMinimumPollIntervalMs = 50;
constexpr int kMaximumPollIntervalMs = 10000;

constexpr std::size_t kMaximumPendingWrites = 1U;
constexpr int kMaximumOutputMessages = 1000;

} // namespace ProjectLimits

+ 94
- 0
app/src/domain/project_model.cpp Parādīt failu

@@ -1,5 +1,7 @@
#include "project_model.h"

#include "project_limits.h"

#include <algorithm>
#include <cctype>

@@ -76,6 +78,11 @@ bool RegisterComment::validate(std::string *error) const
setError(error, "软元件注释内容不能为空");
return false;
}
if (text.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "软元件注释不能超过 4096 个 UTF-8 字节");
return false;
}
return true;
}

@@ -98,6 +105,42 @@ bool Project::validate(std::string *error) const
setError(error, "工程 ID、名称和格式版本不能为空");
return false;
}
if (metadata.id.size() > ProjectLimits::kMaximumIdBytes
|| metadata.formatVersion.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "工程 ID 和格式版本不能超过 128 个 UTF-8 字节");
return false;
}
if (metadata.name.size() > ProjectLimits::kMaximumTextBytes)
{
setError(error, "工程名称不能超过 4096 个 UTF-8 字节");
return false;
}
if (initialHmiPageId.size() > ProjectLimits::kMaximumIdBytes)
{
setError(error, "初始 HMI 页面 ID 不能超过 128 个 UTF-8 字节");
return false;
}
if (hmiPages.size() > ProjectLimits::kMaximumHmiPages)
{
setError(error, "单个工程最多包含 128 个 HMI 页面");
return false;
}
if (alarmDefinitions.size() > ProjectLimits::kMaximumAlarmDefinitions)
{
setError(error, "单个工程最多包含 512 条报警定义");
return false;
}
if (registerComments.size() > ProjectLimits::kMaximumRegisterComments)
{
setError(error, "单个工程最多包含 8002 条软元件注释");
return false;
}
if (controlLogics.size() > ProjectLimits::kMaximumControlLogics)
{
setError(error, "单个工程最多包含 128 组控制逻辑");
return false;
}
if (containsDuplicateId(hmiPages))
{
setError(error, "工程内的 HMI 页面 ID 必须唯一");
@@ -203,6 +246,57 @@ bool Project::validate(std::string *error) const
return false;
}
}

std::vector<RegisterAddress> poll_addresses;
for (const HmiPage &page : hmiPages)
{
for (const HmiControl &control : page.controls)
{
if (control.binding.has_value())
{
poll_addresses.push_back(*control.binding);
}
}
}
for (const AlarmDefinition &definition : alarmDefinitions)
{
poll_addresses.push_back(definition.address);
}
for (const ControlLogic &logic : controlLogics)
{
for (const LadderRung &rung : logic.rungs)
{
std::vector<const LogicNode *> nodes;
if (rung.condition.has_value())
{
collectConditionNodes(*rung.condition, &nodes);
}
if (rung.output.has_value())
{
nodes.push_back(&*rung.output);
}
for (const LogicNode *node : nodes)
{
collectRegisterAddressesForLogicNode(node->config, &poll_addresses);
}
}
}
std::sort(
poll_addresses.begin(), poll_addresses.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
poll_addresses.erase(
std::unique(poll_addresses.begin(), poll_addresses.end()),
poll_addresses.end());
if (poll_addresses.size() > ProjectLimits::kMaximumPollAddresses)
{
setError(error, "工程中需要 PLC 轮询的去重 M/D 地址最多为 1024 个");
return false;
}
return true;
}



+ 147
- 34
app/src/infrastructure/json_project_storage.cpp Parādīt failu

@@ -1,6 +1,7 @@
#include "json_project_storage.h"

#include "domain/hmi_control_registry.h"
#include "domain/project_limits.h"

#include <QFile>
#include <QJsonArray>
@@ -81,7 +82,8 @@ bool readString(
const char *field,
const std::string &context,
std::string *value,
ParseState *state)
ParseState *state,
std::size_t maximum_bytes = ProjectLimits::kMaximumTextBytes)
{
QJsonValue json_value;
if (!readValue(object, field, context, &json_value, state))
@@ -95,6 +97,12 @@ bool readString(
fieldPath(context, field) + " 必须是字符串");
}
*value = toUtf8(json_value.toString());
if (value->size() > maximum_bytes)
{
return state->fail(
ProjectStorageError::InvalidField,
fieldPath(context, field) + " 超出允许的 UTF-8 字节长度");
}
return true;
}

@@ -185,7 +193,8 @@ bool readArray(
const char *field,
const std::string &context,
QJsonArray *value,
ParseState *state)
ParseState *state,
int maximum_count = std::numeric_limits<int>::max())
{
QJsonValue json_value;
if (!readValue(object, field, context, &json_value, state))
@@ -199,6 +208,12 @@ bool readArray(
fieldPath(context, field) + " 必须是数组");
}
*value = json_value.toArray();
if (value->size() > maximum_count)
{
return state->fail(
ProjectStorageError::InvalidField,
fieldPath(context, field) + " 的元素数量超过上限");
}
return true;
}

@@ -535,7 +550,9 @@ bool parseAlarmDefinition(
QJsonObject address;
std::string condition;
int threshold = 0;
if (!readString(object, "id", context, &definition->id, state)
if (!readString(
object, "id", context, &definition->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readObject(object, "address", context, &address, state)
|| !readString(object, "condition", context, &condition, state)
|| !readInt(
@@ -604,16 +621,16 @@ bool parseBounds(
object,
"x",
context,
std::numeric_limits<int>::min(),
std::numeric_limits<int>::max(),
0,
ProjectLimits::kMaximumHmiPageWidth,
&bounds->x,
state)
&& readInt(
object,
"y",
context,
std::numeric_limits<int>::min(),
std::numeric_limits<int>::max(),
0,
ProjectLimits::kMaximumHmiPageHeight,
&bounds->y,
state)
&& readInt(
@@ -621,7 +638,7 @@ bool parseBounds(
"width",
context,
1,
std::numeric_limits<int>::max(),
ProjectLimits::kMaximumHmiControlWidth,
&bounds->width,
state)
&& readInt(
@@ -629,7 +646,7 @@ bool parseBounds(
"height",
context,
1,
std::numeric_limits<int>::max(),
ProjectLimits::kMaximumHmiControlHeight,
&bounds->height,
state);
}
@@ -651,6 +668,12 @@ bool parseProperties(
std::map<std::string, std::string> *properties,
ParseState *state)
{
if (object.size() > static_cast<int>(ProjectLimits::kMaximumHmiProperties))
{
return state->fail(
ProjectStorageError::InvalidField,
"单个 HMI 控件最多保存 64 对扩展属性");
}
for (auto current = object.constBegin(); current != object.constEnd(); ++current)
{
if (!current.value().isString())
@@ -659,7 +682,16 @@ bool parseProperties(
ProjectStorageError::InvalidField,
"HMI 控件属性值必须是字符串");
}
properties->emplace(toUtf8(current.key()), toUtf8(current.value().toString()));
const std::string key = toUtf8(current.key());
const std::string value = toUtf8(current.value().toString());
if (key.empty() || key.size() > ProjectLimits::kMaximumPropertyKeyBytes
|| value.size() > ProjectLimits::kMaximumPropertyValueBytes)
{
return state->fail(
ProjectStorageError::InvalidField,
"HMI 控件属性名称最多 128 字节,属性值最多 4096 字节");
}
properties->emplace(key, value);
}
return true;
}
@@ -718,7 +750,9 @@ bool parseHmiControl(
QJsonObject bounds;
QJsonObject properties;
QJsonValue binding;
if (!readString(object, "id", context, &control->id, state)
if (!readString(
object, "id", context, &control->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "type", context, &type_text, state)
|| !readObject(object, "bounds", context, &bounds, state)
|| !readString(object, "text", context, &control->text, state)
@@ -751,7 +785,9 @@ bool parseHmiControl(
if (control->type == HmiControlType::PageJump)
{
std::string target_page_id;
if (!readString(object, "targetPageId", context, &target_page_id, state))
if (!readString(
object, "targetPageId", context, &target_page_id, state,
ProjectLimits::kMaximumIdBytes))
{
return false;
}
@@ -836,13 +872,19 @@ bool parseHmiPage(
ParseState *state)
{
QJsonArray controls;
if (!readString(object, "id", context, &page->id, state)
if (!readString(
object, "id", context, &page->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "name", context, &page->name, state)
|| !readInt(object, "width", context, 1, std::numeric_limits<int>::max(),
|| !readInt(object, "width", context, 1,
ProjectLimits::kMaximumHmiPageWidth,
&page->width, state)
|| !readInt(object, "height", context, 1, std::numeric_limits<int>::max(),
|| !readInt(object, "height", context, 1,
ProjectLimits::kMaximumHmiPageHeight,
&page->height, state)
|| !readArray(object, "controls", context, &controls, state))
|| !readArray(
object, "controls", context, &controls, state,
static_cast<int>(ProjectLimits::kMaximumHmiControlsPerPage)))
{
return false;
}
@@ -1452,7 +1494,9 @@ bool parseLogicNode(
ParseState *state)
{
QJsonObject config;
if (!readString(object, "id", context, &node->id, state)
if (!readString(
object, "id", context, &node->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readBool(object, "configured", context, &node->configured, state)
|| !readObject(object, "config", context, &config, state)
|| !parseNodeConfig(config, context + ".config", &node->config, state))
@@ -1507,10 +1551,27 @@ bool parseConditionExpression(
const QJsonObject &object,
const std::string &context,
ConditionExpression *expression,
ParseState *state)
ParseState *state,
std::size_t depth,
std::size_t *node_count)
{
if (depth > ProjectLimits::kMaximumExpressionDepth)
{
return state->fail(
ProjectStorageError::InvalidField,
context + " 的嵌套深度超过 20 层");
}
++*node_count;
if (*node_count > ProjectLimits::kMaximumExpressionNodesPerRung)
{
return state->fail(
ProjectStorageError::InvalidField,
context + " 的表达式节点总数超过 4096 个");
}
std::string kind;
if (!readString(object, "id", context, &expression->id, state)
if (!readString(
object, "id", context, &expression->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "kind", context, &kind, state))
{
return false;
@@ -1556,7 +1617,9 @@ bool parseConditionExpression(
context + ".kind 必须是 node、wire、series 或 parallel");
}
QJsonArray children;
if (!readArray(object, "children", context, &children, state))
if (!readArray(
object, "children", context, &children, state,
static_cast<int>(ProjectLimits::kMaximumExpressionChildren)))
{
return false;
}
@@ -1576,7 +1639,9 @@ bool parseConditionExpression(
children.at(index).toObject(),
context + ".children[" + std::to_string(index) + ']',
&child,
state))
state,
depth + 1U,
node_count))
{
return false;
}
@@ -1611,7 +1676,9 @@ bool parseLadderRung(
{
QJsonValue condition;
QJsonValue output;
if (!readString(object, "id", context, &rung->id, state)
if (!readString(
object, "id", context, &rung->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "name", context, &rung->name, state)
|| !readString(object, "comment", context, &rung->comment, state)
|| !readValue(object, "output", context, &output, state))
@@ -1635,8 +1702,10 @@ bool parseLadderRung(
else
{
ConditionExpression parsed_condition;
std::size_t expression_node_count = 0U;
if (!parseConditionExpression(
condition.toObject(), context + ".condition", &parsed_condition, state))
condition.toObject(), context + ".condition", &parsed_condition,
state, 1U, &expression_node_count))
{
return false;
}
@@ -1684,10 +1753,14 @@ bool parseControlLogic(
ParseState *state)
{
QJsonArray rungs;
if (!readString(object, "id", context, &logic->id, state)
if (!readString(
object, "id", context, &logic->id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "name", context, &logic->name, state)
|| !readBool(object, "enabled", context, &logic->enabled, state)
|| !readArray(object, "rungs", context, &rungs, state))
|| !readArray(
object, "rungs", context, &rungs, state,
static_cast<int>(ProjectLimits::kMaximumRungsPerLogic)))
{
return false;
}
@@ -1767,7 +1840,8 @@ bool parseProject(
"formatVersion",
"project",
&project->metadata.formatVersion,
state))
state,
ProjectLimits::kMaximumIdBytes))
{
return false;
}
@@ -1779,14 +1853,26 @@ bool parseProject(
"不支持的工程格式版本:" + project->metadata.formatVersion);
}

if (!readString(object, "id", "project", &project->metadata.id, state)
if (!readString(
object, "id", "project", &project->metadata.id, state,
ProjectLimits::kMaximumIdBytes)
|| !readString(object, "name", "project", &project->metadata.name, state)
|| !readArray(object, "hmiPages", "project", &pages, state)
|| !readString(object, "initialHmiPageId", "project",
&project->initialHmiPageId, state)
|| !readArray(object, "alarmDefinitions", "project", &alarms, state)
|| !readArray(object, "registerComments", "project", &register_comments, state)
|| !readArray(object, "controlLogics", "project", &logics, state))
|| !readArray(
object, "hmiPages", "project", &pages, state,
static_cast<int>(ProjectLimits::kMaximumHmiPages))
|| !readString(
object, "initialHmiPageId", "project",
&project->initialHmiPageId, state,
ProjectLimits::kMaximumIdBytes)
|| !readArray(
object, "alarmDefinitions", "project", &alarms, state,
static_cast<int>(ProjectLimits::kMaximumAlarmDefinitions))
|| !readArray(
object, "registerComments", "project", &register_comments, state,
static_cast<int>(ProjectLimits::kMaximumRegisterComments))
|| !readArray(
object, "controlLogics", "project", &logics, state,
static_cast<int>(ProjectLimits::kMaximumControlLogics)))
{
return false;
}
@@ -1913,6 +1999,14 @@ ProjectSaveResult JsonProjectStorage::save(

const QByteArray data = QJsonDocument(serializeProject(project)).toJson(
QJsonDocument::Indented);
if (static_cast<std::size_t>(data.size())
> ProjectLimits::kMaximumProjectFileBytes)
{
file.cancelWriting();
return {false,
ProjectStorageError::InvalidProject,
"工程 JSON 文件不能超过 16 MiB"};
}
// 短写入也视为失败,并取消临时文件提交
if (file.write(data) != data.size())
{
@@ -1937,8 +2031,19 @@ ProjectLoadResult JsonProjectStorage::load(const std::string &file_path)
ProjectStorageError::FileOpenFailed,
toUtf8(file.errorString())};
}
if (file.size() < 0
|| static_cast<quint64>(file.size())
> static_cast<quint64>(ProjectLimits::kMaximumProjectFileBytes))
{
return {false,
{},
ProjectStorageError::InvalidJson,
"工程 JSON 文件不能超过 16 MiB"};
}

const QByteArray data = file.readAll();
// 最多只读到上限加 1 字节,避免文件属性检查后文件变大导致无限制分配
const QByteArray data = file.read(
static_cast<qint64>(ProjectLimits::kMaximumProjectFileBytes) + 1);
if (file.error() != QFileDevice::NoError)
{
return {false,
@@ -1946,6 +2051,14 @@ ProjectLoadResult JsonProjectStorage::load(const std::string &file_path)
ProjectStorageError::FileReadFailed,
toUtf8(file.errorString())};
}
if (static_cast<std::size_t>(data.size())
> ProjectLimits::kMaximumProjectFileBytes)
{
return {false,
{},
ProjectStorageError::InvalidJson,
"工程 JSON 文件不能超过 16 MiB"};
}

// 顶层必须是 JSON 对象,数组或标量不能表示完整工程
QJsonParseError parse_error;


+ 101
- 9
app/src/infrastructure/plc_communication_service.cpp Parādīt failu

@@ -15,7 +15,6 @@

namespace {

constexpr int kMaximumReadCount = 120;
constexpr int kRecoveryProbeIntervalMs = 2000;

std::string toUtf8(const QString &value)
@@ -42,6 +41,67 @@ bool isReadingState(PlcConnectionState state)
|| state == PlcConnectionState::Recovering;
}

bool normalizePollAddresses(
const std::vector<RegisterAddress> &addresses,
std::vector<RegisterAddress> *normalized,
std::string *error)
{
*normalized = addresses;
if (std::any_of(
normalized->cbegin(), normalized->cend(),
[](const RegisterAddress &address) { return !address.isValid(); }))
{
*error = "PLC 轮询地址中包含无效的 M/D 地址";
return false;
}
std::sort(
normalized->begin(), normalized->end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
normalized->erase(
std::unique(normalized->begin(), normalized->end()), normalized->end());
if (normalized->size() > ProjectLimits::kMaximumPollAddresses)
{
*error = "PLC 轮询的去重 M/D 地址最多为 1024 个";
return false;
}
return true;
}

std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses)
{
if (addresses.empty())
{
return 2U;
}
std::size_t blocks = 0U;
RegisterArea current_area = RegisterArea::M;
int start_address = 0;
int count = 0;
for (const RegisterAddress &address : addresses)
{
if (count == 0
|| address.area() != current_area
|| address.index() > start_address + count
|| count >= ProjectLimits::kMaximumModbusReadCount)
{
++blocks;
current_area = address.area();
start_address = address.index();
count = 1;
}
else
{
count = address.index() - start_address + 1;
}
}
return blocks;
}

} // namespace

PlcCommunicationService::PlcCommunicationService(
@@ -115,10 +175,11 @@ PlcCommunicationService::~PlcCommunicationService() = default;
PlcCommunicationResult PlcCommunicationService::connectDevice(
const PlcSerialConfiguration &configuration)
{
if (QString::fromStdString(configuration.portName).trimmed().isEmpty()
|| configuration.serverAddress < 1 || configuration.serverAddress > 247)
const PlcCommunicationResult validation =
validatePlcSerialConfiguration(configuration);
if (!validation.succeeded)
{
return {false, "必须填写串口端口,并将 PLC 站号设置为 1~247"};
return validation;
}
if (state_ == PlcConnectionState::Disconnected
&& master_->state() != QModbusDevice::UnconnectedState)
@@ -179,16 +240,27 @@ void PlcCommunicationService::disconnectDevice()
setState(PlcConnectionState::Disconnected);
}

void PlcCommunicationService::setPollAddresses(
PlcCommunicationResult PlcCommunicationService::setPollAddresses(
const std::vector<RegisterAddress> &addresses)
{
std::vector<RegisterAddress> normalized;
std::string error;
if (!normalizePollAddresses(addresses, &normalized, &error))
{
return {false, error};
}
if (pollBlockCount(normalized) > ProjectLimits::kMaximumPollBlocks)
{
return {false, "PLC 轮询地址拆分后最多允许 64 个读块"};
}
if (pending_reply_ != nullptr)
{
pending_poll_addresses_ = addresses;
pending_poll_addresses_ = std::move(normalized);
poll_update_pending_ = true;
return;
return {true, {}};
}
applyPollAddresses(addresses);
applyPollAddresses(normalized);
return {true, {}};
}

PlcConnectionState PlcCommunicationService::state() const
@@ -259,7 +331,8 @@ void PlcCommunicationService::rebuildPollBlocks()
|| poll_blocks_.back().area != address.area()
|| address.index() > poll_blocks_.back().startAddress
+ poll_blocks_.back().count
|| poll_blocks_.back().count >= kMaximumReadCount)
|| poll_blocks_.back().count
>= ProjectLimits::kMaximumModbusReadCount)
{
poll_blocks_.push_back({address.area(), address.index(), 1});
}
@@ -491,6 +564,10 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite(
{
return {false, RegisterError::Unavailable};
}
if (pending_write_reply_ != nullptr)
{
return {false, RegisterError::WriteRejected};
}
QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1);
unit.setValue(0, value ? 1U : 0U);
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
@@ -499,10 +576,15 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite(
handleModbusError(master_->error());
return {false, RegisterError::WriteRejected};
}
pending_write_reply_ = reply;
connect(reply, &QModbusReply::finished,
this,
[this, reply, generation = connection_generation_]
{
if (pending_write_reply_ == reply)
{
pending_write_reply_ = nullptr;
}
if (generation != connection_generation_)
{
reply->deleteLater();
@@ -524,6 +606,10 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite(
{
return {false, RegisterError::Unavailable};
}
if (pending_write_reply_ != nullptr)
{
return {false, RegisterError::WriteRejected};
}
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1);
unit.setValue(0, static_cast<quint16>(value));
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
@@ -532,10 +618,15 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite(
handleModbusError(master_->error());
return {false, RegisterError::WriteRejected};
}
pending_write_reply_ = reply;
connect(reply, &QModbusReply::finished,
this,
[this, reply, generation = connection_generation_]
{
if (pending_write_reply_ == reply)
{
pending_write_reply_ = nullptr;
}
if (generation != connection_generation_)
{
reply->deleteLater();
@@ -601,6 +692,7 @@ void PlcCommunicationService::closeSerialSession()
poll_timer_.stop();
recovery_timer_.stop();
pending_reply_ = nullptr;
pending_write_reply_ = nullptr;
poll_update_pending_ = false;
pending_poll_addresses_.clear();
if (master_->state() != QModbusDevice::UnconnectedState)


+ 3
- 1
app/src/infrastructure/plc_communication_service.h Parādīt failu

@@ -30,7 +30,8 @@ public:
PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) override;
void disconnectDevice() override;
void setPollAddresses(const std::vector<RegisterAddress> &addresses) override;
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) override;

PlcConnectionState state() const override;
bool initialReadCompleted() const override;
@@ -84,6 +85,7 @@ private:
std::vector<PollBlock> poll_blocks_;
std::size_t next_poll_block_ = 0;
QModbusReply *pending_reply_ = nullptr;
QModbusReply *pending_write_reply_ = nullptr;
bool poll_update_pending_ = false;
bool disconnecting_ = false;
bool serial_session_opened_ = false;


+ 5
- 0
app/src/services/alarm_editor_service.cpp Parādīt failu

@@ -1,6 +1,7 @@
#include "alarm_editor_service.h"

#include "project_service.h"
#include "domain/project_limits.h"

#include <algorithm>

@@ -36,6 +37,10 @@ const AlarmDefinition *AlarmEditorService::findDefinition(
AlarmEditorResult AlarmEditorService::addDefinition(
const AlarmDefinition &definition)
{
if (definitions().size() >= ProjectLimits::kMaximumAlarmDefinitions)
{
return failure(AlarmEditorError::InvalidDefinition, "单个工程最多包含 512 条报警定义");
}
AlarmDefinition candidate = definition;
candidate.id = makeUniqueId();
std::string error;


+ 17
- 0
app/src/services/hmi_editor_service.cpp Parādīt failu

@@ -2,6 +2,7 @@

#include "domain/hmi_control_registry.h"
#include "project_service.h"
#include "domain/project_limits.h"

#include <algorithm>
#include <cctype>
@@ -210,6 +211,14 @@ HmiEditorResult HmiEditorService::addPage(const std::string &name)
return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空");
}
const Project &current = project_service_.project();
if (current.hmiPages.size() >= ProjectLimits::kMaximumHmiPages)
{
return failure(HmiEditorError::InvalidPage, "单个工程最多包含 128 个 HMI 页面");
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节");
}
const bool duplicate = std::any_of(
current.hmiPages.cbegin(), current.hmiPages.cend(),
[&name](const HmiPage &page) { return page.name == name; });
@@ -239,6 +248,10 @@ HmiEditorResult HmiEditorService::renamePage(
{
return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空");
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 4096 个 UTF-8 字节");
}
const Project &current = project_service_.project();
const auto existing = std::find_if(
current.hmiPages.cbegin(), current.hmiPages.cend(),
@@ -376,6 +389,10 @@ HmiEditorResult HmiEditorService::addControl(
{
return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
}
if (page->controls.size() >= ProjectLimits::kMaximumHmiControlsPerPage)
{
return failure(HmiEditorError::InvalidControl, "单个 HMI 页面最多包含 512 个控件");
}
const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type);
if (descriptor == nullptr)
{


+ 72
- 0
app/src/services/logic_editor_service.cpp Parādīt failu

@@ -1,6 +1,7 @@
#include "logic_editor_service.h"

#include "project_service.h"
#include "domain/project_limits.h"

#include <algorithm>
#include <cctype>
@@ -651,6 +652,14 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name)
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空");
}
const Project &current = project_service_.project();
if (current.controlLogics.size() >= ProjectLimits::kMaximumControlLogics)
{
return failure(LogicEditorError::InvalidOperation, "单个工程最多包含 128 组控制逻辑");
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节");
}
const bool duplicate = std::any_of(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&name](const ControlLogic &logic) { return logic.name == name; });
@@ -677,6 +686,10 @@ LogicEditorResult LogicEditorService::renameLogic(
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空");
}
if (name.size() > ProjectLimits::kMaximumTextBytes)
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能超过 4096 个 UTF-8 字节");
}
const Project &current = project_service_.project();
const auto logic = std::find_if(
current.controlLogics.cbegin(), current.controlLogics.cend(),
@@ -795,6 +808,10 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic)
{
return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络");
}
LadderRung rung;
rung.id = makeUniqueRungId(*logic);
rung.name = "网络 " + std::to_string(logic->rungs.size() + 1U);
@@ -848,6 +865,12 @@ LogicEditorResult LogicEditorService::updateRungComment(
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
if (comment.size() > ProjectLimits::kMaximumTextBytes)
{
return failure(
LogicEditorError::InvalidOperation,
"梯形图网络注释不能超过 4096 个 UTF-8 字节");
}
if (rung->comment == comment)
{
return {true, LogicEditorError::None, {}, rung_id};
@@ -897,6 +920,12 @@ LogicEditorResult LogicEditorService::appendCondition(
std::move(*rung->condition),
std::move(leaf));
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project.controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
@@ -941,6 +970,12 @@ LogicEditorResult LogicEditorService::appendWire(
std::move(*rung->condition),
std::move(leaf));
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project_service_.editProject().controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}
@@ -990,6 +1025,12 @@ LogicEditorResult LogicEditorService::insertConditionAfter(
std::move(original),
std::move(leaf));
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project.controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
@@ -1046,6 +1087,12 @@ LogicEditorResult LogicEditorService::insertWireAfter(
std::move(original),
std::move(leaf));
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project_service_.editProject().controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}
@@ -1075,6 +1122,12 @@ LogicEditorResult LogicEditorService::replaceWireWithCondition(
ConditionExpression *editable = findConditionExpression(
*rung->condition, wire_expression_id);
*editable = ConditionExpression::fromNode(makeNode(node_id, config));
std::string validation_error;
if (!rung->validate(&validation_error))
{
project_service_.editProject().controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidNode, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
@@ -1131,10 +1184,17 @@ LogicEditorResult LogicEditorService::addParallelBranch(
parallel_id,
series_id))
{
project.controlLogics = before.logics;
return failure(
LogicEditorError::InvalidOperation,
"并联选择必须是一个连续的逻辑范围");
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project.controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
@@ -1197,10 +1257,17 @@ LogicEditorResult LogicEditorService::addParallelWireBranch(
parallel_id,
series_id))
{
project_service_.editProject().controlLogics = before.logics;
return failure(
LogicEditorError::InvalidOperation,
"竖线连接只能围绕同一支路中的连续逻辑范围");
}
std::string validation_error;
if (!rung->validate(&validation_error))
{
project_service_.editProject().controlLogics = std::move(before.logics);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}
@@ -1223,6 +1290,11 @@ LogicEditorResult LogicEditorService::setOutput(
? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config));
LogicNode node = makeNode(node_id, config);
node.configured = configured;
std::string validation_error;
if (!node.validate(&validation_error))
{
return failure(LogicEditorError::InvalidNode, validation_error);
}
if (existing_rung->output.has_value()
&& nodesEqual(*existing_rung->output, node))
{


+ 59
- 1
app/src/services/plc_communication_gateway.h Parādīt failu

@@ -1,7 +1,10 @@
#pragma once

#include "domain/register_address.h"
#include "domain/project_limits.h"

#include <algorithm>
#include <cctype>
#include <functional>
#include <string>
#include <vector>
@@ -50,6 +53,61 @@ struct PlcCommunicationResult
std::string message;
};

inline PlcCommunicationResult validatePlcSerialConfiguration(
const PlcSerialConfiguration &configuration)
{
const bool has_port_name = std::any_of(
configuration.portName.cbegin(), configuration.portName.cend(),
[](unsigned char character) { return std::isspace(character) == 0; });
if (!has_port_name)
{
return {false, "必须填写串口端口"};
}
if (configuration.serverAddress < ProjectLimits::kMinimumPlcServerAddress
|| configuration.serverAddress > ProjectLimits::kMaximumPlcServerAddress)
{
return {false, "PLC 站号必须在 1~247 范围内"};
}
if (configuration.baudRate != 9600
&& configuration.baudRate != 19200
&& configuration.baudRate != 38400
&& configuration.baudRate != 57600
&& configuration.baudRate != 115200)
{
return {false, "波特率只支持 9600、19200、38400、57600 或 115200"};
}
if (configuration.dataBits != 7 && configuration.dataBits != 8)
{
return {false, "数据位只支持 7 或 8"};
}
if (configuration.parity != 0
&& configuration.parity != 2
&& configuration.parity != 3)
{
return {false, "校验方式只支持无校验、偶校验或奇校验"};
}
if (configuration.stopBits != 1 && configuration.stopBits != 2)
{
return {false, "停止位只支持 1 或 2"};
}
if (configuration.responseTimeoutMs < ProjectLimits::kMinimumResponseTimeoutMs
|| configuration.responseTimeoutMs > ProjectLimits::kMaximumResponseTimeoutMs)
{
return {false, "PLC 响应超时必须在 100~30000 ms 范围内"};
}
if (configuration.retries < ProjectLimits::kMinimumRetries
|| configuration.retries > ProjectLimits::kMaximumRetries)
{
return {false, "PLC 失败重试次数必须在 0~5 范围内"};
}
if (configuration.pollIntervalMs < ProjectLimits::kMinimumPollIntervalMs
|| configuration.pollIntervalMs > ProjectLimits::kMaximumPollIntervalMs)
{
return {false, "PLC 轮询周期必须在 50~10000 ms 范围内"};
}
return {true, {}};
}

class PlcCommunicationGateway
{
public:
@@ -58,7 +116,7 @@ public:
virtual PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) = 0;
virtual void disconnectDevice() = 0;
virtual void setPollAddresses(
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) = 0;
virtual PlcConnectionState state() const = 0;
virtual bool initialReadCompleted() const = 0;


+ 13
- 4
app/src/services/runtime_mode_service.cpp Parādīt failu

@@ -10,6 +10,7 @@

#include "project_service.h"
#include "domain/active_register_repository.h"
#include "domain/project_limits.h"

#include <algorithm>

@@ -189,7 +190,11 @@ PlcCommunicationResult RuntimeModeService::connectPlc(
{
plc_gateway_->disconnectDevice();
}
refreshPlcPollAddresses();
const PlcCommunicationResult poll_result = refreshPlcPollAddresses();
if (!poll_result.succeeded)
{
return poll_result;
}
setInitialPlcReadCompleted(false);
return plc_gateway_->connectDevice(configuration);
}
@@ -201,11 +206,11 @@ void RuntimeModeService::setMonitorAddresses(
refreshPlcPollAddresses();
}

void RuntimeModeService::refreshPlcPollAddresses()
PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
{
if (plc_gateway_ == nullptr)
{
return;
return {false, "PLC 通信服务尚未配置"};
}
std::vector<RegisterAddress> addresses = monitor_addresses_;
const Project &project = project_service_.project();
@@ -258,7 +263,11 @@ void RuntimeModeService::refreshPlcPollAddresses()
return left.index() < right.index();
});
addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
plc_gateway_->setPollAddresses(addresses);
if (addresses.size() > ProjectLimits::kMaximumPollAddresses)
{
return {false, "PLC 轮询的去重 M/D 地址最多为 1024 个"};
}
return plc_gateway_->setPollAddresses(addresses);
}

void RuntimeModeService::disconnectPlc()


+ 1
- 1
app/src/services/runtime_mode_service.h Parādīt failu

@@ -76,7 +76,7 @@ public:
RegisterRepository &plc_repository);
PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration);
void setMonitorAddresses(const std::vector<RegisterAddress> &addresses);
void refreshPlcPollAddresses();
PlcCommunicationResult refreshPlcPollAddresses();
void disconnectPlc();
PlcConnectionState plcConnectionState() const;
const std::string &plcError() const;


+ 13
- 8
app/src/services/software_logic_executor.cpp Parādīt failu

@@ -228,14 +228,19 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
continue;
}

bool rung_value = false;
LogicScanResult result = evaluateExpression(
logic.id,
*rung.condition,
repository,
logic_trace,
true,
&rung_value);
// 没有条件节点的网络等价于左侧电源线直接接通,作为恒真条件执行
bool rung_value = true;
LogicScanResult result = success();
if (rung.condition.has_value())
{
result = evaluateExpression(
logic.id,
*rung.condition,
repository,
logic_trace,
true,
&rung_value);
}
if (!result.succeeded)
{
result.logicId = logic.id;


+ 126
- 2
app/tests/domain_tests.cpp Parādīt failu

@@ -2,12 +2,14 @@
#include "domain/hmi_control_registry.h"
#include "domain/hmi_model.h"
#include "domain/project_model.h"
#include "domain/project_limits.h"
#include "domain/register_address.h"
#include "domain/register_repository.h"
#include "domain/runtime_state.h"

#include <cstdint>
#include <exception>
#include <functional>
#include <iostream>
#include <set>
#include <stdexcept>
@@ -311,6 +313,102 @@ void testMultiPageAndLogicDomainRules()
require(!project.validate(), "unknown HMI control types must be rejected");
}

void testQuantityBoundaries()
{
Project project = makeValidProject();
for (std::size_t index = 1U; index < ProjectLimits::kMaximumHmiPages; ++index)
{
project.hmiPages.push_back({
"page-" + std::to_string(index),
"Page " + std::to_string(index),
800,
480,
{}});
}
require(project.validate(), "an HMI page count of 128 must be accepted");
project.hmiPages.push_back({"page-over", "Page over", 800, 480, {}});
require(!project.validate(), "an HMI page count of 129 must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.clear();
for (std::size_t index = 0U;
index < ProjectLimits::kMaximumHmiControlsPerPage;
++index)
{
HmiControl label;
label.id = "label-" + std::to_string(index);
label.type = HmiControlType::Label;
label.bounds = {0, 0, 1, 1};
label.text = "label";
project.hmiPages.front().controls.push_back(std::move(label));
}
require(project.validate(), "a page control count of 512 must be accepted");
HmiControl extra_label;
extra_label.id = "label-over";
extra_label.type = HmiControlType::Label;
extra_label.bounds = {0, 0, 1, 1};
extra_label.text = "label";
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.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight;
require(project.validate(), "an HMI page size of 8192 by 8192 must be accepted");
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1;
require(!project.validate(), "an HMI page width of 8193 must be rejected");

ConditionExpression leaf = ConditionExpression::fromNode({
"depth-node-0",
ContactNodeConfig{RegisterAddress{RegisterArea::M, 0}},
true});
std::function<ConditionExpression(int, int *)> makeNested =
[&makeNested](int depth, int *next_address)
{
if (depth == 1)
{
const int address = (*next_address)++;
return ConditionExpression::fromNode({
"depth-node-" + std::to_string(address),
ContactNodeConfig{RegisterAddress{RegisterArea::M, address}},
true});
}
const int address = (*next_address)++;
ConditionExpression expression;
expression.id = "depth-expression-" + std::to_string(address);
expression.kind = depth % 2 == 0
? ConditionExpressionKind::Parallel
: ConditionExpressionKind::Series;
expression.children = {
makeNested(depth - 1, next_address),
ConditionExpression::fromNode({
"depth-node-" + std::to_string(address),
ContactNodeConfig{RegisterAddress{RegisterArea::M, address}},
true})};
return expression;
};
int next_address = 1;
ConditionExpression maximum_depth = makeNested(
static_cast<int>(ProjectLimits::kMaximumExpressionDepth), &next_address);
require(maximum_depth.validate(),
"an expression depth of 20 must be accepted");
ConditionExpression excessive_depth = makeNested(
static_cast<int>(ProjectLimits::kMaximumExpressionDepth) + 1,
&next_address);
require(!excessive_depth.validate(),
"an expression depth of 21 must be rejected");

CounterNodeConfig counter_config{
CounterAddress{0},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 0},
WordOperand{WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, -1},
RegisterAddress{RegisterArea::M, 0}};
LogicNode counter{"counter-negative-preset", counter_config, true};
require(!counter.validate(), "a negative constant counter preset must be rejected");
(void)leaf;
}

void testLogicNodeConfigurationBoundaries()
{
// 触点只能绑定 M 区,数值比较只能绑定 D 区
@@ -613,6 +711,31 @@ void testLadderLogicBoundaries()
"wire-too-wide", WireSegment::kMaximumColumnSpan + 1);
require(!invalid_wire.validate(), "an oversized horizontal wire must be rejected");

ConditionExpression maximum_columns;
maximum_columns.id = "maximum-columns";
maximum_columns.kind = ConditionExpressionKind::Series;
for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
{
LogicNode node;
node.id = "column-" + std::to_string(column + 1);
node.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, column},
ContactMode::NormallyOpen};
maximum_columns.children.push_back(
ConditionExpression::fromNode(std::move(node)));
}
require(maximum_columns.validate(),
"ten condition columns must be accepted");
LogicNode extra_column;
extra_column.id = "column-11";
extra_column.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 10},
ContactMode::NormallyOpen};
maximum_columns.children.push_back(
ConditionExpression::fromNode(std::move(extra_column)));
require(!maximum_columns.validate(),
"an eleventh condition column must be rejected");

ConditionExpression wired_series;
wired_series.id = "wired-series";
wired_series.kind = ConditionExpressionKind::Series;
@@ -641,8 +764,8 @@ void testLadderLogicBoundaries()

empty_rung.output = coil;
require(empty_rung.validate(), "output-only network may remain in an editable draft");
require(!empty_rung.validateForRunning(),
"an output without conditions must block runtime validation");
require(empty_rung.validateForRunning(),
"an output-only network must be valid as an unconditional rung");

logic.rungs.front().output = coil;
logic.rungs.front().condition = root;
@@ -740,6 +863,7 @@ int main()
testLadderLogicBoundaries();
testModelsValidateBindingsAndIdentifiers();
testMultiPageAndLogicDomainRules();
testQuantityBoundaries();
testRuntimeStateBoundaries();
}
catch (const std::exception &error)


+ 1
- 0
app/tests/domain_tests.pro Parādīt failu

@@ -24,5 +24,6 @@ HEADERS += \
../src/domain/hmi_model.h \
../src/domain/hmi_control_registry.h \
../src/domain/control_logic_model.h \
../src/domain/project_limits.h \
../src/domain/project_model.h \
../src/domain/runtime_state.h

+ 51
- 0
app/tests/logic_editor_service_tests.cpp Parādīt failu

@@ -192,6 +192,55 @@ void testStructuredWireEditing()
"wire branch deletion must participate in ladder undo history");
}

void testConditionColumnLimit()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);

for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column)
{
require(service.appendCondition(logic_id, rung_id, contact(column)).succeeded,
"the first ten condition columns must be editable");
}
const LogicEditorResult overflow = service.appendCondition(
logic_id, rung_id, contact(ProjectLimits::kMaximumConditionColumns));
require(!overflow.succeeded
&& overflow.error == LogicEditorError::InvalidOperation,
"the eleventh condition column must be rejected by the editor service");
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.size()
== static_cast<std::size_t>(
ProjectLimits::kMaximumConditionColumns),
"a rejected eleventh column must leave the ten-column network unchanged");
}

void testUnconditionalOutputEditing()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);

require(service.setOutput(
logic_id,
rung_id,
CoilNodeConfig{
RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
true)
.succeeded,
"the editor must allow a coil before any condition is added");
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && !rung->condition.has_value()
&& rung->output.has_value() && rung->validateForRunning(),
"an editor-created output-only network must be runnable as unconditional");
}

void testLogicLifecycleAndOrdering()
{
TestProjectStorage storage;
@@ -366,6 +415,8 @@ int main()
testStructuredEditingAndNormalization();
testRangeParallelInsertion();
testStructuredWireEditing();
testConditionColumnLimit();
testUnconditionalOutputEditing();
testLogicLifecycleAndOrdering();
testEdgeTimerNodesAndRungComments();
testHistoryAndAtomicBatchDelete();


+ 23
- 0
app/tests/offline_simulation_service_tests.cpp Parādīt failu

@@ -257,6 +257,28 @@ void testNestedSeriesParallelExpression()
require(readBit(repository, 10), "A branch must independently energize output");
}

void testUnconditionalCoil()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
LadderRung unconditional;
unconditional.id = "unconditional-rung";
unconditional.name = "unconditional-rung";
unconditional.output = coil("unconditional-coil", 10);
const ControlLogic program = logic({unconditional});

LogicTraceSnapshot trace;
require(executor.validate({program}).succeeded,
"an output-only network must pass runtime validation");
require(executor.executeScan({program}, repository, &trace).succeeded,
"an output-only network scan must succeed");
require(readBit(repository, 10),
"an output-only network must energize its coil as a constant-true rung");
require(trace.rungValues.at("unconditional-rung")
&& trace.nodePowerValues.at("unconditional-coil"),
"an unconditional rung must report energized power flow");
}

void testWirePassThroughAndPowerTrace()
{
VirtualRegisterRepository repository;
@@ -900,6 +922,7 @@ int main(int argc, char *argv[])
{
testSeriesParallelContactsAndSequentialVisibility();
testNestedSeriesParallelExpression();
testUnconditionalCoil();
testWirePassThroughAndPowerTrace();
testAllComparisons();
testSetResetAndDisabledLogic();


+ 69
- 1
app/tests/plc_runtime_tests.cpp Parādīt failu

@@ -1,6 +1,8 @@
#include "domain/active_register_repository.h"
#include "domain/project_limits.h"
#include "domain/project_storage.h"
#include "infrastructure/plc_communication_error_classifier.h"
#include "infrastructure/plc_communication_service.h"
#include "infrastructure/plc_register_repository.h"
#include "services/offline_simulation_service.h"
#include "services/plc_communication_gateway.h"
@@ -61,9 +63,11 @@ public:
}
}

void setPollAddresses(const std::vector<RegisterAddress> &addresses) override
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) override
{
poll_addresses = addresses;
return {true, {}};
}

PlcConnectionState state() const override { return connection_state; }
@@ -311,6 +315,68 @@ void testPlcCommunicationErrorClassification()
"protocol errors must remain distinct from timeouts and disconnections");
}

void testPlcConfigurationBoundaries()
{
PlcSerialConfiguration configuration;
configuration.portName = "COM3";
require(validatePlcSerialConfiguration(configuration).succeeded,
"the standard COM3 9600 8E1 configuration must be accepted");

configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress;
require(validatePlcSerialConfiguration(configuration).succeeded,
"PLC station 247 must be accepted");
configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress + 1;
require(!validatePlcSerialConfiguration(configuration).succeeded,
"PLC station 248 must be rejected");

configuration.serverAddress = 1;
configuration.retries = ProjectLimits::kMaximumRetries;
require(validatePlcSerialConfiguration(configuration).succeeded,
"five retries must be accepted");
configuration.retries = ProjectLimits::kMaximumRetries + 1;
require(!validatePlcSerialConfiguration(configuration).succeeded,
"six retries must be rejected");

configuration.retries = 0;
configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs - 1;
require(!validatePlcSerialConfiguration(configuration).succeeded,
"a response timeout below 100 ms must be rejected");
configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs;
require(validatePlcSerialConfiguration(configuration).succeeded,
"a response timeout of 100 ms must be accepted");
}

void testPlcPollQuantityBoundaries()
{
PlcRegisterRepository repository;
PlcCommunicationService service(repository);

std::vector<RegisterAddress> too_many_addresses;
for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index)
{
too_many_addresses.push_back({RegisterArea::M, index});
}
too_many_addresses.push_back({RegisterArea::D, 0});
require(!service.setPollAddresses(too_many_addresses).succeeded,
"more than 1024 poll addresses must be rejected");

std::vector<RegisterAddress> too_many_blocks;
for (int index = 0; index < 65; ++index)
{
too_many_blocks.push_back({RegisterArea::M, index * 2});
}
require(!service.setPollAddresses(too_many_blocks).succeeded,
"more than 64 Modbus read blocks must be rejected");

std::vector<RegisterAddress> maximum_contiguous_block;
for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)
{
maximum_contiguous_block.push_back({RegisterArea::M, index});
}
require(service.setPollAddresses(maximum_contiguous_block).succeeded,
"a contiguous Modbus read block of 120 values must be accepted");
}

} // namespace

int main()
@@ -321,6 +387,8 @@ int main()
testRuntimeRepositorySwitchingAndDisconnect();
testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect();
testPlcCommunicationErrorClassification();
testPlcConfigurationBoundaries();
testPlcPollQuantityBoundaries();
}
catch (const std::exception &error)
{


+ 6
- 3
app/tests/plc_runtime_tests.pro Parādīt failu

@@ -1,4 +1,4 @@
QT += core serialbus
QT += core serialbus serialport

TEMPLATE = app
TARGET = plc_runtime_tests
@@ -25,7 +25,8 @@ SOURCES += \
../src/services/offline_simulation_service.cpp \
../src/services/runtime_mode_service.cpp \
../src/infrastructure/plc_communication_error_classifier.cpp \
../src/infrastructure/plc_register_repository.cpp
../src/infrastructure/plc_register_repository.cpp \
../src/infrastructure/plc_communication_service.cpp

HEADERS += \
../src/domain/register_address.h \
@@ -36,6 +37,7 @@ HEADERS += \
../src/domain/hmi_model.h \
../src/domain/hmi_control_registry.h \
../src/domain/control_logic_model.h \
../src/domain/project_limits.h \
../src/domain/project_model.h \
../src/domain/project_storage.h \
../src/domain/runtime_state.h \
@@ -45,4 +47,5 @@ HEADERS += \
../src/services/runtime_mode_service.h \
../src/services/plc_communication_gateway.h \
../src/infrastructure/plc_communication_error_classifier.h \
../src/infrastructure/plc_register_repository.h
../src/infrastructure/plc_register_repository.h \
../src/infrastructure/plc_communication_service.h

+ 126
- 0
app/tests/project_management_tests.cpp Parādīt failu

@@ -2,6 +2,7 @@
#include "infrastructure/json_project_storage.h"
#include "services/register_comment_service.h"
#include "services/project_service.h"
#include "domain/project_limits.h"

#include <QFile>
#include <QJsonArray>
@@ -374,6 +375,39 @@ void testEmptyProjectRoundTrip()
"empty project must have no alarm definitions");
}

void testUnconditionalOutputRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");

JsonProjectStorage storage;
ProjectService service(storage);
require(service.createNewProject("Unconditional output").succeeded,
"unconditional output project creation must succeed");
LadderRung rung;
rung.id = "unconditional-rung";
rung.name = "Unconditional rung";
LogicNode coil;
coil.id = "unconditional-coil";
coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal};
rung.output = coil;
service.editProject().controlLogics.push_back(
ControlLogic{"logic-1", "Logic 1", {rung}, true});
require(service.project().validateForRunning(),
"an output-only project must be runnable before saving");

const QString path = directory.filePath("unconditional-output.json");
require(service.saveAs(path.toStdString()).succeeded,
"an output-only project must save successfully");
require(service.load(path.toStdString()).succeeded,
"an output-only project must load successfully");
const LadderRung &loaded = service.project().controlLogics.front().rungs.front();
require(!loaded.condition.has_value() && loaded.output.has_value()
&& loaded.validateForRunning(),
"JSON round trip must preserve unconditional output semantics");
}

void testExampleProjectRoundTrip()
{
// 验证各层嵌套字段往返后保持不变且序列化结果稳定
@@ -694,6 +728,96 @@ void testInvalidFiles()
"unsupported versions must be rejected");
}

void testQuantityFileLimits()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");

JsonProjectStorage storage;
ProjectService service(storage);
require(service.createNewProject("Quantity limits").succeeded,
"the quantity-limit fixture project must be created");

const QString oversized_path = directory.filePath("oversized.json");
const QByteArray oversized(
static_cast<int>(ProjectLimits::kMaximumProjectFileBytes + 1U), ' ');
writeText(oversized_path, oversized);
const ProjectLoadResult oversized_result =
storage.load(oversized_path.toStdString());
require(!oversized_result.succeeded
&& oversized_result.error == ProjectStorageError::InvalidJson,
"a JSON file over 16 MiB must be rejected before parsing");

const QString valid_path = directory.filePath("fixture.json");
require(service.saveAs(valid_path.toStdString()).succeeded,
"the quantity-limit fixture must be saved");
QJsonObject root = QJsonDocument::fromJson(readBytes(valid_path)).object();
QJsonArray too_many_pages;
for (int index = 0;
index <= static_cast<int>(ProjectLimits::kMaximumHmiPages);
++index)
{
too_many_pages.append(QJsonObject{});
}
root.insert(QStringLiteral("hmiPages"), too_many_pages);
const QString too_many_pages_path = directory.filePath("too-many-pages.json");
writeText(
too_many_pages_path,
QJsonDocument(root).toJson(QJsonDocument::Compact));
const ProjectLoadResult too_many_pages_result =
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");

service.editProject() = makeExampleProject();
require(service.saveAs(valid_path.toStdString()).succeeded,
"the nested-expression fixture must be saved");
root = QJsonDocument::fromJson(readBytes(valid_path)).object();
QJsonArray logics = root.value(QStringLiteral("controlLogics")).toArray();
QJsonObject logic = logics.at(0).toObject();
QJsonArray rungs = logic.value(QStringLiteral("rungs")).toArray();
QJsonObject rung = rungs.at(0).toObject();
QJsonObject leaf = rung.value(QStringLiteral("condition")).toObject();
while (leaf.value(QStringLiteral("kind")).toString() != QStringLiteral("node"))
{
leaf = leaf.value(QStringLiteral("children")).toArray().at(0).toObject();
}
QJsonObject nested = leaf;
for (int depth = 1;
depth <= static_cast<int>(ProjectLimits::kMaximumExpressionDepth) + 1;
++depth)
{
QJsonObject sibling = leaf;
sibling.insert(
QStringLiteral("id"),
QStringLiteral("json-depth-sibling-") + QString::number(depth));
QJsonArray children;
children.append(nested);
children.append(sibling);
QJsonObject parent;
parent.insert(
QStringLiteral("id"),
QStringLiteral("json-depth-") + QString::number(depth));
parent.insert(
QStringLiteral("kind"),
depth % 2 == 0 ? QStringLiteral("parallel") : QStringLiteral("series"));
parent.insert(QStringLiteral("children"), children);
nested = parent;
}
rung.insert(QStringLiteral("condition"), nested);
rungs[0] = rung;
logic.insert(QStringLiteral("rungs"), rungs);
logics[0] = logic;
root.insert(QStringLiteral("controlLogics"), logics);
const QString deep_path = directory.filePath("too-deep.json");
writeText(deep_path, QJsonDocument(root).toJson(QJsonDocument::Compact));
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");
}

void testServiceStateAndSaveErrors()
{
// 保存路径和修改标记只在成功持久化后更新
@@ -764,9 +888,11 @@ int main()
{
// 工程服务和 JSON 存储在同一测试进程中验证完整闭环
testEmptyProjectRoundTrip();
testUnconditionalOutputRoundTrip();
testExampleProjectRoundTrip();
testRegisterCommentService();
testInvalidFiles();
testQuantityFileLimits();
testServiceStateAndSaveErrors();
}
catch (const std::exception &error)


+ 1
- 0
app/tests/project_management_tests.pro Parādīt failu

@@ -29,6 +29,7 @@ HEADERS += \
../src/domain/hmi_model.h \
../src/domain/hmi_control_registry.h \
../src/domain/control_logic_model.h \
../src/domain/project_limits.h \
../src/domain/project_model.h \
../src/domain/runtime_state.h \
../src/domain/project_storage.h \


+ 2
- 1
app/tests/runtime_mode_service_tests.cpp Parādīt failu

@@ -46,10 +46,11 @@ public:
initial_read = false;
}

void setPollAddresses(
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) override
{
poll_addresses = addresses;
return {true, {}};
}
PlcConnectionState state() const override { return connection_state; }
bool initialReadCompleted() const override { return initial_read; }


Notiek ielāde…
Atcelt
Saglabāt