| @@ -1,6 +1,7 @@ | |||||
| #include "control_logic_model.h" | #include "control_logic_model.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <utility> | |||||
| namespace { | namespace { | ||||
| @@ -84,6 +85,53 @@ bool hasDuplicateId(const std::vector<TItem> &items) | |||||
| return false; | 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 | } // namespace | ||||
| bool LogicNode::validate(std::string *error) const | bool LogicNode::validate(std::string *error) const | ||||
| @@ -94,10 +142,7 @@ bool LogicNode::validate(std::string *error) const | |||||
| return false; | return false; | ||||
| } | } | ||||
| return std::visit( | return std::visit( | ||||
| [error](const auto &config) | |||||
| { | |||||
| return validateConfig(config, error); | |||||
| }, | |||||
| [error](const auto &config) { return validateConfig(config, error); }, | |||||
| config); | config); | ||||
| } | } | ||||
| @@ -116,50 +161,171 @@ bool LogicNode::isOutput() const | |||||
| return std::holds_alternative<CoilNodeConfig>(config); | 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; | 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; | 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; | return false; | ||||
| } | } | ||||
| if (!node.isCondition()) | |||||
| if (!child.validate(error)) | |||||
| { | { | ||||
| setError(error, "ladder stage may contain condition nodes only"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| return true; | return true; | ||||
| } | } | ||||
| bool LadderStage::validateForRunning(std::string *error) const | |||||
| bool ConditionExpression::validateForRunning(std::string *error) const | |||||
| { | { | ||||
| if (!validate(error)) | if (!validate(error)) | ||||
| { | { | ||||
| return false; | 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 false; | ||||
| } | } | ||||
| } | } | ||||
| return true; | 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 | bool LadderRung::validate(std::string *error) const | ||||
| { | { | ||||
| return validateStructure(error); | return validateStructure(error); | ||||
| @@ -171,21 +337,18 @@ bool LadderRung::validateForRunning(std::string *error) const | |||||
| { | { | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (stages.empty() && !output.has_value()) | |||||
| if (!condition.has_value() && !output.has_value()) | |||||
| { | { | ||||
| return true; | 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"); | setError(error, "incomplete ladder network requires conditions and an output coil"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| for (const LadderStage &stage : stages) | |||||
| if (!condition->validateForRunning(error)) | |||||
| { | { | ||||
| if (!stage.validateForRunning(error)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| return false; | |||||
| } | } | ||||
| if (!output->isConfigured()) | 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"); | setError(error, "ladder rung id and name must not be empty"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (hasDuplicateId(stages)) | |||||
| { | |||||
| setError(error, "ladder stage ids must be unique within a rung"); | |||||
| return false; | |||||
| } | |||||
| std::vector<std::string> node_ids; | 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; | 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.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"); | setError(error, "ladder rung output must be a coil node"); | ||||
| return false; | return false; | ||||
| @@ -270,18 +426,22 @@ bool ControlLogic::validateStructure(std::string *error) const | |||||
| return false; | return false; | ||||
| } | } | ||||
| std::vector<std::string> node_ids; | std::vector<std::string> node_ids; | ||||
| std::vector<std::string> expression_ids; | |||||
| for (const LadderRung &rung : rungs) | for (const LadderRung &rung : rungs) | ||||
| { | { | ||||
| if (!rung.validateStructure(error)) | if (!rung.validateStructure(error)) | ||||
| { | { | ||||
| return false; | 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()) | 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(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()) | if (std::adjacent_find(node_ids.cbegin(), node_ids.cend()) != node_ids.cend()) | ||||
| { | { | ||||
| setError(error, "logic node ids must be unique within a logic"); | setError(error, "logic node ids must be unique within a logic"); | ||||
| return false; | 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; | return true; | ||||
| } | } | ||||
| @@ -64,22 +64,47 @@ struct LogicNode | |||||
| bool isOutput() const; | bool isOutput() const; | ||||
| }; | }; | ||||
| // 同一个串联级内的条件互为并联关系,任一条件成立即通过该级 | |||||
| struct LadderStage | |||||
| enum class ConditionExpressionKind | |||||
| { | |||||
| Node, | |||||
| Series, | |||||
| Parallel | |||||
| }; | |||||
| // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 | |||||
| struct ConditionExpression | |||||
| { | { | ||||
| std::string id; | 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 validate(std::string *error = nullptr) const; | ||||
| bool validateForRunning(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 | struct LadderRung | ||||
| { | { | ||||
| std::string id; | std::string id; | ||||
| std::string name; | std::string name; | ||||
| std::vector<LadderStage> stages; | |||||
| std::optional<ConditionExpression> condition; | |||||
| std::optional<LogicNode> output; | std::optional<LogicNode> output; | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -3,7 +3,6 @@ | |||||
| #include "project_service.h" | #include "project_service.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <sstream> | |||||
| #include <type_traits> | #include <type_traits> | ||||
| #include <utility> | #include <utility> | ||||
| @@ -11,11 +10,85 @@ namespace { | |||||
| LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) | 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 | } // namespace | ||||
| @@ -29,12 +102,8 @@ const ControlLogic *LogicEditorService::findLogic(const std::string &logic_id) c | |||||
| { | { | ||||
| const auto &logics = project_service_.project().controlLogics; | const auto &logics = project_service_.project().controlLogics; | ||||
| const auto logic = std::find_if( | 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; | return logic == logics.cend() ? nullptr : &*logic; | ||||
| } | } | ||||
| @@ -47,12 +116,8 @@ const LadderRung *LogicEditorService::findRung( | |||||
| return nullptr; | return nullptr; | ||||
| } | } | ||||
| const auto rung = std::find_if( | 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; | return rung == logic->rungs.cend() ? nullptr : &*rung; | ||||
| } | } | ||||
| @@ -66,28 +131,31 @@ const LogicNode *LogicEditorService::findNode( | |||||
| } | } | ||||
| for (const LadderRung &rung : logic->rungs) | 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) | if (rung.output.has_value() && rung.output->id == node_id) | ||||
| { | { | ||||
| return &*rung.output; | return &*rung.output; | ||||
| } | } | ||||
| if (rung.condition.has_value()) | |||||
| { | |||||
| if (const LogicNode *node = findConditionNode(*rung.condition, node_id)) | |||||
| { | |||||
| return node; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| return nullptr; | 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 | std::string LogicEditorService::firstLogicId() const | ||||
| { | { | ||||
| const auto &logics = project_service_.project().controlLogics; | 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 | std::string LogicEditorService::firstRungId(const std::string &logic_id) const | ||||
| { | { | ||||
| const ControlLogic *logic = findLogic(logic_id); | 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( | std::string LogicEditorService::rungIdForNode( | ||||
| @@ -111,50 +178,12 @@ std::string LogicEditorService::rungIdForNode( | |||||
| } | } | ||||
| for (const LadderRung &rung : logic->rungs) | 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; | 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 {}; | return {}; | ||||
| } | } | ||||
| @@ -168,7 +197,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = "logic-1"; | logic.id = "logic-1"; | ||||
| logic.name = "控制逻辑 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 &project = project_service_.editProject(); | ||||
| project.controlLogics.push_back(std::move(logic)); | project.controlLogics.push_back(std::move(logic)); | ||||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | 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); | rung.name = "网络 " + std::to_string(logic->rungs.size() + 1U); | ||||
| Project &project = project_service_.editProject(); | Project &project = project_service_.editProject(); | ||||
| auto target = std::find_if( | 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)); | target->rungs.push_back(std::move(rung)); | ||||
| return {true, LogicEditorError::None, {}, target->rungs.back().id}; | return {true, LogicEditorError::None, {}, target->rungs.back().id}; | ||||
| } | } | ||||
| @@ -230,124 +255,146 @@ LogicEditorResult LogicEditorService::appendCondition( | |||||
| const LogicNodeConfig &config) | const LogicNodeConfig &config) | ||||
| { | { | ||||
| const ControlLogic *logic = findLogic(logic_id); | 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)) | if (!isConditionConfig(config)) | ||||
| { | { | ||||
| return failure(LogicEditorError::InvalidNode, "ladder condition cannot be a coil"); | return failure(LogicEditorError::InvalidNode, "ladder condition cannot be a coil"); | ||||
| } | } | ||||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | 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}; | return {true, LogicEditorError::None, {}, node_id}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::addParallelCondition( | |||||
| LogicEditorResult LogicEditorService::insertCondition( | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &stage_id, | |||||
| const std::string &target_expression_id, | |||||
| SeriesInsertPosition position, | |||||
| const LogicNodeConfig &config) | const LogicNodeConfig &config) | ||||
| { | { | ||||
| const ControlLogic *logic = findLogic(logic_id); | 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)); | 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(); | 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}; | return {true, LogicEditorError::None, {}, node_id}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::setOutput( | |||||
| LogicEditorResult LogicEditorService::addParallelCondition( | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &target_expression_id, | |||||
| const LogicNodeConfig &config) | const LogicNodeConfig &config) | ||||
| { | { | ||||
| const ControlLogic *logic = findLogic(logic_id); | 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(); | 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}; | return {true, LogicEditorError::None, {}, node_id}; | ||||
| } | } | ||||
| @@ -367,8 +414,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig( | |||||
| LogicEditorError::UnsupportedNodeChange, | LogicEditorError::UnsupportedNodeChange, | ||||
| "node category cannot be changed after creation"); | "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; | std::string error; | ||||
| if (!candidate.validate(&error)) | if (!candidate.validate(&error)) | ||||
| { | { | ||||
| @@ -383,24 +429,19 @@ LogicEditorResult LogicEditorService::updateNodeConfig( | |||||
| } | } | ||||
| for (LadderRung &rung : logic.rungs) | 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) | 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}; | 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"); | return failure(LogicEditorError::NodeNotFound, "logic node was not found"); | ||||
| @@ -409,40 +450,42 @@ LogicEditorResult LogicEditorService::updateNodeConfig( | |||||
| LogicEditorResult LogicEditorService::removeNode( | LogicEditorResult LogicEditorService::removeNode( | ||||
| const std::string &logic_id, const std::string &node_id) | 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"); | 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(); | 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) | bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) | ||||
| @@ -469,10 +512,7 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | |||||
| { | { | ||||
| return "coil"; | return "coil"; | ||||
| } | } | ||||
| else | |||||
| { | |||||
| return "compare"; | |||||
| } | |||||
| return "compare"; | |||||
| }, | }, | ||||
| config); | config); | ||||
| } | } | ||||
| @@ -486,13 +526,9 @@ std::string LogicEditorService::makeUniqueNodeId( | |||||
| bool found = false; | bool found = false; | ||||
| for (const LadderRung &rung : logic.rungs) | 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) | 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) | 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; | return candidate; | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| std::string LogicEditorService::makeUniqueStageId(const LadderRung &rung) | |||||
| std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic) | |||||
| { | { | ||||
| for (std::size_t index = 1;; ++index) | 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( | 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; | return candidate; | ||||
| } | } | ||||
| @@ -11,13 +11,19 @@ enum class LogicEditorError | |||||
| None, | None, | ||||
| LogicNotFound, | LogicNotFound, | ||||
| RungNotFound, | RungNotFound, | ||||
| StageNotFound, | |||||
| ExpressionNotFound, | |||||
| NodeNotFound, | NodeNotFound, | ||||
| InvalidNode, | InvalidNode, | ||||
| InvalidOperation, | InvalidOperation, | ||||
| UnsupportedNodeChange | UnsupportedNodeChange | ||||
| }; | }; | ||||
| enum class SeriesInsertPosition | |||||
| { | |||||
| Before, | |||||
| After | |||||
| }; | |||||
| struct LogicEditorResult | struct LogicEditorResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -36,12 +42,14 @@ public: | |||||
| const std::string &logic_id, const std::string &rung_id) const; | const std::string &logic_id, const std::string &rung_id) const; | ||||
| const LogicNode *findNode( | const LogicNode *findNode( | ||||
| const std::string &logic_id, const std::string &node_id) const; | 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 firstLogicId() const; | ||||
| std::string firstRungId(const std::string &logic_id) const; | std::string firstRungId(const std::string &logic_id) const; | ||||
| std::string rungIdForNode( | std::string rungIdForNode( | ||||
| const std::string &logic_id, const std::string &node_id) const; | 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 ensureDefaultLogic(); | ||||
| LogicEditorResult addRung(const std::string &logic_id); | LogicEditorResult addRung(const std::string &logic_id); | ||||
| @@ -51,10 +59,16 @@ public: | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const LogicNodeConfig &config); | 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( | LogicEditorResult addParallelCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &stage_id, | |||||
| const std::string &target_expression_id, | |||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| LogicEditorResult setOutput( | LogicEditorResult setOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| @@ -66,6 +80,10 @@ public: | |||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| LogicEditorResult removeNode( | LogicEditorResult removeNode( | ||||
| const std::string &logic_id, const std::string &node_id); | 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: | private: | ||||
| static bool isConditionConfig(const LogicNodeConfig &config); | static bool isConditionConfig(const LogicNodeConfig &config); | ||||
| @@ -73,8 +91,8 @@ private: | |||||
| static std::string nodePrefix(const LogicNodeConfig &config); | static std::string nodePrefix(const LogicNodeConfig &config); | ||||
| static std::string makeUniqueNodeId( | static std::string makeUniqueNodeId( | ||||
| const ControlLogic &logic, const std::string &prefix); | const ControlLogic &logic, const std::string &prefix); | ||||
| static std::string makeUniqueExpressionId(const ControlLogic &logic); | |||||
| static std::string makeUniqueRungId(const ControlLogic &logic); | static std::string makeUniqueRungId(const ControlLogic &logic); | ||||
| static std::string makeUniqueStageId(const LadderRung &rung); | |||||
| static LogicEditorResult failure( | static LogicEditorResult failure( | ||||
| LogicEditorError error, const std::string &message); | LogicEditorError error, const std::string &message); | ||||
| @@ -35,6 +35,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| logic_snapshot_ = std::move(snapshot); | logic_snapshot_ = std::move(snapshot); | ||||
| successful_scan_count_ = 0; | successful_scan_count_ = 0; | ||||
| last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | ||||
| trace_snapshot_.clear(); | |||||
| state_ = SimulationState::Running; | state_ = SimulationState::Running; | ||||
| timer_.start(); | timer_.start(); | ||||
| emit stateChanged(); | emit stateChanged(); | ||||
| @@ -66,13 +67,14 @@ LogicScanResult OfflineSimulationService::executeOnce() | |||||
| {}}; | {}}; | ||||
| } | } | ||||
| const LogicScanResult result = executor_.executeScan( | const LogicScanResult result = executor_.executeScan( | ||||
| logic_snapshot_, repository_); | |||||
| logic_snapshot_, repository_, &trace_snapshot_); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| enterFault(result); | enterFault(result); | ||||
| return result; | return result; | ||||
| } | } | ||||
| ++successful_scan_count_; | ++successful_scan_count_; | ||||
| emit scanCompleted(); | |||||
| return result; | return result; | ||||
| } | } | ||||
| @@ -96,6 +98,11 @@ const LogicScanResult &OfflineSimulationService::lastError() const | |||||
| return last_error_; | return last_error_; | ||||
| } | } | ||||
| const LogicTraceSnapshot &OfflineSimulationService::traceSnapshot() const | |||||
| { | |||||
| return trace_snapshot_; | |||||
| } | |||||
| void OfflineSimulationService::handleTimeout() | void OfflineSimulationService::handleTimeout() | ||||
| { | { | ||||
| executeOnce(); | executeOnce(); | ||||
| @@ -49,9 +49,11 @@ public: | |||||
| int scanIntervalMs() const; | int scanIntervalMs() const; | ||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| const LogicScanResult &lastError() const; | const LogicScanResult &lastError() const; | ||||
| const LogicTraceSnapshot &traceSnapshot() const; | |||||
| signals: | signals: | ||||
| void stateChanged(); | void stateChanged(); | ||||
| void scanCompleted(); | |||||
| private: | private: | ||||
| void handleTimeout(); | void handleTimeout(); | ||||
| @@ -64,4 +66,5 @@ private: | |||||
| SimulationState state_ = SimulationState::Stopped; | SimulationState state_ = SimulationState::Stopped; | ||||
| std::uint64_t successful_scan_count_ = 0; | std::uint64_t successful_scan_count_ = 0; | ||||
| LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; | LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; | ||||
| LogicTraceSnapshot trace_snapshot_; | |||||
| }; | }; | ||||
| @@ -60,6 +60,13 @@ bool compareWord( | |||||
| } // namespace | } // namespace | ||||
| void LogicTraceSnapshot::clear() | |||||
| { | |||||
| nodeValues.clear(); | |||||
| expressionValues.clear(); | |||||
| rungValues.clear(); | |||||
| } | |||||
| LogicScanResult SoftwareLogicExecutor::validate( | LogicScanResult SoftwareLogicExecutor::validate( | ||||
| const std::vector<ControlLogic> &logics) const | const std::vector<ControlLogic> &logics) const | ||||
| { | { | ||||
| @@ -112,7 +119,8 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| LogicScanResult SoftwareLogicExecutor::executeScan( | LogicScanResult SoftwareLogicExecutor::executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository) const | |||||
| RegisterRepository &repository, | |||||
| LogicTraceSnapshot *trace) const | |||||
| { | { | ||||
| const LogicScanResult validation = validate(logics); | const LogicScanResult validation = validate(logics); | ||||
| if (!validation.succeeded) | if (!validation.succeeded) | ||||
| @@ -120,6 +128,10 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| return validation; | return validation; | ||||
| } | } | ||||
| if (trace != nullptr) | |||||
| { | |||||
| trace->clear(); | |||||
| } | |||||
| for (const ControlLogic &logic : logics) | for (const ControlLogic &logic : logics) | ||||
| { | { | ||||
| if (!logic.enabled) | if (!logic.enabled) | ||||
| @@ -128,32 +140,26 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| } | } | ||||
| for (const LadderRung &rung : logic.rungs) | for (const LadderRung &rung : logic.rungs) | ||||
| { | { | ||||
| if (rung.stages.empty() && !rung.output.has_value()) | |||||
| if (!rung.condition.has_value() && !rung.output.has_value()) | |||||
| { | { | ||||
| continue; | 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); | *rung.output, rung_value, repository); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| @@ -166,6 +172,47 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| return success(); | 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( | LogicScanResult SoftwareLogicExecutor::evaluateCondition( | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| @@ -4,6 +4,7 @@ | |||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include <string> | #include <string> | ||||
| #include <unordered_map> | |||||
| #include <vector> | #include <vector> | ||||
| enum class LogicScanError | enum class LogicScanError | ||||
| @@ -25,6 +26,15 @@ struct LogicScanResult | |||||
| std::string nodeId; | 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 | class SoftwareLogicExecutor | ||||
| { | { | ||||
| @@ -32,13 +42,19 @@ public: | |||||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | ||||
| LogicScanResult executeScan( | LogicScanResult executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository) const; | |||||
| RegisterRepository &repository, | |||||
| LogicTraceSnapshot *trace = nullptr) const; | |||||
| private: | private: | ||||
| LogicScanResult evaluateCondition( | LogicScanResult evaluateCondition( | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value) const; | bool *value) const; | ||||
| LogicScanResult evaluateExpression( | |||||
| const ConditionExpression &expression, | |||||
| RegisterRepository &repository, | |||||
| LogicTraceSnapshot *trace, | |||||
| bool *value) const; | |||||
| LogicScanResult writeOutput( | LogicScanResult writeOutput( | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include "domain/control_logic_model.h" | #include "domain/control_logic_model.h" | ||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/software_logic_executor.h" | |||||
| #include <QGraphicsView> | #include <QGraphicsView> | ||||
| @@ -15,20 +16,30 @@ class LogicEditorWidget final : public QGraphicsView | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| class NodeItem; | |||||
| class ExpressionItem; | |||||
| class RungItem; | |||||
| explicit LogicEditorWidget( | explicit LogicEditorWidget( | ||||
| LogicEditorService &editor_service, | LogicEditorService &editor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| void setLogicId(const std::string &logic_id); | void setLogicId(const std::string &logic_id); | ||||
| void setEditingEnabled(bool enabled); | void setEditingEnabled(bool enabled); | ||||
| void setRuntimeTrace( | |||||
| const LogicTraceSnapshot &trace, | |||||
| const std::string &fault_node_id = {}); | |||||
| void clearRuntimeTrace(); | |||||
| void reloadLogic(); | void reloadLogic(); | ||||
| void selectNode(const std::string &node_id); | void selectNode(const std::string &node_id); | ||||
| std::string selectedNodeId() const; | std::string selectedNodeId() const; | ||||
| std::string selectedExpressionId() const; | |||||
| std::string selectedRungId() const; | std::string selectedRungId() const; | ||||
| std::string selectedStageId() const; | |||||
| LogicEditorResult addRung(); | LogicEditorResult addRung(); | ||||
| LogicEditorResult appendCondition(const LogicNodeConfig &config); | LogicEditorResult appendCondition(const LogicNodeConfig &config); | ||||
| LogicEditorResult insertCondition( | |||||
| const LogicNodeConfig &config, SeriesInsertPosition position); | |||||
| LogicEditorResult addParallelCondition(const LogicNodeConfig &config); | LogicEditorResult addParallelCondition(const LogicNodeConfig &config); | ||||
| LogicEditorResult setOutput(const LogicNodeConfig &config); | LogicEditorResult setOutput(const LogicNodeConfig &config); | ||||
| LogicEditorResult deleteSelected(); | LogicEditorResult deleteSelected(); | ||||
| @@ -42,9 +53,6 @@ protected: | |||||
| void resizeEvent(QResizeEvent *event) override; | void resizeEvent(QResizeEvent *event) override; | ||||
| private: | private: | ||||
| class NodeItem; | |||||
| class RungItem; | |||||
| void handleSelectionChanged(); | void handleSelectionChanged(); | ||||
| void reportFailure(const LogicEditorResult &result); | void reportFailure(const LogicEditorResult &result); | ||||
| std::string currentRungId() const; | std::string currentRungId() const; | ||||
| @@ -53,5 +61,8 @@ private: | |||||
| QGraphicsScene *scene_ = nullptr; | QGraphicsScene *scene_ = nullptr; | ||||
| std::string logic_id_; | std::string logic_id_; | ||||
| std::string current_rung_id_; | std::string current_rung_id_; | ||||
| LogicTraceSnapshot trace_; | |||||
| std::string fault_node_id_; | |||||
| bool runtime_trace_enabled_ = false; | |||||
| bool editing_enabled_ = true; | bool editing_enabled_ = true; | ||||
| }; | }; | ||||
| @@ -83,13 +83,10 @@ Project makeValidProject() | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = "start-logic"; | logic.id = "start-logic"; | ||||
| logic.name = "Start logic"; | logic.name = "Start logic"; | ||||
| LadderStage stage; | |||||
| stage.id = "stage-1"; | |||||
| stage.branches.push_back(contact); | |||||
| LadderRung rung; | LadderRung rung; | ||||
| rung.id = "rung-1"; | rung.id = "rung-1"; | ||||
| rung.name = "Network 1"; | rung.name = "Network 1"; | ||||
| rung.stages.push_back(stage); | |||||
| rung.condition = ConditionExpression::fromNode(contact); | |||||
| rung.output = coil; | rung.output = coil; | ||||
| logic.rungs.push_back(rung); | logic.rungs.push_back(rung); | ||||
| @@ -153,19 +150,30 @@ void testLadderLogicBoundaries() | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = "hold-logic"; | logic.id = "hold-logic"; | ||||
| logic.name = "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; | LadderRung rung; | ||||
| rung.id = "rung-1"; | rung.id = "rung-1"; | ||||
| rung.name = "Self hold"; | 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; | rung.output = coil; | ||||
| logic.rungs.push_back(rung); | logic.rungs.push_back(rung); | ||||
| require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid"); | 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; | logic.rungs.front().output = start; | ||||
| require(!logic.validate(), "a ladder output must be a coil"); | require(!logic.validate(), "a ladder output must be a coil"); | ||||
| @@ -174,7 +182,7 @@ void testLadderLogicBoundaries() | |||||
| require(!logic.validateForRunning(), | require(!logic.validateForRunning(), | ||||
| "conditions without an output must block runtime validation"); | "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"); | require(empty_rung.validate(), "an empty editing network must be valid"); | ||||
| empty_rung.output = coil; | empty_rung.output = coil; | ||||
| @@ -183,8 +191,18 @@ void testLadderLogicBoundaries() | |||||
| "an output without conditions must block runtime validation"); | "an output without conditions must block runtime validation"); | ||||
| logic.rungs.front().output = coil; | 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"); | 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() | void testModelsValidateBindingsAndIdentifiers() | ||||
| @@ -2,7 +2,6 @@ | |||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include <exception> | |||||
| #include <iostream> | #include <iostream> | ||||
| #include <stdexcept> | #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; | TestProjectStorage storage; | ||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| LogicEditorService service(project_service); | 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); | 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, | logic_id, | ||||
| rung_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, | logic_id, | ||||
| stop_result.id, | |||||
| ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 1}, | |||||
| ContactMode::NormallyClosed}) | |||||
| rung_id, | |||||
| CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}) | |||||
| .succeeded, | .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(), | 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 | } // namespace | ||||
| @@ -131,14 +129,14 @@ int main() | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| testEditorOperations(); | |||||
| testStructuredEditingAndNormalization(); | |||||
| testBranchLevelParallelInsertion(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| std::cerr << "logic editor service tests failed: " << error.what() << '\n'; | std::cerr << "logic editor service tests failed: " << error.what() << '\n'; | ||||
| return 1; | return 1; | ||||
| } | } | ||||
| std::cout << "logic editor service tests passed\n"; | std::cout << "logic editor service tests passed\n"; | ||||
| return 0; | return 0; | ||||
| } | } | ||||
| @@ -49,9 +49,38 @@ LadderRung rung(const std::string &id, | |||||
| LadderRung result; | LadderRung result; | ||||
| result.id = id; | result.id = id; | ||||
| result.name = id; | result.name = id; | ||||
| std::vector<ConditionExpression> series_children; | |||||
| for (std::size_t index = 0; index < stages.size(); ++index) | 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; | result.output = output; | ||||
| return result; | return result; | ||||
| @@ -82,6 +111,50 @@ void writeWord(RegisterRepository &repository, int address, std::int16_t value) | |||||
| "test word write must succeed"); | "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() | void testSeriesParallelContactsAndSequentialVisibility() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -269,6 +342,7 @@ int main(int argc, char *argv[]) | |||||
| try | try | ||||
| { | { | ||||
| testSeriesParallelContactsAndSequentialVisibility(); | testSeriesParallelContactsAndSequentialVisibility(); | ||||
| testNestedSeriesParallelExpression(); | |||||
| testAllComparisons(); | testAllComparisons(); | ||||
| testSetResetAndDisabledLogic(); | testSetResetAndDisabledLogic(); | ||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||