ソースを参照

feat: 完善结构化梯形图编辑与轨迹

main
suyu 1ヶ月前
コミット
7ed3d8b388
13個のファイルの変更1273行の追加687行の削除
  1. +208
    -41
      app/src/domain/control_logic_model.cpp
  2. +30
    -5
      app/src/domain/control_logic_model.h
  3. +264
    -224
      app/src/services/logic_editor_service.cpp
  4. +23
    -5
      app/src/services/logic_editor_service.h
  5. +8
    -1
      app/src/services/offline_simulation_service.cpp
  6. +3
    -0
      app/src/services/offline_simulation_service.h
  7. +68
    -21
      app/src/services/software_logic_executor.cpp
  8. +17
    -1
      app/src/services/software_logic_executor.h
  9. +450
    -288
      app/src/ui/logic_editor_widget.cpp
  10. +15
    -4
      app/src/ui/logic_editor_widget.h
  11. +30
    -12
      app/tests/domain_tests.cpp
  12. +82
    -84
      app/tests/logic_editor_service_tests.cpp
  13. +75
    -1
      app/tests/offline_simulation_service_tests.cpp

+ 208
- 41
app/src/domain/control_logic_model.cpp ファイルの表示

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

#include <algorithm>
#include <utility>

namespace {

@@ -84,6 +85,53 @@ bool hasDuplicateId(const std::vector<TItem> &items)
return false;
}

bool validateUniqueExpressionIds(
const ConditionExpression &expression, std::string *error)
{
std::vector<std::string> ids;
collectConditionExpressionIds(expression, &ids);
std::sort(ids.begin(), ids.end());
if (std::adjacent_find(ids.cbegin(), ids.cend()) != ids.cend())
{
setError(error, "condition expression ids must be unique within a rung");
return false;
}
return true;
}

void normalizeExpression(ConditionExpression *expression)
{
if (expression == nullptr || expression->kind == ConditionExpressionKind::Node)
{
return;
}
for (ConditionExpression &child : expression->children)
{
normalizeExpression(&child);
}

std::vector<ConditionExpression> flattened;
for (ConditionExpression &child : expression->children)
{
if (child.kind == expression->kind)
{
for (ConditionExpression &grandchild : child.children)
{
flattened.push_back(std::move(grandchild));
}
}
else
{
flattened.push_back(std::move(child));
}
}
expression->children = std::move(flattened);
if (expression->children.size() == 1U)
{
*expression = std::move(expression->children.front());
}
}

} // namespace

bool LogicNode::validate(std::string *error) const
@@ -94,10 +142,7 @@ bool LogicNode::validate(std::string *error) const
return false;
}
return std::visit(
[error](const auto &config)
{
return validateConfig(config, error);
},
[error](const auto &config) { return validateConfig(config, error); },
config);
}

@@ -116,50 +161,171 @@ bool LogicNode::isOutput() const
return std::holds_alternative<CoilNodeConfig>(config);
}

bool LadderStage::validate(std::string *error) const
ConditionExpression ConditionExpression::fromNode(LogicNode logic_node)
{
if (id.empty() || branches.empty())
ConditionExpression expression;
expression.id = logic_node.id;
expression.kind = ConditionExpressionKind::Node;
expression.node = std::move(logic_node);
return expression;
}

bool ConditionExpression::validate(std::string *error) const
{
if (id.empty())
{
setError(error, "ladder stage id and branches must not be empty");
setError(error, "condition expression id must not be empty");
return false;
}
if (hasDuplicateId(branches))
if (kind == ConditionExpressionKind::Node)
{
setError(error, "parallel branch node ids must be unique");
if (!node.has_value() || !children.empty() || !node->isCondition())
{
setError(error, "condition leaf requires one condition node and no children");
return false;
}
return node->validate(error);
}
if (node.has_value() || children.size() < 2U)
{
setError(error, "series and parallel expressions require at least two children");
return false;
}
for (const LogicNode &node : branches)
for (const ConditionExpression &child : children)
{
if (!node.validate(error))
if (child.kind == kind)
{
setError(error, "nested expressions of the same kind must be normalized");
return false;
}
if (!node.isCondition())
if (!child.validate(error))
{
setError(error, "ladder stage may contain condition nodes only");
return false;
}
}
return true;
}

bool LadderStage::validateForRunning(std::string *error) const
bool ConditionExpression::validateForRunning(std::string *error) const
{
if (!validate(error))
{
return false;
}
for (const LogicNode &node : branches)
if (kind == ConditionExpressionKind::Node)
{
if (!node.isConfigured())
if (!node->isConfigured())
{
setError(error, "ladder condition " + node->id + " is not configured");
return false;
}
return true;
}
for (const ConditionExpression &child : children)
{
if (!child.validateForRunning(error))
{
setError(error, "ladder condition " + node.id + " is not configured");
return false;
}
}
return true;
}

void normalizeConditionExpression(std::optional<ConditionExpression> *expression)
{
if (expression == nullptr || !expression->has_value())
{
return;
}
normalizeExpression(&expression->value());
if (expression->value().kind != ConditionExpressionKind::Node
&& expression->value().children.empty())
{
expression->reset();
}
}

const LogicNode *findConditionNode(
const ConditionExpression &expression, const std::string &node_id)
{
if (expression.kind == ConditionExpressionKind::Node)
{
return expression.node->id == node_id ? &*expression.node : nullptr;
}
for (const ConditionExpression &child : expression.children)
{
if (const LogicNode *node = findConditionNode(child, node_id))
{
return node;
}
}
return nullptr;
}

LogicNode *findConditionNode(
ConditionExpression &expression, const std::string &node_id)
{
return const_cast<LogicNode *>(findConditionNode(
static_cast<const ConditionExpression &>(expression), node_id));
}

const ConditionExpression *findConditionExpression(
const ConditionExpression &expression, const std::string &expression_id)
{
if (expression.id == expression_id)
{
return &expression;
}
for (const ConditionExpression &child : expression.children)
{
if (const ConditionExpression *found = findConditionExpression(
child, expression_id))
{
return found;
}
}
return nullptr;
}

ConditionExpression *findConditionExpression(
ConditionExpression &expression, const std::string &expression_id)
{
return const_cast<ConditionExpression *>(findConditionExpression(
static_cast<const ConditionExpression &>(expression), expression_id));
}

