Преглед на файлове

feat: 扩展多字数值类型与梯形图编辑能力

main
suyu преди 3 седмици
родител
ревизия
d730d3bc1b
променени са 39 файла, в които са добавени 216 реда и са изтрити 697 реда
  1. +0
    -5
      app/src/domain/control_logic_model.cpp
  2. +0
    -1
      app/src/domain/control_logic_model.h
  3. +0
    -10
      app/src/domain/hmi_model.cpp
  4. +0
    -2
      app/src/domain/hmi_model.h
  5. +0
    -7
      app/src/domain/project_limits.h
  6. +0
    -10
      app/src/domain/project_model.cpp
  7. +0
    -2
      app/src/domain/project_model.h
  8. +0
    -22
      app/src/domain/register_repository.h
  9. +1
    -2
      app/src/infrastructure/json_project_storage.h
  10. +0
    -10
      app/src/infrastructure/plc_communication_service.cpp
  11. +0
    -4
      app/src/infrastructure/plc_communication_service.h
  12. +0
    -7
      app/src/infrastructure/plc_register_repository.cpp
  13. +0
    -2
      app/src/infrastructure/plc_register_repository.h
  14. +1
    -1
      app/src/services/hmi_editor_service.h
  15. +0
    -309
      app/src/services/logic_editor_service.cpp
  16. +0
    -26
      app/src/services/logic_editor_service.h
  17. +0
    -5
      app/src/services/offline_simulation_service.cpp
  18. +0
    -2
      app/src/services/offline_simulation_service.h
  19. +1
    -5
      app/src/services/plc_communication_gateway.h
  20. +0
    -10
      app/src/services/project_service.cpp
  21. +1
    -3
      app/src/services/project_service.h
  22. +0
    -12
      app/src/services/register_monitor_service.cpp
  23. +0
    -4
      app/src/services/register_monitor_service.h
  24. +0
    -5
      app/src/ui/logic_editor_widget.cpp
  25. +0
    -1
      app/src/ui/logic_editor_widget.h
  26. +0
    -10
      app/src/ui/main_window.cpp
  27. +0
    -5
      app/src/ui/main_window.h
  28. +0
    -10
      app/src/ui/project_workspace_controller.cpp
  29. +0
    -5
      app/src/ui/project_workspace_controller.h
  30. +1
    -1
      app/tests/alarm_service_tests.cpp
  31. +67
    -65
      app/tests/domain_tests.cpp
  32. +14
    -14
      app/tests/hmi_editor_service_tests.cpp
  33. +38
    -64
      app/tests/logic_editor_service_tests.cpp
  34. +0
    -1
      app/tests/offline_simulation_service_tests.cpp
  35. +15
    -11
      app/tests/plc_runtime_tests.cpp
  36. +12
    -12
      app/tests/project_management_tests.cpp
  37. +18
    -10
      app/tests/register_monitor_service_tests.cpp
  38. +3
    -3
      app/tests/runtime_mode_service_tests.cpp
  39. +44
    -19
      app/tests/runtime_panel_controller_tests.cpp

+ 0
- 5
app/src/domain/control_logic_model.cpp Целия файл

@@ -531,11 +531,6 @@ bool LadderRung::validateForRunning(std::string *error) const
return true;
}

bool ControlLogic::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool ControlLogic::validate(
const ProjectLimitSettings &limits, std::string *error) const
{


+ 0
- 1
app/src/domain/control_logic_model.h Целия файл

@@ -175,7 +175,6 @@ struct ControlLogic
bool enabled = true;
std::vector<VerticalConnection> verticalConnections;

bool validate(std::string *error = nullptr) const;
bool validate(
const ProjectLimitSettings &limits,
std::string *error = nullptr) const;


+ 0
- 10
app/src/domain/hmi_model.cpp Целия файл

@@ -247,11 +247,6 @@ bool HmiControl::isConfigured() const
return binding_area.has_value() && binding->area() == *binding_area;
}

bool HmiPage::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool HmiPage::validate(
const ProjectLimitSettings &limits, std::string *error) const
{
@@ -316,11 +311,6 @@ bool HmiPage::validate(
return true;
}

bool HmiPage::validateForRunning(std::string *error) const
{
return validateForRunning(defaultProjectLimitSettings(), error);
}

bool HmiPage::validateForRunning(
const ProjectLimitSettings &limits, std::string *error) const
{


+ 0
- 2
app/src/domain/hmi_model.h Целия файл

@@ -121,11 +121,9 @@ struct HmiPage
* @param error 可选错误输出,失败时写入首个校验原因
* @return 页面及其全部控件有效时返回 true
*/
bool validate(std::string *error = nullptr) const;
bool validate(
const ProjectLimitSettings &limits,
std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
bool validateForRunning(
const ProjectLimitSettings &limits,
std::string *error = nullptr) const;


+ 0
- 7
app/src/domain/project_limits.h Целия файл

@@ -92,10 +92,3 @@ inline const ProjectLimitSettings &defaultProjectLimitSettings()
static const ProjectLimitSettings settings;
return settings;
}

// 返回长期有效的默认页面尺寸,供没有传入 INI 配置的代码使用
inline const HmiDefaultSettings &defaultHmiSettings()
{
static const HmiDefaultSettings settings;
return settings;
}

+ 0
- 10
app/src/domain/project_model.cpp Целия файл

@@ -156,11 +156,6 @@ const RegisterComment *Project::findRegisterComment(
return comment == registerComments.cend() ? nullptr : &*comment;
}

bool Project::validate(std::string *error) const
{
return validate(defaultProjectLimitSettings(), error);
}

bool Project::validate(
const ProjectLimitSettings &limits, std::string *error) const
{
@@ -456,11 +451,6 @@ bool Project::validate(
return true;
}

bool Project::validateForRunning(std::string *error) const
{
return validateForRunning(defaultProjectLimitSettings(), error);
}

bool Project::validateForRunning(
const ProjectLimitSettings &limits, std::string *error) const
{


+ 0
- 2
app/src/domain/project_model.h Целия файл

@@ -48,12 +48,10 @@ struct Project
// 按地址查找工程注释,找不到时返回 nullptr
const RegisterComment *findRegisterComment(const RegisterAddress &address) const;
// 编辑态校验:允许尚未绑定完成的草稿
bool validate(std::string *error = nullptr) const;
bool validate(
const ProjectLimitSettings &limits,
std::string *error = nullptr) const;
// 运行态校验:额外要求所有运行所需对象都已配置
bool validateForRunning(std::string *error = nullptr) const;
bool validateForRunning(
const ProjectLimitSettings &limits,
std::string *error = nullptr) const;


+ 0
- 22
app/src/domain/register_repository.h Целия файл

@@ -3,7 +3,6 @@
#include "register_address.h"

#include <cstdint>
#include <array>
#include <utility>
#include <vector>

@@ -40,13 +39,6 @@ struct RegisterWriteResult
RegisterError error = RegisterError::Unavailable; // 写入失败原因
};

struct WordPairReadResult
{
bool succeeded = false;
std::array<std::int16_t, 2> values{};
RegisterError error = RegisterError::Unavailable;
};

struct WordsReadResult
{
bool succeeded = false;
@@ -118,18 +110,4 @@ public:
}
return {true, RegisterError::None};
}
virtual WordPairReadResult readWordPair(const RegisterAddress &address) const
{
const WordsReadResult result = readWords(address, 2);
return result.succeeded
? WordPairReadResult{
true, {result.values[0], result.values[1]}, RegisterError::None}
: WordPairReadResult{false, {}, result.error};
}
virtual RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
return writeWords(address, {values[0], values[1]});
}
};

+ 1
- 2
app/src/infrastructure/json_project_storage.h Целия файл

@@ -7,8 +7,7 @@ class JsonProjectStorage final : public ProjectStorage
{
public:
explicit JsonProjectStorage(
const ProjectLimitSettings &project_limits =
defaultProjectLimitSettings());
const ProjectLimitSettings &project_limits);
ProjectSaveResult save(
const Project &project, const std::string &file_path) override;
ProjectLoadResult load(const std::string &file_path) override;


+ 0
- 10
app/src/infrastructure/plc_communication_service.cpp Целия файл

@@ -401,16 +401,6 @@ const std::string &PlcCommunicationService::lastError() const
return last_error_;
}

const PlcSerialConfiguration &PlcCommunicationService::configuration() const
{
return configuration_;
}

const std::vector<PlcPollBlock> &PlcCommunicationService::pollBlocks() const
{
return poll_blocks_;
}

