#include "json_project_storage.h" #include "domain/hmi_control_registry.h" #include "domain/project_limits.h" #include #include #include #include #include #include #include #include #include #include namespace { // 当前读写实现支持的工程文件格式版本 constexpr const char *kCurrentFormatVersion = "3.0"; // 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因 struct ParseState { ProjectStorageError error = ProjectStorageError::None; std::string message; // 记录首次解析错误并返回 false,便于解析函数直接向上传播失败 bool fail(ProjectStorageError new_error, const std::string &new_message) { if (error == ProjectStorageError::None) { error = new_error; message = new_message; } return false; } }; // 将领域层使用的 UTF-8 字符串转换为 Qt 字符串 QString fromUtf8(const std::string &value) { return QString::fromUtf8(value.data(), static_cast(value.size())); } // 将 Qt 字符串转换为领域层使用的 UTF-8 字符串 std::string toUtf8(const QString &value) { const QByteArray bytes = value.toUtf8(); return std::string(bytes.constData(), static_cast(bytes.size())); } // 拼接带上下文的字段路径,用于生成可定位的错误信息 std::string fieldPath(const std::string &context, const char *field) { return context + '.' + field; } // 读取必填 JSON 字段,字段不存在时记录 MissingField 错误 bool readValue( const QJsonObject &object, const char *field, const std::string &context, QJsonValue *value, ParseState *state) { const QString key = QString::fromLatin1(field); if (!object.contains(key)) { return state->fail( ProjectStorageError::MissingField, "缺少必填字段:" + fieldPath(context, field)); } *value = object.value(key); return true; } // 读取必填字符串字段并转换为 UTF-8 标准字符串 bool readString( const QJsonObject &object, const char *field, const std::string &context, std::string *value, ParseState *state, std::size_t maximum_bytes = ProjectLimits::kMaximumTextBytes) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isString()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是字符串"); } *value = toUtf8(json_value.toString()); if (value->size() > maximum_bytes) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 超出允许的 UTF-8 字节长度"); } return true; } // 读取必填布尔字段并校验 JSON 类型 bool readBool( const QJsonObject &object, const char *field, const std::string &context, bool *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isBool()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是布尔值"); } *value = json_value.toBool(); return true; } // 读取指定闭区间内的整数,拒绝小数、非有限值和越界值 bool readInt( const QJsonObject &object, const char *field, const std::string &context, int minimum, int maximum, int *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isDouble()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是整数"); } // Qt JSON 统一用 double 表示数值,需要额外确认它能无损转换为 int const double number = json_value.toDouble(); if (!std::isfinite(number) || std::floor(number) != number || number < minimum || number > maximum) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 超出支持的整数范围"); } *value = static_cast(number); return true; } // 读取必填对象字段并校验 JSON 类型 bool readObject( const QJsonObject &object, const char *field, const std::string &context, QJsonObject *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isObject()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是对象"); } *value = json_value.toObject(); return true; } // 读取必填数组字段并校验 JSON 类型 bool readArray( const QJsonObject &object, const char *field, const std::string &context, QJsonArray *value, ParseState *state, int maximum_count = std::numeric_limits::max()) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isArray()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是数组"); } *value = json_value.toArray(); if (value->size() > maximum_count) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 的元素数量为 " + std::to_string(value->size()) + ",当前上限为 " + std::to_string(maximum_count)); } return true; } // 将寄存器地址序列化为包含区域和原始索引的 JSON 对象 QJsonObject serializeAddress(const RegisterAddress &address) { QJsonObject object; object.insert(QStringLiteral("area"), address.area() == RegisterArea::M ? QStringLiteral("M") : QStringLiteral("D")); object.insert(QStringLiteral("index"), address.index()); return object; } // 解析寄存器地址并校验区域名称及项目允许的索引范围 bool parseAddress( const QJsonObject &object, const std::string &context, RegisterAddress *address, ParseState *state) { std::string area_text; int index = 0; if (!readString(object, "area", context, &area_text, state) || !readInt( object, "index", context, RegisterAddress::kMinimumIndex, RegisterAddress::kMaximumIndex, &index, state)) { return false; } RegisterArea area = RegisterArea::M; if (area_text == "M") { area = RegisterArea::M; } else if (area_text == "D") { area = RegisterArea::D; } else { return state->fail( ProjectStorageError::InvalidField, context + ".area 必须是 M 或 D"); } *address = RegisterAddress{area, index}; return true; } QString wordOperandKindName(WordOperandKind kind) { return kind == WordOperandKind::Constant ? QStringLiteral("constant") : QStringLiteral("register"); } QJsonObject serializeWordOperand(const WordOperand &operand) { QJsonObject object; object.insert(QStringLiteral("kind"), wordOperandKindName(operand.kind)); if (operand.kind == WordOperandKind::Constant) { object.insert(QStringLiteral("value"), operand.constant); } else { object.insert(QStringLiteral("address"), serializeAddress(operand.address)); } return object; } bool parseWordOperand( const QJsonObject &object, const std::string &context, WordOperand *operand, ParseState *state) { std::string kind; if (!readString(object, "kind", context, &kind, state)) { return false; } if (kind == "constant") { int value = 0; if (!readInt( object, "value", context, std::numeric_limits::min(), std::numeric_limits::max(), &value, state)) { return false; } *operand = WordOperand{ WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, static_cast(value)}; return true; } if (kind != "register") { return state->fail( ProjectStorageError::InvalidField, context + ".kind 必须是 constant 或 register"); } QJsonObject address_object; RegisterAddress address{RegisterArea::D, 0}; if (!readObject(object, "address", context, &address_object, state) || !parseAddress(address_object, context + ".address", &address, state)) { return false; } if (address.area() != RegisterArea::D) { return state->fail( ProjectStorageError::InvalidField, context + ".address 必须使用 D 区地址"); } *operand = WordOperand{ WordOperandKind::Register, address, 0}; return true; } // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串 QString hmiControlTypeName(HmiControlType type) { const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); return descriptor == nullptr ? QString{} : QString::fromLatin1(descriptor->storageName); } // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举 bool parseHmiControlType( const std::string &value, HmiControlType *type, ParseState *state) { const HmiControlDescriptor *descriptor = findHmiControlDescriptor(value); if (descriptor == nullptr) { return state->fail( ProjectStorageError::InvalidField, "不支持的 HMI 控件类型:" + value); } *type = descriptor->type; return true; } QString hmiButtonOperationName(HmiButtonOperation operation) { switch (operation) { case HmiButtonOperation::SetOn: { return QStringLiteral("setOn"); } case HmiButtonOperation::SetOff: { return QStringLiteral("setOff"); } case HmiButtonOperation::Toggle: { return QStringLiteral("toggle"); } case HmiButtonOperation::MomentaryOn: default: { return QStringLiteral("momentaryOn"); } } } bool parseHmiButtonOperation( const std::string &value, HmiButtonOperation *operation, ParseState *state) { if (value == "setOn") { *operation = HmiButtonOperation::SetOn; } else if (value == "setOff") { *operation = HmiButtonOperation::SetOff; } else if (value == "toggle") { *operation = HmiButtonOperation::Toggle; } else if (value == "momentaryOn") { *operation = HmiButtonOperation::MomentaryOn; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的 HMI 按钮操作:" + value); } return true; } QString alarmConditionName(AlarmCondition condition) { switch (condition) { case AlarmCondition::MOn: { return QStringLiteral("mOn"); } case AlarmCondition::MOff: { return QStringLiteral("mOff"); } case AlarmCondition::DHigh: { return QStringLiteral("dHigh"); } case AlarmCondition::DLow: { return QStringLiteral("dLow"); } default: { return {}; } } } bool parseAlarmCondition( const std::string &value, AlarmCondition *condition, ParseState *state) { if (value == "mOn") { *condition = AlarmCondition::MOn; } else if (value == "mOff") { *condition = AlarmCondition::MOff; } else if (value == "dHigh") { *condition = AlarmCondition::DHigh; } else if (value == "dLow") { *condition = AlarmCondition::DLow; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的报警触发条件:" + value); } return true; } QJsonObject serializeAlarmDefinition(const AlarmDefinition &definition) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(definition.id)); object.insert(QStringLiteral("address"), serializeAddress(definition.address)); object.insert( QStringLiteral("condition"), alarmConditionName(definition.condition)); object.insert(QStringLiteral("threshold"), definition.threshold); object.insert(QStringLiteral("message"), fromUtf8(definition.message)); return object; } bool parseAlarmDefinition( const QJsonObject &object, const std::string &context, AlarmDefinition *definition, ParseState *state) { QJsonObject address; std::string condition; int threshold = 0; if (!readString( object, "id", context, &definition->id, state, ProjectLimits::kMaximumIdBytes) || !readObject(object, "address", context, &address, state) || !readString(object, "condition", context, &condition, state) || !readInt( object, "threshold", context, std::numeric_limits::min(), std::numeric_limits::max(), &threshold, state) || !readString(object, "message", context, &definition->message, state)) { return false; } if (!parseAddress(address, context + ".address", &definition->address, state) || !parseAlarmCondition(condition, &definition->condition, state)) { return false; } definition->threshold = static_cast(threshold); return true; } QJsonObject serializeRegisterComment(const RegisterComment &comment) { QJsonObject object; object.insert(QStringLiteral("address"), serializeAddress(comment.address)); object.insert(QStringLiteral("text"), fromUtf8(comment.text)); return object; } bool parseRegisterComment( const QJsonObject &object, const std::string &context, RegisterComment *comment, ParseState *state) { QJsonObject address; if (!readObject(object, "address", context, &address, state) || !readString(object, "text", context, &comment->text, state)) { return false; } return parseAddress(address, context + ".address", &comment->address, state); } // 将 HMI 控件矩形区域序列化为 JSON 对象 QJsonObject serializeBounds(const HmiRect &bounds) { QJsonObject object; object.insert(QStringLiteral("x"), bounds.x); object.insert(QStringLiteral("y"), bounds.y); object.insert(QStringLiteral("width"), bounds.width); object.insert(QStringLiteral("height"), bounds.height); return object; } // 解析控件矩形区域,允许任意坐标但要求宽高为正数 bool parseBounds( const QJsonObject &object, const std::string &context, HmiRect *bounds, ParseState *state) { return readInt( object, "x", context, 0, ProjectLimits::kMaximumHmiPageWidth, &bounds->x, state) && readInt( object, "y", context, 0, ProjectLimits::kMaximumHmiPageHeight, &bounds->y, state) && readInt( object, "width", context, 1, ProjectLimits::kMaximumHmiControlWidth, &bounds->width, state) && readInt( object, "height", context, 1, ProjectLimits::kMaximumHmiControlHeight, &bounds->height, state); } // 将 HMI 控件的字符串扩展属性序列化为 JSON 对象 QJsonObject serializeProperties(const std::map &properties) { QJsonObject object; for (const auto &property : properties) { object.insert(fromUtf8(property.first), fromUtf8(property.second)); } return object; } // 解析 HMI 控件扩展属性,并确保所有属性值都是字符串 bool parseProperties( const QJsonObject &object, std::map *properties, ParseState *state) { if (object.size() > static_cast(ProjectLimits::kMaximumHmiProperties)) { return state->fail( ProjectStorageError::InvalidField, "单个 HMI 控件最多保存 64 对扩展属性"); } for (auto current = object.constBegin(); current != object.constEnd(); ++current) { if (!current.value().isString()) { return state->fail( ProjectStorageError::InvalidField, "HMI 控件属性值必须是字符串"); } const std::string key = toUtf8(current.key()); const std::string value = toUtf8(current.value().toString()); if (key.empty() || key.size() > ProjectLimits::kMaximumPropertyKeyBytes || value.size() > ProjectLimits::kMaximumPropertyValueBytes) { return state->fail( ProjectStorageError::InvalidField, "HMI 控件属性名称最多 128 字节,属性值最多 1024 字节"); } properties->emplace(key, value); } return true; } QJsonValue serializeOptionalNumber(const std::optional &value) { return value.has_value() ? QJsonValue(*value) : QJsonValue(QJsonValue::Null); } QJsonObject serializeStatusTextConfig(const HmiStatusTextConfig &config) { QJsonObject object; if (const auto *bit = std::get_if(&config)) { object.insert(QStringLiteral("source"), QStringLiteral("m")); object.insert(QStringLiteral("offText"), fromUtf8(bit->offText)); object.insert(QStringLiteral("onText"), fromUtf8(bit->onText)); return object; } object.insert(QStringLiteral("source"), QStringLiteral("d")); QJsonArray ranges; for (const HmiStatusValueRange &range : std::get(config).ranges) { QJsonObject range_object; range_object.insert( QStringLiteral("lower"), serializeOptionalNumber(range.lowerBound)); range_object.insert( QStringLiteral("upper"), serializeOptionalNumber(range.upperBound)); range_object.insert(QStringLiteral("text"), fromUtf8(range.text)); ranges.append(range_object); } object.insert(QStringLiteral("ranges"), ranges); return object; } bool readNullableFiniteNumber( const QJsonObject &object, const char *field, const std::string &context, std::optional *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (json_value.isNull()) { value->reset(); return true; } if (!json_value.isDouble() || !std::isfinite(json_value.toDouble())) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " 必须是有限数值或 null"); } *value = json_value.toDouble(); return true; } bool parseStatusTextConfig( const QJsonObject &object, const std::string &context, HmiStatusTextConfig *config, ParseState *state) { std::string source; if (!readString(object, "source", context, &source, state)) { return false; } if (source == "m") { HmiStatusBitTextConfig bit; if (!readString(object, "offText", context, &bit.offText, state) || !readString(object, "onText", context, &bit.onText, state)) { return false; } *config = std::move(bit); return true; } if (source != "d") { return state->fail( ProjectStorageError::InvalidField, context + ".source 必须是 m 或 d"); } QJsonArray range_array; if (!readArray(object, "ranges", context, &range_array, state) || range_array.isEmpty() || range_array.size() > static_cast(ProjectLimits::kMaximumStatusTextRanges)) { return state->fail( ProjectStorageError::InvalidField, context + ".ranges 必须包含 1~16 个区间"); } HmiStatusWordTextConfig word; word.ranges.reserve(static_cast(range_array.size())); for (int index = 0; index < range_array.size(); ++index) { if (!range_array.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".ranges 的元素必须是对象"); } const QJsonObject range_object = range_array.at(index).toObject(); const std::string range_context = context + ".ranges[" + std::to_string(index) + ']'; HmiStatusValueRange range; if (!readNullableFiniteNumber( range_object, "lower", range_context, &range.lowerBound, state) || !readNullableFiniteNumber( range_object, "upper", range_context, &range.upperBound, state) || !readString( range_object, "text", range_context, &range.text, state)) { return false; } word.ranges.push_back(std::move(range)); } *config = std::move(word); return true; } // 将单个 HMI 控件及其可选寄存器绑定序列化为 JSON 对象 QJsonObject serializeHmiControl(const HmiControl &control) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(control.id)); object.insert(QStringLiteral("type"), hmiControlTypeName(control.type)); object.insert(QStringLiteral("bounds"), serializeBounds(control.bounds)); object.insert(QStringLiteral("text"), fromUtf8(control.text)); // 使用 JSON null 明确表示控件没有寄存器绑定 if (control.binding.has_value()) { object.insert(QStringLiteral("binding"), serializeAddress(*control.binding)); } else { object.insert(QStringLiteral("binding"), QJsonValue::Null); } object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); const bool status_word = control.type == HmiControlType::StatusText && control.statusText.has_value() && std::holds_alternative(*control.statusText); if (control.type == HmiControlType::NumericDisplay || control.type == HmiControlType::NumericInput || status_word) { object.insert( QStringLiteral("dataType"), QString::fromLatin1(registerDataTypeName(control.dataType))); } if (control.type == HmiControlType::Button) { object.insert( QStringLiteral("buttonOperation"), hmiButtonOperationName(control.buttonOperation)); } if (control.type == HmiControlType::PageJump) { object.insert( QStringLiteral("targetPageId"), fromUtf8(control.pageJump.has_value() ? control.pageJump->targetPageId : std::string{})); } if (control.type == HmiControlType::StatusText) { object.insert( QStringLiteral("statusText"), control.statusText.has_value() ? serializeStatusTextConfig(*control.statusText) : QJsonObject{}); } return object; } // 解析单个 HMI 控件,并逐层校验类型、区域、绑定和扩展属性 bool parseHmiControl( const QJsonObject &object, const std::string &context, HmiControl *control, ParseState *state) { std::string type_text; QJsonObject bounds; QJsonObject properties; QJsonValue binding; if (!readString( object, "id", context, &control->id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "type", context, &type_text, state) || !readObject(object, "bounds", context, &bounds, state) || !readString(object, "text", context, &control->text, state) || !readValue(object, "binding", context, &binding, state) || !readObject(object, "properties", context, &properties, state)) { return false; } if (!parseHmiControlType(type_text, &control->type, state) || !parseBounds(bounds, context + ".bounds", &control->bounds, state) || !parseProperties(properties, &control->properties, state)) { return false; } if (control->type == HmiControlType::StatusText) { QJsonObject status_object; HmiStatusTextConfig status_config = HmiStatusBitTextConfig{}; if (!readObject(object, "statusText", context, &status_object, state) || !parseStatusTextConfig( status_object, context + ".statusText", &status_config, state)) { return false; } control->statusText = std::move(status_config); } const bool status_word = control->type == HmiControlType::StatusText && control->statusText.has_value() && std::holds_alternative(*control->statusText); if (control->type == HmiControlType::NumericDisplay || control->type == HmiControlType::NumericInput || status_word) { std::string data_type_text; if (!readString(object, "dataType", context, &data_type_text, state) || !parseRegisterDataType(data_type_text, &control->dataType)) { return state->fail( ProjectStorageError::InvalidField, context + ".dataType 必须是 int16、int32、float32 或 float64"); } } if (control->type == HmiControlType::Button) { std::string operation_text; if (!readString( object, "buttonOperation", context, &operation_text, state) || !parseHmiButtonOperation( operation_text, &control->buttonOperation, state)) { return false; } } if (control->type == HmiControlType::PageJump) { std::string target_page_id; if (!readString( object, "targetPageId", context, &target_page_id, state, ProjectLimits::kMaximumIdBytes)) { return false; } control->pageJump = HmiPageJumpConfig{std::move(target_page_id)}; } // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 if (binding.isNull()) { control->binding.reset(); return true; } if (!binding.isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".binding 必须是对象或 null"); } RegisterAddress address{RegisterArea::M, 0}; if (!parseAddress(binding.toObject(), context + ".binding", &address, state)) { return false; } control->binding = address; return true; } // 将 HMI 页面及其全部控件序列化为 JSON 对象 QJsonObject serializeHmiPage(const HmiPage &page) { QJsonArray controls; for (const HmiControl &control : page.controls) { controls.append(serializeHmiControl(control)); } QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(page.id)); object.insert(QStringLiteral("name"), fromUtf8(page.name)); object.insert(QStringLiteral("width"), page.width); object.insert(QStringLiteral("height"), page.height); object.insert(QStringLiteral("controls"), controls); return object; } // 解析 HMI 页面基础信息,并按数组顺序构造页面控件 bool parseHmiPage( const QJsonObject &object, const std::string &context, const ProjectLimitSettings &limits, HmiPage *page, ParseState *state) { QJsonArray controls; if (!readString( object, "id", context, &page->id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "name", context, &page->name, state) || !readInt(object, "width", context, ProjectLimits::kMinimumHmiPageWidth, ProjectLimits::kMaximumHmiPageWidth, &page->width, state) || !readInt(object, "height", context, ProjectLimits::kMinimumHmiPageHeight, ProjectLimits::kMaximumHmiPageHeight, &page->height, state) || !readArray( object, "controls", context, &controls, state, static_cast(limits.maximumHmiControlsPerPage))) { return false; } // 预留准确容量,避免逐项加入控件时重复扩容 page->controls.reserve(static_cast(controls.size())); for (int index = 0; index < controls.size(); ++index) { if (!controls.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".controls 的元素必须是对象"); } HmiControl control; if (!parseHmiControl( controls.at(index).toObject(), context + ".controls[" + std::to_string(index) + ']', &control, state)) { return false; } page->controls.push_back(std::move(control)); } return true; } // 将触点模式枚举转换为工程文件中的稳定字符串 QString contactModeName(ContactMode mode) { return mode == ContactMode::NormallyOpen ? QStringLiteral("normallyOpen") : QStringLiteral("normallyClosed"); } // 将工程文件中的触点模式字符串转换为枚举 bool parseContactMode( const std::string &value, ContactMode *mode, ParseState *state) { if (value == "normallyOpen") { *mode = ContactMode::NormallyOpen; } else if (value == "normallyClosed") { *mode = ContactMode::NormallyClosed; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的触点模式:" + value); } return true; } QString edgeModeName(EdgeMode mode) { return mode == EdgeMode::Rising ? QStringLiteral("rising") : QStringLiteral("falling"); } bool parseEdgeMode( const std::string &value, EdgeMode *mode, ParseState *state) { if (value == "rising") { *mode = EdgeMode::Rising; } else if (value == "falling") { *mode = EdgeMode::Falling; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的边沿模式:" + value); } return true; } // 将线圈模式枚举转换为工程文件中的稳定字符串 QString coilModeName(CoilMode mode) { switch (mode) { case CoilMode::Normal: { return QStringLiteral("normal"); } case CoilMode::Set: { return QStringLiteral("set"); } case CoilMode::Reset: { return QStringLiteral("reset"); } default: { return {}; } } } // 将工程文件中的线圈模式字符串转换为枚举 bool parseCoilMode(const std::string &value, CoilMode *mode, ParseState *state) { if (value == "normal") { *mode = CoilMode::Normal; } else if (value == "set") { *mode = CoilMode::Set; } else if (value == "reset") { *mode = CoilMode::Reset; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的线圈模式:" + value); } return true; } // 将比较运算符枚举转换为工程文件中的稳定字符串 QString comparisonName(ComparisonOperator comparison) { switch (comparison) { case ComparisonOperator::Equal: { return QStringLiteral("equal"); } case ComparisonOperator::NotEqual: { return QStringLiteral("notEqual"); } case ComparisonOperator::LessThan: { return QStringLiteral("lessThan"); } case ComparisonOperator::LessThanOrEqual: { return QStringLiteral("lessThanOrEqual"); } case ComparisonOperator::GreaterThan: { return QStringLiteral("greaterThan"); } case ComparisonOperator::GreaterThanOrEqual: { return QStringLiteral("greaterThanOrEqual"); } default: { return {}; } } } // 将工程文件中的比较运算符字符串转换为枚举 bool parseComparison( const std::string &value, ComparisonOperator *comparison, ParseState *state) { if (value == "equal") { *comparison = ComparisonOperator::Equal; } else if (value == "notEqual") { *comparison = ComparisonOperator::NotEqual; } else if (value == "lessThan") { *comparison = ComparisonOperator::LessThan; } else if (value == "lessThanOrEqual") { *comparison = ComparisonOperator::LessThanOrEqual; } else if (value == "greaterThan") { *comparison = ComparisonOperator::GreaterThan; } else if (value == "greaterThanOrEqual") { *comparison = ComparisonOperator::GreaterThanOrEqual; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的比较运算符:" + value); } return true; } // 将触点节点配置序列化,并写入用于反序列化分派的类型标记 QJsonObject serializeNodeConfig(const ContactNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("contact")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("mode"), contactModeName(config.mode)); return object; } QJsonObject serializeNodeConfig(const EdgeContactNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("edgeContact")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("mode"), edgeModeName(config.mode)); return object; } // 将线圈节点配置序列化,并写入用于反序列化分派的类型标记 QJsonObject serializeNodeConfig(const CoilNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("coil")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("mode"), coilModeName(config.mode)); return object; } // 将数值比较节点配置序列化,并保留有符号 16 位比较常量 QJsonObject serializeNodeConfig(const CompareNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("compare")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("comparison"), comparisonName(config.comparison)); object.insert(QStringLiteral("value"), config.value); return object; } QJsonObject serializeNodeConfig(const MoveNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("move")); object.insert(QStringLiteral("source"), serializeWordOperand(config.source)); object.insert(QStringLiteral("destination"), serializeAddress(config.destination)); return object; } QString arithmeticOperationName(ArithmeticOperation operation) { return operation == ArithmeticOperation::Add ? QStringLiteral("add") : QStringLiteral("subtract"); } QJsonObject serializeNodeConfig(const ArithmeticNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("arithmetic")); object.insert(QStringLiteral("operation"), arithmeticOperationName(config.operation)); object.insert(QStringLiteral("left"), serializeWordOperand(config.left)); object.insert(QStringLiteral("right"), serializeWordOperand(config.right)); object.insert(QStringLiteral("destination"), serializeAddress(config.destination)); return object; } // 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载 QJsonObject serializeLogicNode(const LogicNode &node) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(node.id)); object.insert(QStringLiteral("configured"), node.configured); // std::visit 将不同节点配置统一转换为 config JSON 对象 object.insert( QStringLiteral("config"), std::visit( [](const auto &config) { return serializeNodeConfig(config); }, node.config)); return object; } // 根据 type 字段解析具体节点配置,并写入 LogicNodeConfig 变体 bool parseNodeConfig( const QJsonObject &object, const std::string &context, LogicNodeConfig *config, ParseState *state) { std::string type; if (!readString(object, "type", context, &type, state)) { return false; } if (type == "move") { QJsonObject source_object; QJsonObject destination_object; WordOperand source; RegisterAddress destination{RegisterArea::D, 0}; if (!readObject(object, "source", context, &source_object, state) || !parseWordOperand(source_object, context + ".source", &source, state) || !readObject(object, "destination", context, &destination_object, state) || !parseAddress( destination_object, context + ".destination", &destination, state)) { return false; } *config = MoveNodeConfig{source, destination}; return true; } if (type == "arithmetic") { std::string operation_text; QJsonObject left_object; QJsonObject right_object; QJsonObject destination_object; WordOperand left; WordOperand right; RegisterAddress destination{RegisterArea::D, 0}; ArithmeticOperation operation = ArithmeticOperation::Add; if (!readString(object, "operation", context, &operation_text, state)) { return false; } if (operation_text == "add") { operation = ArithmeticOperation::Add; } else if (operation_text == "subtract") { operation = ArithmeticOperation::Subtract; } else { return state->fail( ProjectStorageError::InvalidField, "不支持的算术运算:" + operation_text); } if (!readObject(object, "left", context, &left_object, state) || !parseWordOperand(left_object, context + ".left", &left, state) || !readObject(object, "right", context, &right_object, state) || !parseWordOperand(right_object, context + ".right", &right, state) || !readObject(object, "destination", context, &destination_object, state) || !parseAddress( destination_object, context + ".destination", &destination, state)) { return false; } *config = ArithmeticNodeConfig{operation, left, right, destination}; return true; } if (type != "contact" && type != "edgeContact" && type != "coil" && type != "compare") { return state->fail( ProjectStorageError::InvalidField, "不支持的逻辑节点类型:" + type); } QJsonObject address_object; if (!readObject(object, "address", context, &address_object, state)) { return false; } RegisterAddress address{RegisterArea::M, 0}; if (!parseAddress(address_object, context + ".address", &address, state)) { return false; } // 每种节点只读取自身需要的字段,避免无关配置进入领域模型 if (type == "contact") { std::string mode_text; ContactMode mode = ContactMode::NormallyOpen; if (!readString(object, "mode", context, &mode_text, state) || !parseContactMode(mode_text, &mode, state)) { return false; } *config = ContactNodeConfig{address, mode}; return true; } if (type == "edgeContact") { std::string mode_text; EdgeMode mode = EdgeMode::Rising; if (!readString(object, "mode", context, &mode_text, state) || !parseEdgeMode(mode_text, &mode, state)) { return false; } *config = EdgeContactNodeConfig{address, mode}; return true; } if (type == "coil") { std::string mode_text; CoilMode mode = CoilMode::Normal; if (!readString(object, "mode", context, &mode_text, state) || !parseCoilMode(mode_text, &mode, state)) { return false; } *config = CoilNodeConfig{address, mode}; return true; } if (type == "compare") { std::string comparison_text; int value = 0; ComparisonOperator comparison = ComparisonOperator::Equal; if (!readString(object, "comparison", context, &comparison_text, state) || !readInt( object, "value", context, std::numeric_limits::min(), std::numeric_limits::max(), &value, state) || !parseComparison(comparison_text, &comparison, state)) { return false; } // 先按 int 校验范围,再安全收窄为领域模型要求的 int16_t *config = CompareNodeConfig{ address, comparison, static_cast(value)}; return true; } return state->fail(ProjectStorageError::InvalidField, "逻辑节点类型解析失败"); } // 解析逻辑节点标识及其多态配置 bool parseLogicNode( const QJsonObject &object, const std::string &context, LogicNode *node, ParseState *state) { QJsonObject config; if (!readString( object, "id", context, &node->id, state, ProjectLimits::kMaximumIdBytes) || !readBool(object, "configured", context, &node->configured, state) || !readObject(object, "config", context, &config, state) || !parseNodeConfig(config, context + ".config", &node->config, state)) { return false; } return true; } QJsonObject serializeLadderRung(const LadderRung &rung) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(rung.id)); object.insert(QStringLiteral("name"), fromUtf8(rung.name)); object.insert(QStringLiteral("comment"), fromUtf8(rung.comment)); QJsonArray cells; for (const LadderCell &cell : rung.cells) { QJsonObject cell_object; cell_object.insert(QStringLiteral("id"), fromUtf8(cell.id)); cell_object.insert( QStringLiteral("kind"), cell.kind == LadderCellKind::Node ? QStringLiteral("node") : cell.kind == LadderCellKind::Wire ? QStringLiteral("wire") : QStringLiteral("gap")); if (cell.node.has_value()) { cell_object.insert(QStringLiteral("node"), serializeLogicNode(*cell.node)); } cells.append(cell_object); } object.insert(QStringLiteral("cells"), cells); object.insert( QStringLiteral("output"), rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output)) : QJsonValue(QJsonValue::Null)); return object; } bool parseLadderRung( const QJsonObject &object, const std::string &context, LadderRung *rung, ParseState *state) { QJsonArray cells; QJsonValue output; if (!readString( object, "id", context, &rung->id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "name", context, &rung->name, state) || !readString(object, "comment", context, &rung->comment, state) || !readArray(object, "cells", context, &cells, state, ProjectLimits::kMaximumConditionColumns) || !readValue(object, "output", context, &output, state)) { return false; } if (cells.size() != ProjectLimits::kMaximumConditionColumns) { return state->fail(ProjectStorageError::InvalidField, context + ".cells 必须严格包含 10 个网格"); } for (int index = 0; index < cells.size(); ++index) { if (!cells.at(index).isObject()) { return state->fail(ProjectStorageError::InvalidField, context + ".cells 的元素必须是对象"); } const QJsonObject cell_object = cells.at(index).toObject(); const std::string cell_context = context + ".cells[" + std::to_string(index) + ']'; LadderCell cell; std::string kind; if (!readString(cell_object, "id", cell_context, &cell.id, state, ProjectLimits::kMaximumIdBytes) || !readString(cell_object, "kind", cell_context, &kind, state)) { return false; } if (kind == "gap") { cell.kind = LadderCellKind::Gap; } else if (kind == "wire") { cell.kind = LadderCellKind::Wire; } else if (kind == "node") { cell.kind = LadderCellKind::Node; QJsonObject node_object; if (!readObject(cell_object, "node", cell_context, &node_object, state)) { return false; } LogicNode node; if (!parseLogicNode(node_object, cell_context + ".node", &node, state)) { return false; } cell.node = std::move(node); } else { return state->fail(ProjectStorageError::InvalidField, cell_context + ".kind 必须是 gap、wire 或 node"); } rung->cells.push_back(std::move(cell)); } if (output.isNull()) { rung->output.reset(); return true; } if (!output.isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".output 必须是对象或 null"); } LogicNode node; if (!parseLogicNode(output.toObject(), context + ".output", &node, state)) { return false; } rung->output = std::move(node); return true; } QJsonObject serializeControlLogic(const ControlLogic &logic) { QJsonArray rungs; for (const LadderRung &rung : logic.rungs) { rungs.append(serializeLadderRung(rung)); } QJsonArray connections; for (const VerticalConnection &connection : logic.verticalConnections) { QJsonObject item; item.insert(QStringLiteral("id"), fromUtf8(connection.id)); item.insert(QStringLiteral("upperRungId"), fromUtf8(connection.upperRungId)); item.insert(QStringLiteral("lowerRungId"), fromUtf8(connection.lowerRungId)); item.insert(QStringLiteral("columnBoundary"), connection.columnBoundary); connections.append(item); } QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(logic.id)); object.insert(QStringLiteral("name"), fromUtf8(logic.name)); object.insert(QStringLiteral("enabled"), logic.enabled); object.insert(QStringLiteral("rungs"), rungs); object.insert(QStringLiteral("verticalConnections"), connections); return object; } bool parseControlLogic( const QJsonObject &object, const std::string &context, const ProjectLimitSettings &limits, ControlLogic *logic, ParseState *state) { QJsonArray rungs; QJsonArray connections; if (!readString( object, "id", context, &logic->id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "name", context, &logic->name, state) || !readBool(object, "enabled", context, &logic->enabled, state) || !readArray( object, "rungs", context, &rungs, state, static_cast(limits.maximumRungsPerLogic)) || !readArray( object, "verticalConnections", context, &connections, state, static_cast(ProjectLimits::kMaximumVerticalConnectionsPerLogic))) { return false; } logic->rungs.reserve(static_cast(rungs.size())); for (int index = 0; index < rungs.size(); ++index) { if (!rungs.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".rungs 的元素必须是对象"); } LadderRung rung; if (!parseLadderRung( rungs.at(index).toObject(), context + ".rungs[" + std::to_string(index) + ']', &rung, state)) { return false; } logic->rungs.push_back(std::move(rung)); } for (int index = 0; index < connections.size(); ++index) { if (!connections.at(index).isObject()) { return state->fail(ProjectStorageError::InvalidField, context + ".verticalConnections 的元素必须是对象"); } const QJsonObject item = connections.at(index).toObject(); VerticalConnection connection; int boundary = 0; const std::string item_context = context + ".verticalConnections[" + std::to_string(index) + ']'; if (!readString(item, "id", item_context, &connection.id, state, ProjectLimits::kMaximumIdBytes) || !readString(item, "upperRungId", item_context, &connection.upperRungId, state, ProjectLimits::kMaximumIdBytes) || !readString(item, "lowerRungId", item_context, &connection.lowerRungId, state, ProjectLimits::kMaximumIdBytes) || !readInt(item, "columnBoundary", item_context, 0, ProjectLimits::kMaximumConditionColumns, &boundary, state)) { return false; } connection.columnBoundary = boundary; logic->verticalConnections.push_back(std::move(connection)); } return true; } // 将完整领域工程聚合为工程文件的顶层 JSON 对象 QJsonObject serializeProject(const Project &project) { QJsonArray pages; for (const HmiPage &page : project.hmiPages) { pages.append(serializeHmiPage(page)); } QJsonArray logics; for (const ControlLogic &logic : project.controlLogics) { logics.append(serializeControlLogic(logic)); } QJsonArray alarms; for (const AlarmDefinition &definition : project.alarmDefinitions) { alarms.append(serializeAlarmDefinition(definition)); } QJsonArray register_comments; for (const RegisterComment &comment : project.registerComments) { register_comments.append(serializeRegisterComment(comment)); } 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("initialHmiPageId"), fromUtf8(project.initialHmiPageId)); object.insert(QStringLiteral("alarmDefinitions"), alarms); object.insert(QStringLiteral("registerComments"), register_comments); object.insert(QStringLiteral("controlLogics"), logics); return object; } // 从顶层 JSON 对象解析完整工程,并在解析子对象前检查格式版本 bool parseProject( const QJsonObject &object, const ProjectLimitSettings &limits, Project *project, ParseState *state) { QJsonArray pages; QJsonArray alarms; QJsonArray register_comments; QJsonArray logics; if (!readString( object, "formatVersion", "project", &project->metadata.formatVersion, state, ProjectLimits::kMaximumIdBytes)) { return false; } // 不尝试猜测其他版本的结构,避免按当前格式错误解释数据 if (project->metadata.formatVersion != kCurrentFormatVersion) { return state->fail( ProjectStorageError::UnsupportedVersion, "不支持的工程格式版本:" + project->metadata.formatVersion); } if (!readString( object, "id", "project", &project->metadata.id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "name", "project", &project->metadata.name, state) || !readArray( object, "hmiPages", "project", &pages, state, static_cast(limits.maximumHmiPages)) || !readString( object, "initialHmiPageId", "project", &project->initialHmiPageId, state, ProjectLimits::kMaximumIdBytes) || !readArray( object, "alarmDefinitions", "project", &alarms, state, static_cast(limits.maximumAlarmDefinitions)) || !readArray( object, "registerComments", "project", ®ister_comments, state, static_cast(ProjectLimits::kMaximumRegisterComments)) || !readArray( object, "controlLogics", "project", &logics, state, static_cast(limits.maximumControlLogics))) { return false; } // 逐层解析时持续传递字段路径,任何失败都保留最初的具体位置 project->hmiPages.reserve(static_cast(pages.size())); for (int index = 0; index < pages.size(); ++index) { if (!pages.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.hmiPages 的元素必须是对象"); } HmiPage page; if (!parseHmiPage( pages.at(index).toObject(), "project.hmiPages[" + std::to_string(index) + ']', limits, &page, state)) { return false; } project->hmiPages.push_back(std::move(page)); } project->alarmDefinitions.reserve(static_cast(alarms.size())); for (int index = 0; index < alarms.size(); ++index) { if (!alarms.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.alarmDefinitions 的元素必须是对象"); } AlarmDefinition definition; if (!parseAlarmDefinition( alarms.at(index).toObject(), "project.alarmDefinitions[" + std::to_string(index) + ']', &definition, state)) { return false; } project->alarmDefinitions.push_back(std::move(definition)); } project->registerComments.reserve(static_cast(register_comments.size())); for (int index = 0; index < register_comments.size(); ++index) { if (!register_comments.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.registerComments 的元素必须是对象"); } RegisterComment comment; if (!parseRegisterComment( register_comments.at(index).toObject(), "project.registerComments[" + std::to_string(index) + ']', &comment, state)) { return false; } project->registerComments.push_back(std::move(comment)); } project->controlLogics.reserve(static_cast(logics.size())); for (int index = 0; index < logics.size(); ++index) { if (!logics.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.controlLogics 的元素必须是对象"); } ControlLogic logic; if (!parseControlLogic( logics.at(index).toObject(), "project.controlLogics[" + std::to_string(index) + ']', limits, &logic, state)) { return false; } project->controlLogics.push_back(std::move(logic)); } return true; } // 将 Qt 文件错误转换为领域层统一的工程保存失败结果 ProjectSaveResult saveFailure( ProjectStorageError error, const QString &message) { return {false, error, toUtf8(message)}; } } // namespace JsonProjectStorage::JsonProjectStorage( const ProjectLimitSettings &project_limits) : project_limits_(project_limits) { } // 校验工程后将其序列化,并通过 QSaveFile 原子写入目标文件 ProjectSaveResult JsonProjectStorage::save( const Project &project, const std::string &file_path) { // 写文件前先执行领域校验,防止持久化内部关系不合法的工程 std::string validation_error; if (!project.validate(project_limits_, &validation_error)) { return {false, ProjectStorageError::InvalidProject, validation_error}; } if (project.metadata.formatVersion != kCurrentFormatVersion) { return {false, ProjectStorageError::UnsupportedVersion, "不支持的工程格式版本:" + project.metadata.formatVersion}; } // QSaveFile 先写临时文件,仅在 commit 成功后替换目标文件 QSaveFile file(fromUtf8(file_path)); if (!file.open(QIODevice::WriteOnly)) { return saveFailure(ProjectStorageError::FileOpenFailed, file.errorString()); } const QByteArray data = QJsonDocument(serializeProject(project)).toJson( QJsonDocument::Indented); if (static_cast(data.size()) > ProjectLimits::kMaximumProjectFileBytes) { file.cancelWriting(); return {false, ProjectStorageError::InvalidProject, "工程 JSON 文件不能超过 16 MiB"}; } // 短写入也视为失败,并取消临时文件提交 if (file.write(data) != data.size()) { file.cancelWriting(); return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString()); } if (!file.commit()) { return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString()); } return {true, ProjectStorageError::None, {}}; } // 读取并解析工程文件,全部校验通过后才返回新的领域工程 ProjectLoadResult JsonProjectStorage::load(const std::string &file_path) { QFile file(fromUtf8(file_path)); if (!file.open(QIODevice::ReadOnly)) { return {false, {}, ProjectStorageError::FileOpenFailed, toUtf8(file.errorString())}; } if (file.size() < 0 || static_cast(file.size()) > static_cast(ProjectLimits::kMaximumProjectFileBytes)) { return {false, {}, ProjectStorageError::InvalidJson, "工程 JSON 文件不能超过 16 MiB"}; } // 最多只读到上限加 1 字节,避免文件属性检查后文件变大导致无限制分配 const QByteArray data = file.read( static_cast(ProjectLimits::kMaximumProjectFileBytes) + 1); if (file.error() != QFileDevice::NoError) { return {false, {}, ProjectStorageError::FileReadFailed, toUtf8(file.errorString())}; } if (static_cast(data.size()) > ProjectLimits::kMaximumProjectFileBytes) { return {false, {}, ProjectStorageError::InvalidJson, "工程 JSON 文件不能超过 16 MiB"}; } // 顶层必须是 JSON 对象,数组或标量不能表示完整工程 QJsonParseError parse_error; const QJsonDocument document = QJsonDocument::fromJson(data, &parse_error); if (parse_error.error != QJsonParseError::NoError || !document.isObject()) { const QString message = parse_error.error == QJsonParseError::NoError ? QStringLiteral("工程 JSON 根节点必须是对象") : parse_error.errorString(); return {false, {}, ProjectStorageError::InvalidJson, toUtf8(message)}; } // 先在局部对象中完成结构解析,失败时不会暴露半成品工程 Project project; ParseState state; if (!parseProject(document.object(), project_limits_, &project, &state)) { return {false, {}, state.error, state.message}; } // JSON 字段合法不代表业务关系合法,还需执行领域层整体校验 std::string validation_error; if (!project.validate(project_limits_, &validation_error)) { return {false, {}, ProjectStorageError::InvalidProject, validation_error}; } return {true, std::move(project), ProjectStorageError::None, {}}; }