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:
parent
e6f4de251f
commit
6bdd467998
|
|
@ -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)
|
||||
|
||||
### 需求背景
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <time.h> // clock_gettime 非阻塞定时器
|
||||
#include <sys/stat.h> // stat() 文件变化检测
|
||||
#include "tinyxml2.h" // XML 配置文件解析
|
||||
|
||||
|
||||
#define MAX_OUT_NODES 20 // 最大输出节点数
|
||||
|
|
@ -30,8 +30,8 @@ std::vector<stru_plc_cfg> g_plc_cfg={};
|
|||
|
||||
// ==================== 常量定义 ====================
|
||||
#define MAX_LOGIC_GRAPHS 4 // 最大逻辑图数量
|
||||
#define MAX_NODES_PER_GRAPH 20 // 每个逻辑图最大节点数
|
||||
#define MAX_LINKS_PER_GRAPH 20 // 每个逻辑图最大链路数
|
||||
#define MAX_NODES_PER_GRAPH 64 // 每个逻辑图最大节点数
|
||||
#define MAX_LINKS_PER_GRAPH 64 // 每个逻辑图最大链路数
|
||||
|
||||
// 节点类型标识
|
||||
#define NODE_TYPE_HW_OUTPUT 0 // 硬件输出点(dev_in)
|
||||
|
|
@ -64,8 +64,6 @@ typedef struct {
|
|||
typedef struct {
|
||||
int type;
|
||||
int id;
|
||||
int x;
|
||||
int y;
|
||||
int value;
|
||||
int last_input;
|
||||
// 非阻塞延时定时器字段(62p/62d门使用)
|
||||
|
|
@ -97,7 +95,7 @@ typedef struct {
|
|||
LOCAL LogicGraph g_cached_graphs[MAX_LOGIC_GRAPHS]; // 缓存的逻辑图
|
||||
LOCAL int g_cached_graph_count = 0; // 缓存的逻辑图数量
|
||||
LOCAL time_t g_last_file_mtime = 0; // 配置文件最后修改时间
|
||||
LOCAL char g_plc_config_path[256] = "/mnt/RTU/config/PLC/Reclose_logic.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) {
|
||||
|
|
@ -241,171 +239,258 @@ static LogicNode* find_node(LogicGraph *graph, int type, int id) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// ==================== 逻辑图解析 ====================
|
||||
int parse_reclose_logic(const char *file_path, LogicGraph graphs[], int *graph_count) {
|
||||
FILE *fp = fopen(file_path, "rb");
|
||||
if (!fp) {
|
||||
MY_LOG_E("无法打开PLC配置文件: %s", file_path);
|
||||
// ==================== XML 配置文件解析 (Comb结构) ====================
|
||||
|
||||
static int gate_type_from_xml_str(const char *type_str)
|
||||
{
|
||||
if(NULL == type_str) return -1;
|
||||
if(strcmp(type_str, "INPUT") == 0) return NODE_TYPE_HW_INPUT;
|
||||
if(strcmp(type_str, "OUTPUT") == 0) return NODE_TYPE_HW_OUTPUT;
|
||||
if(strcmp(type_str, "OR") == 0) return NODE_TYPE_OR_GATE;
|
||||
if(strcmp(type_str, "AND") == 0) return NODE_TYPE_AND_GATE;
|
||||
if(strcmp(type_str, "NOT") == 0) return NODE_TYPE_NOT_GATE;
|
||||
if(strcmp(type_str, "T62P") == 0) return NODE_TYPE_62P_GATE;
|
||||
if(strcmp(type_str, "T62D") == 0) return NODE_TYPE_62D_GATE;
|
||||
if(strcmp(type_str, "RISING") == 0) return NODE_TYPE_RISING_EDGE;
|
||||
if(strcmp(type_str, "SR") == 0) return NODE_TYPE_SR_LATCH;
|
||||
if(strcmp(type_str, "RS") == 0) return NODE_TYPE_RS_LATCH;
|
||||
if(strcmp(type_str, "FALLING") == 0) return NODE_TYPE_FALLING_EDGE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int find_node_index(LogicGraph *graph, int type, int id)
|
||||
{
|
||||
for(int i = 0; i < graph->node_count; i++)
|
||||
{
|
||||
if(graph->nodes[i].type == type)
|
||||
{
|
||||
if(type <= 1 && graph->nodes[i].id == id) return i;
|
||||
if(type > 1 && (graph->nodes[i].id & 0xFF) == (id & 0xFF)) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Comb 内部信号:不存在则先创建
|
||||
static int ensure_signal(LogicGraph *graph, int io_type, int sig_no)
|
||||
{
|
||||
int idx = find_node_index(graph, io_type, sig_no);
|
||||
if(idx >= 0) return idx;
|
||||
if(graph->node_count >= MAX_NODES_PER_GRAPH) return -1;
|
||||
LogicNode &n = graph->nodes[graph->node_count++];
|
||||
memset(&n, 0, sizeof(n)); n.timer_pending_output = -1;
|
||||
n.type = io_type; n.id = sig_no;
|
||||
return graph->node_count - 1;
|
||||
}
|
||||
|
||||
// 递归解析 <Comb> 块: Inputs → [nested Comb] → Gate → Outputs
|
||||
// 返回值: 本 Comb 中 Gate 节点的索引,-1 表示失败
|
||||
static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph)
|
||||
{
|
||||
if(NULL == comb_el || graph->node_count >= MAX_NODES_PER_GRAPH) return -1;
|
||||
tinyxml2::XMLElement *gate_el = comb_el->FirstChildElement("Gate");
|
||||
if(NULL == gate_el) { MY_LOG_E("Comb缺少Gate"); return -1; }
|
||||
|
||||
LogicNode &gn = graph->nodes[graph->node_count++];
|
||||
memset(&gn, 0, sizeof(gn)); gn.timer_pending_output = -1;
|
||||
gn.type = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
gn.id = gate_el->IntAttribute("id");
|
||||
int gi = graph->node_count - 1;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
tinyxml2::XMLElement *root = doc.FirstChildElement("PLCConfig");
|
||||
if(NULL == root) { MY_LOG_E("XML根元素<PLCConfig>未找到"); return -1; }
|
||||
|
||||
*graph_count = 0;
|
||||
char buffer[4096];
|
||||
size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, fp);
|
||||
if (bytes_read <= 0) {
|
||||
MY_LOG_E("PLC配置文件为空或读取失败: %s", file_path);
|
||||
fclose(fp);
|
||||
return -1;
|
||||
LogicGraph *cur = &graphs[0];
|
||||
memset(cur, 0, sizeof(LogicGraph));
|
||||
|
||||
const char *ka = root->Attribute("key");
|
||||
const char *na = root->Attribute("name");
|
||||
if(ka) strncpy(cur->key, ka, sizeof(cur->key) - 1);
|
||||
if(na) strncpy(cur->name, na, sizeof(cur->name) - 1);
|
||||
|
||||
// 第一遍:收集所有节点
|
||||
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;
|
||||
char *clean_buffer = buffer;
|
||||
while (*clean_buffer) {
|
||||
if (!isspace((unsigned char)*clean_buffer)) {
|
||||
*write_ptr++ = *clean_buffer;
|
||||
// 第二遍:Comb 输出 → 链级 Gate,Gate → Outputs
|
||||
for(tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); chain;
|
||||
chain = chain->NextSiblingElement("Chain"))
|
||||
{
|
||||
bool has_comb = (chain->FirstChildElement("Comb") != NULL);
|
||||
|
||||
// 收集链级 Gate(不在 Comb 内)
|
||||
tinyxml2::XMLElement *gate_el = NULL;
|
||||
tinyxml2::XMLElement *ins_el = NULL;
|
||||
tinyxml2::XMLElement *ous_el = NULL;
|
||||
tinyxml2::XMLElement *c = chain->FirstChildElement();
|
||||
while(c)
|
||||
{
|
||||
if(strcmp(c->Value(), "Gate") == 0) gate_el = c;
|
||||
if(strcmp(c->Value(), "Inputs") == 0) ins_el = c;
|
||||
if(strcmp(c->Value(), "Outputs") == 0) ous_el = c;
|
||||
c = c->NextSiblingElement();
|
||||
}
|
||||
clean_buffer++;
|
||||
}
|
||||
*write_ptr = '\0';
|
||||
|
||||
char *ptr = buffer;
|
||||
int graph_index = 0;
|
||||
while (ptr && *graph_count < MAX_LOGIC_GRAPHS) {
|
||||
while (*ptr && *ptr != '"') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
|
||||
char key[20];
|
||||
int key_len = 0;
|
||||
while (*ptr && *ptr != '"' && key_len < (int)sizeof(key) - 1) {
|
||||
key[key_len++] = *ptr++;
|
||||
}
|
||||
if (!*ptr) break;
|
||||
key[key_len] = '\0';
|
||||
ptr++;
|
||||
|
||||
while (*ptr && *ptr != ':') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
while (*ptr && *ptr != '"') ptr++;
|
||||
if (!*ptr) break;
|
||||
ptr++;
|
||||
|
||||
char graph_data[1024];
|
||||
int data_len = 0;
|
||||
while (*ptr && *ptr != '"' && data_len < (int)sizeof(graph_data) - 1) {
|
||||
graph_data[data_len++] = *ptr++;
|
||||
}
|
||||
if (!*ptr) break;
|
||||
graph_data[data_len] = '\0';
|
||||
ptr++;
|
||||
|
||||
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, "{},");
|
||||
if(has_comb)
|
||||
{
|
||||
// 收集所有 Comb 的 Gate 索引(第一遍已解析)
|
||||
// Comb Gate → 链级 Gate (一个Comb一根线)
|
||||
if(gate_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi >= 0)
|
||||
{
|
||||
// 遍历所有顶层 Comb Gate
|
||||
for(c = chain->FirstChildElement("Comb"); c; c = c->NextSiblingElement("Comb"))
|
||||
{
|
||||
tinyxml2::XMLElement *cg = c->FirstChildElement("Gate");
|
||||
if(!cg) continue;
|
||||
int ct = gate_type_from_xml_str(cg->Attribute("type"));
|
||||
int ci = find_node_index(cur, ct, cg->IntAttribute("id"));
|
||||
if(ci >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH)
|
||||
{
|
||||
LogicLink &l = cur->links[cur->link_count++];
|
||||
l.src_type = cur->nodes[ci].type;
|
||||
l.src_id = cur->nodes[ci].id;
|
||||
l.dest_type = cur->nodes[cgi].type;
|
||||
l.dest_id = cur->nodes[cgi].id;
|
||||
}
|
||||
} 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 {
|
||||
MY_LOG_E("逻辑图[%d] (key=%s) 缺少 node= 或 link= 参数,已跳过",
|
||||
graph_index, key);
|
||||
|
||||
// 链级 Gate → 链级 Outputs
|
||||
if(gate_el && ous_el)
|
||||
{
|
||||
int cgt = gate_type_from_xml_str(gate_el->Attribute("type"));
|
||||
int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id"));
|
||||
if(cgi >= 0)
|
||||
{
|
||||
for(tinyxml2::XMLElement *sig = ous_el->FirstChildElement("Signal"); sig;
|
||||
sig = sig->NextSiblingElement("Signal"))
|
||||
{
|
||||
if(cur->link_count >= MAX_LINKS_PER_GRAPH) break;
|
||||
int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no"));
|
||||
if(si >= 0)
|
||||
{ LogicLink &l = cur->links[cur->link_count++];
|
||||
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) {
|
||||
MY_LOG_E("PLC配置文件解析失败:未找到任何有效的逻辑图");
|
||||
return -1;
|
||||
}
|
||||
*graph_count = 1;
|
||||
MY_LOG_I("XML配置解析成功: %d节点 %d连线", cur->node_count, cur->link_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -823,9 +908,10 @@ int PLC_Logic(void) {
|
|||
g_last_file_mtime = file_stat.st_mtime;
|
||||
}
|
||||
|
||||
// 文件已变化或首次加载,重新解析
|
||||
// 文件已变化或首次加载,重新解析 XML
|
||||
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) {
|
||||
MY_LOG_E("PLC配置文件重新解析失败,继续使用上次缓存(%d个逻辑图)",
|
||||
|
|
@ -902,7 +988,7 @@ int app_plc_init1(void *arg)
|
|||
// 动态设置PLC配置文件路径
|
||||
char proc_dir[512] = {0};
|
||||
if (0 == func_get_process_self_dir(proc_dir, sizeof(proc_dir))) {
|
||||
std::string plc_path = std::string(proc_dir) + "config/PLC/Reclose_logic.txt";
|
||||
std::string plc_path = std::string(proc_dir) + "config/PLC/Reclose_logic.xml";
|
||||
plc_set_config_path(plc_path.c_str());
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -44,7 +44,7 @@ var PlcConfigPage = (function() {
|
|||
var editDragCanvas = null; // 画布拖拽状态
|
||||
var editPanX = 0, editPanY = 0; // 画布偏移
|
||||
var editScale = 1.0; // 缩放
|
||||
var editNextGateIdx = 0; // 逻辑门计数器
|
||||
var editNextGateIdx = {}; // {type: nextIdx} 每种门类型独立序号从0开始
|
||||
var editLoadedFromFile = false; // 是否从文件加载
|
||||
var editLastClickTime = 0; // 上次 click 时间戳(双击检测)
|
||||
var editLastClickNode = -1; // 上次 click 的节点索引
|
||||
|
|
@ -152,7 +152,7 @@ var PlcConfigPage = (function() {
|
|||
function renderTabs() {
|
||||
var tabs = [
|
||||
{ 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 逻辑配置' }
|
||||
];
|
||||
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-reset').onclick = function() { viewAutoFit = true; drawViewer(); };
|
||||
document.getElementById('plc-view-reload').onclick = function() {
|
||||
loadedFiles['config/PLC/Reclose_logic.txt'] = null;
|
||||
loadedFiles['config/PLC/Reclose_logic.txt.bak'] = null;
|
||||
loadedFiles['config/PLC/Reclose_logic.xml'] = null;
|
||||
loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
|
||||
logicGraphs = [];
|
||||
loadLogicFileAll(function() { refreshGraphSelect(); });
|
||||
};
|
||||
|
|
@ -341,109 +341,204 @@ var PlcConfigPage = (function() {
|
|||
cb();
|
||||
}
|
||||
}
|
||||
requestFile('config/PLC/Reclose_logic.txt', function(content) {
|
||||
if (!content) { failedFiles.push('Reclose_logic.txt'); tryCb(); return; }
|
||||
parseLogicFile(content, 'Reclose_logic.txt');
|
||||
loadedFiles2.push('Reclose_logic.txt');
|
||||
requestFile('config/PLC/Reclose_logic.xml', function(content) {
|
||||
if (!content) { failedFiles.push('Reclose_logic.xml'); tryCb(); return; }
|
||||
parseLogicFile(content, 'Reclose_logic.xml');
|
||||
loadedFiles2.push('Reclose_logic.xml');
|
||||
tryCb();
|
||||
});
|
||||
requestFile('config/PLC/Reclose_logic.txt.bak', function(content) {
|
||||
requestFile('config/PLC/Reclose_logic.xml.bak', function(content) {
|
||||
if (!content) { tryCb(); return; }
|
||||
parseLogicFile(content, 'Reclose_logic.txt.bak');
|
||||
loadedFiles2.push('Reclose_logic.txt.bak');
|
||||
parseLogicFile(content, 'Reclose_logic.xml.bak');
|
||||
loadedFiles2.push('Reclose_logic.xml.bak');
|
||||
tryCb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadLogicFile(cb) {
|
||||
requestFile('config/PLC/Reclose_logic.txt', function(content) {
|
||||
if (!content) { showToast('加载 Reclose_logic.txt 失败', 'err'); return; }
|
||||
parseLogicFile(content, 'Reclose_logic.txt');
|
||||
requestFile('config/PLC/Reclose_logic.xml', function(content) {
|
||||
if (!content) { showToast('加载 Reclose_logic.xml 失败', 'err'); return; }
|
||||
parseLogicFile(content, 'Reclose_logic.xml');
|
||||
if (cb) cb();
|
||||
});
|
||||
}
|
||||
function parseLogicFile(content, sourceName) {
|
||||
// 先移除同源文件的旧解析结果,保留其他源的数据
|
||||
logicGraphs = logicGraphs.filter(function(g) {
|
||||
return g._source !== sourceName;
|
||||
});
|
||||
logicGraphs = logicGraphs.filter(function(g) { return g._source !== sourceName; });
|
||||
onlineCfgId = onlineCfgId || '';
|
||||
try {
|
||||
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');
|
||||
}
|
||||
parseLogicXml(content, sourceName);
|
||||
}
|
||||
|
||||
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)] = decodeURIComponent(pairs[i].substring(idx + 1));
|
||||
// XML → 扁平 graph,与 C++ parse_xml_comb 完全一致
|
||||
function parseLogicXml(xmlStr, sourceName) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(xmlStr, 'text/xml');
|
||||
var root = doc.documentElement;
|
||||
if (!root || root.nodeName !== 'PLCConfig') {
|
||||
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);
|
||||
|
||||
// 解析节点
|
||||
if (params.node) {
|
||||
var m = params.node.match(/\[(.*)\]/);
|
||||
if (m) {
|
||||
var toks = m[1].match(/\{[^}]+\}/g) || [];
|
||||
for (var j = 0; j < toks.length; j++) {
|
||||
var inner = toks[j].replace(/[{}]/g, '');
|
||||
var parts = inner.split(':');
|
||||
if (parts.length >= 4) {
|
||||
var nodeData = {
|
||||
type: parseInt(parts[0]),
|
||||
id: parseInt(parts[1], 16),
|
||||
x: parseInt(parts[2]),
|
||||
y: parseInt(parts[3])
|
||||
};
|
||||
if (parts.length >= 5) nodeData.inPins = parseInt(parts[4]);
|
||||
graph.nodes.push(nodeData);
|
||||
var c = combEl.firstElementChild;
|
||||
while (c) {
|
||||
if (c.nodeName === 'Comb') {
|
||||
var upi = parseComb(c);
|
||||
if (upi >= 0) g.links.push({ src_type:g.nodes[upi].type, src_id:g.nodes[upi].id, dst_type:t, dst_id:gid });
|
||||
}
|
||||
c = c.nextElementSibling;
|
||||
}
|
||||
|
||||
var ie = combEl.firstElementChild;
|
||||
while (ie && ie.nodeName !== 'Inputs') ie = ie.nextElementSibling;
|
||||
if (ie) {
|
||||
var s = ie.firstElementChild;
|
||||
while (s) {
|
||||
if (s.nodeName === 'Signal') {
|
||||
var no = parseInt(s.getAttribute('no'));
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析链路
|
||||
if (params.link) {
|
||||
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;
|
||||
g._source = sourceName;
|
||||
logicGraphs.push(g);
|
||||
}
|
||||
|
||||
function refreshGraphSelect() {
|
||||
|
|
@ -967,7 +1062,7 @@ var PlcConfigPage = (function() {
|
|||
editPanX = 0; editPanY = 0;
|
||||
editScale = 1.0;
|
||||
editLoadedFromFile = false;
|
||||
editNextGateIdx = 0;
|
||||
editNextGateIdx = {};
|
||||
|
||||
container.innerHTML = ''
|
||||
+ '<div class="plc-editor-toolbar">'
|
||||
|
|
@ -1007,7 +1102,7 @@ var PlcConfigPage = (function() {
|
|||
setupEditorCanvas();
|
||||
drawEditor();
|
||||
|
||||
// 加载当前 Reclose_logic.txt 为编辑起点
|
||||
// 加载当前 Reclose_logic.xml 为编辑起点
|
||||
function initEditor() {
|
||||
loadLogicFile(function() {
|
||||
if (logicGraphs.length > 0) {
|
||||
|
|
@ -1023,26 +1118,23 @@ var PlcConfigPage = (function() {
|
|||
editKey = g.key;
|
||||
editName = g.name || '';
|
||||
editOnlineCfgId = onlineCfgId;
|
||||
// 复制节点到编辑器
|
||||
var maxGateIdx = 0;
|
||||
for (var i = 0; i < g.nodes.length; i++) {
|
||||
var n = g.nodes[i];
|
||||
var newNode = {
|
||||
type: n.type, id: n.id, x: n.x, y: n.y,
|
||||
_lx: n.x, _ly: n.y,
|
||||
saddr: '', desc: '', no: n.id
|
||||
};
|
||||
// 查找信号信息
|
||||
var sig = findSignalByNo(n.id);
|
||||
if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; }
|
||||
// 逻辑门 inPins — 优先使用文件中保存的配置
|
||||
if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) {
|
||||
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);
|
||||
// 复制节点到编辑器
|
||||
for (var i = 0; i < g.nodes.length; i++) {
|
||||
var n = g.nodes[i];
|
||||
var newNode = {
|
||||
type: n.type, id: n.id, x: n.x, y: n.y,
|
||||
_lx: n.x, _ly: n.y,
|
||||
saddr: '', desc: '', no: n.id
|
||||
};
|
||||
// 查找信号信息
|
||||
var sig = findSignalByNo(n.id);
|
||||
if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; }
|
||||
// 逻辑门 inPins — 优先使用文件中保存的配置
|
||||
if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) {
|
||||
newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2);
|
||||
}
|
||||
editNodes.push(newNode);
|
||||
}
|
||||
// 复制链路(按节点在新数组中的索引)
|
||||
for (var j = 0; j < g.links.length; j++) {
|
||||
var l = g.links[j];
|
||||
|
|
@ -1051,22 +1143,33 @@ var PlcConfigPage = (function() {
|
|||
if (si >= 0 && di >= 0) editLinks.push({ srcIdx: si, dstIdx: di });
|
||||
}
|
||||
editLoadedFromFile = true;
|
||||
// 更新全局门计数器,避免后续新增门 ID 冲突
|
||||
editNextGateIdx = maxGateIdx + 1;
|
||||
|
||||
// 消除已有的重复 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]) {
|
||||
editNodes[di].id = (dn.id & ~0xFF) | editNextGateIdx;
|
||||
editNextGateIdx++;
|
||||
} else {
|
||||
seenIds[key2] = true;
|
||||
}
|
||||
// 按类型统计最大序号,后续新增时每种类型独立递增
|
||||
var maxByType = {};
|
||||
for (var di = 0; di < editNodes.length; di++) {
|
||||
var dn = editNodes[di];
|
||||
if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue;
|
||||
var idx = dn.id & 0xFF;
|
||||
if (idx >= (maxByType[dn.type] || -1)) maxByType[dn.type] = idx;
|
||||
}
|
||||
for (var t in maxByType) {
|
||||
editNextGateIdx[t] = maxByType[t] + 1;
|
||||
}
|
||||
|
||||
// 消除已有的重复 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');
|
||||
|
|
@ -1123,6 +1226,7 @@ var PlcConfigPage = (function() {
|
|||
document.getElementById('plc-modal-cancel').onclick = closeModal;
|
||||
document.getElementById('plc-modal-ok').onclick = function() {
|
||||
closeModal();
|
||||
var delType = node.type;
|
||||
editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; });
|
||||
for (var i = 0; i < editLinks.length; i++) {
|
||||
if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--;
|
||||
|
|
@ -1130,6 +1234,20 @@ var PlcConfigPage = (function() {
|
|||
}
|
||||
editNodes.splice(idx, 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();
|
||||
showToast('模块已删除', 'ok');
|
||||
};
|
||||
|
|
@ -1165,10 +1283,9 @@ var PlcConfigPage = (function() {
|
|||
drawEditor();
|
||||
if (node.type === NT_INPUT || node.type === NT_OUTPUT) {
|
||||
showRebindSignalModal(idx);
|
||||
} else if (node.type === NT_AND || node.type === NT_OR) {
|
||||
showGateConfigModal(idx);
|
||||
} 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;
|
||||
}).length * 120;
|
||||
|
||||
// 62p/62d 需要延时参数
|
||||
var gateId = editNextGateIdx;
|
||||
// 每种门类型独立序号,从已有最大序号+1 开始
|
||||
var nextIdx = editNextGateIdx[gateType] || 0;
|
||||
var gateId = nextIdx;
|
||||
if (gateType === NT_62P || gateType === NT_62D) {
|
||||
var delayMs = prompt('请输入延时毫秒数 (默认 1000ms):', '1000');
|
||||
if (!delayMs) { closeModal(); return; }
|
||||
gateId = (parseInt(delayMs) << 8) | editNextGateIdx;
|
||||
gateId = (parseInt(delayMs) << 8) | nextIdx;
|
||||
}
|
||||
|
||||
editNodes.push({
|
||||
|
|
@ -1688,10 +1806,10 @@ var PlcConfigPage = (function() {
|
|||
_lx: 0, _ly: gateY,
|
||||
saddr: '', desc: '', inf: ''
|
||||
});
|
||||
editNextGateIdx++;
|
||||
editNextGateIdx[gateType] = nextIdx + 1;
|
||||
closeModal();
|
||||
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 的下游,只保留 B(combXml 从 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
function generateAndSaveConfig() {
|
||||
if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; }
|
||||
|
||||
// 验证
|
||||
var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; });
|
||||
var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; });
|
||||
if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; }
|
||||
|
||||
if (editLinks.length === 0) { showToast('请先连线', 'err'); return; }
|
||||
|
||||
// 保存前去重:确保门电路 type+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]) {
|
||||
editNodes[di].id = (dn.id & ~0xFF) | editNextGateIdx;
|
||||
editNextGateIdx++;
|
||||
showToast('自动修正重复 ID: ' + NODE_INFO[dn.type].name + ' → #' + (editNextGateIdx - 1), 'info');
|
||||
} else {
|
||||
seenIds[key2] = true;
|
||||
}
|
||||
var next = editNextGateIdx[dn.type] || 0;
|
||||
editNodes[di].id = (dn.id & ~0xFF) | next;
|
||||
editNextGateIdx[dn.type] = next + 1;
|
||||
} else { seenIds[key2] = true; }
|
||||
}
|
||||
|
||||
// 生成 Reclose_logic.txt 内容
|
||||
var nodeParts = [];
|
||||
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 fileContent = generateXmlConfig();
|
||||
var preview = fileContent.length > 2000 ? fileContent.substring(0,2000)+'…' : fileContent;
|
||||
|
||||
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">'
|
||||
+ '<h3>✅ 配置预览</h3>'
|
||||
+ '<h3>✅ 配置预览 (XML)</h3>'
|
||||
+ '<p>Key: <strong>' + escHtml(editKey) + '</strong> | 名称: <strong>' + escHtml(editName || 'PLC Logic') + '</strong></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">'
|
||||
+ '<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>'
|
||||
+ '</div></div></div>';
|
||||
|
||||
|
|
@ -1856,20 +2105,19 @@ var PlcConfigPage = (function() {
|
|||
document.getElementById('plc-modal-cancel').onclick = closeModal;
|
||||
document.getElementById('plc-modal-save').onclick = function() {
|
||||
closeModal();
|
||||
var oldContent = loadedFiles['config/PLC/Reclose_logic.txt'] || '';
|
||||
var oldContent = loadedFiles['config/PLC/Reclose_logic.xml'] || '';
|
||||
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({
|
||||
type: 'set_file',
|
||||
path: 'config/PLC/Reclose_logic.txt',
|
||||
path: 'config/PLC/Reclose_logic.xml',
|
||||
content: fileContent
|
||||
});
|
||||
if (sent) {
|
||||
showToast('配置已保存!请重启 PLC 模块生效', 'ok');
|
||||
// 清除缓存以便下次查看器重新加载
|
||||
loadedFiles['config/PLC/Reclose_logic.txt'] = null;
|
||||
loadedFiles['config/PLC/Reclose_logic.txt.bak'] = null;
|
||||
showToast('XML 配置已保存!请重启 PLC 模块生效', 'ok');
|
||||
loadedFiles['config/PLC/Reclose_logic.xml'] = null;
|
||||
loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null;
|
||||
logicGraphs = [];
|
||||
} else {
|
||||
showToast('保存失败:WebSocket 未连接', 'err');
|
||||
|
|
@ -1909,16 +2157,19 @@ var PlcConfigPage = (function() {
|
|||
var content = data.content || '';
|
||||
var error = data.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 {
|
||||
loadedFiles[path] = content;
|
||||
// 触发回调
|
||||
if (PlcConfigPage._pending && PlcConfigPage._pending[path]) {
|
||||
PlcConfigPage._pending[path](content);
|
||||
delete PlcConfigPage._pending[path];
|
||||
}
|
||||
}
|
||||
// 重试队列中移除
|
||||
if (PlcConfigPage._retryList) {
|
||||
var rIdx = PlcConfigPage._retryList.indexOf(path);
|
||||
if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1);
|
||||
|
|
|
|||
Loading…
Reference in New Issue