node-red-contrib-symi-modbus 2.9.9 → 2.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -952,7 +952,23 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
952
952
 
953
953
  ## 版本信息
954
954
 
955
- **当前版本**: v2.9.9 (2026-01-08)
955
+ **当前版本**: v2.9.10 (2026-01-08)
956
+
957
+ **v2.9.10 更新内容**:
958
+ - **日志系统优化(解决日志占用问题)**:
959
+ - **彻底静默重连日志**:将 TCP/串口连接过程中的 `node.error` 降级为 `node.log` 或 `node.debug`,不再发送到 Node-RED 调试面板(Debug Tab),彻底解决重连期间日志刷屏问题。
960
+ - **智能过滤**:相同的连接错误在重试期间不再重复输出日志(每 10 分钟仅后台提醒一次)。
961
+ - **状态栏增强**:将具体错误信息(如“拒绝连接”、“串口不存在”)直接显示在节点状态文字中,无需查看日志即可掌握连接状况。
962
+ - **多主站配置隔离修复**:
963
+ - 修复了多个Modbus主站节点在编辑时配置互相干扰的问题(通过Scoped Event Handler实现)
964
+ - 确保每个主站节点的从站列表配置完全独立,互不影响
965
+ - **从站开关主站关联**:
966
+ - **新增关联主站功能**:从站开关节点新增"关联主站"配置项
967
+ - **多主站支持**:支持选择特定的主站节点,解决多主站环境下相同从站地址冲突的问题
968
+ - **智能过滤**:从站开关只响应关联主站的状态更新,避免误触发
969
+ - **事件机制优化**:
970
+ - 优化内部事件总线,携带主站ID信息,实现精确的消息路由
971
+ - 增强`modbus-master`和`modbus-slave-switch`之间的免连线通信稳定性
956
972
 
957
973
  **v2.9.9 更新内容**:
958
974
  - **深度优化多 TCP 主站并发**:
@@ -21,7 +21,7 @@ module.exports = function(RED) {
21
21
  // 获取串口配置节点
22
22
  var serialNode = RED.nodes.getNode(node.config.serialConfig);
23
23
  if (!serialNode) {
24
- node.error('未找到串口配置节点');
24
+ node.log('未找到串口配置节点');
25
25
  node.status({fill: "red", shape: "ring", text: "未配置串口"});
26
26
  return;
27
27
  }
@@ -38,21 +38,21 @@ module.exports = function(RED) {
38
38
 
39
39
  // 确保是偶数长度
40
40
  if (hex.length % 2 !== 0) {
41
- node.warn('16进制字符串长度必须为偶数: ' + hexString);
41
+ node.log('16进制字符串长度必须为偶数: ' + hexString);
42
42
  return null;
43
43
  }
44
44
 
45
45
  // 限制48字节
46
46
  if (hex.length > 96) {
47
47
  hex = hex.substring(0, 96);
48
- node.warn('指令长度超过48字节,已截断');
48
+ node.log('指令长度超过48字节,已截断');
49
49
  }
50
50
 
51
51
  // 转换为Buffer
52
52
  var buffer = Buffer.from(hex, 'hex');
53
53
  return buffer;
54
54
  } catch (err) {
55
- node.error('16进制字符串转换失败: ' + err.message);
55
+ node.log('16进制字符串转换失败: ' + err.message);
56
56
  return null;
57
57
  }
58
58
  }
