Procházet zdrojové kódy

feat: 完善离线逻辑执行与定时器扫描

main
suyu před 1 měsícem
rodič
revize
610fbb71a2
9 změnil soubory, kde provedl 565 přidání a 34 odebrání
  1. +46
    -5
      app/src/services/logic_editor_service.cpp
  2. +5
    -0
      app/src/services/logic_editor_service.h
  3. +3
    -0
      app/src/services/offline_simulation_service.cpp
  4. +6
    -3
      app/src/services/runtime_mode_service.cpp
  5. +131
    -18
      app/src/services/software_logic_executor.cpp
  6. +43
    -5
      app/src/services/software_logic_executor.h
  7. +62
    -0
      app/tests/logic_editor_service_tests.cpp
  8. +201
    -1
      app/tests/offline_simulation_service_tests.cpp
  9. +68
    -2
      app/tests/runtime_mode_service_tests.cpp

+ 46
- 5
app/src/services/logic_editor_service.cpp Zobrazit soubor

@@ -353,6 +353,13 @@ std::string LogicEditorService::rungIdForNode(
return {};
}

std::string LogicEditorService::registerCommentFor(
const RegisterAddress &address) const
{
const RegisterComment *comment = project_service_.project().findRegisterComment(address);
return comment == nullptr ? std::string{} : comment->text;
}

LogicEditorResult LogicEditorService::ensureDefaultLogic()
{
if (!project_service_.project().controlLogics.empty())
@@ -362,7 +369,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic()
ControlLogic logic;
logic.id = "logic-1";
logic.name = "控制逻辑 1";
logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, std::nullopt});
logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt, std::nullopt});
Project &project = project_service_.editProject();
project.controlLogics.push_back(std::move(logic));
return {true, LogicEditorError::None, {}, project.controlLogics.back().id};
@@ -386,7 +393,7 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name)
ControlLogic logic;
logic.id = makeUniqueLogicId(current);
logic.name = name;
logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, std::nullopt});
logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt, std::nullopt});
Project &project = project_service_.editProject();
project.controlLogics.push_back(std::move(logic));
return {true, LogicEditorError::None, {}, project.controlLogics.back().id};
@@ -539,6 +546,22 @@ LogicEditorResult LogicEditorService::removeRung(
return {true, LogicEditorError::None, {}, rung_id};
}

LogicEditorResult LogicEditorService::updateRungComment(
const std::string &logic_id,
const std::string &rung_id,
const std::string &comment)
{
const LadderRung *rung = findRung(logic_id, rung_id);
if (rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
LadderRung &editable = *findEditableRung(
project_service_.editProject(), logic_id, rung_id);
editable.comment = comment;
return {true, LogicEditorError::None, {}, rung_id};
}

LogicEditorResult LogicEditorService::appendCondition(
const std::string &logic_id,
const std::string &rung_id,
@@ -692,7 +715,9 @@ LogicEditorResult LogicEditorService::setOutput(
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config))
{
return failure(LogicEditorError::InvalidNode, "梯形图输出必须使用有效线圈");
return failure(
LogicEditorError::InvalidNode,
"梯形图输出必须使用有效线圈或 TON 指令");
}
const std::string node_id = existing_rung->output.has_value()
? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config));
@@ -793,12 +818,16 @@ LogicEditorResult LogicEditorService::removeExpression(

bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config)
{
return !std::holds_alternative<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)
{
return std::holds_alternative<CoilNodeConfig>(config);
return std::holds_alternative<CoilNodeConfig>(config)
|| std::holds_alternative<TonNodeConfig>(config);
}

std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config)
@@ -811,10 +840,22 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config)
{
return "contact";
}
else if constexpr (std::is_same_v<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>)
{
return "coil";
}
else if constexpr (std::is_same_v<Config, TonNodeConfig>)
{
return "ton";
}
return "compare";
},
config);


+ 5
- 0
app/src/services/logic_editor_service.h Zobrazit soubor

@@ -47,6 +47,7 @@ public:
std::string firstRungId(const std::string &logic_id) const;
std::string rungIdForNode(
const std::string &logic_id, const std::string &node_id) const;
std::string registerCommentFor(const RegisterAddress &address) const;

