diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index cff9ee6..574a304 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -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(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(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(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(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; } diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index c342714..f1b6656 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -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( - 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");