feat: PLC逻辑配置页面 + 配置文件去重修复 + 前端WebSocket修复
【PLC逻辑配置页面】(new: plc_config.js, 1947行) - 三Tab页: 信号列表 / 逻辑图查看器 / 新建配置编辑器 - Canvas渲染: 节点渐变+阴影+左侧色条+引脚圈+贝塞尔连线+发光效果 - 查看器: 使用文件中保存的x/y坐标(与编辑器布局一致),首次命中链路匹配 - 编辑器: 左击选中连线高亮(橙色)+右键/Delete删除,双击节点修改,拖拽布局 - 支持11种逻辑元件: 输入/输出/与门/或门/非门/62p延时/62d延时/上升/下降沿/SR/RS触发器 【配置文件去重修复】 - Reclose_logic.txt: 消除两个AND#0重复ID,修复为AND#0 + AND#3 - generateAndSaveConfig: 保存前自动检测并修正重复 type+id - initEditor: 加载时消除已有重复ID + 更新editNextGateIdx防止后续冲突 - 保存格式扩展: node支持inPins(第5字段),link支持dstPin(第4字段) 【前端WebSocket修复】 - plc_config.js语法错误(缺少闭合})导致PlcConfigPage未定义 - App.init()在WsClient.connect()之前崩溃显示已断开 【后端】 - ws_method.cpp: curd:add 后立即推送信号列表 - dc_cfg_check.cpp: 数据中心配置检查 - plc.cpp: PLC模块配置路径更新 - myDatacenter.h: 新增接口声明 【文档+样式】 - PLC配置文件解析与生成.md: 模块分析文档 - style.css: PLC配置页面样式(Canvas/表格/图例/Modal) - datacenter_out.xml: Out信号注册数据
This commit is contained in:
parent
a339ed73a0
commit
12dcec0f31
|
|
@ -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)` 实现非阻塞毫秒级延时
|
||||||
|
|
@ -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);
|
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);
|
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
|
typedef struct
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,48 @@
|
||||||
#include "dc_signal_internal.h"
|
#include "dc_signal_internal.h"
|
||||||
#include "tinyxml2.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)
|
void dc_param_cfg_check(const std::string &path)
|
||||||
{
|
{
|
||||||
if (false == dc_get_param_cfg_change()) return;
|
if (false == dc_get_param_cfg_change()) return;
|
||||||
|
|
|
||||||
|
|
@ -170,16 +170,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,
|
static void extract_hw_points_from_graphs(LogicGraph graphs[], int graph_count,
|
||||||
HardwareManager *hw, int default_input_value) {
|
HardwareManager *hw, int default_input_value) {
|
||||||
stru_plc_cfg *p = nullptr;
|
|
||||||
for (int g = 0; g < graph_count; g++) {
|
for (int g = 0; g < graph_count; g++) {
|
||||||
LogicGraph *graph = &graphs[g];
|
LogicGraph *graph = &graphs[g];
|
||||||
for (int n = 0; n < graph->node_count; n++) {
|
for (int n = 0; n < graph->node_count; n++) {
|
||||||
LogicNode *node = &graph->nodes[n];
|
LogicNode *node = &graph->nodes[n];
|
||||||
if (node->type == NODE_TYPE_HW_INPUT) {
|
if (node->type == NODE_TYPE_HW_INPUT) {
|
||||||
for (uint32_t i = 0; i < g_plc_cfg.size(); i++) {
|
// node->id 现在是 datacenter out 信号的 no(序号)
|
||||||
p = &g_plc_cfg.at(i);
|
// 通过 dc_get_signal_info_by_id 找到 saddr,再读取当前值
|
||||||
if (p->base.inf == node->id) {
|
std::string saddr, desc, dtype_str;
|
||||||
default_input_value = *(uint8_t *)p->p_data;
|
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,
|
set_hw_point_value(&hw->input_head, &hw->input_count,
|
||||||
|
|
@ -726,11 +741,23 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) {
|
||||||
dest_node->value = src_node->value;
|
dest_node->value = src_node->value;
|
||||||
set_hw_point_value(&hw->output_head, &hw->output_count,
|
set_hw_point_value(&hw->output_head, &hw->output_count,
|
||||||
dest_node->id, dest_node->value);
|
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);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf(" 硬件输出:ID=%d = %d(未找到datacenter信号,跳过写入)\n",
|
||||||
|
dest_node->id, dest_node->value);
|
||||||
}
|
}
|
||||||
printf(" 硬件输出:ID=%d = %d(已同步到硬件管理器)\n",
|
|
||||||
dest_node->id, dest_node->value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -777,11 +804,16 @@ int PLC_Logic(void) {
|
||||||
execute_logic_graph(&g_cached_graphs[i], &hw);
|
execute_logic_graph(&g_cached_graphs[i], &hw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步输出到全局数组
|
// 同步输出到实际 datacenter out 信号
|
||||||
HWPoint *curr = hw.output_head;
|
HWPoint *curr = hw.output_head;
|
||||||
while (curr) {
|
while (curr) {
|
||||||
if (curr->id < MAX_OUT_NODES) {
|
std::string saddr, desc, dtype_str;
|
||||||
g_plc_st_out[curr->id] = curr->value;
|
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;
|
curr = curr->next;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@
|
||||||
#include "myDatacenter.h"
|
#include "myDatacenter.h"
|
||||||
#include "mongoose.h"
|
#include "mongoose.h"
|
||||||
#include "myCmd.h"
|
#include "myCmd.h"
|
||||||
|
#include "myFunc.h"
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#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;
|
std::vector<const char*> dc_types;
|
||||||
const char *sub = data->valuestring + 10;
|
const char *sub = data->valuestring + 10;
|
||||||
while(*sub == ' ' || *sub == '\t') sub++;
|
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))
|
if(0 == strcmp(sub, "all") || 0 == strncmp(sub, "all ", 4))
|
||||||
{
|
{
|
||||||
dc_types = {"out", "in", "yk", "ao", "param"};
|
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);
|
cJSON_Delete(root);
|
||||||
return;
|
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");
|
cJSON *curd = cJSON_GetObjectItem(root, "curd");
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"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"}
|
{"1783070862205":"key=1783070862205&name=PLC%20Logic&desc=desc&desc_pos_x=0&desc_pos_y=0&node=[{1:3b:-674:-216},{1:3c:-674:16},{2:0:-326:-101},{0:551:1:-101},{1:3d:-674:223},{4:1:-321:220},{0:552:1:221},{1:42:-668:414},{1:43:-660:553},{1:44:-660:698},{1:45:-652:819},{3:0:-348:627},{0:554:-15:558},{0:553:-11:437},{0:555:-11:696},{3:3:-44:893},{1:46:-339:989},{0:556:326:886},{2:1:295:1061},{1:47:-12:1113},{0:557:574:1058},{4:2:561:1210},{0:558:882:1203}]&link=[{2:0-0:551},{1:3b-2:0},{1:3c-2:0},{1:3d-4:1},{4:1-0:552},{1:42-3:0},{1:43-3:0},{1:44-3:0},{1:45-3:0},{3:0-0:553},{3:0-0:554},{3:0-0:555},{3:0-3:0},{1:47-2:1},{2:1-0:557},{2:1-4:2},{4:2-0:558},{3:0-3:0},{3:0-3:0},{3:0-3:3},{1:46-3:3},{3:3-0:556},{3:3-2:1}]","online_cfgId":"3486BE04"}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"1783070862205":"key=1783070862205&name=PLC%20Logic&desc=desc&desc_pos_x=0&desc_pos_y=0&node=[{1:3b:-674:-216},{1:3c:-674:16},{2:0:-326:-101},{0:551:1:-101},{1:3d:-674:223},{4:1:-321:220},{0:552:1:221},{1:42:-668:414},{1:43:-660:553},{1:44:-660:698},{1:45:-652:819},{3:0:-348:627},{0:554:-15:558},{0:553:-11:437},{0:555:-11:696},{3:3:-44:893},{1:46:-339:989},{0:556:326:886},{2:1:295:1061},{1:47:-12:1113},{0:557:574:1058},{4:2:561:1210},{0:558:882:1203}]&link=[{2:0-0:551},{1:3b-2:0},{1:3c-2:0},{1:3d-4:1},{4:1-0:552},{1:42-3:0},{1:43-3:0},{1:44-3:0},{1:45-3:0},{3:0-0:553},{3:0-0:554},{3:0-0:555},{3:0-3:0},{1:47-2:1},{2:1-0:557},{2:1-4:2},{4:2-0:558},{3:0-3:0},{3:0-3:0},{1:46-3:0},{3:0-0:556},{3:0-2:1}]","online_cfgId":"3486BE04"}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -844,3 +844,157 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
||||||
|
|
||||||
/* === Dashboard plc card === */
|
/* === Dashboard plc card === */
|
||||||
.dash-card.plc { border-top: 4px solid #FF9800; }
|
.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/ws-client.js"></script>
|
||||||
<script src="js/pages.js"></script>
|
<script src="js/pages.js"></script>
|
||||||
<script src="js/plc_debug.js"></script>
|
<script src="js/plc_debug.js"></script>
|
||||||
|
<script src="js/plc_config.js"></script>
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ var App = (function() {
|
||||||
{ hash: '#param', label: '⚙ 定值 (param)' },
|
{ hash: '#param', label: '⚙ 定值 (param)' },
|
||||||
{ hash: '#datacenter', label: '📊 数据中心' },
|
{ hash: '#datacenter', label: '📊 数据中心' },
|
||||||
{ hash: '#plc_debug', label: '🔌 PLC调试' },
|
{ hash: '#plc_debug', label: '🔌 PLC调试' },
|
||||||
|
{ hash: '#plc_config', label: '🧮 PLC逻辑配置' },
|
||||||
{ hash: '#monitor', label: '📋 数据监控' },
|
{ hash: '#monitor', label: '📋 数据监控' },
|
||||||
{ hash: '#terminal', label: '💻 命令终端' }
|
{ hash: '#terminal', label: '💻 命令终端' }
|
||||||
];
|
];
|
||||||
|
|
@ -188,6 +189,12 @@ var App = (function() {
|
||||||
return;
|
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'];
|
var signalTypes = ['out', 'in', 'yk', 'ao', 'param'];
|
||||||
for (var i = 0; i < signalTypes.length; i++) {
|
for (var i = 0; i < signalTypes.length; i++) {
|
||||||
var st = signalTypes[i];
|
var st = signalTypes[i];
|
||||||
|
|
@ -224,6 +231,7 @@ var App = (function() {
|
||||||
registerPage('#param', ParamPage);
|
registerPage('#param', ParamPage);
|
||||||
registerPage('#datacenter', DatacenterPage);
|
registerPage('#datacenter', DatacenterPage);
|
||||||
registerPage('#plc_debug', PlcDebugPage);
|
registerPage('#plc_debug', PlcDebugPage);
|
||||||
|
registerPage('#plc_config', PlcConfigPage);
|
||||||
registerPage('#monitor', MonitorPage);
|
registerPage('#monitor', MonitorPage);
|
||||||
registerPage('#terminal', TerminalPage);
|
registerPage('#terminal', TerminalPage);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,8 @@ var DashboardPage = {
|
||||||
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
|
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
|
||||||
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
|
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
|
||||||
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' },
|
{ 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>';
|
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
|
||||||
html += '<div class="dashboard-grid">';
|
html += '<div class="dashboard-grid">';
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue