Ver código fonte

feat: 支持多控制逻辑管理与执行

main
suyu 1 mês atrás
pai
commit
bcda701316
6 arquivos alterados com 293 adições e 10 exclusões
  1. +152
    -0
      app/src/services/logic_editor_service.cpp
  2. +9
    -1
      app/src/services/logic_editor_service.h
  3. +53
    -7
      app/src/services/software_logic_executor.cpp
  4. +10
    -2
      app/src/services/software_logic_executor.h
  5. +34
    -0
      app/tests/logic_editor_service_tests.cpp
  6. +35
    -0
      app/tests/offline_simulation_service_tests.cpp

+ 152
- 0
app/src/services/logic_editor_service.cpp Ver arquivo

@@ -3,6 +3,8 @@
#include "project_service.h"

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <iterator>
#include <unordered_set>
#include <type_traits>
@@ -10,6 +12,31 @@

namespace {

bool isBlank(const std::string &value)
{
return value.empty()
|| std::all_of(
value.cbegin(), value.cend(),
[](unsigned char character) { return std::isspace(character) != 0; });
}

std::string makeUniqueLogicId(const Project &project)
{
int suffix = 1;
while (true)
{
const std::string candidate = "logic-" + std::to_string(suffix);
const bool found = std::any_of(
project.controlLogics.cbegin(), project.controlLogics.cend(),
[&candidate](const ControlLogic &logic) { return logic.id == candidate; });
if (!found)
{
return candidate;
}
++suffix;
}
}

LogicNode makeNode(const std::string &id, const LogicNodeConfig &config)
{
return {id, config, false};
@@ -341,6 +368,131 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic()
return {true, LogicEditorError::None, {}, project.controlLogics.back().id};
}

LogicEditorResult LogicEditorService::addLogic(const std::string &name)
{
if (isBlank(name))
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空");
}
const Project &current = project_service_.project();
const bool duplicate = std::any_of(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&name](const ControlLogic &logic) { return logic.name == name; });
if (duplicate)
{
return failure(LogicEditorError::DuplicateName, "控制逻辑名称必须唯一");
}

ControlLogic logic;
logic.id = makeUniqueLogicId(current);
logic.name = name;
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};
}

LogicEditorResult LogicEditorService::renameLogic(
const std::string &logic_id, const std::string &name)
{
if (isBlank(name))
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑名称不能为空");
}
const Project &current = project_service_.project();
const auto logic = std::find_if(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
if (logic == current.controlLogics.cend())
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
const bool duplicate = std::any_of(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&logic_id, &name](const ControlLogic &candidate)
{
return candidate.id != logic_id && candidate.name == name;
});
if (duplicate)
{
return failure(LogicEditorError::DuplicateName, "控制逻辑名称必须唯一");
}

Project &project = project_service_.editProject();
auto editable = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
editable->name = name;
return {true, LogicEditorError::None, {}, logic_id};
}

LogicEditorResult LogicEditorService::removeLogic(const std::string &logic_id)
{
const Project &current = project_service_.project();
const auto logic = std::find_if(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
if (logic == current.controlLogics.cend())
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (current.controlLogics.size() <= 1)
{
return failure(LogicEditorError::LastLogicRequired, "工程至少需要保留一个控制逻辑");
}
Project &project = project_service_.editProject();
project.controlLogics.erase(
std::remove_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }),
project.controlLogics.end());
return {true, LogicEditorError::None, {}, logic_id};
}