@@ -60,13 +60,13 @@ module.exports = function(RED) {
60
60
  // 发送指令到串口
61
61
  function sendCommand(buffer, cmdName) {
62
62
  if (!buffer) {
63
- node.warn('指令为空,跳过发送');
63
+ node.log('指令为空,跳过发送');
64
64
  return;
65
65
  }
66
66
 
67
67
  // 直接通过串口配置节点发送数据
68
68
  if (!serialNode || !serialNode.connection) {
69
- node.error('串口连接未建立');
69
+ node.log('串口连接未建立');
70
70
  node.status({fill: "red", shape: "ring", text: "连接未建立"});
71
71
  return;
72
72
  }
@@ -80,7 +80,7 @@ module.exports = function(RED) {
80
80
  }
81
81
 
82
82
  if (!isConnected) {
83
- node.error('串口/TCP连接未打开');
83
+ node.log('串口/TCP连接未打开');
84
84
  node.status({fill: "red", shape: "ring", text: "连接未打开"});
85
85
  return;
86
86
  }
@@ -88,7 +88,7 @@ module.exports = function(RED) {
88
88
  // 使用串口配置节点的write方法(带队列机制)
89
89
  serialNode.write(buffer, function(err) {
90
90
  if (err) {
91
- node.error('发送失败: ' + err.message);
91
+ node.log('发送失败: ' + err.message);
92
92
  node.status({fill: "red", shape: "ring", text: "发送失败"});
93
93
  } else {
94
94
  node.log(cmdName + '指令已发送: ' + buffer.toString('hex').toUpperCase());
@@ -108,7 +108,7 @@ module.exports = function(RED) {
108
108
 
109
109
  // 只接受布尔值
110
110
  if (typeof value !== 'boolean') {
111
- node.warn('输入必须为true/false,当前类型: ' + typeof value);
111
+ node.log('输入必须为true/false,当前类型: ' + typeof value);
112
112
  return;
113
113
  }
114
114
 
@@ -126,7 +126,7 @@ module.exports = function(RED) {
126
126
  // 获取当前指令
127
127
  var currentCmd = commands[node.curtainStateIndex];
128
128
  if (!currentCmd || !currentCmd.hex) {
129
- node.warn('窗帘模式缺少' + currentCmd.name + '指令配置');
129
+ node.log('窗帘模式缺少' + currentCmd.name + '指令配置');
130
130
  return;
131
131
  }
132
132
 
@@ -144,7 +144,7 @@ module.exports = function(RED) {
144
144
  var hexString = value ? node.config.openCmd : node.config.closeCmd;
145
145
 
146
146
  if (!hexString) {
147
- node.warn(cmdName + '指令未配置');
147
+ node.log(cmdName + '指令未配置');
148
148
  return;
149
149
  }
150
150
 
@@ -54,7 +54,7 @@ module.exports = function(RED) {
54
54
  // 获取主站节点
55
55
  node.masterNode = RED.nodes.getNode(node.config.masterNodeId);
56
56
  if (!node.masterNode) {
57
- node.error("未找到主站节点");
57
+ node.log("未找到主站节点");
58
58
  node.status({fill: "red", shape: "dot", text: "主站节点未找到"});
59
59
  return;
60
60
  }
@@ -62,7 +62,7 @@ module.exports = function(RED) {
62
62
  // 从主站节点的 config.slaves 获取从站配置
63
63
  const slaves = node.masterNode.config ? node.masterNode.config.slaves : null;
64
64
  if (!slaves || slaves.length === 0) {
65
- node.error("主站节点未配置从站");
65
+ node.log("主站节点未配置从站");
66
66
  node.status({fill: "red", shape: "dot", text: "主站未配置从站"});
67
67
  return;
68
68
  }
@@ -82,8 +82,8 @@ module.exports = function(RED) {
82
82
  node.log(`端口: ${node.config.port}`);
83
83
 
84
84
  } catch (err) {
85
- node.error(`初始化失败: ${err.message}`);
86
- node.status({fill: "red", shape: "dot", text: "初始化失败"});
85
+ node.log(`初始化异常: ${err.message}`);
86
+ node.status({fill: "red", shape: "ring", text: `初始化失败: ${err.message}`});
87
87
  }
88
88
  }
89
89
 
@@ -198,7 +198,7 @@ module.exports = function(RED) {
198
198
  node.log(`配件名称已更新: ${accessory._slaveAddr}_${accessory._coil} -> ${newName}`);
199
199
  }
200
200
  } catch (err) {
201
- node.error(`更新配件名称失败: ${err.message}`);
201
+ node.log(`更新配件名称失败: ${err.message}`);
202
202
  }
203
203
  }
204
204
 
@@ -259,7 +259,7 @@ module.exports = function(RED) {
259
259
  node.log(`状态同步到HomeKit: 从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
260
260
  }
261
261
  } catch (err) {
262
- node.error(`状态变化监听器错误: ${err.message}`);
262
+ node.log(`状态变化监听器错误: ${err.message}`);
263
263
  }
264
264
  };
265
265
 
@@ -287,7 +287,7 @@ module.exports = function(RED) {
287
287
  });
288
288
  node.log(`已更新 ${updateCount} 个配件名称`);
289
289
  } catch (err) {
290
- node.error(`批量更新配件名称失败: ${err.message}`);
290
+ node.log(`批量更新配件名称失败: ${err.message}`);
291
291
  }
292
292
  }
293
293
 
@@ -307,7 +307,7 @@ module.exports = function(RED) {
307
307
  node.bridge.unpublish();
308
308
  node.log("HomeKit网桥已停止");
309
309
  } catch (err) {
310
- node.warn(`停止网桥时出错: ${err.message}`);
310
+ node.log(`停止网桥时出错: ${err.message}`);
311
311
  }
312
312
  }
313
313
 
@@ -23,7 +23,7 @@ module.exports = function(RED) {
23
23
  // 获取主站节点
24
24
  var masterNode = RED.nodes.getNode(node.config.masterNode);
25
25
  if (!masterNode) {
26
- node.error('未找到主站节点');
26
+ node.log('未找到主站节点');
27
27
  node.status({fill: "red", shape: "ring", text: "未配置主站"});
28
28
  return;
29
29
  }
@@ -92,7 +92,7 @@ module.exports = function(RED) {
92
92
  if (node.sourceType === "serial") {
93
93
  if (!node.serialPortConfig) {
94
94
  node.status({ fill: "red", shape: "ring", text: "未选择串口配置" });
95
- node.error("未配置RS-485连接,请在节点中选择 serial-port-config 配置节点");
95
+ node.log("未配置RS-485连接,请在节点中选择 serial-port-config 配置节点");
96
96
  return;
97
97
  }
98
98
  // 使用共享连接
@@ -104,14 +104,14 @@ module.exports = function(RED) {
104
104
  node.log(`modbus-debug 监听共享连接:${desc}`);
105
105
  node.status({ fill: "blue", shape: "ring", text: "监听中" });
106
106
  } catch (err) {
107
- node.error(`注册数据监听器失败: ${err.message}`);
107
+ node.log(`注册数据监听器失败: ${err.message}`);
108
108
  node.status({ fill: "red", shape: "ring", text: "监听失败" });
109
109
  }
110
110
  } else if (node.sourceType === "modbus") {
111
111
  // 已废弃:不再支持独立连接到Modbus服务器(会导致串口冲突)
112
112
  // 请使用 "serial" 源类型并选择 serial-port-config 配置节点
113
113
  node.status({ fill: "red", shape: "ring", text: "请使用serial源类型" });
114
- node.error("不再支持独立连接到Modbus服务器(会导致串口冲突),请将源类型改为 'serial' 并选择 serial-port-config 配置节点");
114
+ node.log("不再支持独立连接到Modbus服务器(会导致串口冲突),请将源类型改为 'serial' 并选择 serial-port-config 配置节点");
115
115
  return;
116
116
  }
117
117
  };
@@ -128,7 +128,7 @@ module.exports = function(RED) {
128
128
  node.serialPortConfig.unregisterDataListener(sendHexMsg);
129
129
  }
130
130
  } catch (e) {
131
- node.warn(`注销监听器时出错: ${e.message}`);
131
+ node.log(`注销监听器时出错: ${e.message}`);
132
132
  }
133
133
  try {
134
134
  if (node.localConnection) {
@@ -140,7 +140,7 @@ module.exports = function(RED) {
140
140
  node.localConnection = null;
141
141
  }
142
142
  } catch (e) {
143
- node.warn(`关闭本地连接时出错: ${e.message}`);
143
+ node.log(`关闭本地连接时出错: ${e.message}`);
144
144
  }
145
145
  done();
146
146
  });
@@ -122,7 +122,7 @@
122
122
  renderSlaveList();
123
123
  });
124
124
 
125
- $(document).on("click", ".btn-delete-slave", function() {
125
+ $("#slave-list-container").on("click", ".btn-delete-slave", function() {
126
126
  var index = parseInt($(this).data("index"));
127
127
  if (node.slaves.length > 1) {
128
128
  node.slaves.splice(index, 1);
@@ -132,7 +132,7 @@
132
132
  }
133
133
  });
134
134
 
135
- $(document).on("change", ".slave-address, .slave-coil-start, .slave-coil-end, .slave-poll-interval", function() {
135
+ $("#slave-list-container").on("change", ".slave-address, .slave-coil-start, .slave-coil-end, .slave-poll-interval", function() {
136
136
  var index = parseInt($(this).data("index"));
137
137
  var className = $(this).attr("class").split(" ")[0];
138
138
  var value = parseInt($(this).val());
@@ -130,7 +130,7 @@ module.exports = function(RED) {
130
130
 
131
131
  // 验证Modbus服务器配置
132
132
  if (!node.modbusServerConfig) {
133
- node.warn('未配置Modbus服务器,将使用默认配置');
133
+ node.log('未配置Modbus服务器,将使用默认配置');
134
134
  }
135
135
 
136
136
  // 记录主站配置信息(用于调试多主站问题)
@@ -150,6 +150,7 @@ module.exports = function(RED) {
150
150
  node.mqttClient = null;
151
151
  node.mqttConnected = false; // MQTT连接状态
152
152
  node.isClosing = false;
153
+ node.lastConnectError = ""; // 记录最后一次连接错误,用于日志限流
153
154
  node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
154
155
  node.lastMqttErrorLog = 0; // MQTT错误日志时间
155
156
  node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5分钟
@@ -197,7 +198,18 @@ module.exports = function(RED) {
197
198
 
198
199
  // 更新节点状态显示
199
200
  node.updateNodeStatus = function() {
200
- const modbusStatus = node.isConnected ? "Modbus-OK" : "Modbus-ERR";
201
+ let modbusStatus = node.isConnected ? "Modbus-OK" : "Modbus-ERR";
202
+
203
+ // 如果连接失败,尝试显示简短错误信息
204
+ if (!node.isConnected && node.lastConnectError) {
205
+ // 提取错误关键词,避免状态栏过长
206
+ let shortErr = node.lastConnectError;
207
+ if (shortErr.includes("ECONNREFUSED")) shortErr = "拒绝连接";
208
+ if (shortErr.includes("ENOENT") || shortErr.includes("No such file")) shortErr = "串口不存在";
209
+ if (shortErr.includes("EACCES")) shortErr = "权限不足";
210
+ if (shortErr.includes("timeout")) shortErr = "连接超时";
211
+ modbusStatus = `ERR: ${shortErr}`;
212
+ }
201
213
 
202
214
  // 如果MQTT未启用,显示"本地模式"
203
215
  let mqttStatus;
@@ -252,7 +264,7 @@ module.exports = function(RED) {
252
264
  });
253
265
  });
254
266
  } catch (err) {
255
- node.warn(`关闭旧连接时出错: ${err.message}`);
267
+ node.log(`关闭旧连接时出错: ${err.message}`);
256
268
  }
257
269
  }
258
270
 
@@ -325,14 +337,14 @@ module.exports = function(RED) {
325
337
  // 设置错误处理器,防止未捕获的错误导致进程崩溃
326
338
  if (node.client._port) {
327
339
  node.client._port.on('error', (err) => {
328
- node.warn(`串口错误(已忽略): ${err.message}`);
340
+ node.log(`串口错误(已忽略): ${err.message}`);
329
341
  });
330
342
  }
331
343
 
332
344
  // TCP模式:设置socket错误处理器
333
345
  if (node.client._client) {
334
346
  node.client._client.on('error', (err) => {
335
- node.warn(`TCP socket错误(已忽略): ${err.message}`);
347
+ node.log(`TCP socket错误(已忽略): ${err.message}`);
336
348
  });
337
349
  }
338
350
 
@@ -370,8 +382,22 @@ module.exports = function(RED) {
370
382
  }
371
383
 
372
384
  } catch (err) {
373
- node.error(`Modbus连接失败: ${err.message}`);
374
385
  node.isConnected = false;
386
+
387
+ // 优化:如果错误消息没有变化,且不是第一次重试,则不输出 node.error 到全局日志
388
+ // 这样可以避免 ECONNREFUSED 等持续性错误刷屏
389
+ const errorMsg = err.message || "未知连接错误";
390
+ const isNewError = node.lastConnectError !== errorMsg;
391
+
392
+ if (isNewError || node.reconnectAttempts % 12 === 0) { // 每分钟(约12次5秒重试)输出一次日志
393
+ if (isNewError) {
394
+ node.log(`Modbus连接失败: ${errorMsg}`);
395
+ } else {
396
+ node.debug(`Modbus连接持续失败: ${errorMsg} (已重试 ${node.reconnectAttempts} 次)`);
397
+ }
398
+ node.lastConnectError = errorMsg;
399
+ }
400
+
375
401
  node.updateNodeStatus();
376
402
 
377
403
  // 使用指数退避策略重试(5秒、10秒、20秒...最大60秒)
@@ -473,8 +499,8 @@ module.exports = function(RED) {
473
499
 
474
500
  // 验证MQTT broker配置
475
501
  if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
476
- node.warn('MQTT已启用但broker地址未配置 - 使用纯本地模式');
477
- node.warn('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
502
+ node.log('MQTT已启用但broker地址未配置 - 使用纯本地模式');
503
+ node.log('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
478
504
  return;
479
505
  }
480
506
 
@@ -539,11 +565,17 @@ module.exports = function(RED) {
539
565
  node.mqttClient.on('error', (err) => {
540
566
  // 连接失败,尝试下一个候选地址
541
567
  const errorMsg = err.message || err.code || '连接失败';
542
- node.warn(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
543
568
 
544
569
  const now = Date.now();
545
570
  const timeSinceLastAttempt = now - lastConnectAttempt;
546
571
 
572
+ // 减少 MQTT 错误日志频率
573
+ const shouldLogWarn = (now - node.lastMqttErrorLog) > 60000; // 每分钟最多显示一次 warn
574
+ if (shouldLogWarn) {
575
+ node.log(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
576
+ node.lastMqttErrorLog = now;
577
+ }
578
+
547
579
  // 避免频繁重试(至少等待1秒),但仍要记录错误
548
580
  if (timeSinceLastAttempt < 1000) {
549
581
  setTimeout(() => {
@@ -565,20 +597,18 @@ module.exports = function(RED) {
565
597
  const isSingleIpConfig = brokerCandidates.length === 1;
566
598
 
567
599
  if (isSingleIpConfig) {
568
- // 局域网IP配置失败,立即输出错误(不受日志限流限制)
569
- node.error(`MQTT连接失败: ${errorMsg}`);
570
- node.error(`无法连接到MQTT broker: ${brokerCandidates[0]}`);
571
- node.error('请检查:1) MQTT broker是否在该地址运行 2) 网络是否连通 3) 端口是否正确');
572
- node.error('提示:可以使用命令测试: telnet 192.168.2.12 1883');
600
+ // 局域网IP配置失败,立即输出错误
601
+ const shouldLogDetailed = (now - node.lastMqttErrorLog) > 60000;
602
+ if (shouldLogDetailed) {
603
+ node.log(`MQTT连接失败: ${errorMsg} (${brokerCandidates[0]})`);
604
+ node.lastMqttErrorLog = now;
605
+ }
573
606
  } else {
574
607
  // 多个fallback地址都失败,使用日志限流
575
608
  const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
576
609
 
577
610
  if (shouldLog) {
578
- node.error(`MQTT错误: ${errorMsg}`);
579
- node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
580
- node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
581
- node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
611
+ node.log(`MQTT错误: ${errorMsg} (所有候选地址均失败)`);
582
612
  node.lastMqttErrorLog = now;
583
613
  }
584
614
  }
@@ -602,7 +632,7 @@ module.exports = function(RED) {
602
632
  const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
603
633
 
604
634
  if (shouldLog) {
605
- node.warn('MQTT离线,正在尝试重连...');
635
+ node.log('MQTT离线,正在尝试重连...');
606
636
  node.lastMqttErrorLog = now;
607
637
  }
608
638
 
@@ -620,7 +650,7 @@ module.exports = function(RED) {
620
650
  });
621
651
 
622
652
  } catch (err) {
623
- node.error(`MQTT连接异常: ${err.message}`);
653
+ node.log(`MQTT连接异常: ${err.message}`);
624
654
 
625
655
  // 尝试下一个候选地址
626
656
  currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
@@ -658,8 +688,8 @@ module.exports = function(RED) {
658
688
  node.mqttClient.publish(availabilityTopic, 'online', { retain: true });
659
689
 
660
690
  for (let coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
661
- // 使用稳定的唯一ID:从站地址_线圈编号(确保全局唯一)
662
- const uniqueId = `modbus_relay_${slaveId}_${coil}`;
691
+ // 使用更稳定的唯一ID:主站ID_从站地址_线圈编号(确保全局绝对唯一)
692
+ const uniqueId = `modbus_${node.id}_${slaveId}_${coil}`;
663
693
  const objectId = `relay_${slaveId}_${coil}`;
664
694
  const discoveryTopic = `homeassistant/switch/${uniqueId}/config`;
665
695
 
@@ -728,7 +758,7 @@ module.exports = function(RED) {
728
758
  // 使用QoS=1订阅,确保命令不丢失
729
759
  node.mqttClient.subscribe(commandTopic, { qos: 1 }, (err) => {
730
760
  if (err) {
731
- node.error(`订阅MQTT命令主题失败: ${err.message}`);
761
+ node.log(`订阅MQTT命令主题失败: ${err.message}`);
732
762
  } else {
733
763
  node.log(`已订阅MQTT命令主题: ${commandTopic}(QoS=1)`);
734
764
  }
@@ -759,7 +789,7 @@ module.exports = function(RED) {
759
789
  try {
760
790
  await node.writeSingleCoil(cmd.slaveId, cmd.coil, cmd.value);
761
791
  } catch (err) {
762
- node.error(`MQTT命令执行失败: 从站${cmd.slaveId} 线圈${cmd.coil} - ${err.message}`);
792
+ node.log(`MQTT命令执行失败: 从站${cmd.slaveId} 线圈${cmd.coil} - ${err.message}`);
763
793
  }
764
794
  }
765
795
  };
@@ -813,7 +843,7 @@ module.exports = function(RED) {
813
843
  if (err) {
814
844
  // 只在首次错误时输出警告
815
845
  if (!node.lastMqttPublishError || Date.now() - node.lastMqttPublishError > 60000) {
816
- node.warn(`发布状态失败: ${stateTopic} - ${err.message}`);
846
+ node.log(`发布状态失败: ${stateTopic} - ${err.message}`);
817
847
  node.lastMqttPublishError = Date.now();
818
848
  }
819
849
  }
@@ -829,7 +859,7 @@ module.exports = function(RED) {
829
859
 
830
860
  // 检查从站配置
831
861
  if (!node.config.slaves || node.config.slaves.length === 0) {
832
- node.error('未配置从站设备,无法启动轮询');
862
+ node.log('未配置从站设备,无法启动轮询');
833
863
  return;
834
864
  }
835
865
 
@@ -993,6 +1023,7 @@ module.exports = function(RED) {
993
1023
  // 后续轮询:只在状态真正改变时广播
994
1024
  if (isFirstPoll || oldValue !== newValue) {
995
1025
  RED.events.emit('modbus:coilStateChanged', {
1026
+ masterId: node.id,
996
1027
  slave: slaveId,
997
1028
  coil: coilIndex,
998
1029
  value: newValue,
@@ -1096,7 +1127,7 @@ module.exports = function(RED) {
1096
1127
  if (isConnectionError) {
1097
1128
  // 连接断开,尝试重连
1098
1129
  if (shouldLog) {
1099
- node.warn('检测到连接断开,尝试重连...');
1130
+ node.log('检测到连接断开,尝试重连...');
1100
1131
  }
1101
1132
 
1102
1133
  node.isConnected = false;
@@ -1184,7 +1215,7 @@ module.exports = function(RED) {
1184
1215
 
1185
1216
  // 写入线圈(异步,不阻塞)
1186
1217
  node.writeSingleCoil(slaveId, coilNumber, state).catch(err => {
1187
- node.error(`Symi按键控制失败: ${err.message}`);
1218
+ node.log(`Symi按键控制失败: ${err.message}`);
1188
1219
  });
1189
1220
 
1190
1221
  // 发送应答帧(REPORT类型0x04,反馈LED状态)
@@ -1238,7 +1269,7 @@ module.exports = function(RED) {
1238
1269
  task.reject(err);
1239
1270
  }
1240
1271
  // 写入失败不中断队列,继续处理下一个任务
1241
- node.warn(`队列任务失败,继续处理下一个任务: ${err.message}`);
1272
+ node.log(`队列任务失败,继续处理下一个任务: ${err.message}`);
1242
1273
  }
1243
1274
 
1244
1275
  // 等待一段时间再处理下一个任务(40ms间隔,确保总线稳定)
@@ -1338,7 +1369,7 @@ module.exports = function(RED) {
1338
1369
  node.pausePolling = false;
1339
1370
  node.pollingPausedCount = 0;
1340
1371
 
1341
- node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
1372
+ node.log(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
1342
1373
  throw err;
1343
1374
  }
1344
1375
  };
@@ -1443,7 +1474,7 @@ module.exports = function(RED) {
1443
1474
  node.pausePolling = false;
1444
1475
  node.pollingPausedCount = 0;
1445
1476
 
1446
- node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
1477
+ node.log(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
1447
1478
  throw err;
1448
1479
  }
1449
1480
  };
@@ -1478,6 +1509,18 @@ module.exports = function(RED) {
1478
1509
  const value = Boolean(data.value);
1479
1510
  const triggerSource = data.triggerSource || 'unknown'; // 传递触发源标识
1480
1511
 
1512
+ // 检查主站关联(如果指定了主站ID,则必须匹配)
1513
+ if (data.masterId && data.masterId !== node.id) {
1514
+ return;
1515
+ }
1516
+
1517
+ // 检查该主站是否配置了此从站
1518
+ const slaveConfig = node.config.slaves.find(s => s.address === slave);
1519
+ if (!slaveConfig) {
1520
+ // 该主站不管理此从站,忽略
1521
+ return;
1522
+ }
1523
+
1481
1524
  // 输出日志确认收到事件
1482
1525
  node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'} (触发源: ${triggerSource})`);
1483
1526
 
@@ -1487,7 +1530,7 @@ module.exports = function(RED) {
1487
1530
 
1488
1531
  node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1489
1532
  } catch (err) {
1490
- node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
1533
+ node.log(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
1491
1534
  }
1492
1535
  };
1493
1536
 
@@ -1519,10 +1562,10 @@ module.exports = function(RED) {
1519
1562
  const value = Boolean(msg.payload.value);
1520
1563
 
1521
1564
  node.writeSingleCoil(slave, coil, value).catch(err => {
1522
- node.error(`连线模式写入失败: ${err.message}`);
1565
+ node.log(`连线模式写入失败: ${err.message}`);
1523
1566
  });
1524
1567
  } else {
1525
- node.warn(`writeCoil命令参数不完整: slave=${msg.payload.slave}, coil=${msg.payload.coil}, value=${msg.payload.value}`);
1568
+ node.log(`writeCoil命令参数不完整: slave=${msg.payload.slave}, coil=${msg.payload.coil}, value=${msg.payload.value}`);
1526
1569
  }
1527
1570
  break;
1528
1571
 
@@ -1535,15 +1578,15 @@ module.exports = function(RED) {
1535
1578
  node.log(`接收到本地模式批量写入命令: 从站${slave} 起始线圈${startCoil} 共${values.length}个`);
1536
1579
 
1537
1580
  node.writeMultipleCoils(slave, startCoil, values).catch(err => {
1538
- node.error(`本地模式批量写入失败: ${err.message}`);
1581
+ node.log(`本地模式批量写入失败: ${err.message}`);
1539
1582
  });
1540
1583
  } else {
1541
- node.warn(`writeCoils命令参数不完整`);
1584
+ node.log(`writeCoils命令参数不完整`);
1542
1585
  }
1543
1586
  break;
1544
1587
 
1545
1588
  default:
1546
- node.warn(`未知命令: ${cmd}`);
1589
+ node.log(`未知命令: ${cmd}`);
1547
1590
  }
1548
1591
  });
1549
1592
 
@@ -1617,7 +1660,7 @@ module.exports = function(RED) {
1617
1660
  });
1618
1661
  }
1619
1662
  } catch (err) {
1620
- node.warn(`关闭Modbus连接时出错: ${err.message}`);
1663
+ node.log(`关闭Modbus连接时出错: ${err.message}`);
1621
1664
  }
1622
1665
  node.client = null; // 释放引用
1623
1666
  }
@@ -1643,7 +1686,7 @@ module.exports = function(RED) {
1643
1686
  done();
1644
1687
  }
1645
1688
  } catch (err) {
1646
- node.warn(`关闭MQTT连接时出错: ${err.message}`);
1689
+ node.log(`关闭MQTT连接时出错: ${err.message}`);
1647
1690
  node.mqttClient = null;
1648
1691
  done();
1649
1692
  }
@@ -24,7 +24,8 @@
24
24
  meshWirelessMode: {value: false}, // 无线模式(场景触发,不响应LED反馈)
25
25
  // 映射到继电器
26
26
  targetSlaveAddress: {value: 10, validate: RED.validators.number()},
27
- targetCoilNumber: {value: 1, validate: RED.validators.number()} // 默认值改为1(显示为1路)
27
+ targetCoilNumber: {value: 1, validate: RED.validators.number()}, // 默认值改为1(显示为1路)
28
+ modbusMaster: {value: ""} // 关联的主站节点
28
29
  },