void collectConditionNodes(
const ConditionExpression &expression, std::vector<const LogicNode *> *nodes)
{
if (nodes == nullptr)
{
return;
}
if (expression.kind == ConditionExpressionKind::Node)
{
nodes->push_back(&*expression.node);
return;
}
for (const ConditionExpression &child : expression.children)
{
collectConditionNodes(child, nodes);
}
}

void collectConditionExpressionIds(
const ConditionExpression &expression, std::vector<std::string> *ids)
{
if (ids == nullptr)
{
return;
}
ids->push_back(expression.id);
for (const ConditionExpression &child : expression.children)
{
collectConditionExpressionIds(child, ids);
}
}

bool LadderRung::validate(std::string *error) const
{
return validateStructure(error);
@@ -171,21 +337,18 @@ bool LadderRung::validateForRunning(std::string *error) const
{
return false;
}
if (stages.empty() && !output.has_value())
if (!condition.has_value() && !output.has_value())
{
return true;
}
if (stages.empty() || !output.has_value())
if (!condition.has_value() || !output.has_value())
{
setError(error, "incomplete ladder network requires conditions and an output coil");
return false;
}
for (const LadderStage &stage : stages)
if (!condition->validateForRunning(error))
{
if (!stage.validateForRunning(error))
{
return false;
}
return false;
}
if (!output->isConfigured())
{
@@ -202,30 +365,23 @@ bool LadderRung::validateStructure(std::string *error) const
setError(error, "ladder rung id and name must not be empty");
return false;
}
if (hasDuplicateId(stages))
{
setError(error, "ladder stage ids must be unique within a rung");
return false;
}
std::vector<std::string> node_ids;
for (const LadderStage &stage : stages)
if (condition.has_value())
{
if (!stage.validate(error))
if (!condition->validate(error) || !validateUniqueExpressionIds(*condition, error))
{
return false;
}
for (const LogicNode &node : stage.branches)
std::vector<const LogicNode *> nodes;
collectConditionNodes(*condition, &nodes);
for (const LogicNode *node : nodes)
{
node_ids.push_back(node.id);
node_ids.push_back(node->id);
}
}
if (output.has_value())
{
if (!output->validate(error))
{
return false;
}
if (!output->isOutput())
if (!output->validate(error) || !output->isOutput())
{
setError(error, "ladder rung output must be a coil node");
return false;
@@ -270,18 +426,22 @@ bool ControlLogic::validateStructure(std::string *error) const
return false;
}
std::vector<std::string> node_ids;
std::vector<std::string> expression_ids;
for (const LadderRung &rung : rungs)
{
if (!rung.validateStructure(error))
{
return false;
}
for (const LadderStage &stage : rung.stages)
if (rung.condition.has_value())
{
for (const LogicNode &node : stage.branches)
std::vector<const LogicNode *> nodes;
collectConditionNodes(*rung.condition, &nodes);
for (const LogicNode *node : nodes)
{
node_ids.push_back(node.id);
node_ids.push_back(node->id);
}
collectConditionExpressionIds(*rung.condition, &expression_ids);
}
if (rung.output.has_value())
{
@@ -289,11 +449,18 @@ bool ControlLogic::validateStructure(std::string *error) const
}
}
std::sort(node_ids.begin(), node_ids.end());
std::sort(expression_ids.begin(), expression_ids.end());
if (std::adjacent_find(node_ids.cbegin(), node_ids.cend()) != node_ids.cend())
{
setError(error, "logic node ids must be unique within a logic");
return false;
}
if (std::adjacent_find(expression_ids.cbegin(), expression_ids.cend())
!= expression_ids.cend())
{
setError(error, "condition expression ids must be unique within a logic");
return false;
}
return true;
}



+ 30
- 5
app/src/domain/control_logic_model.h ファイルの表示

@@ -64,22 +64,47 @@ struct LogicNode
bool isOutput() const;
};

// 同一个串联级内的条件互为并联关系,任一条件成立即通过该级
struct LadderStage
enum class ConditionExpressionKind
{
Node,
Series,
Parallel
};

