| @@ -353,6 +353,13 @@ std::string LogicEditorService::rungIdForNode( | |||||
| return {}; | 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() | LogicEditorResult LogicEditorService::ensureDefaultLogic() | ||||
| { | { | ||||
| if (!project_service_.project().controlLogics.empty()) | if (!project_service_.project().controlLogics.empty()) | ||||
| @@ -362,7 +369,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = "logic-1"; | logic.id = "logic-1"; | ||||
| logic.name = "控制逻辑 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 &project = project_service_.editProject(); | ||||
| project.controlLogics.push_back(std::move(logic)); | project.controlLogics.push_back(std::move(logic)); | ||||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | ||||
| @@ -386,7 +393,7 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name) | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = makeUniqueLogicId(current); | logic.id = makeUniqueLogicId(current); | ||||
| logic.name = name; | 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 &project = project_service_.editProject(); | ||||
| project.controlLogics.push_back(std::move(logic)); | project.controlLogics.push_back(std::move(logic)); | ||||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | ||||
| @@ -539,6 +546,22 @@ LogicEditorResult LogicEditorService::removeRung( | |||||
| return {true, LogicEditorError::None, {}, rung_id}; | 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( | LogicEditorResult LogicEditorService::appendCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -692,7 +715,9 @@ LogicEditorResult LogicEditorService::setOutput( | |||||
| const LadderRung *existing_rung = findRung(logic_id, rung_id); | const LadderRung *existing_rung = findRung(logic_id, rung_id); | ||||
| if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config)) | 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() | const std::string node_id = existing_rung->output.has_value() | ||||
| ? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); | ? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); | ||||
| @@ -793,12 +818,16 @@ LogicEditorResult LogicEditorService::removeExpression( | |||||
| bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) | bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) | ||||
| { | { | ||||
| return !std::holds_alternative<CoilNodeConfig>(config); | |||||
| return std::holds_alternative<ContactNodeConfig>(config) | |||||
| || std::holds_alternative<EdgeContactNodeConfig>(config) | |||||
| || std::holds_alternative<TimerContactNodeConfig>(config) | |||||
| || std::holds_alternative<CompareNodeConfig>(config); | |||||
| } | } | ||||
| bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) | bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) | ||||
| { | { | ||||
| return std::holds_alternative<CoilNodeConfig>(config); | |||||
| return std::holds_alternative<CoilNodeConfig>(config) | |||||
| || std::holds_alternative<TonNodeConfig>(config); | |||||
| } | } | ||||
| std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | ||||
| @@ -811,10 +840,22 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | |||||
| { | { | ||||
| return "contact"; | return "contact"; | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | |||||
| { | |||||
| return "edge"; | |||||
| } | |||||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||||
| { | |||||
| return "timer-contact"; | |||||
| } | |||||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | ||||
| { | { | ||||
| return "coil"; | return "coil"; | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, TonNodeConfig>) | |||||
| { | |||||
| return "ton"; | |||||
| } | |||||
| return "compare"; | return "compare"; | ||||
| }, | }, | ||||
| config); | config); | ||||
| @@ -47,6 +47,7 @@ public: | |||||
| std::string firstRungId(const std::string &logic_id) const; | std::string firstRungId(const std::string &logic_id) const; | ||||
| std::string rungIdForNode( | std::string rungIdForNode( | ||||
| const std::string &logic_id, const std::string &node_id) const; | const std::string &logic_id, const std::string &node_id) const; | ||||
| std::string registerCommentFor(const RegisterAddress &address) const; | |||||
| LogicEditorResult ensureDefaultLogic(); | LogicEditorResult ensureDefaultLogic(); | ||||
| LogicEditorResult addLogic(const std::string &name); | LogicEditorResult addLogic(const std::string &name); | ||||
| @@ -58,6 +59,10 @@ public: | |||||
| 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); | ||||
| LogicEditorResult updateRungComment( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| const std::string &comment); | |||||
| LogicEditorResult appendCondition( | LogicEditorResult appendCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -31,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| } | } | ||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | |||||
| repository_.clear(); | repository_.clear(); | ||||
| logic_snapshot_ = std::move(snapshot); | logic_snapshot_ = std::move(snapshot); | ||||
| successful_scan_count_ = 0; | successful_scan_count_ = 0; | ||||
| @@ -45,6 +46,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| void OfflineSimulationService::stop() | void OfflineSimulationService::stop() | ||||
| { | { | ||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | |||||
| logic_snapshot_.clear(); | logic_snapshot_.clear(); | ||||
| if (state_ == SimulationState::Stopped) | if (state_ == SimulationState::Stopped) | ||||
| { | { | ||||
| @@ -111,6 +113,7 @@ void OfflineSimulationService::handleTimeout() | |||||
| void OfflineSimulationService::enterFault(const LogicScanResult &error) | void OfflineSimulationService::enterFault(const LogicScanResult &error) | ||||
| { | { | ||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | |||||
| last_error_ = error; | last_error_ = error; | ||||
| state_ = SimulationState::Faulted; | state_ = SimulationState::Faulted; | ||||
| emit stateChanged(); | emit stateChanged(); | ||||
| @@ -238,9 +238,12 @@ void RuntimeModeService::refreshPlcPollAddresses() | |||||
| } | } | ||||
| for (const LogicNode *node : nodes) | for (const LogicNode *node : nodes) | ||||
| { | { | ||||
| std::visit( | |||||
| [&addresses](const auto &config) { addresses.push_back(config.address); }, | |||||
| node->config); | |||||
| const std::optional<RegisterAddress> address = | |||||
| registerAddressForLogicNode(node->config); | |||||
| if (address.has_value()) | |||||
| { | |||||
| addresses.push_back(*address); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -75,6 +75,7 @@ void LogicTraceValues::clear() | |||||
| nodeValues.clear(); | nodeValues.clear(); | ||||
| expressionValues.clear(); | expressionValues.clear(); | ||||
| rungValues.clear(); | rungValues.clear(); | ||||
| tonValues.clear(); | |||||
| } | } | ||||
| void LogicTraceSnapshot::clear() | void LogicTraceSnapshot::clear() | ||||
| @@ -93,6 +94,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| projection.nodeValues = values->second.nodeValues; | projection.nodeValues = values->second.nodeValues; | ||||
| projection.expressionValues = values->second.expressionValues; | projection.expressionValues = values->second.expressionValues; | ||||
| projection.rungValues = values->second.rungValues; | projection.rungValues = values->second.rungValues; | ||||
| projection.tonValues = values->second.tonValues; | |||||
| } | } | ||||
| return projection; | return projection; | ||||
| } | } | ||||
| @@ -131,9 +133,13 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | ||||
| if (output == nullptr) | if (output == nullptr) | ||||
| { | { | ||||
| if (std::holds_alternative<TonNodeConfig>(rung.output->config)) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "梯形图输出不是线圈节点", | |||||
| "梯形图输出不是线圈或 TON 指令", | |||||
| logic.id, | logic.id, | ||||
| rung.id, | rung.id, | ||||
| rung.output->id); | rung.output->id); | ||||
| @@ -152,13 +158,33 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| output_modes[output->address.index()] = output->mode; | output_modes[output->address.index()] = output->mode; | ||||
| } | } | ||||
| } | } | ||||
| std::string timer_error; | |||||
| if (!validateTimerReferencesForRunning(logics, &timer_error)) | |||||
| { | |||||
| return failure(LogicScanError::InvalidLogic, timer_error); | |||||
| } | |||||
| return success(); | return success(); | ||||
| } | } | ||||
| void SoftwareLogicExecutor::resetRuntime() | |||||
| { | |||||
| previous_edge_inputs_.clear(); | |||||
| ton_states_.clear(); | |||||
| } | |||||
| LogicScanResult SoftwareLogicExecutor::executeScan( | LogicScanResult SoftwareLogicExecutor::executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace) const | |||||
| LogicTraceSnapshot *trace) | |||||
| { | |||||
| return executeScanAt(logics, repository, Clock::now(), trace); | |||||
| } | |||||
| LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| const std::vector<ControlLogic> &logics, | |||||
| RegisterRepository &repository, | |||||
| TimePoint now, | |||||
| LogicTraceSnapshot *trace) | |||||
| { | { | ||||
| const LogicScanResult validation = validate(logics); | const LogicScanResult validation = validate(logics); | ||||
| if (!validation.succeeded) | if (!validation.succeeded) | ||||
| @@ -187,7 +213,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| bool rung_value = false; | bool rung_value = false; | ||||
| LogicScanResult result = evaluateExpression( | LogicScanResult result = evaluateExpression( | ||||
| *rung.condition, repository, logic_trace, &rung_value); | |||||
| logic.id, *rung.condition, repository, logic_trace, &rung_value); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| result.logicId = logic.id; | result.logicId = logic.id; | ||||
| @@ -199,14 +225,24 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| logic_trace->rungValues[rung.id] = rung_value; | logic_trace->rungValues[rung.id] = rung_value; | ||||
| logic_trace->nodeValues[rung.output->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) | if (!result.succeeded) | ||||
| { | { | ||||
| result.logicId = logic.id; | result.logicId = logic.id; | ||||
| result.rungId = rung.id; | result.rungId = rung.id; | ||||
| return result; | return result; | ||||
| } | } | ||||
| if (logic_trace != nullptr) | |||||
| { | |||||
| logic_trace->nodeValues[rung.output->id] = output_value; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| if (trace != nullptr) | if (trace != nullptr) | ||||
| @@ -222,6 +258,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| trace->nodeValues = values->second.nodeValues; | trace->nodeValues = values->second.nodeValues; | ||||
| trace->expressionValues = values->second.expressionValues; | trace->expressionValues = values->second.expressionValues; | ||||
| trace->rungValues = values->second.rungValues; | trace->rungValues = values->second.rungValues; | ||||
| trace->tonValues = values->second.tonValues; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -229,10 +266,11 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::evaluateExpression( | LogicScanResult SoftwareLogicExecutor::evaluateExpression( | ||||
| const std::string &logic_id, | |||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *value) const | |||||
| bool *value) | |||||
| { | { | ||||
| if (value == nullptr) | if (value == nullptr) | ||||
| { | { | ||||
| @@ -240,7 +278,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||||
| } | } | ||||
| if (expression.kind == ConditionExpressionKind::Node) | 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) | if (result.succeeded && trace != nullptr) | ||||
| { | { | ||||
| trace->nodeValues[expression.node->id] = *value; | trace->nodeValues[expression.node->id] = *value; | ||||
| @@ -253,7 +292,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||||
| for (const ConditionExpression &child : expression.children) | for (const ConditionExpression &child : expression.children) | ||||
| { | { | ||||
| bool child_value = false; | 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) | if (!result.succeeded) | ||||
| { | { | ||||
| return result; | return result; | ||||
| @@ -270,9 +310,10 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::evaluateCondition( | LogicScanResult SoftwareLogicExecutor::evaluateCondition( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value) const | |||||
| bool *value) | |||||
| { | { | ||||
| if (value == nullptr) | if (value == nullptr) | ||||
| { | { | ||||
| @@ -285,7 +326,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| } | } | ||||
| return std::visit( | 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<decltype(config)>; | using Config = std::decay_t<decltype(config)>; | ||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | if constexpr (std::is_same_v<Config, ContactNodeConfig>) | ||||
| @@ -304,6 +346,31 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| ? read.value : !read.value; | ? read.value : !read.value; | ||||
| return success(); | return success(); | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | |||||
| { | |||||
| 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<Config, TimerContactNodeConfig>) | |||||
| { | |||||
| 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<Config, CompareNodeConfig>) | else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | ||||
| { | { | ||||
| const WordReadResult read = repository.readWord(config.address); | const WordReadResult read = repository.readWord(config.address); | ||||
| @@ -323,7 +390,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| { | { | ||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "条件节点不能包含输出线圈", | |||||
| "条件节点不能包含输出节点", | |||||
| {}, | {}, | ||||
| {}, | {}, | ||||
| node.id); | node.id); | ||||
| @@ -332,24 +399,68 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| node.config); | node.config); | ||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::writeOutput( | |||||
| LogicScanResult SoftwareLogicExecutor::executeOutput( | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | 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<TonNodeConfig>(&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<std::chrono::milliseconds>( | |||||
| 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<CoilNodeConfig>(&node.config); | const auto *config = std::get_if<CoilNodeConfig>(&node.config); | ||||
| if (config == nullptr) | if (config == nullptr) | ||||
| { | { | ||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "梯形图输出不是线圈节点", | |||||
| "梯形图输出不是线圈或 TON 指令", | |||||
| {}, | {}, | ||||
| {}, | {}, | ||||
| node.id); | node.id); | ||||
| } | } | ||||
| bool should_write = true; | bool should_write = true; | ||||
| bool output_value = rung_value; | |||||
| bool register_value = rung_value; | |||||
| switch (config->mode) | switch (config->mode) | ||||
| { | { | ||||
| case CoilMode::Normal: | case CoilMode::Normal: | ||||
| @@ -359,13 +470,13 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( | |||||
| case CoilMode::Set: | case CoilMode::Set: | ||||
| { | { | ||||
| should_write = rung_value; | should_write = rung_value; | ||||
| output_value = true; | |||||
| register_value = true; | |||||
| break; | break; | ||||
| } | } | ||||
| case CoilMode::Reset: | case CoilMode::Reset: | ||||
| { | { | ||||
| should_write = rung_value; | should_write = rung_value; | ||||
| output_value = false; | |||||
| register_value = false; | |||||
| break; | break; | ||||
| } | } | ||||
| default: | default: | ||||
| @@ -381,10 +492,11 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( | |||||
| if (!should_write) | if (!should_write) | ||||
| { | { | ||||
| *output_value = rung_value; | |||||
| return success(); | return success(); | ||||
| } | } | ||||
| const RegisterWriteResult write = repository.writeBit( | const RegisterWriteResult write = repository.writeBit( | ||||
| config->address, output_value); | |||||
| config->address, register_value); | |||||
| if (!write.succeeded) | if (!write.succeeded) | ||||
| { | { | ||||
| return failure( | return failure( | ||||
| @@ -394,5 +506,6 @@ LogicScanResult SoftwareLogicExecutor::writeOutput( | |||||
| {}, | {}, | ||||
| node.id); | node.id); | ||||
| } | } | ||||
| *output_value = rung_value; | |||||
| return success(); | return success(); | ||||
| } | } | ||||
| @@ -3,8 +3,12 @@ | |||||
| #include "domain/control_logic_model.h" | #include "domain/control_logic_model.h" | ||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include <chrono> | |||||
| #include <cstdint> | |||||
| #include <map> | |||||
| #include <string> | #include <string> | ||||
| #include <unordered_map> | #include <unordered_map> | ||||
| #include <utility> | |||||
| #include <vector> | #include <vector> | ||||
| enum class LogicScanError | enum class LogicScanError | ||||
| @@ -26,11 +30,20 @@ struct LogicScanResult | |||||
| std::string nodeId; | std::string nodeId; | ||||
| }; | }; | ||||
| struct TonTraceValue | |||||
| { | |||||
| bool input = false; | |||||
| bool done = false; | |||||
| std::int64_t elapsedMs = 0; | |||||
| int presetMs = 0; | |||||
| }; | |||||
| struct LogicTraceValues | 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; | ||||
| std::unordered_map<std::string, bool> rungValues; | std::unordered_map<std::string, bool> rungValues; | ||||
| std::unordered_map<std::string, TonTraceValue> tonValues; | |||||
| void clear(); | void clear(); | ||||
| }; | }; | ||||
| @@ -47,24 +60,49 @@ struct LogicTraceSnapshot : LogicTraceValues | |||||
| class SoftwareLogicExecutor | class SoftwareLogicExecutor | ||||
| { | { | ||||
| public: | public: | ||||
| using Clock = std::chrono::steady_clock; | |||||
| using TimePoint = Clock::time_point; | |||||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | ||||
| void resetRuntime(); | |||||
| LogicScanResult executeScan( | LogicScanResult executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace = nullptr) const; | |||||
| LogicTraceSnapshot *trace = nullptr); | |||||
| LogicScanResult executeScanAt( | |||||
| const std::vector<ControlLogic> &logics, | |||||
| RegisterRepository &repository, | |||||
| TimePoint now, | |||||
| LogicTraceSnapshot *trace = nullptr); | |||||
| private: | private: | ||||
| struct TonRuntimeState | |||||
| { | |||||
| bool timing = false; | |||||
| bool done = false; | |||||
| TimePoint startedAt{}; | |||||
| std::chrono::milliseconds elapsed{0}; | |||||
| }; | |||||
| LogicScanResult evaluateCondition( | LogicScanResult evaluateCondition( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value) const; | |||||
| bool *value); | |||||
| LogicScanResult evaluateExpression( | LogicScanResult evaluateExpression( | ||||
| const std::string &logic_id, | |||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *value) const; | |||||
| LogicScanResult writeOutput( | |||||
| bool *value); | |||||
| LogicScanResult executeOutput( | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| RegisterRepository &repository) const; | |||||
| RegisterRepository &repository, | |||||
| TimePoint now, | |||||
| LogicTraceValues *trace, | |||||
| bool *output_value); | |||||
| std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_; | |||||
| std::unordered_map<int, TonRuntimeState> ton_states_; | |||||
| }; | }; | ||||
| @@ -165,6 +165,67 @@ void testLogicLifecycleAndOrdering() | |||||
| "the project must retain at least one control logic module"); | "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<EdgeContactNodeConfig>( | |||||
| rung->condition->children.front().node->config).mode | |||||
| == EdgeMode::Falling | |||||
| && std::get<TimerContactNodeConfig>( | |||||
| rung->condition->children.at(1).node->config).address | |||||
| == TimerAddress{4} | |||||
| && std::get<TonNodeConfig>(rung->output->config).presetMs == 750, | |||||
| "edge, T contact, TON and rung comment updates must remain in the model"); | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| int main() | int main() | ||||
| @@ -174,6 +235,7 @@ int main() | |||||
| testStructuredEditingAndNormalization(); | testStructuredEditingAndNormalization(); | ||||
| testRangeParallelInsertion(); | testRangeParallelInsertion(); | ||||
| testLogicLifecycleAndOrdering(); | testLogicLifecycleAndOrdering(); | ||||
| testEdgeTimerNodesAndRungComments(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -28,6 +28,27 @@ LogicNode contact(const std::string &id, int address, | |||||
| return {id, ContactNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true}; | 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, | LogicNode comparison(const std::string &id, int address, | ||||
| ComparisonOperator operation, std::int16_t value) | ComparisonOperator operation, std::int16_t value) | ||||
| { | { | ||||
| @@ -260,11 +281,187 @@ void testMultipleLogicScanOrderAndTraceIsolation() | |||||
| disabled_draft.name = "Draft"; | disabled_draft.name = "Draft"; | ||||
| disabled_draft.enabled = false; | disabled_draft.enabled = false; | ||||
| disabled_draft.rungs.push_back( | 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, | require(executor.validate({first, disabled_draft}).succeeded, | ||||
| "a disabled incomplete logic module must not block offline execution"); | "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() | void testSetResetPairOnSameAddress() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -428,6 +625,9 @@ int main(int argc, char *argv[]) | |||||
| testAllComparisons(); | testAllComparisons(); | ||||
| testSetResetAndDisabledLogic(); | testSetResetAndDisabledLogic(); | ||||
| testMultipleLogicScanOrderAndTraceIsolation(); | testMultipleLogicScanOrderAndTraceIsolation(); | ||||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | |||||
| testTonUsesElapsedTimeAndResetsAsNonRetentive(); | |||||
| testMultipleTimersAndNetworkOrder(); | |||||
| testSetResetPairOnSameAddress(); | testSetResetPairOnSameAddress(); | ||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||
| testHmiSimulationClosedLoop(); | testHmiSimulationClosedLoop(); | ||||
| @@ -123,6 +123,69 @@ void testModeTransitions() | |||||
| {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"}); | {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"}); | ||||
| project.alarmDefinitions.push_back( | project.alarmDefinitions.push_back( | ||||
| {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"}); | {"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, | require(service.mode() == ApplicationMode::Editing, | ||||
| "service must start in editing mode"); | "service must start in editing mode"); | ||||
| @@ -152,8 +215,11 @@ void testModeTransitions() | |||||
| "PLC connection must be established before its initial read can complete"); | "PLC connection must be established before its initial read can complete"); | ||||
| require(gateway.pollAddresses() | require(gateway.pollAddresses() | ||||
| == std::vector<RegisterAddress>({ | == std::vector<RegisterAddress>({ | ||||
| {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(); | gateway.completeInitialRead(); | ||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||