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:
ypc 2026-07-06 14:56:04 +08:00
parent a339ed73a0
commit 12dcec0f31
14 changed files with 11902 additions and 2901 deletions

View File

@ -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/1ID 即为信号点号
- 对于逻辑元件(类型 2~10ID 编码规则见下表
- **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=输入2RS: 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)` 实现非阻塞毫秒级延时

View File

@ -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

View File

@ -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;

View File

@ -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,
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,
@ -726,11 +741,23 @@ 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);
}
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);
}
// 同步输出到全局数组
// 同步输出到实际 datacenter out 信号
HWPoint *curr = hw.output_head;
while (curr) {
if (curr->id < MAX_OUT_NODES) {
g_plc_st_out[curr->id] = curr->value;
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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");

View File

@ -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"}

View File

@ -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

View File

@ -844,3 +844,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;
}

View File

@ -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>

View File

@ -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);

View File

@ -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