void PlcCommunicationService::setCallbacks(
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,


+ 0
- 4
app/src/infrastructure/plc_communication_service.h Целия файл

@@ -58,10 +58,6 @@ public:
PlcCommunicationError lastErrorType() const override;
// 返回最近一次通信错误的文字
const std::string &lastError() const override;
// 返回当前使用的串口和 Modbus 参数
const PlcSerialConfiguration &configuration() const;
// 返回当前拆分后的只读轮询计划,供诊断和边界测试使用
const std::vector<PlcPollBlock> &pollBlocks() const;
// 注册状态、首读、缓存更新和错误通知回调
void setCallbacks(
std::function<void()> state_changed,


+ 0
- 7
app/src/infrastructure/plc_register_repository.cpp Целия файл

@@ -136,10 +136,3 @@ void PlcRegisterRepository::invalidate()
valid_bits_.fill(false);
valid_words_.fill(false);
}

bool PlcRegisterRepository::hasAnyValidValue() const
{
return std::any_of(valid_bits_.cbegin(), valid_bits_.cend(), [](bool value) { return value; })
|| std::any_of(
valid_words_.cbegin(), valid_words_.cend(), [](bool value) { return value; });
}

+ 0
- 2
app/src/infrastructure/plc_register_repository.h Целия файл

@@ -35,8 +35,6 @@ public:
void updateWord(int address, std::int16_t value);
// 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断
void invalidate();
// 判断是否至少收到过一个有效 M/D 值
bool hasAnyValidValue() const;

private:
// M/D 区共用的数组长度,覆盖 0 到最大地址


+ 1
- 1
app/src/services/hmi_editor_service.h Целия файл

@@ -61,7 +61,7 @@ public:
*/
explicit HmiEditorService(
ProjectService &project_service,
const HmiDefaultSettings &hmi_defaults = defaultHmiSettings());
const HmiDefaultSettings &hmi_defaults);

/**
* @brief 按页面标识查找只读页面


+ 0
- 309
app/src/services/logic_editor_service.cpp Целия файл

@@ -1245,46 +1245,6 @@ LogicEditorResult LogicEditorService::updateRungComment(
return {true, LogicEditorError::None, {}, rung_id};
}

LogicEditorResult LogicEditorService::setConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
const LadderCell *cell = findCell(logic_id, rung_id, column);
if (cell == nullptr)
{
return failure(LogicEditorError::CellNotFound, "未找到目标条件网格");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
LogicNode candidate{"candidate", config, configured};
std::string error;
if (!candidate.validate(&error))
{
return failure(LogicEditorError::InvalidNode, error);
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *logic = editableLogic(&project, logic_id);
LadderRung *rung = editableRung(logic, rung_id);
LadderCell &editable_cell = rung->cells[static_cast<std::size_t>(column)];
const std::string node_id = makeUniqueId(*logic, nodePrefix(config));
editable_cell.kind = LadderCellKind::Node;
editable_cell.node = LogicNode{node_id, config, configured};
if (!logic->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditResult LogicEditorService::applyConditionAndAdvance(
const std::string &logic_id,
const LogicEditCursor &cursor,
@@ -1295,211 +1255,6 @@ LogicEditResult LogicEditorService::applyConditionAndAdvance(
logic_id, cursor, &config, configured);
}

LogicEditorResult LogicEditorService::insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
const LadderRung *existing = findRung(logic_id, rung_id);
if (existing == nullptr || column < 0
|| column >= ProjectLimits::kMaximumConditionColumns)
{
return failure(LogicEditorError::CellNotFound, "未找到目标条件网格");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
if (existing->cells.back().kind != LadderCellKind::Gap)
{
return failure(
LogicEditorError::InvalidOperation,
"第 10 列已有内容,无法继续向右插入");
}

HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *logic = editableLogic(&project, logic_id);
LadderRung *rung = editableRung(logic, rung_id);
for (int index = ProjectLimits::kMaximumConditionColumns - 1;
index > column;
--index)
{
rung->cells[static_cast<std::size_t>(index)] =
std::move(rung->cells[static_cast<std::size_t>(index - 1)]);
}
// 插入列会让右侧网格右移,边界 10 是固定输出侧边界,不能继续右移
for (VerticalConnection &connection : logic->verticalConnections)
{
if ((connection.upperRungId == rung_id
|| connection.lowerRungId == rung_id)
&& connection.columnBoundary >= column
&& connection.columnBoundary < ProjectLimits::kMaximumConditionColumns)
{
++connection.columnBoundary;
}
}
LadderCell inserted;
inserted.id = makeUniqueId(*logic, "cell");
inserted.kind = LadderCellKind::Node;
const std::string node_id = makeUniqueId(*logic, nodePrefix(config));
inserted.node = LogicNode{node_id, config, configured};
rung->cells[static_cast<std::size_t>(column)] = std::move(inserted);
std::string error;
if (!logic->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令");
}
std::string target_rung_id = rung_id;
if (target_rung_id.empty())
{
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(
LogicEditorError::InvalidOperation,
"梯形图行数已经达到当前上限");
}
// 新建首行和放置首个节点必须共用一条历史记录
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
target_rung_id = insertEmptyRungAt(editable, editable->rungs.size());
LadderRung *created = editableRung(editable, target_rung_id);
const auto empty = std::find_if(
created->cells.begin(), created->cells.end(),
[](const LadderCell &cell)
{
return cell.kind == LadderCellKind::Gap;
});
if (empty == created->cells.end())
{
rollbackEdit(std::move(before), modified_before);
return failure(
LogicEditorError::InvalidOperation,
"条件区 10 列已经占满");
}
const std::string node_id = makeUniqueId(*editable, nodePrefix(config));
LadderCell &cell = created->cells[
static_cast<std::size_t>(std::distance(created->cells.begin(), empty))];
cell.kind = LadderCellKind::Node;
cell.node = LogicNode{node_id, config, configured};
std::string error;
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}
const LadderRung *rung = findRung(logic_id, target_rung_id);
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图行");
}
const auto empty = std::find_if(
rung->cells.cbegin(), rung->cells.cend(),
[](const LadderCell &cell) { return cell.kind == LadderCellKind::Gap; });
if (empty == rung->cells.cend())
{
return failure(LogicEditorError::InvalidOperation, "条件区 10 列已经占满");
}
return setConditionAtColumn(
logic_id,
target_rung_id,
static_cast<int>(std::distance(rung->cells.cbegin(), empty)),
config,
configured);
}

LogicEditorResult LogicEditorService::appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (column_span <= 0
|| column_span > ProjectLimits::kMaximumConditionColumns)
{
return failure(LogicEditorError::InvalidOperation, "横线参数无效");
}
if (rung_id.empty())
{
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(
LogicEditorError::InvalidOperation,
"梯形图行数已经达到当前上限");
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
const std::string new_rung_id = insertEmptyRungAt(
editable, editable->rungs.size());
LadderRung *rung = editableRung(editable, new_rung_id);
const int first = ProjectLimits::kMaximumConditionColumns - column_span;
for (int column = first;
column < ProjectLimits::kMaximumConditionColumns;
++column)
{
rung->cells[static_cast<std::size_t>(column)].kind =
LadderCellKind::Wire;
}
std::string error;
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, new_rung_id};
}
const LadderRung *rung = findRung(logic_id, rung_id);
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图行");
}
const int first = ProjectLimits::kMaximumConditionColumns - column_span;
return setHorizontalWireRange(logic_id, rung_id, first,
ProjectLimits::kMaximumConditionColumns - 1,
true);
}

LogicEditResult LogicEditorService::applyWireAndAdvance(
const std::string &logic_id,
const LogicEditCursor &cursor)
@@ -3055,70 +2810,6 @@ bool LogicEditorService::areConditionNodesContiguous(
== static_cast<int>(columns.size());
}

LogicEditorResult LogicEditorService::pasteRung(
const std::string &logic_id,
const LadderRung &source)
{
const ControlLogic *logic = findLogic(logic_id);
std::string error;
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (!source.validateStructure(&error))
{
return failure(LogicEditorError::InvalidOperation, error);
}
if (logic->rungs.size()
>= project_service_.projectLimits().maximumRungsPerLogic
|| totalRungCount(project_service_.project())
>= ProjectLimits::kMaximumRungsPerProject)
{
return failure(LogicEditorError::InvalidOperation, "梯形图行数已经达到上限");
}
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
ControlLogic *editable = editableLogic(&project, logic_id);
LadderRung pasted = makeEmptyRung(*editable, editable->rungs.size());
pasted.comment = source.comment;
for (std::size_t column = 0U; column < source.cells.size(); ++column)
{
pasted.cells[column].kind = source.cells[column].kind;
if (source.cells[column].node.has_value())
{
const LogicNode &source_node = *source.cells[column].node;
pasted.cells[column].node = LogicNode{
makeUniqueId(*editable, nodePrefix(source_node.config)),
source_node.config,
source_node.configured};
}
}
if (source.output.has_value())
{
pasted.output = LogicNode{
makeUniqueId(*editable, nodePrefix(source.output->config)),
source.output->config,
source.output->configured};
}
const std::string pasted_id = pasted.id;
editable->rungs.push_back(std::move(pasted));
refreshRungNames(editable);
if (!editable->validateStructure(project_service_.projectLimits(), &error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, pasted_id};
}

LogicEditorResult LogicEditorService::removeNode(
const std::string &logic_id, const std::string &node_id)
{
return removeNodes(logic_id, {node_id});
}

LogicEditorResult LogicEditorService::removeNodes(
const std::string &logic_id,
const std::vector<std::string> &node_ids)


+ 0
- 26
app/src/services/logic_editor_service.h Целия файл

@@ -214,38 +214,17 @@ public:
const std::string &rung_id,
const std::string &comment);

LogicEditorResult setConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured = false);
LogicEditResult applyConditionAndAdvance(
const std::string &logic_id,
const LogicEditCursor &cursor,
const LogicNodeConfig &config,
bool configured = false);
LogicEditorResult insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured = false);
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured = false);
LogicEditorResult setHorizontalWireRange(
const std::string &logic_id,
const std::string &rung_id,
int first_column,
int last_column,
bool connected);
LogicEditorResult appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span = 1);
LogicEditResult applyWireAndAdvance(
const std::string &logic_id,
const LogicEditCursor &cursor);
@@ -320,11 +299,6 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &node_ids) const;
LogicEditorResult pasteRung(
const std::string &logic_id,
const LadderRung &source);
LogicEditorResult removeNode(
const std::string &logic_id, const std::string &node_id);
LogicEditorResult removeNodes(
const std::string &logic_id,
const std::vector<std::string> &node_ids);


+ 0
- 5
app/src/services/offline_simulation_service.cpp Целия файл

@@ -105,11 +105,6 @@ SimulationState OfflineSimulationService::state() const
return state_;
}

int OfflineSimulationService::scanIntervalMs() const
{
return timer_.interval();
}

std::uint64_t OfflineSimulationService::successfulScanCount() const
{
return successful_scan_count_;


+ 0
- 2
app/src/services/offline_simulation_service.h Целия файл

@@ -64,8 +64,6 @@ public:

// 返回当前离线仿真生命周期状态
SimulationState state() const;
// 返回当前定时器扫描周期,单位为毫秒
int scanIntervalMs() const;
// 返回本次会话成功完成的扫描轮数
std::uint64_t successfulScanCount() const;
// 返回最近一次扫描错误;无错误时为成功结果


+ 1
- 5
app/src/services/plc_communication_gateway.h Целия файл

@@ -163,11 +163,7 @@ public:
*/
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterWordRange> &multi_word_ranges)
{
(void)multi_word_ranges;
return setPollAddresses(addresses);
}
const std::vector<RegisterWordRange> &multi_word_ranges) = 0;

