feat(plc): PLC配置文件格式从JSON/TXT迁移到XML

- 将 Reclose_logic 配置文件格式从 JSON/TXT 统一改为 XML
- 适配 plc.cpp 中的 XML 配置解析逻辑
- 更新 plc_config.js 前端 JS 以支持 XML 格式配置
- 更新 packed_fs.c Web 资源打包
- 更新问题处理文档
This commit is contained in:
ypc 2026-07-07 09:32:54 +08:00
parent e6f4de251f
commit 6bdd467998
9 changed files with 5787 additions and 4775 deletions

View File

@ -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) ## 2026-07-01: com_channel_recv_cb 三级路由重构 (转发→bind→IEC)
### 需求背景 ### 需求背景

View File

@ -7,9 +7,9 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <ctype.h>
#include <time.h> // clock_gettime 非阻塞定时器 #include <time.h> // clock_gettime 非阻塞定时器
#include <sys/stat.h> // stat() 文件变化检测 #include <sys/stat.h> // stat() 文件变化检测
#include "tinyxml2.h" // XML 配置文件解析
#define MAX_OUT_NODES 20 // 最大输出节点数 #define MAX_OUT_NODES 20 // 最大输出节点数
@ -30,8 +30,8 @@ std::vector<stru_plc_cfg> g_plc_cfg={};
// ==================== 常量定义 ==================== // ==================== 常量定义 ====================
#define MAX_LOGIC_GRAPHS 4 // 最大逻辑图数量 #define MAX_LOGIC_GRAPHS 4 // 最大逻辑图数量
#define MAX_NODES_PER_GRAPH 20 // 每个逻辑图最大节点数 #define MAX_NODES_PER_GRAPH 64 // 每个逻辑图最大节点数
#define MAX_LINKS_PER_GRAPH 20 // 每个逻辑图最大链路数 #define MAX_LINKS_PER_GRAPH 64 // 每个逻辑图最大链路数
// 节点类型标识 // 节点类型标识
#define NODE_TYPE_HW_OUTPUT 0 // 硬件输出点dev_in #define NODE_TYPE_HW_OUTPUT 0 // 硬件输出点dev_in
@ -64,8 +64,6 @@ typedef struct {
typedef struct { typedef struct {
int type; int type;
int id; int id;
int x;
int y;
int value; int value;
int last_input; int last_input;
// 非阻塞延时定时器字段62p/62d门使用 // 非阻塞延时定时器字段62p/62d门使用
@ -97,7 +95,7 @@ typedef struct {
LOCAL LogicGraph g_cached_graphs[MAX_LOGIC_GRAPHS]; // 缓存的逻辑图 LOCAL LogicGraph g_cached_graphs[MAX_LOGIC_GRAPHS]; // 缓存的逻辑图
LOCAL int g_cached_graph_count = 0; // 缓存的逻辑图数量 LOCAL int g_cached_graph_count = 0; // 缓存的逻辑图数量
LOCAL time_t g_last_file_mtime = 0; // 配置文件最后修改时间 LOCAL time_t g_last_file_mtime = 0; // 配置文件最后修改时间
LOCAL char g_plc_config_path[256] = "/mnt/RTU/config/PLC/Reclose_logic.txt"; // 可配置路径 LOCAL char g_plc_config_path[256] = "/mnt/RTU/config/PLC/Reclose_logic.xml"; // 优先XML
// ==================== 硬件点操作函数 ==================== // ==================== 硬件点操作函数 ====================
static HWPoint* create_hw_point(int id, int value) { static HWPoint* create_hw_point(int id, int value) {
@ -241,171 +239,258 @@ static LogicNode* find_node(LogicGraph *graph, int type, int id) {
return NULL; return NULL;
} }
// ==================== 逻辑图解析 ==================== // ==================== XML 配置文件解析 (Comb结构) ====================
int parse_reclose_logic(const char *file_path, LogicGraph graphs[], int *graph_count) {
FILE *fp = fopen(file_path, "rb"); static int gate_type_from_xml_str(const char *type_str)
if (!fp) { {
MY_LOG_E("无法打开PLC配置文件: %s", file_path); 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;
for(tinyxml2::XMLElement *nc = comb_el->FirstChildElement("Comb"); nc;
nc = nc->NextSiblingElement("Comb"))
{
int up = parse_xml_comb(nc, graph);
if(up >= 0 && graph->link_count < MAX_LINKS_PER_GRAPH)
{ LogicLink &l = graph->links[graph->link_count++];
l.src_type = graph->nodes[up].type; l.src_id = graph->nodes[up].id;
l.dest_type = gn.type; l.dest_id = gn.id; }
}
tinyxml2::XMLElement *ins = comb_el->FirstChildElement("Inputs");
if(ins)
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++];
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; return -1;
} }
tinyxml2::XMLElement *root = doc.FirstChildElement("PLCConfig");
if(NULL == root) { MY_LOG_E("XML根元素<PLCConfig>未找到"); return -1; }
*graph_count = 0; *graph_count = 0;
char buffer[4096]; LogicGraph *cur = &graphs[0];
size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, fp); memset(cur, 0, sizeof(LogicGraph));
if (bytes_read <= 0) {
MY_LOG_E("PLC配置文件为空或读取失败: %s", file_path); const char *ka = root->Attribute("key");
fclose(fp); const char *na = root->Attribute("name");
return -1; if(ka) strncpy(cur->key, ka, sizeof(cur->key) - 1);
if(na) strncpy(cur->name, na, sizeof(cur->name) - 1);
// 第一遍:收集所有节点
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"));
for(tinyxml2::XMLElement *cb = chain->FirstChildElement("Comb"); cb;
cb = cb->NextSiblingElement("Comb"))
parse_xml_comb(cb, cur);
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");
}
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"));
} }
buffer[bytes_read] = '\0';
fclose(fp);
char *write_ptr = buffer; // 第二遍Comb 输出 → 链级 GateGate → Outputs
char *clean_buffer = buffer; for(tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); chain;
while (*clean_buffer) { chain = chain->NextSiblingElement("Chain"))
if (!isspace((unsigned char)*clean_buffer)) { {
*write_ptr++ = *clean_buffer; 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();
} }
clean_buffer++;
}
*write_ptr = '\0';
char *ptr = buffer; if(has_comb)
int graph_index = 0; {
while (ptr && *graph_count < MAX_LOGIC_GRAPHS) { // 收集所有 Comb 的 Gate 索引(第一遍已解析)
while (*ptr && *ptr != '"') ptr++; // Comb Gate → 链级 Gate (一个Comb一根线)
if (!*ptr) break; if(gate_el)
ptr++; {
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
char key[20]; int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
int key_len = 0; if(cgi >= 0)
while (*ptr && *ptr != '"' && key_len < (int)sizeof(key) - 1) { {
key[key_len++] = *ptr++; // 遍历所有顶层 Comb Gate
} for(c = chain->FirstChildElement("Comb"); c; c = c->NextSiblingElement("Comb"))
if (!*ptr) break; {
key[key_len] = '\0'; tinyxml2::XMLElement *cg = c->FirstChildElement("Gate");
ptr++; if(!cg) continue;
int ct = gate_type_from_xml_str(cg->Attribute("type"));
while (*ptr && *ptr != ':') ptr++; int ci = find_node_index(cur, ct, cg->IntAttribute("id"));
if (!*ptr) break; if(ci >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
ptr++; {
while (*ptr && *ptr != '"') ptr++; LogicLink &l = cur->links[cur->link_count++];
if (!*ptr) break; l.src_type = cur->nodes[ci].type;
ptr++; l.src_id = cur->nodes[ci].id;
l.dest_type = cur->nodes[cgi].type;
char graph_data[1024]; l.dest_id = cur->nodes[cgi].id;
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++;
graph_index++;
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;
}
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;
}
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, "{},");
int node_index = 0;
while (token && cur_graph->node_count < MAX_NODES_PER_GRAPH) {
node_index++;
if (strlen(token) > 0) {
LogicNode node;
memset(&node, 0, sizeof(node));
node.timer_pending_output = -1; // 初始无待定输出
if (sscanf(token, "%d:%x:%d:%d",
&node.type, &node.id, &node.x, &node.y) == 4) {
cur_graph->nodes[cur_graph->node_count++] = node;
} else {
MY_LOG_E("逻辑图[%s]节点解析失败,第%d个节点数据: %s",
key, node_index, token);
}
}
token = strtok(NULL, "{},");
} }
} else {
MY_LOG_E("逻辑图[%s]的node参数格式错误缺少'['或']'", key);
}
} 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, "{},");
int link_index = 0;
while (token && cur_graph->link_count < MAX_LINKS_PER_GRAPH) {
link_index++;
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;
} else {
MY_LOG_E("逻辑图[%s]链路解析失败,第%d条链路数据: %s",
key, link_index, token);
}
}
token = strtok(NULL, "{},");
}
} else {
MY_LOG_E("逻辑图[%s]的link参数格式错误缺少'['或']'", key);
} }
} }
data_ptr = value_end;
if (*data_ptr == '&') data_ptr++;
} }
(*graph_count)++;
} else { // 链级 Gate → 链级 Outputs
MY_LOG_E("逻辑图[%d] (key=%s) 缺少 node= 或 link= 参数,已跳过", if(gate_el && ous_el)
graph_index, key); {
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++];
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; }
}
}
}
}
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++];
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++];
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; }
}
} }
} }
if (*graph_count == 0) { *graph_count = 1;
MY_LOG_E("PLC配置文件解析失败未找到任何有效的逻辑图"); MY_LOG_I("XML配置解析成功: %d节点 %d连线", cur->node_count, cur->link_count);
return -1;
}
return 0; return 0;
} }
@ -823,9 +908,10 @@ int PLC_Logic(void) {
g_last_file_mtime = file_stat.st_mtime; g_last_file_mtime = file_stat.st_mtime;
} }
// 文件已变化或首次加载,重新解析 // 文件已变化或首次加载,重新解析 XML
int graph_count = 0; int graph_count = 0;
if (parse_reclose_logic(g_plc_config_path, g_cached_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) { if (g_cached_graph_count > 0) {
MY_LOG_E("PLC配置文件重新解析失败继续使用上次缓存%d个逻辑图", MY_LOG_E("PLC配置文件重新解析失败继续使用上次缓存%d个逻辑图",
@ -902,7 +988,7 @@ int app_plc_init1(void *arg)
// 动态设置PLC配置文件路径 // 动态设置PLC配置文件路径
char proc_dir[512] = {0}; char proc_dir[512] = {0};
if (0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir))) { if (0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir))) {
std::string plc_path = std::string(proc_dir) + "config/PLC/Reclose_logic.txt"; std::string plc_path = std::string(proc_dir) + "config/PLC/Reclose_logic.xml";
plc_set_config_path(plc_path.c_str()); plc_set_config_path(plc_path.c_str());
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,151 +0,0 @@
{
"_doc": "Reclose_logic.txt → JSON 分层配置",
"name": "PLC Logic",
"key": "1783070862205",
"checksum": "3486BE04",
"scale": "23节点 / 20连线 / 3条链",
"chains": [
{
"gates_count": 1,
"logic": {
"type": "OR",
"id": 0,
"x": -346,
"y": 141,
"from": [
{
"no": 59,
"x": -676,
"y": 69
},
{
"no": 60,
"x": -674,
"y": 183
}
],
"to": {
"no": 1361,
"x": -17,
"y": 137
}
}
},
{
"gates_count": 1,
"logic": {
"type": "NOT",
"id": 0,
"x": -345,
"y": 296,
"from": [
{
"no": 61,
"x": -669,
"y": 298
}
],
"to": {
"no": 1362,
"x": -10,
"y": 294
}
}
},
{
"gates_count": 4,
"logic": {
"type": "NOT",
"id": 1,
"x": 591,
"y": 971,
"gates_in": {
"type": "OR",
"id": 1,
"x": 288,
"y": 890,
"from": [
{
"no": 71,
"x": -7,
"y": 964
}
],
"gates_in": {
"type": "AND",
"id": 1,
"x": -43,
"y": 803,
"from": [
{
"no": 70,
"x": -323,
"y": 858
}
],
"gates_in": {
"type": "AND",
"id": 0,
"x": -346,
"y": 595,
"from": [
{
"no": 66,
"x": -668,
"y": 414
},
{
"no": 67,
"x": -667,
"y": 532
},
{
"no": 68,
"x": -669,
"y": 650
},
{
"no": 69,
"x": -672,
"y": 767
}
],
"to": [
{
"no": 1363,
"x": -12,
"y": 488
},
{
"no": 1364,
"x": -13,
"y": 592
},
{
"no": 1365,
"x": -11,
"y": 696
}
]
},
"to": {
"no": 1366,
"x": 314,
"y": 769
}
},
"to": {
"no": 1367,
"x": 621,
"y": 853
}
},
"to": {
"no": 1368,
"x": 892,
"y": 970
}
}
}
]
}

View File

@ -1 +0,0 @@
{"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

@ -1 +0,0 @@
{"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"}

View File

@ -0,0 +1,60 @@
<?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>
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb>
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<Inputs>
<Signal no="62" x="-669" y="413" />
<Signal no="63" x="-670" y="534" />
<Signal no="64" x="-667" y="650" />
<Signal no="65" x="-669" y="769" />
</Inputs>
<Gate type="AND" id="0" x="-346" y="595" />
<Outputs>
<Signal no="1363" x="-9" y="490" />
<Signal no="1364" x="-11" y="599" />
<Signal no="1365" x="-10" y="700" />
</Outputs>
</Comb>
<Gate type="AND" id="1" x="-42" y="829" />
<Outputs>
<Signal no="1366" x="324" y="773" />
</Outputs>
</Comb>
<Gate type="OR" id="1" x="288" y="890" />
<Outputs>
<Signal no="1367" x="627" y="836" />
</Outputs>
</Comb>
<Gate type="NOT" id="1" x="594" y="950" />
<Outputs>
<Signal no="1368" x="915" y="947" />
</Outputs>
</Chain>
</PLCConfig>

View File

@ -0,0 +1,60 @@
<?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>
<Inputs>
<Signal no="67" x="-6" y="937" />
</Inputs>
<Comb>
<Inputs>
<Signal no="66" x="-333" y="876" />
</Inputs>
<Comb>
<Inputs>
<Signal no="62" x="-669" y="413" />
<Signal no="63" x="-670" y="534" />
<Signal no="64" x="-667" y="650" />
<Signal no="65" x="-669" y="769" />
</Inputs>
<Gate type="AND" id="0" x="-346" y="595" />
<Outputs>
<Signal no="1363" x="-9" y="490" />
<Signal no="1364" x="-11" y="599" />
<Signal no="1365" x="-10" y="700" />
</Outputs>
</Comb>
<Gate type="AND" id="1" x="-42" y="829" />
<Outputs>
<Signal no="1366" x="324" y="773" />
</Outputs>
</Comb>
<Gate type="OR" id="1" x="288" y="890" />
<Outputs>
<Signal no="1367" x="627" y="836" />
</Outputs>
</Comb>
<Gate type="NOT" id="1" x="594" y="950" />
<Outputs>
<Signal no="1368" x="915" y="947" />
</Outputs>
</Chain>
</PLCConfig>

View File

@ -44,7 +44,7 @@ var PlcConfigPage = (function() {
var editDragCanvas = null; // 画布拖拽状态 var editDragCanvas = null; // 画布拖拽状态
var editPanX = 0, editPanY = 0; // 画布偏移 var editPanX = 0, editPanY = 0; // 画布偏移
var editScale = 1.0; // 缩放 var editScale = 1.0; // 缩放
var editNextGateIdx = 0; // 逻辑门计数器 var editNextGateIdx = {}; // {type: nextIdx} 每种门类型独立序号从0开始
var editLoadedFromFile = false; // 是否从文件加载 var editLoadedFromFile = false; // 是否从文件加载
var editLastClickTime = 0; // 上次 click 时间戳(双击检测) var editLastClickTime = 0; // 上次 click 时间戳(双击检测)
var editLastClickNode = -1; // 上次 click 的节点索引 var editLastClickNode = -1; // 上次 click 的节点索引
@ -152,7 +152,7 @@ var PlcConfigPage = (function() {
function renderTabs() { function renderTabs() {
var tabs = [ var tabs = [
{ id: 'signals', label: '📋 信号列表', title: 'datacenter_out.xml — 已注册 Out 信号' }, { id: 'signals', label: '📋 信号列表', title: 'datacenter_out.xml — 已注册 Out 信号' },
{ id: 'viewer', label: '👁️ 逻辑图查看', title: 'Reclose_logic.txt 可视化' }, { id: 'viewer', label: '👁️ 逻辑图查看', title: 'Reclose_logic.xml 可视化' },
{ id: 'editor', label: '✏️ 新建配置', title: '新建/编辑 PLC 逻辑配置' } { id: 'editor', label: '✏️ 新建配置', title: '新建/编辑 PLC 逻辑配置' }
]; ];
var html = '<div class="plc-cfg-tabs">'; var html = '<div class="plc-cfg-tabs">';
@ -308,8 +308,8 @@ var PlcConfigPage = (function() {
document.getElementById('plc-view-zoomout').onclick = function() { viewScale = Math.max(0.2, viewScale / 1.2); drawViewer(); }; document.getElementById('plc-view-zoomout').onclick = function() { viewScale = Math.max(0.2, viewScale / 1.2); drawViewer(); };
document.getElementById('plc-view-reset').onclick = function() { viewAutoFit = true; drawViewer(); }; document.getElementById('plc-view-reset').onclick = function() { viewAutoFit = true; drawViewer(); };
document.getElementById('plc-view-reload').onclick = function() { document.getElementById('plc-view-reload').onclick = function() {
loadedFiles['config/PLC/Reclose_logic.txt'] = null; loadedFiles['config/PLC/Reclose_logic.xml'] = null;
loadedFiles['config/PLC/Reclose_logic.txt.bak'] = null; loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
logicGraphs = []; logicGraphs = [];
loadLogicFileAll(function() { refreshGraphSelect(); }); loadLogicFileAll(function() { refreshGraphSelect(); });
}; };
@ -341,109 +341,204 @@ var PlcConfigPage = (function() {
cb(); cb();
} }
} }
requestFile('config/PLC/Reclose_logic.txt', function(content) { requestFile('config/PLC/Reclose_logic.xml', function(content) {
if (!content) { failedFiles.push('Reclose_logic.txt'); tryCb(); return; } if (!content) { failedFiles.push('Reclose_logic.xml'); tryCb(); return; }
parseLogicFile(content, 'Reclose_logic.txt'); parseLogicFile(content, 'Reclose_logic.xml');
loadedFiles2.push('Reclose_logic.txt'); loadedFiles2.push('Reclose_logic.xml');
tryCb(); tryCb();
}); });
requestFile('config/PLC/Reclose_logic.txt.bak', function(content) { requestFile('config/PLC/Reclose_logic.xml.bak', function(content) {
if (!content) { tryCb(); return; } if (!content) { tryCb(); return; }
parseLogicFile(content, 'Reclose_logic.txt.bak'); parseLogicFile(content, 'Reclose_logic.xml.bak');
loadedFiles2.push('Reclose_logic.txt.bak'); loadedFiles2.push('Reclose_logic.xml.bak');
tryCb(); tryCb();
}); });
} }
function loadLogicFile(cb) { function loadLogicFile(cb) {
requestFile('config/PLC/Reclose_logic.txt', function(content) { requestFile('config/PLC/Reclose_logic.xml', function(content) {
if (!content) { showToast('加载 Reclose_logic.txt 失败', 'err'); return; } if (!content) { showToast('加载 Reclose_logic.xml 失败', 'err'); return; }
parseLogicFile(content, 'Reclose_logic.txt'); parseLogicFile(content, 'Reclose_logic.xml');
if (cb) cb(); if (cb) cb();
}); });
} }
function parseLogicFile(content, sourceName) { function parseLogicFile(content, sourceName) {
// 先移除同源文件的旧解析结果,保留其他源的数据 logicGraphs = logicGraphs.filter(function(g) { return g._source !== sourceName; });
logicGraphs = logicGraphs.filter(function(g) {
return g._source !== sourceName;
});
onlineCfgId = onlineCfgId || ''; onlineCfgId = onlineCfgId || '';
try { parseLogicXml(content, sourceName);
var clean = content.replace(/\s/g, '');
var data = JSON.parse(clean);
// 只在首次(非 bak加载时更新 onlineCfgId
if (sourceName.indexOf('.bak') < 0) {
onlineCfgId = data.online_cfgId || '';
}
for (var key in data) {
if (key === 'online_cfgId') continue;
var g = parseGraphData(key, data[key]);
if (g) { g._source = sourceName; logicGraphs.push(g); }
}
} catch(e) {
showToast('解析 ' + sourceName + ' 失败: ' + e.message, 'err');
}
} }
function parseGraphData(key, dataStr) { // XML → 扁平 graph与 C++ parse_xml_comb 完全一致
var params = {}; function parseLogicXml(xmlStr, sourceName) {
var pairs = dataStr.split('&'); var parser = new DOMParser();
for (var i = 0; i < pairs.length; i++) { var doc = parser.parseFromString(xmlStr, 'text/xml');
var idx = pairs[i].indexOf('='); var root = doc.documentElement;
if (idx === -1) continue; if (!root || root.nodeName !== 'PLCConfig') {
params[pairs[i].substring(0, idx)] = decodeURIComponent(pairs[i].substring(idx + 1)); showToast('XML 根元素不是 PLCConfig', 'err'); return;
}
if (sourceName.indexOf('.bak') < 0) {
onlineCfgId = root.getAttribute('checksum') || '';
}
var thisKey = root.getAttribute('key') || sourceName;
var thisName = root.getAttribute('name') || '';
var g = { key: thisKey, name: thisName, nodes: [], links: [] };
var GTM = {'INPUT':1,'OUTPUT':0,'OR':2,'AND':3,'NOT':4,'T62P':5,'T62D':6,'RISING':7,'SR':8,'RS':9,'FALLING':10};
var sigNodeMap = {};
function ensureSig(ioType, no, x, y) {
var k = ioType + ':' + no;
if (sigNodeMap[k] !== undefined) {
if (x !== undefined) { g.nodes[sigNodeMap[k]].x = x; g.nodes[sigNodeMap[k]].y = y; }
return sigNodeMap[k];
}
g.nodes.push({ type:ioType, id:no, x:x||0, y:y||0 });
sigNodeMap[k] = g.nodes.length - 1;
return g.nodes.length - 1;
}
function ensureGate(type, id, x, y, inPins, delayMs) {
for (var i = 0; i < g.nodes.length; i++)
if (g.nodes[i].type === type && (g.nodes[i].id & 0xFF) === (id & 0xFF)) return i;
var nd = { type:type, id:id, x:x||0, y:y||0 };
if (inPins) nd.inPins = inPins;
if (delayMs) nd.id = (delayMs << 8) | nd.id;
g.nodes.push(nd);
return g.nodes.length - 1;
} }
var graph = { key: key, name: params.name || '', nodes: [], links: [] }; function parseComb(combEl) {
var ge = combEl.firstElementChild;
while (ge && ge.nodeName !== 'Gate') ge = ge.nextElementSibling;
if (!ge) return -1;
var t = GTM[ge.getAttribute('type')] || 0;
var gid = parseInt(ge.getAttribute('id'));
var gi = ensureGate(t, gid,
parseInt(ge.getAttribute('x')), parseInt(ge.getAttribute('y')),
ge.getAttribute('inPins') ? parseInt(ge.getAttribute('inPins')) : null,
ge.getAttribute('delayMs') ? parseInt(ge.getAttribute('delayMs')) : null);
// 解析节点 var c = combEl.firstElementChild;
if (params.node) { while (c) {
var m = params.node.match(/\[(.*)\]/); if (c.nodeName === 'Comb') {
if (m) { var upi = parseComb(c);
var toks = m[1].match(/\{[^}]+\}/g) || []; if (upi >= 0) g.links.push({ src_type:g.nodes[upi].type, src_id:g.nodes[upi].id, dst_type:t, dst_id:gid });
for (var j = 0; j < toks.length; j++) { }
var inner = toks[j].replace(/[{}]/g, ''); c = c.nextElementSibling;
var parts = inner.split(':'); }
if (parts.length >= 4) {
var nodeData = { var ie = combEl.firstElementChild;
type: parseInt(parts[0]), while (ie && ie.nodeName !== 'Inputs') ie = ie.nextElementSibling;
id: parseInt(parts[1], 16), if (ie) {
x: parseInt(parts[2]), var s = ie.firstElementChild;
y: parseInt(parts[3]) while (s) {
}; if (s.nodeName === 'Signal') {
if (parts.length >= 5) nodeData.inPins = parseInt(parts[4]); var no = parseInt(s.getAttribute('no'));
graph.nodes.push(nodeData); ensureSig(NT_INPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
g.links.push({ src_type:NT_INPUT, src_id:no, dst_type:t, dst_id:gid });
} }
s = s.nextElementSibling;
}
}
var oe = combEl.firstElementChild;
while (oe && oe.nodeName !== 'Outputs') oe = oe.nextElementSibling;
if (oe) {
var s = oe.firstElementChild;
while (s) {
if (s.nodeName === 'Signal') {
var no = parseInt(s.getAttribute('no'));
ensureSig(NT_OUTPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
g.links.push({ src_type:t, src_id:gid, dst_type:NT_OUTPUT, dst_id:no });
}
s = s.nextElementSibling;
}
}
return gi;
}
var chains = root.getElementsByTagName('Chain');
for (var ci = 0; ci < chains.length; ci++) {
var chain = chains[ci];
var c = chain.firstElementChild;
while (c) {
if (c.nodeName === 'Inputs') {
var s = c.firstElementChild;
while (s) {
if (s.nodeName === 'Signal')
ensureSig(NT_INPUT, parseInt(s.getAttribute('no')),
parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
s = s.nextElementSibling;
}
}
if (c.nodeName === 'Outputs') {
var s = c.firstElementChild;
while (s) {
if (s.nodeName === 'Signal')
ensureSig(NT_OUTPUT, parseInt(s.getAttribute('no')),
parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y')));
s = s.nextElementSibling;
}
}
if (c.nodeName === 'Comb') parseComb(c);
if (c.nodeName === 'Gate') {
var gt = GTM[c.getAttribute('type')] || 0;
ensureGate(gt, parseInt(c.getAttribute('id')),
parseInt(c.getAttribute('x')), parseInt(c.getAttribute('y')));
}
c = c.nextElementSibling;
}
}
// Comb链: Comb Gate → 链级 Gate (一个Comb一根线) + 链级 Gate → Outputs
// 简单链: Inputs → Gate → Outputs
for (var ci2 = 0; ci2 < chains.length; ci2++) {
var chain2 = chains[ci2];
var hasComb = false;
var c3 = chain2.firstElementChild;
while (c3) { if (c3.nodeName === 'Comb') { hasComb = true; break; } c3 = c3.nextElementSibling; }
var ge2 = null, ie2 = null, oe2 = null;
c3 = chain2.firstElementChild;
while (c3) {
if (c3.nodeName === 'Gate') ge2 = c3;
if (c3.nodeName === 'Inputs') ie2 = c3;
if (c3.nodeName === 'Outputs') oe2 = c3;
c3 = c3.nextElementSibling;
}
if (hasComb && ge2) {
// 链级 Comb → 取其 Gate → 链级 Gate
c3 = chain2.firstElementChild;
while (c3) {
if (c3.nodeName === 'Comb') {
var cg = c3.firstElementChild;
while (cg && cg.nodeName !== 'Gate') cg = cg.nextElementSibling;
if (cg) {
var ct = GTM[cg.getAttribute('type')] || 0;
var ci = parseInt(cg.getAttribute('id'));
g.links.push({ src_type:ct, src_id:ci, dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) });
}
}
c3 = c3.nextElementSibling;
}
if (oe2) {
var s = oe2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:GTM[ge2.getAttribute('type')] || 0, src_id:parseInt(ge2.getAttribute('id')), dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
}
} else if (!hasComb && ge2) {
var gt = GTM[ge2.getAttribute('type')] || 0;
var gid = parseInt(ge2.getAttribute('id'));
if (ie2) {
var s = ie2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:gt, dst_id:gid }); s = s.nextElementSibling; }
}
if (oe2) {
var s = oe2.firstElementChild;
while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:gt, src_id:gid, dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; }
} }
} }
} }
// 解析链路 g._source = sourceName;
if (params.link) { logicGraphs.push(g);
var m2 = params.link.match(/\[(.*)\]/);
if (m2) {
var toks2 = m2[1].match(/\{[^}]+\}/g) || [];
for (var k = 0; k < toks2.length; k++) {
var inner2 = toks2[k].replace(/[{}]/g, '');
var linkParts = inner2.split('-');
if (linkParts.length === 2) {
var src = linkParts[0].split(':');
var dst = linkParts[1].split(':');
var linkData = {
src_type: parseInt(src[0]),
src_id: parseInt(src[1], 16),
dst_type: parseInt(dst[0]),
dst_id: parseInt(dst[1], 16)
};
if (dst.length >= 3) linkData.dstPin = parseInt(dst[2]);
graph.links.push(linkData);
}
}
}
}
return graph;
} }
function refreshGraphSelect() { function refreshGraphSelect() {
@ -967,7 +1062,7 @@ var PlcConfigPage = (function() {
editPanX = 0; editPanY = 0; editPanX = 0; editPanY = 0;
editScale = 1.0; editScale = 1.0;
editLoadedFromFile = false; editLoadedFromFile = false;
editNextGateIdx = 0; editNextGateIdx = {};
container.innerHTML = '' container.innerHTML = ''
+ '<div class="plc-editor-toolbar">' + '<div class="plc-editor-toolbar">'
@ -1007,7 +1102,7 @@ var PlcConfigPage = (function() {
setupEditorCanvas(); setupEditorCanvas();
drawEditor(); drawEditor();
// 加载当前 Reclose_logic.txt 为编辑起点 // 加载当前 Reclose_logic.xml 为编辑起点
function initEditor() { function initEditor() {
loadLogicFile(function() { loadLogicFile(function() {
if (logicGraphs.length > 0) { if (logicGraphs.length > 0) {
@ -1023,26 +1118,23 @@ var PlcConfigPage = (function() {
editKey = g.key; editKey = g.key;
editName = g.name || ''; editName = g.name || '';
editOnlineCfgId = onlineCfgId; editOnlineCfgId = onlineCfgId;
// 复制节点到编辑器 // 复制节点到编辑器
var maxGateIdx = 0; for (var i = 0; i < g.nodes.length; i++) {
for (var i = 0; i < g.nodes.length; i++) { var n = g.nodes[i];
var n = g.nodes[i]; var newNode = {
var newNode = { type: n.type, id: n.id, x: n.x, y: n.y,
type: n.type, id: n.id, x: n.x, y: n.y, _lx: n.x, _ly: n.y,
_lx: n.x, _ly: n.y, saddr: '', desc: '', no: n.id
saddr: '', desc: '', no: n.id };
}; // 查找信号信息
// 查找信号信息 var sig = findSignalByNo(n.id);
var sig = findSignalByNo(n.id); if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; }
if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; } // 逻辑门 inPins — 优先使用文件中保存的配置
// 逻辑门 inPins — 优先使用文件中保存的配置 if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) {
if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) { newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2);
newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2);
var idx = n.id & 0xFF;
if (idx > maxGateIdx) maxGateIdx = idx;
}
editNodes.push(newNode);
} }
editNodes.push(newNode);
}
// 复制链路(按节点在新数组中的索引) // 复制链路(按节点在新数组中的索引)
for (var j = 0; j < g.links.length; j++) { for (var j = 0; j < g.links.length; j++) {
var l = g.links[j]; var l = g.links[j];
@ -1051,22 +1143,33 @@ var PlcConfigPage = (function() {
if (si >= 0 && di >= 0) editLinks.push({ srcIdx: si, dstIdx: di }); if (si >= 0 && di >= 0) editLinks.push({ srcIdx: si, dstIdx: di });
} }
editLoadedFromFile = true; editLoadedFromFile = true;
// 更新全局门计数器,避免后续新增门 ID 冲突
editNextGateIdx = maxGateIdx + 1;
// 消除已有的重复 type+id门电路限定确保保存后文件 ID 唯一 // 按类型统计最大序号,后续新增时每种类型独立递增
var seenIds = {}; var maxByType = {};
for (var di = 0; di < editNodes.length; di++) { for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di]; var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue; if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var key2 = dn.type + ':' + (dn.id & 0xFF); var idx = dn.id & 0xFF;
if (seenIds[key2]) { if (idx >= (maxByType[dn.type] || -1)) maxByType[dn.type] = idx;
editNodes[di].id = (dn.id & ~0xFF) | editNextGateIdx; }
editNextGateIdx++; for (var t in maxByType) {
} else { editNextGateIdx[t] = maxByType[t] + 1;
seenIds[key2] = true; }
}
// 消除已有的重复 type+id 并基于每类型独立序号分配新 ID
var seenIds = {};
for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var key2 = dn.type + ':' + (dn.id & 0xFF);
if (seenIds[key2]) {
var next = editNextGateIdx[dn.type] || 0;
editNodes[di].id = (dn.id & ~0xFF) | next;
editNextGateIdx[dn.type] = next + 1;
} else {
seenIds[key2] = true;
} }
}
// 更新输入框 // 更新输入框
var keyEl = document.getElementById('plc-edit-key'); var keyEl = document.getElementById('plc-edit-key');
@ -1123,6 +1226,7 @@ var PlcConfigPage = (function() {
document.getElementById('plc-modal-cancel').onclick = closeModal; document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-ok').onclick = function() { document.getElementById('plc-modal-ok').onclick = function() {
closeModal(); closeModal();
var delType = node.type;
editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; }); editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; });
for (var i = 0; i < editLinks.length; i++) { for (var i = 0; i < editLinks.length; i++) {
if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--; if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--;
@ -1130,6 +1234,20 @@ var PlcConfigPage = (function() {
} }
editNodes.splice(idx, 1); editNodes.splice(idx, 1);
editSelectedNode = -1; editSelectedNode = -1;
// 压缩同类型门序号:删除 #1 后 #2→#1, #3→#2 …
if (delType !== NT_INPUT && delType !== NT_OUTPUT) {
var sameType = [];
for (var s = 0; s < editNodes.length; s++) {
if (editNodes[s].type === delType) sameType.push(s);
}
sameType.sort(function(a, b) { return (editNodes[a].id & 0xFF) - (editNodes[b].id & 0xFF); });
for (var a = 0; a < sameType.length; a++) {
editNodes[sameType[a]].id = (editNodes[sameType[a]].id & ~0xFF) | a;
}
editNextGateIdx[delType] = sameType.length;
}
drawEditor(); drawEditor();
showToast('模块已删除', 'ok'); showToast('模块已删除', 'ok');
}; };
@ -1165,10 +1283,9 @@ var PlcConfigPage = (function() {
drawEditor(); drawEditor();
if (node.type === NT_INPUT || node.type === NT_OUTPUT) { if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
showRebindSignalModal(idx); showRebindSignalModal(idx);
} else if (node.type === NT_AND || node.type === NT_OR) {
showGateConfigModal(idx);
} else { } else {
showToast(getNodeDisplayLabel(node) + ' (无需额外配置)', 'info'); // 运算模块:双击即选中,不再弹窗对齐显示信息在状态栏即可
showToast(getNodeDisplayLabel(node), 'info');
} }
} }
@ -1675,12 +1792,13 @@ var PlcConfigPage = (function() {
return n.type !== NT_INPUT && n.type !== NT_OUTPUT; return n.type !== NT_INPUT && n.type !== NT_OUTPUT;
}).length * 120; }).length * 120;
// 62p/62d 需要延时参数 // 每种门类型独立序号,从已有最大序号+1 开始
var gateId = editNextGateIdx; var nextIdx = editNextGateIdx[gateType] || 0;
var gateId = nextIdx;
if (gateType === NT_62P || gateType === NT_62D) { if (gateType === NT_62P || gateType === NT_62D) {
var delayMs = prompt('请输入延时毫秒数 (默认 1000ms):', '1000'); var delayMs = prompt('请输入延时毫秒数 (默认 1000ms):', '1000');
if (!delayMs) { closeModal(); return; } if (!delayMs) { closeModal(); return; }
gateId = (parseInt(delayMs) << 8) | editNextGateIdx; gateId = (parseInt(delayMs) << 8) | nextIdx;
} }
editNodes.push({ editNodes.push({
@ -1688,10 +1806,10 @@ var PlcConfigPage = (function() {
_lx: 0, _ly: gateY, _lx: 0, _ly: gateY,
saddr: '', desc: '', inf: '' saddr: '', desc: '', inf: ''
}); });
editNextGateIdx++; editNextGateIdx[gateType] = nextIdx + 1;
closeModal(); closeModal();
drawEditor(); drawEditor();
showToast('已添加: ' + info.name, 'ok'); showToast('已添加: ' + info.name + ' #' + nextIdx, 'ok');
}; };
} }
} }
@ -1775,77 +1893,208 @@ var PlcConfigPage = (function() {
} }
/* ---- 生成并保存配置 ---- */ /* ---- 生成并保存配置 ---- */
// 生成 XML 配置文件
function generateXmlConfig() {
var typeNames = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING'};
var adjIn = {}, adjOut = {};
for (var i = 0; i < editNodes.length; i++) { adjIn[i] = []; adjOut[i] = []; }
for (var j = 0; j < editLinks.length; j++) {
var l = editLinks[j];
adjOut[l.srcIdx].push(l.dstIdx);
adjIn[l.dstIdx].push(l.srcIdx);
}
var visited = {};
var chains = [];
for (var s = 0; s < editNodes.length; s++) {
if (visited[s]) continue;
var c = [], q = [s]; visited[s] = true;
while (q.length > 0) {
var cur = q.shift(); c.push(cur);
var nbrs = adjOut[cur].concat(adjIn[cur]);
for (var nb = 0; nb < nbrs.length; nb++)
if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; q.push(nbrs[nb]); }
}
chains.push(c);
}
function sigXml(n) { return '<Signal no="' + n.id + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '" />'; }
function gateTag(n) {
var tn = typeNames[n.type] || '?';
var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"';
if (n.inPins && n.inPins > 2) a += ' inPins="' + n.inPins + '"';
if (n.type === NT_62P || n.type === NT_62D) a += ' delayMs="' + (n.id >> 8) + '"';
return '<Gate' + a + ' />';
}
function combXml(idx, indent) {
var s = '';
var di = [];
for (var fi = 0; fi < adjIn[idx].length; fi++)
if (editNodes[adjIn[idx][fi]].type === NT_INPUT) di.push(adjIn[idx][fi]);
if (di.length > 0) {
s += indent + '<Inputs>\n';
for (var d = 0; d < di.length; d++) s += indent + ' ' + sigXml(editNodes[di[d]]) + '\n';
s += indent + '</Inputs>\n';
}
for (var ui = 0; ui < adjIn[idx].length; ui++)
if (editNodes[adjIn[idx][ui]].type > 1) {
s += indent + '<Comb>\n';
s += combXml(adjIn[idx][ui], indent + ' ');
s += indent + '</Comb>\n';
}
s += indent + gateTag(editNodes[idx]) + '\n';
var dos = [];
for (var oi = 0; oi < adjOut[idx].length; oi++)
if (editNodes[adjOut[idx][oi]].type === NT_OUTPUT) dos.push(adjOut[idx][oi]);
if (dos.length > 0) {
s += indent + '<Outputs>\n';
for (var o = 0; o < dos.length; o++) s += indent + ' ' + sigXml(editNodes[dos[o]]) + '\n';
s += indent + '</Outputs>\n';
}
return s;
}
var xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<PLCConfig name="' + escXml(editName || 'PLC Logic') + '" key="' + escXml(editKey)
+ '" checksum="' + (editOnlineCfgId || '00000000') + '">\n';
for (var ci = 0; ci < chains.length; ci++) {
var chain = chains[ci];
var cIns = [], cGates = [], cOuts = [];
for (var ni = 0; ni < chain.length; ni++) {
var nd = editNodes[chain[ni]];
if (nd.type === NT_INPUT) cIns.push(chain[ni]);
else if (nd.type === NT_OUTPUT) cOuts.push(chain[ni]);
else cGates.push(chain[ni]);
}
if (cGates.length === 0) continue;
xml += ' <Chain gates="' + cGates.length + '" inputs="' + cIns.length + '" outputs="' + cOuts.length + '">\n';
if (cGates.length === 1) {
// 简单链: Inputs → Gate → Outputs
xml += ' <Inputs>\n';
for (var ii = 0; ii < cIns.length; ii++) xml += ' ' + sigXml(editNodes[cIns[ii]]) + '\n';
xml += ' </Inputs>\n';
xml += ' ' + gateTag(editNodes[cGates[0]]) + '\n';
xml += ' <Outputs>\n';
for (var oo = 0; oo < cOuts.length; oo++) xml += ' ' + sigXml(editNodes[cOuts[oo]]) + '\n';
xml += ' </Outputs>\n';
} else {
// 组合链: 链级门 + Comb + Outputs
// 链级门 = 下游没有 cGates 内其他门的门(只连输出或链外)
var chainGates = [];
for (var gg = 0; gg < cGates.length; gg++) {
var allToOut = true;
for (var so = 0; so < adjOut[cGates[gg]].length; so++)
if (cGates.indexOf(adjOut[cGates[gg]][so]) >= 0) { allToOut = false; break; }
if (allToOut) chainGates.push(cGates[gg]);
}
// combRoots: 直接馈入 chainGate 的门combXml 会递归处理上游嵌套)
var combRootSet = {};
for (var cg = 0; cg < chainGates.length; cg++) {
for (var ui = 0; ui < adjIn[chainGates[cg]].length; ui++) {
var up = adjIn[chainGates[cg]][ui];
if (cGates.indexOf(up) >= 0) combRootSet[up] = true;
}
}
// 去重:若 A 是 B 的下游,只保留 BcombXml 从 B 开始会递归包含 A
var combRootArr = Object.keys(combRootSet).map(Number);
var combRoots = [];
for (var rr = 0; rr < combRootArr.length; rr++) {
var isDownstream = false;
for (var rr2 = 0; rr2 < combRootArr.length; rr2++) {
if (rr === rr2) continue;
var visited2 = {};
var queue2 = [combRootArr[rr2]];
visited2[combRootArr[rr2]] = true;
while (queue2.length > 0) {
var cur2 = queue2.shift();
if (cur2 === combRootArr[rr]) { isDownstream = true; break; }
for (var si = 0; si < adjOut[cur2].length; si++) {
var nx = adjOut[cur2][si];
if (!visited2[nx] && cGates.indexOf(nx) >= 0) { visited2[nx] = true; queue2.push(nx); }
}
}
if (isDownstream) break;
}
if (!isDownstream) combRoots.push(combRootArr[rr]);
}
// 输出 Comb每个 combRoot 对应一个顶层 Comb内部递归嵌套
for (var tt = 0; tt < combRoots.length; tt++) {
xml += ' <Comb>\n';
xml += combXml(combRoots[tt], ' ');
xml += ' </Comb>\n';
}
// 输出链级门
for (var cg = 0; cg < chainGates.length; cg++) {
xml += ' ' + gateTag(editNodes[chainGates[cg]]) + '\n';
}
// 链级 Outputs — 仅包含直接连到 chainGate 的输出信号
// Comb 内部的输出信号已在 combXml 中处理)
var chainOutSet = {};
for (var cg2 = 0; cg2 < chainGates.length; cg2++) {
for (var oi = 0; oi < adjOut[chainGates[cg2]].length; oi++) {
var outIdx = adjOut[chainGates[cg2]][oi];
if (editNodes[outIdx].type === NT_OUTPUT) chainOutSet[outIdx] = true;
}
}
var chainOuts = Object.keys(chainOutSet).map(Number);
// 同时保留直接连到输入信号(无门)的输出
for (var oo2 = 0; oo2 < cOuts.length; oo2++) {
var hasGateSrc = false;
for (var ai = 0; ai < adjIn[cOuts[oo2]].length; ai++) {
if (cGates.indexOf(adjIn[cOuts[oo2]][ai]) >= 0) hasGateSrc = true;
}
if (!hasGateSrc) chainOutSet[cOuts[oo2]] = true;
}
chainOuts = Object.keys(chainOutSet).map(Number);
if (chainOuts.length > 0) {
xml += ' <Outputs>\n';
for (var oo = 0; oo < chainOuts.length; oo++) xml += ' ' + sigXml(editNodes[chainOuts[oo]]) + '\n';
xml += ' </Outputs>\n';
}
}
xml += ' </Chain>\n';
}
xml += '</PLCConfig>\n';
return xml;
}
function escXml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function generateAndSaveConfig() { function generateAndSaveConfig() {
if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; } if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; }
// 验证
var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; }); var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; });
var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; }); var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; });
if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; } if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; }
if (editLinks.length === 0) { showToast('请先连线', 'err'); return; } if (editLinks.length === 0) { showToast('请先连线', 'err'); return; }
// 保存前去重:确保门电路 type+id 唯一
var seenIds = {}; var seenIds = {};
for (var di = 0; di < editNodes.length; di++) { for (var di = 0; di < editNodes.length; di++) {
var dn = editNodes[di]; var dn = editNodes[di];
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue; if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
var key2 = dn.type + ':' + (dn.id & 0xFF); var key2 = dn.type + ':' + (dn.id & 0xFF);
if (seenIds[key2]) { if (seenIds[key2]) {
editNodes[di].id = (dn.id & ~0xFF) | editNextGateIdx; var next = editNextGateIdx[dn.type] || 0;
editNextGateIdx++; editNodes[di].id = (dn.id & ~0xFF) | next;
showToast('自动修正重复 ID: ' + NODE_INFO[dn.type].name + ' → #' + (editNextGateIdx - 1), 'info'); editNextGateIdx[dn.type] = next + 1;
} else { } else { seenIds[key2] = true; }
seenIds[key2] = true;
}
} }
// 生成 Reclose_logic.txt 内容 var fileContent = generateXmlConfig();
var nodeParts = []; var preview = fileContent.length > 2000 ? fileContent.substring(0,2000)+'…' : fileContent;
for (var i = 0; i < editNodes.length; i++) {
var n = editNodes[i];
// 文件中 ID 保持十六进制,格式: {type:id:x:y[:inPins]}
var nodeStr = '{' + n.type + ':' + n.id.toString(16) + ':' + Math.round(n._lx) + ':' + Math.round(n._ly);
if (n.type > NT_INPUT && n.inPins !== undefined && n.inPins > (NODE_INFO[n.type]||{}).inPins) nodeStr += ':' + n.inPins;
nodeStr += '}';
nodeParts.push(nodeStr);
}
var linkParts = [];
for (var j = 0; j < editLinks.length; j++) {
var l = editLinks[j];
var sn = editNodes[l.srcIdx], dn = editNodes[l.dstIdx];
var linkStr = '{' + sn.type + ':' + sn.id.toString(16) + '-' + dn.type + ':' + dn.id.toString(16);
if (l.dstPin !== undefined && l.dstPin > 0) linkStr += ':' + l.dstPin;
linkStr += '}';
linkParts.push(linkStr);
}
var params = [];
params.push('key=' + editKey);
params.push('name=' + encodeURIComponent(editName || 'PLC Logic'));
params.push('desc=' + encodeURIComponent('desc'));
params.push('desc_pos_x=0');
params.push('desc_pos_y=0');
params.push('node=[' + nodeParts.join(',') + ']');
params.push('link=[' + linkParts.join(',') + ']');
var graphData = params.join('&');
// 构建完整文件
var fileObj = {};
fileObj[editKey] = graphData;
fileObj.online_cfgId = editOnlineCfgId || '00000000';
var fileContent = JSON.stringify(fileObj);
// 显示预览
var previewHtml = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:700px">' var previewHtml = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:700px">'
+ '<h3>✅ 配置预览</h3>' + '<h3>✅ 配置预览 (XML)</h3>'
+ '<p>Key: <strong>' + escHtml(editKey) + '</strong> | 名称: <strong>' + escHtml(editName || 'PLC Logic') + '</strong></p>' + '<p>Key: <strong>' + escHtml(editKey) + '</strong> | 名称: <strong>' + escHtml(editName || 'PLC Logic') + '</strong></p>'
+ '<p>节点: ' + editNodes.length + ' | 连线: ' + editLinks.length + '</p>' + '<p>节点: ' + editNodes.length + ' | 连线: ' + editLinks.length + '</p>'
+ '<div style="max-height:200px;overflow:auto;background:#1a1a2e;color:#0f0;padding:12px;border-radius:6px;font-family:monospace;font-size:12px;word-break:break-all;white-space:pre-wrap">' + escHtml(fileContent) + '</div>' + '<div style="max-height:250px;overflow:auto;background:#1a1a2e;color:#0f0;padding:12px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap">' + escHtml(preview) + '</div>'
+ '<div style="margin-top:12px;display:flex;gap:8px">' + '<div style="margin-top:12px;display:flex;gap:8px">'
+ '<button class="btn btn-primary" id="plc-modal-save">💾 保存并替换 Reclose_logic.txt</button>' + '<button class="btn btn-primary" id="plc-modal-save">💾 保存 Reclose_logic.xml</button>'
+ '<button class="btn btn-outline" id="plc-modal-cancel">取消</button>' + '<button class="btn btn-outline" id="plc-modal-cancel">取消</button>'
+ '</div></div></div>'; + '</div></div></div>';
@ -1856,20 +2105,19 @@ var PlcConfigPage = (function() {
document.getElementById('plc-modal-cancel').onclick = closeModal; document.getElementById('plc-modal-cancel').onclick = closeModal;
document.getElementById('plc-modal-save').onclick = function() { document.getElementById('plc-modal-save').onclick = function() {
closeModal(); closeModal();
var oldContent = loadedFiles['config/PLC/Reclose_logic.txt'] || ''; var oldContent = loadedFiles['config/PLC/Reclose_logic.xml'] || '';
if (oldContent && oldContent.length > 10) { if (oldContent && oldContent.length > 10) {
WsClient.send({ type: 'set_file', path: 'config/PLC/Reclose_logic.txt.bak', content: oldContent }); WsClient.send({ type: 'set_file', path: 'config/PLC/Reclose_logic.xml.bak', content: oldContent });
} }
var sent = WsClient.send({ var sent = WsClient.send({
type: 'set_file', type: 'set_file',
path: 'config/PLC/Reclose_logic.txt', path: 'config/PLC/Reclose_logic.xml',
content: fileContent content: fileContent
}); });
if (sent) { if (sent) {
showToast('配置已保存!请重启 PLC 模块生效', 'ok'); showToast('XML 配置已保存!请重启 PLC 模块生效', 'ok');
// 清除缓存以便下次查看器重新加载 loadedFiles['config/PLC/Reclose_logic.xml'] = null;
loadedFiles['config/PLC/Reclose_logic.txt'] = null; loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
loadedFiles['config/PLC/Reclose_logic.txt.bak'] = null;
logicGraphs = []; logicGraphs = [];
} else { } else {
showToast('保存失败WebSocket 未连接', 'err'); showToast('保存失败WebSocket 未连接', 'err');
@ -1909,16 +2157,19 @@ var PlcConfigPage = (function() {
var content = data.content || ''; var content = data.content || '';
var error = data.error || ''; var error = data.error || '';
if (error) { if (error) {
showToast('读取文件失败: ' + path + ' — ' + error, 'err'); // 文件不存在等情况仍然触发回调传null确保 loadLogicFileAll 的 tryCb 能正常计数
loadedFiles[path] = null;
if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
PlcConfigPage._pending[path](null);
delete PlcConfigPage._pending[path];
}
} else { } else {
loadedFiles[path] = content; loadedFiles[path] = content;
// 触发回调
if (PlcConfigPage._pending && PlcConfigPage._pending[path]) { if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
PlcConfigPage._pending[path](content); PlcConfigPage._pending[path](content);
delete PlcConfigPage._pending[path]; delete PlcConfigPage._pending[path];
} }
} }
// 重试队列中移除
if (PlcConfigPage._retryList) { if (PlcConfigPage._retryList) {
var rIdx = PlcConfigPage._retryList.indexOf(path); var rIdx = PlcConfigPage._retryList.indexOf(path);
if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1); if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1);