// 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接
struct ConditionExpression
{
std::string id;
std::vector<LogicNode> branches;
ConditionExpressionKind kind = ConditionExpressionKind::Node;
std::optional<LogicNode> node;
std::vector<ConditionExpression> children;

static ConditionExpression fromNode(LogicNode node);
bool validate(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

// 梯级中的各级按顺序串联,输出线圈固定在最右侧
// 删除后折叠单子项容器并合并相邻同类容器,保持表达式的规范形态
void normalizeConditionExpression(std::optional<ConditionExpression> *expression);
const LogicNode *findConditionNode(
const ConditionExpression &expression, const std::string &node_id);
LogicNode *findConditionNode(
ConditionExpression &expression, const std::string &node_id);
const ConditionExpression *findConditionExpression(
const ConditionExpression &expression, const std::string &expression_id);
ConditionExpression *findConditionExpression(
ConditionExpression &expression, const std::string &expression_id);
void collectConditionNodes(
const ConditionExpression &expression, std::vector<const LogicNode *> *nodes);
void collectConditionExpressionIds(
const ConditionExpression &expression, std::vector<std::string> *ids);

// 一个网络包含一棵结构化条件表达式,输出线圈固定在最右侧
struct LadderRung
{
std::string id;
std::string name;
std::vector<LadderStage> stages;
std::optional<ConditionExpression> condition;
std::optional<LogicNode> output;

bool validate(std::string *error = nullptr) const;


+ 264
- 224
app/src/services/logic_editor_service.cpp ファイルの表示

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

#include <algorithm>
#include <sstream>
#include <type_traits>
#include <utility>

@@ -11,11 +10,85 @@ namespace {

LogicNode makeNode(const std::string &id, const LogicNodeConfig &config)
{
LogicNode node;
node.id = id;
node.config = config;
node.configured = false;
return node;
return {id, config, false};
}

ConditionExpression makeContainer(
const std::string &id,
ConditionExpressionKind kind,
ConditionExpression first,
ConditionExpression second)
{
ConditionExpression expression;
expression.id = id;
expression.kind = kind;
expression.children.push_back(std::move(first));
expression.children.push_back(std::move(second));
return expression;
}

ConditionExpression *findParentExpression(
ConditionExpression &expression, const std::string &child_id)
{
for (ConditionExpression &child : expression.children)
{
if (child.id == child_id)
{
return &expression;
}
if (ConditionExpression *parent = findParentExpression(child, child_id))
{
return parent;
}
}
return nullptr;
}

bool removeExpressionRecursive(
ConditionExpression *expression, const std::string &expression_id)
{
if (expression == nullptr || expression->kind == ConditionExpressionKind::Node)
{
return false;
}
const auto removable = std::find_if(
expression->children.begin(),
expression->children.end(),
[&expression_id](const ConditionExpression &child)
{
return child.id == expression_id;
});
if (removable != expression->children.end())
{
expression->children.erase(removable);
return true;
}
for (ConditionExpression &child : expression->children)
{
if (removeExpressionRecursive(&child, expression_id))
{
return true;
}
}
return false;
}

LadderRung *findEditableRung(
Project &project,
const std::string &logic_id,
const std::string &rung_id)
{
const auto logic = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
if (logic == project.controlLogics.end())
{
return nullptr;
}
const auto rung = std::find_if(
logic->rungs.begin(), logic->rungs.end(),
[&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; });
return rung == logic->rungs.end() ? nullptr : &*rung;
}

} // namespace
@@ -29,12 +102,8 @@ const ControlLogic *LogicEditorService::findLogic(const std::string &logic_id) c
{
const auto &logics = project_service_.project().controlLogics;
const auto logic = std::find_if(
logics.cbegin(),
logics.cend(),
[&logic_id](const ControlLogic &candidate)
{
return candidate.id == logic_id;
});
logics.cbegin(), logics.cend(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
return logic == logics.cend() ? nullptr : &*logic;
}

@@ -47,12 +116,8 @@ const LadderRung *LogicEditorService::findRung(
return nullptr;
}
const auto rung = std::find_if(
logic->rungs.cbegin(),
logic->rungs.cend(),
[&rung_id](const LadderRung &candidate)
{
return candidate.id == rung_id;
});
logic->rungs.cbegin(), logic->rungs.cend(),
[&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; });
return rung == logic->rungs.cend() ? nullptr : &*rung;
}

@@ -66,28 +131,31 @@ const LogicNode *LogicEditorService::findNode(
}
for (const LadderRung &rung : logic->rungs)
{
for (const LadderStage &stage : rung.stages)
{
const auto node = std::find_if(
stage.branches.cbegin(),
stage.branches.cend(),
[&node_id](const LogicNode &candidate)
{
return candidate.id == node_id;
});
if (node != stage.branches.cend())
{
return &*node;
}
}
if (rung.output.has_value() && rung.output->id == node_id)
{
return &*rung.output;
}
if (rung.condition.has_value())
{
if (const LogicNode *node = findConditionNode(*rung.condition, node_id))
{
return node;
}
}
}
return nullptr;
}

const ConditionExpression *LogicEditorService::findExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id) const
{
const LadderRung *rung = findRung(logic_id, rung_id);
return rung == nullptr || !rung->condition.has_value()
? nullptr : findConditionExpression(*rung->condition, expression_id);
}