// 返回当前连接生命周期状态;Connected 不代表首读资格已经完成
virtual PlcConnectionState state() const = 0;


+ 0
- 10
app/src/services/project_service.cpp Целия файл

@@ -24,11 +24,6 @@ std::string generateProjectId()

} // namespace

ProjectService::ProjectService(ProjectStorage &storage)
: ProjectService(storage, defaultProjectLimitSettings())
{
}

ProjectService::ProjectService(
ProjectStorage &storage,
const ProjectLimitSettings &project_limits)
@@ -49,11 +44,6 @@ Project &ProjectService::editProject()
return project_;
}

const std::string &ProjectService::currentFilePath() const
{
return current_file_path_;
}

bool ProjectService::hasCurrentFile() const
{
return !current_file_path_.empty();


+ 1
- 3
app/src/services/project_service.h Целия файл

@@ -27,8 +27,7 @@ struct ProjectOperationResult
class ProjectService
{
public:
// 使用外部提供的工程存储实现创建服务
explicit ProjectService(ProjectStorage &storage);
// 使用外部提供的工程存储实现和启动配置创建服务
ProjectService(
ProjectStorage &storage,
const ProjectLimitSettings &project_limits);
@@ -37,7 +36,6 @@ public:
const Project &project() const;
// 获取服务层授权的可编辑引用,调用方必须随后完成校验
Project &editProject();
const std::string &currentFilePath() const;
bool hasCurrentFile() const;
bool isModified() const;
const ProjectLimitSettings &projectLimits() const;


+ 0
- 12
app/src/services/register_monitor_service.cpp Целия файл

@@ -225,18 +225,6 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit(
return captureInitialBit(address, value, active_result);
}

RegisterMonitorWriteResult RegisterMonitorService::writeWord(
const RegisterAddress &address, std::int16_t value)
{
return writeNumeric(address, RegisterDataType::Int16, value);
}

RegisterMonitorWriteResult RegisterMonitorService::writeFloat(
const RegisterAddress &address, float value)
{
return writeNumeric(address, RegisterDataType::Float32, value);
}

RegisterMonitorWriteResult RegisterMonitorService::writeNumeric(
const RegisterAddress &address, RegisterDataType data_type, double value)
{


+ 0
- 4
app/src/services/register_monitor_service.h Целия файл

@@ -91,10 +91,6 @@ public:
* @param value 要写入的 D 字值
* @return 成功或包含底层 RegisterError 和用户可读文本的失败结果
*/
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
RegisterMonitorWriteResult writeFloat(
const RegisterAddress &address, float value);
RegisterMonitorWriteResult writeNumeric(
const RegisterAddress &address, RegisterDataType data_type, double value);
/**


+ 0
- 5
app/src/ui/logic_editor_widget.cpp Целия файл

@@ -840,11 +840,6 @@ void LogicEditorWidget::clearRuntimeTrace()
rebuildScene();
}

bool LogicEditorWidget::runtimeTraceEnabled() const
{
return runtime_trace_enabled_;
}

void LogicEditorWidget::reloadLogic()
{
setLogicId(logic_id_);


+ 0
- 1
app/src/ui/logic_editor_widget.h Целия файл

@@ -40,7 +40,6 @@ public:
const LogicTraceSnapshot &trace,
const std::string &fault_node_id = {});
void clearRuntimeTrace();
bool runtimeTraceEnabled() const;
void reloadLogic();
void clearSelection();
void selectNode(const std::string &node_id);


+ 0
- 10
app/src/ui/main_window.cpp Целия файл

@@ -576,16 +576,6 @@ void MainWindow::closeEvent(QCloseEvent *event)
QMainWindow::closeEvent(event);
}

const std::string &MainWindow::currentHmiPageId() const
{
return current_hmi_page_id_;
}

const std::string &MainWindow::currentLogicId() const
{
return current_logic_id_;
}

void MainWindow::configureActions()
{
mode_action_group_ = new QActionGroup(this);


+ 0
- 5
app/src/ui/main_window.h Целия файл

@@ -111,11 +111,6 @@ public:
/** 释放窗口拥有的控制器和可选导航服务 */
~MainWindow() override;

/** 返回当前选中的 HMI 页面标识 */
const std::string &currentHmiPageId() const;
/** 返回当前选中的控制逻辑标识 */
const std::string &currentLogicId() const;

protected:
/** 处理主窗口及子控件的事件过滤请求 */
bool eventFilter(QObject *watched, QEvent *event) override;


+ 0
- 10
app/src/ui/project_workspace_controller.cpp Целия файл

@@ -256,16 +256,6 @@ void ProjectWorkspaceController::refresh()
updateActions();
}

const std::string &ProjectWorkspaceController::currentHmiPageId() const
{
return current_hmi_page_id_;
}

const std::string &ProjectWorkspaceController::currentLogicId() const
{
return current_logic_id_;
}

void ProjectWorkspaceController::handleSelectionChanged()
{
QTreeWidgetItem *item = ui_.projectTree->currentItem();


+ 0
- 5
app/src/ui/project_workspace_controller.h Целия файл

@@ -89,11 +89,6 @@ public:
/** 启用或停用当前选中的控制逻辑 */
void toggleSelectedLogicEnabled();

/** 返回当前 HMI 页面标识 */
const std::string &currentHmiPageId() const;
/** 返回当前控制逻辑标识 */
const std::string &currentLogicId() const;

private:
/** 工程树项目的三种业务类型 */
enum class ItemKind


+ 1
- 1
app/tests/alarm_service_tests.cpp Целия файл

@@ -33,7 +33,7 @@ const AlarmRecord *findRecord(
void testAlarmDefinitionsAndRuntimeLifecycle()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
AlarmEditorService editor_service(project_service);
VirtualRegisterRepository repository;
AlarmService alarm_service(project_service, repository);


+ 67
- 65
app/tests/domain_tests.cpp Целия файл

@@ -159,13 +159,15 @@ void testMultiWordCodecsAndBlockAccess()

VirtualRegisterRepository repository;
const RegisterAddress d10{RegisterArea::D, 10};
require(repository.writeWordPair(d10, Float32Codec::encode(-2.5f)).succeeded,
const auto float_words = Float32Codec::encode(-2.5f);
require(repository.writeWords(
d10, {float_words[0], float_words[1]}).succeeded,
"virtual repository must write Float32 low and high words");
const WordPairReadResult pair = repository.readWordPair(d10);
const WordsReadResult pair = repository.readWords(d10, 2);
require(pair.succeeded
&& Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f,
"virtual repository must read Float32 from two consecutive words");
require(!repository.writeWordPair({RegisterArea::D, 4000}, {}).succeeded,
require(!repository.writeWords({RegisterArea::D, 4000}, {}).succeeded,
"Float32 write at D4000 must be rejected");
require(repository.writeWords(
{RegisterArea::D, 30},
@@ -294,12 +296,12 @@ void testMultiWordHmiBoundaries()
display.binding = RegisterAddress{RegisterArea::D, 3999};
display.dataType = RegisterDataType::Float32;
project.hmiPages.front().controls.push_back(display);
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"Float32 D3999 must be valid and occupy D3999~D4000");

project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 4000};
require(!project.validate(), "Float32 D4000 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Float32 D4000 must be rejected");

project = makeValidProject();
HmiControl int32_display = display;
@@ -307,10 +309,10 @@ void testMultiWordHmiBoundaries()
int32_display.dataType = RegisterDataType::Int32;
int32_display.binding = RegisterAddress{RegisterArea::D, 3999};
project.hmiPages.front().controls.push_back(int32_display);
require(project.validate(), "Int32 D3999 must be valid");
require(project.validate(defaultProjectLimitSettings()), "Int32 D3999 must be valid");
project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 4000};
require(!project.validate(), "Int32 D4000 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Int32 D4000 must be rejected");

project = makeValidProject();
HmiControl double_display = display;
@@ -318,13 +320,13 @@ void testMultiWordHmiBoundaries()
double_display.dataType = RegisterDataType::Float64;
double_display.binding = RegisterAddress{RegisterArea::D, 3996};
project.hmiPages.front().controls.push_back(double_display);
require(project.validate(), "Double D3996 must be a valid even start address");
require(project.validate(defaultProjectLimitSettings()), "Double D3996 must be a valid even start address");
project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 3997};
require(!project.validate(), "Double D3997 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Double D3997 must be rejected");
project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 3995};
require(!project.validate(), "Double odd start addresses must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "Double odd start addresses must be rejected");

project = makeValidProject();
HmiControl invalid_display = display;
@@ -332,7 +334,7 @@ void testMultiWordHmiBoundaries()
invalid_display.binding.reset();
invalid_display.dataType = static_cast<RegisterDataType>(99);
project.hmiPages.front().controls.push_back(invalid_display);
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"an unbound HMI draft must still reject an unknown numeric type");

project = makeValidProject();
@@ -342,12 +344,12 @@ void testMultiWordHmiBoundaries()
second.id = "float-display-duplicate";
project.hmiPages.front().controls.push_back(first);
project.hmiPages.front().controls.push_back(second);
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"same Float32 start address and type may be bound more than once");

second.dataType = RegisterDataType::Int16;
project.hmiPages.front().controls.back() = second;
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"different HMI data types may not partially overlap");

for (const RegisterDataType type : {
@@ -372,7 +374,7 @@ void testMultiWordHmiBoundaries()
RegisterArea::D,
10 + registerDataTypeWordCount(type) - 1}},
true};
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"16-bit instructions must not write inside any multi-word HMI range");
}
}
@@ -386,28 +388,28 @@ void testHmiAppearancePropertyBoundaries()
button.properties[HmiAppearanceProperty::kFontSize] = "18";
button.properties[HmiAppearanceProperty::kFontBold] = "true";
button.properties[HmiAppearanceProperty::kFontItalic] = "false";
require(project.validate(), "valid HMI appearance properties must pass validation");
require(project.validate(defaultProjectLimitSettings()), "valid HMI appearance properties must pass validation");