LogicEditorResult LogicEditorService::moveLogic(const std::string &logic_id, int offset)
{
if (offset != -1 && offset != 1)
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑每次只能上移或下移一位");
}
const Project &current = project_service_.project();
const auto logic = std::find_if(
current.controlLogics.cbegin(), current.controlLogics.cend(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
if (logic == current.controlLogics.cend())
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
const auto index = std::distance(current.controlLogics.cbegin(), logic);
const std::ptrdiff_t target_index = index + offset;
if (target_index < 0
|| target_index
>= static_cast<std::ptrdiff_t>(current.controlLogics.size()))
{
return failure(LogicEditorError::InvalidOperation, "控制逻辑已经位于目标边界");
}
Project &project = project_service_.editProject();
std::iter_swap(
project.controlLogics.begin() + index,
project.controlLogics.begin() + target_index);
return {true, LogicEditorError::None, {}, logic_id};
}

LogicEditorResult LogicEditorService::setLogicEnabled(
const std::string &logic_id, bool enabled)
{
if (findLogic(logic_id) == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
Project &project = project_service_.editProject();
auto logic = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
logic->enabled = enabled;
return {true, LogicEditorError::None, {}, logic_id};
}

LogicEditorResult LogicEditorService::addRung(const std::string &logic_id)
{
const ControlLogic *logic = findLogic(logic_id);


+ 9
- 1
app/src/services/logic_editor_service.h Ver arquivo

@@ -16,7 +16,9 @@ enum class LogicEditorError
NodeNotFound,
InvalidNode,
InvalidOperation,
UnsupportedNodeChange
UnsupportedNodeChange,
DuplicateName,
LastLogicRequired
};

struct LogicEditorResult
@@ -47,6 +49,12 @@ public:
const std::string &logic_id, const std::string &node_id) const;

LogicEditorResult ensureDefaultLogic();
LogicEditorResult addLogic(const std::string &name);
LogicEditorResult renameLogic(
const std::string &logic_id, const std::string &name);
LogicEditorResult removeLogic(const std::string &logic_id);
LogicEditorResult moveLogic(const std::string &logic_id, int offset);
LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled);
LogicEditorResult addRung(const std::string &logic_id);
LogicEditorResult removeRung(
const std::string &logic_id, const std::string &rung_id);


+ 53
- 7
app/src/services/software_logic_executor.cpp Ver arquivo

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

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

@@ -69,13 +70,33 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current)

} // namespace

void LogicTraceSnapshot::clear()
void LogicTraceValues::clear()
{
nodeValues.clear();
expressionValues.clear();
rungValues.clear();
}

void LogicTraceSnapshot::clear()
{
LogicTraceValues::clear();
logicValues.clear();
}

LogicTraceSnapshot LogicTraceSnapshot::forLogic(
const std::string &logic_id) const
{
LogicTraceSnapshot projection;
const auto values = logicValues.find(logic_id);
if (values != logicValues.cend())
{
projection.nodeValues = values->second.nodeValues;
projection.expressionValues = values->second.expressionValues;
projection.rungValues = values->second.rungValues;
}
return projection;
}