LogicEditorResult ensureDefaultLogic();
LogicEditorResult addLogic(const std::string &name);
@@ -58,6 +59,10 @@ public:
LogicEditorResult addRung(const std::string &logic_id);
LogicEditorResult removeRung(
const std::string &logic_id, const std::string &rung_id);
LogicEditorResult updateRungComment(
const std::string &logic_id,
const std::string &rung_id,
const std::string &comment);
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,


+ 3
- 0
app/src/services/offline_simulation_service.cpp Zobrazit soubor

@@ -31,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start(
}

timer_.stop();
executor_.resetRuntime();
repository_.clear();
logic_snapshot_ = std::move(snapshot);
successful_scan_count_ = 0;
@@ -45,6 +46,7 @@ SimulationStartResult OfflineSimulationService::start(
void OfflineSimulationService::stop()
{
timer_.stop();
executor_.resetRuntime();
logic_snapshot_.clear();
if (state_ == SimulationState::Stopped)
{
@@ -111,6 +113,7 @@ void OfflineSimulationService::handleTimeout()
void OfflineSimulationService::enterFault(const LogicScanResult &error)
{
timer_.stop();
executor_.resetRuntime();
last_error_ = error;
state_ = SimulationState::Faulted;
emit stateChanged();


+ 6
- 3
app/src/services/runtime_mode_service.cpp Zobrazit soubor

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


+ 131
- 18
app/src/services/software_logic_executor.cpp Zobrazit soubor

@@ -75,6 +75,7 @@ void LogicTraceValues::clear()
nodeValues.clear();
expressionValues.clear();
rungValues.clear();
tonValues.clear();
}

void LogicTraceSnapshot::clear()
@@ -93,6 +94,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic(
projection.nodeValues = values->second.nodeValues;
projection.expressionValues = values->second.expressionValues;
projection.rungValues = values->second.rungValues;
projection.tonValues = values->second.tonValues;
}
return projection;
}
@@ -131,9 +133,13 @@ LogicScanResult SoftwareLogicExecutor::validate(
const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config);
if (output == nullptr)
{
if (std::holds_alternative<TonNodeConfig>(rung.output->config))
{
continue;
}
return failure(
LogicScanError::InvalidLogic,
"梯形图输出不是线圈节点",
"梯形图输出不是线圈或 TON 指令",
logic.id,
rung.id,
rung.output->id);
@@ -152,13 +158,33 @@ LogicScanResult SoftwareLogicExecutor::validate(
output_modes[output->address.index()] = output->mode;
}
}
std::string timer_error;
if (!validateTimerReferencesForRunning(logics, &timer_error))
{
return failure(LogicScanError::InvalidLogic, timer_error);
}
return success();
}

void SoftwareLogicExecutor::resetRuntime()
{
previous_edge_inputs_.clear();
ton_states_.clear();
}

LogicScanResult SoftwareLogicExecutor::executeScan(
const std::vector<ControlLogic> &logics,
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);
if (!validation.succeeded)
@@ -187,7 +213,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan(

bool rung_value = false;
LogicScanResult result = evaluateExpression(
*rung.condition, repository, logic_trace, &rung_value);
logic.id, *rung.condition, repository, logic_trace, &rung_value);
if (!result.succeeded)
{
result.logicId = logic.id;
@@ -199,14 +225,24 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
logic_trace->rungValues[rung.id] = rung_value;
logic_trace->nodeValues[rung.output->id] = rung_value;
}
result = writeOutput(
*rung.output, rung_value, repository);
bool output_value = false;
result = executeOutput(
*rung.output,
rung_value,
repository,
now,
logic_trace,
&output_value);
if (!result.succeeded)
{
result.logicId = logic.id;
result.rungId = rung.id;
return result;
}
if (logic_trace != nullptr)
{
logic_trace->nodeValues[rung.output->id] = output_value;
}
}
}
if (trace != nullptr)
@@ -222,6 +258,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
trace->nodeValues = values->second.nodeValues;
trace->expressionValues = values->second.expressionValues;
trace->rungValues = values->second.rungValues;
trace->tonValues = values->second.tonValues;
}
}
}
@@ -229,10 +266,11 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
}