button.properties[HmiAppearanceProperty::kTextColor] = "red";
require(!project.validate(), "text colors must use the #RRGGBB format");
require(!project.validate(defaultProjectLimitSettings()), "text colors must use the #RRGGBB format");

project = makeValidProject();
HmiControl &font_control = project.hmiPages.front().controls.front();
font_control.properties[HmiAppearanceProperty::kFontSize] = "5";
require(!project.validate(), "font sizes below the minimum must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "font sizes below the minimum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "73";
require(!project.validate(), "font sizes above the maximum must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "font sizes above the maximum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "large";
require(!project.validate(), "non-numeric font sizes must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "non-numeric font sizes must be rejected");

project = makeValidProject();
HmiControl &style_control = project.hmiPages.front().controls.front();
style_control.properties[HmiAppearanceProperty::kFontBold] = "yes";
require(!project.validate(), "font style flags must be true or false");
require(!project.validate(defaultProjectLimitSettings()), "font style flags must be true or false");

project = makeValidProject();
project.hmiPages.front().controls.front().properties["legacyColor"] = "green";
require(project.validate(), "unknown HMI extension properties must remain supported");
require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported");
}

Project makeValidProject()
@@ -475,11 +477,11 @@ void testMultiPageAndLogicDomainRules()
label.type = HmiControlType::Label;
label.text = "Machine";
project.hmiPages.front().controls.push_back(label);
require(project.validate(), "an unbound label must be a valid static control");
require(project.validate(defaultProjectLimitSettings()), "an unbound label must be a valid static control");

project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::M, 10};
require(!project.validate(), "labels must reject register bindings");
require(!project.validate(defaultProjectLimitSettings()), "labels must reject register bindings");

project = makeValidProject();
project.hmiPages.push_back(settings);
@@ -489,21 +491,21 @@ void testMultiPageAndLogicDomainRules()
jump.text = "Settings";
jump.pageJump = HmiPageJumpConfig{settings.id};
project.hmiPages.front().controls.push_back(jump);
require(project.validate() && project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()) && project.validateForRunning(defaultProjectLimitSettings()),
"a page jump must resolve its target by stable page id");

project.hmiPages.front().controls.back().pageJump->targetPageId = "missing";
require(!project.validate(), "a page jump must reject a missing target page");
require(!project.validate(defaultProjectLimitSettings()), "a page jump must reject a missing target page");

project = makeValidProject();
project.initialHmiPageId = "missing";
require(!project.validate(), "the initial HMI page id must resolve to a page");
require(!project.validate(defaultProjectLimitSettings()), "the initial HMI page id must resolve to a page");

project = makeValidProject();
HmiPage duplicate_name = settings;
duplicate_name.name = project.hmiPages.front().name;
project.hmiPages.push_back(duplicate_name);
require(!project.validate(), "HMI page names must be unique");
require(!project.validate(defaultProjectLimitSettings()), "HMI page names must be unique");

project = makeValidProject();
ControlLogic disabled_draft;
@@ -515,17 +517,17 @@ void testMultiPageAndLogicDomainRules()
draft_rung.name = "Draft network";
disabled_draft.rungs.push_back(std::move(draft_rung));
project.controlLogics.push_back(disabled_draft);
require(project.validateForRunning(),
require(project.validateForRunning(defaultProjectLimitSettings()),
"a disabled draft logic must not block offline running");

project.controlLogics.back().name = project.controlLogics.front().name;
require(!project.validate(), "control logic names must be unique");
require(!project.validate(defaultProjectLimitSettings()), "control logic names must be unique");

project = makeValidProject();
project.hmiPages.front().controls.front().type =
static_cast<HmiControlType>(99);
project.hmiPages.front().controls.front().binding.reset();
require(!project.validate(), "unknown HMI control types must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "unknown HMI control types must be rejected");
}

void testQuantityBoundaries()
@@ -540,9 +542,9 @@ void testQuantityBoundaries()
400,
{}});
}
require(project.validate(), "an HMI page count at the configured limit must be accepted");
require(project.validate(defaultProjectLimitSettings()), "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");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page count of 129 must be rejected");

