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.
@@ -1,8 +1,6 @@
1
1
  module.exports = function(RED) {
2
2
  "use strict";
3
3
  const mqtt = require("mqtt");
4
- const ModbusRTU = require("modbus-serial");
5
- const net = require("net");
6
4
  const protocol = require("./lightweight-protocol");
7
5
 
8
6
  // 串口列表API - 支持Windows、Linux、macOS所有串口设备
@@ -60,52 +58,61 @@ module.exports = function(RED) {
60
58
  function ModbusSlaveSwitchNode(config) {
61
59
  RED.nodes.createNode(this, config);
62
60
  const node = this;
63
-
61
+
62
+ // 获取RS-485连接配置节点
63
+ node.serialPortConfig = RED.nodes.getNode(config.serialPortConfig);
64
+ if (!node.serialPortConfig) {
65
+ node.error('未配置RS-485连接,请在节点配置中选择连接配置');
66
+ node.status({fill: "red", shape: "ring", text: "未配置连接"});
67
+ return;
68
+ }
69
+
64
70
  // 获取MQTT服务器配置节点
65
71
  node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
66
-
72
+
67
73
  // 配置参数
68
74
  node.config = {
69
- // RS-485总线连接配置
70
- connectionType: config.connectionType,
71
- tcpHost: config.tcpHost,
72
- tcpPort: parseInt(config.tcpPort),
73
- serialPort: config.serialPort,
74
- serialBaudRate: parseInt(config.serialBaudRate),
75
- serialDataBits: parseInt(config.serialDataBits),
76
- serialStopBits: parseInt(config.serialStopBits),
77
- serialParity: config.serialParity,
78
- // 从config节点读取MQTT配置
75
+ // 从MQTT配置节点读取MQTT配置
79
76
  mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "mqtt://localhost:1883",
80
77
  mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
81
78
  mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
82
79
  mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
80
+ // 开关面板配置
83
81
  switchBrand: config.switchBrand || "symi", // 面板品牌(默认亖米)
84
82
  switchId: parseInt(config.switchId) || 0, // 开关ID(0-255,物理面板地址)
85
83
  buttonNumber: parseInt(config.buttonNumber) || 1, // 按钮编号(1-8)
86
84
  targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
87
85
  targetCoilNumber: parseInt(config.targetCoilNumber) || 0 // 目标继电器线圈编号
88
86
  };
89
-
87
+
90
88
  node.currentState = false;
91
89
  node.mqttClient = null;
92
- node.rs485Client = new ModbusRTU(); // RS-485总线客户端
93
- node.isRs485Connected = false;
94
- node.reconnectTimer = null;
95
90
  node.isClosing = false;
96
91
  node.lastMqttErrorLog = 0; // MQTT错误日志时间
97
92
  node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
98
-
99
- // TCP帧缓冲区(用于处理分包和粘包)
100
- node.frameBuffer = Buffer.alloc(0);
101
-
102
- // 命令队列(处理多个按键同时按下)
93
+
94
+ // 命令队列(处理多个按键同时按下)- 带时间戳
103
95
  node.commandQueue = [];
104
96
  node.isProcessingCommand = false;
105
-
106
- // 指示灯反馈队列(40ms间隔发送)
97
+
98
+ // 指示灯反馈队列(40ms间隔发送)- 带时间戳
107
99
  node.ledFeedbackQueue = [];
108
100
  node.isProcessingLedFeedback = false;
101
+
102
+ // 队列超时时间(3秒)
103
+ node.queueTimeout = 3000;
104
+
105
+ // 防死循环:记录最后一次状态变化的时间戳和值
106
+ node.lastStateChange = {
107
+ timestamp: 0,
108
+ value: false
109
+ };
110
+
111
+ // 节点初始化标志(用于静默初始化期间的警告)
112
+ node.isInitializing = true;
113
+
114
+ // 节点关闭标志(用于静默关闭期间的警告)
115
+ node.isClosing = false;
109
116
 
110
117
  // MQTT主题(映射到继电器设备)
111
118
  node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
@@ -188,184 +195,110 @@ module.exports = function(RED) {
188
195
  // 连接RS-485总线(物理开关面板)
189
196
  node.connectRs485 = async function() {
190
197
  try {
191
- if (node.config.connectionType === "tcp") {
192
- // TCP连接验证
193
- if (!node.config.tcpHost) {
194
- throw new Error("TCP主机地址未配置");
195
- }
196
- await node.rs485Client.connectTCP(node.config.tcpHost, {
197
- port: node.config.tcpPort
198
- });
199
- node.log(`RS485连接成功(TCP): ${node.config.tcpHost}:${node.config.tcpPort}`);
200
- node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
201
- } else {
202
- // 串口连接验证
203
- if (!node.config.serialPort) {
204
- throw new Error("串口未配置,请配置串口路径(如:COM1、/dev/ttyUSB0、/dev/ttyS1)");
205
- }
206
- await node.rs485Client.connectRTUBuffered(node.config.serialPort, {
207
- baudRate: node.config.serialBaudRate || 9600,
208
- dataBits: node.config.serialDataBits || 8,
209
- stopBits: node.config.serialStopBits || 1,
210
- parity: node.config.serialParity || 'none'
211
- });
212
- node.log(`RS485连接成功(串口): ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
213
- node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
214
- }
215
-
216
- node.rs485Client.setTimeout(5000);
198
+ // 使用共享连接配置(由配置节点管理)
199
+ node.log(`使用共享RS-485连接配置: ${node.serialPortConfig.connectionType === 'tcp' ?
200
+ `TCP ${node.serialPortConfig.tcpHost}:${node.serialPortConfig.tcpPort}` :
201
+ `串口 ${node.serialPortConfig.serialPort} @ ${node.serialPortConfig.baudRate}bps`}`);
202
+ node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
203
+
217
204
  node.isRs485Connected = true;
218
205
  node.updateStatus();
219
-
206
+
207
+ // 结束初始化阶段(5秒后)- 避免部署时大量LED反馈同时发送
208
+ setTimeout(() => {
209
+ node.isInitializing = false;
210
+ // 初始化完成后,处理积累的LED反馈队列
211
+ node.processLedFeedbackQueue();
212
+ }, 5000);
213
+
220
214
  // 监听物理开关面板的按键事件
221
215
  node.startListeningButtonEvents();
222
-
216
+
223
217
  } catch (err) {
224
218
  node.error(`RS-485连接失败: ${err.message}`);
225
219
  node.isRs485Connected = false;
226
220
  node.updateStatus();
227
-
228
- // 5秒后重试连接(仅在配置有效时)
229
- if (!node.isClosing && !node.reconnectTimer) {
230
- // 如果是配置错误,不重试
231
- if (err.message.includes("未配置")) {
232
- node.warn("RS-485连接配置无效,请检查配置");
233
- return;
234
- }
235
- node.reconnectTimer = setTimeout(() => {
236
- node.reconnectTimer = null;
237
- node.connectRs485();
238
- }, 5000);
239
- }
240
221
  }
241
222
  };
242
223
 
243
224
  // 监听物理开关面板的按键事件
244
225
  node.startListeningButtonEvents = function() {
245
226
  node.log(`开始监听开关面板 ${node.config.switchId} 的按钮 ${node.config.buttonNumber}`);
246
-
247
- // 监听RS-485数据
248
- if (node.config.connectionType === "tcp") {
249
- // TCP模式:监听socket数据(需要处理帧边界)
250
- if (node.rs485Client._port && node.rs485Client._port.socket) {
251
- const socket = node.rs485Client._port.socket;
252
- socket.on('data', (data) => {
253
- node.handleTcpData(data);
254
- });
255
- node.log('TCP数据监听已启动(使用帧缓冲区)');
256
- }
227
+
228
+ // 使用共享连接配置的数据监听器
229
+ if (node.serialPortConfig) {
230
+ // 定义数据监听器函数
231
+ node.serialDataListener = (data) => {
232
+ node.log(`RS-485收到 ${data.length} 字节: ${data.toString('hex').toUpperCase()}`);
233
+ node.handleRs485Data(data);
234
+ };
235
+
236
+ // 注册到共享连接配置
237
+ node.serialPortConfig.registerDataListener(node.serialDataListener);
238
+
239
+ node.log('RS-485数据监听已启动(共享连接)');
257
240
  } else {
258
- // 串口模式:监听串口数据(串口通常自动处理帧边界)
259
- if (node.rs485Client._port) {
260
- node.rs485Client._port.on('data', (data) => {
261
- node.handleRs485Data(data);
262
- });
263
- node.log('串口数据监听已启动');
264
- }
265
- }
266
- };
267
-
268
- // 处理TCP流式数据(需要缓冲和帧边界检测)
269
- node.handleTcpData = function(data) {
270
- try {
271
- // 将接收到的数据追加到缓冲区
272
- node.frameBuffer = Buffer.concat([node.frameBuffer, data]);
273
-
274
- // 输出缓冲区状态
275
- node.log(`TCP收到 ${data.length} 字节,缓冲区共 ${node.frameBuffer.length} 字节`);
276
-
277
- // 从缓冲区提取完整的帧
278
- while (node.frameBuffer.length > 0) {
279
- // 查找帧头 0x7E
280
- const startIdx = node.frameBuffer.indexOf(0x7E);
281
-
282
- if (startIdx === -1) {
283
- // 没有找到帧头,清空缓冲区
284
- node.frameBuffer = Buffer.alloc(0);
285
- break;
286
- }
287
-
288
- if (startIdx > 0) {
289
- // 丢弃帧头之前的无效数据
290
- node.frameBuffer = node.frameBuffer.slice(startIdx);
291
- }
292
-
293
- // 查找帧尾 0x7D
294
- const endIdx = node.frameBuffer.indexOf(0x7D, 1);
295
-
296
- if (endIdx === -1) {
297
- // 没有找到帧尾,等待更多数据
298
- if (node.frameBuffer.length > 100) {
299
- // 缓冲区过大,可能是错误数据,清空
300
- node.warn('缓冲区过大,清空');
301
- node.frameBuffer = Buffer.alloc(0);
302
- }
303
- break;
304
- }
305
-
306
- // 提取完整的帧(包括帧头和帧尾)
307
- const frame = node.frameBuffer.slice(0, endIdx + 1);
308
-
309
- // 从缓冲区移除已处理的帧
310
- node.frameBuffer = node.frameBuffer.slice(endIdx + 1);
311
-
312
- // 处理这一帧
313
- node.handleRs485Data(frame);
314
- }
315
- } catch (err) {
316
- node.error(`TCP数据处理错误: ${err.message}`);
317
- node.frameBuffer = Buffer.alloc(0); // 错误时清空缓冲区
241
+ node.error('RS-485连接配置未初始化,无法监听数据');
318
242
  }
319
243
  };
320
-
244
+
321
245
  // 处理RS-485接收到的数据
322
246
  node.handleRs485Data = function(data) {
323
247
  try {
324
248
  // 输出原始数据(十六进制)- 方便调试
325
249
  const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
326
250
  node.log(`RS485收到数据: ${hexData}`);
327
-
251
+
328
252
  // 解析轻量级协议帧
329
253
  const frame = protocol.parseFrame(data);
330
254
  if (!frame) {
331
255
  node.warn(`无效的协议帧(CRC校验失败或格式错误): ${hexData}`);
332
256
  return;
333
257
  }
334
-
258
+
335
259
  node.log(`解析成功: 设备地址=${frame.deviceAddr} 通道=${frame.channel} 数据类型=0x${frame.dataType.toString(16).toUpperCase()} 操作码=0x${frame.opCode.toString(16).toUpperCase()}`);
336
-
260
+
261
+ // 忽略 REPORT (0x04) 类型的帧(这是面板对我们指令的确认,不是按键事件)
262
+ // 只处理 SET (0x03) 类型的帧(真正的按键事件)
263
+ if (frame.dataType === 0x04) {
264
+ node.log(`忽略 REPORT 帧(面板确认收到指令)`);
265
+ return;
266
+ }
267
+
337
268
  // 检测是否是按键按下事件
338
269
  const buttonEvent = protocol.detectButtonPress(frame);
339
270
  if (!buttonEvent) {
340
271
  node.log(`不是按键事件(设备类型=${frame.deviceType} 数据类型=${frame.dataType} 操作码=${frame.opCode})`);
341
272
  return;
342
273
  }
343
-
344
- node.log(`检测到按键事件: 类型=${buttonEvent.type} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
345
-
346
- // 检查是否是我们监听的开关和按钮
347
- if (buttonEvent.deviceAddr === node.config.switchId) {
274
+
275
+ node.log(`检测到按键事件: 类型=${buttonEvent.type} 本地地址=${buttonEvent.raw.localAddr} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
276
+
277
+ // 计算实际按键编号(Symi协议公式)
278
+ // 例如:devAddr=1,channel=1→按键1;devAddr=2,channel=1→按键5
279
+ const actualButtonNumber = buttonEvent.deviceAddr * 4 - 4 + buttonEvent.channel;
280
+
281
+ node.log(`实际按键编号: ${actualButtonNumber}(设备${buttonEvent.deviceAddr} × 4 - 4 + 通道${buttonEvent.channel})`);
282
+
283
+ // 检查是否是我们监听的开关面板和按钮
284
+ // switchId对应本地地址(物理面板地址)
285
+ // buttonNumber对应实际按键编号(1-8)
286
+ if (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber) {
348
287
  if (buttonEvent.type === 'single') {
349
- // 单键按下
350
- if (buttonEvent.channel === node.config.buttonNumber) {
351
- node.log(`匹配成功!开关${node.config.switchId} 按钮${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
352
- // 发送MQTT命令到继电器
353
- node.sendMqttCommand(buttonEvent.state);
354
- } else {
355
- node.log(`按钮编号不匹配: 收到${buttonEvent.channel} 期望${node.config.buttonNumber}`);
356
- }
288
+ node.log(`匹配成功!面板${node.config.switchId} 按键${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
289
+ // 发送MQTT命令到继电器
290
+ node.sendMqttCommand(buttonEvent.state);
357
291
  } else if (buttonEvent.type === 'multi') {
358
- // 多键按下
359
- const buttonIndex = node.config.buttonNumber - 1; // 转换为0-7索引
360
- if (buttonIndex >= 0 && buttonIndex < 8) {
361
- const state = buttonEvent.buttonStates[buttonIndex];
362
- node.log(`匹配成功!开关${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${state ? 'ON' : 'OFF'}`);
363
- // 发送MQTT命令到继电器
364
- node.sendMqttCommand(state);
365
- }
292
+ node.log(`匹配成功!面板${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
293
+ // 发送MQTT命令到继电器
294
+ node.sendMqttCommand(buttonEvent.state);
366
295
  }
367
296
  } else {
368
- node.log(`开关ID不匹配: 收到${buttonEvent.deviceAddr} 期望${node.config.switchId}`);
297
+ if (buttonEvent.raw.localAddr !== node.config.switchId) {
298
+ node.log(`面板地址不匹配: 收到${buttonEvent.raw.localAddr} 期望${node.config.switchId}`);
299
+ } else {
300
+ node.log(`按键编号不匹配: 收到${actualButtonNumber} 期望${node.config.buttonNumber}`);
301
+ }
369
302
  }
370
303
  } catch (err) {
371
304
  node.error(`解析RS-485数据失败: ${err.message}`);
@@ -373,19 +306,27 @@ module.exports = function(RED) {
373
306
  }
374
307
  };
375
308
 
376
- // 发送MQTT命令到继电器(入队)
309
+ // 发送命令到继电器(通过MQTT,由主站节点统一处理)
377
310
  node.sendMqttCommand = function(state) {
378
311
  if (!node.mqttClient || !node.mqttClient.connected) {
379
312
  node.warn('MQTT未连接,无法发送命令');
380
313
  return;
381
314
  }
382
-
383
- // 加入命令队列
384
- node.commandQueue.push(state);
385
- node.log(`MQTT命令已入队,队列长度: ${node.commandQueue.length}`);
386
-
387
- // 启动队列处理
388
- node.processCommandQueue();
315
+
316
+ // 直接发送MQTT命令(不使用队列,立即发送)
317
+ // 主站节点会处理去重和防抖
318
+ const commandTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/set`;
319
+ const payload = state ? 'ON' : 'OFF';
320
+
321
+ node.mqttClient.publish(commandTopic, payload, { qos: 1 }, (err) => {
322
+ if (err) {
323
+ node.error(`MQTT命令发送失败: ${err.message}`);
324
+ } else {
325
+ node.log(`MQTT命令已发送: ${payload} → ${commandTopic}`);
326
+ // 不立即发送LED反馈,等待MQTT状态消息确认后再发送
327
+ // 这样可以确保继电器真的动作了,避免重复发送
328
+ }
329
+ });
389
330
  };
390
331
 
391
332
  // 处理命令队列(防止多个按键同时按下造成冲突)
@@ -396,8 +337,20 @@ module.exports = function(RED) {
396
337
 
397
338
  node.isProcessingCommand = true;
398
339
 
340
+ // 清理过期队列项
341
+ const now = Date.now();
342
+ node.commandQueue = node.commandQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
343
+
399
344
  while (node.commandQueue.length > 0) {
400
- const state = node.commandQueue.shift();
345
+ const item = node.commandQueue.shift();
346
+
347
+ // 再次检查是否过期
348
+ if (Date.now() - item.timestamp >= node.queueTimeout) {
349
+ node.warn(`丢弃过期命令(${Date.now() - item.timestamp}ms)`);
350
+ continue;
351
+ }
352
+
353
+ const state = item.state;
401
354
  const command = state ? 'ON' : 'OFF';
402
355
 
403
356
  try {
@@ -429,57 +382,97 @@ module.exports = function(RED) {
429
382
 
430
383
  // 发送控制指令到物理开关面板(控制指示灯等)- 入队
431
384
  node.sendCommandToPanel = function(state) {
432
- if (!node.isRs485Connected) {
433
- node.warn('RS-485未连接,无法发送指示灯反馈');
385
+ // 检查连接状态
386
+ if (!node.serialPortConfig || !node.serialPortConfig.connection) {
387
+ // 初始化期间静默警告
388
+ if (!node.isInitializing) {
389
+ node.warn('RS-485连接未建立,无法发送指示灯反馈');
390
+ }
434
391
  return;
435
392
  }
436
-
437
- // 加入LED反馈队列
438
- node.ledFeedbackQueue.push(state);
439
-
393
+
394
+ // 清理过期队列项(超过3秒)
395
+ const now = Date.now();
396
+ node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
397
+
398
+ // 加入LED反馈队列(带时间戳)
399
+ // 注意:这里不指定协议类型,在发送时根据情况选择
400
+ node.ledFeedbackQueue.push({ state, timestamp: now });
401
+
440
402
  // 启动队列处理
441
403
  node.processLedFeedbackQueue();
442
404
  };
443
405
 
444
- // 处理LED反馈队列(40ms间隔发送)
406
+ // 处理LED反馈队列(基于面板ID的固定延迟)
445
407
  node.processLedFeedbackQueue = async function() {
446
408
  if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
447
409
  return;
448
410
  }
449
411
 
412
+ // 初始化期间不处理LED反馈(避免部署时大量LED同时发送)
413
+ if (node.isInitializing) {
414
+ return;
415
+ }
416
+
450
417
  node.isProcessingLedFeedback = true;
451
418
 
419
+ // 清理过期队列项
420
+ const now = Date.now();
421
+ node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
422
+
423
+ // 基于面板ID的固定延迟,避免多个节点同时写入TCP冲突
424
+ // 面板1=100ms, 面板2=200ms, 面板3=300ms, 面板4=400ms...
425
+ // 这样不同面板的LED反馈永远不会同时发送
426
+ const fixedDelay = node.config.switchId * 100;
427
+ if (fixedDelay > 0) {
428
+ await new Promise(resolve => setTimeout(resolve, fixedDelay));
429
+ }
430
+
452
431
  while (node.ledFeedbackQueue.length > 0) {
453
- const state = node.ledFeedbackQueue.shift();
432
+ const item = node.ledFeedbackQueue.shift();
433
+
434
+ // 再次检查是否过期
435
+ if (Date.now() - item.timestamp >= node.queueTimeout) {
436
+ node.warn(`丢弃过期LED反馈(${Date.now() - item.timestamp}ms)`);
437
+ continue;
438
+ }
439
+
440
+ const state = item.state;
454
441
 
455
442
  try {
456
- // 使用轻量级协议构建单灯控制指令
457
- // localAddr=0x01(本机),deviceAddr=开关ID,channel=按钮编号,state=开关状态
458
- const command = protocol.buildSingleLightCommand(
459
- 0x01, // 本机地址(网关地址)
460
- node.config.switchId,
461
- node.config.buttonNumber,
443
+ // 根据按键编号反推设备地址和通道(Symi协议)
444
+ // 例如:按键1→devAddr=1,channel=1;按键5→devAddr=2,channel=1
445
+ const deviceAddr = Math.floor((node.config.buttonNumber - 1) / 4) + 1;
446
+ const channel = ((node.config.buttonNumber - 1) % 4) + 1;
447
+
448
+ // 使用轻量级协议构建LED反馈指令(REPORT类型)
449
+ // 协议规定:面板发送0x03(SET)按键事件,主机应该用0x04(REPORT)反馈LED状态
450
+ // localAddr=面板地址,deviceAddr和channel根据按键编号计算
451
+ const command = protocol.buildSingleLightReport(
452
+ node.config.switchId, // 本地地址(面板地址)
453
+ deviceAddr, // 设备地址(根据按键编号计算)
454
+ channel, // 通道(根据按键编号计算)
462
455
  state
463
456
  );
464
-
465
- // 发送到RS-485总线
466
- if (node.config.connectionType === "tcp") {
467
- // TCP模式
468
- if (node.rs485Client._port && node.rs485Client._port.socket) {
469
- node.rs485Client._port.socket.write(command);
470
- node.log(`LED反馈已发送(TCP): 开关${node.config.switchId} 按钮${node.config.buttonNumber} ${state ? 'ON' : 'OFF'}`);
471
- }
457
+
458
+ // 发送到RS-485总线(使用共享连接配置)
459
+ if (node.serialPortConfig) {
460
+ node.serialPortConfig.write(command, (err) => {
461
+ if (err) {
462
+ node.error(`LED反馈发送失败: ${err.message}`);
463
+ } else {
464
+ const hexCmd = Array.from(command).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
465
+ const connType = node.serialPortConfig.connectionType === 'tcp' ? 'TCP' : '串口';
466
+ node.log(`LED反馈已发送(${connType}/REPORT): 面板${node.config.switchId} 按键${node.config.buttonNumber} ${state ? 'ON' : 'OFF'} [${hexCmd}]`);
467
+ }
468
+ });
472
469
  } else {
473
- // 串口模式
474
- if (node.rs485Client._port) {
475
- node.rs485Client._port.write(command);
476
- node.log(`LED反馈已发送(串口): 开关${node.config.switchId} 按钮${node.config.buttonNumber} ${state ? 'ON' : 'OFF'}`);
477
- }
470
+ node.warn('RS-485连接未配置,无法发送LED反馈');
478
471
  }
479
-
480
- // 队列间隔40ms
472
+
473
+ // 队列间隔100ms(确保RS-485总线有足够时间处理)
481
474
  if (node.ledFeedbackQueue.length > 0) {
482
- await new Promise(resolve => setTimeout(resolve, 40));
475
+ await new Promise(resolve => setTimeout(resolve, 100));
483
476
  }
484
477
  } catch (err) {
485
478
  node.error(`发送LED反馈失败: ${err.message}`);
@@ -537,9 +530,7 @@ module.exports = function(RED) {
537
530
  if (node.config.mqttUsername) {
538
531
  options.username = node.config.mqttUsername;
539
532
  options.password = node.config.mqttPassword;
540
- node.log(`MQTT认证: 用户名=${node.config.mqttUsername}, 密码已设置=${!!node.config.mqttPassword}`);
541
- } else {
542
- node.warn('MQTT未配置认证信息,如果broker需要认证将会连接失败');
533
+ node.log(`MQTT认证: 用户名=${node.config.mqttUsername}`);
543
534
  }
544
535
 
545
536
  // 尝试连接函数
@@ -558,6 +549,7 @@ module.exports = function(RED) {
558
549
  lastConnectAttempt = Date.now();
559
550
 
560
551
  node.mqttClient.on('connect', () => {
552
+ node.mqttConnected = true;
561
553
  node.log(`MQTT已连接: ${brokerUrl}`);
562
554
 
563
555
  // 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
@@ -670,13 +662,29 @@ module.exports = function(RED) {
670
662
  node.mqttClient.on('message', (topic, message) => {
671
663
  if (topic === node.stateTopic) {
672
664
  const state = message.toString();
673
- node.currentState = (state === 'ON' || state === 'true' || state === '1');
674
-
665
+ const newState = (state === 'ON' || state === 'true' || state === '1');
666
+
667
+ // 防死循环:如果状态在100ms内没有变化,跳过LED反馈
668
+ const now = Date.now();
669
+ const timeSinceLastChange = now - node.lastStateChange.timestamp;
670
+ const stateChanged = (newState !== node.lastStateChange.value);
671
+
672
+ if (!stateChanged && timeSinceLastChange < 100) {
673
+ // 状态未变化且时间间隔太短,跳过LED反馈(避免死循环)
674
+ node.log(`跳过LED反馈(状态未变化,间隔${timeSinceLastChange}ms)`);
675
+ return;
676
+ }
677
+
678
+ // 更新状态
679
+ node.currentState = newState;
680
+ node.lastStateChange.timestamp = now;
681
+ node.lastStateChange.value = newState;
682
+
675
683
  node.updateStatus();
676
-
684
+
677
685
  // 发送控制指令到物理开关面板(同步指示灯等)
678
686
  node.sendCommandToPanel(node.currentState);
679
-
687
+
680
688
  // 输出状态
681
689
  node.send({
682
690
  payload: node.currentState,
@@ -751,23 +759,46 @@ module.exports = function(RED) {
751
759
  node.reconnectTimer = null;
752
760
  }
753
761
 
754
- // 关闭RS-485连接
755
- if (node.rs485Client && node.isRs485Connected) {
762
+ // 清理队列(释放内存)
763
+ node.commandQueue = [];
764
+ node.ledFeedbackQueue = [];
765
+ node.frameBuffer = Buffer.alloc(0);
766
+
767
+ // 注销RS-485数据监听器(不关闭连接,由配置节点管理)
768
+ if (node.serialPortConfig && node.serialDataListener) {
756
769
  try {
757
- node.rs485Client.close(() => {
758
- node.log('RS-485连接已关闭');
759
- });
770
+ node.serialPortConfig.unregisterDataListener(node.serialDataListener);
771
+ node.log('RS-485数据监听器已注销');
760
772
  } catch (err) {
761
- node.warn(`关闭RS-485连接时出错: ${err.message}`);
773
+ node.warn(`注销RS-485监听器时出错: ${err.message}`);
762
774
  }
775
+ node.serialDataListener = null;
763
776
  }
764
777
 
765
778
  // 关闭MQTT连接
766
- if (node.mqttClient && node.mqttClient.connected) {
767
- node.mqttClient.end(false, () => {
768
- node.log('MQTT连接已关闭');
779
+ if (node.mqttClient) {
780
+ try {
781
+ if (node.mqttClient.connected) {
782
+ // 移除所有监听器(防止内存泄漏)
783
+ node.mqttClient.removeAllListeners();
784
+ node.mqttClient.end(false, () => {
785
+ node.log('MQTT连接已关闭');
786
+ node.mqttClient = null;
787
+ node.mqttConnected = false;
788
+ done();
789
+ });
790
+ } else {
791
+ node.mqttClient.removeAllListeners();
792
+ node.mqttClient = null;
793
+ node.mqttConnected = false;
794
+ done();
795
+ }
796
+ } catch (err) {
797
+ node.warn(`关闭MQTT连接时出错: ${err.message}`);
798
+ node.mqttClient = null;
799
+ node.mqttConnected = false;
769
800
  done();
770
- });
801
+ }
771
802
  } else {
772
803
  done();
773
804
  }