LogicScanResult SoftwareLogicExecutor::evaluateExpression(
const std::string &logic_id,
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceValues *trace,
bool *value) const
bool *value)
{
if (value == nullptr)
{
@@ -240,7 +278,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression(
}
if (expression.kind == ConditionExpressionKind::Node)
{
LogicScanResult result = evaluateCondition(*expression.node, repository, value);
LogicScanResult result = evaluateCondition(
logic_id, *expression.node, repository, value);
if (result.succeeded && trace != nullptr)
{
trace->nodeValues[expression.node->id] = *value;
@@ -253,7 +292,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression(
for (const ConditionExpression &child : expression.children)
{
bool child_value = false;
LogicScanResult result = evaluateExpression(child, repository, trace, &child_value);
LogicScanResult result = evaluateExpression(
logic_id, child, repository, trace, &child_value);
if (!result.succeeded)
{
return result;
@@ -270,9 +310,10 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression(
}

LogicScanResult SoftwareLogicExecutor::evaluateCondition(
const std::string &logic_id,
const LogicNode &node,
RegisterRepository &repository,
bool *value) const
bool *value)
{
if (value == nullptr)
{
@@ -285,7 +326,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
}

return std::visit(
[&repository, value, &node](const auto &config) -> LogicScanResult
[this, &logic_id, &repository, value, &node](
const auto &config) -> LogicScanResult
{
using Config = std::decay_t<decltype(config)>;
if constexpr (std::is_same_v<Config, ContactNodeConfig>)
@@ -304,6 +346,31 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
? read.value : !read.value;
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>)
{
const WordReadResult read = repository.readWord(config.address);
@@ -323,7 +390,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
{
return failure(
LogicScanError::InvalidLogic,
"条件节点不能包含输出线圈",
"条件节点不能包含输出节点",
{},
{},
node.id);
@@ -332,24 +399,68 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
node.config);
}

LogicScanResult SoftwareLogicExecutor::writeOutput(
LogicScanResult SoftwareLogicExecutor::executeOutput(
const LogicNode &node,
bool rung_value,
RegisterRepository &repository) const
RegisterRepository &repository,
TimePoint now,
LogicTraceValues *trace,
bool *output_value)
{
if (output_value == nullptr)
{
return failure(
LogicScanError::InvalidLogic,
"缺少输出指令结果接收对象",
{},
{},
node.id);
}
if (const auto *ton = std::get_if<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);
if (config == nullptr)
{
return failure(
LogicScanError::InvalidLogic,
"梯形图输出不是线圈节点",
"梯形图输出不是线圈或 TON 指令",
{},
{},
node.id);
}

bool should_write = true;
bool output_value = rung_value;
bool register_value = rung_value;
switch (config->mode)
{
case CoilMode::Normal:
@@ -359,13 +470,13 @@ LogicScanResult SoftwareLogicExecutor::writeOutput(
case CoilMode::Set:
{
should_write = rung_value;
output_value = true;
register_value = true;
break;
}
case CoilMode::Reset:
{
should_write = rung_value;
output_value = false;
register_value = false;
break;
}
default:
@@ -381,10 +492,11 @@ LogicScanResult SoftwareLogicExecutor::writeOutput(

if (!should_write)
{
*output_value = rung_value;
return success();
}
const RegisterWriteResult write = repository.writeBit(
config->address, output_value);
config->address, register_value);
if (!write.succeeded)
{
return failure(
@@ -394,5 +506,6 @@ LogicScanResult SoftwareLogicExecutor::writeOutput(
{},
node.id);
}
*output_value = rung_value;
return success();
}

+ 43
- 5
app/src/services/software_logic_executor.h Zobrazit soubor

@@ -3,8 +3,12 @@
#include "domain/control_logic_model.h"
#include "domain/register_repository.h"

#include <chrono>
#include <cstdint>
#include <map>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

enum class LogicScanError
@@ -26,11 +30,20 @@ struct LogicScanResult
std::string nodeId;
};

struct TonTraceValue
{
bool input = false;
bool done = false;
std::int64_t elapsedMs = 0;
int presetMs = 0;
};

struct LogicTraceValues
{
std::unordered_map<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;

void clear();
};
@@ -47,24 +60,49 @@ struct LogicTraceSnapshot : LogicTraceValues
class SoftwareLogicExecutor
{
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;

LogicScanResult validate(const std::vector<ControlLogic> &logics) const;
void resetRuntime();
LogicScanResult executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
LogicTraceSnapshot *trace = nullptr) const;
LogicTraceSnapshot *trace = nullptr);
LogicScanResult executeScanAt(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
TimePoint now,
LogicTraceSnapshot *trace = nullptr);

private:
struct TonRuntimeState
{
bool timing = false;
bool done = false;
TimePoint startedAt{};
std::chrono::milliseconds elapsed{0};
};

LogicScanResult evaluateCondition(
const std::string &logic_id,
const LogicNode &node,
RegisterRepository &repository,
bool *value) const;
bool *value);
LogicScanResult evaluateExpression(
const std::string &logic_id,
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceValues *trace,
bool *value) const;
LogicScanResult writeOutput(
bool *value);
LogicScanResult executeOutput(
const LogicNode &node,
bool rung_value,
RegisterRepository &repository) const;
RegisterRepository &repository,
TimePoint now,
LogicTraceValues *trace,
bool *output_value);

std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_;
std::unordered_map<int, TonRuntimeState> ton_states_;
};

+ 62
- 0
app/tests/logic_editor_service_tests.cpp Zobrazit soubor

@@ -165,6 +165,67 @@ void testLogicLifecycleAndOrdering()
"the project must retain at least one control logic module");
}

void testEdgeTimerNodesAndRungComments()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);

const LogicEditorResult rising = service.appendCondition(
logic_id,
rung_id,
EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
require(rising.succeeded && rising.id == "edge-1",
"the editor must create rising edge nodes with a stable prefix");
const LogicEditorResult timer = service.addParallelBranch(
logic_id,
rung_id,
{rising.id},
TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen});
require(timer.succeeded && timer.id == "timer-contact-1",
"the editor must insert T contacts as conditions");
const LogicEditorResult ton = service.setOutput(
logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500});
require(ton.succeeded && ton.id == "ton-1",
"the editor must create TON outputs with a stable prefix");

require(service.updateNodeConfig(
logic_id,
rising.id,
EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
.succeeded,
"the editor must apply edge mode and M address properties");
require(service.updateNodeConfig(
logic_id,
timer.id,
TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed})
.succeeded,
"the editor must apply T contact mode and address properties");
require(service.updateNodeConfig(
logic_id,
ton.id,
TonNodeConfig{TimerAddress{4}, 750})
.succeeded,
"the editor must apply TON preset properties");
require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
"the editor must update a network comment by stable rung id");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->comment == "延时启动网络"
&& std::get<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

int main()
@@ -174,6 +235,7 @@ int main()
testStructuredEditingAndNormalization();
testRangeParallelInsertion();
testLogicLifecycleAndOrdering();
testEdgeTimerNodesAndRungComments();
}
catch (const std::exception &error)
{


+ 201
- 1
app/tests/offline_simulation_service_tests.cpp Zobrazit soubor

@@ -28,6 +28,27 @@ LogicNode contact(const std::string &id, int address,
return {id, ContactNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
}

LogicNode edgeContact(const std::string &id, int address, EdgeMode mode)
{
return {id,
EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, address}, mode},
true};
}