std::string LogicEditorService::firstLogicId() const
{
const auto &logics = project_service_.project().controlLogics;
@@ -97,8 +165,7 @@ std::string LogicEditorService::firstLogicId() const
std::string LogicEditorService::firstRungId(const std::string &logic_id) const
{
const ControlLogic *logic = findLogic(logic_id);
return logic == nullptr || logic->rungs.empty()
? std::string{} : logic->rungs.front().id;
return logic == nullptr || logic->rungs.empty() ? std::string{} : logic->rungs.front().id;
}

std::string LogicEditorService::rungIdForNode(
@@ -111,50 +178,12 @@ std::string LogicEditorService::rungIdForNode(
}
for (const LadderRung &rung : logic->rungs)
{
if (rung.output.has_value() && rung.output->id == node_id)
if ((rung.output.has_value() && rung.output->id == node_id)
|| (rung.condition.has_value()
&& findConditionNode(*rung.condition, node_id) != nullptr))
{
return rung.id;
}
for (const LadderStage &stage : rung.stages)
{
if (std::any_of(
stage.branches.cbegin(),
stage.branches.cend(),
[&node_id](const LogicNode &node)
{
return node.id == node_id;
}))
{
return rung.id;
}
}
}
return {};
}

std::string LogicEditorService::stageIdForNode(
const std::string &logic_id, const std::string &node_id) const
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return {};
}
for (const LadderRung &rung : logic->rungs)
{
for (const LadderStage &stage : rung.stages)
{
if (std::any_of(
stage.branches.cbegin(),
stage.branches.cend(),
[&node_id](const LogicNode &node)
{
return node.id == node_id;
}))
{
return stage.id;
}
}
}
return {};
}
@@ -168,7 +197,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic()
ControlLogic logic;
logic.id = "logic-1";
logic.name = "控制逻辑 1";
logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt});
logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, std::nullopt});
Project &project = project_service_.editProject();
project.controlLogics.push_back(std::move(logic));
return {true, LogicEditorError::None, {}, project.controlLogics.back().id};
@@ -186,12 +215,8 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id)
rung.name = "网络 " + std::to_string(logic->rungs.size() + 1U);
Project &project = project_service_.editProject();
auto target = std::find_if(
project.controlLogics.begin(),
project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate)
{
return candidate.id == logic_id;
});
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
target->rungs.push_back(std::move(rung));
return {true, LogicEditorError::None, {}, target->rungs.back().id};
}
@@ -230,124 +255,146 @@ LogicEditorResult LogicEditorService::appendCondition(
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *rung = findRung(logic_id, rung_id);
if (logic == nullptr)
if (logic == nullptr || findRung(logic_id, rung_id) == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "control logic was not found");
}
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "ladder rung was not found");
return failure(
logic == nullptr ? LogicEditorError::LogicNotFound : LogicEditorError::RungNotFound,
logic == nullptr ? "control logic was not found" : "ladder rung was not found");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "ladder condition cannot be a coil");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
LogicNode node = makeNode(node_id, config);
std::string error;
if (!node.validate(&error))
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
Project &project = project_service_.editProject();
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
if (!rung->condition.has_value())
{
return failure(LogicEditorError::InvalidNode, error);
rung->condition = std::move(leaf);
}
else if (rung->condition->kind == ConditionExpressionKind::Series)
{
rung->condition->children.push_back(std::move(leaf));
}
else
{
rung->condition = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(*rung->condition),
std::move(leaf));
}
LadderStage stage;
stage.id = makeUniqueStageId(*rung);
stage.branches.push_back(std::move(node));
Project &project = project_service_.editProject();
auto &logics = project.controlLogics;
auto target_logic = std::find_if(
logics.begin(), logics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
auto target_rung = std::find_if(
target_logic->rungs.begin(), target_logic->rungs.end(),
[&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; });
target_rung->stages.push_back(std::move(stage));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelCondition(
LogicEditorResult LogicEditorService::insertCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &stage_id,
const std::string &target_expression_id,
SeriesInsertPosition position,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *rung = findRung(logic_id, rung_id);
if (logic == nullptr)
if (logic == nullptr || findRung(logic_id, rung_id) == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "control logic was not found");
return failure(LogicEditorError::RungNotFound, "ladder network was not found");
}
if (rung == nullptr)
if (!isConditionConfig(config)
|| findExpression(logic_id, rung_id, target_expression_id) == nullptr)
{
return failure(LogicEditorError::RungNotFound, "ladder rung was not found");
}
const auto stage = std::find_if(
rung->stages.cbegin(), rung->stages.cend(),
[&stage_id](const LadderStage &candidate) { return candidate.id == stage_id; });
if (stage == rung->stages.cend())
{
return failure(LogicEditorError::StageNotFound, "ladder stage was not found");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "parallel branch requires a condition");
return failure(LogicEditorError::ExpressionNotFound, "series insertion target was not found");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
LogicNode node = makeNode(node_id, config);
std::string error;
if (!node.validate(&error))
{
return failure(LogicEditorError::InvalidNode, error);
}
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
Project &project = project_service_.editProject();
auto target_logic = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
auto target_rung = std::find_if(
target_logic->rungs.begin(), target_logic->rungs.end(),
[&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; });
auto target_stage = std::find_if(
target_rung->stages.begin(), target_rung->stages.end(),
[&stage_id](const LadderStage &candidate) { return candidate.id == stage_id; });
target_stage->branches.push_back(std::move(node));
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id);
ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id);
if (parent != nullptr && parent->kind == ConditionExpressionKind::Series)
{
const auto target_iterator = std::find_if(
parent->children.begin(), parent->children.end(),
[&target_expression_id](const ConditionExpression &child)
{
return child.id == target_expression_id;
});
parent->children.insert(
position == SeriesInsertPosition::Before
? target_iterator : target_iterator + 1,
std::move(leaf));
}
else
{
ConditionExpression original = std::move(*target);
*target = position == SeriesInsertPosition::Before
? makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(leaf),
std::move(original))
: makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(original),
std::move(leaf));
}
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::setOutput(
LogicEditorResult LogicEditorService::addParallelCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
if (logic == nullptr || findRung(logic_id, rung_id) == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "control logic was not found");
return failure(LogicEditorError::RungNotFound, "ladder network was not found");
}
if (findRung(logic_id, rung_id) == nullptr)
if (!isConditionConfig(config)
|| findExpression(logic_id, rung_id, target_expression_id) == nullptr)
{
return failure(LogicEditorError::RungNotFound, "ladder rung was not found");
return failure(LogicEditorError::ExpressionNotFound, "parallel target was not found");
}
if (!isOutputConfig(config))
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
Project &project = project_service_.editProject();
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id);
ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id);
if (parent != nullptr && parent->kind == ConditionExpressionKind::Parallel)
{
return failure(LogicEditorError::InvalidNode, "ladder output must be a coil");
parent->children.push_back(std::move(leaf));
}
const LadderRung *rung = findRung(logic_id, rung_id);
const std::string node_id = rung->output.has_value()
? rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config));
LogicNode node = makeNode(node_id, config);
std::string error;
if (!node.validate(&error))
else
{
return failure(LogicEditorError::InvalidNode, error);
ConditionExpression original = std::move(*target);
*target = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Parallel,
std::move(original),
std::move(leaf));
}
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::setOutput(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config))
{
return failure(LogicEditorError::InvalidNode, "ladder output requires a valid coil");
}
const std::string node_id = existing_rung->output.has_value()
? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config));
Project &project = project_service_.editProject();
auto target_logic = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
auto target_rung = std::find_if(
target_logic->rungs.begin(), target_logic->rungs.end(),
[&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; });
target_rung->output = std::move(node);
findEditableRung(project, logic_id, rung_id)->output = makeNode(node_id, config);
return {true, LogicEditorError::None, {}, node_id};
}

@@ -367,8 +414,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
LogicEditorError::UnsupportedNodeChange,
"node category cannot be changed after creation");
}
LogicNode candidate = makeNode(node_id, config);
candidate.configured = true;
LogicNode candidate{node_id, config, true};
std::string error;
if (!candidate.validate(&error))
{
@@ -383,24 +429,19 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
}
for (LadderRung &rung : logic.rungs)
{
for (LadderStage &stage : rung.stages)
{
for (LogicNode &editable : stage.branches)
{
if (editable.id == node_id)
{
editable.config = config;
editable.configured = true;
return {true, LogicEditorError::None, {}, node_id};
}
}
}
if (rung.output.has_value() && rung.output->id == node_id)
{
rung.output->config = config;
rung.output->configured = true;
*rung.output = candidate;
return {true, LogicEditorError::None, {}, node_id};
}
if (rung.condition.has_value())
{
if (LogicNode *editable = findConditionNode(*rung.condition, node_id))
{
*editable = candidate;
return {true, LogicEditorError::None, {}, node_id};
}
}
}
}
return failure(LogicEditorError::NodeNotFound, "logic node was not found");
@@ -409,40 +450,42 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
LogicEditorResult LogicEditorService::removeNode(
const std::string &logic_id, const std::string &node_id)
{
if (findNode(logic_id, node_id) == nullptr)
const std::string rung_id = rungIdForNode(logic_id, node_id);
if (rung_id.empty())
{
return failure(LogicEditorError::NodeNotFound, "logic node was not found");
}
const LadderRung *rung = findRung(logic_id, rung_id);
if (rung->output.has_value() && rung->output->id == node_id)
{
Project &project = project_service_.editProject();
findEditableRung(project, logic_id, rung_id)->output.reset();
return {true, LogicEditorError::None, {}, node_id};
}
return removeExpression(logic_id, rung_id, node_id);
}

