Bladeren bron

feat: 持久化梯形图表达式与数据点

main
suyu 1 maand geleden
bovenliggende
commit
f33f28450f
2 gewijzigde bestanden met toevoegingen van 184 en 62 verwijderingen
  1. +150
    -52
      app/src/infrastructure/json_project_storage.cpp
  2. +34
    -10
      app/tests/project_management_tests.cpp

+ 150
- 52
app/src/infrastructure/json_project_storage.cpp Bestand weergeven

@@ -253,6 +253,28 @@ bool parseAddress(
return true;
}

QJsonObject serializeDataPoint(const DataPoint &data_point)
{
QJsonObject object;
object.insert(QStringLiteral("address"), serializeAddress(data_point.address));
object.insert(QStringLiteral("name"), fromUtf8(data_point.name));
object.insert(QStringLiteral("comment"), fromUtf8(data_point.comment));
return object;
}

bool parseDataPoint(
const QJsonObject &object,
const std::string &context,
DataPoint *data_point,
ParseState *state)
{
QJsonObject address;
return readObject(object, "address", context, &address, state)
&& parseAddress(address, context + ".address", &data_point->address, state)
&& readString(object, "name", context, &data_point->name, state)
&& readString(object, "comment", context, &data_point->comment, state);
}

// 将 HMI 控件类型枚举转换为工程文件中的稳定字符串
QString hmiControlTypeName(HmiControlType type)
{
@@ -816,80 +838,124 @@ bool parseLogicNode(
{
QJsonObject config;
if (!readString(object, "id", context, &node->id, state)
|| !readBool(object, "configured", context, &node->configured, state)
|| !readObject(object, "config", context, &config, state)
|| !parseNodeConfig(config, context + ".config", &node->config, state))
{
return false;
}
// 1.0 旧工程没有 configured 字段,按历史默认地址视为已配置
node->configured = true;
if (object.contains(QStringLiteral("configured"))
&& !readBool(object, "configured", context, &node->configured, state))
{
return false;
}
return true;
}

QJsonObject serializeLadderStage(const LadderStage &stage)
QString expressionKindText(ConditionExpressionKind kind)
{
QJsonArray branches;
for (const LogicNode &node : stage.branches)
switch (kind)
{
branches.append(serializeLogicNode(node));
case ConditionExpressionKind::Node:
return QStringLiteral("node");
case ConditionExpressionKind::Series:
return QStringLiteral("series");
case ConditionExpressionKind::Parallel:
return QStringLiteral("parallel");
}
return {};
}

QJsonObject serializeConditionExpression(const ConditionExpression &expression)
{
QJsonObject object;
object.insert(QStringLiteral("id"), fromUtf8(stage.id));
object.insert(QStringLiteral("branches"), branches);
object.insert(QStringLiteral("id"), fromUtf8(expression.id));
object.insert(QStringLiteral("kind"), expressionKindText(expression.kind));
if (expression.kind == ConditionExpressionKind::Node)
{
object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node));
}
else
{
QJsonArray children;
for (const ConditionExpression &child : expression.children)
{
children.append(serializeConditionExpression(child));
}
object.insert(QStringLiteral("children"), children);
}
return object;
}

bool parseLadderStage(
bool parseConditionExpression(
const QJsonObject &object,
const std::string &context,
LadderStage *stage,
ConditionExpression *expression,
ParseState *state)
{
QJsonArray branches;
if (!readString(object, "id", context, &stage->id, state)
|| !readArray(object, "branches", context, &branches, state))
std::string kind;
if (!readString(object, "id", context, &expression->id, state)
|| !readString(object, "kind", context, &kind, state))
{
return false;
}
stage->branches.reserve(static_cast<std::size_t>(branches.size()));
for (int index = 0; index < branches.size(); ++index)
if (kind == "node")
{
if (!branches.at(index).isObject())
QJsonObject node;
if (!readObject(object, "node", context, &node, state))
{
return false;
}
LogicNode parsed_node;
if (!parseLogicNode(node, context + ".node", &parsed_node, state))
{
return false;
}
expression->kind = ConditionExpressionKind::Node;
expression->node = std::move(parsed_node);
return true;
}
if (kind != "series" && kind != "parallel")
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".kind must be node, series or parallel");
}
QJsonArray children;
if (!readArray(object, "children", context, &children, state))
{
return false;
}
expression->kind = kind == "series"
? ConditionExpressionKind::Series : ConditionExpressionKind::Parallel;
expression->children.reserve(static_cast<std::size_t>(children.size()));
for (int index = 0; index < children.size(); ++index)
{
if (!children.at(index).isObject())
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".branches items must be objects");
context + ".children items must be objects");
}
LogicNode node;
if (!parseLogicNode(
branches.at(index).toObject(),
context + ".branches[" + std::to_string(index) + ']',
&node,
ConditionExpression child;
if (!parseConditionExpression(
children.at(index).toObject(),
context + ".children[" + std::to_string(index) + ']',
&child,
state))
{
return false;
}
stage->branches.push_back(std::move(node));
expression->children.push_back(std::move(child));
}
return true;
}

