node-red-contrib-symi-modbus 2.6.6 → 2.6.8

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.
@@ -139,23 +139,56 @@ module.exports = function(RED) {
139
139
  node.isClosing = false;
140
140
  node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
141
141
  node.lastMqttErrorLog = 0; // MQTT错误日志时间
142
- node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
142
+ node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5分钟(降低到5分钟,更快发现问题)
143
+ node.consecutiveErrors = {}; // 记录每个从站的连续错误次数
143
144
  node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
144
145
  node.lastWriteTime = {}; // 记录每个从站的最后写入时间
145
146
  node.pausePolling = false; // 暂停轮询标志(从站上报时暂停)
146
147
  node.pollingPausedCount = 0; // 暂停轮询计数器
147
148
  node._discoveryPublished = false; // Discovery发布标志(避免重复)
149
+
150
+ // 定期清理机制(每小时清理一次,防止内存泄漏)
151
+ node.cleanupTimer = setInterval(() => {
152
+ // 清理过期的错误日志记录
153
+ const now = Date.now();
154
+ const cleanupThreshold = 24 * 60 * 60 * 1000; // 24小时
155
+
156
+ Object.keys(node.lastErrorLog).forEach(slaveId => {
157
+ if (now - node.lastErrorLog[slaveId] > cleanupThreshold) {
158
+ delete node.lastErrorLog[slaveId];
159
+ }
160
+ });
161
+
162
+ Object.keys(node.consecutiveErrors).forEach(slaveId => {
163
+ if (now - (node.deviceStates[slaveId]?.lastTimeoutTime || 0) > cleanupThreshold) {
164
+ delete node.consecutiveErrors[slaveId];
165
+ }
166
+ });
167
+
168
+ node.debug(`内存清理完成(错误日志记录数: ${Object.keys(node.lastErrorLog).length})`);
169
+ }, 60 * 60 * 1000); // 每小时执行一次
148
170
 
149
171
  // 更新节点状态显示
150
172
  node.updateNodeStatus = function() {
151
173
  const modbusStatus = node.isConnected ? "Modbus-OK" : "Modbus-ERR";
152
- const mqttStatus = node.mqttConnected ? "MQTT-OK" : "MQTT-ERR";
153
-
154
- if (node.isConnected && node.mqttConnected) {
174
+
175
+ // 如果MQTT未启用,显示"本地模式"
176
+ let mqttStatus;
177
+ if (!node.config.enableMqtt) {
178
+ mqttStatus = "本地模式";
179
+ } else {
180
+ mqttStatus = node.mqttConnected ? "MQTT-OK" : "MQTT-ERR";
181
+ }
182
+
183
+ // 状态显示逻辑
184
+ if (node.isConnected && (node.mqttConnected || !node.config.enableMqtt)) {
185
+ // Modbus已连接 且 (MQTT已连接 或 MQTT未启用)
155
186
  node.status({fill: "green", shape: "dot", text: `${modbusStatus} ${mqttStatus}`});
156
187
  } else if (node.isConnected || node.mqttConnected) {
188
+ // Modbus已连接 或 MQTT已连接(部分连接)
157
189
  node.status({fill: "yellow", shape: "ring", text: `${modbusStatus} ${mqttStatus}`});
158
190
  } else {
191
+ // 全部未连接
159
192
  node.status({fill: "red", shape: "ring", text: `${modbusStatus} ${mqttStatus}`});
160
193
  }
161
194
  };
@@ -169,8 +202,7 @@ module.exports = function(RED) {
169
202
  error: null,
170
203
  config: slave, // 保存该从站的配置
171
204
  initialPublished: false, // 标记是否已发布初始状态
172
- timeoutCount: 0, // 连续超时次数
173
- isIgnored: false, // 是否被临时忽略(超时次数过多时)
205
+ timeoutCount: 0, // 连续超时次数(仅用于统计)
174
206
  lastTimeoutTime: 0 // 最后一次超时时间
175
207
  };
176
208
  });
