综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1256 lines
36 KiB

  1. #include "json_project_storage.h"
  2. #include <QFile>
  3. #include <QJsonArray>
  4. #include <QJsonDocument>
  5. #include <QJsonObject>
  6. #include <QJsonParseError>
  7. #include <QSaveFile>
  8. #include <cmath>
  9. #include <limits>
  10. #include <string>
  11. #include <utility>
  12. namespace {
  13. // 当前读写实现支持的工程文件格式版本
  14. constexpr const char *kCurrentFormatVersion = "1.0";
  15. // 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因
  16. struct ParseState
  17. {
  18. ProjectStorageError error = ProjectStorageError::None;
  19. std::string message;
  20. // 记录首次解析错误并返回 false,便于解析函数直接向上传播失败
  21. bool fail(ProjectStorageError new_error, const std::string &new_message)
  22. {
  23. if (error == ProjectStorageError::None)
  24. {
  25. error = new_error;
  26. message = new_message;
  27. }
  28. return false;
  29. }
  30. };
  31. // 将领域层使用的 UTF-8 字符串转换为 Qt 字符串
  32. QString fromUtf8(const std::string &value)
  33. {
  34. return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
  35. }
  36. // 将 Qt 字符串转换为领域层使用的 UTF-8 字符串
  37. std::string toUtf8(const QString &value)
  38. {
  39. const QByteArray bytes = value.toUtf8();
  40. return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
  41. }
  42. // 拼接带上下文的字段路径,用于生成可定位的错误信息
  43. std::string fieldPath(const std::string &context, const char *field)
  44. {
  45. return context + '.' + field;
  46. }
  47. // 读取必填 JSON 字段,字段不存在时记录 MissingField 错误
  48. bool readValue(
  49. const QJsonObject &object,
  50. const char *field,
  51. const std::string &context,
  52. QJsonValue *value,
  53. ParseState *state)
  54. {
  55. const QString key = QString::fromLatin1(field);
  56. if (!object.contains(key))
  57. {
  58. return state->fail(
  59. ProjectStorageError::MissingField,
  60. "missing required field " + fieldPath(context, field));
  61. }
  62. *value = object.value(key);
  63. return true;
  64. }
  65. // 读取必填字符串字段并转换为 UTF-8 标准字符串
  66. bool readString(
  67. const QJsonObject &object,
  68. const char *field,
  69. const std::string &context,
  70. std::string *value,
  71. ParseState *state)
  72. {
  73. QJsonValue json_value;
  74. if (!readValue(object, field, context, &json_value, state))
  75. {
  76. return false;
  77. }
  78. if (!json_value.isString())
  79. {
  80. return state->fail(
  81. ProjectStorageError::InvalidField,
  82. fieldPath(context, field) + " must be a string");
  83. }
  84. *value = toUtf8(json_value.toString());
  85. return true;
  86. }
  87. // 读取必填布尔字段并校验 JSON 类型
  88. bool readBool(
  89. const QJsonObject &object,
  90. const char *field,
  91. const std::string &context,
  92. bool *value,
  93. ParseState *state)
  94. {
  95. QJsonValue json_value;
  96. if (!readValue(object, field, context, &json_value, state))
  97. {
  98. return false;
  99. }
  100. if (!json_value.isBool())
  101. {
  102. return state->fail(
  103. ProjectStorageError::InvalidField,
  104. fieldPath(context, field) + " must be a boolean");
  105. }
  106. *value = json_value.toBool();
  107. return true;
  108. }
  109. // 读取指定闭区间内的整数,拒绝小数、非有限值和越界值
  110. bool readInt(
  111. const QJsonObject &object,
  112. const char *field,
  113. const std::string &context,
  114. int minimum,
  115. int maximum,
  116. int *value,
  117. ParseState *state)
  118. {
  119. QJsonValue json_value;
  120. if (!readValue(object, field, context, &json_value, state))
  121. {
  122. return false;
  123. }
  124. if (!json_value.isDouble())
  125. {
  126. return state->fail(
  127. ProjectStorageError::InvalidField,
  128. fieldPath(context, field) + " must be an integer");
  129. }
  130. // Qt JSON 统一用 double 表示数值,需要额外确认它能无损转换为 int
  131. const double number = json_value.toDouble();
  132. if (!std::isfinite(number) || std::floor(number) != number
  133. || number < minimum || number > maximum)
  134. {
  135. return state->fail(
  136. ProjectStorageError::InvalidField,
  137. fieldPath(context, field) + " is outside the supported integer range");
  138. }
  139. *value = static_cast<int>(number);
  140. return true;
  141. }
  142. // 读取必填对象字段并校验 JSON 类型
  143. bool readObject(
  144. const QJsonObject &object,
  145. const char *field,
  146. const std::string &context,
  147. QJsonObject *value,
  148. ParseState *state)
  149. {
  150. QJsonValue json_value;
  151. if (!readValue(object, field, context, &json_value, state))
  152. {
  153. return false;
  154. }
  155. if (!json_value.isObject())
  156. {
  157. return state->fail(
  158. ProjectStorageError::InvalidField,
  159. fieldPath(context, field) + " must be an object");
  160. }
  161. *value = json_value.toObject();
  162. return true;
  163. }
  164. // 读取必填数组字段并校验 JSON 类型
  165. bool readArray(
  166. const QJsonObject &object,
  167. const char *field,
  168. const std::string &context,
  169. QJsonArray *value,
  170. ParseState *state)
  171. {
  172. QJsonValue json_value;
  173. if (!readValue(object, field, context, &json_value, state))
  174. {
  175. return false;
  176. }
  177. if (!json_value.isArray())
  178. {
  179. return state->fail(
  180. ProjectStorageError::InvalidField,
  181. fieldPath(context, field) + " must be an array");
  182. }
  183. *value = json_value.toArray();
  184. return true;
  185. }
  186. // 将寄存器地址序列化为包含区域和原始索引的 JSON 对象
  187. QJsonObject serializeAddress(const RegisterAddress &address)
  188. {
  189. QJsonObject object;
  190. object.insert(QStringLiteral("area"),
  191. address.area() == RegisterArea::M ? QStringLiteral("M")
  192. : QStringLiteral("D"));
  193. object.insert(QStringLiteral("index"), address.index());
  194. return object;
  195. }
  196. // 解析寄存器地址并校验区域名称及项目允许的索引范围
  197. bool parseAddress(
  198. const QJsonObject &object,
  199. const std::string &context,
  200. RegisterAddress *address,
  201. ParseState *state)
  202. {
  203. std::string area_text;
  204. int index = 0;
  205. if (!readString(object, "area", context, &area_text, state)
  206. || !readInt(
  207. object,
  208. "index",
  209. context,
  210. RegisterAddress::kMinimumIndex,
  211. RegisterAddress::kMaximumIndex,
  212. &index,
  213. state))
  214. {
  215. return false;
  216. }
  217. RegisterArea area = RegisterArea::M;
  218. if (area_text == "M")
  219. {
  220. area = RegisterArea::M;
  221. }
  222. else if (area_text == "D")
  223. {
  224. area = RegisterArea::D;
  225. }
  226. else
  227. {
  228. return state->fail(
  229. ProjectStorageError::InvalidField,
  230. context + ".area must be M or D");
  231. }
  232. *address = RegisterAddress{area, index};
  233. return true;
  234. }
  235. // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串
  236. QString hmiControlTypeName(HmiControlType type)
  237. {
  238. switch (type)
  239. {
  240. case HmiControlType::Button:
  241. {
  242. return QStringLiteral("button");
  243. }
  244. case HmiControlType::Indicator:
  245. {
  246. return QStringLiteral("indicator");
  247. }
  248. case HmiControlType::NumericDisplay:
  249. {
  250. return QStringLiteral("numericDisplay");
  251. }
  252. case HmiControlType::NumericInput:
  253. {
  254. return QStringLiteral("numericInput");
  255. }
  256. case HmiControlType::Label:
  257. {
  258. return QStringLiteral("label");
  259. }
  260. default:
  261. {
  262. return {};
  263. }
  264. }
  265. }
  266. // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举
  267. bool parseHmiControlType(
  268. const std::string &value, HmiControlType *type, ParseState *state)
  269. {
  270. if (value == "button")
  271. {
  272. *type = HmiControlType::Button;
  273. }
  274. else if (value == "indicator")
  275. {
  276. *type = HmiControlType::Indicator;
  277. }
  278. else if (value == "numericDisplay")
  279. {
  280. *type = HmiControlType::NumericDisplay;
  281. }
  282. else if (value == "numericInput")
  283. {
  284. *type = HmiControlType::NumericInput;
  285. }
  286. else if (value == "label")
  287. {
  288. *type = HmiControlType::Label;
  289. }
  290. else
  291. {
  292. return state->fail(
  293. ProjectStorageError::InvalidField,
  294. "unsupported HMI control type " + value);
  295. }
  296. return true;
  297. }
  298. // 将 HMI 控件矩形区域序列化为 JSON 对象
  299. QJsonObject serializeBounds(const HmiRect &bounds)
  300. {
  301. QJsonObject object;
  302. object.insert(QStringLiteral("x"), bounds.x);
  303. object.insert(QStringLiteral("y"), bounds.y);
  304. object.insert(QStringLiteral("width"), bounds.width);
  305. object.insert(QStringLiteral("height"), bounds.height);
  306. return object;
  307. }
  308. // 解析控件矩形区域,允许任意坐标但要求宽高为正数
  309. bool parseBounds(
  310. const QJsonObject &object,
  311. const std::string &context,
  312. HmiRect *bounds,
  313. ParseState *state)
  314. {
  315. return readInt(
  316. object,
  317. "x",
  318. context,
  319. std::numeric_limits<int>::min(),
  320. std::numeric_limits<int>::max(),
  321. &bounds->x,
  322. state)
  323. && readInt(
  324. object,
  325. "y",
  326. context,
  327. std::numeric_limits<int>::min(),
  328. std::numeric_limits<int>::max(),
  329. &bounds->y,
  330. state)
  331. && readInt(
  332. object,
  333. "width",
  334. context,
  335. 1,
  336. std::numeric_limits<int>::max(),
  337. &bounds->width,
  338. state)
  339. && readInt(
  340. object,
  341. "height",
  342. context,
  343. 1,
  344. std::numeric_limits<int>::max(),
  345. &bounds->height,
  346. state);
  347. }
  348. // 将 HMI 控件的字符串扩展属性序列化为 JSON 对象
  349. QJsonObject serializeProperties(const std::map<std::string, std::string> &properties)
  350. {
  351. QJsonObject object;
  352. for (const auto &property : properties)
  353. {
  354. object.insert(fromUtf8(property.first), fromUtf8(property.second));
  355. }
  356. return object;
  357. }
  358. // 解析 HMI 控件扩展属性,并确保所有属性值都是字符串
  359. bool parseProperties(
  360. const QJsonObject &object,
  361. std::map<std::string, std::string> *properties,
  362. ParseState *state)
  363. {
  364. for (auto current = object.constBegin(); current != object.constEnd(); ++current)
  365. {
  366. if (!current.value().isString())
  367. {
  368. return state->fail(
  369. ProjectStorageError::InvalidField,
  370. "HMI control property values must be strings");
  371. }
  372. properties->emplace(toUtf8(current.key()), toUtf8(current.value().toString()));
  373. }
  374. return true;
  375. }
  376. // 将单个 HMI 控件及其可选寄存器绑定序列化为 JSON 对象
  377. QJsonObject serializeHmiControl(const HmiControl &control)
  378. {
  379. QJsonObject object;
  380. object.insert(QStringLiteral("id"), fromUtf8(control.id));
  381. object.insert(QStringLiteral("type"), hmiControlTypeName(control.type));
  382. object.insert(QStringLiteral("bounds"), serializeBounds(control.bounds));
  383. object.insert(QStringLiteral("text"), fromUtf8(control.text));
  384. // 使用 JSON null 明确表示控件没有寄存器绑定
  385. if (control.binding.has_value())
  386. {
  387. object.insert(QStringLiteral("binding"), serializeAddress(*control.binding));
  388. }
  389. else
  390. {
  391. object.insert(QStringLiteral("binding"), QJsonValue::Null);
  392. }
  393. object.insert(QStringLiteral("properties"), serializeProperties(control.properties));
  394. return object;
  395. }
  396. // 解析单个 HMI 控件,并逐层校验类型、区域、绑定和扩展属性
  397. bool parseHmiControl(
  398. const QJsonObject &object,
  399. const std::string &context,
  400. HmiControl *control,
  401. ParseState *state)
  402. {
  403. std::string type_text;
  404. QJsonObject bounds;
  405. QJsonObject properties;
  406. QJsonValue binding;
  407. if (!readString(object, "id", context, &control->id, state)
  408. || !readString(object, "type", context, &type_text, state)
  409. || !readObject(object, "bounds", context, &bounds, state)
  410. || !readString(object, "text", context, &control->text, state)
  411. || !readValue(object, "binding", context, &binding, state)
  412. || !readObject(object, "properties", context, &properties, state))
  413. {
  414. return false;
  415. }
  416. if (!parseHmiControlType(type_text, &control->type, state)
  417. || !parseBounds(bounds, context + ".bounds", &control->bounds, state)
  418. || !parseProperties(properties, &control->properties, state))
  419. {
  420. return false;
  421. }
  422. // binding 允许为 null,其余非空值必须是合法的寄存器地址对象
  423. if (binding.isNull())
  424. {
  425. control->binding.reset();
  426. return true;
  427. }
  428. if (!binding.isObject())
  429. {
  430. return state->fail(
  431. ProjectStorageError::InvalidField,
  432. context + ".binding must be an object or null");
  433. }
  434. RegisterAddress address{RegisterArea::M, 0};
  435. if (!parseAddress(binding.toObject(), context + ".binding", &address, state))
  436. {
  437. return false;
  438. }
  439. control->binding = address;
  440. return true;
  441. }
  442. // 将 HMI 页面及其全部控件序列化为 JSON 对象
  443. QJsonObject serializeHmiPage(const HmiPage &page)
  444. {
  445. QJsonArray controls;
  446. for (const HmiControl &control : page.controls)
  447. {
  448. controls.append(serializeHmiControl(control));
  449. }
  450. QJsonObject object;
  451. object.insert(QStringLiteral("id"), fromUtf8(page.id));
  452. object.insert(QStringLiteral("name"), fromUtf8(page.name));
  453. object.insert(QStringLiteral("width"), page.width);
  454. object.insert(QStringLiteral("height"), page.height);
  455. object.insert(QStringLiteral("controls"), controls);
  456. return object;
  457. }
  458. // 解析 HMI 页面基础信息,并按数组顺序构造页面控件
  459. bool parseHmiPage(
  460. const QJsonObject &object,
  461. const std::string &context,
  462. HmiPage *page,
  463. ParseState *state)
  464. {
  465. QJsonArray controls;
  466. if (!readString(object, "id", context, &page->id, state)
  467. || !readString(object, "name", context, &page->name, state)
  468. || !readInt(object, "width", context, 1, std::numeric_limits<int>::max(),
  469. &page->width, state)
  470. || !readInt(object, "height", context, 1, std::numeric_limits<int>::max(),
  471. &page->height, state)
  472. || !readArray(object, "controls", context, &controls, state))
  473. {
  474. return false;
  475. }
  476. // 预留准确容量,避免逐项加入控件时重复扩容
  477. page->controls.reserve(static_cast<std::size_t>(controls.size()));
  478. for (int index = 0; index < controls.size(); ++index)
  479. {
  480. if (!controls.at(index).isObject())
  481. {
  482. return state->fail(
  483. ProjectStorageError::InvalidField,
  484. context + ".controls items must be objects");
  485. }
  486. HmiControl control;
  487. if (!parseHmiControl(
  488. controls.at(index).toObject(),
  489. context + ".controls[" + std::to_string(index) + ']',
  490. &control,
  491. state))
  492. {
  493. return false;
  494. }
  495. page->controls.push_back(std::move(control));
  496. }
  497. return true;
  498. }
  499. // 将触点模式枚举转换为工程文件中的稳定字符串
  500. QString contactModeName(ContactMode mode)
  501. {
  502. return mode == ContactMode::NormallyOpen
  503. ? QStringLiteral("normallyOpen")
  504. : QStringLiteral("normallyClosed");
  505. }
  506. // 将工程文件中的触点模式字符串转换为枚举
  507. bool parseContactMode(
  508. const std::string &value, ContactMode *mode, ParseState *state)
  509. {
  510. if (value == "normallyOpen")
  511. {
  512. *mode = ContactMode::NormallyOpen;
  513. }
  514. else if (value == "normallyClosed")
  515. {
  516. *mode = ContactMode::NormallyClosed;
  517. }
  518. else
  519. {
  520. return state->fail(
  521. ProjectStorageError::InvalidField,
  522. "unsupported contact mode " + value);
  523. }
  524. return true;
  525. }
  526. // 将线圈模式枚举转换为工程文件中的稳定字符串
  527. QString coilModeName(CoilMode mode)
  528. {
  529. switch (mode)
  530. {
  531. case CoilMode::Normal:
  532. {
  533. return QStringLiteral("normal");
  534. }
  535. case CoilMode::Set:
  536. {
  537. return QStringLiteral("set");
  538. }
  539. case CoilMode::Reset:
  540. {
  541. return QStringLiteral("reset");
  542. }
  543. default:
  544. {
  545. return {};
  546. }
  547. }
  548. }
  549. // 将工程文件中的线圈模式字符串转换为枚举
  550. bool parseCoilMode(const std::string &value, CoilMode *mode, ParseState *state)
  551. {
  552. if (value == "normal")
  553. {
  554. *mode = CoilMode::Normal;
  555. }
  556. else if (value == "set")
  557. {
  558. *mode = CoilMode::Set;
  559. }
  560. else if (value == "reset")
  561. {
  562. *mode = CoilMode::Reset;
  563. }
  564. else
  565. {
  566. return state->fail(
  567. ProjectStorageError::InvalidField,
  568. "unsupported coil mode " + value);
  569. }
  570. return true;
  571. }
  572. // 将比较运算符枚举转换为工程文件中的稳定字符串
  573. QString comparisonName(ComparisonOperator comparison)
  574. {
  575. switch (comparison)
  576. {
  577. case ComparisonOperator::Equal:
  578. {
  579. return QStringLiteral("equal");
  580. }
  581. case ComparisonOperator::NotEqual:
  582. {
  583. return QStringLiteral("notEqual");
  584. }
  585. case ComparisonOperator::LessThan:
  586. {
  587. return QStringLiteral("lessThan");
  588. }
  589. case ComparisonOperator::LessThanOrEqual:
  590. {
  591. return QStringLiteral("lessThanOrEqual");
  592. }
  593. case ComparisonOperator::GreaterThan:
  594. {
  595. return QStringLiteral("greaterThan");
  596. }
  597. case ComparisonOperator::GreaterThanOrEqual:
  598. {
  599. return QStringLiteral("greaterThanOrEqual");
  600. }
  601. default:
  602. {
  603. return {};
  604. }
  605. }
  606. }
  607. // 将工程文件中的比较运算符字符串转换为枚举
  608. bool parseComparison(
  609. const std::string &value,
  610. ComparisonOperator *comparison,
  611. ParseState *state)
  612. {
  613. if (value == "equal")
  614. {
  615. *comparison = ComparisonOperator::Equal;
  616. }
  617. else if (value == "notEqual")
  618. {
  619. *comparison = ComparisonOperator::NotEqual;
  620. }
  621. else if (value == "lessThan")
  622. {
  623. *comparison = ComparisonOperator::LessThan;
  624. }
  625. else if (value == "lessThanOrEqual")
  626. {
  627. *comparison = ComparisonOperator::LessThanOrEqual;
  628. }
  629. else if (value == "greaterThan")
  630. {
  631. *comparison = ComparisonOperator::GreaterThan;
  632. }
  633. else if (value == "greaterThanOrEqual")
  634. {
  635. *comparison = ComparisonOperator::GreaterThanOrEqual;
  636. }
  637. else
  638. {
  639. return state->fail(
  640. ProjectStorageError::InvalidField,
  641. "unsupported comparison operator " + value);
  642. }
  643. return true;
  644. }
  645. // 将触点节点配置序列化,并写入用于反序列化分派的类型标记
  646. QJsonObject serializeNodeConfig(const ContactNodeConfig &config)
  647. {
  648. QJsonObject object;
  649. object.insert(QStringLiteral("type"), QStringLiteral("contact"));
  650. object.insert(QStringLiteral("address"), serializeAddress(config.address));
  651. object.insert(QStringLiteral("mode"), contactModeName(config.mode));
  652. return object;
  653. }
  654. // 将线圈节点配置序列化,并写入用于反序列化分派的类型标记
  655. QJsonObject serializeNodeConfig(const CoilNodeConfig &config)
  656. {
  657. QJsonObject object;
  658. object.insert(QStringLiteral("type"), QStringLiteral("coil"));
  659. object.insert(QStringLiteral("address"), serializeAddress(config.address));
  660. object.insert(QStringLiteral("mode"), coilModeName(config.mode));
  661. return object;
  662. }
  663. // 将数值比较节点配置序列化,并保留有符号 16 位比较常量
  664. QJsonObject serializeNodeConfig(const CompareNodeConfig &config)
  665. {
  666. QJsonObject object;
  667. object.insert(QStringLiteral("type"), QStringLiteral("compare"));
  668. object.insert(QStringLiteral("address"), serializeAddress(config.address));
  669. object.insert(QStringLiteral("comparison"), comparisonName(config.comparison));
  670. object.insert(QStringLiteral("value"), config.value);
  671. return object;
  672. }
  673. // 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载
  674. QJsonObject serializeLogicNode(const LogicNode &node)
  675. {
  676. QJsonObject object;
  677. object.insert(QStringLiteral("id"), fromUtf8(node.id));
  678. object.insert(QStringLiteral("configured"), node.configured);
  679. // std::visit 将不同节点配置统一转换为 config JSON 对象
  680. object.insert(
  681. QStringLiteral("config"),
  682. std::visit(
  683. [](const auto &config)
  684. {
  685. return serializeNodeConfig(config);
  686. },
  687. node.config));
  688. return object;
  689. }
  690. // 根据 type 字段解析具体节点配置,并写入 LogicNodeConfig 变体
  691. bool parseNodeConfig(
  692. const QJsonObject &object,
  693. const std::string &context,
  694. LogicNodeConfig *config,
  695. ParseState *state)
  696. {
  697. std::string type;
  698. QJsonObject address_object;
  699. if (!readString(object, "type", context, &type, state)
  700. || !readObject(object, "address", context, &address_object, state))
  701. {
  702. return false;
  703. }
  704. RegisterAddress address{RegisterArea::M, 0};
  705. if (!parseAddress(address_object, context + ".address", &address, state))
  706. {
  707. return false;
  708. }
  709. // 每种节点只读取自身需要的字段,避免无关配置进入领域模型
  710. if (type == "contact")
  711. {
  712. std::string mode_text;
  713. ContactMode mode = ContactMode::NormallyOpen;
  714. if (!readString(object, "mode", context, &mode_text, state)
  715. || !parseContactMode(mode_text, &mode, state))
  716. {
  717. return false;
  718. }
  719. *config = ContactNodeConfig{address, mode};
  720. return true;
  721. }
  722. if (type == "coil")
  723. {
  724. std::string mode_text;
  725. CoilMode mode = CoilMode::Normal;
  726. if (!readString(object, "mode", context, &mode_text, state)
  727. || !parseCoilMode(mode_text, &mode, state))
  728. {
  729. return false;
  730. }
  731. *config = CoilNodeConfig{address, mode};
  732. return true;
  733. }
  734. if (type == "compare")
  735. {
  736. std::string comparison_text;
  737. int value = 0;
  738. ComparisonOperator comparison = ComparisonOperator::Equal;
  739. if (!readString(object, "comparison", context, &comparison_text, state)
  740. || !readInt(
  741. object,
  742. "value",
  743. context,
  744. std::numeric_limits<std::int16_t>::min(),
  745. std::numeric_limits<std::int16_t>::max(),
  746. &value,
  747. state)
  748. || !parseComparison(comparison_text, &comparison, state))
  749. {
  750. return false;
  751. }
  752. // 先按 int 校验范围,再安全收窄为领域模型要求的 int16_t
  753. *config = CompareNodeConfig{
  754. address, comparison, static_cast<std::int16_t>(value)};
  755. return true;
  756. }
  757. return state->fail(
  758. ProjectStorageError::InvalidField,
  759. "unsupported logic node type " + type);
  760. }
  761. // 解析逻辑节点标识及其多态配置
  762. bool parseLogicNode(
  763. const QJsonObject &object,
  764. const std::string &context,
  765. LogicNode *node,
  766. ParseState *state)
  767. {
  768. QJsonObject config;
  769. if (!readString(object, "id", context, &node->id, state)
  770. || !readBool(object, "configured", context, &node->configured, state)
  771. || !readObject(object, "config", context, &config, state)
  772. || !parseNodeConfig(config, context + ".config", &node->config, state))
  773. {
  774. return false;
  775. }
  776. return true;
  777. }
  778. QString expressionKindText(ConditionExpressionKind kind)
  779. {
  780. switch (kind)
  781. {
  782. case ConditionExpressionKind::Node:
  783. return QStringLiteral("node");
  784. case ConditionExpressionKind::Series:
  785. return QStringLiteral("series");
  786. case ConditionExpressionKind::Parallel:
  787. return QStringLiteral("parallel");
  788. }
  789. return {};
  790. }
  791. QJsonObject serializeConditionExpression(const ConditionExpression &expression)
  792. {
  793. QJsonObject object;
  794. object.insert(QStringLiteral("id"), fromUtf8(expression.id));
  795. object.insert(QStringLiteral("kind"), expressionKindText(expression.kind));
  796. if (expression.kind == ConditionExpressionKind::Node)
  797. {
  798. object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node));
  799. }
  800. else
  801. {
  802. QJsonArray children;
  803. for (const ConditionExpression &child : expression.children)
  804. {
  805. children.append(serializeConditionExpression(child));
  806. }
  807. object.insert(QStringLiteral("children"), children);
  808. }
  809. return object;
  810. }
  811. bool parseConditionExpression(
  812. const QJsonObject &object,
  813. const std::string &context,
  814. ConditionExpression *expression,
  815. ParseState *state)
  816. {
  817. std::string kind;
  818. if (!readString(object, "id", context, &expression->id, state)
  819. || !readString(object, "kind", context, &kind, state))
  820. {
  821. return false;
  822. }
  823. if (kind == "node")
  824. {
  825. QJsonObject node;
  826. if (!readObject(object, "node", context, &node, state))
  827. {
  828. return false;
  829. }
  830. LogicNode parsed_node;
  831. if (!parseLogicNode(node, context + ".node", &parsed_node, state))
  832. {
  833. return false;
  834. }
  835. expression->kind = ConditionExpressionKind::Node;
  836. expression->node = std::move(parsed_node);
  837. return true;
  838. }
  839. if (kind != "series" && kind != "parallel")
  840. {
  841. return state->fail(
  842. ProjectStorageError::InvalidField,
  843. context + ".kind must be node, series or parallel");
  844. }
  845. QJsonArray children;
  846. if (!readArray(object, "children", context, &children, state))
  847. {
  848. return false;
  849. }
  850. expression->kind = kind == "series"
  851. ? ConditionExpressionKind::Series : ConditionExpressionKind::Parallel;
  852. expression->children.reserve(static_cast<std::size_t>(children.size()));
  853. for (int index = 0; index < children.size(); ++index)
  854. {
  855. if (!children.at(index).isObject())
  856. {
  857. return state->fail(
  858. ProjectStorageError::InvalidField,
  859. context + ".children items must be objects");
  860. }
  861. ConditionExpression child;
  862. if (!parseConditionExpression(
  863. children.at(index).toObject(),
  864. context + ".children[" + std::to_string(index) + ']',
  865. &child,
  866. state))
  867. {
  868. return false;
  869. }
  870. expression->children.push_back(std::move(child));
  871. }
  872. return true;
  873. }
  874. QJsonObject serializeLadderRung(const LadderRung &rung)
  875. {
  876. QJsonObject object;
  877. object.insert(QStringLiteral("id"), fromUtf8(rung.id));
  878. object.insert(QStringLiteral("name"), fromUtf8(rung.name));
  879. object.insert(
  880. QStringLiteral("condition"),
  881. rung.condition.has_value()
  882. ? QJsonValue(serializeConditionExpression(*rung.condition))
  883. : QJsonValue(QJsonValue::Null));
  884. object.insert(
  885. QStringLiteral("output"),
  886. rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output))
  887. : QJsonValue(QJsonValue::Null));
  888. return object;
  889. }
  890. bool parseLadderRung(
  891. const QJsonObject &object,
  892. const std::string &context,
  893. LadderRung *rung,
  894. ParseState *state)
  895. {
  896. QJsonValue condition;
  897. QJsonValue output;
  898. if (!readString(object, "id", context, &rung->id, state)
  899. || !readString(object, "name", context, &rung->name, state)
  900. || !readValue(object, "output", context, &output, state))
  901. {
  902. return false;
  903. }
  904. if (!readValue(object, "condition", context, &condition, state))
  905. {
  906. return false;
  907. }
  908. if (condition.isNull())
  909. {
  910. rung->condition.reset();
  911. }
  912. else if (!condition.isObject())
  913. {
  914. return state->fail(
  915. ProjectStorageError::InvalidField,
  916. context + ".condition must be an object or null");
  917. }
  918. else
  919. {
  920. ConditionExpression parsed_condition;
  921. if (!parseConditionExpression(
  922. condition.toObject(), context + ".condition", &parsed_condition, state))
  923. {
  924. return false;
  925. }
  926. rung->condition = std::move(parsed_condition);
  927. }
  928. if (output.isNull())
  929. {
  930. rung->output.reset();
  931. return true;
  932. }
  933. if (!output.isObject())
  934. {
  935. return state->fail(
  936. ProjectStorageError::InvalidField,
  937. context + ".output must be an object or null");
  938. }
  939. LogicNode node;
  940. if (!parseLogicNode(output.toObject(), context + ".output", &node, state))
  941. {
  942. return false;
  943. }
  944. rung->output = std::move(node);
  945. return true;
  946. }
  947. QJsonObject serializeControlLogic(const ControlLogic &logic)
  948. {
  949. QJsonArray rungs;
  950. for (const LadderRung &rung : logic.rungs)
  951. {
  952. rungs.append(serializeLadderRung(rung));
  953. }
  954. QJsonObject object;
  955. object.insert(QStringLiteral("id"), fromUtf8(logic.id));
  956. object.insert(QStringLiteral("name"), fromUtf8(logic.name));
  957. object.insert(QStringLiteral("enabled"), logic.enabled);
  958. object.insert(QStringLiteral("rungs"), rungs);
  959. return object;
  960. }
  961. bool parseControlLogic(
  962. const QJsonObject &object,
  963. const std::string &context,
  964. ControlLogic *logic,
  965. ParseState *state)
  966. {
  967. QJsonArray rungs;
  968. if (!readString(object, "id", context, &logic->id, state)
  969. || !readString(object, "name", context, &logic->name, state)
  970. || !readBool(object, "enabled", context, &logic->enabled, state)
  971. || !readArray(object, "rungs", context, &rungs, state))
  972. {
  973. return false;
  974. }
  975. logic->rungs.reserve(static_cast<std::size_t>(rungs.size()));
  976. for (int index = 0; index < rungs.size(); ++index)
  977. {
  978. if (!rungs.at(index).isObject())
  979. {
  980. return state->fail(
  981. ProjectStorageError::InvalidField,
  982. context + ".rungs items must be objects");
  983. }
  984. LadderRung rung;
  985. if (!parseLadderRung(
  986. rungs.at(index).toObject(),
  987. context + ".rungs[" + std::to_string(index) + ']',
  988. &rung,
  989. state))
  990. {
  991. return false;
  992. }
  993. logic->rungs.push_back(std::move(rung));
  994. }
  995. return true;
  996. }
  997. // 将完整领域工程聚合为工程文件的顶层 JSON 对象
  998. QJsonObject serializeProject(const Project &project)
  999. {
  1000. QJsonArray pages;
  1001. for (const HmiPage &page : project.hmiPages)
  1002. {
  1003. pages.append(serializeHmiPage(page));
  1004. }
  1005. QJsonArray logics;
  1006. for (const ControlLogic &logic : project.controlLogics)
  1007. {
  1008. logics.append(serializeControlLogic(logic));
  1009. }
  1010. QJsonObject object;
  1011. object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion));
  1012. object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id));
  1013. object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name));
  1014. object.insert(QStringLiteral("hmiPages"), pages);
  1015. object.insert(QStringLiteral("controlLogics"), logics);
  1016. return object;
  1017. }
  1018. // 从顶层 JSON 对象解析完整工程,并在解析子对象前检查格式版本
  1019. bool parseProject(
  1020. const QJsonObject &object, Project *project, ParseState *state)
  1021. {
  1022. QJsonArray pages;
  1023. QJsonArray logics;
  1024. if (!readString(
  1025. object,
  1026. "formatVersion",
  1027. "project",
  1028. &project->metadata.formatVersion,
  1029. state))
  1030. {
  1031. return false;
  1032. }
  1033. // 不尝试猜测其他版本的结构,避免按当前格式错误解释数据
  1034. if (project->metadata.formatVersion != kCurrentFormatVersion)
  1035. {
  1036. return state->fail(
  1037. ProjectStorageError::UnsupportedVersion,
  1038. "unsupported project format version " + project->metadata.formatVersion);
  1039. }
  1040. if (!readString(object, "id", "project", &project->metadata.id, state)
  1041. || !readString(object, "name", "project", &project->metadata.name, state)
  1042. || !readArray(object, "hmiPages", "project", &pages, state)
  1043. || !readArray(object, "controlLogics", "project", &logics, state))
  1044. {
  1045. return false;
  1046. }
  1047. // 逐层解析时持续传递字段路径,任何失败都保留最初的具体位置
  1048. project->hmiPages.reserve(static_cast<std::size_t>(pages.size()));
  1049. for (int index = 0; index < pages.size(); ++index)
  1050. {
  1051. if (!pages.at(index).isObject())
  1052. {
  1053. return state->fail(
  1054. ProjectStorageError::InvalidField,
  1055. "project.hmiPages items must be objects");
  1056. }
  1057. HmiPage page;
  1058. if (!parseHmiPage(
  1059. pages.at(index).toObject(),
  1060. "project.hmiPages[" + std::to_string(index) + ']',
  1061. &page,
  1062. state))
  1063. {
  1064. return false;
  1065. }
  1066. project->hmiPages.push_back(std::move(page));
  1067. }
  1068. project->controlLogics.reserve(static_cast<std::size_t>(logics.size()));
  1069. for (int index = 0; index < logics.size(); ++index)
  1070. {
  1071. if (!logics.at(index).isObject())
  1072. {
  1073. return state->fail(
  1074. ProjectStorageError::InvalidField,
  1075. "project.controlLogics items must be objects");
  1076. }
  1077. ControlLogic logic;
  1078. if (!parseControlLogic(
  1079. logics.at(index).toObject(),
  1080. "project.controlLogics[" + std::to_string(index) + ']',
  1081. &logic,
  1082. state))
  1083. {
  1084. return false;
  1085. }
  1086. project->controlLogics.push_back(std::move(logic));
  1087. }
  1088. return true;
  1089. }
  1090. // 将 Qt 文件错误转换为领域层统一的工程保存失败结果
  1091. ProjectSaveResult saveFailure(
  1092. ProjectStorageError error, const QString &message)
  1093. {
  1094. return {false, error, toUtf8(message)};
  1095. }
  1096. } // namespace
  1097. // 校验工程后将其序列化,并通过 QSaveFile 原子写入目标文件
  1098. ProjectSaveResult JsonProjectStorage::save(
  1099. const Project &project, const std::string &file_path)
  1100. {
  1101. // 写文件前先执行领域校验,防止持久化内部关系不合法的工程
  1102. std::string validation_error;
  1103. if (!project.validate(&validation_error))
  1104. {
  1105. return {false, ProjectStorageError::InvalidProject, validation_error};
  1106. }
  1107. if (project.metadata.formatVersion != kCurrentFormatVersion)
  1108. {
  1109. return {false,
  1110. ProjectStorageError::UnsupportedVersion,
  1111. "unsupported project format version " + project.metadata.formatVersion};
  1112. }
  1113. // QSaveFile 先写临时文件,仅在 commit 成功后替换目标文件
  1114. QSaveFile file(fromUtf8(file_path));
  1115. if (!file.open(QIODevice::WriteOnly))
  1116. {
  1117. return saveFailure(ProjectStorageError::FileOpenFailed, file.errorString());
  1118. }
  1119. const QByteArray data = QJsonDocument(serializeProject(project)).toJson(
  1120. QJsonDocument::Indented);
  1121. // 短写入也视为失败,并取消临时文件提交
  1122. if (file.write(data) != data.size())
  1123. {
  1124. file.cancelWriting();
  1125. return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString());
  1126. }
  1127. if (!file.commit())
  1128. {
  1129. return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString());
  1130. }
  1131. return {true, ProjectStorageError::None, {}};
  1132. }
  1133. // 读取并解析工程文件,全部校验通过后才返回新的领域工程
  1134. ProjectLoadResult JsonProjectStorage::load(const std::string &file_path)
  1135. {
  1136. QFile file(fromUtf8(file_path));
  1137. if (!file.open(QIODevice::ReadOnly))
  1138. {
  1139. return {false,
  1140. {},
  1141. ProjectStorageError::FileOpenFailed,
  1142. toUtf8(file.errorString())};
  1143. }
  1144. const QByteArray data = file.readAll();
  1145. if (file.error() != QFileDevice::NoError)
  1146. {
  1147. return {false,
  1148. {},
  1149. ProjectStorageError::FileReadFailed,
  1150. toUtf8(file.errorString())};
  1151. }
  1152. // 顶层必须是 JSON 对象,数组或标量不能表示完整工程
  1153. QJsonParseError parse_error;
  1154. const QJsonDocument document = QJsonDocument::fromJson(data, &parse_error);
  1155. if (parse_error.error != QJsonParseError::NoError || !document.isObject())
  1156. {
  1157. const QString message = parse_error.error == QJsonParseError::NoError
  1158. ? QStringLiteral("project JSON root must be an object")
  1159. : parse_error.errorString();
  1160. return {false,
  1161. {},
  1162. ProjectStorageError::InvalidJson,
  1163. toUtf8(message)};
  1164. }
  1165. // 先在局部对象中完成结构解析,失败时不会暴露半成品工程
  1166. Project project;
  1167. ParseState state;
  1168. if (!parseProject(document.object(), &project, &state))
  1169. {
  1170. return {false, {}, state.error, state.message};
  1171. }
  1172. // JSON 字段合法不代表业务关系合法,还需执行领域层整体校验
  1173. std::string validation_error;
  1174. if (!project.validate(&validation_error))
  1175. {
  1176. return {false,
  1177. {},
  1178. ProjectStorageError::InvalidProject,
  1179. validation_error};
  1180. }
  1181. return {true, std::move(project), ProjectStorageError::None, {}};
  1182. }