29
30
  inputs: 1,
30
31
  outputs: 1,
@@ -209,6 +210,17 @@
209
210
  // 始终加载已保存的Mesh设备列表(无论当前是什么模式)
210
211
  // 这样切换到Mesh模式时,列表已经加载好了
211
212
  loadSavedMeshDevices();
213
+
214
+ // 加载所有Modbus主站节点
215
+ const masterSelect = $("#node-input-modbusMaster");
216
+ const currentMasterId = node.modbusMaster;
217
+
218
+ RED.nodes.eachNode(function(n) {
219
+ if (n.type === "modbus-master") {
220
+ const isSelected = n.id === currentMasterId;
221
+ masterSelect.append(`<option value="${n.id}" ${isSelected ? 'selected' : ''}>${n.name || n.id}</option>`);
222
+ }
223
+ });
212
224
  }
213
225
  });
214
226
  </script>
@@ -396,6 +408,16 @@
396
408
  </div>
397
409
  </div>
398
410
 
411
+ <div class="form-row">
412
+ <label for="node-input-modbusMaster" style="width: 110px;"><i class="fa fa-sitemap"></i> 关联主站</label>
413
+ <select id="node-input-modbusMaster" style="width: calc(70% - 110px);">
414
+ <option value="">所有主站 (不推荐)</option>
415
+ </select>
416
+ <div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
417
+ 选择该继电器所属的Modbus主站节点,避免多主站冲突
418
+ </div>
419
+ </div>
420
+
399
421
  <div class="form-row" style="margin-top: 20px; padding: 14px; background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%); border-left: 4px solid #4caf50; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.08);">
