24 KiB
PLC 配置文件(XML)解析与生成说明文档
面向对象: QT 上位机开发人员
配置文件:config/PLC/Reclose_logic.xml
文件编码: UTF-8
文档日期: 2026-07-07
一、概述
PLC 逻辑配置使用 XML 格式存储。一个 XML 文件定义一个 PLC 逻辑工程,由若干条**逻辑链(Chain)**组成。每条链描述一组输入信号经逻辑门运算后输出到目标信号的过程。
QT 上位机需要实现:
- 解析(读取):将 XML 文件解析为内存中的逻辑图数据结构,用于画布展示和编辑
- 生成(写入):将用户编辑完的逻辑图保存/导出为 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> — 输入信号集
<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> — 输出信号集
<Outputs>
<Signal no="1361" x="-17" y="137" />
</Outputs>
与 <Inputs> 结构完全相同。区别在于语义:<Outputs> 中的信号编号对应 PLC 输出信号(通常 no >= 1361 为 RTU 内置预留号段)。
3.4 <Gate> — 逻辑门
<Gate type="OR" id="0" x="-346" y="141" />
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 <Comb> — 组合逻辑块(核心难点)
<Comb> 是可递归嵌套的逻辑组合块,用于表达多层级的复杂逻辑。
<Comb>
<Inputs> <!-- 本层直接输入信号 -->
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb> <!-- 嵌套子Comb(可多层) -->
<Inputs> ... </Inputs>
<Comb> ... </Comb>
<Gate type="AND" id="0" .../>
<Outputs> ... </Outputs>
</Comb>
<Gate type="OR" id="1" .../> <!-- 本层逻辑门 -->
<Outputs> <!-- 本层输出信号 -->
<Signal no="1367" x="627" y="836" />
</Outputs>
</Comb>
解析规则:
- 本层 Inputs:直接连到本层 Gate 的输入端
- 嵌套子 Comb:子 Comb 的 Gate 输出 → 连到本层 Gate 的输入端(一根线)
- 本层 Gate:接收上述所有输入,运算后输出到 Outputs
- 递归终止:最内层 Comb 没有嵌套子 Comb,只有 Inputs → Gate → Outputs
数据流示意:
[本层Inputs] ──┐
├──→ [本层 Gate] ──→ [本层 Outputs]
[子Comb Gate] ─┘
四、完整解析算法(C++ / Qt)
4.1 数据结构定义
#include <QString>
#include <QVector>
#include <QPoint>
#include <QtXml/QDomDocument>
// 逻辑门类型枚举
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<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; // (简单链) 输入信号
PlcGate gate; // (简单链) 逻辑门
PlcComb comb; // (组合链) 顶层Comb
QVector<PlcSignal> outputs; // 输出信号
};
// 整个配置文件
struct PlcConfig {
QString name;
QString key;
QString checksum;
QVector<PlcChain> chains;
};
4.2 XML 解析实现
#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;
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";
}
// 解析 <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();
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;
// 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;
}
// 解析整条 <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
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 <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 生成实现
#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);
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");
// 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 + 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 计算方法
#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 简单链示例
<!-- 一条简单链: 信号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 组合链示例
<!-- 组合链: 多层嵌套 -->
<!-- 内层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>
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb>
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<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
七、信号编号(no)的获取
QT 上位机需要通过 WebSocket 与 RTU 通信获取当前已注册的信号列表。
7.1 请求信号列表
{
"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 固件支持 |
| 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 | Comb 内 Gate 的 id 表示同类型逻辑门内的序号(OR/AND/NOT 分别计数,从 0 起递增) |
| 8 | 生成 XML 后建议通过 RTU Web 界面或命令行 plc cfg reload 触发配置重新加载 |
| 9 | 一个 <PLCConfig> 可含多条 <Chain>,但目前 RTU 将所有 Chain 合并为一个逻辑图处理 |
九、附录:完整 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>
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb>
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<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 中的 XML 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。