Kaynağa Gözat

feat: 实现计数与字操作离线执行

main
suyu 1 ay önce
ebeveyn
işleme
1aa9e53b77
5 değiştirilmiş dosya ile 590 ekleme ve 14 silme
  1. +1
    -6
      app/src/services/runtime_mode_service.cpp
  2. +249
    -6
      app/src/services/software_logic_executor.cpp
  3. +29
    -0
      app/src/services/software_logic_executor.h
  4. +242
    -0
      app/tests/offline_simulation_service_tests.cpp
  5. +69
    -2
      app/tests/runtime_mode_service_tests.cpp

+ 1
- 6
app/src/services/runtime_mode_service.cpp Dosyayı Görüntüle

@@ -238,12 +238,7 @@ void RuntimeModeService::refreshPlcPollAddresses()
}
for (const LogicNode *node : nodes)
{
const std::optional<RegisterAddress> address =
registerAddressForLogicNode(node->config);
if (address.has_value())
{
addresses.push_back(*address);
}
collectRegisterAddressesForLogicNode(node->config, &addresses);
}
}
}


+ 249
- 6
app/src/services/software_logic_executor.cpp Dosyayı Görüntüle

@@ -1,6 +1,7 @@
#include "software_logic_executor.h"

#include <algorithm>
#include <limits>
#include <map>
#include <type_traits>

@@ -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<CoilNodeConfig>(&rung.output->config);
if (output == nullptr)
{
if (std::holds_alternative<TonNodeConfig>(rung.output->config))
if (std::holds_alternative<TonNodeConfig>(rung.output->config)
|| std::holds_alternative<CounterNodeConfig>(rung.output->config)
|| std::holds_alternative<MoveNodeConfig>(rung.output->config)
|| std::holds_alternative<ArithmeticNodeConfig>(
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<Config, CounterContactNodeConfig>)
{
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<Config, CompareNodeConfig>)
{
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<CounterNodeConfig>(&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<std::int32_t>(0, preset);
preset = static_cast<std::int16_t>(
std::min<std::int32_t>(bounded_preset, std::numeric_limits<std::int16_t>::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<std::int32_t>(0, current_read.value);
current = std::min<std::int32_t>(current, std::numeric_limits<std::int16_t>::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<std::int16_t>(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<MoveNodeConfig>(&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<ArithmeticNodeConfig>(&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<std::int32_t>(left) + right
: static_cast<std::int32_t>(left) - right;
const bool overflow = raw < std::numeric_limits<std::int16_t>::min()
|| raw > std::numeric_limits<std::int16_t>::max();
const std::int32_t bounded = std::max<std::int32_t>(
std::numeric_limits<std::int16_t>::min(),
std::min<std::int32_t>(raw, std::numeric_limits<std::int16_t>::max()));
const std::int16_t result_value = static_cast<std::int16_t>(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<CoilNodeConfig>(&node.config);
if (config == nullptr)
{
return failure(
LogicScanError::InvalidLogic,
"梯形图输出不是线圈或 TON 指令",
"梯形图输出不是有效的输出指令",
{},
{},
node.id);


+ 29
- 0
app/src/services/software_logic_executor.h Dosyayı Görüntüle

@@ -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<std::string, bool> nodeValues;
std::unordered_map<std::string, bool> expressionValues;
std::unordered_map<std::string, bool> rungValues;
std::unordered_map<std::string, TonTraceValue> tonValues;
std::unordered_map<std::string, CounterTraceValue> counterValues;
std::unordered_map<std::string, WordTraceValue> 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<std::pair<std::string, std::string>, bool> previous_edge_inputs_;
std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_;
std::unordered_map<int, TonRuntimeState> ton_states_;
struct CounterRuntimeState
{
bool done = false;
};
std::unordered_map<int, CounterRuntimeState> counter_states_;
};

+ 242
- 0
app/tests/offline_simulation_service_tests.cpp Dosyayı Görüntüle

@@ -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();


+ 69
- 2
app/tests/runtime_mode_service_tests.cpp Dosyayı Görüntüle

@@ -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");


Yükleniyor…
İptal
Kaydet