QJsonObject serializeLadderRung(const LadderRung &rung)
{
QJsonArray stages;
for (const LadderStage &stage : rung.stages)
{
stages.append(serializeLadderStage(stage));
}
QJsonObject object;
object.insert(QStringLiteral("id"), fromUtf8(rung.id));
object.insert(QStringLiteral("name"), fromUtf8(rung.name));
object.insert(QStringLiteral("stages"), stages);
object.insert(
QStringLiteral("condition"),
rung.condition.has_value()
? QJsonValue(serializeConditionExpression(*rung.condition))
: QJsonValue(QJsonValue::Null));
object.insert(
QStringLiteral("output"),
rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output))
@@ -903,34 +969,37 @@ bool parseLadderRung(
LadderRung *rung,
ParseState *state)
{
QJsonArray stages;
QJsonValue condition;
QJsonValue output;
if (!readString(object, "id", context, &rung->id, state)
|| !readString(object, "name", context, &rung->name, state)
|| !readArray(object, "stages", context, &stages, state)
|| !readValue(object, "output", context, &output, state))
{
return false;
}
rung->stages.reserve(static_cast<std::size_t>(stages.size()));
for (int index = 0; index < stages.size(); ++index)
if (!readValue(object, "condition", context, &condition, state))
{
if (!stages.at(index).isObject())
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".stages items must be objects");
}
LadderStage stage;
if (!parseLadderStage(
stages.at(index).toObject(),
context + ".stages[" + std::to_string(index) + ']',
&stage,
state))
return false;
}
if (condition.isNull())
{
rung->condition.reset();
}
else if (!condition.isObject())
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".condition must be an object or null");
}
else
{
ConditionExpression parsed_condition;
if (!parseConditionExpression(
condition.toObject(), context + ".condition", &parsed_condition, state))
{
return false;
}
rung->stages.push_back(std::move(stage));
rung->condition = std::move(parsed_condition);
}
if (output.isNull())
{
@@ -1019,12 +1088,19 @@ QJsonObject serializeProject(const Project &project)
logics.append(serializeControlLogic(logic));
}

QJsonArray data_points;
for (const DataPoint &data_point : project.dataPoints)
{
data_points.append(serializeDataPoint(data_point));
}

QJsonObject object;
object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion));
object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id));
object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name));
object.insert(QStringLiteral("hmiPages"), pages);
object.insert(QStringLiteral("controlLogics"), logics);
object.insert(QStringLiteral("dataPoints"), data_points);
return object;
}