400
422
  <div style="font-size: 12px; color: #333; line-height: 1.8;">
401
423
  <div style="font-weight: 600; color: #2e7d32; margin-bottom: 10px; font-size: 13px;">
@@ -353,7 +353,7 @@ module.exports = function(RED) {
353
353
  // 获取RS-485连接配置节点
354
354
  node.serialPortConfig = RED.nodes.getNode(config.serialPortConfig);
355
355
  if (!node.serialPortConfig) {
356
- node.error('未配置RS-485连接,请在节点配置中选择连接配置');
356
+ node.log('未配置RS-485连接,请在节点配置中选择连接配置');
357
357
  node.status({fill: "red", shape: "ring", text: "未配置连接"});
358
358
  return;
359
359
  }
@@ -385,10 +385,14 @@ module.exports = function(RED) {
385
385
  meshTotalButtons: parseInt(config.meshTotalButtons) || 1, // Mesh开关总路数(1-6)
386
386
  // 目标继电器配置
387
387
  targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
388
- targetCoilNumber: modbusCoilNumber // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
388
+ targetCoilNumber: modbusCoilNumber, // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
389
+ modbusMaster: config.modbusMaster || "" // 关联的主站节点ID
389
390
  };
390
391
 
391
392
  node.currentState = false;
393
+ node.isRs485Connected = false;
394
+ node.lastErrorLog = 0;
395
+ node.lastErrorMsg = "";
392
396
  node.mqttClient = null;
393
397
  node.isClosing = false;
394
398
  node.lastTriggerTime = 0; // 上次触发时间(用于场景模式防抖)
@@ -410,10 +414,10 @@ module.exports = function(RED) {
410
414
  node.config.meshShortAddress = savedDevice.shortAddr;
411
415
  }
412
416
  } catch (err) {
413
- node.warn(`更新Mesh设备短地址失败: ${err.message}`);
417
+ node.log(`更新Mesh设备短地址失败: ${err.message}`);
414
418
  }
