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

32 KiB
Raw Permalink Blame History

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> — 输入信号集

<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"),从中取得各信号的 nosaddr(信号地址)。

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 坐标
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 延时示例

<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 属性

RS 的值是来源描述字符串,格式为 Comb#NInput#N

值格式 含义
Input#59 该引脚由输入信号 no=59 直接驱动
Comb#0 该引脚由 id=0 的 Comb 块内的 Gate 输出驱动
<!-- 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"
<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 信号的场景:

<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 数据结构定义

#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 解析实现

#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 生成实现

#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 计算方法

#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 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 时序门简单链示例

<!-- 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 触发器示例

<!-- 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 请求信号列表

{
    "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 输入信号no5967 左右(实现中通过 out 信号表动态注册)
  • RTU 内置 PLC 输出信号no13611368 左右
  • 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/T62DdelayMs 为可选属性,默认 1000ms。范围 0~600000
8 SR/RS 触发器有两个固定输入引脚SR 的 pin0=S(置位)/pin1=R(复位)RS 的 pin0=R(复位)/pin1=S(置位)。每个引脚只能接一根线
9 SR/RS 的 R/S 属性仅在 Gate 上输出,格式为 Comb#NInput#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 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 中的 XML 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。