LogicEditorResult LogicEditorService::removeExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id)
{
if (findExpression(logic_id, rung_id, expression_id) == nullptr)
{
return failure(LogicEditorError::ExpressionNotFound, "condition branch was not found");
}
Project &project = project_service_.editProject();
for (ControlLogic &logic : project.controlLogics)
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
if (rung->condition->id == expression_id)
{
if (logic.id != logic_id)
{
continue;
}
for (LadderRung &rung : logic.rungs)
{
if (rung.output.has_value() && rung.output->id == node_id)
{
rung.output.reset();
return {true, LogicEditorError::None, {}, node_id};
}
for (LadderStage &stage : rung.stages)
{
stage.branches.erase(
std::remove_if(
stage.branches.begin(), stage.branches.end(),
[&node_id](const LogicNode &node) { return node.id == node_id; }),
stage.branches.end());
}
rung.stages.erase(
std::remove_if(
rung.stages.begin(), rung.stages.end(),
[](const LadderStage &stage) { return stage.branches.empty(); }),
rung.stages.end());
}
rung->condition.reset();
}
return {true, LogicEditorError::None, {}, node_id};
else
{
removeExpressionRecursive(&*rung->condition, expression_id);
normalizeConditionExpression(&rung->condition);
}
return {true, LogicEditorError::None, {}, expression_id};
}

bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config)
@@ -469,10 +512,7 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config)
{
return "coil";
}
else
{
return "compare";
}
return "compare";
},
config);
}
@@ -486,13 +526,9 @@ std::string LogicEditorService::makeUniqueNodeId(
bool found = false;
for (const LadderRung &rung : logic.rungs)
{
found = (rung.output.has_value() && rung.output->id == candidate);
for (const LadderStage &stage : rung.stages)
{
found = found || std::any_of(
stage.branches.cbegin(), stage.branches.cend(),
[&candidate](const LogicNode &node) { return node.id == candidate; });
}
found = found || (rung.output.has_value() && rung.output->id == candidate)
|| (rung.condition.has_value()
&& findConditionNode(*rung.condition, candidate) != nullptr);
}
if (!found)
{
@@ -501,28 +537,32 @@ std::string LogicEditorService::makeUniqueNodeId(
}
}

std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic)
std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic)
{
for (std::size_t index = 1;; ++index)
{
const std::string candidate = "rung-" + std::to_string(index);
if (std::none_of(
logic.rungs.cbegin(), logic.rungs.cend(),
[&candidate](const LadderRung &rung) { return rung.id == candidate; }))
const std::string candidate = "expression-" + std::to_string(index);
bool found = false;
for (const LadderRung &rung : logic.rungs)
{
found = found || (rung.condition.has_value()
&& findConditionExpression(*rung.condition, candidate) != nullptr);
}
if (!found)
{
return candidate;
}
}
}

std::string LogicEditorService::makeUniqueStageId(const LadderRung &rung)
std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic)
{
for (std::size_t index = 1;; ++index)
{
const std::string candidate = "stage-" + std::to_string(index);
const std::string candidate = "rung-" + std::to_string(index);
if (std::none_of(
rung.stages.cbegin(), rung.stages.cend(),
[&candidate](const LadderStage &stage) { return stage.id == candidate; }))
logic.rungs.cbegin(), logic.rungs.cend(),
[&candidate](const LadderRung &rung) { return rung.id == candidate; }))
{
return candidate;
}


+ 23
- 5
app/src/services/logic_editor_service.h ファイルの表示

@@ -11,13 +11,19 @@ enum class LogicEditorError
None,
LogicNotFound,
RungNotFound,
StageNotFound,
ExpressionNotFound,
NodeNotFound,
InvalidNode,
InvalidOperation,
UnsupportedNodeChange
};

enum class SeriesInsertPosition
{
Before,
After
};

struct LogicEditorResult
{
bool succeeded = false;
@@ -36,12 +42,14 @@ public:
const std::string &logic_id, const std::string &rung_id) const;
const LogicNode *findNode(
const std::string &logic_id, const std::string &node_id) const;
const ConditionExpression *findExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id) const;
std::string firstLogicId() const;
std::string firstRungId(const std::string &logic_id) const;
std::string rungIdForNode(
const std::string &logic_id, const std::string &node_id) const;
std::string stageIdForNode(
const std::string &logic_id, const std::string &node_id) const;

LogicEditorResult ensureDefaultLogic();
LogicEditorResult addRung(const std::string &logic_id);
@@ -51,10 +59,16 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config);
LogicEditorResult insertCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
SeriesInsertPosition position,
const LogicNodeConfig &config);
LogicEditorResult addParallelCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &stage_id,
const std::string &target_expression_id,
const LogicNodeConfig &config);
LogicEditorResult setOutput(
const std::string &logic_id,
@@ -66,6 +80,10 @@ public:
const LogicNodeConfig &config);
LogicEditorResult removeNode(
const std::string &logic_id, const std::string &node_id);
LogicEditorResult removeExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id);

private:
static bool isConditionConfig(const LogicNodeConfig &config);
@@ -73,8 +91,8 @@ private:
static std::string nodePrefix(const LogicNodeConfig &config);
static std::string makeUniqueNodeId(
const ControlLogic &logic, const std::string &prefix);
static std::string makeUniqueExpressionId(const ControlLogic &logic);
static std::string makeUniqueRungId(const ControlLogic &logic);
static std::string makeUniqueStageId(const LadderRung &rung);
static LogicEditorResult failure(
LogicEditorError error, const std::string &message);



+ 8
- 1
app/src/services/offline_simulation_service.cpp ファイルの表示

@@ -35,6 +35,7 @@ SimulationStartResult OfflineSimulationService::start(
logic_snapshot_ = std::move(snapshot);
successful_scan_count_ = 0;
last_error_ = {true, LogicScanError::None, {}, {}, {}, {}};
trace_snapshot_.clear();
state_ = SimulationState::Running;
timer_.start();
emit stateChanged();
@@ -66,13 +67,14 @@ LogicScanResult OfflineSimulationService::executeOnce()
{}};
}
const LogicScanResult result = executor_.executeScan(
logic_snapshot_, repository_);
logic_snapshot_, repository_, &trace_snapshot_);
if (!result.succeeded)
{
enterFault(result);
return result;
}
++successful_scan_count_;
emit scanCompleted();
return result;
}

@@ -96,6 +98,11 @@ const LogicScanResult &OfflineSimulationService::lastError() const
return last_error_;
}

const LogicTraceSnapshot &OfflineSimulationService::traceSnapshot() const
{
return trace_snapshot_;
}

