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