project = makeValidProject();
project.hmiPages.clear();
@@ -569,7 +571,7 @@ void testQuantityBoundaries()
project.hmiPages.push_back(std::move(page));
}
project.initialHmiPageId = project.hmiPages.front().id;
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"an HMI control count over the project limit must be rejected");

project = makeValidProject();
@@ -585,14 +587,14 @@ void testQuantityBoundaries()
label.text = "label";
project.hmiPages.front().controls.push_back(std::move(label));
}
require(project.validate(), "a page control count at the configured limit must be accepted");
require(project.validate(defaultProjectLimitSettings()), "a page control count at the configured limit 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");
require(!project.validate(defaultProjectLimitSettings()), "a page control count of 513 must be rejected");

project = makeValidProject();
project.controlLogics.clear();
@@ -616,24 +618,24 @@ void testQuantityBoundaries()
}
project.controlLogics.push_back(std::move(logic));
}
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"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;
require(project.validate(), "an HMI page size of 1600 by 800 must be accepted");
require(project.validate(defaultProjectLimitSettings()), "an HMI page size of 1600 by 800 must be accepted");
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1;
require(!project.validate(), "an HMI page width of 1601 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 1601 must be rejected");
project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1;
require(!project.validate(), "an HMI page width of 319 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 319 must be rejected");
project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth;
project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1;
require(!project.validate(), "an HMI page height of 199 must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "an HMI page height of 199 must be rejected");

project = makeValidProject();
project.controlLogics.front().rungs.front().cells.pop_back();
require(!project.validate(),
require(!project.validate(defaultProjectLimitSettings()),
"a ladder row with fewer than ten cells must be rejected");
}

@@ -686,19 +688,19 @@ void testEdgeAndCommentBoundaries()
comment_rung.id = "comment-rung";
comment_rung.name = "Comment rung";
comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a');
require(comment_rung.validate(), "a rung comment at the byte limit must be valid");
require(comment_rung.validateStructure(), "a rung comment at the byte limit must be valid");
comment_rung.comment.push_back('a');
require(!comment_rung.validate(), "a rung comment above the byte limit must fail");
require(!comment_rung.validateStructure(), "a rung comment above the byte limit must fail");
comment_rung.comment = "第一行\n第二行";
require(!comment_rung.validate(), "a multiline rung comment must be rejected");
require(!comment_rung.validateStructure(), "a multiline rung comment must be rejected");
comment_rung.comment = "第一行\r第二行";
require(!comment_rung.validate(), "a rung comment containing CR must be rejected");
require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected");

Project project = makeValidProject();
project.registerComments = {
{RegisterAddress{RegisterArea::M, 0}, "启动按钮"},
{RegisterAddress{RegisterArea::M, 0}, "重复地址"}};
require(!project.validate(), "duplicate register comments must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "duplicate register comments must be rejected");
}

void testDataInstructionBoundaries()
@@ -772,13 +774,13 @@ void testLadderLogicBoundaries()
logic.verticalConnections = {
{"vertical-left", "rung-1", "rung-2", 0},
{"vertical-right", "rung-1", "rung-2", 1}};
require(logic.validate() && logic.validateForRunning(),
require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
"a ten-cell grid with adjacent vertical edges must be valid");

logic.rungs.front().cells[5].kind = LadderCellKind::Gap;
std::string connectivity_error;
require(
logic.validate() && !logic.validateForRunning(&connectivity_error)
logic.validate(defaultProjectLimitSettings()) && !logic.validateForRunning(&connectivity_error)
&& connectivity_error.find("第 1 行") != std::string::npos
&& connectivity_error.find("第 6 列") != std::string::npos,
"a disconnected output must report its visual row and break column");
@@ -794,7 +796,7 @@ void testLadderLogicBoundaries()
{"vertical-left", "rung-1", "rung-2", 0},
{"vertical-bypass", "rung-1", "rung-2", 6}};
require(
logic.validateForRunning(),
logic.validateForRunning(defaultProjectLimitSettings()),
"a vertical branch that bypasses a gap must keep the output reachable");

logic.rungs = {upper, lower};
@@ -806,7 +808,7 @@ void testLadderLogicBoundaries()
CoilNodeConfig{
RegisterAddress{RegisterArea::M, 2}, CoilMode::Normal},
true};
require(!logic.validate(), "a condition cell must reject output nodes");
require(!logic.validate(defaultProjectLimitSettings()), "a condition cell must reject output nodes");

logic.rungs.front() = upper;
logic.rungs.front().output = LogicNode{
@@ -814,25 +816,25 @@ void testLadderLogicBoundaries()
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 2}, ContactMode::NormallyOpen},
true};
require(!logic.validate(), "the output slot must reject condition nodes");
require(!logic.validate(defaultProjectLimitSettings()), "the output slot must reject condition nodes");

logic.rungs.front() = upper;
logic.rungs.front().output.reset();
require(logic.validate() && logic.validateForRunning(),
require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()),
"a row without an output may act as a connected branch");

logic.rungs.front() = upper;
logic.rungs.front().cells[1].id = logic.rungs.front().cells[0].id;
require(!logic.validate(), "cell ids must be unique within a logic");
require(!logic.validate(defaultProjectLimitSettings()), "cell ids must be unique within a logic");

logic.rungs.front() = upper;
logic.verticalConnections.front().lowerRungId = "missing-rung";
require(!logic.validate(), "vertical edges must reference adjacent rows");
require(!logic.validate(defaultProjectLimitSettings()), "vertical edges must reference adjacent rows");

logic.verticalConnections = {
{"vertical-left", "rung-1", "rung-2", 0},
{"vertical-copy", "rung-1", "rung-2", 0}};
require(!logic.validate(),
require(!logic.validate(defaultProjectLimitSettings()),
"one row boundary must not contain duplicate vertical edges");
}

