diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index fce5e83..c9ac6ba 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -3,6 +3,8 @@ #include "project_service.h" #include +#include +#include #include #include #include @@ -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(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); diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index 3b4fe00..380f653 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -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); diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index d3b9421..1c2befc 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -1,5 +1,6 @@ #include "software_logic_executor.h" +#include #include #include @@ -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 &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) diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index 4458e7a..754f6ae 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -26,7 +26,7 @@ struct LogicScanResult std::string nodeId; }; -struct LogicTraceSnapshot +struct LogicTraceValues { std::unordered_map nodeValues; std::unordered_map expressionValues; @@ -35,6 +35,14 @@ struct LogicTraceSnapshot void clear(); }; +struct LogicTraceSnapshot : LogicTraceValues +{ + std::unordered_map 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, diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index 6fb4ea0..bcf80ed 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -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) { diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 62bf52a..f28788b 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -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();