@@ -244,10 +276,17 @@ module.exports = function(RED) {
244
276
  node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
245
277
  }
246
278
 
247
- // 设置超时时间(串口需要更长的超时时间)
248
- const timeout = node.config.connectionType === "serial" ? 10000 : 5000;
279
+ // 设置超时时间(串口需要更长的超时时间,但不能太长以免影响轮询)
280
+ const timeout = node.config.connectionType === "serial" ? 3000 : 2000;
249
281
  node.client.setTimeout(timeout);
250
282
  node.log(`Modbus超时设置: ${timeout}ms`);
283
+
284
+ // 设置错误处理器,防止未捕获的错误导致进程崩溃
285
+ if (node.client._port) {
286
+ node.client._port.on('error', (err) => {
287
+ node.warn(`串口错误(已忽略): ${err.message}`);
288
+ });
289
+ }
251
290
 
252
291
  node.isConnected = true;
253
292
  node.reconnectAttempts = 0; // 重置重连计数
@@ -259,12 +298,19 @@ module.exports = function(RED) {
259
298
  node.lastMqttErrorLog = 0;
260
299
  node._discoveryPublished = false; // 重置Discovery发布标志
261
300
 
262
- // 添加Symi按键事件监听(串口模式)
263
- if (node.config.connectionType === "serial" && node.client._port) {
301
+ // 添加Symi按键事件监听(串口和TCP模式都支持)
302
+ if (node.client._port) {
303
+ // 串口模式:直接监听串口数据
264
304
  node.client._port.on('data', (data) => {
265
305
  node.handleSymiButtonEvent(data);
266
306
  });
267
307
  node.log('已启用Symi按键事件监听(串口模式)');
308
+ } else if (node.client._client && node.config.connectionType === "tcp") {
309
+ // TCP模式:监听TCP socket数据
310
+ node.client._client.on('data', (data) => {
311
+ node.handleSymiButtonEvent(data);
312
+ });
313
+ node.log('已启用Symi按键事件监听(TCP模式)');
268
314
  }
269
315
 
270
316
  // 立即启动轮询(不等待MQTT连接)
@@ -372,7 +418,7 @@ module.exports = function(RED) {
372
418
  // 连接MQTT(带智能重试和fallback)
373
419
  node.connectMqtt = function() {
374
420
  if (!node.config.enableMqtt) {
375
- node.log('MQTT未启用 - 使用纯本地模式(仅串口通信)');
421
+ node.log('MQTT未启用 - 使用本地模式(内部事件通信)');
376
422
  node.log('提示:如需Home Assistant集成,请在节点配置中启用MQTT');
377
423
  return;
378
424
  }
@@ -729,6 +775,13 @@ module.exports = function(RED) {
729
775
  // 开始轮询(使用递归调用而非定时器,避免并发)
730
776
  node.startPolling = function() {
731
777
  if (node.isPolling) {
778
+ node.log('轮询已在运行中,跳过重复启动');
779
+ return;
780
+ }
781
+
782
+ // 检查从站配置
783
+ if (!node.config.slaves || node.config.slaves.length === 0) {
784
+ node.error('未配置从站设备,无法启动轮询');
732
785
  return;
733
786
  }
734
787
 
@@ -737,22 +790,37 @@ module.exports = function(RED) {
737
790
  node.lastMqttErrorLog = 0;
738
791
 
739
792
  const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd},间隔${s.pollInterval}ms)`).join(', ');
740
- node.log(`开始轮询 ${node.config.slaves.length} 个从站设备: ${slaveList}`);
793
+ node.log(`========== 开始轮询 ${node.config.slaves.length} 个从站设备 ==========`);
794
+ node.log(`从站列表: ${slaveList}`);
741
795
  node.log(`Modbus连接: ${node.isConnected ? '已连接' : '未连接'}, MQTT连接: ${node.mqttConnected ? '已连接' : '未连接'}`);
742
796
  node.currentSlaveIndex = 0;
743
797
  node.isPolling = true;
798
+ node.pollingLoopCount = 0; // 轮询循环计数器
744
799
 
745
800
  // 使用递归调用实现轮询,确保不会并发
746
801
  const pollLoop = async () => {
747
- if (!node.isPolling || !node.isConnected || node.isClosing) {
802
+ if (!node.isPolling) {
803
+ node.log('轮询已停止(isPolling=false)');
804
+ return;
805
+ }
806
+
807
+ if (!node.isConnected) {
808
+ node.log('轮询已停止(Modbus未连接)');
809
+ return;
810
+ }
811
+
812
+ if (node.isClosing) {
813
+ node.log('轮询已停止(节点正在关闭)');
748
814
  return;
749
815
  }
750
816
 
751
817
  // 获取当前从站配置(在轮询前获取,确保使用正确的间隔)
752
818
  const currentSlave = node.config.slaves[node.currentSlaveIndex];
753
819
  if (!currentSlave) {
754
- node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
820
+ node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
755
821
  node.currentSlaveIndex = 0;
822
+ // 继续轮询,不要中断
823
+ node.pollTimer = setTimeout(pollLoop, 200);
756
824
  return;
757
825
  }
758
826
 
@@ -761,14 +829,26 @@ module.exports = function(RED) {
761
829
  try {
762
830
  await node.pollNextSlave();
763
831
  } catch (err) {
764
- node.error(`轮询错误: ${err.message}`);
832
+ // 捕获所有异常,防止轮询中断(只记录到日志文件,不输出到调试窗口)
833
+ node.log(`轮询异常(已忽略,继续轮询): ${err.message}`);
765
834
  }
766
835
 
767
- // 等待指定间隔后继续下一次轮询
836
+ // 移动到下一个从站
837
+ node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
838
+
839
+ // 每完成一轮轮询,计数器加1
840
+ if (node.currentSlaveIndex === 0) {
841
+ node.pollingLoopCount++;
842
+ // 轮询计数器静默运行,不输出日志(避免日志过多)
843
+ // 轮询会永久运行,无需频繁确认
844
+ }
845
+
846
+ // 等待指定间隔后继续下一次轮询(确保轮询永不停止)
768
847
  node.pollTimer = setTimeout(pollLoop, interval);
769
848
  };
770
849
 
771
850
  // 立即开始轮询
851
+ node.log('立即启动轮询循环...');
772
852
  pollLoop();
773
853
  };
774
854
 
@@ -797,20 +877,19 @@ module.exports = function(RED) {
797
877
  // 获取当前从站配置
798
878
  const slave = node.config.slaves[node.currentSlaveIndex];
799
879
  if (!slave) {
800
- node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
880
+ node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
801
881
  node.currentSlaveIndex = 0;
802
882
  return;
803
883
  }
804
884
 
805
885
  const slaveId = slave.address;
886
+
887
+ // 轮询静默运行,不输出日志(避免日志过多)
806
888
  const deviceState = node.deviceStates[slaveId];
807
889
 
808
- // 检查该从站是否被临时忽略(连续超时过多)
809
- if (deviceState.isIgnored) {
810
- // 移动到下一个从站
811
- node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
812
- return;
813
- }
890
+ // 不再跳过任何从站,即使连续超时也继续轮询
891
+ // 原则:轮询永不停止,即使从站不响应也不影响总线轮询机制
892
+ // 只记录错误日志,不跳过轮询
814
893
 
815
894
  const coilCount = slave.coilEnd - slave.coilStart + 1;
816
895
 
@@ -818,15 +897,13 @@ module.exports = function(RED) {
818
897
  const lastWrite = node.lastWriteTime[slaveId] || 0;
819
898
  const timeSinceWrite = Date.now() - lastWrite;
820
899
  if (timeSinceWrite < 100) {
821
- // 移动到下一个从站
822
- node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
900
+ // 跳过该从站(不移动索引,由pollLoop统一管理)
823
901
  return;
824
902
  }
825
903
 
826
904
  // 检查锁状态(如果有写操作正在进行,跳过本次轮询)
827
905
  if (node.modbusLock) {
828
- // 移动到下一个从站
829
- node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
906
+ // 跳过该从站(不移动索引,由pollLoop统一管理)
830
907
  return;
831
908
  }
832
909
 
@@ -862,6 +939,17 @@ module.exports = function(RED) {
862
939
  coil: coilIndex,
863
940
  value: newValue
864
941
  });
942
+
943
+ // 广播状态变化事件(用于LED反馈)
944
+ // 只在状态真正改变时广播(不包括首次轮询)
945
+ if (!isFirstPoll && oldValue !== newValue) {
946
+ RED.events.emit('modbus:coilStateChanged', {
947
+ slave: slaveId,
948
+ coil: coilIndex,
949
+ value: newValue,
950
+ source: 'polling'
951
+ });
952
+ }
865
953
  }
866
954
  }
867
955
 
@@ -877,7 +965,6 @@ module.exports = function(RED) {
877
965
 
878
966
  // 轮询成功,重置超时计数
879
967
  node.deviceStates[slaveId].timeoutCount = 0;
880
- node.deviceStates[slaveId].isIgnored = false;
881
968
 
882
969
  // 输出消息
883
970
  const output = {
@@ -904,6 +991,12 @@ module.exports = function(RED) {
904
991
  // 释放锁
905
992
  node.modbusLock = false;
906
993
 
994
+ // 确保deviceState存在(防止未定义错误)
995
+ if (!node.deviceStates[slaveId]) {
996
+ node.log(`从站${slaveId}状态未初始化,跳过错误记录`);
997
+ return;
998
+ }
999
+
907
1000
  node.deviceStates[slaveId].error = err.message;
908
1001
  node.deviceStates[slaveId].lastTimeoutTime = Date.now();
909
1002
 
@@ -920,26 +1013,18 @@ module.exports = function(RED) {
920
1013
  );
921
1014
 
922
1015
  if (isTimeout) {
923
- // 增加超时计数
1016
+ // 增加超时计数(仅用于统计,不影响轮询)
924
1017
  node.deviceStates[slaveId].timeoutCount++;
925
1018
 
926
- // 连续超时5次后临时忽略该从站(避免拖慢其他从站)
927
- if (node.deviceStates[slaveId].timeoutCount >= 5) {
928
- node.deviceStates[slaveId].isIgnored = true;
929
- if (shouldLog) {
930
- node.warn(`从站${slaveId}连续超时${node.deviceStates[slaveId].timeoutCount}次,临时忽略该从站(重启或重新部署后恢复)`);
931
- node.lastErrorLog[slaveId] = now;
932
- }
933
- } else {
934
- if (shouldLog) {
935
- node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [超时${node.deviceStates[slaveId].timeoutCount}/5]`);
936
- node.lastErrorLog[slaveId] = now;
937
- }
1019
+ // 只记录日志到文件,不输出到调试窗口(避免日志刷屏)
1020
+ if (shouldLog) {
1021
+ node.log(`从站${slaveId}轮询超时: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [连续超时${node.deviceStates[slaveId].timeoutCount}次]`);
1022
+ node.lastErrorLog[slaveId] = now;
938
1023
  }
939
1024
  } else {
940
- // 非超时错误,记录日志
1025
+ // 非超时错误(如CRC错误、总线干扰),记录日志到文件,不输出到调试窗口
941
1026
  if (shouldLog) {
942
- node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
1027
+ node.log(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
943
1028
  node.lastErrorLog[slaveId] = now;
944
1029
  }
945
1030
  }
@@ -992,28 +1077,32 @@ module.exports = function(RED) {
992
1077
  }
993
1078
  }
994
1079
 
995
- // 移动到下一个从站
996
- node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
1080
+ // 注意:不在这里移动索引,由pollLoop统一管理
1081
+ // 这样可以确保每个从站都按照正确的间隔轮询
997
1082
  };
998
1083
 
999
1084
  // 处理Symi按键事件(私有协议)
1085
+ // 静默处理:只处理本节点相关的数据,忽略总线上的其他数据
1000
1086
  node.handleSymiButtonEvent = function(data) {
1001
1087
  try {
1002
- // 解析Symi协议帧
1088
+ // 解析Symi协议帧(带CRC校验)
1003
1089
  const frame = protocol.parseFrame(data);
1004
1090
  if (!frame) {
1005
- return; // 不是有效的Symi帧,忽略
1091
+ // 不是有效的Symi帧(CRC校验失败或格式错误),静默忽略
1092
+ return;
1006
1093
  }
1007
1094
 
1008
1095
  // 只处理SET类型(0x03)的按键事件,忽略REPORT类型(0x04)
1009
1096
  // REPORT类型是面板对我们指令的确认,不是按键事件
1010
1097
  if (frame.dataType !== 0x03) {
1011
- return; // 不是按键事件
1098
+ // 不是按键事件,静默忽略
1099
+ return;
1012
1100
  }
1013
1101
 
1014
1102
  // 检查是否是灯光设备
1015
1103
  if (frame.deviceType !== 0x01) {
1016
- return; // 不是灯光设备
1104
+ // 不是灯光设备,静默忽略
1105
+ return;
1017
1106
  }
1018
1107
 
1019
1108
  // 提取按键信息
@@ -1021,8 +1110,6 @@ module.exports = function(RED) {
1021
1110
  const channel = frame.channel; // 通道号(1-8)
1022
1111
  const state = frame.opInfo[0] === 0x01; // 状态(1=开,0=关)
1023
1112
 
1024
- node.debug(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
1025
-
1026
1113
  // 查找对应的从站和线圈
1027
1114
  // 假设:设备地址1对应从站10,设备地址2对应从站11,以此类推
1028
1115
  // 通道号直接对应线圈号(1-8 → 0-7)
@@ -1032,16 +1119,18 @@ module.exports = function(RED) {
1032
1119
  // 检查从站是否在配置中
1033
1120
  const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
1034
1121
  if (!slaveConfig) {
1035
- node.warn(`Symi按键事件: 从站${slaveId}未配置,忽略`);
1122
+ // 从站未配置,静默忽略(不是本节点的数据)
1036
1123
  return;
1037
1124
  }
1038
1125
 
1039
1126
  // 检查线圈是否在范围内
1040
1127
  if (coilNumber < slaveConfig.coilStart || coilNumber > slaveConfig.coilEnd) {
1041
- node.warn(`Symi按键事件: 线圈${coilNumber}不在从站${slaveId}的范围内(${slaveConfig.coilStart}-${slaveConfig.coilEnd}),忽略`);
1128
+ // 线圈不在范围内,静默忽略(不是本节点的数据)
1042
1129
  return;
1043
1130
  }
1044
1131
 
1132
+ // 到这里说明是本节点的数据,输出日志
1133
+ node.debug(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
1045
1134
  node.debug(`Symi按键映射: 设备${deviceAddr}通道${channel} → 从站${slaveId}线圈${coilNumber}`);
1046
1135
 
1047
1136
  // 写入线圈(异步,不阻塞)
@@ -1055,11 +1144,16 @@ module.exports = function(RED) {
1055
1144
  if (node.client._port && node.client._port.write) {
1056
1145
  node.client._port.write(responseFrame);
1057
1146
  node.log(`Symi应答已发送(REPORT): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
1147
+ } else if (node.client._client && node.client._client.write) {
1148
+ // TCP模式
1149
+ node.client._client.write(responseFrame);
1150
+ node.log(`Symi应答已发送(REPORT/TCP): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
1058
1151
  }
1059
1152
 
1060
1153
  } catch (err) {
1061
1154
  // 静默处理错误,避免干扰Modbus通信
1062
- // node.warn(`Symi按键事件处理错误: ${err.message}`);
1155
+ // 总线上可能有各种数据,不是所有数据都能解析成功
1156
+ // 只有真正的错误才输出日志
1063
1157
  }
1064
1158
  };
1065
1159
 
@@ -1092,17 +1186,21 @@ module.exports = function(RED) {
1092
1186
  try {
1093
1187
  // 设置锁
1094
1188
  node.modbusLock = true;
1095
-
1189
+
1096
1190
  node.client.setID(slaveId);
1191
+
1192
+ // 记录写入操作(帮助追踪总线数据来源)
1193
+ node.log(`写入线圈: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1194
+
1097
1195
  await node.client.writeCoil(coil, value);
1098
-
1196
+
1099
1197
  // 记录写入时间(用于暂停轮询)
1100
1198
  node.lastWriteTime[slaveId] = Date.now();
1101
-
1199
+
1102
1200
  // 更新本地状态
1103
1201
  node.deviceStates[slaveId].coils[coil] = value;
1104
-
1105
- node.debug(`写入成功: 从站${slaveId} 线圈${coil} = ${value}`);
1202
+
1203
+ node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1106
1204
 
1107
1205
  // 发布到MQTT和触发事件
1108
1206
  node.publishMqttState(slaveId, coil, value);
@@ -1213,43 +1311,89 @@ module.exports = function(RED) {
1213
1311
  }
1214
1312
  };
1215
1313
 
1216
- // 处理输入消息
1314
+ // 监听内部事件(从站开关节点发送的写入命令)
1315
+ // 这是免连线通信的核心机制
1316
+ node.internalEventHandler = async function(data) {
1317
+ if (!data || typeof data !== 'object') {
1318
+ return;
1319
+ }
1320
+
1321
+ const slave = parseInt(data.slave);
1322
+ const coil = parseInt(data.coil);
1323
+ const value = Boolean(data.value);
1324
+
1325
+ // 输出日志确认收到事件
1326
+ node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1327
+
1328
+ try {
1329
+ // 执行写入操作
1330
+ await node.writeSingleCoil(slave, coil, value);
1331
+
1332
+ // 写入成功后,广播状态变化事件(用于LED反馈)
1333
+ RED.events.emit('modbus:coilStateChanged', {
1334
+ slave: slave,
1335
+ coil: coil,
1336
+ value: value,
1337
+ source: 'master'
1338
+ });
1339
+
1340
+ node.log(`内部事件写入成功,已广播状态变化:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1341
+ } catch (err) {
1342
+ node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
1343
+ }
1344
+ };
1345
+
1346
+ // 注册内部事件监听器
1347
+ RED.events.on('modbus:writeCoil', node.internalEventHandler);
1348
+ node.log('已注册内部事件监听器(免连线通信)');
1349
+
1350
+ // 处理输入消息(保留兼容性,支持连线模式)
1217
1351
  node.on('input', function(msg) {
1218
1352
  if (!msg.payload || typeof msg.payload !== 'object') {
1219
1353
  return;
1220
1354
  }
1221
-
1355
+
1222
1356
  const cmd = msg.payload.cmd;
1223
-
1357
+
1224
1358
  switch(cmd) {
1225
1359
  case 'start':
1226
1360
  node.startPolling();
1227
1361
  break;
1228
-
1362
+
1229
1363
  case 'stop':
1230
1364
  node.stopPolling();
1231
1365
  break;
1232
-
1366
+
1233
1367
  case 'writeCoil':
1234
- if (msg.payload.slave && msg.payload.coil !== undefined && msg.payload.value !== undefined) {
1235
- node.writeSingleCoil(
1236
- msg.payload.slave,
1237
- msg.payload.coil,
1238
- msg.payload.value
1239
- );
1368
+ if (msg.payload.slave !== undefined && msg.payload.coil !== undefined && msg.payload.value !== undefined) {
1369
+ const slave = parseInt(msg.payload.slave);
1370
+ const coil = parseInt(msg.payload.coil);
1371
+ const value = Boolean(msg.payload.value);
1372
+
1373
+ node.writeSingleCoil(slave, coil, value).catch(err => {
1374
+ node.error(`连线模式写入失败: ${err.message}`);
1375
+ });
1376
+ } else {
1377
+ node.warn(`writeCoil命令参数不完整: slave=${msg.payload.slave}, coil=${msg.payload.coil}, value=${msg.payload.value}`);
1240
1378
  }
1241
1379
  break;
1242
-
1380
+
1243
1381
  case 'writeCoils':
1244
- if (msg.payload.slave && msg.payload.startCoil !== undefined && Array.isArray(msg.payload.values)) {
1245
- node.writeMultipleCoils(
1246
- msg.payload.slave,
1247
- msg.payload.startCoil,
1248
- msg.payload.values
1249
- );
1382
+ if (msg.payload.slave !== undefined && msg.payload.startCoil !== undefined && Array.isArray(msg.payload.values)) {
1383
+ const slave = parseInt(msg.payload.slave);
1384
+ const startCoil = parseInt(msg.payload.startCoil);
1385
+ const values = msg.payload.values.map(v => Boolean(v));
1386
+
1387
+ node.log(`接收到本地模式批量写入命令: 从站${slave} 起始线圈${startCoil} 共${values.length}个`);
1388
+
1389
+ node.writeMultipleCoils(slave, startCoil, values).catch(err => {
1390
+ node.error(`本地模式批量写入失败: ${err.message}`);
1391
+ });
1392
+ } else {
1393
+ node.warn(`writeCoils命令参数不完整`);
1250
1394
  }
1251
1395
  break;
1252
-
1396
+
1253
1397
  default:
1254
1398
  node.warn(`未知命令: ${cmd}`);
1255
1399
  }
@@ -1274,13 +1418,25 @@ module.exports = function(RED) {
1274
1418
  node.on('close', function(done) {
1275
1419
  node.isClosing = true;
1276
1420
  node.stopPolling();
1277
-
1421
+
1422
+ // 移除内部事件监听器
1423
+ if (node.internalEventHandler) {
1424
+ RED.events.removeListener('modbus:writeCoil', node.internalEventHandler);
1425
+ node.internalEventHandler = null;
1426
+ }
1427
+
1278
1428
  // 清除重连定时器
1279
1429
  if (node.reconnectTimer) {
1280
1430
  clearTimeout(node.reconnectTimer);
1281
1431
  node.reconnectTimer = null;
1282
1432
  }
1283
-
1433
+
1434
+ // 清除内存清理定时器
1435
+ if (node.cleanupTimer) {
1436
+ clearInterval(node.cleanupTimer);
1437
+ node.cleanupTimer = null;
1438
+ }
1439
+
1284
1440
  // 发布离线状态到MQTT(让HA知道设备离线)
1285
1441
  if (node.config.enableMqtt && node.mqttClient && node.mqttClient.connected) {
1286
1442
  node.publishOfflineStatus();
@@ -57,7 +57,8 @@
57
57
  <span style="font-size: 14px; font-weight: 600; color: #333;">RS-485连接配置</span>
58
58
  </label>
59
59
  <div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #fff3cd 0%, #fffbe6 100%); border-left: 4px solid #ffc107; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
60
- <strong>说明:</strong>多个从站开关节点可以共享同一个RS-485连接配置(支持TCP网关或串口)
60
+ <strong>说明:</strong>多个从站开关节点可以共享同一个RS-485连接配置(支持TCP网关或串口)<br>
61
+ <strong>批量更换:</strong>如需更换连接配置,请先创建新配置,然后在配置节点侧边栏中删除旧配置,系统会提示你选择替换配置
61
62
  </div>
62
63
  </div>
63
64