LogicScanResult SoftwareLogicExecutor::validate(
const std::vector<ControlLogic> &logics) const
{
@@ -83,7 +104,7 @@ LogicScanResult SoftwareLogicExecutor::validate(
for (const ControlLogic &logic : logics)
{
std::string validation_error;
if (!logic.validateForRunning(&validation_error))
if (!logic.validateStructure(&validation_error))
{
return failure(
LogicScanError::InvalidLogic,
@@ -94,6 +115,13 @@ LogicScanResult SoftwareLogicExecutor::validate(
{
continue;
}
if (!logic.validateForRunning(&validation_error))
{
return failure(
LogicScanError::InvalidLogic,
validation_error,
logic.id);
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.output.has_value())
@@ -148,6 +176,8 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
{
continue;
}
LogicTraceValues *logic_trace = trace == nullptr
? nullptr : &trace->logicValues[logic.id];
for (const LadderRung &rung : logic.rungs)
{
if (!rung.condition.has_value() && !rung.output.has_value())
@@ -157,17 +187,17 @@ LogicScanResult SoftwareLogicExecutor::executeScan(

bool rung_value = false;
LogicScanResult result = evaluateExpression(
*rung.condition, repository, trace, &rung_value);
*rung.condition, repository, logic_trace, &rung_value);
if (!result.succeeded)
{
result.logicId = logic.id;
result.rungId = rung.id;
return result;
}
if (trace != nullptr)
if (logic_trace != nullptr)
{
trace->rungValues[rung.id] = rung_value;
trace->nodeValues[rung.output->id] = rung_value;
logic_trace->rungValues[rung.id] = rung_value;
logic_trace->nodeValues[rung.output->id] = rung_value;
}
result = writeOutput(
*rung.output, rung_value, repository);
@@ -179,13 +209,29 @@ LogicScanResult SoftwareLogicExecutor::executeScan(
}
}
}
if (trace != nullptr)
{
const auto first_enabled = std::find_if(
logics.cbegin(), logics.cend(),
[](const ControlLogic &logic) { return logic.enabled; });
if (first_enabled != logics.cend())
{
const auto values = trace->logicValues.find(first_enabled->id);
if (values != trace->logicValues.cend())
{
trace->nodeValues = values->second.nodeValues;
trace->expressionValues = values->second.expressionValues;
trace->rungValues = values->second.rungValues;
}
}
}
return success();
}

LogicScanResult SoftwareLogicExecutor::evaluateExpression(
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceSnapshot *trace,
LogicTraceValues *trace,
bool *value) const
{
if (value == nullptr)


+ 10
- 2
app/src/services/software_logic_executor.h Ver arquivo

@@ -26,7 +26,7 @@ struct LogicScanResult
std::string nodeId;
};

struct LogicTraceSnapshot
struct LogicTraceValues
{
std::unordered_map<std::string, bool> nodeValues;
std::unordered_map<std::string, bool> expressionValues;
@@ -35,6 +35,14 @@ struct LogicTraceSnapshot
void clear();
};

struct LogicTraceSnapshot : LogicTraceValues
{
std::unordered_map<std::string, LogicTraceValues> logicValues;

void clear();
LogicTraceSnapshot forLogic(const std::string &logic_id) const;
};

// 按工程顺序执行受限梯形图的一次确定性扫描
class SoftwareLogicExecutor
{
@@ -53,7 +61,7 @@ private:
LogicScanResult evaluateExpression(
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceSnapshot *trace,
LogicTraceValues *trace,
bool *value) const;
LogicScanResult writeOutput(
const LogicNode &node,


+ 34
- 0
app/tests/logic_editor_service_tests.cpp Ver arquivo

@@ -132,6 +132,39 @@ void testRangeParallelInsertion()
"a non-contiguous selection must be rejected");
}

void testLogicLifecycleAndOrdering()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string first_id = service.ensureDefaultLogic().id;
const LogicEditorResult second = service.addLogic("Safety logic");
const LogicEditorResult third = service.addLogic("Alarm logic");
require(second.succeeded && third.succeeded,
"multiple control logic modules must be creatable");
require(service.renameLogic(second.id, "Interlock logic").succeeded,
"control logic modules must be renamable by stable id");
require(service.renameLogic(third.id, "Interlock logic").error
== LogicEditorError::DuplicateName,
"control logic names must remain unique");
require(service.moveLogic(third.id, -1).succeeded
&& project_service.project().controlLogics.at(1).id == third.id,
"logic scan order must follow editable vector order");
require(service.setLogicEnabled(second.id, false).succeeded
&& !service.findLogic(second.id)->enabled,
"a control logic module must support explicit disable and enable");
require(service.setLogicEnabled(second.id, true).succeeded
&& service.findLogic(second.id)->enabled,
"a disabled control logic module must be re-enableable");
require(service.removeLogic(third.id).succeeded,
"a non-final control logic module must be deletable");
require(service.removeLogic(second.id).succeeded,
"logic deletion must preserve the remaining module");
require(service.removeLogic(first_id).error
== LogicEditorError::LastLogicRequired,
"the project must retain at least one control logic module");
}

} // namespace

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


+ 35
- 0
app/tests/offline_simulation_service_tests.cpp Ver arquivo

@@ -231,6 +231,40 @@ void testSetResetAndDisabledLogic()
require(readBit(repository, 5), "disabled logic must not change outputs");
}

void testMultipleLogicScanOrderAndTraceIsolation()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
ControlLogic first = logic({
rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
first.id = "logic-first";
first.name = "First";
ControlLogic second = logic({
rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
second.id = "logic-second";
second.name = "Second";

writeBit(repository, 0, true);
LogicTraceSnapshot trace;
require(executor.executeScan({first, second}, repository, &trace).succeeded,
"all enabled logic modules must execute in project order");
require(readBit(repository, 1) && readBit(repository, 2),
"a later logic module must observe an earlier module write in one scan");
require(trace.logicValues.size() == 2U
&& trace.forLogic(first.id).rungValues.at("rung-1")
&& trace.forLogic(second.id).rungValues.at("rung-1"),
"runtime traces must be partitioned by logic id when node ids repeat");

ControlLogic disabled_draft;
disabled_draft.id = "logic-draft";
disabled_draft.name = "Draft";
disabled_draft.enabled = false;
disabled_draft.rungs.push_back(
{"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 testSetResetPairOnSameAddress()
{
VirtualRegisterRepository repository;
@@ -393,6 +427,7 @@ int main(int argc, char *argv[])
testNestedSeriesParallelExpression();
testAllComparisons();
testSetResetAndDisabledLogic();
testMultipleLogicScanOrderAndTraceIsolation();
testSetResetPairOnSameAddress();
testConflictingCoilsAreRejected();
testHmiSimulationClosedLoop();


Carregando…
Cancelar
Salvar