RTU/mimo/工程/PLC_XML配置文件解析与生成_QT上位机开发文档.md

968 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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#13R 接 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");
}
// 链级 InputsComb 之后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#13R 接 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` 在 **5967** 左右(实现中通过 `out` 信号表动态注册)
- **RTU 内置 PLC 输出信号**`no` 在 **13611368** 左右
- QT 上位机应**动态读取信号列表**而非硬编码号段
---
## 八、注意事项
| 编号 | 注意点 |
|------|--------|
| 1 | `<Comb>` 最多嵌套 **3 层**(见示例),更深层级需验证 RTU 固件支持。**每个 Comb 必须有唯一 `id` 属性**,后序遍历自底向上从 0 开始分配 |
| 2 | `checksum` 必须使用 CRC-32/MPEG-2 算法,且大写十六进制格式 |
| 3 | `key` 建议使用 `QDateTime::currentMSecsSinceEpoch()` 生成,保证唯一性 |
| 4 | `x`/`y` 坐标可不填写(设为 0RTU 解析时忽略这些字段 |
| 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 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。