415
419
  }).catch(err => {
416
- node.warn(`初始化Mesh存储失败: ${err.message}`);
420
+ node.log(`初始化Mesh存储失败: ${err.message}`);
417
421
  });
418
422
  }
419
423
 
@@ -546,7 +550,13 @@ module.exports = function(RED) {
546
550
  node.startListeningStateChanges();
547
551
 
548
552
  } catch (err) {
549
- node.error(`RS-485连接失败: ${err.message}`);
553
+ const now = Date.now();
554
+ const errMsg = err.message || "";
555
+ if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
556
+ node.log(`RS-485连接异常: ${errMsg}`);
557
+ node.lastErrorLog = now;
558
+ node.lastErrorMsg = errMsg;
559
+ }
550
560
  node.isRs485Connected = false;
551
561
  node.updateStatus();
552
562
  }
@@ -569,7 +579,7 @@ module.exports = function(RED) {
569
579
  // 注册到共享连接配置
570
580
  node.serialPortConfig.registerDataListener(node.serialDataListener);
571
581
  } else {
572
- node.error('RS-485连接配置未初始化,无法监听数据');
582
+ node.log('RS-485连接配置未初始化,无法监听数据');
573
583
  }
574
584
  };
575
585
 
@@ -584,8 +594,14 @@ module.exports = function(RED) {
584
594
  const slave = parseInt(data.slave);
585
595
  const coil = parseInt(data.coil);
586
596
  const value = Boolean(data.value);
597
+ const masterId = data.masterId || "";
587
598
  const triggerSource = data.triggerSource || data.source || 'unknown'; // 触发源:'button-press', 'relay-control', 'init', 'unknown'
588
599
 
600
+ // 检查主站关联(如果配置了主站,则只响应对应主站的消息)
601
+ if (node.config.modbusMaster && masterId && node.config.modbusMaster !== masterId) {
602
+ return;
603
+ }
604
+
589
605
  // 检查是否是我们关注的从站和线圈
590
606
  // 识别首次轮询(source: 'init'),标记已接收初始状态
591
607
  if (data.source === 'init' && !node.hasReceivedInitialState) {
@@ -867,7 +883,7 @@ module.exports = function(RED) {
867
883
  // 不匹配的节点静默忽略,不输出任何日志
868
884
  }
869
885
  } catch (err) {
870
- node.error(`解析RS-485数据失败: ${err.message}`);
886
+ node.log(`解析RS-485数据失败: ${err.message}`);
871
887
  }
872
888
  };
