| @@ -3,6 +3,8 @@ | |||||
| #include "project_service.h" | #include "project_service.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <cctype> | |||||
| #include <cstddef> | |||||
| #include <iterator> | #include <iterator> | ||||
| #include <unordered_set> | #include <unordered_set> | ||||
| #include <type_traits> | #include <type_traits> | ||||
| @@ -10,6 +12,31 @@ | |||||
| namespace { | namespace { | ||||
| bool isBlank(const std::string &value) | |||||
| { | |||||
| return value.empty() | |||||
| || std::all_of( | |||||
| value.cbegin(), value.cend(), | |||||
| [](unsigned char character) { return std::isspace(character) != 0; }); | |||||
| } | |||||
| std::string makeUniqueLogicId(const Project &project) | |||||
| { | |||||
| int suffix = 1; | |||||
| while (true) | |||||
| { | |||||
| const std::string candidate = "logic-" + std::to_string(suffix); | |||||
| const bool found = std::any_of( | |||||
| project.controlLogics.cbegin(), project.controlLogics.cend(), | |||||
| [&candidate](const ControlLogic &logic) { return logic.id == candidate; }); | |||||
| if (!found) | |||||
| { | |||||
| return candidate; | |||||
| } | |||||
| ++suffix; | |||||
| } | |||||
| } | |||||
| LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) | LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) | ||||
| { | { | ||||
| return {id, config, false}; | return {id, config, false}; | ||||
| @@ -341,6 +368,131 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::addLogic(const std::string &name) | |||||
| { | |||||
| if (isBlank(name)) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空"); | |||||
| } | |||||
| const Project ¤t = project_service_.project(); | |||||
| const bool duplicate = std::any_of( | |||||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||||
| [&name](const ControlLogic &logic) { return logic.name == name; }); | |||||
| if (duplicate) | |||||
| { | |||||
| return failure(LogicEditorError::DuplicateName, "控制逻辑名称必须唯一"); | |||||
| } | |||||
| ControlLogic logic; | |||||
| logic.id = makeUniqueLogicId(current); | |||||
| logic.name = name; | |||||
| 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}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::renameLogic( | |||||
| const std::string &logic_id, const std::string &name) | |||||
| { | |||||
| if (isBlank(name)) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空"); | |||||
| } | |||||
| const Project ¤t = project_service_.project(); | |||||
| const auto logic = std::find_if( | |||||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||||
| if (logic == current.controlLogics.cend()) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||||
| } | |||||
| const bool duplicate = std::any_of( | |||||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||||
| [&logic_id, &name](const ControlLogic &candidate) | |||||
| { | |||||
| return candidate.id != logic_id && candidate.name == name; | |||||
| }); | |||||
| if (duplicate) | |||||
| { | |||||
| return failure(LogicEditorError::DuplicateName, "控制逻辑名称必须唯一"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto editable = std::find_if( | |||||
| project.controlLogics.begin(), project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||||
| editable->name = name; | |||||
| return {true, LogicEditorError::None, {}, logic_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::removeLogic(const std::string &logic_id) | |||||
| { | |||||
| const Project ¤t = project_service_.project(); | |||||
| const auto logic = std::find_if( | |||||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||||
| if (logic == current.controlLogics.cend()) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||||
| } | |||||
| if (current.controlLogics.size() <= 1) | |||||
| { | |||||
| return failure(LogicEditorError::LastLogicRequired, "工程至少需要保留一个控制逻辑"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| project.controlLogics.erase( | |||||
| std::remove_if( | |||||
| project.controlLogics.begin(), project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }), | |||||
| project.controlLogics.end()); | |||||
| return {true, LogicEditorError::None, {}, logic_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::moveLogic(const std::string &logic_id, int offset) | |||||
| { | |||||
| if (offset != -1 && offset != 1) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑每次只能上移或下移一位"); | |||||
| } | |||||
| const Project ¤t = project_service_.project(); | |||||
| const auto logic = std::find_if( | |||||
| current.controlLogics.cbegin(), current.controlLogics.cend(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||||
| if (logic == current.controlLogics.cend()) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||||
| } | |||||
| const auto index = std::distance(current.controlLogics.cbegin(), logic); | |||||
| const std::ptrdiff_t target_index = index + offset; | |||||
| if (target_index < 0 | |||||
| || target_index | |||||
| >= static_cast<std::ptrdiff_t>(current.controlLogics.size())) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑已经位于目标边界"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| std::iter_swap( | |||||
| project.controlLogics.begin() + index, | |||||
| project.controlLogics.begin() + target_index); | |||||
| return {true, LogicEditorError::None, {}, logic_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::setLogicEnabled( | |||||
| const std::string &logic_id, bool enabled) | |||||
| { | |||||
| if (findLogic(logic_id) == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto logic = std::find_if( | |||||
| project.controlLogics.begin(), project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||||
| logic->enabled = enabled; | |||||
| return {true, LogicEditorError::None, {}, logic_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) | LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) | ||||
| { | { | ||||
| const ControlLogic *logic = findLogic(logic_id); | const ControlLogic *logic = findLogic(logic_id); | ||||
| @@ -16,7 +16,9 @@ enum class LogicEditorError | |||||
| NodeNotFound, | NodeNotFound, | ||||
| InvalidNode, | InvalidNode, | ||||
| InvalidOperation, | InvalidOperation, | ||||
| UnsupportedNodeChange | |||||
| UnsupportedNodeChange, | |||||
| DuplicateName, | |||||
| LastLogicRequired | |||||
| }; | }; | ||||
| struct LogicEditorResult | struct LogicEditorResult | ||||
| @@ -47,6 +49,12 @@ public: | |||||
| const std::string &logic_id, const std::string &node_id) const; | const std::string &logic_id, const std::string &node_id) const; | ||||
| LogicEditorResult ensureDefaultLogic(); | LogicEditorResult ensureDefaultLogic(); | ||||
| LogicEditorResult addLogic(const std::string &name); | |||||
| LogicEditorResult renameLogic( | |||||
| const std::string &logic_id, const std::string &name); | |||||
| LogicEditorResult removeLogic(const std::string &logic_id); | |||||
| LogicEditorResult moveLogic(const std::string &logic_id, int offset); | |||||
| LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | |||||
| LogicEditorResult addRung(const std::string &logic_id); | LogicEditorResult addRung(const std::string &logic_id); | ||||
| LogicEditorResult removeRung( | LogicEditorResult removeRung( | ||||
| const std::string &logic_id, const std::string &rung_id); | const std::string &logic_id, const std::string &rung_id); | ||||
| @@ -1,5 +1,6 @@ | |||||
| #include "software_logic_executor.h" | #include "software_logic_executor.h" | ||||
| #include <algorithm> | |||||
| #include <map> | #include <map> | ||||
| #include <type_traits> | #include <type_traits> | ||||
| @@ -69,13 +70,33 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current) | |||||
| } // namespace | } // namespace | ||||
| void LogicTraceSnapshot::clear() | |||||
| void LogicTraceValues::clear() | |||||
| { | { | ||||
| nodeValues.clear(); | nodeValues.clear(); | ||||
| expressionValues.clear(); | expressionValues.clear(); | ||||
| rungValues.clear(); | rungValues.clear(); | ||||
| } | } | ||||
| void LogicTraceSnapshot::clear() | |||||
| { | |||||
| LogicTraceValues::clear(); | |||||
| logicValues.clear(); | |||||
| } | |||||
| LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| const std::string &logic_id) const | |||||
| { | |||||
| LogicTraceSnapshot projection; | |||||
| const auto values = logicValues.find(logic_id); | |||||
| if (values != logicValues.cend()) | |||||
| { | |||||
| projection.nodeValues = values->second.nodeValues; | |||||
| projection.expressionValues = values->second.expressionValues; | |||||
| projection.rungValues = values->second.rungValues; | |||||
| } | |||||
| return projection; | |||||
| } | |||||
| LogicScanResult SoftwareLogicExecutor::validate( | LogicScanResult SoftwareLogicExecutor::validate( | ||||
| const std::vector<ControlLogic> &logics) const | const std::vector<ControlLogic> &logics) const | ||||
| { | { | ||||
| @@ -83,7 +104,7 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| for (const ControlLogic &logic : logics) | for (const ControlLogic &logic : logics) | ||||
| { | { | ||||
| std::string validation_error; | std::string validation_error; | ||||
| if (!logic.validateForRunning(&validation_error)) | |||||
| if (!logic.validateStructure(&validation_error)) | |||||
| { | { | ||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| @@ -94,6 +115,13 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| { | { | ||||
| continue; | continue; | ||||
| } | } | ||||
| if (!logic.validateForRunning(&validation_error)) | |||||
| { | |||||
| return failure( | |||||
| LogicScanError::InvalidLogic, | |||||
| validation_error, | |||||
| logic.id); | |||||
| } | |||||
| for (const LadderRung &rung : logic.rungs) | for (const LadderRung &rung : logic.rungs) | ||||
| { | { | ||||
| if (!rung.output.has_value()) | if (!rung.output.has_value()) | ||||
| @@ -148,6 +176,8 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| { | { | ||||
| continue; | continue; | ||||
| } | } | ||||
| LogicTraceValues *logic_trace = trace == nullptr | |||||
| ? nullptr : &trace->logicValues[logic.id]; | |||||
| for (const LadderRung &rung : logic.rungs) | for (const LadderRung &rung : logic.rungs) | ||||
| { | { | ||||
| if (!rung.condition.has_value() && !rung.output.has_value()) | if (!rung.condition.has_value() && !rung.output.has_value()) | ||||
| @@ -157,17 +187,17 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| bool rung_value = false; | bool rung_value = false; | ||||
| LogicScanResult result = evaluateExpression( | LogicScanResult result = evaluateExpression( | ||||
| *rung.condition, repository, trace, &rung_value); | |||||
| *rung.condition, repository, logic_trace, &rung_value); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| result.logicId = logic.id; | result.logicId = logic.id; | ||||
| result.rungId = rung.id; | result.rungId = rung.id; | ||||
| return result; | return result; | ||||
| } | } | ||||
| if (trace != nullptr) | |||||
| if (logic_trace != nullptr) | |||||
| { | { | ||||
| trace->rungValues[rung.id] = rung_value; | |||||
| trace->nodeValues[rung.output->id] = rung_value; | |||||
| logic_trace->rungValues[rung.id] = rung_value; | |||||
| logic_trace->nodeValues[rung.output->id] = rung_value; | |||||
| } | } | ||||
| result = writeOutput( | result = writeOutput( | ||||
| *rung.output, rung_value, repository); | *rung.output, rung_value, repository); | ||||
| @@ -179,13 +209,29 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if (trace != nullptr) | |||||
| { | |||||
| const auto first_enabled = std::find_if( | |||||
| logics.cbegin(), logics.cend(), | |||||
| [](const ControlLogic &logic) { return logic.enabled; }); | |||||
| if (first_enabled != logics.cend()) | |||||
| { | |||||
| const auto values = trace->logicValues.find(first_enabled->id); | |||||
| if (values != trace->logicValues.cend()) | |||||
| { | |||||
| trace->nodeValues = values->second.nodeValues; | |||||
| trace->expressionValues = values->second.expressionValues; | |||||
| trace->rungValues = values->second.rungValues; | |||||
| } | |||||
| } | |||||
| } | |||||
| return success(); | return success(); | ||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::evaluateExpression( | LogicScanResult SoftwareLogicExecutor::evaluateExpression( | ||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace, | |||||
| LogicTraceValues *trace, | |||||
| bool *value) const | bool *value) const | ||||
| { | { | ||||
| if (value == nullptr) | if (value == nullptr) | ||||
| @@ -26,7 +26,7 @@ struct LogicScanResult | |||||
| std::string nodeId; | std::string nodeId; | ||||
| }; | }; | ||||
| struct LogicTraceSnapshot | |||||
| struct LogicTraceValues | |||||
| { | { | ||||
| std::unordered_map<std::string, bool> nodeValues; | std::unordered_map<std::string, bool> nodeValues; | ||||
| std::unordered_map<std::string, bool> expressionValues; | std::unordered_map<std::string, bool> expressionValues; | ||||
| @@ -35,6 +35,14 @@ struct LogicTraceSnapshot | |||||
| void clear(); | void clear(); | ||||
| }; | }; | ||||
| struct LogicTraceSnapshot : LogicTraceValues | |||||
| { | |||||
| std::unordered_map<std::string, LogicTraceValues> logicValues; | |||||
| void clear(); | |||||
| LogicTraceSnapshot forLogic(const std::string &logic_id) const; | |||||
| }; | |||||
| // 按工程顺序执行受限梯形图的一次确定性扫描 | // 按工程顺序执行受限梯形图的一次确定性扫描 | ||||
| class SoftwareLogicExecutor | class SoftwareLogicExecutor | ||||
| { | { | ||||
| @@ -53,7 +61,7 @@ private: | |||||
| LogicScanResult evaluateExpression( | LogicScanResult evaluateExpression( | ||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace, | |||||
| LogicTraceValues *trace, | |||||
| bool *value) const; | bool *value) const; | ||||
| LogicScanResult writeOutput( | LogicScanResult writeOutput( | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| @@ -132,6 +132,39 @@ void testRangeParallelInsertion() | |||||
| "a non-contiguous selection must be rejected"); | "a non-contiguous selection must be rejected"); | ||||
| } | } | ||||
| void testLogicLifecycleAndOrdering() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| LogicEditorService service(project_service); | |||||
| const std::string first_id = service.ensureDefaultLogic().id; | |||||
| const LogicEditorResult second = service.addLogic("Safety logic"); | |||||
| const LogicEditorResult third = service.addLogic("Alarm logic"); | |||||
| require(second.succeeded && third.succeeded, | |||||
| "multiple control logic modules must be creatable"); | |||||
| require(service.renameLogic(second.id, "Interlock logic").succeeded, | |||||
| "control logic modules must be renamable by stable id"); | |||||
| require(service.renameLogic(third.id, "Interlock logic").error | |||||
| == LogicEditorError::DuplicateName, | |||||
| "control logic names must remain unique"); | |||||
| require(service.moveLogic(third.id, -1).succeeded | |||||
| && project_service.project().controlLogics.at(1).id == third.id, | |||||
| "logic scan order must follow editable vector order"); | |||||
| require(service.setLogicEnabled(second.id, false).succeeded | |||||
| && !service.findLogic(second.id)->enabled, | |||||
| "a control logic module must support explicit disable and enable"); | |||||
| require(service.setLogicEnabled(second.id, true).succeeded | |||||
| && service.findLogic(second.id)->enabled, | |||||
| "a disabled control logic module must be re-enableable"); | |||||
| require(service.removeLogic(third.id).succeeded, | |||||
| "a non-final control logic module must be deletable"); | |||||
| require(service.removeLogic(second.id).succeeded, | |||||
| "logic deletion must preserve the remaining module"); | |||||
| require(service.removeLogic(first_id).error | |||||
| == LogicEditorError::LastLogicRequired, | |||||
| "the project must retain at least one control logic module"); | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| int main() | int main() | ||||
| @@ -140,6 +173,7 @@ int main() | |||||
| { | { | ||||
| testStructuredEditingAndNormalization(); | testStructuredEditingAndNormalization(); | ||||
| testRangeParallelInsertion(); | testRangeParallelInsertion(); | ||||
| testLogicLifecycleAndOrdering(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -231,6 +231,40 @@ void testSetResetAndDisabledLogic() | |||||
| require(readBit(repository, 5), "disabled logic must not change outputs"); | require(readBit(repository, 5), "disabled logic must not change outputs"); | ||||
| } | } | ||||
| void testMultipleLogicScanOrderAndTraceIsolation() | |||||
| { | |||||
| VirtualRegisterRepository repository; | |||||
| SoftwareLogicExecutor executor; | |||||
| ControlLogic first = logic({ | |||||
| rung("rung-1", {{contact("input", 0)}}, coil("output", 1))}); | |||||
| first.id = "logic-first"; | |||||
| first.name = "First"; | |||||
| ControlLogic second = logic({ | |||||
| rung("rung-1", {{contact("input", 1)}}, coil("output", 2))}); | |||||
| second.id = "logic-second"; | |||||
| second.name = "Second"; | |||||
| writeBit(repository, 0, true); | |||||
| LogicTraceSnapshot trace; | |||||
| require(executor.executeScan({first, second}, repository, &trace).succeeded, | |||||
| "all enabled logic modules must execute in project order"); | |||||
| require(readBit(repository, 1) && readBit(repository, 2), | |||||
| "a later logic module must observe an earlier module write in one scan"); | |||||
| require(trace.logicValues.size() == 2U | |||||
| && trace.forLogic(first.id).rungValues.at("rung-1") | |||||
| && trace.forLogic(second.id).rungValues.at("rung-1"), | |||||
| "runtime traces must be partitioned by logic id when node ids repeat"); | |||||
| ControlLogic disabled_draft; | |||||
| disabled_draft.id = "logic-draft"; | |||||
| disabled_draft.name = "Draft"; | |||||
| disabled_draft.enabled = false; | |||||
| disabled_draft.rungs.push_back( | |||||
| {"rung-1", "Draft", std::nullopt, std::nullopt}); | |||||
| require(executor.validate({first, disabled_draft}).succeeded, | |||||
| "a disabled incomplete logic module must not block offline execution"); | |||||
| } | |||||
| void testSetResetPairOnSameAddress() | void testSetResetPairOnSameAddress() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -393,6 +427,7 @@ int main(int argc, char *argv[]) | |||||
| testNestedSeriesParallelExpression(); | testNestedSeriesParallelExpression(); | ||||
| testAllComparisons(); | testAllComparisons(); | ||||
| testSetResetAndDisabledLogic(); | testSetResetAndDisabledLogic(); | ||||
| testMultipleLogicScanOrderAndTraceIsolation(); | |||||
| testSetResetPairOnSameAddress(); | testSetResetPairOnSameAddress(); | ||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||
| testHmiSimulationClosedLoop(); | testHmiSimulationClosedLoop(); | ||||