Compare commits
10 Commits
fd34c6815f
...
323dc1ced0
| Author | SHA1 | Date |
|---|---|---|
|
|
323dc1ced0 | |
|
|
7d68713f4c | |
|
|
780b8cc311 | |
|
|
fac40ea953 | |
|
|
6bdd467998 | |
|
|
e6f4de251f | |
|
|
2ed51a5caa | |
|
|
7b40f4aecf | |
|
|
12dcec0f31 | |
|
|
a339ed73a0 |
|
|
@ -0,0 +1,967 @@
|
|||
# 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 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
# PLC 配置文件解析与生成文档
|
||||
|
||||
> **文件路径**: `config/PLC/Reclose_logic.txt`
|
||||
> **相关模块**: `src/system/libplc/` (PLC 逻辑引擎)
|
||||
> **前端页面**: `test/web_root/js/plc_debug.js` (PLC 调试页)
|
||||
> **文档日期**: 2026-07-03
|
||||
|
||||
---
|
||||
|
||||
## 一、文件格式总览
|
||||
|
||||
`Reclose_logic.txt` 是一个**单行 JSON 文件**(无换行、无空格),包含多个逻辑图的配置。
|
||||
|
||||
### 整体结构
|
||||
|
||||
```json
|
||||
{
|
||||
"<逻辑图key>": "<逻辑图数据字符串>",
|
||||
"online_cfgId": "<在线配置ID>"
|
||||
}
|
||||
```
|
||||
|
||||
### 逻辑图数据字符串格式
|
||||
|
||||
逻辑图数据是一个 `key=value&key=value&...` 格式的 URL 参数字符串:
|
||||
|
||||
```
|
||||
key=840957954&name=Block Over Current(G)&desc=desc&desc_pos_x=0&desc_pos_y=0&node=[...]&link=[...]
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `key` | string | 逻辑图唯一标识(数字字符串) |
|
||||
| `name` | string | 逻辑图名称(显示用) |
|
||||
| `desc` | string | 描述文本 |
|
||||
| `desc_pos_x` | int | 描述文本显示坐标 X |
|
||||
| `desc_pos_y` | int | 描述文本显示坐标 Y |
|
||||
| `node` | array | 节点数组,格式 `[{type:id:x:y}, ...]` |
|
||||
| `link` | array | 链路数组,格式 `[{src_type:src_id-dest_type:dest_id}, ...]` |
|
||||
|
||||
---
|
||||
|
||||
## 二、节点类型定义
|
||||
|
||||
### 2.1 节点类型枚举
|
||||
|
||||
| 类型标识 | 节点类型 | 说明 |
|
||||
|----------|----------|------|
|
||||
| `0` | 硬件输出点 | 映射 `dev_in` 中的输出点 ID,逻辑运算结果输出到硬件 |
|
||||
| `1` | 硬件输入点 | 映射 `dev_out` 中的输入点 ID,硬件输入信号作为逻辑输入 |
|
||||
| `2` | 或门 (OR) | 多输入逻辑或元件 |
|
||||
| `3` | 与门 (AND) | 多输入逻辑与元件 |
|
||||
| `4` | 非门 (NOT) | 单输入逻辑取反元件 |
|
||||
| `5` | 62p 延时元件 | 输入 1 延时输出 1,输入 0 立即输出 0(上升沿延时) |
|
||||
| `6` | 62d 延时元件 | 输入 1 立即输出 1,输入 0 延时输出 0(下降沿延时) |
|
||||
| `7` | 上升沿元件 | 检测 0→1 跳变,输出一个脉冲 |
|
||||
| `8` | SR 触发器 | S 优先的置位/复位触发器(S=1 置位,R=1 复位) |
|
||||
| `9` | RS 触发器 | R 优先的置位/复位触发器(R=1 复位,S=1 置位) |
|
||||
| `10` | 下降沿元件 | 检测 1→0 跳变,输出一个脉冲 |
|
||||
|
||||
### 2.2 节点编码格式
|
||||
|
||||
```
|
||||
{类型:ID:X坐标:Y坐标}
|
||||
```
|
||||
|
||||
- **类型 (type)**: 整数,对应上表中的类型标识
|
||||
- **ID**: 十六进制整数
|
||||
- 对于硬件输入/输出点(类型 0/1),ID 即为信号点号
|
||||
- 对于逻辑元件(类型 2~10),ID 编码规则见下表
|
||||
- **X 坐标 / Y 坐标**: 有符号整数,用于可视化布局
|
||||
|
||||
#### 逻辑元件 ID 编码
|
||||
|
||||
| 元件类型 | ID 编码规则 | 示例 |
|
||||
|----------|------------|------|
|
||||
| 与门 (3) | 低 8 位 = 元件序号 (0, 1, 2...) | `3:0` = 第 0 号与门 |
|
||||
| 或门 (2) | 低 8 位 = 元件序号 | `2:1` = 第 1 号或门 |
|
||||
| 非门 (4) | 低 8 位 = 元件序号 | `4:0` = 第 0 号非门 |
|
||||
| 62p (5) | 高 24 位 = 延时毫秒 tp,低 8 位 = 序号 | `5:0x3E800` = 延时 1000ms 的第 0 号 62p |
|
||||
| 62d (6) | 高 24 位 = 延时毫秒 td,低 8 位 = 序号 | `6:0x7D000` = 延时 2000ms 的第 0 号 62d |
|
||||
| 上升沿 (7) | 低 8 位 = 元件序号 | `7:0` = 第 0 号上升沿 |
|
||||
| SR (8) | 低 8 位 = 元件序号 | `8:0` = 第 0 号 SR 触发器 |
|
||||
| RS (9) | 低 8 位 = 元件序号 | `9:0` = 第 0 号 RS 触发器 |
|
||||
| 下降沿 (10) | 低 8 位 = 元件序号 | `10:0` = 第 0 号下降沿 |
|
||||
|
||||
---
|
||||
|
||||
## 三、链路 (link) 规则
|
||||
|
||||
### 3.1 链路编码格式
|
||||
|
||||
```
|
||||
{源节点类型:源节点ID-目标节点类型:目标节点ID}
|
||||
```
|
||||
|
||||
### 3.2 连接约束
|
||||
|
||||
| 源节点类型 | 目标节点类型 | 约束 |
|
||||
|-----------|-------------|------|
|
||||
| 硬件输入点 (1) | 逻辑元件 (2~10) | 单输入可以连接多个目标 |
|
||||
| 硬件输入点 (1) | 硬件输出点 (0) | 直接驱动(无逻辑运算) |
|
||||
| 逻辑元件 (2~10) → 输出 | 逻辑元件 (2~10) → 输入 | 级联连接 |
|
||||
| 逻辑元件 (2~10) → 输出 | 硬件输出点 (0) | 运算结果驱动输出 |
|
||||
| 非门 (4) → 输出 | 任何 | 输出为输入信号的取反 |
|
||||
| 非门 (4) → 输入 | — | **仅接收单个输入信号**(单输入元件) |
|
||||
| 与/或门 (2/3) → 输入 | — | **可接收多个输入信号**(多输入元件) |
|
||||
|
||||
### 3.3 关键约束
|
||||
|
||||
1. **非门 (4) 是单输入元件**,只能有一个源节点连接到它
|
||||
2. **与门 (3) / 或门 (2) 是多输入元件**,可以有多个源节点连接
|
||||
3. **62p/62d 是单输入元件**
|
||||
4. **上升沿/下降沿是单输入元件**
|
||||
5. **SR/RS 触发器是双输入元件**(SR: S=输入1, R=输入2;RS: R=输入1, S=输入2)
|
||||
|
||||
---
|
||||
|
||||
## 四、完整示例
|
||||
|
||||
### 4.1 原始文件内容
|
||||
|
||||
```json
|
||||
{"840957954":"key=840957954&name=Block Over Current(G)&desc=desc&desc_pos_x=0&desc_pos_y=0&node=[{1:38:-330:-120},{0:1:130:-120},{1:36:-390:-10},{0:2:170:-10},{1:37:-400:80},{0:3:170:110},{3:0:-10:30}]&link=[{1:38-0:1},{3:0-0:2},{3:0-0:3},{1:36-3:0},{1:37-3:0}]","online_cfgId":"3486BE04"}
|
||||
```
|
||||
|
||||
### 4.2 节点拆解
|
||||
|
||||
| 节点编码 | 类型 | ID | 坐标 | 含义 |
|
||||
|----------|------|-----|------|------|
|
||||
| `1:38:-330:-120` | 1 (输入点) | 0x38 (56) | (-330,-120) | 硬件输入信号 56 |
|
||||
| `0:1:130:-120` | 0 (输出点) | 0x1 (1) | (130,-120) | 硬件输出信号 1 |
|
||||
| `1:36:-390:-10` | 1 (输入点) | 0x36 (54) | (-390,-10) | 硬件输入信号 54 |
|
||||
| `0:2:170:-10` | 0 (输出点) | 0x2 (2) | (170,-10) | 硬件输出信号 2 |
|
||||
| `1:37:-400:80` | 1 (输入点) | 0x37 (55) | (-400,80) | 硬件输入信号 55 |
|
||||
| `0:3:170:110` | 0 (输出点) | 0x3 (3) | (170,110) | 硬件输出信号 3 |
|
||||
| `3:0:-10:30` | 3 (与门) | 0 (第0号) | (-10,30) | 第 0 号与门 |
|
||||
|
||||
### 4.3 链路拆解
|
||||
|
||||
| 链路编码 | 源 | 目标 | 含义 |
|
||||
|----------|-----|------|------|
|
||||
| `1:38-0:1` | 输入点 0x38 | 输出点 0x1 | 输入 56 直驱输出 1 |
|
||||
| `3:0-0:2` | 与门 0 号 | 输出点 0x2 | 与门结果 → 输出 2 |
|
||||
| `3:0-0:3` | 与门 0 号 | 输出点 0x3 | 与门结果 → 输出 3 |
|
||||
| `1:36-3:0` | 输入点 0x36 | 与门 0 号 | 输入 54 → 与门输入 1 |
|
||||
| `1:37-3:0` | 输入点 0x37 | 与门 0 号 | 输入 55 → 与门输入 2 |
|
||||
|
||||
### 4.4 逻辑表达式
|
||||
|
||||
```
|
||||
输出1 = 输入56
|
||||
输出2 = 输入54 && 输入55
|
||||
输出3 = 输入54 && 输入55
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、解析流程(C 后端)
|
||||
|
||||
参考代码:[src/system/libplc/src/plc.cpp](src/system/libplc/src/plc.cpp#L230)
|
||||
|
||||
### 5.1 解析步骤
|
||||
|
||||
```
|
||||
1. fopen 读取整个文件到 buffer
|
||||
2. 去除所有空白字符(isspace)
|
||||
3. 逐逻辑图解析:
|
||||
a. 查找 "key" 字段 → 提取逻辑图 key
|
||||
b. 查找 "node=[...]" → 按 {type:id:x:y} 格式解析节点
|
||||
- sscanf(token, "%d:%x:%d:%d", &type, &id, &x, &y)
|
||||
c. 查找 "link=[...]" → 按 {src_type:src_id-dest_type:dest_id} 格式解析链路
|
||||
- sscanf(token, "%d:%x-%d:%x", &src_type, &src_id, &dest_type, &dest_id)
|
||||
d. 查找 "name=..." → 提取逻辑图名称
|
||||
4. 缓存解析结果(文件未变化时复用缓存)
|
||||
```
|
||||
|
||||
### 5.2 关键数据结构
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int type; // 节点类型 (0~10)
|
||||
int id; // 节点 ID
|
||||
int x, y; // 可视化坐标
|
||||
int value; // 当前信号值
|
||||
int indegree; // 入度(拓扑排序用)
|
||||
// 延时定时器字段 (62p/62d)
|
||||
uint8_t timer_active;
|
||||
struct timespec timer_start;
|
||||
int timer_delay_ms;
|
||||
int timer_pending_output;
|
||||
} LogicNode;
|
||||
|
||||
typedef struct {
|
||||
int src_type, src_id; // 源节点
|
||||
int dest_type, dest_id; // 目标节点
|
||||
} LogicLink;
|
||||
|
||||
typedef struct {
|
||||
char key[20]; // 逻辑图 key
|
||||
char name[50]; // 逻辑图名称
|
||||
LogicNode nodes[MAX_NODES]; // 节点数组
|
||||
int node_count;
|
||||
LogicLink links[MAX_LINKS]; // 链路数组
|
||||
int link_count;
|
||||
} LogicGraph;
|
||||
```
|
||||
|
||||
### 5.3 执行流程
|
||||
|
||||
```
|
||||
1. 拓扑排序(Kahn 算法):按依赖关系排序节点
|
||||
2. 按拓扑序执行逻辑元件运算:
|
||||
- 非门: value = !input
|
||||
- 与门: value = input1 && input2 && ...
|
||||
- 或门: value = input1 || input2 || ...
|
||||
- 62p: 输入1→延时tp毫秒→输出1;输入0→立即输出0
|
||||
- 62d: 输入1→立即输出1;输入0→延时td毫秒→输出0
|
||||
- 上升沿: last=0 && cur=1 → 脉冲1
|
||||
- 下降沿: last=1 && cur=0 → 脉冲1
|
||||
- SR: S=1→value=1; R=1→value=0 (S优先)
|
||||
- RS: R=1→value=0; S=1→value=1 (R优先)
|
||||
3. 输出链路处理:将源节点值写入硬件输出点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端解析与生成(JavaScript)
|
||||
|
||||
### 6.1 JSON 解析
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 解析 Reclose_logic.txt 文件内容为逻辑图数组
|
||||
* @param {string} fileContent - 文件原始内容
|
||||
* @returns {{ graphs: LogicGraph[], online_cfgId: string }}
|
||||
*/
|
||||
function parsePLCFile(fileContent) {
|
||||
// 去除空白
|
||||
var clean = fileContent.replace(/\s/g, '');
|
||||
var data = JSON.parse(clean);
|
||||
var graphs = [];
|
||||
|
||||
for (var key in data) {
|
||||
if (key === 'online_cfgId') continue;
|
||||
graphs.push(parseGraphData(key, data[key]));
|
||||
}
|
||||
|
||||
return {
|
||||
graphs: graphs,
|
||||
online_cfgId: data.online_cfgId || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个逻辑图数据字符串
|
||||
* @param {string} key - 逻辑图 key
|
||||
* @param {string} dataStr - key=value&... 格式的数据字符串
|
||||
* @returns {LogicGraph}
|
||||
*/
|
||||
function parseGraphData(key, dataStr) {
|
||||
var params = {};
|
||||
var pairs = dataStr.split('&');
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var idx = pairs[i].indexOf('=');
|
||||
if (idx === -1) continue;
|
||||
params[pairs[i].substring(0, idx)] = pairs[i].substring(idx + 1);
|
||||
}
|
||||
|
||||
var graph = {
|
||||
key: key,
|
||||
name: decodeURIComponent(params.name || ''),
|
||||
desc: decodeURIComponent(params.desc || ''),
|
||||
desc_pos_x: parseInt(params.desc_pos_x) || 0,
|
||||
desc_pos_y: parseInt(params.desc_pos_y) || 0,
|
||||
nodes: [],
|
||||
links: []
|
||||
};
|
||||
|
||||
// 解析节点
|
||||
if (params.node) {
|
||||
var nodeMatch = params.node.match(/\[(.*)\]/);
|
||||
if (nodeMatch) {
|
||||
var tokens = nodeMatch[1].match(/\{[^}]+\}/g) || [];
|
||||
for (var j = 0; j < tokens.length; j++) {
|
||||
var inner = tokens[j].replace(/[{}]/g, '');
|
||||
var parts = inner.split(':');
|
||||
if (parts.length === 4) {
|
||||
graph.nodes.push({
|
||||
type: parseInt(parts[0]),
|
||||
id: parseInt(parts[1], 16),
|
||||
x: parseInt(parts[2]),
|
||||
y: parseInt(parts[3])
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析链路
|
||||
if (params.link) {
|
||||
var linkMatch = params.link.match(/\[(.*)\]/);
|
||||
if (linkMatch) {
|
||||
var tokens = linkMatch[1].match(/\{[^}]+\}/g) || [];
|
||||
for (var k = 0; k < tokens.length; k++) {
|
||||
var inner = tokens[k].replace(/[{}]/g, '');
|
||||
var linkParts = inner.split('-');
|
||||
if (linkParts.length === 2) {
|
||||
var src = linkParts[0].split(':');
|
||||
var dest = linkParts[1].split(':');
|
||||
graph.links.push({
|
||||
src_type: parseInt(src[0]),
|
||||
src_id: parseInt(src[1], 16),
|
||||
dest_type: parseInt(dest[0]),
|
||||
dest_id: parseInt(dest[1], 16)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 JSON 生成(序列化)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 将逻辑图数组序列化为 Reclose_logic.txt 文件内容
|
||||
* @param {LogicGraph[]} graphs - 逻辑图数组
|
||||
* @param {string} onlineCfgId - 在线配置 ID
|
||||
* @returns {string} 文件内容(紧凑 JSON,无换行无空格)
|
||||
*/
|
||||
function generatePLCFile(graphs, onlineCfgId) {
|
||||
var result = {};
|
||||
|
||||
for (var i = 0; i < graphs.length; i++) {
|
||||
var g = graphs[i];
|
||||
var params = [];
|
||||
|
||||
params.push('key=' + g.key);
|
||||
params.push('name=' + encodeURIComponent(g.name || ''));
|
||||
params.push('desc=' + encodeURIComponent(g.desc || 'desc'));
|
||||
params.push('desc_pos_x=' + (g.desc_pos_x || 0));
|
||||
params.push('desc_pos_y=' + (g.desc_pos_y || 0));
|
||||
|
||||
// 序列化节点
|
||||
var nodeParts = [];
|
||||
for (var j = 0; j < g.nodes.length; j++) {
|
||||
var n = g.nodes[j];
|
||||
nodeParts.push('{' + n.type + ':' + n.id.toString(16) + ':' + n.x + ':' + n.y + '}');
|
||||
}
|
||||
params.push('node=[' + nodeParts.join(',') + ']');
|
||||
|
||||
// 序列化链路
|
||||
var linkParts = [];
|
||||
for (var k = 0; k < g.links.length; k++) {
|
||||
var l = g.links[k];
|
||||
linkParts.push('{' + l.src_type + ':' + l.src_id.toString(16)
|
||||
+ '-' + l.dest_type + ':' + l.dest_id.toString(16) + '}');
|
||||
}
|
||||
params.push('link=[' + linkParts.join(',') + ']');
|
||||
|
||||
result[g.key] = params.join('&');
|
||||
}
|
||||
|
||||
result.online_cfgId = onlineCfgId || '';
|
||||
|
||||
return JSON.stringify(result); // 紧凑格式,无空格
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 节点类型名称映射
|
||||
|
||||
```javascript
|
||||
var NODE_TYPE_NAMES = {
|
||||
0: '硬件输出点',
|
||||
1: '硬件输入点',
|
||||
2: '或门 (OR)',
|
||||
3: '与门 (AND)',
|
||||
4: '非门 (NOT)',
|
||||
5: '62p 延时元件',
|
||||
6: '62d 延时元件',
|
||||
7: '上升沿元件',
|
||||
8: 'SR 触发器',
|
||||
9: 'RS 触发器',
|
||||
10: '下降沿元件'
|
||||
};
|
||||
|
||||
var NODE_TYPE_ICONS = {
|
||||
0: '📤', 1: '📥', 2: '🔶', 3: '🔷', 4: '🔺',
|
||||
5: '⏱️', 6: '⏱️', 7: '⬆️', 8: '🔒', 9: '🔒', 10: '⬇️'
|
||||
};
|
||||
|
||||
function getNodeTypeName(type) {
|
||||
return NODE_TYPE_NAMES[type] || '未知(' + type + ')';
|
||||
}
|
||||
|
||||
function getNodeTypeIcon(type) {
|
||||
return NODE_TYPE_ICONS[type] || '❓';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、节点类型配置表
|
||||
|
||||
### 7.1 硬件输入/输出点配置(类型 0/1)
|
||||
|
||||
| 配置来源 | 字段 | 说明 |
|
||||
|----------|------|------|
|
||||
| 逻辑图文件 | `node[{0:id:x:y}]` | 定义输出点的 ID 和画布坐标 |
|
||||
| 逻辑图文件 | `node[{1:id:x:y}]` | 定义输入点的 ID 和画布坐标 |
|
||||
| 自点表配置 | `config/SELF_PTL/` | 输入点的实际硬件映射 (`dev_out[id]`) |
|
||||
| 自点表配置 | `config/SELF_PTL/` | 输出点的实际硬件映射 (`dev_in[id]`) |
|
||||
| 数据中心 | `dc_signal_out()` | PLC 模块注册的输出信号 (`plc.st.out.N`) |
|
||||
|
||||
### 7.2 逻辑元件配置(类型 2~10)
|
||||
|
||||
| 元件 | 关联配置 | ID 含义 |
|
||||
|------|----------|---------|
|
||||
| 与门 (3) | `logic_limit[idx=3]` | ID 低 8 位 = 序号 |
|
||||
| 或门 (2) | `logic_limit[idx=2]` | ID 低 8 位 = 序号 |
|
||||
| 非门 (4) | `logic_limit[idx=4]` | ID 低 8 位 = 序号 |
|
||||
| 62p (5) | — | 高 24 位 = tp 延迟 ms |
|
||||
| 62d (6) | — | 高 24 位 = td 延迟 ms |
|
||||
| 上升沿 (7) | — | ID 低 8 位 = 序号 |
|
||||
| 下降沿 (10) | — | ID 低 8 位 = 序号 |
|
||||
| SR 触发器 (8) | — | ID 低 8 位 = 序号 |
|
||||
| RS 触发器 (9) | — | ID 低 8 位 = 序号 |
|
||||
|
||||
---
|
||||
|
||||
## 八、与现有前端的对接
|
||||
|
||||
### 8.1 现有 PLC 调试页(plc_debug.js)
|
||||
|
||||
当前 [plc_debug.js](test/web_root/js/plc_debug.js) 实现了:
|
||||
- 指示灯绑定 out 信号(>0 亮 / ≤0 灭)
|
||||
- 按键绑定 out 信号(点击翻转 0↔1)
|
||||
- 增量刷新(不重建 DOM)
|
||||
- 勾选批量删除
|
||||
|
||||
**不涉及**:逻辑图文件的解析/生成/编辑。
|
||||
|
||||
### 8.2 后续可扩展方向
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **逻辑图编辑器** | 可视化编辑节点和链路,拖拽连线 |
|
||||
| **逻辑图导入/导出** | 支持上传/下载 Reclose_logic.txt |
|
||||
| **逻辑仿真** | 前端实时计算逻辑表达式结果 |
|
||||
| **节点属性编辑** | 修改节点类型、ID、延时参数等 |
|
||||
| **链路管理** | 新增/删除/修改节点间连接 |
|
||||
| **模板库** | 常用逻辑图模板(自保持、互锁等) |
|
||||
|
||||
---
|
||||
|
||||
## 九、注意事项
|
||||
|
||||
1. **文件编码**: 文件为 UTF-8 编码,单行无换行
|
||||
2. **ID 进制**: 节点 ID 和链路中的 ID 均为**十六进制** (`%x` 格式)
|
||||
3. **URL 编码**: `name` 和 `desc` 字段可能包含空格,在数据字符串中需进行 URL 编码(`%20` 等)
|
||||
4. **坐标系统**: X 轴向右为正,Y 轴向下为正,支持负坐标
|
||||
5. **数量上限**: 每个逻辑图最多 20 个节点 + 20 条链路 (`MAX_NODES_PER_GRAPH=20`, `MAX_LINKS_PER_GRAPH=20`)
|
||||
6. **热加载**: PLC 模块通过 `stat()` 检测文件 mtime 变化,自动重新加载
|
||||
7. **在线配置ID**: `online_cfgId` 是全局配置标识,所有逻辑图共用一个
|
||||
8. **逻辑图独立**: 多个逻辑图之间相互独立,各自独立执行
|
||||
9. **拓扑排序**: 后端使用 Kahn 算法确保节点按依赖顺序执行,前端如需仿真也应同样处理
|
||||
10. **62p/62d 延时**: 使用 `clock_gettime(CLOCK_MONOTONIC)` 实现非阻塞毫秒级延时
|
||||
|
|
@ -2,6 +2,37 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-07-07: PLC 逻辑图编辑器"生成配置"输出错误的 XML 配置
|
||||
|
||||
### 问题现象
|
||||
在 PLC 逻辑配置页面(`#plc_config`)中,逻辑图可视化**查看器能正确解析配置文件**并展示逻辑图;但编辑器中点击**"生成配置"**后生成的 XML 配置文件不正确——多门组合链(Chain 3)中 AND#0、AND#1、OR#1 三个中间门及其输出信号全部丢失,只保留了链级门 NOT#1。
|
||||
|
||||
### 原因分析
|
||||
`plc_config.js` 中 `generateXmlConfig()` 函数存在 4 个 Bug:
|
||||
|
||||
1. **核心 Bug:`combTerm` 逻辑错误** — `combTerm` 要求门的**全部**下游连接都在 `cGates` 内部。但中间门 AND#1 的下游同时包含 OR#1(门)和输出信号 1366,导致 `downAllChain = false`,所有中间门被排除,`combTerm` 为空数组。
|
||||
|
||||
2. **`combXml` 递归缺少 `<Comb>` 包装** — 递归处理上游门时直接输出裸内容,未包裹在 `<Comb>` 标签中,导致生成的 XML 结构与解析器期望不匹配。
|
||||
|
||||
3. **链级 `<Outputs>` 包含所有输出** — 旧代码直接输出 `cOuts`(全部输出节点),但 Comb 内的输出信号(1363-1367)已在 `combXml` 中处理,链级只应包含 chainGate 的输出(1368)。
|
||||
|
||||
4. **`delayMs` 提取截断** — `(n.id >> 8) & 0xFF` 只取低 8 位,延迟 > 255ms 时应为 `n.id >> 8`。
|
||||
|
||||
### 解决方案
|
||||
修改 `test/web_root/js/plc_config.js` 中 `generateXmlConfig()` 函数:
|
||||
|
||||
- **Bug #1**:将 `combTerm` 替换为 `combRoots`,基于 chainGate 的上游门计算 Comb 根节点(`combXml` 递归处理嵌套)
|
||||
- **Bug #2**:在 `combXml` 递归调用处包裹 `<Comb>` 标签
|
||||
- **Bug #3**:链级 `<Outputs>` 仅输出连接到 chainGate 的信号
|
||||
- **Bug #4**:`delayMs` 提取改为 `n.id >> 8`
|
||||
|
||||
### 验证
|
||||
- 编译通过,浏览器测试确认生成的 XML 与原始配置完全一致
|
||||
- 查看器可正确解析生成的配置(23节点 20连线)
|
||||
- PLC 模块正常运行,所有输出信号处理正确
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-01: com_channel_recv_cb 三级路由重构 (转发→bind→IEC)
|
||||
|
||||
### 需求背景
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ void dc_set_signal_val_from_str(void *p_data, uint8_t data_type, const std::stri
|
|||
|
||||
uint32_t dc_get_signal_count(const char *type);
|
||||
|
||||
// 保存所有已注册 out 信号到 XML (供前端 PLC 配置读取)
|
||||
int dc_save_out_signals_xml(const std::string &path);
|
||||
|
||||
int dc_get_signal_info_by_id(const char *type, uint32_t id, std::string &saddr, std::string &desc, std::string &data_type, uint8_t &ctrl_type, std::vector<std::string> &link_saddrs);
|
||||
|
||||
typedef struct
|
||||
|
|
|
|||
|
|
@ -1,5 +1,48 @@
|
|||
#include "dc_signal_internal.h"
|
||||
#include "tinyxml2.h"
|
||||
|
||||
int dc_save_out_signals_xml(const std::string &path)
|
||||
{
|
||||
using namespace tinyxml2;
|
||||
|
||||
XMLDocument doc;
|
||||
doc.InsertEndChild(doc.NewDeclaration());
|
||||
|
||||
XMLElement *root = doc.NewElement("Root");
|
||||
doc.InsertEndChild(root);
|
||||
|
||||
uint32_t count = 0;
|
||||
XMLElement *out_elem = doc.NewElement("Out");
|
||||
|
||||
for(uint32_t i = 0; i < g_datacenter.signal_out.signal_id; i++)
|
||||
{
|
||||
stru_signal *p_out = dc_find_signal_by_id(i, g_datacenter.signal_out);
|
||||
if(nullptr == p_out) continue;
|
||||
|
||||
XMLElement *sig_elem = doc.NewElement("Signal");
|
||||
sig_elem->SetAttribute("no", (int)count);
|
||||
sig_elem->SetAttribute("saddr", p_out->saddr.c_str());
|
||||
sig_elem->SetAttribute("desc", p_out->desc.c_str());
|
||||
|
||||
out_elem->InsertEndChild(sig_elem);
|
||||
count++;
|
||||
}
|
||||
|
||||
out_elem->SetAttribute("count", (int)count);
|
||||
root->InsertEndChild(out_elem);
|
||||
|
||||
if(XML_SUCCESS == doc.SaveFile(path.c_str()))
|
||||
{
|
||||
MY_LOG_I("datacenter out signals saved to %s, count=%u", path.c_str(), count);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
MY_LOG_E("failed to save datacenter out signals to %s", path.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void dc_param_cfg_check(const std::string &path)
|
||||
{
|
||||
if (false == dc_get_param_cfg_change()) return;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <locale.h> // ANSI编码支持
|
||||
#include <unistd.h> // usleep函数实现延时
|
||||
#include <time.h> // clock_gettime 非阻塞定时器
|
||||
#include <sys/stat.h> // stat() 文件变化检测
|
||||
#include "tinyxml2.h" // XML 配置文件解析
|
||||
|
||||
|
||||
#define MAX_OUT_NODES 20 // 最大输出节点数
|
||||
|
|
@ -30,8 +30,8 @@ std::vector<stru_plc_cfg> g_plc_cfg={};
|
|||
|
||||
// ==================== 常量定义 ====================
|
||||
#define MAX_LOGIC_GRAPHS 4 // 最大逻辑图数量
|
||||
#define MAX_NODES_PER_GRAPH 20 // 每个逻辑图最大节点数
|
||||
#define MAX_LINKS_PER_GRAPH 20 // 每个逻辑图最大链路数
|
||||
#define MAX_NODES_PER_GRAPH 64 // 每个逻辑图最大节点数
|
||||
#define MAX_LINKS_PER_GRAPH 64 // 每个逻辑图最大链路数
|
||||
|
||||
// 节点类型标识
|
||||
#define NODE_TYPE_HW_OUTPUT 0 // 硬件输出点(dev_in)
|
||||
|
|
@ -64,10 +64,18 @@ typedef struct {
|
|||
typedef struct {
|
||||
int type;
|
||||
int id;
|
||||
int x;
|
||||
int y;
|
||||
int value;
|
||||
int last_input;
|
||||
// 非阻塞延时定时器字段(62p/62d门使用)
|
||||
uint8_t timer_active; // 1=定时器计时中
|
||||
struct timespec timer_start; // 定时器启动时刻
|
||||
int timer_delay_ms; // 延时毫秒数
|
||||
int timer_pending_output; // 定时器到期后的输出值,-1=无待定输出
|
||||
int delay_ms; // 配置的延时毫秒数(62p/62d,从XML delayMs属性读取)
|
||||
char rs_s_src[32]; // S引脚来源描述(SR/RS)
|
||||
char rs_r_src[32]; // R引脚来源描述(SR/RS)
|
||||
// 拓扑排序字段
|
||||
int indegree; // 入度(依赖的前驱节点数)
|
||||
} LogicNode;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -75,6 +83,7 @@ typedef struct {
|
|||
int src_id;
|
||||
int dest_type;
|
||||
int dest_id;
|
||||
int dst_pin; // 目标引脚序号(0=S, 1=R,仅SR/RS有效,-1=默认)
|
||||
} LogicLink;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -86,6 +95,12 @@ typedef struct {
|
|||
int link_count;
|
||||
} LogicGraph;
|
||||
|
||||
// ==================== 全局PLC状态 ====================
|
||||
LOCAL LogicGraph g_cached_graphs[MAX_LOGIC_GRAPHS]; // 缓存的逻辑图
|
||||
LOCAL int g_cached_graph_count = 0; // 缓存的逻辑图数量
|
||||
LOCAL time_t g_last_file_mtime = 0; // 配置文件最后修改时间
|
||||
LOCAL char g_plc_config_path[256] = "/mnt/RTU/config/PLC/Reclose_logic.xml"; // 优先XML
|
||||
|
||||
// ==================== 硬件点操作函数 ====================
|
||||
static HWPoint* create_hw_point(int id, int value) {
|
||||
HWPoint *point = (HWPoint*)malloc(sizeof(HWPoint));
|
||||
|
|
@ -97,6 +112,15 @@ static HWPoint* create_hw_point(int id, int value) {
|
|||
return point;
|
||||
}
|
||||
|
||||
// 计算从 start 到当前时刻经过的毫秒数
|
||||
static long get_elapsed_ms(struct timespec *start) {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
long sec_diff = now.tv_sec - start->tv_sec;
|
||||
long nsec_diff = now.tv_nsec - start->tv_nsec;
|
||||
return sec_diff * 1000 + nsec_diff / 1000000;
|
||||
}
|
||||
|
||||
static void destroy_hw_list(HWPoint *head) {
|
||||
HWPoint *tmp;
|
||||
while (head) {
|
||||
|
|
@ -148,16 +172,31 @@ static int set_hw_point_value(HWPoint **head, int *count, int id, int value) {
|
|||
// 从逻辑图中提取所有硬件点到管理器
|
||||
static void extract_hw_points_from_graphs(LogicGraph graphs[], int graph_count,
|
||||
HardwareManager *hw, int default_input_value) {
|
||||
stru_plc_cfg *p = nullptr;
|
||||
for (int g = 0; g < graph_count; g++) {
|
||||
LogicGraph *graph = &graphs[g];
|
||||
for (int n = 0; n < graph->node_count; n++) {
|
||||
LogicNode *node = &graph->nodes[n];
|
||||
if (node->type == NODE_TYPE_HW_INPUT) {
|
||||
for (uint32_t i = 0; i < g_plc_cfg.size(); i++) {
|
||||
p = &g_plc_cfg.at(i);
|
||||
if (p->base.inf == node->id) {
|
||||
default_input_value = *(uint8_t *)p->p_data;
|
||||
// node->id 现在是 datacenter out 信号的 no(序号)
|
||||
// 通过 dc_get_signal_info_by_id 找到 saddr,再读取当前值
|
||||
std::string saddr, desc, dtype_str;
|
||||
uint8_t ctrl_type;
|
||||
std::vector<std::string> link_saddrs;
|
||||
if(0 == dc_get_signal_info_by_id("out", (uint32_t)node->id, saddr, desc, dtype_str, ctrl_type, link_saddrs))
|
||||
{
|
||||
void *p_data = nullptr;
|
||||
uint8_t data_type = 0;
|
||||
std::string d2;
|
||||
if(0 == dc_get_out_signal_info(saddr, d2, data_type, &p_data) && p_data)
|
||||
{
|
||||
if(data_type == DATA_TYPE_U8)
|
||||
default_input_value = *(uint8_t *)p_data;
|
||||
else if(data_type == DATA_TYPE_U16)
|
||||
default_input_value = (*(uint16_t *)p_data) ? 1 : 0;
|
||||
else if(data_type == DATA_TYPE_U32)
|
||||
default_input_value = (*(uint32_t *)p_data) ? 1 : 0;
|
||||
else if(data_type == DATA_TYPE_F32)
|
||||
default_input_value = (*(float *)p_data > 0.5f) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
set_hw_point_value(&hw->input_head, &hw->input_count,
|
||||
|
|
@ -204,141 +243,391 @@ static LogicNode* find_node(LogicGraph *graph, int type, int id) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// ==================== 逻辑图解析 ====================
|
||||
int parse_reclose_logic(const char *file_path, LogicGraph graphs[], int *graph_count) {
|
||||
FILE *fp = fopen(file_path, "rb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "错误:无法打开文件 %s\n", file_path);
|
||||
// ==================== XML 配置文件解析 (Comb结构) ====================
|
||||
|
||||
static int gate_type_from_xml_str(const char *type_str)
|
||||
{
|
||||
if(NULL == type_str) return -1;
|
||||
if(strcmp(type_str, "INPUT") == 0) return NODE_TYPE_HW_INPUT;
|
||||
if(strcmp(type_str, "OUTPUT") == 0) return NODE_TYPE_HW_OUTPUT;
|
||||
if(strcmp(type_str, "OR") == 0) return NODE_TYPE_OR_GATE;
|
||||
if(strcmp(type_str, "AND") == 0) return NODE_TYPE_AND_GATE;
|
||||
if(strcmp(type_str, "NOT") == 0) return NODE_TYPE_NOT_GATE;
|
||||
if(strcmp(type_str, "T62P") == 0) return NODE_TYPE_62P_GATE;
|
||||
if(strcmp(type_str, "T62D") == 0) return NODE_TYPE_62D_GATE;
|
||||
if(strcmp(type_str, "RISING") == 0) return NODE_TYPE_RISING_EDGE;
|
||||
if(strcmp(type_str, "SR") == 0) return NODE_TYPE_SR_LATCH;
|
||||
if(strcmp(type_str, "RS") == 0) return NODE_TYPE_RS_LATCH;
|
||||
if(strcmp(type_str, "FALLING") == 0) return NODE_TYPE_FALLING_EDGE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int find_node_index(LogicGraph *graph, int type, int id)
|
||||
{
|
||||
for(int i = 0; i < graph->node_count; i++)
|
||||
{
|
||||
if(graph->nodes[i].type == type)
|
||||
{
|
||||
if(type <= 1 && graph->nodes[i].id == id) return i;
|
||||
if(type > 1 && (graph->nodes[i].id & 0xFF) == (id & 0xFF)) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Comb 内部信号:不存在则先创建
|
||||
static int ensure_signal(LogicGraph *graph, int io_type, int sig_no)
|
||||
{
|
||||
int idx = find_node_index(graph, io_type, sig_no);
|
||||
if(idx >= 0) return idx;
|
||||
if(graph->node_count >= MAX_NODES_PER_GRAPH) return -1;
|
||||
LogicNode &n = graph->nodes[graph->node_count++];
|
||||
memset(&n, 0, sizeof(n)); n.timer_pending_output = -1;
|
||||
n.type = io_type; n.id = sig_no;
|
||||
return graph->node_count - 1;
|
||||
}
|
||||
|
||||
// 递归解析 <Comb> 块: Inputs → [nested Comb] → Gate → Outputs
|
||||
// 返回值: 本 Comb 中 Gate 节点的索引,-1 表示失败
|
||||
static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph)
|
||||
{
|
||||
if(NULL == comb_el || graph->node_count >= MAX_NODES_PER_GRAPH) return -1;
|
||||
tinyxml2::XMLElement *gate_el = comb_el->FirstChildElement("Gate");
|
||||
if(NULL == gate_el) { MY_LOG_E("Comb缺少Gate"); return -1; }
|
||||
|
||||
LogicNode &gn = graph->nodes[graph->node_count++];
|
||||
memset(&gn, 0, sizeof(gn)); gn.timer_pending_output = -1;
|
||||
gn.type = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
gn.id = gate_el->IntAttribute("id");
|
||||
int gi = graph->node_count - 1;
|
||||
|
||||
// 读取延时毫秒(62P/62D)
|
||||
const char *delay_str = gate_el->Attribute("delayMs");
|
||||
if(delay_str) gn.delay_ms = atoi(delay_str);
|
||||
else gn.delay_ms = 1000; // 默认1000ms
|
||||
|
||||
// 读取 SR/RS 的 R/S 属性
|
||||
const char *r_attr = gate_el->Attribute("R");
|
||||
const char *s_attr = gate_el->Attribute("S");
|
||||
if(r_attr) { strncpy(gn.rs_r_src, r_attr, sizeof(gn.rs_r_src) - 1); }
|
||||
if(s_attr) { strncpy(gn.rs_s_src, s_attr, sizeof(gn.rs_s_src) - 1); }
|
||||
|
||||
int is_srrs = (gn.type == NODE_TYPE_SR_LATCH || gn.type == NODE_TYPE_RS_LATCH);
|
||||
|
||||
// 收集嵌套 Comb 的 Gate 索引(按 id 映射,用于 R/S 解析)
|
||||
int sub_gate_by_comb_id[16] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
|
||||
int sub_gate_count = 0;
|
||||
|
||||
for(tinyxml2::XMLElement *nc = comb_el->FirstChildElement("Comb"); nc;
|
||||
nc = nc->NextSiblingElement("Comb"))
|
||||
{
|
||||
int up = parse_xml_comb(nc, graph);
|
||||
if(up >= 0)
|
||||
{
|
||||
int cid = nc->IntAttribute("id");
|
||||
if(cid >= 0 && cid < 16) sub_gate_by_comb_id[cid] = up;
|
||||
sub_gate_count++;
|
||||
// 非 SR/RS: 自动创建嵌套 Comb Gate → 当前 Gate 的链接
|
||||
if(!is_srrs && graph->link_count < MAX_LINKS_PER_GRAPH)
|
||||
{
|
||||
LogicLink &l = graph->links[graph->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
|
||||
l.dest_type = gn.type; l.dest_id = gn.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SR/RS: 根据 R/S 属性创建链接
|
||||
if(is_srrs)
|
||||
{
|
||||
// 解析 "Comb#N" 或 "Input#N"
|
||||
for(int pin = 0; pin < 2; pin++)
|
||||
{
|
||||
const char *attr = (pin == 0) ? s_attr : r_attr;
|
||||
if(!attr || graph->link_count >= MAX_LINKS_PER_GRAPH) continue;
|
||||
|
||||
if(strncmp(attr, "Comb#", 5) == 0)
|
||||
{
|
||||
int cid = atoi(attr + 5);
|
||||
if(cid >= 0 && cid < 16 && sub_gate_by_comb_id[cid] >= 0)
|
||||
{
|
||||
int up = sub_gate_by_comb_id[cid];
|
||||
LogicLink &l = graph->links[graph->link_count++];
|
||||
memset(&l, 0, sizeof(l));
|
||||
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
|
||||
l.dest_type = gn.type; l.dest_id = gn.id;
|
||||
// SR: S=pin0, R=pin1; RS: S=pin1, R=pin0
|
||||
if(gn.type == NODE_TYPE_SR_LATCH)
|
||||
l.dst_pin = pin; // 0=S, 1=R
|
||||
else
|
||||
l.dst_pin = 1 - pin; // 0=R, 1=S
|
||||
}
|
||||
}
|
||||
else if(strncmp(attr, "Input#", 6) == 0)
|
||||
{
|
||||
int ino = atoi(attr + 6);
|
||||
int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, ino);
|
||||
if(si >= 0)
|
||||
{
|
||||
LogicLink &l = graph->links[graph->link_count++];
|
||||
memset(&l, 0, sizeof(l));
|
||||
l.src_type = NODE_TYPE_HW_INPUT; l.src_id = graph->nodes[si].id;
|
||||
l.dest_type = gn.type; l.dest_id = gn.id;
|
||||
if(gn.type == NODE_TYPE_SR_LATCH)
|
||||
l.dst_pin = pin;
|
||||
else
|
||||
l.dst_pin = 1 - pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tinyxml2::XMLElement *ins = comb_el->FirstChildElement("Inputs");
|
||||
if(ins && !is_srrs)
|
||||
for(tinyxml2::XMLElement *sig = ins->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(graph->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = graph->links[graph->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=graph->nodes[si].id;
|
||||
l.dest_type=gn.type; l.dest_id=gn.id; }
|
||||
}
|
||||
|
||||
tinyxml2::XMLElement *ous = comb_el->FirstChildElement("Outputs");
|
||||
if(ous)
|
||||
for(tinyxml2::XMLElement *sig = ous->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(graph->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = ensure_signal(graph, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = graph->links[graph->link_count++];
|
||||
l.src_type=gn.type; l.src_id=gn.id;
|
||||
l.dest_type=NODE_TYPE_HW_OUTPUT; l.dest_id=graph->nodes[si].id; }
|
||||
}
|
||||
return gi;
|
||||
}
|
||||
|
||||
int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *graph_count)
|
||||
{
|
||||
tinyxml2::XMLDocument doc;
|
||||
if(doc.LoadFile(file_path) != tinyxml2::XML_SUCCESS)
|
||||
{
|
||||
MY_LOG_E("XML配置文件解析失败: %s", file_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tinyxml2::XMLElement *root = doc.FirstChildElement("PLCConfig");
|
||||
if(NULL == root) { MY_LOG_E("XML根元素<PLCConfig>未找到"); return -1; }
|
||||
|
||||
*graph_count = 0;
|
||||
char buffer[4096];
|
||||
size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, fp);
|
||||
buffer[bytes_read] = '\0';
|
||||
fclose(fp);
|
||||
LogicGraph *cur = &graphs[0];
|
||||
memset(cur, 0, sizeof(LogicGraph));
|
||||
|
||||
char *write_ptr = buffer;
|
||||
char *clean_buffer = buffer;
|
||||
while (*clean_buffer) {
|
||||
if (!isspace((unsigned char)*clean_buffer)) {
|
||||
*write_ptr++ = *clean_buffer;
|
||||
}
|
||||
clean_buffer++;
|
||||
}
|
||||
*write_ptr = '\0';
|
||||
const char *ka = root->Attribute("key");
|
||||
const char *na = root->Attribute("name");
|
||||
if(ka) strncpy(cur->key, ka, sizeof(cur->key) - 1);
|
||||
if(na) strncpy(cur->name, na, sizeof(cur->name) - 1);
|
||||
|
||||
char *ptr = buffer;
|
||||
while (ptr && *graph_count < MAX_LOGIC_GRAPHS) {
|
||||
while (*ptr && *ptr != '"') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
// 第一遍:收集所有节点
|
||||
for(tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); chain;
|
||||
chain = chain->NextSiblingElement("Chain"))
|
||||
{
|
||||
tinyxml2::XMLElement *inputs = chain->FirstChildElement("Inputs");
|
||||
if(inputs)
|
||||
for(tinyxml2::XMLElement *sig = inputs->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
ensure_signal(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
|
||||
|
||||
char key[20];
|
||||
int key_len = 0;
|
||||
while (*ptr && *ptr != '"' && key_len < (int)sizeof(key) - 1) {
|
||||
key[key_len++] = *ptr++;
|
||||
}
|
||||
if (!*ptr) break;
|
||||
key[key_len] = '\0';
|
||||
ptr++;
|
||||
for(tinyxml2::XMLElement *cb = chain->FirstChildElement("Comb"); cb;
|
||||
cb = cb->NextSiblingElement("Comb"))
|
||||
parse_xml_comb(cb, cur);
|
||||
|
||||
while (*ptr && *ptr != ':') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
while (*ptr && *ptr != '"') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
|
||||
char graph_data[1024];
|
||||
int data_len = 0;
|
||||
while (*ptr && *ptr != '"' && data_len < (int)sizeof(graph_data) - 1) {
|
||||
graph_data[data_len++] = *ptr++;
|
||||
}
|
||||
if (!*ptr) break;
|
||||
graph_data[data_len] = '\0';
|
||||
ptr++;
|
||||
|
||||
if (strstr(graph_data, "node=") && strstr(graph_data, "link=")) {
|
||||
LogicGraph *cur_graph = &graphs[*graph_count];
|
||||
memset(cur_graph, 0, sizeof(LogicGraph));
|
||||
strncpy(cur_graph->key, key, sizeof(cur_graph->key) - 1);
|
||||
|
||||
char *data_ptr = graph_data;
|
||||
while (*data_ptr) {
|
||||
char *equals = strchr(data_ptr, '=');
|
||||
if (!equals) break;
|
||||
|
||||
char param_key[20];
|
||||
int pkey_len = equals - data_ptr;
|
||||
if (pkey_len < (int)sizeof(param_key)) {
|
||||
strncpy(param_key, data_ptr, pkey_len);
|
||||
param_key[pkey_len] = '\0';
|
||||
} else {
|
||||
data_ptr = equals + 1;
|
||||
continue;
|
||||
for(tinyxml2::XMLElement *gate = chain->FirstChildElement("Gate"); gate;
|
||||
gate = gate->NextSiblingElement("Gate"))
|
||||
{
|
||||
if(cur->node_count >= MAX_NODES_PER_GRAPH) break;
|
||||
LogicNode &gn = cur->nodes[cur->node_count++];
|
||||
memset(&gn, 0, sizeof(gn)); gn.timer_pending_output = -1;
|
||||
gn.type = gate_type_from_xml_str(gate->Attribute("type"));
|
||||
gn.id = gate->IntAttribute("id");
|
||||
}
|
||||
|
||||
char *value_start = equals + 1;
|
||||
char *value_end = strchr(value_start, '&');
|
||||
if (!value_end) value_end = value_start + strlen(value_start);
|
||||
int value_len = value_end - value_start;
|
||||
char value[512];
|
||||
if (value_len < (int)sizeof(value)) {
|
||||
strncpy(value, value_start, value_len);
|
||||
value[value_len] = '\0';
|
||||
} else {
|
||||
data_ptr = value_end + 1;
|
||||
continue;
|
||||
tinyxml2::XMLElement *outputs = chain->FirstChildElement("Outputs");
|
||||
if(outputs)
|
||||
for(tinyxml2::XMLElement *sig = outputs->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
ensure_signal(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
|
||||
}
|
||||
|
||||
if (strcmp(param_key, "name") == 0) {
|
||||
strncpy(cur_graph->name, value, sizeof(cur_graph->name) - 1);
|
||||
} else if (strcmp(param_key, "node") == 0) {
|
||||
char *node_start = strchr(value, '[');
|
||||
char *node_end = strchr(value, ']');
|
||||
if (node_start && node_end) {
|
||||
node_start++;
|
||||
*node_end = '\0';
|
||||
char *token = strtok(node_start, "{},");
|
||||
while (token && cur_graph->node_count < MAX_NODES_PER_GRAPH) {
|
||||
if (strlen(token) > 0) {
|
||||
LogicNode node;
|
||||
if (sscanf(token, "%d:%x:%d:%d",
|
||||
&node.type, &node.id, &node.x, &node.y) == 4) {
|
||||
node.value = 0;
|
||||
cur_graph->nodes[cur_graph->node_count++] = node;
|
||||
// 第二遍:Comb 输出 → 链级 Gate,Gate → Outputs
|
||||
for(tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); chain;
|
||||
chain = chain->NextSiblingElement("Chain"))
|
||||
{
|
||||
bool has_comb = (chain->FirstChildElement("Comb") != NULL);
|
||||
|
||||
// 收集链级 Gate(不在 Comb 内)
|
||||
tinyxml2::XMLElement *gate_el = NULL;
|
||||
tinyxml2::XMLElement *ins_el = NULL;
|
||||
tinyxml2::XMLElement *ous_el = NULL;
|
||||
tinyxml2::XMLElement *c = chain->FirstChildElement();
|
||||
while(c)
|
||||
{
|
||||
if(strcmp(c->Value(), "Gate") == 0) gate_el = c;
|
||||
if(strcmp(c->Value(), "Inputs") == 0) ins_el = c;
|
||||
if(strcmp(c->Value(), "Outputs") == 0) ous_el = c;
|
||||
c = c->NextSiblingElement();
|
||||
}
|
||||
}
|
||||
token = strtok(NULL, "{},");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(param_key, "link") == 0) {
|
||||
char *link_start = strchr(value, '[');
|
||||
char *link_end = strchr(value, ']');
|
||||
if (link_start && link_end) {
|
||||
link_start++;
|
||||
*link_end = '\0';
|
||||
char *token = strtok(link_start, "{},");
|
||||
while (token && cur_graph->link_count < MAX_LINKS_PER_GRAPH) {
|
||||
if (strlen(token) > 0) {
|
||||
LogicLink link;
|
||||
if (sscanf(token, "%d:%x-%d:%x",
|
||||
&link.src_type, &link.src_id,
|
||||
&link.dest_type, &link.dest_id) == 4) {
|
||||
cur_graph->links[cur_graph->link_count++] = link;
|
||||
}
|
||||
}
|
||||
token = strtok(NULL, "{},");
|
||||
|
||||
if(has_comb)
|
||||
{
|
||||
// 收集所有 Comb 的 Gate 索引(第一遍已解析)
|
||||
// Comb Gate → 链级 Gate (一个Comb一根线)
|
||||
if(gate_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi >= 0)
|
||||
{
|
||||
// 遍历所有顶层 Comb Gate
|
||||
for(c = chain->FirstChildElement("Comb"); c; c = c->NextSiblingElement("Comb"))
|
||||
{
|
||||
tinyxml2::XMLElement *cg = c->FirstChildElement("Gate");
|
||||
if(!cg) continue;
|
||||
int ct = gate_type_from_xml_str(cg->Attribute("type"));
|
||||
int ci = find_node_index(cur, ct, cg->IntAttribute("id"));
|
||||
if(ci >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
|
||||
{
|
||||
LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type = cur->nodes[ci].type;
|
||||
l.src_id = cur->nodes[ci].id;
|
||||
l.dest_type = cur->nodes[cgi].type;
|
||||
l.dest_id = cur->nodes[cgi].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
data_ptr = value_end;
|
||||
if (*data_ptr == '&') data_ptr++;
|
||||
}
|
||||
(*graph_count)++;
|
||||
|
||||
// 链级 Gate → 链级 Outputs
|
||||
if(gate_el && ous_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi >= 0)
|
||||
{
|
||||
for(tinyxml2::XMLElement *sig = ous_el->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=cur->nodes[cgi].type; l.src_id=cur->nodes[cgi].id;
|
||||
l.dest_type=NODE_TYPE_HW_OUTPUT; l.dest_id=cur->nodes[si].id; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 链级 Inputs → 链级 Gate(如 RS/SR 直接连 Input 信号)
|
||||
if(gate_el && ins_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi >= 0)
|
||||
{
|
||||
for(tinyxml2::XMLElement *sig = ins_el->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
|
||||
l.dest_type=cur->nodes[cgi].type; l.dest_id=cur->nodes[cgi].id; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RS/SR 的 R/S 属性引用的 Input 信号(无 <Inputs> 块时也需创建链接)
|
||||
if(gate_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
if(cgt == NODE_TYPE_SR_LATCH || cgt == NODE_TYPE_RS_LATCH)
|
||||
{
|
||||
const char *s_attr2 = gate_el->Attribute("S");
|
||||
const char *r_attr2 = gate_el->Attribute("R");
|
||||
int cgi2 = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi2 >= 0)
|
||||
{
|
||||
if(s_attr2 && strncmp(s_attr2, "Input#", 6) == 0)
|
||||
{
|
||||
int ino = atoi(s_attr2 + 6);
|
||||
int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino);
|
||||
if(si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
|
||||
l.dest_type=cur->nodes[cgi2].type; l.dest_id=cur->nodes[cgi2].id; }
|
||||
}
|
||||
if(r_attr2 && strncmp(r_attr2, "Input#", 6) == 0)
|
||||
{
|
||||
int ino = atoi(r_attr2 + 6);
|
||||
int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino);
|
||||
if(si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
|
||||
l.dest_type=cur->nodes[cgi2].type; l.dest_id=cur->nodes[cgi2].id; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 简单链:Inputs → Gate → Outputs
|
||||
if(!gate_el) continue;
|
||||
int gt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int gi = find_node_index(cur, gt, gate_el->IntAttribute("id"));
|
||||
if(gi < 0) continue;
|
||||
|
||||
if(ins_el)
|
||||
for(tinyxml2::XMLElement *sig = ins_el->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=NODE_TYPE_HW_INPUT; l.src_id=cur->nodes[si].id;
|
||||
l.dest_type=cur->nodes[gi].type; l.dest_id=cur->nodes[gi].id; }
|
||||
}
|
||||
|
||||
if(ous_el)
|
||||
for(tinyxml2::XMLElement *sig = ous_el->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
memset(&l, 0, sizeof(l)); l.dst_pin = -1;
|
||||
l.src_type=cur->nodes[gi].type; l.src_id=cur->nodes[gi].id;
|
||||
l.dest_type=NODE_TYPE_HW_OUTPUT; l.dest_id=cur->nodes[si].id; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*graph_count = 1;
|
||||
MY_LOG_I("XML配置解析成功: %d节点 %d连线", cur->node_count, cur->link_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -353,9 +642,19 @@ static int collect_input_signals(LogicGraph *graph, LogicNode *element_node,
|
|||
((link->dest_id & 0xFF) == (element_node->id & 0xFF))) {
|
||||
LogicNode *src_node = find_node(graph, link->src_type, link->src_id);
|
||||
if (src_node) {
|
||||
// SR/RS: 按 dst_pin 填入对应位置
|
||||
int is_srrs = (element_node->type == NODE_TYPE_SR_LATCH ||
|
||||
element_node->type == NODE_TYPE_RS_LATCH);
|
||||
if(is_srrs && link->dst_pin >= 0) {
|
||||
if(input_count < 2) {
|
||||
input_values[link->dst_pin] = src_node->value;
|
||||
if(link->dst_pin >= input_count) input_count = link->dst_pin + 1;
|
||||
}
|
||||
} else {
|
||||
input_values[input_count++] = src_node->value;
|
||||
printf(" 找到输入信号:%d:%d = %d\n",
|
||||
link->src_type, link->src_id, src_node->value);
|
||||
}
|
||||
printf(" 找到输入信号:%d:%d = %d (pin=%d)\n",
|
||||
link->src_type, link->src_id, src_node->value, link->dst_pin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -392,35 +691,65 @@ static void execute_or_gate(LogicNode *or_node, int *input_values, int input_cou
|
|||
}
|
||||
|
||||
static void execute_62p_gate(LogicNode *node, int input_value) {
|
||||
int tp = (node->id >> 8) & 0xFFFFFF;
|
||||
int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000;
|
||||
int component_id = node->id & 0xFF;
|
||||
if (input_value == 1) {
|
||||
printf(" 62p延时元件 (ID=%d, 序号=%d):输入=1,开始延时%d毫秒\n",
|
||||
node->id, component_id, tp);
|
||||
usleep(tp * 1000);
|
||||
if (!node->timer_active) {
|
||||
// 启动延时定时器,延时 delay_ms 毫秒后输出 1
|
||||
printf(" 62p延时元件 (ID=%d, 序号=%d):输入=1,启动非阻塞延时%d毫秒\n",
|
||||
node->id, component_id, delay_ms);
|
||||
clock_gettime(CLOCK_MONOTONIC, &node->timer_start);
|
||||
node->timer_delay_ms = delay_ms;
|
||||
node->timer_pending_output = 1;
|
||||
node->timer_active = 1;
|
||||
// 保持当前输出值不变,等待定时器到期
|
||||
} else if (get_elapsed_ms(&node->timer_start) >= node->timer_delay_ms) {
|
||||
// 定时器到期,输出 1
|
||||
node->value = 1;
|
||||
node->timer_active = 0;
|
||||
node->timer_pending_output = -1;
|
||||
printf(" 62p延时元件 (ID=%d, 序号=%d):延时结束,输出=1\n",
|
||||
node->id, component_id);
|
||||
}
|
||||
// 否则仍在延时中,保持当前值
|
||||
} else {
|
||||
// 输入为 0,立即输出 0 并取消定时器
|
||||
node->value = 0;
|
||||
node->timer_active = 0;
|
||||
node->timer_pending_output = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void execute_62d_gate(LogicNode *node, int input_value) {
|
||||
int td = (node->id >> 8) & 0xFFFFFF;
|
||||
int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000;
|
||||
int component_id = node->id & 0xFF;
|
||||
if (input_value == 1) {
|
||||
// 输入为 1,立即输出 1 并取消定时器
|
||||
node->value = 1;
|
||||
node->timer_active = 0;
|
||||
node->timer_pending_output = -1;
|
||||
printf(" 62d延时元件 (ID=%d, 序号=%d):输入=1,立即输出=1\n",
|
||||
node->id, component_id);
|
||||
} else {
|
||||
printf(" 62d延时元件 (ID=%d, 序号=%d):输入=0,开始延时%d毫秒\n",
|
||||
node->id, component_id, td);
|
||||
usleep(td * 1000);
|
||||
if (!node->timer_active) {
|
||||
// 启动延时定时器,延时 delay_ms 毫秒后输出 0
|
||||
printf(" 62d延时元件 (ID=%d, 序号=%d):输入=0,启动非阻塞延时%d毫秒\n",
|
||||
node->id, component_id, delay_ms);
|
||||
clock_gettime(CLOCK_MONOTONIC, &node->timer_start);
|
||||
node->timer_delay_ms = delay_ms;
|
||||
node->timer_pending_output = 0;
|
||||
node->timer_active = 1;
|
||||
// 保持当前输出值不变,等待定时器到期
|
||||
} else if (get_elapsed_ms(&node->timer_start) >= node->timer_delay_ms) {
|
||||
// 定时器到期,输出 0
|
||||
node->value = 0;
|
||||
node->timer_active = 0;
|
||||
node->timer_pending_output = -1;
|
||||
printf(" 62d延时元件 (ID=%d, 序号=%d):延时结束,输出=0\n",
|
||||
node->id, component_id);
|
||||
}
|
||||
// 否则仍在延时中,保持当前值
|
||||
}
|
||||
}
|
||||
|
||||
static void execute_rising_edge_gate(LogicNode *node, int input_value) {
|
||||
|
|
@ -475,6 +804,70 @@ static void execute_rs_latch(LogicNode *node, int *input_values, int input_count
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== 拓扑排序 ====================
|
||||
// 对逻辑图的节点进行拓扑排序,确保节点按依赖顺序执行
|
||||
// 返回值:排好序的节点索引数组,由调用者通过 sorted_indices 传出
|
||||
// 返回实际能排序的节点数(可能小于 node_count 如果有环)
|
||||
static int topological_sort(LogicGraph *graph, int *sorted_indices) {
|
||||
if (!graph || !sorted_indices) return 0;
|
||||
int n = graph->node_count;
|
||||
if (n == 0) return 0;
|
||||
|
||||
// 步骤1:计算每个节点的入度(从非硬件输入点的源节点连接过来的链路数)
|
||||
for (int i = 0; i < n; i++) {
|
||||
graph->nodes[i].indegree = 0;
|
||||
}
|
||||
for (int i = 0; i < graph->link_count; i++) {
|
||||
LogicLink *link = &graph->links[i];
|
||||
// 找到目标节点
|
||||
LogicNode *dest = find_node(graph, link->dest_type, link->dest_id);
|
||||
if (dest) {
|
||||
// 找到源节点(如果不是硬件输入点,则增加目标节点的入度)
|
||||
LogicNode *src = find_node(graph, link->src_type, link->src_id);
|
||||
if (src && src->type != NODE_TYPE_HW_INPUT) {
|
||||
dest->indegree++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤2:Kahn算法
|
||||
// 将所有入度为0的节点入队(硬件输入点入度始终为0)
|
||||
int queue[MAX_NODES_PER_GRAPH];
|
||||
int q_head = 0, q_tail = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (graph->nodes[i].indegree == 0) {
|
||||
queue[q_tail++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
int sorted_count = 0;
|
||||
while (q_head < q_tail) {
|
||||
int idx = queue[q_head++];
|
||||
sorted_indices[sorted_count++] = idx;
|
||||
|
||||
// 找到当前节点的所有后继节点,减少它们的入度
|
||||
for (int i = 0; i < graph->link_count; i++) {
|
||||
LogicLink *link = &graph->links[i];
|
||||
LogicNode *src = find_node(graph, link->src_type, link->src_id);
|
||||
if (src == &graph->nodes[idx]) {
|
||||
LogicNode *dest = find_node(graph, link->dest_type, link->dest_id);
|
||||
if (dest) {
|
||||
dest->indegree--;
|
||||
if (dest->indegree == 0) {
|
||||
queue[q_tail++] = (dest - graph->nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sorted_count < n) {
|
||||
MY_LOG_E("逻辑图[%s]存在环路依赖,仅排序了 %d/%d 个节点",
|
||||
graph->key, sorted_count, n);
|
||||
}
|
||||
return sorted_count;
|
||||
}
|
||||
|
||||
// ==================== 逻辑图执行 ====================
|
||||
int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) {
|
||||
if (!graph || !hw) return -1;
|
||||
|
|
@ -508,10 +901,12 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) {
|
|||
type_name, node->type, node->id, node->value);
|
||||
}
|
||||
|
||||
// 步骤2:执行逻辑元件运算
|
||||
printf("\n【逻辑元件运算】\n");
|
||||
for (int i = 0; i < graph->node_count; i++) {
|
||||
LogicNode *node = &graph->nodes[i];
|
||||
// 步骤2:按拓扑顺序执行逻辑元件运算
|
||||
printf("\n【逻辑元件运算】(拓扑排序执行)\n");
|
||||
int sorted_indices[MAX_NODES_PER_GRAPH];
|
||||
int sorted_count = topological_sort(graph, sorted_indices);
|
||||
for (int si = 0; si < sorted_count; si++) {
|
||||
LogicNode *node = &graph->nodes[sorted_indices[si]];
|
||||
int input_values[10];
|
||||
int input_count;
|
||||
switch (node->type) {
|
||||
|
|
@ -578,13 +973,25 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) {
|
|||
dest_node->value = src_node->value;
|
||||
set_hw_point_value(&hw->output_head, &hw->output_count,
|
||||
dest_node->id, dest_node->value);
|
||||
if (dest_node->id < MAX_OUT_NODES) {
|
||||
g_plc_st_out[dest_node->id] = dest_node->value;
|
||||
|
||||
// 通过 datacenter API 写入实际 out 信号
|
||||
std::string saddr, desc, dtype_str;
|
||||
uint8_t ctrl_type;
|
||||
std::vector<std::string> link_saddrs;
|
||||
if(0 == dc_get_signal_info_by_id("out", (uint32_t)dest_node->id, saddr, desc, dtype_str, ctrl_type, link_saddrs))
|
||||
{
|
||||
uint8_t val = (uint8_t)(src_node->value ? 1 : 0);
|
||||
dc_set_out_signal_val(saddr, &val, "plc");
|
||||
printf(" 硬件输出:ID=%d → %s = %d(已同步到datacenter)\n",
|
||||
dest_node->id, saddr.c_str(), val);
|
||||
}
|
||||
printf(" 硬件输出:ID=%d = %d(已同步到硬件管理器)\n",
|
||||
else
|
||||
{
|
||||
printf(" 硬件输出:ID=%d = %d(未找到datacenter信号,跳过写入)\n",
|
||||
dest_node->id, dest_node->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n【逻辑图执行结果】\n");
|
||||
for (int i = 0; i < graph->node_count; i++) {
|
||||
|
|
@ -611,38 +1018,77 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) {
|
|||
|
||||
// ==================== PLC 逻辑入口 ====================
|
||||
int PLC_Logic(void) {
|
||||
char proc_dir[512] = {0};
|
||||
if(0 != func_get_process_self_dir(proc_dir, sizeof(proc_dir)))
|
||||
{
|
||||
MY_LOG_E("func_get_process_self_dir failed");
|
||||
return -1;
|
||||
// 检查配置文件是否变化,仅在文件修改时重新解析
|
||||
struct stat file_stat;
|
||||
if (stat(g_plc_config_path, &file_stat) != 0) {
|
||||
MY_LOG_E("无法获取PLC配置文件状态: %s", g_plc_config_path);
|
||||
// 如果之前有缓存,继续使用缓存执行
|
||||
if (g_cached_graph_count == 0) return 2;
|
||||
// fall through to use cached graphs
|
||||
} else {
|
||||
if (file_stat.st_mtime == g_last_file_mtime && g_cached_graph_count > 0) {
|
||||
// 文件未变化,直接使用缓存的逻辑图执行
|
||||
HardwareManager hw;
|
||||
init_hardware_manager(&hw);
|
||||
extract_hw_points_from_graphs(g_cached_graphs, g_cached_graph_count, &hw, 0);
|
||||
|
||||
for (int i = 0; i < g_cached_graph_count; i++) {
|
||||
execute_logic_graph(&g_cached_graphs[i], &hw);
|
||||
}
|
||||
std::string file_path = std::string(proc_dir) + "config/PLC/Reclose_logic.txt";
|
||||
|
||||
LogicGraph graphs[MAX_LOGIC_GRAPHS];
|
||||
// 同步输出到实际 datacenter out 信号
|
||||
HWPoint *curr = hw.output_head;
|
||||
while (curr) {
|
||||
std::string saddr, desc, dtype_str;
|
||||
uint8_t ctrl_type;
|
||||
std::vector<std::string> link_saddrs;
|
||||
if(0 == dc_get_signal_info_by_id("out", (uint32_t)curr->id, saddr, desc, dtype_str, ctrl_type, link_saddrs))
|
||||
{
|
||||
uint8_t val = (uint8_t)(curr->value ? 1 : 0);
|
||||
dc_set_out_signal_val(saddr, &val, "plc");
|
||||
}
|
||||
curr = curr->next;
|
||||
}
|
||||
destroy_hardware_manager(&hw);
|
||||
return 0;
|
||||
}
|
||||
g_last_file_mtime = file_stat.st_mtime;
|
||||
}
|
||||
|
||||
// 文件已变化或首次加载,重新解析 XML
|
||||
int graph_count = 0;
|
||||
|
||||
if (parse_reclose_logic(file_path.c_str(), graphs, &graph_count) != 0) {
|
||||
int parse_ok = parse_reclose_logic_xml(g_plc_config_path, g_cached_graphs, &graph_count);
|
||||
if (parse_ok != 0) {
|
||||
// 解析失败但之前有缓存,继续使用缓存
|
||||
if (g_cached_graph_count > 0) {
|
||||
MY_LOG_E("PLC配置文件重新解析失败,继续使用上次缓存(%d个逻辑图)",
|
||||
g_cached_graph_count);
|
||||
graph_count = g_cached_graph_count; // 使用旧缓存
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
} else {
|
||||
g_cached_graph_count = graph_count;
|
||||
}
|
||||
|
||||
HardwareManager hw;
|
||||
init_hardware_manager(&hw);
|
||||
|
||||
extract_hw_points_from_graphs(graphs, graph_count, &hw, 0);
|
||||
extract_hw_points_from_graphs(g_cached_graphs, graph_count, &hw, 0);
|
||||
|
||||
#ifndef DEBUG
|
||||
printf("==================== 文件解析结果 ====================\n");
|
||||
printf("配置文件: %s\n", g_plc_config_path);
|
||||
printf("共解析到 %d 个逻辑图\n", graph_count);
|
||||
for (int i = 0; i < graph_count; i++) {
|
||||
printf(" 逻辑图%d:key=%s, name=%s, 节点数=%d, 链路数=%d\n",
|
||||
i + 1, graphs[i].key, graphs[i].name,
|
||||
graphs[i].node_count, graphs[i].link_count);
|
||||
i + 1, g_cached_graphs[i].key, g_cached_graphs[i].name,
|
||||
g_cached_graphs[i].node_count, g_cached_graphs[i].link_count);
|
||||
}
|
||||
print_hardware_manager(&hw);
|
||||
|
||||
for (int i = 0; i < graph_count; i++) {
|
||||
execute_logic_graph(&graphs[i], &hw);
|
||||
execute_logic_graph(&g_cached_graphs[i], &hw);
|
||||
}
|
||||
|
||||
printf("\n==================== 最终硬件输出状态 ====================\n");
|
||||
|
|
@ -659,6 +1105,24 @@ int PLC_Logic(void) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// ==================== 动态配置路径 ====================
|
||||
// 设置PLC配置文件路径(支持运行时动态修改)
|
||||
void plc_set_config_path(const char *path) {
|
||||
if (path && path[0] != '\0') {
|
||||
strncpy(g_plc_config_path, path, sizeof(g_plc_config_path) - 1);
|
||||
g_plc_config_path[sizeof(g_plc_config_path) - 1] = '\0';
|
||||
// 路径变更后重置缓存,强制下次重新加载
|
||||
g_last_file_mtime = 0;
|
||||
g_cached_graph_count = 0;
|
||||
MY_LOG_I("PLC配置路径已更新: %s", g_plc_config_path);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前PLC配置文件路径
|
||||
const char* plc_get_config_path(void) {
|
||||
return g_plc_config_path;
|
||||
}
|
||||
|
||||
// ==================== 模块初始化与线程 ====================
|
||||
int app_plc_init1(void *arg)
|
||||
{
|
||||
|
|
@ -668,6 +1132,13 @@ int app_plc_init1(void *arg)
|
|||
return -1;
|
||||
}
|
||||
|
||||
// 动态设置PLC配置文件路径
|
||||
char proc_dir[512] = {0};
|
||||
if (0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir))) {
|
||||
std::string plc_path = std::string(proc_dir) + "config/PLC/Reclose_logic.xml";
|
||||
plc_set_config_path(plc_path.c_str());
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
ret |= dc_signal_out("plc.run_cnt", "plc线程计数", DATA_TYPE_U32, &p_app->run_cnt);
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@
|
|||
#include "myDatacenter.h"
|
||||
#include "mongoose.h"
|
||||
#include "myCmd.h"
|
||||
#include "myFunc.h"
|
||||
#include <pthread.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
|
@ -815,6 +816,20 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
|
|||
std::vector<const char*> dc_types;
|
||||
const char *sub = data->valuestring + 10;
|
||||
while(*sub == ' ' || *sub == '\t') sub++;
|
||||
|
||||
// 当注册 out/all 时,保存 datacenter out 信号到 XML
|
||||
if(0 == strcmp(sub, "out") || 0 == strncmp(sub, "out ", 4)
|
||||
|| 0 == strcmp(sub, "all") || 0 == strncmp(sub, "all ", 4))
|
||||
{
|
||||
char resolved[512];
|
||||
char proc_dir[256] = {0};
|
||||
if(0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir)))
|
||||
{
|
||||
snprintf(resolved, sizeof(resolved), "%sconfig/SYSTEM/datacenter_out.xml", proc_dir);
|
||||
dc_save_out_signals_xml(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
if(0 == strcmp(sub, "all") || 0 == strncmp(sub, "all ", 4))
|
||||
{
|
||||
dc_types = {"out", "in", "yk", "ao", "param"};
|
||||
|
|
@ -861,6 +876,147 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
|
|||
cJSON_Delete(root);
|
||||
return;
|
||||
}
|
||||
if(0 == strcmp(type->valuestring, "get_file"))
|
||||
{
|
||||
cJSON *path_json = cJSON_GetObjectItem(root, "path");
|
||||
cJSON *resp = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(resp, "type", "file_content");
|
||||
if(path_json && cJSON_IsString(path_json) && path_json->valuestring)
|
||||
{
|
||||
// 相对路径 → 拼接进程所在目录为绝对路径
|
||||
char resolved_path[512] = {0};
|
||||
if(path_json->valuestring[0] == '/')
|
||||
{
|
||||
strncpy(resolved_path, path_json->valuestring, sizeof(resolved_path) - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
char proc_dir[256] = {0};
|
||||
if(0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir)))
|
||||
{
|
||||
snprintf(resolved_path, sizeof(resolved_path), "%s%s", proc_dir, path_json->valuestring);
|
||||
}
|
||||
else
|
||||
{
|
||||
strncpy(resolved_path, path_json->valuestring, sizeof(resolved_path) - 1);
|
||||
}
|
||||
}
|
||||
cJSON_AddStringToObject(resp, "path", path_json->valuestring);
|
||||
FILE *fp = fopen(resolved_path, "rb");
|
||||
if(fp)
|
||||
{
|
||||
fseek(fp, 0, SEEK_END);
|
||||
long fsize = ftell(fp);
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
if(fsize > 0 && fsize < 1024 * 1024) // 限制1MB
|
||||
{
|
||||
char *buf = (char*)malloc(fsize + 1);
|
||||
if(buf)
|
||||
{
|
||||
size_t n = fread(buf, 1, fsize, fp);
|
||||
buf[n] = '\0';
|
||||
cJSON_AddStringToObject(resp, "content", buf);
|
||||
free(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(resp, "error", "内存分配失败");
|
||||
}
|
||||
}
|
||||
else if(fsize <= 0)
|
||||
{
|
||||
cJSON_AddStringToObject(resp, "error", "文件为空");
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(resp, "error", "文件过大(>1MB)");
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(resp, "error", "无法打开文件");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddStringToObject(resp, "error", "缺少path参数");
|
||||
}
|
||||
char *p_tx = cJSON_Print(resp);
|
||||
if(p_tx)
|
||||
{
|
||||
ws_send_one(c->id, p_tx, strlen(p_tx));
|
||||
free(p_tx);
|
||||
}
|
||||
cJSON_Delete(resp);
|
||||
cJSON_Delete(root);
|
||||
return;
|
||||
}
|
||||
if(0 == strcmp(type->valuestring, "set_file"))
|
||||
{
|
||||
cJSON *path_json = cJSON_GetObjectItem(root, "path");
|
||||
cJSON *content_json = cJSON_GetObjectItem(root, "content");
|
||||
cJSON *resp = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(resp, "type", "file_saved");
|
||||
if(path_json && cJSON_IsString(path_json) && path_json->valuestring
|
||||
&& content_json && cJSON_IsString(content_json) && content_json->valuestring)
|
||||
{
|
||||
// 相对路径 → 拼接进程所在目录为绝对路径
|
||||
char resolved_path[512] = {0};
|
||||
if(path_json->valuestring[0] == '/')
|
||||
{
|
||||
strncpy(resolved_path, path_json->valuestring, sizeof(resolved_path) - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
char proc_dir[256] = {0};
|
||||
if(0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir)))
|
||||
{
|
||||
snprintf(resolved_path, sizeof(resolved_path), "%s%s", proc_dir, path_json->valuestring);
|
||||
}
|
||||
else
|
||||
{
|
||||
strncpy(resolved_path, path_json->valuestring, sizeof(resolved_path) - 1);
|
||||
}
|
||||
}
|
||||
cJSON_AddStringToObject(resp, "path", path_json->valuestring);
|
||||
FILE *fp = fopen(resolved_path, "wb");
|
||||
if(fp)
|
||||
{
|
||||
size_t len = strlen(content_json->valuestring);
|
||||
size_t n = fwrite(content_json->valuestring, 1, len, fp);
|
||||
fclose(fp);
|
||||
if(n == len)
|
||||
{
|
||||
cJSON_AddBoolToObject(resp, "success", 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddBoolToObject(resp, "success", 0);
|
||||
cJSON_AddStringToObject(resp, "error", "写入不完整");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddBoolToObject(resp, "success", 0);
|
||||
cJSON_AddStringToObject(resp, "error", "无法创建文件");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cJSON_AddBoolToObject(resp, "success", 0);
|
||||
cJSON_AddStringToObject(resp, "error", "缺少path或content参数");
|
||||
}
|
||||
char *p_tx = cJSON_Print(resp);
|
||||
if(p_tx)
|
||||
{
|
||||
ws_send_one(c->id, p_tx, strlen(p_tx));
|
||||
free(p_tx);
|
||||
}
|
||||
cJSON_Delete(resp);
|
||||
cJSON_Delete(root);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *curd = cJSON_GetObjectItem(root, "curd");
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{"840957954":"key=840957954&name=Block Over Current(G)&desc=desc&desc_pos_x=0&desc_pos_y=0&node=[{1:38:-330:-120},{0:1:130:-120},{1:36:-390:-10},{0:2:170:-10},{1:37:-400:80},{0:3:170:110},{3:0:-10:30}]&link=[{1:38-0:1},{3:0-0:2},{3:0-0:3},{1:36-3:0},{1:37-3:0}]","online_cfgId":"3486BE04"}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PLCConfig name="PLC Logic" key="1783070862205" checksum="3486BE04">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="5" x="-669" y="1065" />
|
||||
</Inputs>
|
||||
<Gate type="T62P" id="0" x="-345" y="1063" delayMs="1000" />
|
||||
<Outputs>
|
||||
<Signal no="1370" x="-10" y="1061" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="6" x="-667" y="1187" />
|
||||
</Inputs>
|
||||
<Gate type="T62D" id="0" x="-334" y="1185" delayMs="1000" />
|
||||
<Outputs>
|
||||
<Signal no="1371" x="-4" y="1186" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="7" x="-670" y="1305" />
|
||||
</Inputs>
|
||||
<Gate type="RISING" id="0" x="-336" y="1305" />
|
||||
<Outputs>
|
||||
<Signal no="1372" x="-3" y="1304" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="8" x="-662" y="1421" />
|
||||
</Inputs>
|
||||
<Gate type="FALLING" id="0" x="-335" y="1418" />
|
||||
<Outputs>
|
||||
<Signal no="1373" x="4" y="1419" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<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="-331" y="2193" />
|
||||
</Inputs>
|
||||
<Gate type="RS" id="0" x="14" y="2133" R="Comb#0" S="Input#15" />
|
||||
<Outputs>
|
||||
<Signal no="1376" x="375" y="2131" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<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>
|
||||
</PLCConfig>
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PLCConfig name="PLC Logic" key="1783070862205" checksum="3486BE04">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="5" x="-669" y="1065" />
|
||||
</Inputs>
|
||||
<Gate type="T62P" id="0" x="-345" y="1063" delayMs="1000" />
|
||||
<Outputs>
|
||||
<Signal no="1370" x="-10" y="1061" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="6" x="-667" y="1187" />
|
||||
</Inputs>
|
||||
<Gate type="T62D" id="0" x="-334" y="1185" delayMs="1000" />
|
||||
<Outputs>
|
||||
<Signal no="1371" x="-4" y="1186" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="7" x="-670" y="1305" />
|
||||
</Inputs>
|
||||
<Gate type="RISING" id="0" x="-336" y="1305" />
|
||||
<Outputs>
|
||||
<Signal no="1372" x="-3" y="1304" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<Chain gates="1" inputs="1" outputs="1">
|
||||
<Inputs>
|
||||
<Signal no="8" x="-662" y="1421" />
|
||||
</Inputs>
|
||||
<Gate type="FALLING" id="0" x="-335" y="1418" />
|
||||
<Outputs>
|
||||
<Signal no="1373" x="4" y="1419" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<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="-331" y="2193" />
|
||||
</Inputs>
|
||||
<Gate type="RS" id="0" x="14" y="2133" R="Comb#0" S="Input#15" />
|
||||
<Outputs>
|
||||
<Signal no="1376" x="375" y="2131" />
|
||||
</Outputs>
|
||||
</Chain>
|
||||
<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>
|
||||
</PLCConfig>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -778,6 +778,83 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
|||
border-bottom: 2px solid var(--primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
/* 信号选择列 */
|
||||
.plc-signal-col {
|
||||
width: 280px;
|
||||
min-width: 260px;
|
||||
flex-shrink: 0;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.plc-signal-col h3 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.plc-arrow-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 4px 0 12px 0;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.plc-arrow-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--primary);
|
||||
background: #fff;
|
||||
color: var(--primary);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
.plc-arrow-btn:hover {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.plc-arrow-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
.plc-sig-list {
|
||||
max-height: calc(100vh - 380px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.plc-sig-check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 6px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.plc-sig-check-item:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
.plc-sig-check-item .plc-sig-saddr {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.plc-sig-check-item .plc-sig-val {
|
||||
font-weight: 700;
|
||||
color: var(--info);
|
||||
min-width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
.plc-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -844,3 +921,157 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
|||
|
||||
/* === Dashboard plc card === */
|
||||
.dash-card.plc { border-top: 4px solid #FF9800; }
|
||||
.dash-card.plc_cfg { border-top: 4px solid #9C27B0; }
|
||||
|
||||
/* === PLC Config Page === */
|
||||
.plc-cfg-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.plc-cfg-tab {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.plc-cfg-tab:hover { background: #f0f0f0; color: #333; }
|
||||
.plc-cfg-tab.active { background: var(--primary); color: #fff; }
|
||||
|
||||
/* Signals Tab */
|
||||
.plc-sig-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.plc-sig-header select,
|
||||
.plc-sig-header input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plc-sig-header select { min-width: 160px; }
|
||||
.plc-sig-header input { flex: 1; min-width: 200px; }
|
||||
.plc-sig-table-wrap {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 260px);
|
||||
}
|
||||
.plc-sig-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plc-sig-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f5f5f5;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #ddd;
|
||||
z-index: 1;
|
||||
}
|
||||
.plc-sig-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.plc-sig-table tr:hover td { background: #f0f8ff; }
|
||||
.plc-sig-table code {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Viewer & Editor Canvas */
|
||||
.plc-viewer-toolbar,
|
||||
.plc-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.plc-editor-toolbar input {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plc-canvas-wrap {
|
||||
background: #1a1a2e;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 300px);
|
||||
min-height: 400px;
|
||||
}
|
||||
.plc-canvas-wrap canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.plc-view-legend {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
}
|
||||
.plc-editor-main {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
.plc-editor-main .plc-canvas-wrap {
|
||||
flex: 1;
|
||||
height: calc(100vh - 280px);
|
||||
min-height: 450px;
|
||||
}
|
||||
.plc-editor-info {
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Modal rows */
|
||||
.plc-modal-row {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.plc-modal-row:hover { background: #f0f8ff; }
|
||||
.plc-modal-row.plc-modal-used {
|
||||
opacity: 0.5;
|
||||
background: #fff5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.plc-modal-row.plc-modal-current {
|
||||
background: #e3f2fd;
|
||||
border-left: 3px solid #2196F3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<script src="js/ws-client.js"></script>
|
||||
<script src="js/pages.js"></script>
|
||||
<script src="js/plc_debug.js"></script>
|
||||
<script src="js/plc_config.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ var App = (function() {
|
|||
{ hash: '#param', label: '⚙ 定值 (param)' },
|
||||
{ hash: '#datacenter', label: '📊 数据中心' },
|
||||
{ hash: '#plc_debug', label: '🔌 PLC调试' },
|
||||
{ hash: '#plc_config', label: '🧮 PLC逻辑配置' },
|
||||
{ hash: '#monitor', label: '📋 数据监控' },
|
||||
{ hash: '#terminal', label: '💻 命令终端' }
|
||||
];
|
||||
|
|
@ -188,6 +189,12 @@ var App = (function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// file_content / file_saved — 转发给当前页面
|
||||
if ((data.type === 'file_content' || data.type === 'file_saved') && currentPage && currentPage.onData) {
|
||||
currentPage.onData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
var signalTypes = ['out', 'in', 'yk', 'ao', 'param'];
|
||||
for (var i = 0; i < signalTypes.length; i++) {
|
||||
var st = signalTypes[i];
|
||||
|
|
@ -224,6 +231,7 @@ var App = (function() {
|
|||
registerPage('#param', ParamPage);
|
||||
registerPage('#datacenter', DatacenterPage);
|
||||
registerPage('#plc_debug', PlcDebugPage);
|
||||
registerPage('#plc_config', PlcConfigPage);
|
||||
registerPage('#monitor', MonitorPage);
|
||||
registerPage('#terminal', TerminalPage);
|
||||
|
||||
|
|
|
|||
|
|
@ -313,7 +313,8 @@ var DashboardPage = {
|
|||
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
|
||||
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
|
||||
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' },
|
||||
{ cls: 'plc', icon: '🔌', title: 'PLC调试', desc: '指示灯/按键绑定 out 信号调试', hash: '#plc_debug' }
|
||||
{ cls: 'plc', icon: '🔌', title: 'PLC调试', desc: '指示灯/按键绑定 out 信号调试', hash: '#plc_debug' },
|
||||
{ cls: 'plc_cfg', icon: '🧮', title: 'PLC逻辑配置', desc: 'PLC 逻辑图可视化查看与在线编辑', hash: '#plc_config' }
|
||||
];
|
||||
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
|
||||
html += '<div class="dashboard-grid">';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,6 +8,7 @@ var PlcDebugPage = (function() {
|
|||
var outSignals = [];
|
||||
var ledChecked = {}; // {idx: true}
|
||||
var btnChecked = {}; // {idx: true}
|
||||
var signalChecked = {}; // {idx: true} 信号选择列勾选
|
||||
|
||||
function getOutSignals() {
|
||||
if (typeof App !== 'undefined' && App.getSignalData) {
|
||||
|
|
@ -135,6 +136,32 @@ var PlcDebugPage = (function() {
|
|||
}
|
||||
html += '</div>';
|
||||
|
||||
// 信号选择列
|
||||
html += '<div class="plc-signal-col">';
|
||||
html += '<h3>📋 Out 信号</h3>';
|
||||
html += '<div class="plc-arrow-row">';
|
||||
html += '<button class="plc-arrow-btn" id="plc-arrow-left" title="选中信号→添加到指示灯">←</button>';
|
||||
html += '<button class="plc-arrow-btn" id="plc-arrow-right" title="选中信号→添加到按键">→</button>';
|
||||
html += '</div>';
|
||||
html += '<div class="plc-sig-list">';
|
||||
if (outSignals.length === 0) {
|
||||
html += '<div style="color:#999;text-align:center;padding:20px">暂无 out 信号</div>';
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var s = 0; s < outSignals.length; s++) {
|
||||
var sig = outSignals[s];
|
||||
var sigChk = signalChecked[s] ? ' checked' : '';
|
||||
html += '<div class="plc-sig-check-item">'
|
||||
+ '<input type="checkbox" class="plc-sig-cb" data-sig-idx="' + s + '"' + sigChk + '>'
|
||||
+ '<span class="plc-sig-saddr" title="' + fmtVal(sig.saddr) + (sig.desc ? ' - ' + fmtVal(sig.desc) : '') + '">' + fmtVal(sig.saddr) + '</span>'
|
||||
+ (sig.desc ? '<span style="color:#999;font-size:11px;max-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + fmtVal(sig.desc) + '</span>' : '')
|
||||
+ '<span class="plc-sig-val">' + fmtVal(sig.val) + '</span>'
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
html += '</div></div>';
|
||||
|
||||
// 按键
|
||||
html += '<div class="plc-col"><h3>🔘 按键</h3>';
|
||||
for (var j = 0; j < PlcDebugButtons.length; j++) {
|
||||
|
|
@ -233,6 +260,86 @@ var PlcDebugPage = (function() {
|
|||
};
|
||||
}
|
||||
|
||||
// 信号选择列勾选框
|
||||
var sigCbs = document.querySelectorAll('.plc-sig-cb');
|
||||
for (var p = 0; p < sigCbs.length; p++) {
|
||||
sigCbs[p].onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var sigIdx = parseInt(this.getAttribute('data-sig-idx'));
|
||||
if (!isNaN(sigIdx)) {
|
||||
if (this.checked) signalChecked[sigIdx] = true; else delete signalChecked[sigIdx];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 信号选择列整行点击切换勾选
|
||||
var sigRows = document.querySelectorAll('.plc-sig-check-item');
|
||||
for (var q = 0; q < sigRows.length; q++) {
|
||||
sigRows[q].onclick = function(e) {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
var cb = this.querySelector('.plc-sig-cb');
|
||||
if (cb) {
|
||||
cb.checked = !cb.checked;
|
||||
var sigIdx = parseInt(cb.getAttribute('data-sig-idx'));
|
||||
if (!isNaN(sigIdx)) {
|
||||
if (cb.checked) signalChecked[sigIdx] = true; else delete signalChecked[sigIdx];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 左箭头 → 添加到指示灯
|
||||
var arrowLeft = document.getElementById('plc-arrow-left');
|
||||
if (arrowLeft) {
|
||||
arrowLeft.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var added = 0;
|
||||
for (var k in signalChecked) {
|
||||
if (signalChecked.hasOwnProperty(k)) {
|
||||
var idx = parseInt(k);
|
||||
if (idx >= 0 && idx < outSignals.length) {
|
||||
PlcDebugLeds.push({ saddr: outSignals[idx].saddr, desc: outSignals[idx].desc || '', val: outSignals[idx].val });
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
showToast('✅ 已添加 ' + added + ' 个信号到指示灯');
|
||||
signalChecked = {};
|
||||
renderAll();
|
||||
}
|
||||
else {
|
||||
showToast('⚠️ 请先勾选信号');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 右箭头 → 添加到按键
|
||||
var arrowRight = document.getElementById('plc-arrow-right');
|
||||
if (arrowRight) {
|
||||
arrowRight.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var added = 0;
|
||||
for (var k in signalChecked) {
|
||||
if (signalChecked.hasOwnProperty(k)) {
|
||||
var idx = parseInt(k);
|
||||
if (idx >= 0 && idx < outSignals.length) {
|
||||
PlcDebugButtons.push({ saddr: outSignals[idx].saddr, desc: outSignals[idx].desc || '', val: outSignals[idx].val });
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
showToast('✅ 已添加 ' + added + ' 个信号到按键');
|
||||
signalChecked = {};
|
||||
renderAll();
|
||||
}
|
||||
else {
|
||||
showToast('⚠️ 请先勾选信号');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('plc-refresh').onclick = function() {
|
||||
refreshOutData();
|
||||
renderAll();
|
||||
|
|
@ -259,7 +366,7 @@ var PlcDebugPage = (function() {
|
|||
|
||||
return {
|
||||
render: function(container) {
|
||||
ledChecked = {}; btnChecked = {};
|
||||
ledChecked = {}; btnChecked = {}; signalChecked = {};
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>PLC 调试</h2><p>指示灯绑定 out 已注册信号(>0 亮/≤0 灭),按键绑定 out 已注册信号(点击翻转 0↔1)</p></div>'
|
||||
+ '<div id="plc-content"></div>';
|
||||
|
|
|
|||
Loading…
Reference in New Issue