873
889
 
@@ -967,7 +983,7 @@ module.exports = function(RED) {
967
983
  }
968
984
  }
969
985
  } catch (err) {
970
- node.error(`解析Clowire数据失败: ${err.message}`);
986
+ node.log(`解析Clowire数据失败: ${err.message}`);
971
987
  }
972
988
  };
973
989
 
@@ -1004,7 +1020,7 @@ module.exports = function(RED) {
1004
1020
  // 开关模式:正常处理状态
1005
1021
  // 获取按钮状态
1006
1022
  if (!event.states || event.states.length < node.config.meshButtonNumber) {
1007
- node.warn(`[Mesh事件] 按键${node.config.meshButtonNumber} 状态数据不完整,states=${JSON.stringify(event.states)}`);
1023
+ node.log(`[Mesh事件] 按键${node.config.meshButtonNumber} 状态数据不完整,states=${JSON.stringify(event.states)}`);
1008
1024
  return; // 状态数据不完整
1009
1025
  }
1010
1026
 
@@ -1098,7 +1114,7 @@ module.exports = function(RED) {
1098
1114
  node.sendMqttCommand(buttonState, isSceneMode);
1099
1115
 
1100
1116
  } catch (err) {
1101
- node.error(`解析Mesh数据失败: ${err.message}`);
1117
+ node.log(`解析Mesh数据失败: ${err.message}`);
1102
1118
  }
1103
1119
  };
1104
1120
 
