From 0d2b4f09420a1d708613d2ea5f2fd444f94e62fa Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Fri, 14 Aug 2026 13:46:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=A2=AF=E5=BD=A2?= =?UTF-8?q?=E5=9B=BE=E7=BA=BF=E5=9C=88=E7=BD=AE=E4=BD=8D=E5=A4=8D=E4=BD=8D?= =?UTF-8?q?=E9=85=8D=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/services/software_logic_executor.cpp | 12 +++- .../offline_simulation_service_tests.cpp | 56 +++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index 6f176c8..d3b9421 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -58,6 +58,15 @@ bool compareWord( } } +bool coilModesAreCompatible(CoilMode existing, CoilMode current) +{ + if (existing == current) + { + return true; + } + return existing != CoilMode::Normal && current != CoilMode::Normal; +} + } // namespace void LogicTraceSnapshot::clear() @@ -102,7 +111,8 @@ LogicScanResult SoftwareLogicExecutor::validate( rung.output->id); } const auto existing = output_modes.find(output->address.index()); - if (existing != output_modes.end() && existing->second != output->mode) + if (existing != output_modes.end() + && !coilModesAreCompatible(existing->second, output->mode)) { return failure( LogicScanError::ConflictingOutput, diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 39fe924..62bf52a 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -231,17 +231,62 @@ void testSetResetAndDisabledLogic() require(readBit(repository, 5), "disabled logic must not change outputs"); } -void testConflictingCoilsAreRejected() +void testSetResetPairOnSameAddress() { + VirtualRegisterRepository repository; SoftwareLogicExecutor executor; const ControlLogic program = logic({ + rung("low-level-rung", + {{comparison("low-level", 0, + ComparisonOperator::LessThanOrEqual, 30)}}, + coil("pump-set", 20, CoilMode::Set)), + rung("high-level-rung", + {{comparison("high-level", 0, + ComparisonOperator::GreaterThanOrEqual, 80)}}, + coil("pump-reset", 20, CoilMode::Reset))}); + + writeWord(repository, 0, 20); + require(executor.executeScan({program}, repository).succeeded, + "set/reset pair scan must succeed at the low limit"); + require(readBit(repository, 20), "low level must latch the pump on"); + + writeWord(repository, 0, 50); + require(executor.executeScan({program}, repository).succeeded, + "set/reset pair scan must succeed inside the deadband"); + require(readBit(repository, 20), "deadband must retain the latched on state"); + + writeWord(repository, 0, 90); + require(executor.executeScan({program}, repository).succeeded, + "set/reset pair scan must succeed at the high limit"); + require(!readBit(repository, 20), "high level must reset the pump"); + + writeWord(repository, 0, 50); + require(executor.executeScan({program}, repository).succeeded, + "set/reset pair rescan must succeed inside the deadband"); + require(!readBit(repository, 20), "deadband must retain the reset state"); +} + +void testConflictingCoilsAreRejected() +{ + SoftwareLogicExecutor executor; + const ControlLogic normal_and_set = logic({ rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)), rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))}); - const LogicScanResult result = executor.validate({program}); - require(!result.succeeded && result.error == LogicScanError::ConflictingOutput, - "mixed output modes for one address must be rejected"); - require(result.rungId == "set-rung" && result.nodeId == "set", + const LogicScanResult set_result = executor.validate({normal_and_set}); + require(!set_result.succeeded + && set_result.error == LogicScanError::ConflictingOutput, + "normal and set coils for one address must be rejected"); + require(set_result.rungId == "set-rung" && set_result.nodeId == "set", "conflict error must identify the offending rung and node"); + + const ControlLogic reset_and_normal = logic({ + rung("reset-rung", {{contact("reset-input", 0)}}, + coil("reset", 9, CoilMode::Reset)), + rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))}); + const LogicScanResult reset_result = executor.validate({reset_and_normal}); + require(!reset_result.succeeded + && reset_result.error == LogicScanError::ConflictingOutput, + "reset and normal coils for one address must be rejected"); } void testHmiSimulationClosedLoop() @@ -348,6 +393,7 @@ int main(int argc, char *argv[]) testNestedSeriesParallelExpression(); testAllComparisons(); testSetResetAndDisabledLogic(); + testSetResetPairOnSameAddress(); testConflictingCoilsAreRejected(); testHmiSimulationClosedLoop(); testSimulationLifecycleSnapshotAndFault();