LogicNode timerContact(
const std::string &id,
int timer_index,
ContactMode mode = ContactMode::NormallyOpen)
{
return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true};
}

LogicNode ton(const std::string &id, int timer_index, int preset_ms)
{
return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true};
}

LogicNode comparison(const std::string &id, int address,
ComparisonOperator operation, std::int16_t value)
{
@@ -260,11 +281,187 @@ void testMultipleLogicScanOrderAndTraceIsolation()
disabled_draft.name = "Draft";
disabled_draft.enabled = false;
disabled_draft.rungs.push_back(
{"rung-1", "Draft", std::nullopt, std::nullopt});
{"rung-1", "Draft", {}, std::nullopt, std::nullopt});
require(executor.validate({first, disabled_draft}).succeeded,
"a disabled incomplete logic module must not block offline execution");
}

void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic program = logic({
rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}},
coil("rising-output", 10)),
rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}},
coil("falling-output", 11))});

writeBit(repository, 0, true);
require(executor.executeScan({program}, repository).succeeded,
"the first high scan must evaluate rising edge input");
require(readBit(repository, 10), "rising edge must pulse on OFF to ON");
require(!readBit(repository, 11), "falling edge must stay off without ON to OFF");
require(executor.executeScan({program}, repository).succeeded,
"a repeated high scan must succeed");
require(!readBit(repository, 10), "rising edge must clear after one scan");

