From 1aa9e53b77e0ecde1e1b1ba46e9d504ffbe2ed65 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Mon, 17 Aug 2026 13:13:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E4=B8=8E=E5=AD=97=E6=93=8D=E4=BD=9C=E7=A6=BB=E7=BA=BF=E6=89=A7?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/services/runtime_mode_service.cpp | 7 +- app/src/services/software_logic_executor.cpp | 255 +++++++++++++++++- app/src/services/software_logic_executor.h | 29 ++ .../offline_simulation_service_tests.cpp | 242 +++++++++++++++++ app/tests/runtime_mode_service_tests.cpp | 71 ++++- 5 files changed, 590 insertions(+), 14 deletions(-) diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index e5ab4f2..da7c169 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -238,12 +238,7 @@ void RuntimeModeService::refreshPlcPollAddresses() } for (const LogicNode *node : nodes) { - const std::optional address = - registerAddressForLogicNode(node->config); - if (address.has_value()) - { - addresses.push_back(*address); - } + collectRegisterAddressesForLogicNode(node->config, &addresses); } } } diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index de8dd46..90670b8 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -1,6 +1,7 @@ #include "software_logic_executor.h" #include +#include #include #include @@ -76,6 +77,8 @@ void LogicTraceValues::clear() expressionValues.clear(); rungValues.clear(); tonValues.clear(); + counterValues.clear(); + wordValues.clear(); } void LogicTraceSnapshot::clear() @@ -95,6 +98,8 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( projection.expressionValues = values->second.expressionValues; projection.rungValues = values->second.rungValues; projection.tonValues = values->second.tonValues; + projection.counterValues = values->second.counterValues; + projection.wordValues = values->second.wordValues; } return projection; } @@ -133,13 +138,17 @@ LogicScanResult SoftwareLogicExecutor::validate( const auto *output = std::get_if(&rung.output->config); if (output == nullptr) { - if (std::holds_alternative(rung.output->config)) + if (std::holds_alternative(rung.output->config) + || std::holds_alternative(rung.output->config) + || std::holds_alternative(rung.output->config) + || std::holds_alternative( + rung.output->config)) { continue; } return failure( LogicScanError::InvalidLogic, - "梯形图输出不是线圈或 TON 指令", + "梯形图输出不是有效的输出指令", logic.id, rung.id, rung.output->id); @@ -158,10 +167,10 @@ LogicScanResult SoftwareLogicExecutor::validate( output_modes[output->address.index()] = output->mode; } } - std::string timer_error; - if (!validateTimerReferencesForRunning(logics, &timer_error)) + std::string resource_error; + if (!validateLogicResourceReferencesForRunning(logics, &resource_error)) { - return failure(LogicScanError::InvalidLogic, timer_error); + return failure(LogicScanError::InvalidLogic, resource_error); } return success(); } @@ -169,7 +178,9 @@ LogicScanResult SoftwareLogicExecutor::validate( void SoftwareLogicExecutor::resetRuntime() { previous_edge_inputs_.clear(); + previous_counter_inputs_.clear(); ton_states_.clear(); + counter_states_.clear(); } LogicScanResult SoftwareLogicExecutor::executeScan( @@ -227,6 +238,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( } bool output_value = false; result = executeOutput( + logic.id, *rung.output, rung_value, repository, @@ -259,6 +271,8 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( trace->expressionValues = values->second.expressionValues; trace->rungValues = values->second.rungValues; trace->tonValues = values->second.tonValues; + trace->counterValues = values->second.counterValues; + trace->wordValues = values->second.wordValues; } } } @@ -371,6 +385,12 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( *value = config.mode == ContactMode::NormallyOpen ? done : !done; return success(); } + else if constexpr (std::is_same_v) + { + const bool done = counter_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); @@ -399,7 +419,51 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( node.config); } +LogicScanResult SoftwareLogicExecutor::readWordOperand( + const WordOperand &operand, + RegisterRepository &repository, + std::int16_t *value, + const std::string &node_id) const +{ + if (value == nullptr) + { + return failure( + LogicScanError::InvalidLogic, + "缺少字操作数结果接收对象", + {}, + {}, + node_id); + } + if (operand.kind == WordOperandKind::Constant) + { + *value = operand.constant; + return success(); + } + if (operand.kind != WordOperandKind::Register) + { + return failure( + LogicScanError::InvalidLogic, + "字操作数类型无效", + {}, + {}, + node_id); + } + const WordReadResult read = repository.readWord(operand.address); + if (!read.succeeded) + { + return failure( + LogicScanError::RegisterReadFailed, + "读取寄存器失败:" + operand.address.toString(), + {}, + {}, + node_id); + } + *value = read.value; + return success(); +} + LogicScanResult SoftwareLogicExecutor::executeOutput( + const std::string &logic_id, const LogicNode &node, bool rung_value, RegisterRepository &repository, @@ -448,12 +512,191 @@ LogicScanResult SoftwareLogicExecutor::executeOutput( return success(); } + if (const auto *counter = std::get_if(&node.config)) + { + const BitReadResult reset_read = repository.readBit(counter->resetAddress); + if (!reset_read.succeeded) + { + return failure( + LogicScanError::RegisterReadFailed, + "读取计数器复位输入失败:" + counter->resetAddress.toString(), + {}, + {}, + node.id); + } + std::int16_t preset = 0; + LogicScanResult result = readWordOperand( + counter->preset, repository, &preset, node.id); + if (!result.succeeded) + { + return result; + } + const std::int32_t bounded_preset = std::max(0, preset); + preset = static_cast( + std::min(bounded_preset, std::numeric_limits::max())); + const WordReadResult current_read = repository.readWord( + counter->currentValueAddress); + if (!current_read.succeeded) + { + return failure( + LogicScanError::RegisterReadFailed, + "读取计数当前值失败:" + counter->currentValueAddress.toString(), + {}, + {}, + node.id); + } + std::int32_t current = std::max(0, current_read.value); + current = std::min(current, std::numeric_limits::max()); + const auto runtime_key = std::make_pair(logic_id, node.id); + const bool previous = previous_counter_inputs_[runtime_key]; + const bool rising = rung_value && !previous; + previous_counter_inputs_[runtime_key] = rung_value; + bool done = counter_states_[counter->address.index()].done; + if (reset_read.value) + { + current = counter->mode == CounterMode::Up ? 0 : preset; + done = counter->mode == CounterMode::Down && current == 0; + } + else if (rising) + { + if (counter->mode == CounterMode::Up) + { + if (current < preset) + { + ++current; + } + done = current >= preset; + } + else + { + if (current > 0) + { + --current; + } + done = current <= 0; + } + } + if (counter->mode == CounterMode::Up && current >= preset) + { + done = true; + } + if (counter->mode == CounterMode::Down && current <= 0) + { + done = true; + } + const std::int16_t current_value = static_cast(current); + const RegisterWriteResult write = repository.writeWord( + counter->currentValueAddress, current_value); + if (!write.succeeded) + { + return failure( + LogicScanError::RegisterWriteFailed, + "写入计数当前值失败:" + counter->currentValueAddress.toString(), + {}, + {}, + node.id); + } + counter_states_[counter->address.index()].done = done; + *output_value = done; + if (trace != nullptr) + { + trace->counterValues[node.id] = { + rung_value, + reset_read.value, + done, + current_value, + preset}; + } + return success(); + } + + if (const auto *move = std::get_if(&node.config)) + { + if (!rung_value) + { + *output_value = false; + return success(); + } + std::int16_t source = 0; + LogicScanResult result = readWordOperand( + move->source, repository, &source, node.id); + if (!result.succeeded) + { + return result; + } + const RegisterWriteResult write = repository.writeWord( + move->destination, source); + if (!write.succeeded) + { + return failure( + LogicScanError::RegisterWriteFailed, + "MOVE 写入寄存器失败:" + move->destination.toString(), + {}, + {}, + node.id); + } + *output_value = true; + if (trace != nullptr) + { + trace->wordValues[node.id] = {source, false}; + } + return success(); + } + + if (const auto *arithmetic = std::get_if(&node.config)) + { + if (!rung_value) + { + *output_value = false; + return success(); + } + std::int16_t left = 0; + std::int16_t right = 0; + LogicScanResult result = readWordOperand( + arithmetic->left, repository, &left, node.id); + if (!result.succeeded) + { + return result; + } + result = readWordOperand(arithmetic->right, repository, &right, node.id); + if (!result.succeeded) + { + return result; + } + const std::int32_t raw = arithmetic->operation == ArithmeticOperation::Add + ? static_cast(left) + right + : static_cast(left) - right; + const bool overflow = raw < std::numeric_limits::min() + || raw > std::numeric_limits::max(); + const std::int32_t bounded = std::max( + std::numeric_limits::min(), + std::min(raw, std::numeric_limits::max())); + const std::int16_t result_value = static_cast(bounded); + const RegisterWriteResult write = repository.writeWord( + arithmetic->destination, result_value); + if (!write.succeeded) + { + return failure( + LogicScanError::RegisterWriteFailed, + "算术指令写入寄存器失败:" + arithmetic->destination.toString(), + {}, + {}, + node.id); + } + *output_value = true; + if (trace != nullptr) + { + trace->wordValues[node.id] = {result_value, overflow}; + } + return success(); + } + const auto *config = std::get_if(&node.config); if (config == nullptr) { return failure( LogicScanError::InvalidLogic, - "梯形图输出不是线圈或 TON 指令", + "梯形图输出不是有效的输出指令", {}, {}, node.id); diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index 121269e..fd59a31 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -38,12 +38,29 @@ struct TonTraceValue int presetMs = 0; }; +struct CounterTraceValue +{ + bool input = false; + bool reset = false; + bool done = false; + std::int16_t value = 0; + std::int16_t preset = 0; +}; + +struct WordTraceValue +{ + std::int16_t value = 0; + bool overflow = false; +}; + struct LogicTraceValues { std::unordered_map nodeValues; std::unordered_map expressionValues; std::unordered_map rungValues; std::unordered_map tonValues; + std::unordered_map counterValues; + std::unordered_map wordValues; void clear(); }; @@ -96,13 +113,25 @@ private: LogicTraceValues *trace, bool *value); LogicScanResult executeOutput( + const std::string &logic_id, const LogicNode &node, bool rung_value, RegisterRepository &repository, TimePoint now, LogicTraceValues *trace, bool *output_value); + LogicScanResult readWordOperand( + const WordOperand &operand, + RegisterRepository &repository, + std::int16_t *value, + const std::string &node_id) const; std::map, bool> previous_edge_inputs_; + std::map, bool> previous_counter_inputs_; std::unordered_map ton_states_; + struct CounterRuntimeState + { + bool done = false; + }; + std::unordered_map counter_states_; }; diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 951013e..01177e3 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -44,11 +44,84 @@ LogicNode timerContact( return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true}; } +LogicNode counterContact( + const std::string &id, + int counter_index, + ContactMode mode = ContactMode::NormallyOpen) +{ + return {id, CounterContactNodeConfig{CounterAddress{counter_index}, mode}, true}; +} + LogicNode ton(const std::string &id, int timer_index, int preset_ms) { return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true}; } +WordOperand constantOperand(std::int16_t value) +{ + return { + WordOperandKind::Constant, + RegisterAddress{RegisterArea::D, 0}, + value}; +} + +WordOperand registerOperand(int address) +{ + return { + WordOperandKind::Register, + RegisterAddress{RegisterArea::D, address}, + 0}; +} + +LogicNode counter( + const std::string &id, + int counter_index, + CounterMode mode, + int current_address, + WordOperand preset, + int reset_address) +{ + return { + id, + CounterNodeConfig{ + CounterAddress{counter_index}, + mode, + RegisterAddress{RegisterArea::D, current_address}, + preset, + RegisterAddress{RegisterArea::M, reset_address}}, + true}; +} + +LogicNode move( + const std::string &id, + WordOperand source, + int destination) +{ + return { + id, + MoveNodeConfig{ + source, + RegisterAddress{RegisterArea::D, destination}}, + true}; +} + +LogicNode arithmetic( + const std::string &id, + ArithmeticOperation operation, + WordOperand left, + WordOperand right, + int destination) +{ + return { + id, + ArithmeticNodeConfig{ + operation, + left, + right, + RegisterAddress{RegisterArea::D, destination}}, + true}; +} + LogicNode comparison(const std::string &id, int address, ComparisonOperator operation, std::int16_t value) { @@ -132,6 +205,14 @@ void writeWord(RegisterRepository &repository, int address, std::int16_t value) "test word write must succeed"); } +std::int16_t readWord(RegisterRepository &repository, int address) +{ + const WordReadResult result = repository.readWord( + RegisterAddress{RegisterArea::D, address}); + require(result.succeeded, "test word read must succeed"); + return result.value; +} + void testNestedSeriesParallelExpression() { VirtualRegisterRepository repository; @@ -462,6 +543,165 @@ void testMultipleTimersAndNetworkOrder() "a later scan must observe the completed TON through T contact"); } +void testCountersUseRisingEdgesAndExternalDValues() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + const ControlLogic up_program = logic({ + rung( + "ctu-rung", + {{contact("count-input", 0)}}, + counter( + "ctu-0", + 0, + CounterMode::Up, + 0, + constantOperand(3), + 1)), + rung( + "ctu-done-rung", + {{counterContact("ctu-done", 0)}}, + coil("ctu-done-output", 2))}); + LogicTraceSnapshot trace; + + require(executor.executeScan({up_program}, repository, &trace).succeeded, + "an inactive CTU must scan successfully"); + require(readWord(repository, 0) == 0 && !readBit(repository, 2), + "an inactive CTU must keep CV zero and Q false"); + + writeBit(repository, 0, true); + require(executor.executeScan({up_program}, repository, &trace).succeeded, + "the first CTU rising edge must scan successfully"); + require(readWord(repository, 0) == 1, + "CTU must increment the external D current value on a rising edge"); + require(executor.executeScan({up_program}, repository, &trace).succeeded, + "a held CTU input must scan successfully"); + require(readWord(repository, 0) == 1, + "a held CTU input must not count every scan"); + + for (int expected = 2; expected <= 3; ++expected) + { + writeBit(repository, 0, false); + require(executor.executeScan({up_program}, repository).succeeded, + "CTU falling preparation scan must succeed"); + writeBit(repository, 0, true); + require(executor.executeScan({up_program}, repository, &trace).succeeded, + "CTU repeated rising edge must succeed"); + require(readWord(repository, 0) == expected, + "CTU must increment exactly once for each rising edge"); + } + require(readBit(repository, 2) + && trace.counterValues.at("ctu-0").done, + "the C contact must observe CTU completion in a later network"); + + writeBit(repository, 1, true); + require(executor.executeScan({up_program}, repository, &trace).succeeded, + "CTU reset must scan successfully"); + require(readWord(repository, 0) == 0 && !readBit(repository, 2), + "CTU reset must clear CV and the C completion state"); + + executor.resetRuntime(); + repository.clear(); + const ControlLogic down_program = logic({ + rung( + "ctd-rung", + {{contact("down-input", 3)}}, + counter( + "ctd-1", + 1, + CounterMode::Down, + 1, + constantOperand(2), + 4)), + rung( + "ctd-done-rung", + {{counterContact("ctd-done", 1)}}, + coil("ctd-done-output", 5))}); + writeBit(repository, 4, true); + require(executor.executeScan({down_program}, repository).succeeded, + "CTD load reset must scan successfully"); + require(readWord(repository, 1) == 2, + "CTD reset must load PV into the external D current value"); + writeBit(repository, 4, false); + for (int expected = 1; expected >= 0; --expected) + { + writeBit(repository, 3, false); + require(executor.executeScan({down_program}, repository).succeeded, + "CTD falling preparation scan must succeed"); + writeBit(repository, 3, true); + require(executor.executeScan({down_program}, repository).succeeded, + "CTD rising edge must scan successfully"); + require(readWord(repository, 1) == expected, + "CTD must decrement exactly once for each rising edge"); + } + require(readBit(repository, 5), + "CTD completion contact must turn on when CV reaches zero"); +} + +void testMoveAndSaturatingArithmetic() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + const ControlLogic program = logic({ + rung( + "move-rung", + {{contact("execute", 0)}}, + move("move", constantOperand(7), 10)), + rung( + "add-rung", + {{contact("execute-add", 0)}}, + arithmetic( + "add", + ArithmeticOperation::Add, + registerOperand(10), + constantOperand(32767), + 11)), + rung( + "sub-rung", + {{contact("execute-sub", 0)}}, + arithmetic( + "sub", + ArithmeticOperation::Subtract, + constantOperand(-32768), + constantOperand(1), + 12))}); + LogicTraceSnapshot trace; + + require(executor.executeScan({program}, repository, &trace).succeeded, + "inactive data instructions must scan successfully"); + require(readWord(repository, 10) == 0, + "MOVE must not write while its rung is false"); + + writeBit(repository, 0, true); + require(executor.executeScan({program}, repository, &trace).succeeded, + "active data instructions must scan successfully"); + require(readWord(repository, 10) == 7, + "MOVE must copy a constant into the destination D register"); + require(readWord(repository, 11) == 32767 + && trace.wordValues.at("add").overflow, + "ADD must saturate positive overflow and expose the overflow trace"); + require(readWord(repository, 12) == -32768 + && trace.wordValues.at("sub").overflow, + "SUB must saturate negative overflow and expose the overflow trace"); + + const ControlLogic repeated_add = logic({ + rung( + "repeated-add-rung", + {{contact("repeated-add-input", 1)}}, + arithmetic( + "repeated-add", + ArithmeticOperation::Add, + registerOperand(20), + constantOperand(1), + 20))}); + writeBit(repository, 1, true); + require(executor.executeScan({repeated_add}, repository).succeeded + && executor.executeScan({repeated_add}, repository).succeeded, + "ADD must execute on every scan while its rung remains true"); + require(readWord(repository, 20) == 2, + "ADD with the same source and destination must accumulate by scan"); +} + void testSetResetPairOnSameAddress() { VirtualRegisterRepository repository; @@ -628,6 +868,8 @@ int main(int argc, char *argv[]) testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); testTonUsesElapsedTimeAndResetsAsNonRetentive(); testMultipleTimersAndNetworkOrder(); + testCountersUseRisingEdgesAndExternalDValues(); + testMoveAndSaturatingArithmetic(); testSetResetPairOnSameAddress(); testConflictingCoilsAreRejected(); testHmiSimulationClosedLoop(); diff --git a/app/tests/runtime_mode_service_tests.cpp b/app/tests/runtime_mode_service_tests.cpp index 44b2add..a152800 100644 --- a/app/tests/runtime_mode_service_tests.cpp +++ b/app/tests/runtime_mode_service_tests.cpp @@ -185,6 +185,68 @@ void testModeTransitions() timer_rung.condition = ConditionExpression::fromNode(timer_contact); timer_rung.output = timer_coil; logic.rungs.push_back(timer_rung); + LogicNode counter_input; + counter_input.id = "poll-counter-input"; + counter_input.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen}; + LogicNode counter_output; + counter_output.id = "poll-counter"; + counter_output.config = CounterNodeConfig{ + CounterAddress{2}, + CounterMode::Up, + RegisterAddress{RegisterArea::D, 36}, + WordOperand{ + WordOperandKind::Register, + RegisterAddress{RegisterArea::D, 37}, + 0}, + RegisterAddress{RegisterArea::M, 26}}; + LadderRung counter_rung; + counter_rung.id = "poll-counter-rung"; + counter_rung.name = "Poll counter"; + counter_rung.condition = ConditionExpression::fromNode(counter_input); + counter_rung.output = counter_output; + logic.rungs.push_back(counter_rung); + LogicNode move_input; + move_input.id = "poll-move-input"; + move_input.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen}; + LogicNode move_output; + move_output.id = "poll-move"; + move_output.config = MoveNodeConfig{ + WordOperand{ + WordOperandKind::Register, + RegisterAddress{RegisterArea::D, 38}, + 0}, + RegisterAddress{RegisterArea::D, 39}}; + LadderRung move_rung; + move_rung.id = "poll-move-rung"; + move_rung.name = "Poll MOVE"; + move_rung.condition = ConditionExpression::fromNode(move_input); + move_rung.output = move_output; + logic.rungs.push_back(move_rung); + LogicNode add_input; + add_input.id = "poll-add-input"; + add_input.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen}; + LogicNode add_output; + add_output.id = "poll-add"; + add_output.config = ArithmeticNodeConfig{ + ArithmeticOperation::Add, + WordOperand{ + WordOperandKind::Register, + RegisterAddress{RegisterArea::D, 40}, + 0}, + WordOperand{ + WordOperandKind::Constant, + RegisterAddress{RegisterArea::D, 0}, + 1}, + RegisterAddress{RegisterArea::D, 41}}; + LadderRung add_rung; + add_rung.id = "poll-add-rung"; + add_rung.name = "Poll ADD"; + add_rung.condition = ConditionExpression::fromNode(add_input); + add_rung.output = add_output; + logic.rungs.push_back(add_rung); project.controlLogics.push_back(logic); require(service.mode() == ApplicationMode::Editing, @@ -218,8 +280,13 @@ void testModeTransitions() {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"); + {RegisterArea::M, 25}, {RegisterArea::M, 26}, + {RegisterArea::M, 27}, {RegisterArea::M, 28}, + {RegisterArea::D, 34}, {RegisterArea::D, 35}, + {RegisterArea::D, 36}, {RegisterArea::D, 37}, + {RegisterArea::D, 38}, {RegisterArea::D, 39}, + {RegisterArea::D, 40}, {RegisterArea::D, 41}}), + "all instruction M/D references must be polled while T/C resources and comments stay offline-only"); gateway.completeInitialRead(); require(service.initialPlcReadCompleted(), "service must retain the initial PLC read state");