void OfflineSimulationService::handleTimeout()
{
executeOnce();


+ 3
- 0
app/src/services/offline_simulation_service.h ファイルの表示

@@ -49,9 +49,11 @@ public:
int scanIntervalMs() const;
std::uint64_t successfulScanCount() const;
const LogicScanResult &lastError() const;
const LogicTraceSnapshot &traceSnapshot() const;

signals:
void stateChanged();
void scanCompleted();

private:
void handleTimeout();
@@ -64,4 +66,5 @@ private:
SimulationState state_ = SimulationState::Stopped;
std::uint64_t successful_scan_count_ = 0;
LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}};
LogicTraceSnapshot trace_snapshot_;
};

+ 68
- 21
app/src/services/software_logic_executor.cpp ファイルの表示

@@ -60,6 +60,13 @@ bool compareWord(

} // namespace

void LogicTraceSnapshot::clear()
{
nodeValues.clear();
expressionValues.clear();
rungValues.clear();
}

LogicScanResult SoftwareLogicExecutor::validate(
const std::vector<ControlLogic> &logics) const
{
@@ -112,7 +119,8 @@ LogicScanResult SoftwareLogicExecutor::validate(

LogicScanResult SoftwareLogicExecutor::executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository) const
RegisterRepository &repository,
LogicTraceSnapshot *trace) const
{
const LogicScanResult validation = validate(logics);
if (!validation.succeeded)
@@ -120,6 +128,10 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
return validation;
}

if (trace != nullptr)
{
trace->clear();
}
for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
@@ -128,32 +140,26 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
}
for (const LadderRung &rung : logic.rungs)
{
if (rung.stages.empty() && !rung.output.has_value())
if (!rung.condition.has_value() && !rung.output.has_value())
{
continue;
}

bool rung_value = true;
for (const LadderStage &stage : rung.stages)
bool rung_value = false;
LogicScanResult result = evaluateExpression(
*rung.condition, repository, trace, &rung_value);
if (!result.succeeded)
{
bool stage_value = false;
for (const LogicNode &node : stage.branches)
{
bool condition_value = false;
LogicScanResult result = evaluateCondition(
node, repository, &condition_value);
if (!result.succeeded)
{
result.logicId = logic.id;
result.rungId = rung.id;
return result;
}
stage_value = stage_value || condition_value;
}
rung_value = rung_value && stage_value;
result.logicId = logic.id;
result.rungId = rung.id;
return result;
}

LogicScanResult result = writeOutput(
if (trace != nullptr)
{
trace->rungValues[rung.id] = rung_value;
trace->nodeValues[rung.output->id] = rung_value;
}
result = writeOutput(
*rung.output, rung_value, repository);
if (!result.succeeded)
{
@@ -166,6 +172,47 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
return success();
}

LogicScanResult SoftwareLogicExecutor::evaluateExpression(
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceSnapshot *trace,
bool *value) const
{
if (value == nullptr)
{
return failure(LogicScanError::InvalidLogic, "expression result target is missing");
}
if (expression.kind == ConditionExpressionKind::Node)
{
LogicScanResult result = evaluateCondition(*expression.node, repository, value);
if (result.succeeded && trace != nullptr)
{
trace->nodeValues[expression.node->id] = *value;
trace->expressionValues[expression.id] = *value;
}
return result;
}

bool accumulated = expression.kind == ConditionExpressionKind::Series;
for (const ConditionExpression &child : expression.children)
{
bool child_value = false;
LogicScanResult result = evaluateExpression(child, repository, trace, &child_value);
if (!result.succeeded)
{
return result;
}
accumulated = expression.kind == ConditionExpressionKind::Series
? accumulated && child_value : accumulated || child_value;
}
*value = accumulated;
if (trace != nullptr)
{
trace->expressionValues[expression.id] = accumulated;
}
return success();
}

LogicScanResult SoftwareLogicExecutor::evaluateCondition(
const LogicNode &node,
RegisterRepository &repository,


+ 17
- 1
app/src/services/software_logic_executor.h ファイルの表示

@@ -4,6 +4,7 @@
#include "domain/register_repository.h"

#include <string>
#include <unordered_map>
#include <vector>

enum class LogicScanError
@@ -25,6 +26,15 @@ struct LogicScanResult
std::string nodeId;
};

struct LogicTraceSnapshot
{
std::unordered_map<std::string, bool> nodeValues;
std::unordered_map<std::string, bool> expressionValues;
std::unordered_map<std::string, bool> rungValues;

void clear();
};

// 按工程顺序执行受限梯形图的一次确定性扫描
class SoftwareLogicExecutor
{
@@ -32,13 +42,19 @@ public:
LogicScanResult validate(const std::vector<ControlLogic> &logics) const;
LogicScanResult executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository) const;
RegisterRepository &repository,
LogicTraceSnapshot *trace = nullptr) const;

private:
LogicScanResult evaluateCondition(
const LogicNode &node,
RegisterRepository &repository,
bool *value) const;
LogicScanResult evaluateExpression(
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceSnapshot *trace,
bool *value) const;
LogicScanResult writeOutput(
const LogicNode &node,
bool rung_value,


+ 450
- 288
app/src/ui/logic_editor_widget.cpp
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 15
- 4
app/src/ui/logic_editor_widget.h ファイルの表示

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

#include "domain/control_logic_model.h"
#include "services/logic_editor_service.h"
#include "services/software_logic_executor.h"

#include <QGraphicsView>

@@ -15,20 +16,30 @@ class LogicEditorWidget final : public QGraphicsView
Q_OBJECT

public:
class NodeItem;
class ExpressionItem;
class RungItem;

explicit LogicEditorWidget(
LogicEditorService &editor_service,
QWidget *parent = nullptr);

void setLogicId(const std::string &logic_id);
void setEditingEnabled(bool enabled);
void setRuntimeTrace(
const LogicTraceSnapshot &trace,
const std::string &fault_node_id = {});
void clearRuntimeTrace();
void reloadLogic();
void selectNode(const std::string &node_id);
std::string selectedNodeId() const;
std::string selectedExpressionId() const;
std::string selectedRungId() const;
std::string selectedStageId() const;

LogicEditorResult addRung();
LogicEditorResult appendCondition(const LogicNodeConfig &config);
LogicEditorResult insertCondition(
const LogicNodeConfig &config, SeriesInsertPosition position);
LogicEditorResult addParallelCondition(const LogicNodeConfig &config);
LogicEditorResult setOutput(const LogicNodeConfig &config);
LogicEditorResult deleteSelected();
@@ -42,9 +53,6 @@ protected:
void resizeEvent(QResizeEvent *event) override;

private:
class NodeItem;
class RungItem;

void handleSelectionChanged();
void reportFailure(const LogicEditorResult &result);
std::string currentRungId() const;
@@ -53,5 +61,8 @@ private:
QGraphicsScene *scene_ = nullptr;
std::string logic_id_;
std::string current_rung_id_;
LogicTraceSnapshot trace_;
std::string fault_node_id_;
bool runtime_trace_enabled_ = false;
bool editing_enabled_ = true;
};

+ 30
- 12
app/tests/domain_tests.cpp ファイルの表示

@@ -83,13 +83,10 @@ Project makeValidProject()
ControlLogic logic;
logic.id = "start-logic";
logic.name = "Start logic";
LadderStage stage;
stage.id = "stage-1";
stage.branches.push_back(contact);
LadderRung rung;
rung.id = "rung-1";
rung.name = "Network 1";
rung.stages.push_back(stage);
rung.condition = ConditionExpression::fromNode(contact);
rung.output = coil;
logic.rungs.push_back(rung);

@@ -153,19 +150,30 @@ void testLadderLogicBoundaries()
ControlLogic logic;
logic.id = "hold-logic";
logic.name = "Hold logic";
ConditionExpression start_parallel;
start_parallel.id = "parallel-start";
start_parallel.kind = ConditionExpressionKind::Parallel;
start_parallel.children = {
ConditionExpression::fromNode(start),
ConditionExpression::fromNode(run_contact)};
ConditionExpression root;
root.id = "series-root";
root.kind = ConditionExpressionKind::Series;
root.children = {
ConditionExpression::fromNode(stop),
start_parallel};
LadderRung rung;
rung.id = "rung-1";
rung.name = "Self hold";
rung.stages.push_back({"stage-stop", {stop}});
rung.stages.push_back({"stage-start", {start, run_contact}});
rung.condition = root;
rung.output = coil;
logic.rungs.push_back(rung);
require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");

logic.rungs.front().stages.front().branches.push_back(coil);
require(!logic.validate(), "a ladder condition stage must reject coils");

logic.rungs.front().stages.front().branches.pop_back();
logic.rungs.front().condition->children.front() =
ConditionExpression::fromNode(coil);
require(!logic.validate(), "a ladder condition expression must reject coils");
logic.rungs.front().condition = root;
logic.rungs.front().output = start;
require(!logic.validate(), "a ladder output must be a coil");

@@ -174,7 +182,7 @@ void testLadderLogicBoundaries()
require(!logic.validateForRunning(),
"conditions without an output must block runtime validation");

LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt};
LadderRung empty_rung{"rung-empty", "Empty network", std::nullopt, std::nullopt};
require(empty_rung.validate(), "an empty editing network must be valid");

