diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index c9ac6ba..faceea3 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -353,6 +353,13 @@ std::string LogicEditorService::rungIdForNode( return {}; } +std::string LogicEditorService::registerCommentFor( + const RegisterAddress &address) const +{ + const RegisterComment *comment = project_service_.project().findRegisterComment(address); + return comment == nullptr ? std::string{} : comment->text; +} + LogicEditorResult LogicEditorService::ensureDefaultLogic() { if (!project_service_.project().controlLogics.empty()) @@ -362,7 +369,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() ControlLogic logic; logic.id = "logic-1"; logic.name = "控制逻辑 1"; - logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, 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}; @@ -386,7 +393,7 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name) ControlLogic logic; logic.id = makeUniqueLogicId(current); logic.name = name; - logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, 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}; @@ -539,6 +546,22 @@ LogicEditorResult LogicEditorService::removeRung( return {true, LogicEditorError::None, {}, rung_id}; } +LogicEditorResult LogicEditorService::updateRungComment( + const std::string &logic_id, + const std::string &rung_id, + const std::string &comment) +{ + const LadderRung *rung = findRung(logic_id, rung_id); + if (rung == nullptr) + { + return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); + } + LadderRung &editable = *findEditableRung( + project_service_.editProject(), logic_id, rung_id); + editable.comment = comment; + return {true, LogicEditorError::None, {}, rung_id}; +} + LogicEditorResult LogicEditorService::appendCondition( const std::string &logic_id, const std::string &rung_id, @@ -692,7 +715,9 @@ LogicEditorResult LogicEditorService::setOutput( const LadderRung *existing_rung = findRung(logic_id, rung_id); if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config)) { - return failure(LogicEditorError::InvalidNode, "梯形图输出必须使用有效线圈"); + return failure( + LogicEditorError::InvalidNode, + "梯形图输出必须使用有效线圈或 TON 指令"); } const std::string node_id = existing_rung->output.has_value() ? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); @@ -793,12 +818,16 @@ LogicEditorResult LogicEditorService::removeExpression( bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) { - return !std::holds_alternative(config); + return std::holds_alternative(config) + || std::holds_alternative(config) + || std::holds_alternative(config) + || std::holds_alternative(config); } bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) { - return std::holds_alternative(config); + return std::holds_alternative(config) + || std::holds_alternative(config); } std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) @@ -811,10 +840,22 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) { return "contact"; } + else if constexpr (std::is_same_v) + { + return "edge"; + } + else if constexpr (std::is_same_v) + { + return "timer-contact"; + } else if constexpr (std::is_same_v) { return "coil"; } + else if constexpr (std::is_same_v) + { + return "ton"; + } return "compare"; }, config); diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index 380f653..79f3d5b 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -47,6 +47,7 @@ public: 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 registerCommentFor(const RegisterAddress &address) const; LogicEditorResult ensureDefaultLogic(); LogicEditorResult addLogic(const std::string &name); @@ -58,6 +59,10 @@ public: LogicEditorResult addRung(const std::string &logic_id); LogicEditorResult removeRung( const std::string &logic_id, const std::string &rung_id); + LogicEditorResult updateRungComment( + const std::string &logic_id, + const std::string &rung_id, + const std::string &comment); LogicEditorResult appendCondition( const std::string &logic_id, const std::string &rung_id, diff --git a/app/src/services/offline_simulation_service.cpp b/app/src/services/offline_simulation_service.cpp index 46910f2..3413d44 100644 --- a/app/src/services/offline_simulation_service.cpp +++ b/app/src/services/offline_simulation_service.cpp @@ -31,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start( } timer_.stop(); + executor_.resetRuntime(); repository_.clear(); logic_snapshot_ = std::move(snapshot); successful_scan_count_ = 0; @@ -45,6 +46,7 @@ SimulationStartResult OfflineSimulationService::start( void OfflineSimulationService::stop() { timer_.stop(); + executor_.resetRuntime(); logic_snapshot_.clear(); if (state_ == SimulationState::Stopped) { @@ -111,6 +113,7 @@ void OfflineSimulationService::handleTimeout() void OfflineSimulationService::enterFault(const LogicScanResult &error) { timer_.stop(); + executor_.resetRuntime(); last_error_ = error; state_ = SimulationState::Faulted; emit stateChanged(); diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index ae19a0a..e5ab4f2 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -238,9 +238,12 @@ void RuntimeModeService::refreshPlcPollAddresses() } for (const LogicNode *node : nodes) { - std::visit( - [&addresses](const auto &config) { addresses.push_back(config.address); }, - node->config); + const std::optional address = + registerAddressForLogicNode(node->config); + if (address.has_value()) + { + addresses.push_back(*address); + } } } } diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index 1c2befc..de8dd46 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -75,6 +75,7 @@ void LogicTraceValues::clear() nodeValues.clear(); expressionValues.clear(); rungValues.clear(); + tonValues.clear(); } void LogicTraceSnapshot::clear() @@ -93,6 +94,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( projection.nodeValues = values->second.nodeValues; projection.expressionValues = values->second.expressionValues; projection.rungValues = values->second.rungValues; + projection.tonValues = values->second.tonValues; } return projection; } @@ -131,9 +133,13 @@ LogicScanResult SoftwareLogicExecutor::validate( const auto *output = std::get_if(&rung.output->config); if (output == nullptr) { + if (std::holds_alternative(rung.output->config)) + { + continue; + } return failure( LogicScanError::InvalidLogic, - "梯形图输出不是线圈节点", + "梯形图输出不是线圈或 TON 指令", logic.id, rung.id, rung.output->id); @@ -152,13 +158,33 @@ LogicScanResult SoftwareLogicExecutor::validate( output_modes[output->address.index()] = output->mode; } } + std::string timer_error; + if (!validateTimerReferencesForRunning(logics, &timer_error)) + { + return failure(LogicScanError::InvalidLogic, timer_error); + } return success(); } +void SoftwareLogicExecutor::resetRuntime() +{ + previous_edge_inputs_.clear(); + ton_states_.clear(); +} + LogicScanResult SoftwareLogicExecutor::executeScan( const std::vector &logics, RegisterRepository &repository, - LogicTraceSnapshot *trace) const + LogicTraceSnapshot *trace) +{ + return executeScanAt(logics, repository, Clock::now(), trace); +} + +LogicScanResult SoftwareLogicExecutor::executeScanAt( + const std::vector &logics, + RegisterRepository &repository, + TimePoint now, + LogicTraceSnapshot *trace) { const LogicScanResult validation = validate(logics); if (!validation.succeeded) @@ -187,7 +213,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan( bool rung_value = false; LogicScanResult result = evaluateExpression( - *rung.condition, repository, logic_trace, &rung_value); + logic.id, *rung.condition, repository, logic_trace, &rung_value); if (!result.succeeded) { result.logicId = logic.id; @@ -199,14 +225,24 @@ LogicScanResult SoftwareLogicExecutor::executeScan( logic_trace->rungValues[rung.id] = rung_value; logic_trace->nodeValues[rung.output->id] = rung_value; } - result = writeOutput( - *rung.output, rung_value, repository); + bool output_value = false; + result = executeOutput( + *rung.output, + rung_value, + repository, + now, + logic_trace, + &output_value); if (!result.succeeded) { result.logicId = logic.id; result.rungId = rung.id; return result; } + if (logic_trace != nullptr) + { + logic_trace->nodeValues[rung.output->id] = output_value; + } } } if (trace != nullptr) @@ -222,6 +258,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan( trace->nodeValues = values->second.nodeValues; trace->expressionValues = values->second.expressionValues; trace->rungValues = values->second.rungValues; + trace->tonValues = values->second.tonValues; } } } @@ -229,10 +266,11 @@ LogicScanResult SoftwareLogicExecutor::executeScan( } LogicScanResult SoftwareLogicExecutor::evaluateExpression( + const std::string &logic_id, const ConditionExpression &expression, RegisterRepository &repository, LogicTraceValues *trace, - bool *value) const + bool *value) { if (value == nullptr) { @@ -240,7 +278,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( } if (expression.kind == ConditionExpressionKind::Node) { - LogicScanResult result = evaluateCondition(*expression.node, repository, value); + LogicScanResult result = evaluateCondition( + logic_id, *expression.node, repository, value); if (result.succeeded && trace != nullptr) { trace->nodeValues[expression.node->id] = *value; @@ -253,7 +292,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( for (const ConditionExpression &child : expression.children) { bool child_value = false; - LogicScanResult result = evaluateExpression(child, repository, trace, &child_value); + LogicScanResult result = evaluateExpression( + logic_id, child, repository, trace, &child_value); if (!result.succeeded) { return result; @@ -270,9 +310,10 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( } LogicScanResult SoftwareLogicExecutor::evaluateCondition( + const std::string &logic_id, const LogicNode &node, RegisterRepository &repository, - bool *value) const + bool *value) { if (value == nullptr) { @@ -285,7 +326,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( } return std::visit( - [&repository, value, &node](const auto &config) -> LogicScanResult + [this, &logic_id, &repository, value, &node]( + const auto &config) -> LogicScanResult { using Config = std::decay_t; if constexpr (std::is_same_v) @@ -304,6 +346,31 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( ? read.value : !read.value; return success(); } + else if constexpr (std::is_same_v) + { + const BitReadResult read = repository.readBit(config.address); + if (!read.succeeded) + { + return failure( + LogicScanError::RegisterReadFailed, + "读取寄存器失败:" + config.address.toString(), + {}, + {}, + node.id); + } + const auto runtime_key = std::make_pair(logic_id, node.id); + const bool previous = previous_edge_inputs_[runtime_key]; + *value = config.mode == EdgeMode::Rising + ? read.value && !previous : !read.value && previous; + previous_edge_inputs_[runtime_key] = read.value; + return success(); + } + else if constexpr (std::is_same_v) + { + const bool done = ton_states_[config.address.index()].done; + *value = config.mode == ContactMode::NormallyOpen ? done : !done; + return success(); + } else if constexpr (std::is_same_v) { const WordReadResult read = repository.readWord(config.address); @@ -323,7 +390,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( { return failure( LogicScanError::InvalidLogic, - "条件节点不能包含输出线圈", + "条件节点不能包含输出节点", {}, {}, node.id); @@ -332,24 +399,68 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( node.config); } -LogicScanResult SoftwareLogicExecutor::writeOutput( +LogicScanResult SoftwareLogicExecutor::executeOutput( const LogicNode &node, bool rung_value, - RegisterRepository &repository) const + RegisterRepository &repository, + TimePoint now, + LogicTraceValues *trace, + bool *output_value) { + if (output_value == nullptr) + { + return failure( + LogicScanError::InvalidLogic, + "缺少输出指令结果接收对象", + {}, + {}, + node.id); + } + if (const auto *ton = std::get_if(&node.config)) + { + TonRuntimeState &state = ton_states_[ton->address.index()]; + if (!rung_value) + { + state = TonRuntimeState{}; + } + else if (!state.timing) + { + state.timing = true; + state.startedAt = now; + state.elapsed = std::chrono::milliseconds{0}; + state.done = false; + } + else + { + state.elapsed = std::chrono::duration_cast( + now >= state.startedAt ? now - state.startedAt : TimePoint::duration::zero()); + state.done = state.elapsed.count() >= ton->presetMs; + } + *output_value = state.done; + if (trace != nullptr) + { + trace->tonValues[node.id] = { + rung_value, + state.done, + state.elapsed.count(), + ton->presetMs}; + } + return success(); + } + const auto *config = std::get_if(&node.config); if (config == nullptr) { return failure( LogicScanError::InvalidLogic, - "梯形图输出不是线圈节点", + "梯形图输出不是线圈或 TON 指令", {}, {}, node.id); } bool should_write = true; - bool output_value = rung_value; + bool register_value = rung_value; switch (config->mode) { case CoilMode::Normal: @@ -359,13 +470,13 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( case CoilMode::Set: { should_write = rung_value; - output_value = true; + register_value = true; break; } case CoilMode::Reset: { should_write = rung_value; - output_value = false; + register_value = false; break; } default: @@ -381,10 +492,11 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( if (!should_write) { + *output_value = rung_value; return success(); } const RegisterWriteResult write = repository.writeBit( - config->address, output_value); + config->address, register_value); if (!write.succeeded) { return failure( @@ -394,5 +506,6 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( {}, node.id); } + *output_value = rung_value; return success(); } diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index 754f6ae..121269e 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -3,8 +3,12 @@ #include "domain/control_logic_model.h" #include "domain/register_repository.h" +#include +#include +#include #include #include +#include #include enum class LogicScanError @@ -26,11 +30,20 @@ struct LogicScanResult std::string nodeId; }; +struct TonTraceValue +{ + bool input = false; + bool done = false; + std::int64_t elapsedMs = 0; + int presetMs = 0; +}; + struct LogicTraceValues { std::unordered_map nodeValues; std::unordered_map expressionValues; std::unordered_map rungValues; + std::unordered_map tonValues; void clear(); }; @@ -47,24 +60,49 @@ struct LogicTraceSnapshot : LogicTraceValues class SoftwareLogicExecutor { public: + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + LogicScanResult validate(const std::vector &logics) const; + void resetRuntime(); LogicScanResult executeScan( const std::vector &logics, RegisterRepository &repository, - LogicTraceSnapshot *trace = nullptr) const; + LogicTraceSnapshot *trace = nullptr); + LogicScanResult executeScanAt( + const std::vector &logics, + RegisterRepository &repository, + TimePoint now, + LogicTraceSnapshot *trace = nullptr); private: + struct TonRuntimeState + { + bool timing = false; + bool done = false; + TimePoint startedAt{}; + std::chrono::milliseconds elapsed{0}; + }; + LogicScanResult evaluateCondition( + const std::string &logic_id, const LogicNode &node, RegisterRepository &repository, - bool *value) const; + bool *value); LogicScanResult evaluateExpression( + const std::string &logic_id, const ConditionExpression &expression, RegisterRepository &repository, LogicTraceValues *trace, - bool *value) const; - LogicScanResult writeOutput( + bool *value); + LogicScanResult executeOutput( const LogicNode &node, bool rung_value, - RegisterRepository &repository) const; + RegisterRepository &repository, + TimePoint now, + LogicTraceValues *trace, + bool *output_value); + + std::map, bool> previous_edge_inputs_; + std::unordered_map ton_states_; }; diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index bcf80ed..799ee30 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -165,6 +165,67 @@ void testLogicLifecycleAndOrdering() "the project must retain at least one control logic module"); } +void testEdgeTimerNodesAndRungComments() +{ + 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); + + const LogicEditorResult rising = service.appendCondition( + logic_id, + rung_id, + EdgeContactNodeConfig{ + RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising}); + require(rising.succeeded && rising.id == "edge-1", + "the editor must create rising edge nodes with a stable prefix"); + const LogicEditorResult timer = service.addParallelBranch( + logic_id, + rung_id, + {rising.id}, + TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen}); + require(timer.succeeded && timer.id == "timer-contact-1", + "the editor must insert T contacts as conditions"); + const LogicEditorResult ton = service.setOutput( + logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500}); + require(ton.succeeded && ton.id == "ton-1", + "the editor must create TON outputs with a stable prefix"); + + require(service.updateNodeConfig( + logic_id, + rising.id, + EdgeContactNodeConfig{ + RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling}) + .succeeded, + "the editor must apply edge mode and M address properties"); + require(service.updateNodeConfig( + logic_id, + timer.id, + TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed}) + .succeeded, + "the editor must apply T contact mode and address properties"); + require(service.updateNodeConfig( + logic_id, + ton.id, + TonNodeConfig{TimerAddress{4}, 750}) + .succeeded, + "the editor must apply TON preset properties"); + require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded, + "the editor must update a network comment by stable rung id"); + + const LadderRung *rung = service.findRung(logic_id, rung_id); + require(rung != nullptr && rung->comment == "延时启动网络" + && std::get( + rung->condition->children.front().node->config).mode + == EdgeMode::Falling + && std::get( + rung->condition->children.at(1).node->config).address + == TimerAddress{4} + && std::get(rung->output->config).presetMs == 750, + "edge, T contact, TON and rung comment updates must remain in the model"); +} + } // namespace int main() @@ -174,6 +235,7 @@ int main() testStructuredEditingAndNormalization(); testRangeParallelInsertion(); testLogicLifecycleAndOrdering(); + testEdgeTimerNodesAndRungComments(); } catch (const std::exception &error) { diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index f28788b..951013e 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -28,6 +28,27 @@ LogicNode contact(const std::string &id, int address, return {id, ContactNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true}; } +LogicNode edgeContact(const std::string &id, int address, EdgeMode mode) +{ + return {id, + EdgeContactNodeConfig{ + RegisterAddress{RegisterArea::M, address}, mode}, + true}; +} + +LogicNode timerContact( + const std::string &id, + int timer_index, + ContactMode mode = ContactMode::NormallyOpen) +{ + return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true}; +} + +LogicNode ton(const std::string &id, int timer_index, int preset_ms) +{ + return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true}; +} + LogicNode comparison(const std::string &id, int address, ComparisonOperator operation, std::int16_t value) { @@ -260,11 +281,187 @@ void testMultipleLogicScanOrderAndTraceIsolation() disabled_draft.name = "Draft"; disabled_draft.enabled = false; disabled_draft.rungs.push_back( - {"rung-1", "Draft", std::nullopt, std::nullopt}); + {"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 testEdgeContactsAreOneScanPulsesAndAreLogicScoped() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + const ControlLogic program = logic({ + rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}}, + coil("rising-output", 10)), + rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}}, + coil("falling-output", 11))}); + + writeBit(repository, 0, true); + require(executor.executeScan({program}, repository).succeeded, + "the first high scan must evaluate rising edge input"); + require(readBit(repository, 10), "rising edge must pulse on OFF to ON"); + require(!readBit(repository, 11), "falling edge must stay off without ON to OFF"); + require(executor.executeScan({program}, repository).succeeded, + "a repeated high scan must succeed"); + require(!readBit(repository, 10), "rising edge must clear after one scan"); + + writeBit(repository, 1, true); + require(executor.executeScan({program}, repository).succeeded, + "the falling edge input must establish its previous high state"); + writeBit(repository, 1, false); + require(executor.executeScan({program}, repository).succeeded, + "the first low scan after high must succeed"); + require(readBit(repository, 11), "falling edge must pulse on ON to OFF"); + require(executor.executeScan({program}, repository).succeeded, + "a repeated low scan must succeed"); + require(!readBit(repository, 11), "falling edge must clear after one scan"); + + ControlLogic first = logic({ + rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}}, + coil("shared-output-first", 20))}); + first.id = "logic-first"; + first.name = "First"; + ControlLogic second = logic({ + rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}}, + coil("shared-output-second", 21))}); + second.id = "logic-second"; + second.name = "Second"; + writeBit(repository, 2, true); + require(executor.executeScan({first, second}, repository).succeeded, + "logic-scoped edge history must support duplicate node ids"); + writeBit(repository, 3, true); + require(executor.executeScan({first, second}, repository).succeeded, + "each duplicate edge node must update its own history"); + require(!readBit(repository, 20) && readBit(repository, 21), + "duplicate node ids in different logic modules must not share edge history"); + + executor.resetRuntime(); + require(executor.executeScan({first, second}, repository).succeeded, + "resetting runtime must clear edge history"); + require(readBit(repository, 20) && readBit(repository, 21), + "a restarted runtime must treat current ON inputs as fresh rising edges"); +} + +void testTonUsesElapsedTimeAndResetsAsNonRetentive() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + const ControlLogic program = logic({ + rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)), + rung("ton-feedback-rung", {{timerContact("timer-done", 0)}}, + coil("ton-output", 30))}); + const auto start = SoftwareLogicExecutor::TimePoint{}; + LogicTraceSnapshot trace; + + require(executor.executeScanAt({program}, repository, start, &trace).succeeded, + "an inactive TON must scan successfully"); + require(!readBit(repository, 30) + && trace.tonValues.at("ton-0").elapsedMs == 0 + && !trace.tonValues.at("ton-0").done, + "an inactive TON must have Q false and ET zero"); + + writeBit(repository, 0, true); + require(executor.executeScanAt({program}, repository, start, &trace).succeeded, + "the first active TON scan must start timing"); + require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0, + "TON must not complete on its starting scan"); + require(executor.executeScanAt( + {program}, repository, + start + std::chrono::milliseconds{99}, &trace) + .succeeded, + "TON must use elapsed monotonic time before the preset"); + require(!readBit(repository, 30) + && trace.tonValues.at("ton-0").elapsedMs == 99, + "TON must remain false before PT"); + require(executor.executeScanAt( + {program}, repository, + start + std::chrono::milliseconds{100}, &trace) + .succeeded, + "TON must complete at its preset time"); + require(readBit(repository, 30) + && trace.tonValues.at("ton-0").done, + "the T contact must observe TON Q in a later network of the same scan"); + + writeBit(repository, 0, false); + require(executor.executeScanAt( + {program}, repository, + start + std::chrono::milliseconds{150}, &trace) + .succeeded, + "disconnecting TON input must scan successfully"); + require(!readBit(repository, 30) + && trace.tonValues.at("ton-0").elapsedMs == 0 + && !trace.tonValues.at("ton-0").done, + "a non-retentive TON must reset ET and Q when input opens"); + + writeBit(repository, 0, true); + require(executor.executeScanAt( + {program}, repository, + start + std::chrono::milliseconds{200}, &trace) + .succeeded, + "TON must restart timing after an input reset"); + require(!readBit(repository, 30), "TON restart must not retain the old done state"); + + executor.resetRuntime(); + require(executor.executeScanAt( + {program}, repository, + start + std::chrono::milliseconds{1000}, &trace) + .succeeded, + "resetRuntime must clear TON state"); + require(!readBit(repository, 30), + "a restarted runtime must start a currently active TON from zero"); +} + +void testMultipleTimersAndNetworkOrder() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + const ControlLogic independent = logic({ + rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)), + rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))}); + writeBit(repository, 1, true); + writeBit(repository, 2, true); + const auto start = SoftwareLogicExecutor::TimePoint{}; + LogicTraceSnapshot trace; + require(executor.executeScanAt({independent}, repository, start, &trace).succeeded, + "multiple TON resources must start independently"); + require(executor.executeScanAt( + {independent}, repository, + start + std::chrono::milliseconds{100}, &trace) + .succeeded, + "the first independent TON must reach PT without completing the second"); + require(trace.tonValues.at("ton-1").done + && !trace.tonValues.at("ton-2").done, + "different T resources must retain independent elapsed time"); + + VirtualRegisterRepository ordered_repository; + SoftwareLogicExecutor ordered_executor; + const ControlLogic ordered = logic({ + rung("feedback-before-ton", {{timerContact("early-timer", 4)}}, + coil("early-output", 40)), + rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))}); + writeBit(ordered_repository, 4, true); + require(ordered_executor.executeScanAt( + {ordered}, ordered_repository, start, nullptr) + .succeeded, + "network order test must start TON after its earlier T contact"); + require(!readBit(ordered_repository, 40), + "an earlier T contact must see the previous scan state"); + require(ordered_executor.executeScanAt( + {ordered}, ordered_repository, + start + std::chrono::milliseconds{100}, nullptr) + .succeeded, + "network order test must complete TON on the next scan"); + require(!readBit(ordered_repository, 40), + "the earlier T contact must remain false on the completion scan"); + require(ordered_executor.executeScanAt( + {ordered}, ordered_repository, + start + std::chrono::milliseconds{101}, nullptr) + .succeeded, + "network order test must expose TON Q on the following scan"); + require(readBit(ordered_repository, 40), + "a later scan must observe the completed TON through T contact"); +} + void testSetResetPairOnSameAddress() { VirtualRegisterRepository repository; @@ -428,6 +625,9 @@ int main(int argc, char *argv[]) testAllComparisons(); testSetResetAndDisabledLogic(); testMultipleLogicScanOrderAndTraceIsolation(); + testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); + testTonUsesElapsedTimeAndResetsAsNonRetentive(); + testMultipleTimersAndNetworkOrder(); testSetResetPairOnSameAddress(); testConflictingCoilsAreRejected(); testHmiSimulationClosedLoop(); diff --git a/app/tests/runtime_mode_service_tests.cpp b/app/tests/runtime_mode_service_tests.cpp index 7f12215..44b2add 100644 --- a/app/tests/runtime_mode_service_tests.cpp +++ b/app/tests/runtime_mode_service_tests.cpp @@ -123,6 +123,69 @@ void testModeTransitions() {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"}); project.alarmDefinitions.push_back( {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"}); + project.registerComments = { + {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"}, + {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}}; + + ControlLogic logic; + logic.id = "poll-logic"; + logic.name = "Poll logic"; + LogicNode edge; + edge.id = "poll-edge"; + edge.config = EdgeContactNodeConfig{ + RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising}; + LogicNode edge_coil; + edge_coil.id = "poll-edge-coil"; + edge_coil.config = CoilNodeConfig{ + RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal}; + LadderRung edge_rung; + edge_rung.id = "poll-edge-rung"; + edge_rung.name = "Poll edge"; + edge_rung.condition = ConditionExpression::fromNode(edge); + edge_rung.output = edge_coil; + logic.rungs.push_back(edge_rung); + LogicNode comparison; + comparison.id = "poll-comparison"; + comparison.config = CompareNodeConfig{ + RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0}; + LogicNode comparison_coil; + comparison_coil.id = "poll-comparison-coil"; + comparison_coil.config = CoilNodeConfig{ + RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal}; + LadderRung comparison_rung; + comparison_rung.id = "poll-comparison-rung"; + comparison_rung.name = "Poll comparison"; + comparison_rung.condition = ConditionExpression::fromNode(comparison); + comparison_rung.output = comparison_coil; + logic.rungs.push_back(comparison_rung); + LogicNode timer_input; + timer_input.id = "poll-timer-input"; + timer_input.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen}; + LogicNode ton; + ton.id = "poll-ton"; + ton.config = TonNodeConfig{TimerAddress{4}, 100}; + LadderRung ton_rung; + ton_rung.id = "poll-ton-rung"; + ton_rung.name = "Poll TON"; + ton_rung.condition = ConditionExpression::fromNode(timer_input); + ton_rung.output = ton; + logic.rungs.push_back(ton_rung); + LogicNode timer_contact; + timer_contact.id = "poll-timer-contact"; + timer_contact.config = TimerContactNodeConfig{ + TimerAddress{4}, ContactMode::NormallyOpen}; + LogicNode timer_coil; + timer_coil.id = "poll-timer-coil"; + timer_coil.config = CoilNodeConfig{ + RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal}; + LadderRung timer_rung; + timer_rung.id = "poll-timer-rung"; + timer_rung.name = "Poll timer contact"; + timer_rung.condition = ConditionExpression::fromNode(timer_contact); + timer_rung.output = timer_coil; + logic.rungs.push_back(timer_rung); + project.controlLogics.push_back(logic); require(service.mode() == ApplicationMode::Editing, "service must start in editing mode"); @@ -152,8 +215,11 @@ void testModeTransitions() "PLC connection must be established before its initial read can complete"); require(gateway.pollAddresses() == std::vector({ - {RegisterArea::M, 12}, {RegisterArea::D, 34}}), - "M and D alarm addresses must be included in PLC polling"); + {RegisterArea::M, 12}, {RegisterArea::M, 20}, + {RegisterArea::M, 21}, {RegisterArea::M, 22}, + {RegisterArea::M, 23}, {RegisterArea::M, 24}, + {RegisterArea::D, 34}, {RegisterArea::D, 35}}), + "M/D logic and alarm addresses must be polled while T and comments stay offline-only"); gateway.completeInitialRead(); require(service.initialPlcReadCompleted(), "service must retain the initial PLC read state");