node-red-contrib-symi-modbus 2.5.0 → 2.5.3

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.
@@ -2,6 +2,7 @@ module.exports = function(RED) {
2
2
  "use strict";
3
3
  const ModbusRTU = require("modbus-serial");
4
4
  const mqtt = require("mqtt");
5
+ const protocol = require("./lightweight-protocol");
5
6
 
6
7
  // 串口列表API - 支持Windows、Linux、macOS所有串口设备
7
8
  RED.httpAdmin.get('/modbus-master/serialports', async function(req, res) {
@@ -90,33 +91,41 @@ module.exports = function(RED) {
90
91
  RED.nodes.createNode(this, config);
91
92
  const node = this;
92
93
 
94
+ // 获取Modbus服务器配置节点
95
+ node.modbusServerConfig = RED.nodes.getNode(config.modbusServer);
96
+
93
97
  // 获取MQTT服务器配置节点
94
98
  node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
95
99
 
96
- // 配置参数
100
+ // 配置参数(从Modbus服务器配置节点读取)
97
101
  node.config = {
98
- connectionType: config.connectionType,
99
- tcpHost: config.tcpHost,
100
- tcpPort: parseInt(config.tcpPort),
101
- serialPort: config.serialPort,
102
- serialBaudRate: parseInt(config.serialBaudRate),
103
- serialDataBits: parseInt(config.serialDataBits),
104
- serialStopBits: parseInt(config.serialStopBits),
105
- serialParity: config.serialParity,
102
+ connectionType: node.modbusServerConfig ? (node.modbusServerConfig.connectionType || "tcp") : "tcp",
103
+ tcpHost: node.modbusServerConfig ? (node.modbusServerConfig.tcpHost || "127.0.0.1") : "127.0.0.1",
104
+ tcpPort: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.tcpPort) || 502) : 502,
105
+ serialPort: node.modbusServerConfig ? (node.modbusServerConfig.serialPort || "/dev/ttyUSB0") : "/dev/ttyUSB0",
106
+ serialBaudRate: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialBaudRate) || 9600) : 9600,
107
+ serialDataBits: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialDataBits) || 8) : 8,
108
+ serialStopBits: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialStopBits) || 1) : 1,
109
+ serialParity: node.modbusServerConfig ? (node.modbusServerConfig.serialParity || "none") : "none",
106
110
  slaves: config.slaves || [{
107
111
  address: 10,
108
112
  coilStart: 0,
109
113
  coilEnd: 31,
110
114
  pollInterval: 200
111
115
  }],
112
- enableMqtt: config.enableMqtt,
116
+ enableMqtt: config.enableMqtt !== false,
113
117
  // 从config节点读取MQTT配置
114
- mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "",
118
+ mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "mqtt://localhost:1883",
115
119
  mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
116
120
  mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
117
121
  mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay"
118
122
  };
119
123
 
124
+ // 验证Modbus服务器配置
125
+ if (!node.modbusServerConfig) {
126
+ node.warn('未配置Modbus服务器,将使用默认配置');
127
+ }
128
+
120
129
  // Modbus客户端
121
130
  node.client = new ModbusRTU();
122
131
  node.isConnected = false;
@@ -132,6 +141,9 @@ module.exports = function(RED) {
132
141
  node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
133
142
  node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
134
143
  node.lastWriteTime = {}; // 记录每个从站的最后写入时间
144
+ node.pausePolling = false; // 暂停轮询标志(从站上报时暂停)
145
+ node.pollingPausedCount = 0; // 暂停轮询计数器
146
+ node._discoveryPublished = false; // Discovery发布标志(避免重复)
135
147
 
136
148
  // 更新节点状态显示
137
149
  node.updateNodeStatus = function() {
@@ -155,7 +167,10 @@ module.exports = function(RED) {
155
167
  lastUpdate: null,
156
168
  error: null,
157
169
  config: slave, // 保存该从站的配置
158
- initialPublished: false // 标记是否已发布初始状态
170
+ initialPublished: false, // 标记是否已发布初始状态
171
+ timeoutCount: 0, // 连续超时次数
172
+ isIgnored: false, // 是否被临时忽略(超时次数过多时)
173
+ lastTimeoutTime: 0 // 最后一次超时时间
159
174
  };
160
175
  });
161
176
 