empty_rung.output = coil;
@@ -183,8 +191,18 @@ void testLadderLogicBoundaries()
"an output without conditions must block runtime validation");

logic.rungs.front().output = coil;
logic.rungs.front().stages.at(1).branches.at(1).id = start.id;
logic.rungs.front().condition = root;
logic.rungs.front().condition->children.at(1).children.at(1).node->id = start.id;
require(!logic.validate(), "logic node ids must be unique");

ConditionExpression nested_parallel;
nested_parallel.id = "parallel-nested";
nested_parallel.kind = ConditionExpressionKind::Parallel;
nested_parallel.children = {
ConditionExpression::fromNode(start),
root};
require(nested_parallel.validate(),
"nested series and parallel expressions must be valid");
}

void testModelsValidateBindingsAndIdentifiers()


+ 82
- 84
app/tests/logic_editor_service_tests.cpp ファイルの表示

@@ -2,7 +2,6 @@
#include "services/logic_editor_service.h"
#include "services/project_service.h"

#include <exception>
#include <iostream>
#include <stdexcept>

@@ -30,99 +29,98 @@ void require(bool condition, const std::string &message)
}
}

void testEditorOperations()
ContactNodeConfig contact(int address)
{
return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen};
}

void testStructuredEditingAndNormalization()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const LogicEditorResult logic_result = service.ensureDefaultLogic();
require(logic_result.succeeded, "default logic must be created");
const std::string logic_id = logic_result.id;

const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);
require(!rung_id.empty(), "default logic must contain an editable rung");

const LogicEditorResult stop_result = service.appendCondition(
logic_id,
rung_id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 1},
ContactMode::NormallyClosed});
const LogicEditorResult start_result = service.appendCondition(
logic_id,
rung_id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen});
require(stop_result.succeeded && start_result.succeeded,
"series contacts must be appended as ladder stages");

const std::string start_stage_id = service.stageIdForNode(
logic_id, start_result.id);
const LogicEditorResult hold_result = service.addParallelCondition(
logic_id,
rung_id,
start_stage_id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 2},
ContactMode::NormallyOpen});
const LogicEditorResult coil_result = service.setOutput(
const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0));
const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1));
require(first.succeeded && second.succeeded, "series append must succeed");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.size() == 2U,
"two appended nodes must form a series expression");

const std::string second_expression_id = second.id;
const LogicEditorResult parallel = service.addParallelCondition(
logic_id, rung_id, second_expression_id, contact(2));
require(parallel.succeeded, "parallel insertion must succeed");
rung = service.findRung(logic_id, rung_id);
const ConditionExpression *parallel_expression = service.findExpression(
logic_id, rung_id, rung->condition->children.at(1).id);
require(parallel_expression != nullptr
&& parallel_expression->kind == ConditionExpressionKind::Parallel,
"selected node must become a parallel expression");

const LogicEditorResult nested_series = service.insertCondition(
logic_id,
rung_id,
CoilNodeConfig{
RegisterAddress{RegisterArea::M, 2},
CoilMode::Normal});
require(hold_result.succeeded && coil_result.succeeded,
"parallel hold contact and output coil must be added");

require(!service.findNode(logic_id, stop_result.id)->configured,
"new logic nodes must remain unconfigured until properties are applied");
require(service.updateNodeConfig(
parallel.id,
SeriesInsertPosition::After,
contact(3));
require(nested_series.succeeded, "a parallel branch must accept a series node");
rung = service.findRung(logic_id, rung_id);
require(rung->condition->kind == ConditionExpressionKind::Series,
"root must remain a series expression");
const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1);
require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel
&& nested_parallel_expression.children.at(1).kind
== ConditionExpressionKind::Series,
"editor must express A AND (B OR (C AND D))");

require(service.removeNode(logic_id, nested_series.id).succeeded,
"nested series node deletion must succeed");
rung = service.findRung(logic_id, rung_id);
require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel
&& rung->condition->children.at(1).children.at(1).kind
== ConditionExpressionKind::Node,
"single-child series container must collapse after deletion");