@@ -840,38 +842,38 @@ void testModelsValidateBindingsAndIdentifiers()
{
// 聚合验证必须拒绝错误绑定、重复标识和越界控件
Project project = makeValidProject();
require(project.validate(), "valid project model must pass validation");
require(project.validate(defaultProjectLimitSettings()), "valid project model must pass validation");

project.hmiPages.front().controls.front().binding =
RegisterAddress{RegisterArea::D, 0};
require(!project.validate(), "button bound to D area must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "button bound to D area must be rejected");

project = makeValidProject();
project.hmiPages.push_back(project.hmiPages.front());
require(!project.validate(), "duplicate HMI page id must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "duplicate HMI page id must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.front().bounds.x = -1;
require(!project.validate(), "controls outside the page must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "controls outside the page must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.front().bounds.width = 801;
require(!project.validate(), "controls wider than the page must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "controls wider than the page must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.front().properties.emplace("", "value");
require(!project.validate(), "empty HMI property names must be rejected");
require(!project.validate(defaultProjectLimitSettings()), "empty HMI property names must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.front().binding.reset();
require(project.validate(), "unbound HMI control must be accepted in a draft");
require(!project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()), "unbound HMI control must be accepted in a draft");
require(!project.validateForRunning(defaultProjectLimitSettings()),
"unbound HMI control must block runtime validation");

project = makeValidProject();
project.controlLogics.front().rungs.front().output->configured = false;
require(project.validate(), "unconfigured ladder node must be accepted in a draft");
require(!project.validateForRunning(),
require(project.validate(defaultProjectLimitSettings()), "unconfigured ladder node must be accepted in a draft");
require(!project.validateForRunning(defaultProjectLimitSettings()),
"unconfigured ladder node must block runtime validation");
}



+ 14
- 14
app/tests/hmi_editor_service_tests.cpp Целия файл

@@ -24,8 +24,8 @@ void testControlEditing()
{
// 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const HmiEditorResult page_result = service.ensureDefaultPage();
require(page_result.succeeded, "default HMI page creation must succeed");

@@ -190,8 +190,8 @@ void testRuntimeUsesRegisterRepository()
void testHistoryAndAtomicBatchDelete()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
@@ -244,8 +244,8 @@ void testHistoryAndAtomicBatchDelete()
void testBatchPasteControls()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult first = service.addControl(
page_id, HmiControlType::NumericDisplay);
@@ -290,8 +290,8 @@ void testBatchPasteControls()
void testRuntimeValidationAndPasteBoundaries()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService editor(project_service, HmiDefaultSettings{});
const std::string page_id = editor.ensureDefaultPage().id;

HmiControl label;
@@ -366,8 +366,8 @@ void testRuntimeValidationAndPasteBoundaries()
void testAppearanceEditing()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
require(label.succeeded, "a label must be available for appearance editing");
@@ -407,8 +407,8 @@ void testAppearanceEditing()
void testPageLifecycleAndNavigation()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string main_id = service.ensureDefaultPage().id;
require(project_service.project().initialHmiPageId == main_id,
"the first page must become the initial HMI page");
@@ -474,8 +474,8 @@ void testPageLifecycleAndNavigation()
void testPageResizeIsAtomicAndUndoable()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
require(label.succeeded, "page resize fixture must add a control");


+ 38
- 64
app/tests/logic_editor_service_tests.cpp Целия файл

@@ -20,10 +20,22 @@ ContactNodeConfig contact(
return {RegisterAddress{RegisterArea::M, address}, mode};
}

LogicEditorResult setConditionAtColumn(
LogicEditorService &editor,
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
return editor.applyConditionAndAdvance(
logic_id, {rung_id, column, false}, config, configured).edit;
}

struct Fixture
{
TestSupport::InMemoryProjectStorage storage;
ProjectService projects{storage};
ProjectService projects{storage, defaultProjectLimitSettings()};
LogicEditorService editor{projects};
std::string logicId;

@@ -109,7 +121,7 @@ void testContinuousGridAndIndependentHorizontalWires()
&& !fixture.editor.canUndo(),
"repeating an existing horizontal wire range must not dirty the project or history");

const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 3, contact(3), true);
require(node.succeeded, "a wire cell must accept a condition node");
require(
@@ -211,39 +223,6 @@ void testInsertRowSplitsVerticalEdges()
}
}

void testInsertConditionMovesOnlyRelatedVerticalBoundaries()
{
Fixture fixture;
const std::string first = fixture.addRung();
const std::string second = fixture.addRung();
const std::string third = fixture.addRung();
const std::string fourth = fixture.addRung();
require(
fixture.editor.setVerticalConnection(
fixture.logicId, first, second, 2, true).succeeded
&& fixture.editor.setVerticalConnection(
fixture.logicId, first, second, 10, true).succeeded
&& fixture.editor.setVerticalConnection(
fixture.logicId, third, fourth, 5, true).succeeded,
"fixture must create related, output-side, and unrelated edges");

const LogicEditorResult inserted = fixture.editor.insertConditionAtColumn(
fixture.logicId, first, 2, contact(12), true);
require(inserted.succeeded,
"inserting a condition into the grid must succeed");
const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId);
require(
connectionAt(*logic, first, second, 2) == nullptr
&& connectionAt(*logic, first, second, 3) != nullptr,
"an edge at the insertion point must follow shifted grid content");
require(
connectionAt(*logic, first, second, 10) != nullptr,
"the fixed output-side boundary must remain at column ten");
require(
connectionAt(*logic, third, fourth, 5) != nullptr,
"inserting into one row must not move edges in unrelated rows");
}

void testDeleteRowMergesOnlyContinuousEdges()
{
Fixture fixture;
@@ -289,9 +268,9 @@ void testParallelBranchCreatesConnectedVisualRow()
{
Fixture fixture;
const std::string source = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(1), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 3, contact(2), true);
require(first.succeeded && second.succeeded,
"fixture must place adjacent conditions");
@@ -323,9 +302,9 @@ void testParallelBranchReusesExistingEdges()
Fixture fixture;
const std::string source = fixture.addRung();
const std::string following = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(4), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 3, contact(5), true);
require(first.succeeded && second.succeeded,
"fixture must place the branch source conditions");
@@ -358,7 +337,7 @@ void testNodeDeletionAndHistoryAreAtomic()
{
Fixture fixture;
const std::string rung_id = fixture.addRung();
const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 0, contact(9), true);
require(node.succeeded, "fixture must place a condition");
fixture.editor.clearHistory();
@@ -380,7 +359,7 @@ void testNodeDeletionAndHistoryAreAtomic()
}
require(fixture.editor.redo().succeeded, "the drag must redo atomically");
require(
fixture.editor.removeNode(fixture.logicId, node.id).succeeded,
fixture.editor.removeNodes(fixture.logicId, {node.id}).succeeded,
"deleting a condition node must succeed");
rung = fixture.editor.findRung(fixture.logicId, rung_id);
require(
@@ -406,7 +385,7 @@ void testSelectionDeletionIsAtomic()
const std::string upper = fixture.addRung();
const std::string lower = fixture.addRung();
require(
fixture.editor.setConditionAtColumn(
setConditionAtColumn(fixture.editor,
fixture.logicId, upper, 0, contact(30), true).succeeded
&& fixture.editor.setHorizontalWireRange(
fixture.logicId, upper, 1, 1, true).succeeded
@@ -470,7 +449,7 @@ void testInvalidSelectionDeletionDoesNotMutateOrRecordHistory()
Fixture fixture;
const std::string rung_id = fixture.addRung();
require(
fixture.editor.setConditionAtColumn(
setConditionAtColumn(fixture.editor,
fixture.logicId, rung_id, 0, contact(40), true).succeeded
&& fixture.editor.setHorizontalWireRange(
fixture.logicId, rung_id, 1, 1, true).succeeded,
@@ -564,11 +543,9 @@ void testProjectRungLimitAppliesToBranchAndPaste()
{
Fixture fixture;
const std::string source = fixture.addRung();
const LogicEditorResult node = fixture.editor.setConditionAtColumn(
const LogicEditorResult node = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 0, contact(20), true);
require(node.succeeded, "fixture must create a branch source node");
const LadderRung source_copy = *fixture.editor.findRung(
fixture.logicId, source);

Project &project = fixture.projects.editProject();
std::size_t remaining = ProjectLimits::kMaximumRungsPerProject - 1U;
@@ -601,16 +578,13 @@ void testProjectRungLimitAppliesToBranchAndPaste()
remaining -= count;
++logic_index;
}
require(project.validate(),
require(project.validate(defaultProjectLimitSettings()),
"the project-wide rung limit fixture must itself be valid");

require(
!fixture.editor.addParallelBranch(
fixture.logicId, source, {node.id}, contact(21), true).succeeded,
"parallel insertion must respect the project-wide rung limit");
require(
!fixture.editor.pasteRung(fixture.logicId, source_copy).succeeded,
"row paste must respect the project-wide rung limit");
require(
fixture.editor.findLogic(fixture.logicId)->rungs.size() == 1U,
"failed limit checks must not partially add a row");
@@ -635,9 +609,9 @@ void testFirstEditOnEmptyLogicIsAtomic()
Fixture fixture;
fixture.editor.clearHistory();

const LogicEditorResult condition = fixture.editor.appendCondition(
const LogicEditResult condition = fixture.editor.applyConditionAndAdvance(
fixture.logicId, {}, contact(11), true);
require(condition.succeeded,
require(condition.edit.succeeded,
"placing the first condition must create the first row");
const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId);
require(logic->rungs.size() == 1U
@@ -653,10 +627,11 @@ void testFirstEditOnEmptyLogicIsAtomic()
"redo must restore the atomically created first condition and row");

fixture.editor.clearHistory();
const LogicEditorResult wire = fixture.editor.appendWire(
fixture.logicId, {}, 1);
require(wire.succeeded,
"appending a wire to an empty logic must create a first row");
const std::string wire_rung = fixture.addRung();
const LogicEditResult wire = fixture.editor.applyWireAndAdvance(
fixture.logicId, {wire_rung, 9, false});
require(wire.edit.succeeded,
"a wire edit must succeed on a newly added row");
logic = fixture.editor.findLogic(fixture.logicId);
require(logic->rungs.size() == 2U
&& logic->rungs.back().cells.back().kind == LadderCellKind::Wire,
@@ -759,9 +734,9 @@ void testOutputAutomaticallyCompletesTrailingWires()
Fixture trailing;
const std::string rung_id = trailing.addRung();
require(
trailing.editor.setConditionAtColumn(
setConditionAtColumn(trailing.editor,
trailing.logicId, rung_id, 0, contact(55), true).succeeded
&& trailing.editor.setConditionAtColumn(
&& setConditionAtColumn(trailing.editor,
trailing.logicId, rung_id, 2, contact(56), true).succeeded,
"the trailing-wire fixture must leave one intentional middle gap");
trailing.editor.clearHistory();
@@ -888,9 +863,9 @@ void testMixedAndSparseGridClipboardFragments()
Fixture fixture;
const std::string source = fixture.addRung();
const std::string target = fixture.addRung();
const LogicEditorResult first = fixture.editor.setConditionAtColumn(
const LogicEditorResult first = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 0, contact(70), true);
const LogicEditorResult second = fixture.editor.setConditionAtColumn(
const LogicEditorResult second = setConditionAtColumn(fixture.editor,
fixture.logicId, source, 2, contact(71), true);
require(
first.succeeded && second.succeeded
@@ -941,7 +916,7 @@ void testMixedAndSparseGridClipboardFragments()
require(
sparse.editor.setHorizontalWireRange(
sparse.logicId, sparse_source, 0, 0, true).succeeded
&& sparse.editor.setConditionAtColumn(
&& setConditionAtColumn(sparse.editor,
sparse.logicId, sparse_source, 2, contact(72), true).succeeded
&& sparse.editor.setHorizontalWireRange(
sparse.logicId, sparse_target, 5, 5, true).succeeded,
@@ -973,7 +948,7 @@ void testGridClipboardFailuresAreAtomic()
require(
fixture.editor.setHorizontalWireRange(
fixture.logicId, source, 0, 1, true).succeeded
&& fixture.editor.setConditionAtColumn(
&& setConditionAtColumn(fixture.editor,
fixture.logicId, target, 4, contact(73), true).succeeded
&& fixture.editor.setHorizontalWireRange(
fixture.logicId, target, 0, 0, true).succeeded,
@@ -1168,7 +1143,7 @@ void testWholeRowClipboardInsertionAndLimit()
const std::string first = fixture.addRung();
const std::string second = fixture.addRung();
const std::string target = fixture.addRung();
const LogicEditorResult source_node = fixture.editor.setConditionAtColumn(
const LogicEditorResult source_node = setConditionAtColumn(fixture.editor,
fixture.logicId, first, 0, contact(90), true);
require(
source_node.succeeded
@@ -1439,7 +1414,6 @@ int main()
testContinuousGridAndIndependentHorizontalWires();
testIndependentVerticalConnectionsAndNetworkSplit();
testInsertRowSplitsVerticalEdges();
testInsertConditionMovesOnlyRelatedVerticalBoundaries();
testDeleteRowMergesOnlyContinuousEdges();
testParallelBranchCreatesConnectedVisualRow();
testParallelBranchReusesExistingEdges();


+ 0
- 1
app/tests/offline_simulation_service_tests.cpp Целия файл

@@ -747,7 +747,6 @@ void testSimulationLifecycleSnapshotAndFault()
require(simulation.start({program}).succeeded, "simulation must start");
require(simulation.state() == SimulationState::Running, "simulation must report running");
require(!readBit(repository, 0), "starting a session must clear virtual registers");
require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
writeBit(repository, 0, true);
program.rungs.front().output = coil("changed-output", 9);
require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");


+ 15
- 11
app/tests/plc_runtime_tests.cpp Целия файл

@@ -64,6 +64,15 @@ public:
return {true, {}};
}

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

PlcConnectionState state() const override { return connection_state; }
bool initialReadCompleted() const override { return initial_read; }
PlcCommunicationError lastErrorType() const override { return last_error_type; }
@@ -120,6 +129,7 @@ public:
std::string last_error;
PlcSerialConfiguration last_configuration;
std::vector<RegisterAddress> poll_addresses;
std::vector<RegisterWordRange> poll_ranges;
std::function<void()> state_changed;
std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated;
@@ -211,14 +221,14 @@ void testPlcCacheAndWriteForwarding()
&& multi_word_write_count == 2,
"Double words must be forwarded in one four-register request");
repository.invalidate();
require(!repository.hasAnyValidValue(),
"disconnecting must invalidate all PLC cache validity flags");
require(!repository.readWord(d0).succeeded,
"disconnecting must invalidate cached PLC values");
}

void testRuntimeRepositorySwitchingAndDisconnect()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiPage page;
page.id = "main-page";
page.name = "Main";
@@ -287,7 +297,7 @@ void testRuntimeRepositorySwitchingAndDisconnect()
void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
PlcRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
@@ -502,13 +512,7 @@ void testPlcPollQuantityBoundaries()
}
require(service.setPollAddresses(
double_boundary_addresses,
{{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded
&& service.pollBlocks().size() == 2U
&& service.pollBlocks()[0].area == RegisterArea::D
&& service.pollBlocks()[0].startAddress == 0
&& service.pollBlocks()[0].count == 118
&& service.pollBlocks()[1].startAddress == 118
&& service.pollBlocks()[1].count == 6,
{{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded,
"a Double at the 120-word boundary must stay in one Modbus read block");

std::vector<RegisterWordRange> unsplittable_ranges;


+ 12
- 12
app/tests/project_management_tests.cpp Целия файл

@@ -163,8 +163,8 @@ void testEmptyProjectRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
ProjectService service(storage);
JsonProjectStorage storage(defaultProjectLimitSettings());
ProjectService service(storage, defaultProjectLimitSettings());
require(service.createNewProject("Empty project").succeeded,
"empty project creation must succeed");
require(service.project().metadata.formatVersion == "2.0",
@@ -184,10 +184,10 @@ void testGridProjectRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("grid.json");
const Project original = makeExampleProject();
require(original.validate(), "the grid fixture must be structurally valid");
require(original.validate(defaultProjectLimitSettings()), "the grid fixture must be structurally valid");
require(storage.save(original, path.toStdString()).succeeded,
"the grid project must save");

@@ -225,7 +225,7 @@ void testMOffAlarmRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("m-off-alarm.json");
Project project = makeExampleProject();
project.alarmDefinitions.push_back({
@@ -254,7 +254,7 @@ void testMultiWordHmiDataTypeRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("multi-word-hmi.json");
Project project = makeExampleProject();

@@ -317,7 +317,7 @@ void testStrictVersionAndRequiredFields()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString fixture_path = directory.filePath("fixture.json");
QJsonObject root = savedFixture(
storage, makeExampleProject(), fixture_path);
@@ -346,7 +346,7 @@ void testInvalidGridAndConnectionsAreRejected()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString fixture_path = directory.filePath("fixture.json");
const QJsonObject original = savedFixture(
storage, makeExampleProject(), fixture_path);
@@ -417,7 +417,7 @@ void testProjectServiceStateAndConfiguredLimits()
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
const QString path = directory.filePath("project.json");
JsonProjectStorage storage;
JsonProjectStorage storage(defaultProjectLimitSettings());
require(storage.save(makeExampleProject(), path.toStdString()).succeeded,
"service fixture must save");

@@ -435,7 +435,7 @@ void testProjectServiceStateAndConfiguredLimits()
&& !limited_service.hasCurrentFile(),
"a failed load must leave the current project untouched");

ProjectService service(storage);
ProjectService service(storage, defaultProjectLimitSettings());
require(service.save().error == ProjectServiceError::FilePathRequired,
"save without a current path must fail explicitly");
require(service.createNewProject("State project").succeeded,
@@ -450,8 +450,8 @@ void testProjectServiceStateAndConfiguredLimits()

void testRegisterCommentService()
{
JsonProjectStorage storage;
ProjectService projects(storage);
JsonProjectStorage storage(defaultProjectLimitSettings());
ProjectService projects(storage, defaultProjectLimitSettings());
RegisterCommentService comments(projects);
require(comments.setComment(
RegisterAddress{RegisterArea::D, 3}, " Target value ")


+ 18
- 10
app/tests/register_monitor_service_tests.cpp Целия файл

@@ -88,20 +88,24 @@ void testRegisterWrites()
require(bit_write.succeeded && repository.readBit(m0).value,
"monitor service must write M values through the active repository");

const RegisterMonitorWriteResult word_write = service.writeWord(d0, -123);
const RegisterMonitorWriteResult word_write = service.writeNumeric(
d0, RegisterDataType::Int16, -123);
require(word_write.succeeded && repository.readWord(d0).value == -123,
"monitor service must write signed D values through the active repository");

const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true);
require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch,
"monitor bit writes must reject D addresses");
const RegisterMonitorWriteResult wrong_word = service.writeWord(m0, 123);
const RegisterMonitorWriteResult wrong_word = service.writeNumeric(
m0, RegisterDataType::Int16, 123);
require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch,
"monitor word writes must reject M addresses");

require(service.writeWord(d0, static_cast<std::int16_t>(-32768)).succeeded
require(service.writeNumeric(
d0, RegisterDataType::Int16, static_cast<std::int16_t>(-32768)).succeeded
&& repository.readWord(d0).value == -32768
&& service.writeWord(d0, static_cast<std::int16_t>(32767)).succeeded
&& service.writeNumeric(
d0, RegisterDataType::Int16, static_cast<std::int16_t>(32767)).succeeded
&& repository.readWord(d0).value == 32767,
"monitor word writes must preserve signed 16-bit boundary values");
}
@@ -116,8 +120,10 @@ void testOfflineInitialValueCapture()
service.setOfflineInitialCaptureEnabled(true);

require(service.writeBit({RegisterArea::M, 3}, true).succeeded
&& service.writeWord({RegisterArea::D, 10}, -456).succeeded
&& service.writeFloat({RegisterArea::D, 20}, 1.5f).succeeded
&& service.writeNumeric(
{RegisterArea::D, 10}, RegisterDataType::Int16, -456).succeeded
&& service.writeNumeric(
{RegisterArea::D, 20}, RegisterDataType::Float32, 1.5f).succeeded
&& service.writeNumeric(
{RegisterArea::D, 30}, RegisterDataType::Int32, 305419896)
.succeeded
@@ -132,8 +138,8 @@ void testOfflineInitialValueCapture()
require(initial_repository.readBit({RegisterArea::M, 3}).value
&& initial_repository.readWord({RegisterArea::D, 10}).value == -456
&& std::fabs(Float32Codec::decode(
initial_repository.readWordPair({RegisterArea::D, 20}).values[0],
initial_repository.readWordPair({RegisterArea::D, 20}).values[1])
initial_repository.readWords({RegisterArea::D, 20}, 2).values[0],
initial_repository.readWords({RegisterArea::D, 20}, 2).values[1])
.value_or(0.0f) - 1.5f) < 0.000001f,
"editing monitor writes must be captured as offline initial values");
require(captured_int32.succeeded
@@ -182,7 +188,8 @@ void testMultiWordMonitoring()
&& service.points().size() == 3U,
"overlapping monitor removal must identify the point type");

require(service.writeFloat({RegisterArea::D, 1}, -2.5f).succeeded,
require(service.writeNumeric(
{RegisterArea::D, 1}, RegisterDataType::Float32, -2.5f).succeeded,
"Float32 monitor writes must use the pair write path");
std::vector<MonitorValue> values = service.values(false);
require(values.front().state == MonitorValueState::Valid
@@ -190,7 +197,8 @@ void testMultiWordMonitoring()
std::get<float>(values.front().numericValue) - (-2.5f))
< 0.000001f,
"Float32 monitor values must decode the pair correctly");
require(!service.writeFloat({RegisterArea::D, 4000}, 1.0f).succeeded,
require(!service.writeNumeric(
{RegisterArea::D, 4000}, RegisterDataType::Float32, 1.0f).succeeded,
"Float32 monitor writes at D4000 must be rejected");

const RegisterMonitorResult int32_added = service.addRange(


+ 3
- 3
app/tests/runtime_mode_service_tests.cpp Целия файл

@@ -138,7 +138,7 @@ void testModeTransitions()
{
// 验证服务将 PLC 首读状态与领域模式切换规则正确组合
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
@@ -325,7 +325,7 @@ void testModeTransitions()
void testMonitorPollRangeRollback()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
@@ -359,7 +359,7 @@ void testMonitorPollRangeRollback()
void testDisconnectedOutputBlocksOfflineAndOnlineRuntime()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);


+ 44
- 19
app/tests/runtime_panel_controller_tests.cpp Целия файл

@@ -42,6 +42,18 @@

namespace {

LogicEditorResult setConditionAtColumn(
LogicEditorService &editor,
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config,
bool configured)
{
return editor.applyConditionAndAdvance(
logic_id, {rung_id, column, false}, config, configured).edit;
}

using TestProjectStorage = TestSupport::InMemoryProjectStorage;
using TestSupport::require;

@@ -98,7 +110,7 @@ void testMonitorOffersAllNumericTypes()
void testAlarmConfigurationOffersMOnAndMOff()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
AlarmEditorService alarm_editor_service(project_service);
AlarmConfigurationDialog dialog(alarm_editor_service);

@@ -123,9 +135,9 @@ void testAlarmConfigurationOffersMOnAndMOff()
void testAlarmListKeepsFixedGeometryWhileRecordsChange()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
HmiEditorService hmi_editor_service(project_service);
HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
HmiRuntimeService hmi_runtime_service(virtual_repository);
AlarmService alarm_service(project_service, virtual_repository);

@@ -195,7 +207,7 @@ void testAlarmListKeepsFixedGeometryWhileRecordsChange()
void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
VirtualRegisterRepository virtual_repository;
OfflineSimulationService simulation_service(virtual_repository);
OnlineLogicMonitorService online_monitor_service(virtual_repository);
@@ -205,7 +217,7 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
logic_editor_service,
simulation_service,
online_monitor_service);
HmiEditorService hmi_editor_service(project_service);
HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
HmiRuntimeService hmi_runtime_service(virtual_repository);
HmiNavigationService hmi_navigation_service(project_service);
AlarmService alarm_service(project_service, virtual_repository);
@@ -301,18 +313,31 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
controller.leaveRuntime(
runtime_mode_service.mode(), runtime_mode_service.plcConnectionState());
logic_editor_widget.clearRuntimeTrace();
require(!logic_editor_widget.runtimeTraceEnabled(),
const auto runtimeOutputIsActive = [runtime_logic_view]
{
for (QGraphicsItem *item : runtime_logic_view->scene()->items())
{
if (item->data(0).toString() == QStringLiteral("output")
&& item->data(1).toString()
== QStringLiteral("always-on-rung"))
{
return item->data(3).toBool() && item->data(4).toBool();
}
}
return false;
};
require(!runtimeOutputIsActive(),
"editing transition must initially clear the runtime trace");

QCoreApplication::processEvents(QEventLoop::AllEvents);
require(!logic_editor_widget.runtimeTraceEnabled(),
require(!runtimeOutputIsActive(),
"a queued offline scan must not restore the trace after returning to editing");
}

void testLogicEditorGridSelectionAndDeletion()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService logic_editor_service(project_service);
const std::string logic_id = logic_editor_service.ensureDefaultLogic().id;

@@ -441,7 +466,7 @@ void testLogicEditorGridSelectionAndDeletion()
== LadderCellKind::Wire,
"the horizontal-wire command must act on an explicitly selected cell");

const LogicEditorResult first_node = logic_editor_service.setConditionAtColumn(
const LogicEditorResult first_node = setConditionAtColumn(logic_editor_service,
logic_id,
rung_id,
0,
@@ -449,7 +474,7 @@ void testLogicEditorGridSelectionAndDeletion()
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen},
true);
const LogicEditorResult second_node = logic_editor_service.setConditionAtColumn(
const LogicEditorResult second_node = setConditionAtColumn(logic_editor_service,
logic_id,
rung_id,
1,
@@ -509,13 +534,13 @@ void testLogicEditorGridSelectionAndDeletion()
void testLadderLayoutAndDragDeletion()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string upper = editor.addRung(logic_id).id;
const std::string lower = editor.addRung(logic_id).id;
require(
editor.setConditionAtColumn(
setConditionAtColumn(editor,
logic_id,
upper,
0,
@@ -688,7 +713,7 @@ void testLadderLayoutAndDragDeletion()
void testWireGesturePreviewKeepsAtomicCommit()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string first = editor.addRung(logic_id).id;
@@ -842,7 +867,7 @@ void testWireGesturePreviewKeepsAtomicCommit()
void testEscapeExitsMouseWireMode()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
LogicEditorWidget widget(editor);

@@ -872,7 +897,7 @@ void testEscapeExitsMouseWireMode()
void testCursorAdvanceAndInlineCommandInput()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
LogicEditorWidget widget(editor);
@@ -970,7 +995,7 @@ void testCursorAdvanceAndInlineCommandInput()
void testVerticalWireShortcutAdvancesDownward()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
std::vector<std::string> rung_ids;
@@ -1059,12 +1084,12 @@ void testVerticalWireShortcutAdvancesDownward()
void testSegmentLevelTraceProjection()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string upper = editor.addRung(logic_id).id;
const std::string lower = editor.addRung(logic_id).id;
const LogicEditorResult condition = editor.setConditionAtColumn(
const LogicEditorResult condition = setConditionAtColumn(editor,
logic_id,
upper,
0,
@@ -1157,7 +1182,7 @@ void testSegmentLevelTraceProjection()
void testLogicClipboardUsesExplicitObjectAndRowSelection()
{
TestProjectStorage storage;
ProjectService project_service(storage);
ProjectService project_service(storage, defaultProjectLimitSettings());
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string source = editor.addRung(logic_id).id;


Зареждане…
Отказ
Запис