From 780b8cc311a181ed2145406c52b4dbb58e569cf5 Mon Sep 17 00:00:00 2001 From: ypc <15051963820@163.com> Date: Tue, 7 Jul 2026 10:22:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20PLC=20XML=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=A7=A3=E6=9E=90=E4=B8=8E=E7=94=9F=E6=88=90=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E6=96=87=E6=A1=A3=EF=BC=88QT=E4=B8=8A=E4=BD=8D?= =?UTF-8?q?=E6=9C=BA=E5=BC=80=E5=8F=91=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 面向QT上位机开发人员,包含: - XML整体结构及元素详解(PLCConfig/Chain/Inputs/Outputs/Gate/Comb) - 递归Comb解析算法(C++/Qt + QDomDocument) - XML生成算法及checksum计算 - 简单链与组合链对照示例及逻辑等效表达式 - 信号编号获取接口(WebSocket) - 9条开发注意事项 --- ...LC_XML配置文件解析与生成_QT上位机开发文档.md | 749 ++++++++++++++++++ 1 file changed, 749 insertions(+) create mode 100644 mimo/工程/PLC_XML配置文件解析与生成_QT上位机开发文档.md diff --git a/mimo/工程/PLC_XML配置文件解析与生成_QT上位机开发文档.md b/mimo/工程/PLC_XML配置文件解析与生成_QT上位机开发文档.md new file mode 100644 index 0000000..cef5a49 --- /dev/null +++ b/mimo/工程/PLC_XML配置文件解析与生成_QT上位机开发文档.md @@ -0,0 +1,749 @@ +# PLC 配置文件(XML)解析与生成说明文档 + +> **面向对象**: QT 上位机开发人员 +> **配置文件**: `config/PLC/Reclose_logic.xml` +> **文件编码**: UTF-8 +> **文档日期**: 2026-07-07 + +--- + +## 一、概述 + +PLC 逻辑配置使用 XML 格式存储。一个 XML 文件定义一个 PLC 逻辑工程,由若干条**逻辑链(Chain)**组成。每条链描述一组输入信号经逻辑门运算后输出到目标信号的过程。 + +QT 上位机需要实现: +1. **解析(读取)**:将 XML 文件解析为内存中的逻辑图数据结构,用于画布展示和编辑 +2. **生成(写入)**:将用户编辑完的逻辑图保存/导出为 XML 文件,下发到 RTU 设备 + +--- + +## 二、XML 整体结构 + +``` + + + + + + + + + + ... + + + + + + + +``` + +### 2.1 根元素 `` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 工程名称,如 `"PLC Logic"` | +| `key` | string | 是 | 工程唯一标识,建议使用时间戳毫秒数(如 `"1783070862205"`) | +| `checksum` | string | 是 | 32 位 CRC 校验值,大写十六进制字符串(如 `"3486BE04"`)。通常使用 CRC-32/MPEG-2 算法对整个 XML 内容(不含 checksum 属性本身)计算得到 | + +--- + +## 三、元素详解 + +### 3.1 `` — 逻辑链 + +每条 `` 代表一组相关的逻辑运算。可独立存在,也可通过信号编号组合。 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `gates` | int | 是 | 链内逻辑门总数(含顶层 Gate 和 Comb 内部 Gate) | +| `inputs` | int | 是 | 链内输入端信号总数 | +| `outputs` | int | 是 | 链内输出端信号总数 | + +**两种形态**: + +| 形态 | 子元素 | 场景 | +|------|--------|------| +| **简单链** | `` + `` + `` | 单层逻辑:若干输入信号经一个逻辑门运算后输出 | +| **组合链** | `` + `` + `` | 多层嵌套逻辑:Comb 内部可嵌套子 Comb | + +### 3.2 `` — 输入信号集 + +```xml + + + + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `no` | int | 是 | **信号编号**,对应 RTU 数据中心 out 信号的序号(dc_signal_out 的 `no` 字段) | +| `x` | int | 否 | 可视化画布 X 坐标像素值,解析/生成时可忽略(仅用于前端画布渲染) | +| `y` | int | 否 | 可视化画布 Y 坐标像素值,解析/生成时可忽略 | + +> **关键映射关系**:`Signal.no` ↔ 数据中心 `dc_signal_out` 记录的 `no` 字段。QT 上位机需通过 WebSocket 或 HTTP API 获取信号列表(`signal_type` = `"out"`),从中取得各信号的 `no` 和 `saddr`(信号地址)。 + +### 3.3 `` — 输出信号集 + +```xml + + + +``` + +与 `` 结构完全相同。区别在于语义:`` 中的信号编号对应 **PLC 输出信号**(通常 `no >= 1361` 为 RTU 内置预留号段)。 + +### 3.4 `` — 逻辑门 + +```xml + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `type` | string | 是 | 逻辑门类型,取值见表(见下方) | +| `id` | int | 是 | 逻辑门序号,从 0 起递增,同类型门内唯一 | +| `x` | int | 否 | 可视化画布 X 坐标 | +| `y` | int | 否 | 可视化画布 Y 坐标 | + +#### 逻辑门类型对照表 + +| `type` 字符串 | 含义 | 输入数 | 说明 | +|---------------|------|--------|------| +| `OR` | 或门 | ≥1 | 任一输入为 1 则输出 1 | +| `AND` | 与门 | ≥1 | 全部输入为 1 则输出 1 | +| `NOT` | 非门 | 1 | 输入取反输出 | + +> **注意**:当前 XML 配置支持的逻辑门类型为 `OR`、`AND`、`NOT`。若后续扩展 `T62P`/`T62D`/`SR`/`RS`/`RISING`/`FALLING` 等时序元件,需同步更新类型对照表。 + +### 3.5 `` — 组合逻辑块(核心难点) + +`` 是**可递归嵌套**的逻辑组合块,用于表达多层级的复杂逻辑。 + +```xml + + + + + + ... + ... + + ... + + + + + + +``` + +**解析规则**: + +1. **本层 Inputs**:直接连到本层 Gate 的输入端 +2. **嵌套子 Comb**:子 Comb 的 Gate 输出 → 连到本层 Gate 的输入端(一根线) +3. **本层 Gate**:接收上述所有输入,运算后输出到 Outputs +4. **递归终止**:最内层 Comb 没有嵌套子 Comb,只有 Inputs → Gate → Outputs + +**数据流示意**: + +``` +[本层Inputs] ──┐ + ├──→ [本层 Gate] ──→ [本层 Outputs] +[子Comb Gate] ─┘ +``` + +--- + +## 四、完整解析算法(C++ / Qt) + +### 4.1 数据结构定义 + +```cpp +#include +#include +#include +#include + +// 逻辑门类型枚举 +enum GateType { + GATE_OR = 2, + GATE_AND = 3, + GATE_NOT = 4 + // 未来可扩展: GATE_T62P=5, GATE_T62D=6, ... +}; + +// 信号节点 +struct PlcSignal { + int no; // 信号编号(映射 dc_signal_out.no) + int x, y; // 画布坐标 +}; + +// 逻辑门 +struct PlcGate { + GateType type; + int id; // 同类型门内序号 + int x, y; // 画布坐标 +}; + +// 连线 +struct PlcLink { + int srcType; // 源节点类型: 1=输入信号, 2~4=逻辑门 + int srcId; // 源节点ID + int dstType; // 目标节点类型: 0=输出信号, 2~4=逻辑门 + int dstId; // 目标节点ID +}; + +// Comb 组合块(递归结构) +struct PlcComb { + QVector inputs; // 本层输入信号 + QVector subCombs; // 嵌套子Comb + PlcGate gate; // 本层逻辑门 + QVector outputs; // 本层输出信号 +}; + +// 逻辑链 +struct PlcChain { + int gatesCount; // gates 属性 + int inputsCount; // inputs 属性 + int outputsCount; // outputs 属性 + + bool isComb; // true=组合链, false=简单链 + QVector inputs; // (简单链) 输入信号 + PlcGate gate; // (简单链) 逻辑门 + PlcComb comb; // (组合链) 顶层Comb + QVector outputs; // 输出信号 +}; + +// 整个配置文件 +struct PlcConfig { + QString name; + QString key; + QString checksum; + QVector chains; +}; +``` + +### 4.2 XML 解析实现 + +```cpp +#include +#include +#include + +// 从 XML 字符串解析逻辑门类型 +static GateType parseGateType(const QString &s) { + if (s == "OR") return GATE_OR; + if (s == "AND") return GATE_AND; + if (s == "NOT") return GATE_NOT; + return GATE_OR; // 默认 +} + +// 从 Gate 类型得到字符串 (生成 XML 用) +static QString gateTypeToString(GateType t) { + switch (t) { + case GATE_OR: return "OR"; + case GATE_AND: return "AND"; + case GATE_NOT: return "NOT"; + } + return "OR"; +} + +// 解析 元素 +static PlcSignal parseSignal(const QDomElement &el) { + PlcSignal s; + s.no = el.attribute("no").toInt(); + s.x = el.attribute("x").toInt(); + s.y = el.attribute("y").toInt(); + return s; +} + +// 解析 元素 +static PlcGate parseGate(const QDomElement &el) { + PlcGate g; + g.type = parseGateType(el.attribute("type")); + g.id = el.attribute("id").toInt(); + g.x = el.attribute("x").toInt(); + g.y = el.attribute("y").toInt(); + return g; +} + +// 解析 元素 +static QVector parseSignalList(const QDomElement &parent, + const QString &tagName) { + QVector list; + QDomElement container = parent.firstChildElement(tagName); + if (!container.isNull()) { + QDomElement sig = container.firstChildElement("Signal"); + while (!sig.isNull()) { + list.append(parseSignal(sig)); + sig = sig.nextSiblingElement("Signal"); + } + } + return list; +} + +// 递归解析 元素 +static bool parseComb(const QDomElement &combEl, PlcComb &comb) { + if (combEl.isNull()) return false; + + // 1. 本层直接输入 + comb.inputs = parseSignalList(combEl, "Inputs"); + + // 2. 递归解析嵌套子 Comb + QDomElement subComb = combEl.firstChildElement("Comb"); + while (!subComb.isNull()) { + PlcComb sub; + if (parseComb(subComb, sub)) { + comb.subCombs.append(sub); + } + subComb = subComb.nextSiblingElement("Comb"); + } + + // 3. 本层 Gate + QDomElement gateEl = combEl.firstChildElement("Gate"); + if (!gateEl.isNull()) { + comb.gate = parseGate(gateEl); + } + + // 4. 本层输出 + comb.outputs = parseSignalList(combEl, "Outputs"); + return true; +} + +// 解析整条 +static PlcChain parseChain(const QDomElement &chainEl) { + PlcChain chain; + chain.gatesCount = chainEl.attribute("gates").toInt(); + chain.inputsCount = chainEl.attribute("inputs").toInt(); + chain.outputsCount = chainEl.attribute("outputs").toInt(); + + // 判断是否有 Comb(组合链) + QDomElement combEl = chainEl.firstChildElement("Comb"); + chain.isComb = !combEl.isNull(); + + if (chain.isComb) { + // 组合链: 解析顶层 Comb + parseComb(combEl, chain.comb); + + // 链级顶层 Gate(在 Comb 之后) + QDomNodeList children = chainEl.childNodes(); + bool foundComb = false; + for (int i = 0; i < children.count(); i++) { + QDomElement el = children.at(i).toElement(); + if (el.tagName() == "Comb") { foundComb = true; continue; } + if (foundComb && el.tagName() == "Gate") { + chain.gate = parseGate(el); + break; + } + } + + // 链级顶层 Outputs + chain.outputs = parseSignalList(chainEl, "Outputs"); + } else { + // 简单链: Inputs → Gate → Outputs + chain.inputs = parseSignalList(chainEl, "Inputs"); + QDomElement gEl = chainEl.firstChildElement("Gate"); + if (!gEl.isNull()) chain.gate = parseGate(gEl); + chain.outputs = parseSignalList(chainEl, "Outputs"); + } + return chain; +} + +// 主解析函数: 从文件路径加载 XML +PlcConfig parseXmlFile(const QString &filePath) { + PlcConfig config; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Cannot open file:" << filePath; + return config; + } + + QDomDocument doc; + if (!doc.setContent(&file)) { + qWarning() << "XML parse error:" << filePath; + file.close(); + return config; + } + file.close(); + + QDomElement root = doc.documentElement(); + if (root.tagName() != "PLCConfig") { + qWarning() << "Root element is not "; + return config; + } + + config.name = root.attribute("name"); + config.key = root.attribute("key"); + config.checksum = root.attribute("checksum"); + + QDomElement chainEl = root.firstChildElement("Chain"); + while (!chainEl.isNull()) { + config.chains.append(parseChain(chainEl)); + chainEl = chainEl.nextSiblingElement("Chain"); + } + + qDebug() << "Parsed" << config.chains.size() << "chains from" << filePath; + return config; +} +``` + +--- + +## 五、XML 生成算法(C++ / Qt) + +### 5.1 生成实现 + +```cpp +#include +#include +#include + +// 计算 CRC-32/MPEG-2(与 RTU 一致) +// 简化版: 实际应使用完整的 CRC32 MPEG2 查表算法 +// 此处仅示意,生产环境请使用完整 CRC 实现库(如 Qt 的 qChecksum 或 zlib crc32) + +// 生成 元素 +static QDomElement createSignalEl(QDomDocument &doc, const PlcSignal &sig) { + QDomElement el = doc.createElement("Signal"); + el.setAttribute("no", sig.no); + el.setAttribute("x", sig.x); + el.setAttribute("y", sig.y); + return el; +} + +// 生成 元素 +static QDomElement createGateEl(QDomDocument &doc, const PlcGate &gate) { + QDomElement el = doc.createElement("Gate"); + el.setAttribute("type", gateTypeToString(gate.type)); + el.setAttribute("id", gate.id); + el.setAttribute("x", gate.x); + el.setAttribute("y", gate.y); + return el; +} + +// 生成 / 容器 +static QDomElement createSignalListEl(QDomDocument &doc, + const QString &tagName, + const QVector &signals) { + QDomElement el = doc.createElement(tagName); + for (const auto &s : signals) { + el.appendChild(createSignalEl(doc, s)); + } + return el; +} + +// 递归生成 元素 +static QDomElement createCombEl(QDomDocument &doc, const PlcComb &comb) { + QDomElement el = doc.createElement("Comb"); + + // Inputs + if (!comb.inputs.isEmpty()) { + el.appendChild(createSignalListEl(doc, "Inputs", comb.inputs)); + } + + // 嵌套子 Comb + for (const auto &sub : comb.subCombs) { + el.appendChild(createCombEl(doc, sub)); + } + + // Gate + el.appendChild(createGateEl(doc, comb.gate)); + + // Outputs + if (!comb.outputs.isEmpty()) { + el.appendChild(createSignalListEl(doc, "Outputs", comb.outputs)); + } + + return el; +} + +// 生成 元素 +static QDomElement createChainEl(QDomDocument &doc, const PlcChain &chain) { + QDomElement el = doc.createElement("Chain"); + el.setAttribute("gates", chain.gatesCount); + el.setAttribute("inputs", chain.inputsCount); + el.setAttribute("outputs", chain.outputsCount); + + if (chain.isComb) { + // 组合链: Comb + Gate + Outputs + el.appendChild(createCombEl(doc, chain.comb)); + // Gate(仅在 Comb 后有意义时输出) + if (chain.gate.type == GATE_OR || + chain.gate.type == GATE_AND || + chain.gate.type == GATE_NOT) { + el.appendChild(createGateEl(doc, chain.gate)); + } + if (!chain.outputs.isEmpty()) { + el.appendChild(createSignalListEl(doc, "Outputs", chain.outputs)); + } + } else { + // 简单链: Inputs → Gate → Outputs + if (!chain.inputs.isEmpty()) { + el.appendChild(createSignalListEl(doc, "Inputs", chain.inputs)); + } + el.appendChild(createGateEl(doc, chain.gate)); + if (!chain.outputs.isEmpty()) { + el.appendChild(createSignalListEl(doc, "Outputs", chain.outputs)); + } + } + return el; +} + +// 主生成函数: 将配置写入 XML 文件 +bool writeXmlFile(const PlcConfig &config, const QString &filePath, + const QString &checksumHex) { + QDomDocument doc; + + // XML 声明 + QDomProcessingInstruction pi = + doc.createProcessingInstruction("xml", + "version=\"1.0\" encoding=\"UTF-8\""); + doc.appendChild(pi); + + // 根元素 + QDomElement root = doc.createElement("PLCConfig"); + root.setAttribute("name", config.name); + root.setAttribute("key", config.key); + root.setAttribute("checksum", checksumHex); // CRC32 十六进制大写 + doc.appendChild(root); + + // 所有 Chain + for (const auto &chain : config.chains) { + root.appendChild(createChainEl(doc, chain)); + } + + // 写入文件 + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Cannot write file:" << filePath; + return false; + } + + QTextStream stream(&file); + stream.setCodec("UTF-8"); + doc.save(stream, 4); // 4 空格缩进 + file.close(); + + qDebug() << "XML saved to" << filePath; + return true; +} +``` + +### 5.2 checksum 计算方法 + +```cpp +#include // 或使用 Qt + 自定义 CRC32 MPEG2 表 + +// CRC-32/MPEG-2 多项式: 0x04C11DB7, 初始值: 0xFFFFFFFF +// 注意: 计算时应对 XML 字符串的内容计算,**不含** checksum 属性自身 +// 实战中建议调用 RTU 侧接口获取 checksum 值,或在生成 XML 后 +// 通过 RTU 命令行方式让设备重新计算 checksum +``` + +> **建议**:`key` 字段用 `QDateTime::currentMSecsSinceEpoch()` 生成时间戳毫秒字符串。`checksum` 字段可调用 RTU 侧提供的 CRC 计算接口,或在生成 XML 后由 RTU 自动校验/更新。 + +--- + +## 六、简单链与组合链对照 + +### 6.1 简单链示例 + +```xml + + + + + + + + + + + +``` + +**逻辑等效**:`out[1361] = in[59] OR in[60]` + +### 6.2 组合链示例 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**逻辑等效**: +``` +内层1 Gate(AND id=0): out[1363..1365] = in[62] AND in[63] AND in[64] AND in[65] +内层2 Gate(AND id=1): out[1366] = in[66] AND 内层1_Gate +外层 Gate(OR id=1): out[1367] = in[67] OR 内层2_Gate +链级 Gate(NOT id=1): out[1368] = NOT 外层_Gate +``` + +--- + +## 七、信号编号(no)的获取 + +QT 上位机需要通过 WebSocket 与 RTU 通信获取当前已注册的信号列表。 + +### 7.1 请求信号列表 + +```json +{ + "curd": "get", + "signal_type": "out" +} +``` + +### 7.2 响应格式 + +RTU 返回 out 信号数组,每条信号包含: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `no` | int | 信号序号(即 XML 中的 `Signal.no`) | +| `saddr` | string | 信号地址,如 `"plc.st.out.0"` | +| `desc` | string | 信号描述,如 `"plc输出0"` | +| `val` | variant | 当前值 | +| `data_type` | string | 数据类型 | + +### 7.3 输出信号号段约定 + +- **RTU 内置 PLC 输入信号**:`no` 在 **59–67** 左右(实现中通过 `out` 信号表动态注册) +- **RTU 内置 PLC 输出信号**:`no` 在 **1361–1368** 左右 +- QT 上位机应**动态读取信号列表**而非硬编码号段 + +--- + +## 八、注意事项 + +| 编号 | 注意点 | +|------|--------| +| 1 | `` 最多嵌套 **3 层**(见示例),更深层级需验证 RTU 固件支持 | +| 2 | `checksum` 必须使用 CRC-32/MPEG-2 算法,且大写十六进制格式 | +| 3 | `key` 建议使用 `QDateTime::currentMSecsSinceEpoch()` 生成,保证唯一性 | +| 4 | `x`/`y` 坐标可不填写(设为 0),RTU 解析时忽略这些字段 | +| 5 | XML 文件编码必须为 **UTF-8**,不要使用 UTF-8 BOM | +| 6 | `` 的 `gates`/`inputs`/`outputs` 属性值为统计数量,生成时必须准确填写 | +| 7 | Comb 内 Gate 的 `id` 表示**同类型逻辑门内的序号**(OR/AND/NOT 分别计数,从 0 起递增) | +| 8 | 生成 XML 后建议通过 RTU Web 界面或命令行 `plc cfg reload` 触发配置重新加载 | +| 9 | 一个 `` 可含多条 ``,但目前 RTU 将所有 Chain 合并为一个逻辑图处理 | + +--- + +## 九、附录:完整 XML 示例文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +> **文档维护**:本文档基于 RTU 固件 [src/system/libplc/src/plc.cpp](src/system/libplc/src/plc.cpp) 中的 XML 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。