@@ -163,32 +178,75 @@ module.exports = function(RED) {
163
178
  node.connectModbus = async function() {
164
179
  try {
165
180
  if (node.config.connectionType === "tcp") {
166
- await node.client.connectTCP(node.config.tcpHost, {
167
- port: node.config.tcpPort
168
- });
169
- node.log(`已连接到TCP Modbus: ${node.config.tcpHost}:${node.config.tcpPort}`);
181
+ // 验证TCP主机地址
182
+ if (!node.config.tcpHost || node.config.tcpHost.trim() === '') {
183
+ throw new Error('TCP主机地址未配置,请在节点配置中填写Modbus服务器IP地址');
184
+ }
185
+
186
+ const tcpMode = node.config.tcpMode || "telnet";
187
+ const modeNames = {
188
+ "telnet": "Telnet ASCII",
189
+ "rtu": "RTU over TCP",
190
+ "tcp": "Modbus TCP"
191
+ };
192
+
193
+ node.log(`正在连接TCP网关(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}...`);
194
+
195
+ if (tcpMode === "telnet") {
196
+ await node.client.connectTelnet(node.config.tcpHost, {
197
+ port: node.config.tcpPort
198
+ });
199
+ } else if (tcpMode === "rtu") {
200
+ await node.client.connectTcpRTUBuffered(node.config.tcpHost, {
201
+ port: node.config.tcpPort
202
+ });
203
+ } else {
204
+ await node.client.connectTCP(node.config.tcpHost, {
205
+ port: node.config.tcpPort
206
+ });
207
+ }
208
+
209
+ node.log(`TCP网关连接成功(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}`);
170
210
  } else {
171
211
  await node.client.connectRTUBuffered(node.config.serialPort, {
172
- baudRate: node.config.serialBaudRate,
173
- dataBits: node.config.serialDataBits,
174
- stopBits: node.config.serialStopBits,
175
- parity: node.config.serialParity
212
+ baudRate: node.config.serialBaudRate || 9600,
213
+ dataBits: node.config.serialDataBits || 8,
214
+ stopBits: node.config.serialStopBits || 1,
215
+ parity: node.config.serialParity || 'none'
176
216
  });
177
- node.log(`已连接到串口 Modbus: ${node.config.serialPort}`);
217
+ node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
178
218
  }
179
-
180
- node.client.setTimeout(5000);
219
+
220
+ // 设置超时时间(串口需要更长的超时时间)
221
+ const timeout = node.config.connectionType === "serial" ? 10000 : 5000;
222
+ node.client.setTimeout(timeout);
223
+ node.log(`Modbus超时设置: ${timeout}ms`);
181
224
  node.isConnected = true;
182
225
  node.log(`Modbus已连接: ${node.config.connectionType === "tcp" ? `${node.config.tcpHost}:${node.config.tcpPort}` : node.config.serialPort}`);
183
226
  node.updateNodeStatus();
184
-
227
+
185
228
  // 清除错误日志记录(重新部署或重连时允许再次显示错误)
186
229
  node.lastErrorLog = {};
187
230
  node.lastMqttErrorLog = 0;
188
-
189
- // 启动轮询
190
- node.startPolling();
191
-
231
+ node._discoveryPublished = false; // 重置Discovery发布标志
232
+
233
+ // 添加Symi按键事件监听(串口模式)
234
+ if (node.config.connectionType === "serial" && node.client._port) {
235
+ node.client._port.on('data', (data) => {
236
+ node.handleSymiButtonEvent(data);
237
+ });
238
+ node.log('已启用Symi按键事件监听(串口模式)');
239
+ }
240
+
241
+ // 立即启动轮询(不等待MQTT连接)
242
+ // 修复:确保轮询在Modbus连接成功后立即启动,不依赖MQTT状态
243
+ if (!node.pollTimer) {
244
+ node.log('Modbus连接成功,立即启动轮询...');
245
+ node.startPolling();
246
+ } else {
247
+ node.log('轮询已在运行中');
248
+ }
249
+
192
250
  } catch (err) {
193
251
  node.error(`Modbus连接失败: ${err.message}`);
194
252
  node.isConnected = false;
@@ -281,6 +339,7 @@ module.exports = function(RED) {
281
339
  // 连接MQTT(带智能重试和fallback)
282
340
  node.connectMqtt = function() {
283
341
  if (!node.config.enableMqtt) {
342
+ node.log('MQTT未启用,跳过MQTT连接');
284
343
  return;
285
344
  }
286
345
 
@@ -299,7 +358,7 @@ module.exports = function(RED) {
299
358
  node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
300
359
 
301
360
  const options = {
302
- clientId: `modbus_master_${Math.random().toString(16).substr(2, 8)}`,
361
+ clientId: `modbus_master_${Math.random().toString(16).substring(2, 10)}`,
303
362
  clean: false, // 持久化会话,断线重连后继续接收消息
304
363
  reconnectPeriod: 0, // 禁用自动重连,我们手动管理
305
364
  connectTimeout: 5000, // 5秒连接超时
@@ -309,9 +368,7 @@ module.exports = function(RED) {
309
368
  if (node.config.mqttUsername) {
310
369
  options.username = node.config.mqttUsername;
311
370
  options.password = node.config.mqttPassword;
312
- node.log(`MQTT认证: 用户名=${node.config.mqttUsername}, 密码已设置=${!!node.config.mqttPassword}`);
313
- } else {
314
- node.warn('MQTT未配置认证信息,如果broker需要认证将会连接失败');
371
+ node.log(`MQTT认证: 用户名=${node.config.mqttUsername}`);
315
372
  }
316
373
 
317
374
  // 尝试连接函数
@@ -332,18 +389,20 @@ module.exports = function(RED) {
332
389
  node.mqttClient.on('connect', () => {
333
390
  node.mqttConnected = true;
334
391
  node.log(`MQTT已连接: ${brokerUrl}`);
335
-
392
+
336
393
  // 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
337
394
  if (brokerUrl !== brokerCandidates[0]) {
338
395
  node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
339
396
  }
340
-
341
- // 发送设备发现消息(Home Assistant MQTT Discovery)
342
- node.publishDiscovery();
343
-
397
+
398
+ // 异步发送设备发现消息(避免阻塞事件循环)
399
+ setImmediate(() => {
400
+ node.publishDiscovery();
401
+ });
402
+
344
403
  // 订阅命令主题
345
404
  node.subscribeCommands();
346
-
405
+
347
406
  // 更新状态显示
348
407
  node.updateNodeStatus();
349
408
  });
@@ -454,6 +513,12 @@ module.exports = function(RED) {
454
513
  return;
455
514
  }
456
515
 
516
+ // 检查是否已经发布过(避免重复)
517
+ if (node._discoveryPublished) {
518
+ node.log('Discovery已发布,跳过重复发布');
519
+ return;
520
+ }
521
+
457
522
  let discoveryCount = 0;
458
523
 
459
524
  node.config.slaves.forEach((slave) => {
@@ -464,7 +529,7 @@ module.exports = function(RED) {
464
529
  node.mqttClient.publish(availabilityTopic, 'online', { retain: true });
465
530
 
466
531
  for (let coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
467
- // 使用稳定的唯一ID:从站地址_线圈编号
532
+ // 使用稳定的唯一ID:从站地址_线圈编号(确保全局唯一)
468
533
  const uniqueId = `modbus_relay_${slaveId}_${coil}`;
469
534
  const objectId = `relay_${slaveId}_${coil}`;
470
535
  const discoveryTopic = `homeassistant/switch/${uniqueId}/config`;
@@ -506,15 +571,11 @@ module.exports = function(RED) {
506
571
  };
507
572
 
508
573
  // 发布发现消息(retain=true 确保HA重启后仍能发现)
574
+ // 使用QoS=0避免阻塞,发现消息只需发送一次即可
509
575
  node.mqttClient.publish(
510
576
  discoveryTopic,
511
577
  JSON.stringify(discoveryPayload),
512
- { retain: true, qos: 1 },
513
- (err) => {
514
- if (err) {
515
- node.error(`发布发现消息失败 ${uniqueId}: ${err.message}`);
516
- }
517
- }
578
+ { retain: true, qos: 0 }
518
579
  );
519
580
 
520
581
  discoveryCount++;
@@ -523,6 +584,9 @@ module.exports = function(RED) {
523
584
 
524
585
  const slaveAddresses = node.config.slaves.map(s => s.address).join(', ');
525
586
  node.log(`已发布 ${discoveryCount} 个MQTT发现消息(从站地址: ${slaveAddresses})`);
587
+
588
+ // 标记为已发布(避免重复)
589
+ node._discoveryPublished = true;
526
590
  };
527
591
 
528
592
  // 订阅命令主题
@@ -542,109 +606,179 @@ module.exports = function(RED) {
542
606
  });
543
607
  };
544
608
 
545
- // 处理MQTT命令(异步)
609
+ // MQTT命令队列(去重 + 防抖)
610
+ node.mqttCommandQueue = new Map(); // key: "slaveId_coil", value: {slaveId, coil, value, timestamp}
611
+ node.mqttCommandTimer = null;
612
+ node.mqttCommandDebounceMs = 50; // 防抖时间:50ms
613
+
614
+ // 处理MQTT命令队列
615
+ node.processMqttCommandQueue = async function() {
616
+ if (node.mqttCommandQueue.size === 0) {
617
+ return;
618
+ }
619
+
620
+ // 获取所有待处理命令
621
+ const commands = Array.from(node.mqttCommandQueue.values());
622
+ node.mqttCommandQueue.clear();
623
+
624
+ // 按优先级排序:物理按键 > HA控制
625
+ // 这里简化处理,直接按时间戳排序(最新的优先)
626
+ commands.sort((a, b) => b.timestamp - a.timestamp);
627
+
628
+ // 批量执行命令
629
+ for (const cmd of commands) {
630
+ try {
631
+ await node.writeSingleCoil(cmd.slaveId, cmd.coil, cmd.value);
632
+ } catch (err) {
633
+ node.error(`MQTT命令执行失败: 从站${cmd.slaveId} 线圈${cmd.coil} - ${err.message}`);
634
+ }
635
+ }
636
+ };
637
+
638
+ // 处理MQTT命令(异步 + 去重 + 防抖)
546
639
  node.handleMqttCommand = async function(topic, message) {
547
640
  const parts = topic.split('/');
548
641
  if (parts.length < 4) {
549
642
  return;
550
643
  }
551
-
644
+
552
645
  const slaveId = parseInt(parts[parts.length - 3]);
553
646
  const coil = parseInt(parts[parts.length - 2]);
554
647
  const command = message.toString();
555
-
648
+
556
649
  const value = (command === 'ON' || command === 'true' || command === '1');
557
-
558
- node.log(`MQTT命令: 从站${slaveId} 线圈${coil} = ${value}`);
559
-
560
- // 写入线圈(异步执行,捕获错误)
561
- try {
562
- await node.writeSingleCoil(slaveId, coil, value);
563
- } catch (err) {
564
- node.error(`MQTT命令执行失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
650
+
651
+ // 添加到队列(去重:相同从站+线圈的命令只保留最新的)
652
+ const key = `${slaveId}_${coil}`;
653
+ node.mqttCommandQueue.set(key, {
654
+ slaveId,
655
+ coil,
656
+ value,
657
+ timestamp: Date.now()
658
+ });
659
+
660
+ // 防抖:延迟执行,合并短时间内的多个命令
661
+ if (node.mqttCommandTimer) {
662
+ clearTimeout(node.mqttCommandTimer);
565
663
  }
664
+
665
+ node.mqttCommandTimer = setTimeout(() => {
666
+ node.mqttCommandTimer = null;
667
+ node.processMqttCommandQueue();
668
+ }, node.mqttCommandDebounceMs);
566
669
  };
567
670
 
568
671
  // 发布MQTT状态
569
672
  node.publishMqttState = function(slaveId, coil, value) {
570
673
  if (!node.mqttClient || !node.mqttClient.connected) {
571
- node.warn(`无法发布状态: MQTT未连接 (从站${slaveId} 线圈${coil})`);
674
+ // 不输出警告,避免日志过多
572
675
  return;
573
676
  }
574
-
677
+
575
678
  const stateTopic = `${node.config.mqttBaseTopic}/${slaveId}/${coil}/state`;
576
679
  const payload = value ? 'ON' : 'OFF';
577
-
578
- // 使用QoS=1确保消息送达,retain=true确保断线重连后可获取最新状态
579
- node.mqttClient.publish(stateTopic, payload, { qos: 1, retain: true }, (err) => {
680
+
681
+ // 使用QoS=0提高性能,retain=true确保断线重连后可获取最新状态
682
+ // QoS=0不等待确认,避免阻塞轮询
683
+ node.mqttClient.publish(stateTopic, payload, { qos: 0, retain: true }, (err) => {
580
684
  if (err) {
581
- node.warn(`发布状态失败: ${stateTopic} - ${err.message}`);
582
- } else {
583
- node.log(`发布状态: ${stateTopic} = ${payload}`);
685
+ // 只在首次错误时输出警告
686
+ if (!node.lastMqttPublishError || Date.now() - node.lastMqttPublishError > 60000) {
687
+ node.warn(`发布状态失败: ${stateTopic} - ${err.message}`);
688
+ node.lastMqttPublishError = Date.now();
689
+ }
584
690
  }
585
691
  });
586
692
  };
587
693
 
588
- // 开始轮询
694
+ // 开始轮询(使用递归调用而非定时器,避免并发)
589
695
  node.startPolling = function() {
590
- if (node.pollTimer) {
696
+ if (node.isPolling) {
591
697
  return;
592
698
  }
593
-
699
+
594
700
  // 清除错误日志记录(重新开始轮询时允许显示错误)
595
701
  node.lastErrorLog = {};
596
702
  node.lastMqttErrorLog = 0;
597
-
598
- const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd})`).join(', ');
703
+
704
+ const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd},间隔${s.pollInterval}ms)`).join(', ');
599
705
  node.log(`开始轮询 ${node.config.slaves.length} 个从站设备: ${slaveList}`);
600
706
  node.log(`Modbus连接: ${node.isConnected ? '已连接' : '未连接'}, MQTT连接: ${node.mqttConnected ? '已连接' : '未连接'}`);
601
707
  node.currentSlaveIndex = 0;
602
-
603
- // 使用最小的轮询间隔
604
- const minInterval = Math.min(...node.config.slaves.map(s => s.pollInterval));
605
-
606
- node.pollTimer = setInterval(() => {
607
- node.pollNextSlave().catch(err => {
708
+ node.isPolling = true;
709
+
710
+ // 使用递归调用实现轮询,确保不会并发
711
+ const pollLoop = async () => {
712
+ if (!node.isPolling || !node.isConnected || node.isClosing) {
713
+ return;
714
+ }
715
+
716
+ // 获取当前从站配置(在轮询前获取,确保使用正确的间隔)
717
+ const currentSlave = node.config.slaves[node.currentSlaveIndex];
718
+ if (!currentSlave) {
719
+ node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
720
+ node.currentSlaveIndex = 0;
721
+ return;
722
+ }
723
+
724
+ const interval = currentSlave.pollInterval || 200;
725
+
726
+ try {
727
+ await node.pollNextSlave();
728
+ } catch (err) {
608
729
  node.error(`轮询错误: ${err.message}`);
609
- });
610
- }, minInterval);
611
-
612
- // 立即执行一次
613
- node.pollNextSlave().catch(err => {
614
- node.error(`轮询错误: ${err.message}`);
615
- });
730
+ }
731
+
732
+ // 等待指定间隔后继续下一次轮询
733
+ node.pollTimer = setTimeout(pollLoop, interval);
734
+ };
735
+
736
+ // 立即开始轮询
737
+ pollLoop();
616
738
  };
617
739
 
618
740
  // 停止轮询
619
741
  node.stopPolling = function() {
742
+ node.isPolling = false;
620
743
  if (node.pollTimer) {
621
- clearInterval(node.pollTimer);
744
+ clearTimeout(node.pollTimer);
622
745
  node.pollTimer = null;
623
746
  node.log('停止轮询');
624
747
  }
625
748
  };
626
749
 
627
- // 轮询下一个从站
750
+ // 轮询下一个从站(串行执行,不会并发)
628
751
  node.pollNextSlave = async function() {
629
752
  if (!node.isConnected || node.isClosing) {
630
753
  return;
631
754
  }
632
-
633
- // 检查互斥锁
634
- if (node.modbusLock) {
635
- return; // 有写操作正在进行,跳过本次轮询
755
+
756
+ // 检查是否暂停轮询(从站上报时优先处理)
757
+ if (node.pausePolling) {
758
+ node.pollingPausedCount++;
759
+ return; // 暂停轮询,优先处理从站上报
636
760
  }
637
-
761
+
638
762
  // 获取当前从站配置
639
763
  const slave = node.config.slaves[node.currentSlaveIndex];
640
764
  if (!slave) {
765
+ node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
641
766
  node.currentSlaveIndex = 0;
642
767
  return;
643
768
  }
644
-
769
+
645
770
  const slaveId = slave.address;
771
+ const deviceState = node.deviceStates[slaveId];
772
+
773
+ // 检查该从站是否被临时忽略(连续超时过多)
774
+ if (deviceState.isIgnored) {
775
+ // 移动到下一个从站
776
+ node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
777
+ return;
778
+ }
779
+
646
780
  const coilCount = slave.coilEnd - slave.coilStart + 1;
647
-
781
+
648
782
  // 检查是否刚写入过(写入后100ms内不轮询该从站,避免读到旧值)
649
783
  const lastWrite = node.lastWriteTime[slaveId] || 0;
650
784
  const timeSinceWrite = Date.now() - lastWrite;
@@ -653,33 +787,40 @@ module.exports = function(RED) {
653
787
  node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
654
788
  return;
655
789
  }
656
-
790
+
791
+ // 检查锁状态(如果有写操作正在进行,跳过本次轮询)
792
+ if (node.modbusLock) {
793
+ // 移动到下一个从站
794
+ node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
795
+ return;
796
+ }
797
+
657
798
  try {
658
- // 设置锁
799
+ // 设置锁(防止轮询期间有写操作)
659
800
  node.modbusLock = true;
660
-
801
+
661
802
  node.client.setID(slaveId);
662
-
803
+
663
804
  // 读取线圈状态(功能码01)
664
805
  const data = await node.client.readCoils(slave.coilStart, coilCount);
665
-
666
- // 释放锁
667
- node.modbusLock = false;
668
-
806
+
669
807
  // 更新设备状态
670
808
  const isFirstPoll = !node.deviceStates[slaveId].initialPublished;
671
809
  let publishCount = 0;
672
-
810
+
673
811
  for (let i = 0; i < coilCount; i++) {
674
812
  const coilIndex = slave.coilStart + i;
675
813
  const oldValue = node.deviceStates[slaveId].coils[coilIndex];
676
814
  const newValue = data.data[i];
677
-
815
+
678
816
  node.deviceStates[slaveId].coils[coilIndex] = newValue;
679
-
817
+
680
818
  // 第一次轮询或状态改变时,发布到MQTT和触发事件
681
819
  if (isFirstPoll || oldValue !== newValue) {
682
- node.publishMqttState(slaveId, coilIndex, newValue);
820
+ // 异步发布MQTT,不阻塞轮询
821
+ setImmediate(() => {
822
+ node.publishMqttState(slaveId, coilIndex, newValue);
823
+ });
683
824
  publishCount++;
684
825
  node.emit('stateUpdate', {
685
826
  slave: slaveId,
@@ -688,17 +829,21 @@ module.exports = function(RED) {
688
829
  });
689
830
  }
690
831
  }
691
-
832
+
692
833
  if (isFirstPoll) {
693
- node.log(`从站${slaveId}首次轮询成功,发布${publishCount}个状态到MQTT`);
834
+ node.log(`从站${slaveId}首次轮询成功,读取${coilCount}个线圈`);
694
835
  } else if (publishCount > 0) {
695
- node.log(`从站${slaveId}状态变化,发布${publishCount}个更新到MQTT`);
836
+ node.log(`从站${slaveId}状态变化: ${publishCount}个线圈`);
696
837
  }
697
-
838
+
698
839
  node.deviceStates[slaveId].lastUpdate = Date.now();
699
840
  node.deviceStates[slaveId].error = null;
700
- node.deviceStates[slaveId].initialPublished = true; // 标记已发布初始状态
701
-
841
+ node.deviceStates[slaveId].initialPublished = true;
842
+
843
+ // 轮询成功,重置超时计数
844
+ node.deviceStates[slaveId].timeoutCount = 0;
845
+ node.deviceStates[slaveId].isIgnored = false;
846
+
702
847
  // 输出消息
703
848
  const output = {
704
849
  payload: {
@@ -710,46 +855,78 @@ module.exports = function(RED) {
710
855
  timestamp: node.deviceStates[slaveId].lastUpdate
711
856
  }
712
857
  };
713
-
858
+
714
859
  node.send(output);
715
-
860
+
716
861
  // 更新状态显示
717
862
  node.updateNodeStatus();
718
-
863
+
864
+ // 释放锁
865
+ node.modbusLock = false;
866
+
719
867
  } catch (err) {
868
+
720
869
  // 释放锁
721
870
  node.modbusLock = false;
722
-
871
+
723
872
  node.deviceStates[slaveId].error = err.message;
724
-
873
+ node.deviceStates[slaveId].lastTimeoutTime = Date.now();
874
+
725
875
  // 日志限流:每个从站的错误日志最多每10分钟输出一次
726
876
  const now = Date.now();
727
877
  const lastLogTime = node.lastErrorLog[slaveId] || 0;
728
878
  const shouldLog = (now - lastLogTime) > node.errorLogInterval;
729
-
730
- if (shouldLog) {
731
- node.warn(`轮询从站${slaveId}失败(不影响其他从站): ${err.message} [此错误将在10分钟后再次显示]`);
732
- node.lastErrorLog[slaveId] = now;
879
+
880
+ // 检查是否是超时错误
881
+ const isTimeout = err.message && (
882
+ err.message.includes('Timed out') ||
883
+ err.message.includes('ETIMEDOUT') ||
884
+ err.message.includes('timeout')
885
+ );
886
+
887
+ if (isTimeout) {
888
+ // 增加超时计数
889
+ node.deviceStates[slaveId].timeoutCount++;
890
+
891
+ // 连续超时5次后临时忽略该从站(避免拖慢其他从站)
892
+ if (node.deviceStates[slaveId].timeoutCount >= 5) {
893
+ node.deviceStates[slaveId].isIgnored = true;
894
+ if (shouldLog) {
895
+ node.warn(`从站${slaveId}连续超时${node.deviceStates[slaveId].timeoutCount}次,临时忽略该从站(重启或重新部署后恢复)`);
896
+ node.lastErrorLog[slaveId] = now;
897
+ }
898
+ } else {
899
+ if (shouldLog) {
900
+ node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [超时${node.deviceStates[slaveId].timeoutCount}/5]`);
901
+ node.lastErrorLog[slaveId] = now;
902
+ }
903
+ }
904
+ } else {
905
+ // 非超时错误,记录日志
906
+ if (shouldLog) {
907
+ node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
908
+ node.lastErrorLog[slaveId] = now;
909
+ }
733
910
  }
734
-
911
+
735
912
  // 更新状态显示
736
913
  node.updateNodeStatus();
737
-
914
+
738
915
  // 检测是否是连接断开错误
739
- if (err.message &&
740
- (err.message.includes('ECONNRESET') ||
741
- err.message.includes('ETIMEDOUT') ||
916
+ if (err.message &&
917
+ (err.message.includes('ECONNRESET') ||
918
+ err.message.includes('ETIMEDOUT') ||
742
919
  err.message.includes('ENOTCONN') ||
743
920
  err.message.includes('Port Not Open'))) {
744
-
921
+
745
922
  // 连接断开,尝试重连
746
923
  if (shouldLog) {
747
924
  node.warn('检测到连接断开,尝试重连...');
748
925
  }
749
-
926
+
750
927
  node.isConnected = false;
751
928
  node.stopPolling();
752
-
929
+
753
930
  // 关闭当前连接
754
931
  if (node.client && node.client.isOpen) {
755
932
  try {
@@ -758,7 +935,7 @@ module.exports = function(RED) {
758
935
  // 忽略关闭错误
759
936
  }
760
937
  }
761
-
938
+
762
939
  // 尝试重连
763
940
  if (!node.isClosing && !node.reconnectTimer) {
764
941
  node.reconnectTimer = setTimeout(() => {
@@ -768,28 +945,102 @@ module.exports = function(RED) {
768
945
  }
769
946
  }
770
947
  }
771
-
948
+
772
949
  // 移动到下一个从站
773
950
  node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
774
951
  };
775
952
 
776
- // 写单个线圈(带互斥锁)
953
+ // 处理Symi按键事件(私有协议)
954
+ node.handleSymiButtonEvent = function(data) {
955
+ try {
956
+ // 解析Symi协议帧
957
+ const frame = protocol.parseFrame(data);
958
+ if (!frame) {
959
+ return; // 不是有效的Symi帧,忽略
960
+ }
961
+
962
+ // 只处理SET类型(0x03)的按键事件,忽略REPORT类型(0x04)
963
+ // REPORT类型是面板对我们指令的确认,不是按键事件
964
+ if (frame.dataType !== 0x03) {
965
+ return; // 不是按键事件
966
+ }
967
+
968
+ // 检查是否是灯光设备
969
+ if (frame.deviceType !== 0x01) {
970
+ return; // 不是灯光设备
971
+ }
972
+
973
+ // 提取按键信息
974
+ const deviceAddr = frame.deviceAddr; // 设备地址(1-255)
975
+ const channel = frame.channel; // 通道号(1-8)
976
+ const state = frame.opInfo[0] === 0x01; // 状态(1=开,0=关)
977
+
978
+ node.log(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
979
+
980
+ // 查找对应的从站和线圈
981
+ // 假设:设备地址1对应从站10,设备地址2对应从站11,以此类推
982
+ // 通道号直接对应线圈号(1-8 → 0-7)
983
+ const slaveId = 10 + (deviceAddr - 1);
984
+ const coilNumber = channel - 1;
985
+
986
+ // 检查从站是否在配置中
987
+ const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
988
+ if (!slaveConfig) {
989
+ node.warn(`Symi按键事件: 从站${slaveId}未配置,忽略`);
990
+ return;
991
+ }
992
+
993
+ // 检查线圈是否在范围内
994
+ if (coilNumber < slaveConfig.coilStart || coilNumber > slaveConfig.coilEnd) {
995
+ node.warn(`Symi按键事件: 线圈${coilNumber}不在从站${slaveId}的范围内(${slaveConfig.coilStart}-${slaveConfig.coilEnd}),忽略`);
996
+ return;
997
+ }
998
+
999
+ node.log(`Symi按键映射: 设备${deviceAddr}通道${channel} → 从站${slaveId}线圈${coilNumber}`);
1000
+
1001
+ // 写入线圈(异步,不阻塞)
1002
+ node.writeSingleCoil(slaveId, coilNumber, state).catch(err => {
1003
+ node.error(`Symi按键控制失败: ${err.message}`);
1004
+ });
1005
+
1006
+ // 发送应答帧(REPORT类型0x04,反馈LED状态)
1007
+ // 协议规定:面板发送0x03(SET)按键事件,主机应该用0x04(REPORT)反馈LED状态
1008
+ const responseFrame = protocol.buildSingleLightReport(0x01, deviceAddr, channel, state);
1009
+ if (node.client._port && node.client._port.write) {
1010
+ node.client._port.write(responseFrame);
1011
+ node.log(`Symi应答已发送(REPORT): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
1012
+ }
1013
+
1014
+ } catch (err) {
1015
+ // 静默处理错误,避免干扰Modbus通信
1016
+ // node.warn(`Symi按键事件处理错误: ${err.message}`);
1017
+ }
1018
+ };
1019
+
1020
+ // 写单个线圈(带互斥锁和轮询暂停)
777
1021
  node.writeSingleCoil = async function(slaveId, coil, value) {
778
1022
  if (!node.isConnected) {
779
1023
  node.warn('Modbus未连接');
780
1024
  return;
781
1025
  }
782
-
783
- // 等待锁释放(最多等待6秒,因为Modbus超时是5秒)
784
- const maxWait = 6000;
1026
+
1027
+ // 暂停轮询(写操作优先)
1028
+ node.pausePolling = true;
1029
+ const pauseStartTime = Date.now();
1030
+
1031
+ // 等待锁释放(最多等待1000ms,减少超时时间)
1032
+ const maxWait = 1000;
785
1033
  const startWait = Date.now();
1034
+ let waitCount = 0;
786
1035
  while (node.modbusLock && (Date.now() - startWait) < maxWait) {
787
- await new Promise(resolve => setTimeout(resolve, 50));
1036
+ await new Promise(resolve => setTimeout(resolve, 10));
1037
+ waitCount++;
788
1038
  }
789
-
1039
+
790
1040
  if (node.modbusLock) {
791
- node.error(`写入线圈超时: 从站${slaveId} 线圈${coil} (等待锁释放超时,轮询可能阻塞)`);
792
- return;
1041
+ // 强制释放锁(避免死锁)
1042
+ node.warn(`写入线圈等待超时(${waitCount * 10}ms),强制释放锁: 从站${slaveId} 线圈${coil}`);
1043
+ node.modbusLock = false;
793
1044
  }
794
1045
 
795
1046
  try {
@@ -818,31 +1069,51 @@ module.exports = function(RED) {
818
1069
  // 释放锁
819
1070
  node.modbusLock = false;
820
1071
 
1072
+ // 延迟恢复轮询(给从站响应预留时间)
1073
+ setTimeout(() => {
1074
+ node.pausePolling = false;
1075
+ const pauseDuration = Date.now() - pauseStartTime;
1076
+ if (node.pollingPausedCount > 0) {
1077
+ node.log(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
1078
+ node.pollingPausedCount = 0;
1079
+ }
1080
+ }, 100);
1081
+
821
1082
  } catch (err) {
822
1083
  // 释放锁
823
1084
  node.modbusLock = false;
824
1085
 
1086
+ // 恢复轮询
1087
+ node.pausePolling = false;
1088
+ node.pollingPausedCount = 0;
1089
+
825
1090
  node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
826
1091
  throw err; // 抛出错误,让调用者知道失败
827
1092
  }
828
1093
  };
829
1094
 
830
- // 批量写入多个线圈(带互斥锁)
1095
+ // 批量写入多个线圈(带互斥锁和轮询暂停)
831
1096
  node.writeMultipleCoils = async function(slaveId, startCoil, values) {
832
1097
  if (!node.isConnected) {
833
1098
  node.warn('Modbus未连接');
834
1099
  return;
835
1100
  }
836
1101
 
837
- // 等待锁释放(最多等待6秒,因为Modbus超时是5秒)
838
- const maxWait = 6000;
1102
+ // 暂停轮询(从站上报优先处理)
1103
+ node.pausePolling = true;
1104
+ const pauseStartTime = Date.now();
1105
+
1106
+ // 等待锁释放(最多等待500ms)
1107
+ const maxWait = 500;
839
1108
  const startWait = Date.now();
840
1109
  while (node.modbusLock && (Date.now() - startWait) < maxWait) {
841
- await new Promise(resolve => setTimeout(resolve, 50));
1110
+ await new Promise(resolve => setTimeout(resolve, 10));
842
1111
  }
843
1112
 
844
1113
  if (node.modbusLock) {
845
- node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (等待锁释放超时,轮询可能阻塞)`);
1114
+ node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (等待锁释放超时)`);
1115
+ // 恢复轮询
1116
+ node.pausePolling = false;
846
1117
  return;
847
1118
  }
848
1119
 
@@ -873,10 +1144,24 @@ module.exports = function(RED) {
873
1144
  // 释放锁
874
1145
  node.modbusLock = false;
875
1146
 
1147
+ // 延迟恢复轮询(给从站响应预留时间)
1148
+ setTimeout(() => {
1149
+ node.pausePolling = false;
1150
+ const pauseDuration = Date.now() - pauseStartTime;
1151
+ if (node.pollingPausedCount > 0) {
1152
+ node.log(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
1153
+ node.pollingPausedCount = 0;
1154
+ }
1155
+ }, 100);
1156
+
876
1157
  } catch (err) {
877
1158
  // 释放锁
878
1159
  node.modbusLock = false;
879
1160
 
1161
+ // 恢复轮询
1162
+ node.pausePolling = false;
1163
+ node.pollingPausedCount = 0;
1164
+
880
1165
  node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
881
1166
  throw err; // 抛出错误,让调用者知道失败
882
1167
  }
@@ -955,29 +1240,49 @@ module.exports = function(RED) {
955
1240
  node.publishOfflineStatus();
956
1241
  }
957
1242
 
1243
+ // 清理设备状态缓存(释放内存)
1244
+ node.deviceStates = {};
1245
+ node.lastErrorLog = {};
1246
+ node.lastWriteTime = {};
1247
+ node.lastMqttErrorLog = 0;
1248
+
958
1249
  // 关闭Modbus连接
959
- if (node.client && node.isConnected) {
1250
+ if (node.client) {
960
1251
  try {
961
- node.client.close(() => {
962
- node.log('Modbus连接已关闭');
963
- });
1252
+ if (node.client.isOpen) {
1253
+ node.client.close(() => {
1254
+ node.log('Modbus连接已关闭');
1255
+ });
1256
+ }
964
1257
  } catch (err) {
965
1258
  node.warn(`关闭Modbus连接时出错: ${err.message}`);
966
1259
  }
1260
+ node.client = null; // 释放引用
967
1261
  }
968
1262
 
969
1263
  // 关闭MQTT连接
970
- if (node.mqttClient && node.mqttClient.connected) {
1264
+ if (node.mqttClient) {
971
1265
  try {
972
- // 等待离线消息发送后再关闭
973
- setTimeout(() => {
974
- node.mqttClient.end(false, () => {
975
- node.log('MQTT连接已关闭');
976
- done();
977
- });
978
- }, 100);
1266
+ if (node.mqttClient.connected) {
1267
+ // 移除所有监听器(防止内存泄漏)
1268
+ node.mqttClient.removeAllListeners();
1269
+
1270
+ // 等待离线消息发送后再关闭
1271
+ setTimeout(() => {
1272
+ node.mqttClient.end(false, () => {
1273
+ node.log('MQTT连接已关闭');
1274
+ node.mqttClient = null; // 释放引用
1275
+ done();
1276
+ });
1277
+ }, 100);
1278
+ } else {
1279
+ node.mqttClient.removeAllListeners();
1280
+ node.mqttClient = null;
1281
+ done();
1282
+ }
979
1283
  } catch (err) {
980
1284
  node.warn(`关闭MQTT连接时出错: ${err.message}`);
1285
+ node.mqttClient = null;
981
1286
  done();
982
1287
  }
983
1288
  } else {