writeBit(repository, 1, true);
require(executor.executeScan({program}, repository).succeeded,
"the falling edge input must establish its previous high state");
writeBit(repository, 1, false);
require(executor.executeScan({program}, repository).succeeded,
"the first low scan after high must succeed");
require(readBit(repository, 11), "falling edge must pulse on ON to OFF");
require(executor.executeScan({program}, repository).succeeded,
"a repeated low scan must succeed");
require(!readBit(repository, 11), "falling edge must clear after one scan");

ControlLogic first = logic({
rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}},
coil("shared-output-first", 20))});
first.id = "logic-first";
first.name = "First";
ControlLogic second = logic({
rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}},
coil("shared-output-second", 21))});
second.id = "logic-second";
second.name = "Second";
writeBit(repository, 2, true);
require(executor.executeScan({first, second}, repository).succeeded,
"logic-scoped edge history must support duplicate node ids");
writeBit(repository, 3, true);
require(executor.executeScan({first, second}, repository).succeeded,
"each duplicate edge node must update its own history");
require(!readBit(repository, 20) && readBit(repository, 21),
"duplicate node ids in different logic modules must not share edge history");

executor.resetRuntime();
require(executor.executeScan({first, second}, repository).succeeded,
"resetting runtime must clear edge history");
require(readBit(repository, 20) && readBit(repository, 21),
"a restarted runtime must treat current ON inputs as fresh rising edges");
}

void testTonUsesElapsedTimeAndResetsAsNonRetentive()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic program = logic({
rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)),
rung("ton-feedback-rung", {{timerContact("timer-done", 0)}},
coil("ton-output", 30))});
const auto start = SoftwareLogicExecutor::TimePoint{};
LogicTraceSnapshot trace;

require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
"an inactive TON must scan successfully");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 0
&& !trace.tonValues.at("ton-0").done,
"an inactive TON must have Q false and ET zero");

writeBit(repository, 0, true);
require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
"the first active TON scan must start timing");
require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0,
"TON must not complete on its starting scan");
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{99}, &trace)
.succeeded,
"TON must use elapsed monotonic time before the preset");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 99,
"TON must remain false before PT");
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{100}, &trace)
.succeeded,
"TON must complete at its preset time");
require(readBit(repository, 30)
&& trace.tonValues.at("ton-0").done,
"the T contact must observe TON Q in a later network of the same scan");

writeBit(repository, 0, false);
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{150}, &trace)
.succeeded,
"disconnecting TON input must scan successfully");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 0
&& !trace.tonValues.at("ton-0").done,
"a non-retentive TON must reset ET and Q when input opens");

writeBit(repository, 0, true);
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{200}, &trace)
.succeeded,
"TON must restart timing after an input reset");
require(!readBit(repository, 30), "TON restart must not retain the old done state");

executor.resetRuntime();
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{1000}, &trace)
.succeeded,
"resetRuntime must clear TON state");
require(!readBit(repository, 30),
"a restarted runtime must start a currently active TON from zero");
}

void testMultipleTimersAndNetworkOrder()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic independent = logic({
rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)),
rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))});
writeBit(repository, 1, true);
writeBit(repository, 2, true);
const auto start = SoftwareLogicExecutor::TimePoint{};
LogicTraceSnapshot trace;
require(executor.executeScanAt({independent}, repository, start, &trace).succeeded,
"multiple TON resources must start independently");
require(executor.executeScanAt(
{independent}, repository,
start + std::chrono::milliseconds{100}, &trace)
.succeeded,
"the first independent TON must reach PT without completing the second");
require(trace.tonValues.at("ton-1").done
&& !trace.tonValues.at("ton-2").done,
"different T resources must retain independent elapsed time");

