| @@ -3,6 +3,8 @@ | |||
| #include "project_service.h" | |||
| #include <algorithm> | |||
| #include <cctype> | |||
| #include <cstddef> | |||
| #include <iterator> | |||
| #include <unordered_set> | |||
| #include <type_traits> | |||
| @@ -10,6 +12,31 @@ | |||
| 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) | |||
| { | |||
| return {id, config, false}; | |||
| @@ -341,6 +368,131 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||
| 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) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| @@ -16,7 +16,9 @@ enum class LogicEditorError | |||
| NodeNotFound, | |||
| InvalidNode, | |||
| InvalidOperation, | |||
| UnsupportedNodeChange | |||
| UnsupportedNodeChange, | |||
| DuplicateName, | |||
| LastLogicRequired | |||
| }; | |||
| struct LogicEditorResult | |||
| @@ -47,6 +49,12 @@ public: | |||
| const std::string &logic_id, const std::string &node_id) const; | |||
| 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 removeRung( | |||
| const std::string &logic_id, const std::string &rung_id); | |||
| @@ -1,5 +1,6 @@ | |||
| #include "software_logic_executor.h" | |||
| #include <algorithm> | |||
| #include <map> | |||
| #include <type_traits> | |||
| @@ -69,13 +70,33 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current) | |||
| } // namespace | |||
| void LogicTraceSnapshot::clear() | |||
| void LogicTraceValues::clear() | |||
| { | |||
| nodeValues.clear(); | |||
| expressionValues.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( | |||
| const std::vector<ControlLogic> &logics) const | |||
| { | |||
| @@ -83,7 +104,7 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| for (const ControlLogic &logic : logics) | |||
| { | |||
| std::string validation_error; | |||
| if (!logic.validateForRunning(&validation_error)) | |||
| if (!logic.validateStructure(&validation_error)) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| @@ -94,6 +115,13 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| { | |||
| continue; | |||
| } | |||
| if (!logic.validateForRunning(&validation_error)) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| validation_error, | |||
| logic.id); | |||
| } | |||
| for (const LadderRung &rung : logic.rungs) | |||
| { | |||
| if (!rung.output.has_value()) | |||
| @@ -148,6 +176,8 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||
| { | |||
| continue; | |||
| } | |||
| LogicTraceValues *logic_trace = trace == nullptr | |||
| ? nullptr : &trace->logicValues[logic.id]; | |||
| for (const LadderRung &rung : logic.rungs) | |||
| { | |||
| if (!rung.condition.has_value() && !rung.output.has_value()) | |||
| @@ -157,17 +187,17 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||
| bool rung_value = false; | |||
| LogicScanResult result = evaluateExpression( | |||
| *rung.condition, repository, trace, &rung_value); | |||
| *rung.condition, repository, logic_trace, &rung_value); | |||
| if (!result.succeeded) | |||
| { | |||
| result.logicId = logic.id; | |||
| result.rungId = rung.id; | |||
| 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( | |||
| *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(); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||
| const ConditionExpression &expression, | |||
| RegisterRepository &repository, | |||
| LogicTraceSnapshot *trace, | |||
| LogicTraceValues *trace, | |||
| bool *value) const | |||
| { | |||
| if (value == nullptr) | |||
| @@ -26,7 +26,7 @@ struct LogicScanResult | |||
| std::string nodeId; | |||
| }; | |||
| struct LogicTraceSnapshot | |||
| struct LogicTraceValues | |||
| { | |||
| std::unordered_map<std::string, bool> nodeValues; | |||
| std::unordered_map<std::string, bool> expressionValues; | |||
| @@ -35,6 +35,14 @@ struct LogicTraceSnapshot | |||
| void clear(); | |||
| }; | |||
| struct LogicTraceSnapshot : LogicTraceValues | |||
| { | |||
| std::unordered_map<std::string, LogicTraceValues> logicValues; | |||
| void clear(); | |||
| LogicTraceSnapshot forLogic(const std::string &logic_id) const; | |||
| }; | |||
| // 按工程顺序执行受限梯形图的一次确定性扫描 | |||
| class SoftwareLogicExecutor | |||
| { | |||
| @@ -53,7 +61,7 @@ private: | |||
| LogicScanResult evaluateExpression( | |||
| const ConditionExpression &expression, | |||
| RegisterRepository &repository, | |||
| LogicTraceSnapshot *trace, | |||
| LogicTraceValues *trace, | |||
| bool *value) const; | |||
| LogicScanResult writeOutput( | |||
| const LogicNode &node, | |||
| @@ -132,6 +132,39 @@ void testRangeParallelInsertion() | |||
| "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 | |||
| int main() | |||
| @@ -140,6 +173,7 @@ int main() | |||
| { | |||
| testStructuredEditingAndNormalization(); | |||
| testRangeParallelInsertion(); | |||
| testLogicLifecycleAndOrdering(); | |||
| } | |||
| catch (const std::exception &error) | |||
| { | |||
| @@ -231,6 +231,40 @@ void testSetResetAndDisabledLogic() | |||
| 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() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| @@ -393,6 +427,7 @@ int main(int argc, char *argv[]) | |||
| testNestedSeriesParallelExpression(); | |||
| testAllComparisons(); | |||
| testSetResetAndDisabledLogic(); | |||
| testMultipleLogicScanOrderAndTraceIsolation(); | |||
| testSetResetPairOnSameAddress(); | |||
| testConflictingCoilsAreRejected(); | |||
| testHmiSimulationClosedLoop(); | |||