@@ -1113,7 +1129,7 @@ module.exports = function(RED) {
1113
1129
 
1114
1130
  node.mqttClient.publish(commandTopic, payload, { qos: 1 }, (err) => {
1115
1131
  if (err) {
1116
- node.error(`MQTT发送失败: ${err.message}`);
1132
+ node.log(`MQTT发送失败: ${err.message}`);
1117
1133
  } else {
1118
1134
  node.debug(`MQTT模式:发送命令到${commandTopic} = ${payload}`);
1119
1135
  }
@@ -1130,7 +1146,9 @@ module.exports = function(RED) {
1130
1146
  value: state,
1131
1147
  source: 'slave-switch',
1132
1148
  triggerSource: isSceneMode ? 'scene-trigger' : 'button-press', // 场景模式使用scene-trigger,开关模式使用button-press
1133
- nodeId: node.id
1149
+ nodeId: node.id,
1150
+ masterId: node.config.modbusMaster || "", // 指定目标主站ID
1151
+ serialPortConfigId: config.serialPortConfig // 增加串口配置ID,用于主站过滤,防止多主站冲突
1134
1152
  });
1135
1153
 
1136
1154
  // 内部事件模式:静默发送(不输出日志)
@@ -1153,7 +1171,7 @@ module.exports = function(RED) {
1153
1171
 
1154
1172
  // 再次检查是否过期
1155
1173
  if (Date.now() - item.timestamp >= node.queueTimeout) {
1156
- node.warn(`丢弃过期命令(${Date.now() - item.timestamp}ms)`);
1174
+ node.log(`丢弃过期命令(${Date.now() - item.timestamp}ms)`);
1157
1175
  continue;
1158
1176
  }
1159
1177
 
@@ -1179,7 +1197,7 @@ module.exports = function(RED) {
1179
1197
  await new Promise(resolve => setTimeout(resolve, 40));
1180
1198
  }
1181
1199
  } catch (err) {
1182
- node.error(`MQTT发送失败: ${err.message}`);
1200
+ node.log(`MQTT发送失败: ${err.message}`);
1183
1201
  }
1184
1202
  }
1185
1203
 
@@ -1195,7 +1213,7 @@ module.exports = function(RED) {
1195
1213
  if (!node.serialPortConfig || !node.serialPortConfig.connection) {
1196
1214
  // 初始化期间静默警告
1197
1215
  if (!node.isInitializing) {
1198
- node.warn('RS-485连接未建立,无法发送指示灯反馈');
1216
+ node.log('RS-485连接未建立,无法发送指示灯反馈');
1199
1217
  }
1200
1218
  node.debug(`[sendCommandToPanel] 连接未建立,退出`);
1201
1219
  return;
@@ -1258,7 +1276,7 @@ module.exports = function(RED) {
1258
1276
  );
1259
1277
 
1260
1278
  if (!command) {
1261
- node.error(`构建Mesh控制帧失败`);
1279
+ node.log(`构建Mesh控制帧失败`);
1262
1280
  return;
1263
1281
  }
1264
1282
 
@@ -1286,7 +1304,7 @@ module.exports = function(RED) {
1286
1304
  );
1287
1305
 
1288
1306
  if (!command) {
1289
- node.error(`构建Clowire LED控制帧失败`);
1307
+ node.log(`构建Clowire LED控制帧失败`);
1290
1308
  return;
1291
1309
  }
1292
1310
 
@@ -1324,7 +1342,7 @@ module.exports = function(RED) {
1324
1342
 
1325
1343
  node.serialPortConfig.write(command, (err) => {
1326
1344
  if (err) {
1327
- node.error(`LED反馈失败: ${err.message}`);
1345
+ node.log(`LED反馈失败: ${err.message}`);
1328
1346
  } else {
1329
1347
  // LED反馈发送成功(静默,不输出日志)
1330
1348
  }
@@ -1381,8 +1399,8 @@ module.exports = function(RED) {
1381
1399
 
1382
1400
  // 验证MQTT broker配置
1383
1401
  if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
1384
- node.warn('MQTT已启用但broker地址未配置 - 使用本地模式');
1385
- node.warn('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
1402
+ node.log('MQTT已启用但broker地址未配置 - 使用本地模式');
1403
+ node.log('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
1386
1404
  return;
1387
1405
  }
1388
1406
 
@@ -1431,7 +1449,7 @@ module.exports = function(RED) {
1431
1449
  // 订阅状态主题(QoS=1确保状态更新不丢失)
1432
1450
  node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
1433
1451
  if (err) {
1434
- node.error(`订阅失败: ${err.message}`);
1452
+ node.log(`订阅失败: ${err.message}`);
1435
1453
  }
1436
1454
  });
1437
1455
  });
@@ -1570,7 +1588,7 @@ module.exports = function(RED) {
1570
1588
  });
1571
1589
 
1572
1590
  } catch (err) {
1573
- node.error(`MQTT连接异常: ${err.message}`);
1591
+ node.log(`MQTT连接异常: ${err.message}`);
1574
1592
 
1575
1593
  // 尝试下一个候选地址
1576
1594
  currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
@@ -1622,7 +1640,7 @@ module.exports = function(RED) {
1622
1640
  const command = value ? 'ON' : 'OFF';
1623
1641
  node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
1624
1642
  if (err) {
1625
- node.error(`发布命令失败: ${err.message}`);
1643
+ node.log(`发布命令失败: ${err.message}`);
1626
1644
  }
1627
1645
  });
1628
1646
  } else {
@@ -1671,7 +1689,7 @@ module.exports = function(RED) {
1671
1689
  try {
1672
1690
  node.serialPortConfig.unregisterDataListener(node.serialDataListener);
1673
1691
  } catch (err) {
1674
- node.warn(`注销RS-485监听器时出错: ${err.message}`);
1692
+ node.log(`注销RS-485监听器时出错: ${err.message}`);
1675
1693
  }
1676
1694
  node.serialDataListener = null;
1677
1695
  }
@@ -1700,7 +1718,7 @@ module.exports = function(RED) {
1700
1718
  done();
1701
1719
  }
1702
1720
  } catch (err) {
1703
- node.warn(`关闭MQTT连接时出错: ${err.message}`);
1721
+ node.log(`关闭MQTT连接时出错: ${err.message}`);
1704
1722
  node.mqttClient = null;
1705
1723
  node.mqttConnected = false;
1706
1724
  done();
@@ -31,7 +31,8 @@ module.exports = function(RED) {
31
31
 
32
32
  // 错误日志限流(避免断网时产生大量日志)
33
33
  node.lastErrorLog = 0; // 上次错误日志时间
34
- node.errorLogInterval = 60 * 1000; // 错误日志间隔:60秒
34
+ node.lastErrorMsg = ""; // 记录上一次错误消息
35
+ node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔提高到10分钟
35
36
 
36
37
  // 数据监听器列表(每个从站开关节点注册一个)
37
38
  node.dataListeners = [];
@@ -52,25 +53,28 @@ module.exports = function(RED) {
52
53
  try {
53
54
  // 检查监听器数量(正常情况下不应超过100个)
54
55
  if (node.dataListeners.length > 100) {
55
- node.warn(`监听器数量异常: ${node.dataListeners.length},可能存在内存泄漏`);
56
+ node.log(`监听器数量异常: ${node.dataListeners.length},可能存在内存泄漏`);
56
57
  }
57
58
 
58
59
  // 检查写入队列长度(正常情况下不应超过1000个)
59
60
  if (node.writeQueue.length > 1000) {
60
- node.warn(`写入队列积压严重: ${node.writeQueue.length},自动清理旧数据`);
61
+ node.log(`写入队列积压严重: ${node.writeQueue.length},自动清理旧数据`);
61
62
  // 只保留最新的100个写入请求
62
63
  const removed = node.writeQueue.length - 100;
63
64
  node.writeQueue = node.writeQueue.slice(-100);
64
- node.warn(`已清理${removed}个积压的写入请求`);
65
+ node.log(`已清理${removed}个积压的写入请求`);
65
66
  }
66
67
 
67
68
  // 检查帧缓冲区大小(正常情况下不应超过10KB)
68
69
  if (node.frameBuffer && node.frameBuffer.length > 10000) {
69
- node.warn(`帧缓冲区过大: ${node.frameBuffer.length}字节,自动清空`);
70
+ node.log(`帧缓冲区过大: ${node.frameBuffer.length}字节,自动清空`);
70
71
  node.frameBuffer = Buffer.alloc(0);
71
72
  }
72
73
  } catch (err) {
73
- node.error(`内存检查失败: ${err.message}`);
74
+ if (now - node.lastErrorLog > node.errorLogInterval) {
75
+ node.log(`内存检查异常: ${err.message}`);
76
+ node.lastErrorLog = now;
77
+ }
74
78
  }
75
79
  }, 5 * 60 * 1000); // 每5分钟检查一次
76
80
 
@@ -107,25 +111,31 @@ module.exports = function(RED) {
107
111
  try {
108
112
  listener(data);
109
113
  } catch (err) {
110
- node.error(`数据监听器错误: ${err.message}`);
114
+ if (now - node.lastErrorLog > node.errorLogInterval) {
115
+ node.log(`数据监听器异常: ${err.message}`);
116
+ node.lastErrorLog = now;
117
+ }
111
118
  }
112
119
  });
113
120
  });
114
121
 
115
122
  // 监听超时
116
123
  node.connection.on('timeout', () => {
117
- node.warn('TCP连接超时,尝试重连...');
124
+ node.log('TCP连接超时,尝试重连...');
118
125
  if (node.connection) {
119
126
  node.connection.destroy();
120
127
  }
121
128
  });
122
129
 
123
- // 监听错误(限流日志)
130
+ // 监听错误(静默处理,避免刷屏)
124
131
  node.connection.on('error', (err) => {
125
132
  const now = Date.now();
126
- if (now - node.lastErrorLog > node.errorLogInterval) {
127
- node.error(`TCP连接错误: ${err.message}`);
133
+ const errMsg = err.message || "";
134
+ // 只有错误消息变化,或者间隔时间到了才打印一条 log(不输出到调试面板)
135
+ if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
136
+ node.log(`TCP连接异常: ${errMsg}`);
128
137
  node.lastErrorLog = now;
138
+ node.lastErrorMsg = errMsg;
129
139
  }
130
140
  // 不在这里重连,在close事件中统一处理
131
141
  });
@@ -152,7 +162,7 @@ module.exports = function(RED) {
152
162
 
153
163
  const nowLog = Date.now();
154
164
  if (nowLog - node.lastErrorLog > node.errorLogInterval) {
155
- node.log(`${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
165
+ node.debug(`[SerialPortConfig] ${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
156
166
  node.lastErrorLog = nowLog;
157
167
  }
158
168
 
@@ -163,7 +173,11 @@ module.exports = function(RED) {
163
173
  }
164
174
  });
165
175
  } catch (err) {
166
- node.error(`TCP连接初始化失败: ${err.message}`);
176
+ const now = Date.now();
177
+ if (now - node.lastErrorLog > node.errorLogInterval) {
178
+ node.log(`TCP连接初始化异常: ${err.message}`);
179
+ node.lastErrorLog = now;
180
+ }
167
181
  }