VirtualRegisterRepository ordered_repository;
SoftwareLogicExecutor ordered_executor;
const ControlLogic ordered = logic({
rung("feedback-before-ton", {{timerContact("early-timer", 4)}},
coil("early-output", 40)),
rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))});
writeBit(ordered_repository, 4, true);
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository, start, nullptr)
.succeeded,
"network order test must start TON after its earlier T contact");
require(!readBit(ordered_repository, 40),
"an earlier T contact must see the previous scan state");
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository,
start + std::chrono::milliseconds{100}, nullptr)
.succeeded,
"network order test must complete TON on the next scan");
require(!readBit(ordered_repository, 40),
"the earlier T contact must remain false on the completion scan");
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository,
start + std::chrono::milliseconds{101}, nullptr)
.succeeded,
"network order test must expose TON Q on the following scan");
require(readBit(ordered_repository, 40),
"a later scan must observe the completed TON through T contact");
}

void testSetResetPairOnSameAddress()
{
VirtualRegisterRepository repository;
@@ -428,6 +625,9 @@ int main(int argc, char *argv[])
testAllComparisons();
testSetResetAndDisabledLogic();
testMultipleLogicScanOrderAndTraceIsolation();
testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
testTonUsesElapsedTimeAndResetsAsNonRetentive();
testMultipleTimersAndNetworkOrder();
testSetResetPairOnSameAddress();
testConflictingCoilsAreRejected();
testHmiSimulationClosedLoop();


+ 68
- 2
app/tests/runtime_mode_service_tests.cpp Zobrazit soubor

@@ -123,6 +123,69 @@ void testModeTransitions()
{"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"});
project.alarmDefinitions.push_back(
{"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"});
project.registerComments = {
{RegisterAddress{RegisterArea::M, 3999}, "只用于说明"},
{RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}};

ControlLogic logic;
logic.id = "poll-logic";
logic.name = "Poll logic";
LogicNode edge;
edge.id = "poll-edge";
edge.config = EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising};
LogicNode edge_coil;
edge_coil.id = "poll-edge-coil";
edge_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal};
LadderRung edge_rung;
edge_rung.id = "poll-edge-rung";
edge_rung.name = "Poll edge";
edge_rung.condition = ConditionExpression::fromNode(edge);
edge_rung.output = edge_coil;
logic.rungs.push_back(edge_rung);
LogicNode comparison;
comparison.id = "poll-comparison";
comparison.config = CompareNodeConfig{
RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0};
LogicNode comparison_coil;
comparison_coil.id = "poll-comparison-coil";
comparison_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal};
LadderRung comparison_rung;
comparison_rung.id = "poll-comparison-rung";
comparison_rung.name = "Poll comparison";
comparison_rung.condition = ConditionExpression::fromNode(comparison);
comparison_rung.output = comparison_coil;
logic.rungs.push_back(comparison_rung);
LogicNode timer_input;
timer_input.id = "poll-timer-input";
timer_input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen};
LogicNode ton;
ton.id = "poll-ton";
ton.config = TonNodeConfig{TimerAddress{4}, 100};
LadderRung ton_rung;
ton_rung.id = "poll-ton-rung";
ton_rung.name = "Poll TON";
ton_rung.condition = ConditionExpression::fromNode(timer_input);
ton_rung.output = ton;
logic.rungs.push_back(ton_rung);
LogicNode timer_contact;
timer_contact.id = "poll-timer-contact";
timer_contact.config = TimerContactNodeConfig{
TimerAddress{4}, ContactMode::NormallyOpen};
LogicNode timer_coil;
timer_coil.id = "poll-timer-coil";
timer_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal};
LadderRung timer_rung;
timer_rung.id = "poll-timer-rung";
timer_rung.name = "Poll timer contact";
timer_rung.condition = ConditionExpression::fromNode(timer_contact);
timer_rung.output = timer_coil;
logic.rungs.push_back(timer_rung);
project.controlLogics.push_back(logic);

require(service.mode() == ApplicationMode::Editing,
"service must start in editing mode");
@@ -152,8 +215,11 @@ void testModeTransitions()
"PLC connection must be established before its initial read can complete");
require(gateway.pollAddresses()
== std::vector<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();
require(service.initialPlcReadCompleted(),
"service must retain the initial PLC read state");


Načítá se…
Zrušit
Uložit