@@ -1034,6 +1110,7 @@ bool parseProject(
{
QJsonArray pages;
QJsonArray logics;
QJsonArray data_points;
if (!readString(
object,
"formatVersion",
@@ -1054,7 +1131,8 @@ bool parseProject(
if (!readString(object, "id", "project", &project->metadata.id, state)
|| !readString(object, "name", "project", &project->metadata.name, state)
|| !readArray(object, "hmiPages", "project", &pages, state)
|| !readArray(object, "controlLogics", "project", &logics, state))
|| !readArray(object, "controlLogics", "project", &logics, state)
|| !readArray(object, "dataPoints", "project", &data_points, state))
{
return false;
}
@@ -1101,6 +1179,26 @@ bool parseProject(
}
project->controlLogics.push_back(std::move(logic));
}
project->dataPoints.reserve(static_cast<std::size_t>(data_points.size()));
for (int index = 0; index < data_points.size(); ++index)
{
if (!data_points.at(index).isObject())
{
return state->fail(
ProjectStorageError::InvalidField,
"project.dataPoints items must be objects");
}
DataPoint data_point{RegisterAddress{RegisterArea::M, 0}, {}, {}};
if (!parseDataPoint(
data_points.at(index).toObject(),
"project.dataPoints[" + std::to_string(index) + ']',
&data_point,
state))
{
return false;
}
project->dataPoints.push_back(std::move(data_point));
}
return true;
}



+ 34
- 10
app/tests/project_management_tests.cpp Bestand weergeven

@@ -95,8 +95,19 @@ Project makeExampleProject()
LadderRung rung;
rung.id = "rung-1";
rung.name = "Network 1";
rung.stages.push_back({"stage-1", {contact, hold_contact}});
rung.stages.push_back({"stage-2", {compare}});
ConditionExpression parallel;
parallel.id = "parallel-start";
parallel.kind = ConditionExpressionKind::Parallel;
parallel.children = {
ConditionExpression::fromNode(contact),
ConditionExpression::fromNode(hold_contact)};
ConditionExpression series;
series.id = "series-root";
series.kind = ConditionExpressionKind::Series;
series.children = {
std::move(parallel),
ConditionExpression::fromNode(compare)};
rung.condition = std::move(series);
rung.output = coil;
logic.rungs.push_back(rung);

@@ -104,6 +115,9 @@ Project makeExampleProject()
project.metadata = {"example-project", "Example project", "1.0"};
project.hmiPages.push_back(page);
project.controlLogics.push_back(logic);
project.dataPoints = {
{RegisterAddress{RegisterArea::M, 0}, "StartCommand", "启动命令"},
{RegisterAddress{RegisterArea::D, 2}, "Temperature", "当前温度"}};
return project;
}

@@ -163,10 +177,14 @@ void testExampleProjectRoundTrip()
"example project save must succeed");
const QByteArray saved_json = readBytes(first_path);
require(saved_json.contains("\"rungs\"")
&& saved_json.contains("\"stages\"")
&& saved_json.contains("\"branches\"")
&& saved_json.contains("\"condition\"")
&& saved_json.contains("\"children\"")
&& saved_json.contains("\"dataPoints\"")
&& saved_json.contains("\"output\""),
"saved logic must use the ladder hierarchy");
"saved project must use structured ladder expressions and data points");
require(!saved_json.contains("\"stages\"")
&& !saved_json.contains("\"branches\""),
"current project format must not contain the removed stage model");
require(!saved_json.contains("\"position\"")
&& !saved_json.contains("\"connections\""),
"saved ladder logic must not contain free-graph fields");
@@ -194,17 +212,23 @@ void testExampleProjectRoundTrip()
require(!project.controlLogics.front().enabled,
"control logic enabled state must survive round trip");
const LadderRung &rung = project.controlLogics.front().rungs.front();
require(rung.stages.size() == 2,
"ladder stage count must survive round trip");
require(rung.stages.front().branches.size() == 2,
"parallel ladder branches must survive round trip");
require(rung.condition.has_value()
&& rung.condition->kind == ConditionExpressionKind::Series,
"series root expression must survive round trip");
require(rung.condition->children.front().kind
== ConditionExpressionKind::Parallel
&& rung.condition->children.front().children.size() == 2U,
"parallel expression branches must survive round trip");
require(rung.output.has_value(),
"ladder output must survive round trip");

const auto &compare = std::get<CompareNodeConfig>(
rung.stages.at(1).branches.front().config);
rung.condition->children.at(1).node->config);
require(compare.address.index() == 2 && compare.value == 100,
"comparison configuration must survive round trip");
require(project.dataPoints.size() == 2
&& project.dataPoints.front().name == "StartCommand",
"data points must survive round trip");

require(service.saveAs(second_path.toStdString()).succeeded,
"save as must succeed after load");


Laden…
Annuleren
Opslaan