168
182
  };
169
183
 
@@ -215,9 +229,11 @@ module.exports = function(RED) {
215
229
 
216
230
  if (err) {
217
231
  const now = Date.now();
218
- if (now - node.lastErrorLog > node.errorLogInterval) {
219
- node.error(`串口打开失败: ${err.message}`);
232
+ const errMsg = err.message || "";
233
+ if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
234
+ node.log(`串口打开失败: ${errMsg}`);
220
235
  node.lastErrorLog = now;
236
+ node.lastErrorMsg = errMsg;
221
237
  }
222
238
 
223
239
  // 打开失败时也要触发重连(如果有监听器在使用)
@@ -227,7 +243,7 @@ module.exports = function(RED) {
227
243
 
228
244
  const nowLog = Date.now();
229
245
  if (nowLog - node.lastErrorLog > node.errorLogInterval) {
230
- node.log(`${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
246
+ node.debug(`[SerialPortConfig] ${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
231
247
  node.lastErrorLog = nowLog;
232
248
  }
233
249
 
@@ -251,7 +267,7 @@ module.exports = function(RED) {
251
267
  } catch (err) {
252
268
  const now = Date.now();
253
269
  if (now - node.lastErrorLog > node.errorLogInterval) {
254
- node.error(`数据监听器错误: ${err.message}`);
270
+ node.log(`数据监听器错误: ${err.message}`);
255
271
  node.lastErrorLog = now;
256
272
  }
257
273
  }
@@ -261,9 +277,11 @@ module.exports = function(RED) {
261
277
  // 监听错误(限流日志)
262
278
  node.connection.on('error', (err) => {
263
279
  const now = Date.now();
264
- if (now - node.lastErrorLog > node.errorLogInterval) {
265
- node.error(`串口错误: ${err.message}`);
280
+ const errMsg = err.message || "";
281
+ if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
282
+ node.log(`串口运行错误: ${errMsg}`);
266
283
  node.lastErrorLog = now;
284
+ node.lastErrorMsg = errMsg;
267
285
  }
268
286
  // 不在这里重连,在close事件中统一处理
269
287
  });
@@ -310,8 +328,12 @@ module.exports = function(RED) {
310
328
  });
311
329
  } catch (err) {
312
330
  node.isOpening = false;
313
- node.error(`串口初始化失败: ${err.message}`);
314
-
331
+ const now = Date.now();
332
+ if (now - node.lastErrorLog > node.errorLogInterval) {
333
+ node.log(`串口初始化异常: ${err.message}`);
334
+ node.lastErrorLog = now;
335
+ }
336
+
315
337
  // 初始化失败时也要触发重连
316
338
  if (!node.isClosing && node.dataListeners.length > 0) {
317
339
  const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000);
@@ -334,7 +356,7 @@ module.exports = function(RED) {
334
356
  // 注册数据监听器
335
357
  node.registerDataListener = function(listener) {
336
358
  if (typeof listener !== 'function') {
337
- node.error('监听器必须是函数');
359
+ node.log('监听器必须是函数');
338
360
  return;
339
361
  }
340
362
 
@@ -433,7 +455,11 @@ module.exports = function(RED) {
433
455
  await new Promise((resolve, reject) => {
434
456
  node.connection.write(data, (err) => {
435
457
  if (err) {
436
- node.error(`TCP写入失败: ${err.message}`);
458
+ const now = Date.now();
459
+ if (now - node.lastErrorLog > node.errorLogInterval) {
460
+ node.log(`TCP写入异常: ${err.message}`);
461
+ node.lastErrorLog = now;
462
+ }
437
463
  if (callback) callback(err);
438
464
  reject(err);
439
465
  } else {
@@ -442,7 +468,7 @@ module.exports = function(RED) {
442
468
  try {
443
469
  listener(data, 'sent');
444
470
  } catch (e) {
445
- node.error('监听器处理发送数据失败: ' + e.message);
471
+ node.log('监听器处理发送数据失败: ' + e.message);
446
472
  }
447
473
  });
448
474
 
@@ -467,7 +493,11 @@ module.exports = function(RED) {
467
493
  await new Promise((resolve, reject) => {
468
494
  node.connection.write(data, (err) => {
469
495
  if (err) {
470
- node.error(`串口写入失败: ${err.message}`);
496
+ const now = Date.now();
497
+ if (now - node.lastErrorLog > node.errorLogInterval) {
498
+ node.log(`串口写入异常: ${err.message}`);
499
+ node.lastErrorLog = now;
500
+ }
471
501
  if (callback) callback(err);
472
502
  reject(err);
473
503
  } else {
@@ -476,7 +506,7 @@ module.exports = function(RED) {
476
506
  try {
477
507
  listener(data, 'sent');
478
508
  } catch (e) {
479
- node.error('监听器处理发送数据失败: ' + e.message);
509
+ node.log('监听器处理发送数据失败: ' + e.message);
480
510
  }
481
511
  });
482
512
 
@@ -493,7 +523,7 @@ module.exports = function(RED) {
493
523
  }
494
524
  } catch (err) {
495
525
  // 写入失败,继续处理下一个
496
- node.warn(`写入失败(已跳过): ${err.message}`);
526
+ node.log(`写入失败(已跳过): ${err.message}`);
497
527
  }
498
528
  }
499
529
 
@@ -538,7 +568,7 @@ module.exports = function(RED) {
538
568
  node.connection.destroy();
539
569
  }
540
570
  } catch (err) {
541
- node.warn(`关闭TCP连接时出错: ${err.message}`);
571
+ node.log(`关闭TCP连接时出错: ${err.message}`);
542
572
  }
543
573
  node.connection = null;
544
574
  done();
@@ -548,7 +578,7 @@ module.exports = function(RED) {
548
578
  if (node.connection.isOpen) {
549
579
  node.connection.close((err) => {
550
580
  if (err) {
551
- node.error(`串口关闭失败: ${err.message}`);
581
+ node.log(`串口关闭失败: ${err.message}`);
552
582
  }
553
583
  node.connection = null;
554
584
  done();
@@ -558,7 +588,7 @@ module.exports = function(RED) {
558
588
  done();
559
589
  }
560
590
  } catch (err) {
561
- node.warn(`关闭串口连接时出错: ${err.message}`);
591
+ node.log(`关闭串口连接时出错: ${err.message}`);
562
592
  node.connection = null;
563
593
  done();
564
594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-modbus",
3
- "version": "2.9.9",
3
+ "version": "2.9.10",
4
4
  "description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
5
5
  "main": "nodes/modbus-master.js",
6
6
  "scripts": {