968 lines
32 KiB
Markdown
968 lines
32 KiB
Markdown
# PLC 配置文件(XML)解析与生成说明文档
|
||
|
||
> **面向对象**: QT 上位机开发人员
|
||
> **配置文件**: `config/PLC/Reclose_logic.xml`
|
||
> **文件编码**: UTF-8
|
||
> **文档日期**: 2026-07-08
|
||
|
||
---
|
||
|
||
## 一、概述
|
||
|
||
PLC 逻辑配置使用 XML 格式存储。一个 XML 文件定义一个 PLC 逻辑工程,由若干条**逻辑链(Chain)**组成。每条链描述一组输入信号经逻辑门运算后输出到目标信号的过程。
|
||
|
||
QT 上位机需要实现:
|
||
1. **解析(读取)**:将 XML 文件解析为内存中的逻辑图数据结构,用于画布展示和编辑
|
||
2. **生成(写入)**:将用户编辑完的逻辑图保存/导出为 XML 文件,下发到 RTU 设备
|
||
|
||
---
|
||
|
||
## 二、XML 整体结构
|
||
|
||
```
|
||
<PLCConfig name="..." key="..." checksum="...">
|
||
<Chain gates="n" inputs="m" outputs="k"> <!-- 简单链 -->
|
||
<Inputs> <Signal .../> </Inputs>
|
||
<Gate .../>
|
||
<Outputs> <Signal .../> </Outputs>
|
||
</Chain>
|
||
<Chain gates="n" inputs="m" outputs="k"> <!-- 组合链(含Comb嵌套) -->
|
||
<Comb>
|
||
<Inputs> <Signal .../> </Inputs>
|
||
<Comb> ... </Comb> <!-- 嵌套子Comb -->
|
||
<Gate .../>
|
||
<Outputs> <Signal .../> </Outputs>
|
||
</Comb>
|
||
<Gate .../> <!-- 链级顶层Gate -->
|
||
<Outputs> <Signal .../> </Outputs>
|
||
</Chain>
|
||
</PLCConfig>
|
||
```
|
||
|
||
### 2.1 根元素 `<PLCConfig>`
|
||
|
||
| 属性 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `name` | string | 是 | 工程名称,如 `"PLC Logic"` |
|
||
| `key` | string | 是 | 工程唯一标识,建议使用时间戳毫秒数(如 `"1783070862205"`) |
|
||
| `checksum` | string | 是 | 32 位 CRC 校验值,大写十六进制字符串(如 `"3486BE04"`)。通常使用 CRC-32/MPEG-2 算法对整个 XML 内容(不含 checksum 属性本身)计算得到 |
|
||
|
||
---
|
||
|
||
## 三、元素详解
|
||
|
||
### 3.1 `<Chain>` — 逻辑链
|
||
|
||
每条 `<Chain>` 代表一组相关的逻辑运算。可独立存在,也可通过信号编号组合。
|
||
|
||
| 属性 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `gates` | int | 是 | 链内逻辑门总数(含顶层 Gate 和 Comb 内部 Gate) |
|
||
| `inputs` | int | 是 | 链内输入端信号总数 |
|
||
| `outputs` | int | 是 | 链内输出端信号总数 |
|
||
|
||
**两种形态**:
|
||
|
||
| 形态 | 子元素 | 场景 |
|
||
|------|--------|------|
|
||
| **简单链** | `<Inputs>` + `<Gate>` + `<Outputs>` | 单层逻辑:若干输入信号经一个逻辑门运算后输出 |
|
||
| **组合链** | `<Comb>` + `<Gate>` + `<Outputs>` | 多层嵌套逻辑:Comb 内部可嵌套子 Comb |
|
||
|
||
### 3.2 `<Inputs>` — 输入信号集
|
||
|
||
```xml
|
||
<Inputs>
|
||
<Signal no="59" x="-676" y="69" />
|
||
<Signal no="60" x="-674" y="183" />
|
||
</Inputs>
|
||
```
|
||
|
||
| 属性 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `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 `<Outputs>` — 输出信号集
|
||
|
||
```xml
|
||
<Outputs>
|
||
<Signal no="1361" x="-17" y="137" />
|
||
</Outputs>
|
||
```
|
||
|
||
与 `<Inputs>` 结构完全相同。区别在于语义:`<Outputs>` 中的信号编号对应 **PLC 输出信号**(通常 `no >= 1361` 为 RTU 内置预留号段)。
|
||
|
||
### 3.4 `<Gate>` — 逻辑门
|
||
|
||
```xml
|
||
<Gate type="OR" id="0" x="-346" y="141" />
|
||
```
|
||
|
||
| 属性 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `type` | string | 是 | 逻辑门类型,取值见下表 |
|
||
| `id` | int | 是 | 同类型门内序号,从 0 起递增,同类型门内唯一 |
|
||
| `x` | int | 否 | 可视化画布 X 坐标 |
|
||
| `y` | int | 否 | 可视化画布 Y 坐标 |
|
||
| `delayMs` | int | 否 | **延迟毫秒数**(仅 `T62P`/`T62D` 有效),默认 1000 |
|
||
| `R` | string | 否 | **复位输入来源**(仅 `SR`/`RS` 有效),格式见下方说明 |
|
||
| `S` | string | 否 | **置位输入来源**(仅 `SR`/`RS` 有效),格式见下方说明 |
|
||
|
||
#### 逻辑门类型对照表
|
||
|
||
| `type` 字符串 | 含义 | 输入数 | 输出数 | 说明 |
|
||
|---------------|------|--------|--------|------|
|
||
| `OR` | 或门 | 1~10 | 1~10 | 任一输入为 1 则输出 1 |
|
||
| `AND` | 与门 | 1~10 | 1~10 | 全部输入为 1 则输出 1 |
|
||
| `NOT` | 非门 | 1 | 1~10 | 输入取反输出 |
|
||
| `T62P` | 62p 延时 | 1 | 1~10 | 输入 1 延时 `delayMs` ms 后输出 1,输入 0 立即输出 0 |
|
||
| `T62D` | 62d 延时 | 1 | 1~10 | 输入 1 立即输出 1,输入 0 延时 `delayMs` ms 后输出 0 |
|
||
| `RISING` | 上升沿 | 1 | 1~10 | 0→1 跳变时输出脉冲 |
|
||
| `FALLING` | 下降沿 | 1 | 1~10 | 1→0 跳变时输出脉冲 |
|
||
| `SR` | SR 触发器 | 2 | 1~10 | S 置位优先。其中引脚顺序为 pin0=S(置位),pin1=R(复位) |
|
||
| `RS` | RS 触发器 | 2 | 1~10 | R 复位优先。其中引脚顺序为 pin0=R(复位),pin1=S(置位) |
|
||
|
||
#### T62P/T62D 延时示例
|
||
|
||
```xml
|
||
<Gate type="T62P" id="0" delayMs="1000" x="-345" y="1063" />
|
||
<Gate type="T62D" id="0" delayMs="500" x="-334" y="1185" />
|
||
```
|
||
|
||
#### SR/RS 触发器的 R/S 属性
|
||
|
||
`R` 和 `S` 的值是**来源描述字符串**,格式为 `Comb#N` 或 `Input#N`:
|
||
|
||
| 值格式 | 含义 |
|
||
|--------|------|
|
||
| `Input#59` | 该引脚由输入信号 `no=59` 直接驱动 |
|
||
| `Comb#0` | 该引脚由 `id=0` 的 Comb 块内的 Gate 输出驱动 |
|
||
|
||
```xml
|
||
<!-- SR: S 接 Input#13,R 接 Input#14 -->
|
||
<Gate type="SR" id="0" S="Input#13" R="Input#14" x="-329" y="1631" />
|
||
|
||
<!-- RS: R 接 Comb#0 的输出,S 接 Input#15 -->
|
||
<Gate type="RS" id="0" R="Comb#0" S="Input#15" x="0" y="2136" />
|
||
```
|
||
|
||
> **注意**:SR/RS 的两个引脚各有独立角色(S/R),连线时每个引脚只能接一根线。QT 上位机需分别显示两个输入引脚并标注 S/R 标签。
|
||
|
||
### 3.5 `<Comb>` — 组合逻辑块(核心难点)
|
||
|
||
`<Comb>` 是**可递归嵌套**的逻辑组合块,用于表达多层级的复杂逻辑。每个 `<Comb>` 必须有唯一的 `id` 属性。
|
||
|
||
| 属性 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `id` | int | 是 | Comb 块唯一标识,从 0 起递增。自底向上分配(最内层 Comb id=0,向外递增)。用于 RS/SR 引脚引用(`R="Comb#0"`) |
|
||
|
||
```xml
|
||
<Comb id="2"> <!-- 最外层 Comb -->
|
||
<Inputs> <!-- 本层直接输入信号 -->
|
||
<Signal no="67" x="-6" y="937" />
|
||
</Inputs>
|
||
<Comb id="1"> <!-- 嵌套子层 -->
|
||
<Inputs>
|
||
<Signal no="66" x="-333" y="876" />
|
||
</Inputs>
|
||
<Comb id="0"> <!-- 最内层 -->
|
||
<Inputs>
|
||
<Signal no="62" x="-669" y="413" />
|
||
</Inputs>
|
||
<Gate type="AND" id="0" x="-346" y="595" />
|
||
<Outputs>
|
||
<Signal no="1363" x="-9" y="490" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="AND" id="1" x="-42" y="829" />
|
||
<Outputs>
|
||
<Signal no="1366" x="324" y="773" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="OR" id="1" x="288" y="890" />
|
||
<Outputs>
|
||
<Signal no="1367" x="627" y="836" />
|
||
</Outputs>
|
||
</Comb>
|
||
```
|
||
|
||
**解析规则**:
|
||
|
||
1. **本层 Inputs**:直接连到本层 Gate 的输入端
|
||
2. **嵌套子 Comb**:子 Comb 的 Gate 输出 → 连到本层 Gate 的输入端(一根线)
|
||
3. **本层 Gate**:接收上述所有输入,运算后输出到 Outputs
|
||
4. **递归终止**:最内层 Comb 没有嵌套子 Comb,只有 Inputs → Gate → Outputs
|
||
5. **Comb id 分配**:后序遍历(自底向上),最内层 id=0,向外逐层递增
|
||
|
||
**数据流示意**:
|
||
|
||
```
|
||
[本层Inputs] ──┐
|
||
├──→ [本层 Gate] ──→ [本层 Outputs]
|
||
[子Comb Gate] ─┘
|
||
```
|
||
|
||
### 3.6 组合链中的链级元素
|
||
|
||
与之前不同,组合链现已支持在 `<Comb>` 块与链级 `<Gate>` 之间插入链级 `<Inputs>`,用于 SR/RS 触发器等需要同时接收 Comb 输出和直接 Input 信号的场景:
|
||
|
||
```xml
|
||
<Chain gates="2" inputs="4" outputs="3">
|
||
<Comb id="0">
|
||
<Inputs>
|
||
<Signal no="10" x="-665" y="1804" />
|
||
<Signal no="11" x="-671" y="1932" />
|
||
<Signal no="12" x="-671" y="2054" />
|
||
</Inputs>
|
||
<Gate type="AND" id="2" x="-322" y="1938" />
|
||
<Outputs>
|
||
<Signal no="1374" x="18" y="1881" />
|
||
<Signal no="1375" x="23" y="2004" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Inputs> <!-- 链级输入:直接连到链级 Gate -->
|
||
<Signal no="15" x="0" y="2136" />
|
||
</Inputs>
|
||
<Gate type="RS" id="0" x="0" y="2136" R="Comb#0" S="Input#15" />
|
||
<Outputs>
|
||
<Signal no="1376" x="375" y="2131" />
|
||
</Outputs>
|
||
</Chain>
|
||
```
|
||
|
||
**组合链子元素顺序**:`<Comb>` → `<Inputs>`(可选)→ `<Gate>` → `<Outputs>`
|
||
|
||
---
|
||
|
||
## 四、完整解析算法(C++ / Qt)
|
||
|
||
### 4.1 数据结构定义
|
||
|
||
```cpp
|
||
#include <QString>
|
||
#include <QVector>
|
||
#include <QPoint>
|
||
#include <QtXml/QDomDocument>
|
||
|
||
// 逻辑门类型枚举
|
||
enum GateType {
|
||
GATE_OR = 2,
|
||
GATE_AND = 3,
|
||
GATE_NOT = 4,
|
||
GATE_T62P = 5, // 62p 延时
|
||
GATE_T62D = 6, // 62d 延时
|
||
GATE_RISING = 7, // 上升沿
|
||
GATE_SR = 8, // SR 触发器
|
||
GATE_RS = 9, // RS 触发器
|
||
GATE_FALLING = 10 // 下降沿
|
||
};
|
||
|
||
// 信号节点
|
||
struct PlcSignal {
|
||
int no; // 信号编号(映射 dc_signal_out.no)
|
||
int x, y; // 画布坐标
|
||
};
|
||
|
||
// 逻辑门
|
||
struct PlcGate {
|
||
GateType type;
|
||
int id; // 同类型门内序号
|
||
int x, y; // 画布坐标
|
||
int delayMs; // 延时毫秒数(仅 T62P/T62D,默认 1000)
|
||
QString rsSSrc; // S 引脚来源(仅 SR/RS,如 "Input#13"、"Comb#0")
|
||
QString rsRSrc; // R 引脚来源(仅 SR/RS)
|
||
int combId; // 所在 Comb 块的 id(用于生成 R/S 引用)
|
||
};
|
||
|
||
// 连线
|
||
struct PlcLink {
|
||
int srcType; // 源节点类型编码
|
||
int srcId; // 源节点 ID
|
||
int dstType; // 目标节点类型编码
|
||
int dstId; // 目标节点 ID
|
||
int dstPin; // 目标输入引脚序号(仅 SR/RS 有效:SR: 0=S/1=R, RS: 0=R/1=S; 其他门默认 -1)
|
||
};
|
||
|
||
// Comb 组合块
|
||
struct PlcComb {
|
||
int id; // Comb 块标识(自底向上编号)
|
||
QVector<PlcSignal> inputs; // 本层输入信号
|
||
QVector<PlcComb> subCombs; // 嵌套子 Comb
|
||
PlcGate gate; // 本层逻辑门
|
||
QVector<PlcSignal> outputs; // 本层输出信号
|
||
};
|
||
|
||
// 逻辑链
|
||
struct PlcChain {
|
||
int gatesCount; // gates 属性
|
||
int inputsCount; // inputs 属性
|
||
int outputsCount; // outputs 属性
|
||
|
||
bool isComb; // true=组合链, false=简单链
|
||
QVector<PlcSignal> inputs; // (简单链/组合链) 链级输入信号
|
||
QVector<PlcComb> topCombs; // (组合链) 顶层 Comb 列表
|
||
PlcGate gate; // 链级逻辑门(组合链的顶层Gate,或简单链的Gate)
|
||
QVector<PlcSignal> outputs; // 输出信号
|
||
};
|
||
|
||
// 整个配置文件
|
||
struct PlcConfig {
|
||
QString name;
|
||
QString key;
|
||
QString checksum;
|
||
QVector<PlcChain> chains;
|
||
};
|
||
```
|
||
|
||
### 4.2 XML 解析实现
|
||
|
||
```cpp
|
||
#include <QtXml/QDomDocument>
|
||
#include <QFile>
|
||
#include <QDebug>
|
||
|
||
// 从 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;
|
||
if (s == "T62P") return GATE_T62P;
|
||
if (s == "T62D") return GATE_T62D;
|
||
if (s == "RISING") return GATE_RISING;
|
||
if (s == "SR") return GATE_SR;
|
||
if (s == "RS") return GATE_RS;
|
||
if (s == "FALLING") return GATE_FALLING;
|
||
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";
|
||
case GATE_T62P: return "T62P";
|
||
case GATE_T62D: return "T62D";
|
||
case GATE_RISING: return "RISING";
|
||
case GATE_SR: return "SR";
|
||
case GATE_RS: return "RS";
|
||
case GATE_FALLING: return "FALLING";
|
||
}
|
||
return "OR";
|
||
}
|
||
|
||
// 解析 <Signal> 元素
|
||
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;
|
||
}
|
||
|
||
// 解析 <Gate> 元素
|
||
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();
|
||
g.delayMs = el.attribute("delayMs", "1000").toInt(); // 默认 1000ms
|
||
g.rsSSrc = el.attribute("S", ""); // SR/RS 的 S 来源
|
||
g.rsRSrc = el.attribute("R", ""); // SR/RS 的 R 来源
|
||
g.combId = -1; // 由解析过程赋值
|
||
return g;
|
||
}
|
||
|
||
// 解析 <Inputs> 或 <Outputs> 元素
|
||
static QVector<PlcSignal> parseSignalList(const QDomElement &parent,
|
||
const QString &tagName) {
|
||
QVector<PlcSignal> 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;
|
||
}
|
||
|
||
// 递归解析 <Comb> 元素
|
||
static bool parseComb(const QDomElement &combEl, PlcComb &comb) {
|
||
if (combEl.isNull()) return false;
|
||
|
||
// 0. 读取 Comb id 属性
|
||
comb.id = combEl.attribute("id", "0").toInt();
|
||
|
||
// 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);
|
||
comb.gate.combId = comb.id; // 回填所在 Comb 的 id
|
||
}
|
||
|
||
// 4. 本层输出
|
||
comb.outputs = parseSignalList(combEl, "Outputs");
|
||
return true;
|
||
}
|
||
|
||
// 解析整条 <Chain>
|
||
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(可能有多个)
|
||
QDomElement cEl = combEl;
|
||
while (!cEl.isNull()) {
|
||
PlcComb comb;
|
||
parseComb(cEl, comb);
|
||
chain.topCombs.append(comb);
|
||
cEl = cEl.nextSiblingElement("Comb");
|
||
}
|
||
|
||
// 链级 Inputs(Comb 之后,Gate 之前,如 RS/SR 的直接 Input 连接)
|
||
chain.inputs = parseSignalList(chainEl, "Inputs");
|
||
|
||
// 链级 Gate
|
||
QDomElement gEl = chainEl.firstChildElement("Gate");
|
||
if (!gEl.isNull()) chain.gate = parseGate(gEl);
|
||
|
||
// 链级 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 <PLCConfig>";
|
||
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 <QtXml/QDomDocument>
|
||
#include <QFile>
|
||
#include <QtCore/QCryptographicHash>
|
||
|
||
// 计算 CRC-32/MPEG-2(与 RTU 一致)
|
||
// 简化版: 实际应使用完整的 CRC32 MPEG2 查表算法
|
||
// 此处仅示意,生产环境请使用完整 CRC 实现库(如 Qt 的 qChecksum 或 zlib crc32)
|
||
|
||
// 生成 <Signal> 元素
|
||
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;
|
||
}
|
||
|
||
// 生成 <Gate> 元素
|
||
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);
|
||
|
||
// T62P/T62D: 延时毫秒
|
||
if (gate.type == GATE_T62P || gate.type == GATE_T62D) {
|
||
el.setAttribute("delayMs", gate.delayMs);
|
||
}
|
||
|
||
// SR/RS: 输出 R/S 属性(格式 "Comb#N" 或 "Input#N")
|
||
if (gate.type == GATE_SR || gate.type == GATE_RS) {
|
||
if (!gate.rsSSrc.isEmpty()) el.setAttribute("S", gate.rsSSrc);
|
||
if (!gate.rsRSrc.isEmpty()) el.setAttribute("R", gate.rsRSrc);
|
||
}
|
||
|
||
return el;
|
||
}
|
||
|
||
// 生成 <Inputs>/<Outputs> 容器
|
||
static QDomElement createSignalListEl(QDomDocument &doc,
|
||
const QString &tagName,
|
||
const QVector<PlcSignal> &signals) {
|
||
QDomElement el = doc.createElement(tagName);
|
||
for (const auto &s : signals) {
|
||
el.appendChild(createSignalEl(doc, s));
|
||
}
|
||
return el;
|
||
}
|
||
|
||
// 递归生成 <Comb> 元素
|
||
static QDomElement createCombEl(QDomDocument &doc, const PlcComb &comb) {
|
||
QDomElement el = doc.createElement("Comb");
|
||
el.setAttribute("id", comb.id); // Comb id 属性
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 生成 <Chain> 元素
|
||
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
|
||
for (const auto &comb : chain.topCombs) {
|
||
el.appendChild(createCombEl(doc, comb));
|
||
}
|
||
// 链级 Inputs(如有直接连接,如 RS/SR 的 Input 直连)
|
||
if (!chain.inputs.isEmpty()) {
|
||
el.appendChild(createSignalListEl(doc, "Inputs", chain.inputs));
|
||
}
|
||
// 链级 Gate
|
||
el.appendChild(createGateEl(doc, chain.gate));
|
||
// 链级 Outputs
|
||
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 <zlib.h> // 或使用 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
|
||
<!-- 一条简单链: 信号59和60做OR运算输出到1361 -->
|
||
<Chain gates="1" inputs="2" outputs="1">
|
||
<Inputs>
|
||
<Signal no="59" x="-676" y="69" />
|
||
<Signal no="60" x="-674" y="183" />
|
||
</Inputs>
|
||
<Gate type="OR" id="0" x="-346" y="141" />
|
||
<Outputs>
|
||
<Signal no="1361" x="-17" y="137" />
|
||
</Outputs>
|
||
</Chain>
|
||
```
|
||
|
||
**逻辑等效**:`out[1361] = in[59] OR in[60]`
|
||
|
||
### 6.2 组合链示例
|
||
|
||
```xml
|
||
<!-- 组合链: 多层嵌套 -->
|
||
<!-- 内层1: 62,63,64,65 做 AND → 输出到 1363,1364,1365 -->
|
||
<!-- 内层2: 66 和 内层1 Gate 做 AND → 输出到 1366 -->
|
||
<!-- 外层: 67 和内层2 Gate 做 OR → 输出到 1367 -->
|
||
<!-- 链级: 外层 Gate(OR) 做 NOT → 输出到 1368 -->
|
||
|
||
<Chain gates="4" inputs="6" outputs="6">
|
||
<Comb id="2">
|
||
<Inputs>
|
||
<Signal no="67" x="-6" y="937" />
|
||
</Inputs>
|
||
<Comb id="1">
|
||
<Inputs>
|
||
<Signal no="66" x="-333" y="876" />
|
||
</Inputs>
|
||
<Comb id="0">
|
||
<Inputs>
|
||
<Signal no="62" x="-669" y="413" />
|
||
<Signal no="63" x="-670" y="534" />
|
||
<Signal no="64" x="-667" y="650" />
|
||
<Signal no="65" x="-669" y="769" />
|
||
</Inputs>
|
||
<Gate type="AND" id="0" x="-346" y="595" />
|
||
<Outputs>
|
||
<Signal no="1363" x="-9" y="490" />
|
||
<Signal no="1364" x="-11" y="599" />
|
||
<Signal no="1365" x="-10" y="700" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="AND" id="1" x="-42" y="829" />
|
||
<Outputs>
|
||
<Signal no="1366" x="324" y="773" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="OR" id="1" x="288" y="890" />
|
||
<Outputs>
|
||
<Signal no="1367" x="627" y="836" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="NOT" id="1" x="594" y="950" />
|
||
<Outputs>
|
||
<Signal no="1368" x="915" y="947" />
|
||
</Outputs>
|
||
</Chain>
|
||
```
|
||
|
||
**逻辑等效**:
|
||
```
|
||
内层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
|
||
```
|
||
|
||
### 6.3 时序门简单链示例
|
||
|
||
```xml
|
||
<!-- 62p 延时: 输入保持 1000ms 后才输出 1,输入 0 立即输出 0 -->
|
||
<Chain gates="1" inputs="1" outputs="1">
|
||
<Inputs>
|
||
<Signal no="1368" x="-669" y="1065" />
|
||
</Inputs>
|
||
<Gate type="T62P" id="0" delayMs="1000" x="-345" y="1063" />
|
||
<Outputs>
|
||
<Signal no="1369" x="-10" y="1061" />
|
||
</Outputs>
|
||
</Chain>
|
||
|
||
<!-- 62d 延时: 输入 1 立即输出 1,输入 0 延时 500ms 后输出 0 -->
|
||
<Chain gates="1" inputs="1" outputs="1">
|
||
<Inputs>
|
||
<Signal no="1368" x="-669" y="1065" />
|
||
</Inputs>
|
||
<Gate type="T62D" id="0" delayMs="500" x="-334" y="1185" />
|
||
<Outputs>
|
||
<Signal no="1369" x="-10" y="1061" />
|
||
</Outputs>
|
||
</Chain>
|
||
|
||
<!-- 上升沿: 0→1 跳变时输出脉冲 -->
|
||
<Chain gates="1" inputs="1" outputs="1">
|
||
<Inputs>
|
||
<Signal no="1368" x="-669" y="1065" />
|
||
</Inputs>
|
||
<Gate type="RISING" id="0" x="-336" y="1305" />
|
||
<Outputs>
|
||
<Signal no="1369" x="-10" y="1061" />
|
||
</Outputs>
|
||
</Chain>
|
||
|
||
<!-- 下降沿: 1→0 跳变时输出脉冲 -->
|
||
<Chain gates="1" inputs="1" outputs="1">
|
||
<Inputs>
|
||
<Signal no="1368" x="-669" y="1065" />
|
||
</Inputs>
|
||
<Gate type="FALLING" id="0" x="-335" y="1418" />
|
||
<Outputs>
|
||
<Signal no="1369" x="-10" y="1061" />
|
||
</Outputs>
|
||
</Chain>
|
||
```
|
||
|
||
### 6.4 RS/SR 触发器示例
|
||
|
||
```xml
|
||
<!-- RS 触发器 (R 复位优先): R 接 Comb#0 输出,S 接 Input#15 直连信号 -->
|
||
<Chain gates="2" inputs="4" outputs="3">
|
||
<Comb id="0">
|
||
<Inputs>
|
||
<Signal no="10" x="-665" y="1804" />
|
||
<Signal no="11" x="-671" y="1932" />
|
||
<Signal no="12" x="-671" y="2054" />
|
||
</Inputs>
|
||
<Gate type="AND" id="2" x="-322" y="1938" />
|
||
<Outputs>
|
||
<Signal no="1374" x="18" y="1881" />
|
||
<Signal no="1375" x="23" y="2004" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Inputs>
|
||
<Signal no="15" x="0" y="2136" />
|
||
</Inputs>
|
||
<Gate type="RS" id="0" x="0" y="2136" R="Comb#0" S="Input#15" />
|
||
<Outputs>
|
||
<Signal no="1376" x="375" y="2131" />
|
||
</Outputs>
|
||
</Chain>
|
||
|
||
<!-- SR 触发器 (S 置位优先): S 接 Input#13,R 接 Input#14 -->
|
||
<Chain gates="1" inputs="2" outputs="1">
|
||
<Inputs>
|
||
<Signal no="13" x="-664" y="1545" />
|
||
<Signal no="14" x="-665" y="1679" />
|
||
</Inputs>
|
||
<Gate type="SR" id="0" x="-329" y="1631" S="Input#13" R="Input#14" />
|
||
<Outputs>
|
||
<Signal no="1377" x="14" y="1628" />
|
||
</Outputs>
|
||
</Chain>
|
||
```
|
||
|
||
---
|
||
|
||
## 七、信号编号(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 | `<Comb>` 最多嵌套 **3 层**(见示例),更深层级需验证 RTU 固件支持。**每个 Comb 必须有唯一 `id` 属性**,后序遍历自底向上从 0 开始分配 |
|
||
| 2 | `checksum` 必须使用 CRC-32/MPEG-2 算法,且大写十六进制格式 |
|
||
| 3 | `key` 建议使用 `QDateTime::currentMSecsSinceEpoch()` 生成,保证唯一性 |
|
||
| 4 | `x`/`y` 坐标可不填写(设为 0),RTU 解析时忽略这些字段 |
|
||
| 5 | XML 文件编码必须为 **UTF-8**,不要使用 UTF-8 BOM |
|
||
| 6 | `<Chain>` 的 `gates`/`inputs`/`outputs` 属性值为统计数量,生成时必须准确填写 |
|
||
| 7 | **T62P/T62D** 的 `delayMs` 为可选属性,默认 1000ms。范围 0~600000 |
|
||
| 8 | **SR/RS 触发器**有两个固定输入引脚:SR 的 pin0=S(置位)/pin1=R(复位),RS 的 pin0=R(复位)/pin1=S(置位)。每个引脚只能接一根线 |
|
||
| 9 | **SR/RS 的 R/S 属性**仅在 Gate 上输出,格式为 `Comb#N` 或 `Input#N`。若 RS/SR 所在链为组合链且有直接 Input 连接,需在链级 `<Inputs>` 块中包含对应信号 |
|
||
| 10 | 输入信号 `no` 必须是 datacenter out 信号表中的有效序号。QT 上位机需通过 WebSocket 动态获取信号列表,不可硬编码号段 |
|
||
| 11 | Comb 内 Gate 的 `id` 表示**同类型逻辑门内的序号**(OR/AND/NOT/T62P/T62D/... 分别计数,从 0 起递增) |
|
||
| 12 | 一个 `<PLCConfig>` 可含多条 `<Chain>`,目前 RTU 将所有 Chain 合并为一个逻辑图处理 |
|
||
|
||
---
|
||
|
||
## 九、附录:完整 XML 示例文件
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<PLCConfig name="PLC Logic" key="1783070862205" checksum="3486BE04">
|
||
<!-- 链1: 简单或门 (59 OR 60 → 1361) -->
|
||
<Chain gates="1" inputs="2" outputs="1">
|
||
<Inputs>
|
||
<Signal no="59" x="-676" y="69" />
|
||
<Signal no="60" x="-674" y="183" />
|
||
</Inputs>
|
||
<Gate type="OR" id="0" x="-346" y="141" />
|
||
<Outputs>
|
||
<Signal no="1361" x="-17" y="137" />
|
||
</Outputs>
|
||
</Chain>
|
||
<!-- 链2: 简单的非门 (61 → NOT → 1362) -->
|
||
<Chain gates="1" inputs="1" outputs="1">
|
||
<Inputs>
|
||
<Signal no="61" x="-669" y="298" />
|
||
</Inputs>
|
||
<Gate type="NOT" id="0" x="-345" y="296" />
|
||
<Outputs>
|
||
<Signal no="1362" x="-10" y="294" />
|
||
</Outputs>
|
||
</Chain>
|
||
<!-- 链3: 三层嵌套组合逻辑 -->
|
||
<Chain gates="4" inputs="6" outputs="6">
|
||
<Comb id="2">
|
||
<Inputs>
|
||
<Signal no="67" x="-6" y="937" />
|
||
</Inputs>
|
||
<Comb id="1">
|
||
<Inputs>
|
||
<Signal no="66" x="-333" y="876" />
|
||
</Inputs>
|
||
<Comb id="0">
|
||
<Inputs>
|
||
<Signal no="62" x="-669" y="413" />
|
||
<Signal no="63" x="-670" y="534" />
|
||
<Signal no="64" x="-667" y="650" />
|
||
<Signal no="65" x="-669" y="769" />
|
||
</Inputs>
|
||
<Gate type="AND" id="0" x="-346" y="595" />
|
||
<Outputs>
|
||
<Signal no="1363" x="-9" y="490" />
|
||
<Signal no="1364" x="-11" y="599" />
|
||
<Signal no="1365" x="-10" y="700" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="AND" id="1" x="-42" y="829" />
|
||
<Outputs>
|
||
<Signal no="1366" x="324" y="773" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="OR" id="1" x="288" y="890" />
|
||
<Outputs>
|
||
<Signal no="1367" x="627" y="836" />
|
||
</Outputs>
|
||
</Comb>
|
||
<Gate type="NOT" id="1" x="594" y="950" />
|
||
<Outputs>
|
||
<Signal no="1368" x="915" y="947" />
|
||
</Outputs>
|
||
</Chain>
|
||
</PLCConfig>
|
||
```
|
||
|
||
---
|
||
|
||
> **文档维护**:本文档基于 RTU 固件 [src/system/libplc/src/plc.cpp](src/system/libplc/src/plc.cpp) 中的 XML 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。
|