Procházet zdrojové kódy

feat: 完善编辑复制粘贴与数量边界

main
suyu před 3 týdny
rodič
revize
66ce714bf9
17 změnil soubory, kde provedl 403 přidání a 78 odebrání
  1. +1
    -1
      app/src/domain/alarm_model.cpp
  2. +8
    -8
      app/src/domain/control_logic_model.cpp
  3. +4
    -4
      app/src/domain/hmi_model.cpp
  4. +15
    -13
      app/src/domain/project_limits.h
  5. +25
    -5
      app/src/domain/project_model.cpp
  6. +3
    -3
      app/src/infrastructure/json_project_storage.cpp
  7. +2
    -2
      app/src/infrastructure/plc_communication_service.cpp
  8. +1
    -1
      app/src/services/alarm_editor_service.cpp
  9. +2
    -2
      app/src/services/editor_history.h
  10. +25
    -5
      app/src/services/hmi_editor_service.cpp
  11. +47
    -19
      app/src/services/logic_editor_service.cpp
  12. +1
    -1
      app/src/services/runtime_mode_service.cpp
  13. +57
    -4
      app/tests/domain_tests.cpp
  14. +80
    -2
      app/tests/hmi_editor_service_tests.cpp
  15. +124
    -2
      app/tests/logic_editor_service_tests.cpp
  16. +6
    -4
      app/tests/plc_runtime_tests.cpp
  17. +2
    -2
      app/tests/project_management_tests.cpp

+ 1
- 1
app/src/domain/alarm_model.cpp Zobrazit soubor

@@ -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())


+ 8
- 8
app/src/domain/control_logic_model.cpp Zobrazit soubor

@@ -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))


+ 4
- 4
app/src/domain/hmi_model.cpp Zobrazit soubor

@@ -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;
}



+ 15
- 13
app/src/domain/project_limits.h Zobrazit soubor

@@ -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 开始


+ 25
- 5
app/src/domain/project_model.cpp Zobrazit soubor

@@ -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;


+ 3
- 3
app/src/infrastructure/json_project_storage.cpp Zobrazit soubor

@@ -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(


+ 2
- 2
app/src/infrastructure/plc_communication_service.cpp Zobrazit soubor

@@ -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)


+ 1
- 1
app/src/services/alarm_editor_service.cpp Zobrazit soubor

@@ -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();


+ 2
- 2
app/src/services/editor_history.h Zobrazit soubor

@@ -16,8 +16,8 @@ template <typename State>
class EditorHistory final
{
public:
// 单个编辑服务最多保留 100 条撤销和重做历史
static constexpr std::size_t kMaximumEntries = 100U;
// 单个编辑服务最多保留 30 条撤销和重做历史
static constexpr std::size_t kMaximumEntries = 30U;

template <typename Equal>
/**


+ 25
- 5
app/src/services/hmi_editor_service.cpp Zobrazit soubor

@@ -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 &current = 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 &current = 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 控件");
}

// 让整组控件尽量保留相对位置,靠近页面边界时自动向页面内收拢


+ 47
- 19
app/src/services/logic_editor_service.cpp Zobrazit soubor

@@ -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 &current = 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 &current = 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();


+ 1
- 1
app/src/services/runtime_mode_service.cpp Zobrazit soubor

@@ -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);
}


+ 57
- 4
app/tests/domain_tests.cpp Zobrazit soubor

@@ -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<int>(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<int>(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;
}


+ 80
- 2
app/tests/hmi_editor_service_tests.cpp Zobrazit soubor

@@ -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<int>(EditorHistory<int>::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<HmiControl> 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();


+ 124
- 2
app/tests/logic_editor_service_tests.cpp Zobrazit soubor

@@ -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<int>(EditorHistory<int>::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)
{


+ 6
- 4
app/tests/plc_runtime_tests.cpp Zobrazit soubor

@@ -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<RegisterAddress> 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<int>(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<RegisterAddress> maximum_contiguous_block;
for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)


+ 2
- 2
app/tests/project_management_tests.cpp Zobrazit soubor

@@ -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()


Načítá se…
Zrušit
Uložit