require(service.removeNode(logic_id, parallel.id).succeeded,
"parallel leaf deletion must succeed");
rung = service.findRung(logic_id, rung_id);
require(rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.size() == 2U,
"single-child parallel container must collapse after deletion");

require(service.setOutput(
logic_id,
stop_result.id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 1},
ContactMode::NormallyClosed})
rung_id,
CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal})
.succeeded,
"applying logic node properties must succeed");
require(service.findNode(logic_id, stop_result.id)->configured,
"applying logic node properties must mark the node configured");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->stages.size() == 2,
"series conditions must occupy ordered stages");
require(rung->stages.at(1).branches.size() == 2,
"parallel conditions must share one stage");
require(rung->output.has_value(), "rung output must be fixed separately");
"output coil must be set");
require(service.findLogic(logic_id)->validate(),
"configured self-hold ladder must pass full validation");
"structured editing result must remain a valid draft");
}

const LogicNode *start = service.findNode(logic_id, start_result.id);
require(start != nullptr, "added condition must be discoverable");
require(service.updateNodeConfig(
logic_id,
start_result.id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 2},
ContactMode::NormallyClosed})
.succeeded,
"contact properties must be editable");

require(service.removeNode(logic_id, hold_result.id).succeeded,
"parallel branch deletion must succeed");
require(service.findRung(logic_id, rung_id)->stages.at(1).branches.size() == 1,
"deleting one branch must keep the ladder stage");
require(service.removeNode(logic_id, start_result.id).succeeded,
"last branch deletion must succeed");
require(service.findRung(logic_id, rung_id)->stages.size() == 1,
"deleting the last branch must remove the empty stage");

const LogicEditorResult second_rung = service.addRung(logic_id);
require(second_rung.succeeded, "additional ladder rungs must be supported");
require(service.removeRung(logic_id, second_rung.id).succeeded,
"additional ladder rungs must be removable");
require(!service.removeRung(logic_id, rung_id).succeeded,
"the only remaining ladder rung must not be removed");
void testBranchLevelParallelInsertion()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);
service.appendCondition(logic_id, rung_id, contact(0));
service.appendCondition(logic_id, rung_id, contact(1));

const std::string series_id = service.findRung(logic_id, rung_id)->condition->id;
const LogicEditorResult branch = service.addParallelCondition(
logic_id, rung_id, series_id, contact(2));
require(branch.succeeded, "a whole series branch must accept a parallel condition");
const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition;
require(root.kind == ConditionExpressionKind::Parallel
&& root.children.front().kind == ConditionExpressionKind::Series,
"branch-level insertion must express (A AND B) OR C");
}

} // namespace
@@ -131,14 +129,14 @@ int main()
{
try
{
testEditorOperations();
testStructuredEditingAndNormalization();
testBranchLevelParallelInsertion();
}
catch (const std::exception &error)
{
std::cerr << "logic editor service tests failed: " << error.what() << '\n';
return 1;
}

std::cout << "logic editor service tests passed\n";
return 0;
}

+ 75
- 1
app/tests/offline_simulation_service_tests.cpp ファイルの表示

@@ -49,9 +49,38 @@ LadderRung rung(const std::string &id,
LadderRung result;
result.id = id;
result.name = id;
std::vector<ConditionExpression> series_children;
for (std::size_t index = 0; index < stages.size(); ++index)
{
result.stages.push_back({id + "-stage-" + std::to_string(index), stages[index]});
std::vector<ConditionExpression> parallel_children;
for (const LogicNode &node : stages[index])
{
parallel_children.push_back(ConditionExpression::fromNode(node));
}
if (parallel_children.size() == 1U)
{
series_children.push_back(std::move(parallel_children.front()));
}
else
{
ConditionExpression parallel;
parallel.id = id + "-parallel-" + std::to_string(index);
parallel.kind = ConditionExpressionKind::Parallel;
parallel.children = std::move(parallel_children);
series_children.push_back(std::move(parallel));
}
}
if (series_children.size() == 1U)
{
result.condition = std::move(series_children.front());
}
else
{
ConditionExpression series;
series.id = id + "-series";
series.kind = ConditionExpressionKind::Series;
series.children = std::move(series_children);
result.condition = std::move(series);
}
result.output = output;
return result;
@@ -82,6 +111,50 @@ void writeWord(RegisterRepository &repository, int address, std::int16_t value)
"test word write must succeed");
}

void testNestedSeriesParallelExpression()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
ConditionExpression nested_series;
nested_series.id = "nested-series";
nested_series.kind = ConditionExpressionKind::Series;
nested_series.children = {
ConditionExpression::fromNode(contact("b", 1)),
ConditionExpression::fromNode(contact("c", 2))};
ConditionExpression root;
root.id = "root-parallel";
root.kind = ConditionExpressionKind::Parallel;
root.children = {
ConditionExpression::fromNode(contact("a", 0)),
nested_series};
LadderRung nested_rung;
nested_rung.id = "nested-rung";
nested_rung.name = "nested-rung";
nested_rung.condition = root;
nested_rung.output = coil("nested-output", 10);
const ControlLogic program = logic({nested_rung});

writeBit(repository, 1, true);
writeBit(repository, 2, true);
LogicTraceSnapshot trace;
require(executor.executeScan({program}, repository, &trace).succeeded,
"nested expression scan must succeed");
require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)");
require(trace.expressionValues.at("nested-series")
&& trace.expressionValues.at("root-parallel")
&& trace.rungValues.at("nested-rung"),
"scan trace must expose active nested expression and rung values");

writeBit(repository, 2, false);
require(executor.executeScan({program}, repository, &trace).succeeded,
"nested false scan must succeed");
require(!readBit(repository, 10), "incomplete B AND C branch must be false");
writeBit(repository, 0, true);
require(executor.executeScan({program}, repository, &trace).succeeded,
"alternate branch scan must succeed");
require(readBit(repository, 10), "A branch must independently energize output");
}

void testSeriesParallelContactsAndSequentialVisibility()
{
VirtualRegisterRepository repository;
@@ -269,6 +342,7 @@ int main(int argc, char *argv[])
try
{
testSeriesParallelContactsAndSequentialVisibility();
testNestedSeriesParallelExpression();
testAllComparisons();
testSetResetAndDisabledLogic();
testConflictingCoilsAreRejected();


読み込み中…
キャンセル
保存