node-red-contrib-symi-modbus 2.7.5 → 2.7.7

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,10 +2,256 @@ module.exports = function(RED) {
2
2
  "use strict";
3
3
  const mqtt = require("mqtt");
4
4
  const protocol = require("./lightweight-protocol");
5
+ const meshProtocol = require("./mesh-protocol")(RED);
6
+ const storage = require('node-persist');
7
+ const path = require('path');
8
+ const os = require('os');
5
9
 
6
10
  // 全局防抖缓存:防止多个节点重复处理同一个按键事件
7
11
  const globalDebounceCache = new Map(); // key: "switchId-buttonNumber", value: timestamp
8
12
 
13
+ // 初始化Mesh设备持久化存储
14
+ const meshPersistDir = path.join(RED.settings.userDir || os.homedir() + '/.node-red', 'mesh-devices-persist');
15
+ let meshStorageInitialized = false;
16
+
17
+ async function initMeshStorage() {
18
+ if (meshStorageInitialized) {
19
+ return;
20
+ }
21
+ try {
22
+ await storage.init({
23
+ dir: meshPersistDir,
24
+ stringify: JSON.stringify,
25
+ parse: JSON.parse,
26
+ encoding: 'utf8',
27
+ logging: false,
28
+ ttl: false,
29
+ expiredInterval: 2 * 60 * 1000,
30
+ forgiveParseErrors: true
31
+ });
32
+ meshStorageInitialized = true;
33
+ RED.log.info(`Mesh设备持久化存储初始化成功: ${meshPersistDir}`);
34
+ } catch (err) {
35
+ RED.log.error(`Mesh设备持久化存储初始化失败: ${err.message}`);
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ // Mesh设备发现API(使用共享连接)
41
+ RED.httpAdmin.post('/symi-mesh/discover', async function(req, res) {
42
+ try {
43
+ const serialPortConfigId = req.body.serialPortConfig;
44
+ RED.log.info(`[Mesh设备发现] 收到请求,连接配置ID: ${serialPortConfigId}`);
45
+
46
+ if (!serialPortConfigId) {
47
+ RED.log.warn('[Mesh设备发现] 缺少连接配置参数');
48
+ return res.status(400).json({error: '缺少连接配置'});
49
+ }
50
+
51
+ // 获取配置节点
52
+ const serialPortConfig = RED.nodes.getNode(serialPortConfigId);
53
+ if (!serialPortConfig) {
54
+ RED.log.warn(`[Mesh设备发现] 连接配置节点不存在: ${serialPortConfigId}`);
55
+ return res.status(400).json({error: '连接配置不存在,请先创建RS-485连接配置'});
56
+ }
57
+
58
+ RED.log.info(`[Mesh设备发现] 配置节点类型: ${serialPortConfig.type || '未知'}`);
59
+ RED.log.info(`[Mesh设备发现] 连接类型: ${serialPortConfig.connectionType || '串口'}`);
60
+
61
+ // 初始化持久化存储
62
+ await initMeshStorage();
63
+
64
+ // 发送设备列表请求
65
+ const requestFrame = meshProtocol.buildGetDeviceListFrame();
66
+ RED.log.info(`[Mesh设备发现] 发送设备列表请求: ${requestFrame.toString('hex').toUpperCase()}`);
67
+
68
+ // 设置响应超时
69
+ let timeoutHandle;
70
+ let isCompleted = false;
71
+ let dataHandler = null;
72
+
73
+ const completeRequest = async (devices, reason) => {
74
+ if (isCompleted) return;
75
+ isCompleted = true;
76
+
77
+ clearTimeout(timeoutHandle);
78
+
79
+ // 注销数据监听器
80
+ if (dataHandler) {
81
+ serialPortConfig.unregisterDataListener(dataHandler);
82
+ RED.log.info(`[Mesh设备发现] 已注销数据监听器`);
83
+ }
84
+
85
+ if (devices.length === 0) {
86
+ RED.log.warn(`[Mesh设备发现] 未发现设备 (${reason || '超时'})`);
87
+ } else {
88
+ RED.log.info(`[Mesh设备发现] 发现 ${devices.length} 个设备`);
89
+
90
+ // 持久化保存设备列表(MAC地址 → 短地址映射)
91
+ const deviceMap = {};
92
+ devices.forEach(device => {
93
+ deviceMap[device.mac] = {
94
+ shortAddr: device.shortAddr,
95
+ type: device.type,
96
+ buttons: device.buttons,
97
+ lastUpdate: Date.now()
98
+ };
99
+ RED.log.info(` - MAC: ${device.mac}, 短地址: ${device.shortAddr}, 类型: ${device.type}, 按键数: ${device.buttons}`);
100
+ });
101
+
102
+ await storage.setItem('meshDeviceMap', deviceMap);
103
+ RED.log.info(`[Mesh设备发现] 设备列表已保存到持久化存储`);
104
+ }
105
+
106
+ res.json(devices);
107
+ };
108
+
109
+ timeoutHandle = setTimeout(() => {
110
+ RED.log.warn(`[Mesh设备发现] 5秒超时,未收到完整响应`);
111
+ completeRequest([], '超时');
112
+ }, 5000);
113
+
114
+ // 收集响应帧
115
+ const responseFrames = [];
116
+ let totalDevices = 0;
117
+ let receivedDevices = 0;
118
+ let buffer = Buffer.alloc(0);
119
+
120
+ dataHandler = (data) => {
121
+ // 拼接数据到缓冲区
122
+ buffer = Buffer.concat([buffer, data]);
123
+ RED.log.info(`[Mesh设备发现] 收到数据: ${data.toString('hex').toUpperCase()}, 缓冲区总长: ${buffer.length}`);
124
+
125
+ // 解析所有完整的帧
126
+ // 协议格式: 53 92 00 10 [总数] [索引] [MAC 6字节] [短地址 2字节] [vendor_id 2字节] [dev_type] [dev_sub_type] [online/status] [resv] [校验]
127
+ // 总长度: 1+1+1+1+16+1 = 21字节
128
+ while (buffer.length >= 21) {
129
+ // 查找帧头 0x53
130
+ const headerIndex = buffer.indexOf(0x53);
131
+ if (headerIndex === -1) {
132
+ buffer = Buffer.alloc(0);
133
+ break;
134
+ }
135
+
136
+ if (headerIndex > 0) {
137
+ buffer = buffer.slice(headerIndex);
138
+ }
139
+
140
+ if (buffer.length < 21) break;
141
+
142
+ // 检查是否是设备列表响应 (0x53 0x92 0x00 0x10)
143
+ if (buffer[1] !== 0x92 || buffer[2] !== 0x00 || buffer[3] !== 0x10) {
144
+ buffer = buffer.slice(1);
145
+ continue;
146
+ }
147
+
148
+ // 提取完整帧(21字节)
149
+ const frame = buffer.slice(0, 21);
150
+ buffer = buffer.slice(21);
151
+
152
+ // 解析帧数据
153
+ const deviceCount = frame[4]; // 设备总数
154
+ const index = frame[5]; // 索引 (00, 01, 02...)
155
+ const mac = Array.from(frame.slice(6, 12))
156
+ .map(b => b.toString(16).padStart(2, '0'))
157
+ .join(':');
158
+ const shortAddr = frame[12] | (frame[13] << 8); // 小端序
159
+ const vendorId = frame[14] | (frame[15] << 8); // 小端序
160
+ const devType = frame[16];
161
+ const devSubType = frame[17]; // 按键数
162
+ const online = frame[18];
163
+ const resv = frame[19];
164
+ const checksum = frame[20];
165
+
166
+ RED.log.info(`[Mesh设备发现] 收到响应帧,索引: ${index}/${deviceCount-1}, 完整帧: ${frame.toString('hex').toUpperCase()}`);
167
+
168
+ // 第一帧:记录设备总数
169
+ if (totalDevices === 0) {
170
+ totalDevices = deviceCount;
171
+ RED.log.info(`[Mesh设备发现] 设备总数: ${totalDevices}`);
172
+ if (totalDevices === 0) {
173
+ completeRequest([], '网关下无设备');
174
+ return;
175
+ }
176
+ }
177
+
178
+ // 只保存开关类型的设备(devType=0x01或0x02)
179
+ if ((devType === 0x01 || devType === 0x02) && devSubType > 0) {
180
+ responseFrames.push({
181
+ shortAddr,
182
+ mac,
183
+ type: devType,
184
+ buttons: devSubType
185
+ });
186
+ RED.log.info(` - 索引${index}: MAC=${mac}, 短地址=0x${shortAddr.toString(16).toUpperCase().padStart(4, '0')}, 类型=0x${devType.toString(16).toUpperCase()}, 按键数=${devSubType}, 在线=${online}`);
187
+ } else {
188
+ RED.log.debug(` - 索引${index}: 跳过非开关设备,MAC=${mac}, 类型=0x${devType.toString(16).toUpperCase()}`);
189
+ }
190
+
191
+ receivedDevices++;
192
+ RED.log.info(`[Mesh设备发现] 已收到 ${receivedDevices}/${totalDevices} 个设备数据帧`);
193
+
194
+ // 如果收到所有设备数据,返回结果
195
+ if (receivedDevices >= totalDevices) {
196
+ RED.log.info(`[Mesh设备发现] 所有设备数据接收完成,共${responseFrames.length}个开关设备`);
197
+ completeRequest(responseFrames, '完成');
198
+ return;
199
+ }
200
+ }
201
+ };
202
+
203
+ // 使用共享连接(TCP和串口都一样)
204
+ RED.log.info(`[Mesh设备发现] 使用共享连接(${serialPortConfig.connectionType})`);
205
+
206
+ // 注册数据监听器
207
+ serialPortConfig.registerDataListener(dataHandler);
208
+
209
+ // 发送请求
210
+ try {
211
+ serialPortConfig.write(requestFrame, (err) => {
212
+ if (err) {
213
+ RED.log.error(`[Mesh设备发现] 发送请求失败: ${err.message}`);
214
+ serialPortConfig.unregisterDataListener(dataHandler);
215
+ completeRequest([], `发送失败: ${err.message}`);
216
+ } else {
217
+ RED.log.info(`[Mesh设备发现] 请求已发送,等待响应...`);
218
+ }
219
+ });
220
+ } catch (err) {
221
+ RED.log.error(`[Mesh设备发现] 发送请求异常: ${err.message}`);
222
+ serialPortConfig.unregisterDataListener(dataHandler);
223
+ return res.status(500).json({error: `发送请求失败: ${err.message}`});
224
+ }
225
+
226
+ } catch (err) {
227
+ RED.log.error(`Mesh设备发现失败: ${err.message}`);
228
+ res.status(500).json({error: err.message});
229
+ }
230
+ });
231
+
232
+ // 获取已保存的Mesh设备列表API
233
+ RED.httpAdmin.get('/symi-mesh/devices', async function(req, res) {
234
+ try {
235
+ await initMeshStorage();
236
+ const deviceMap = await storage.getItem('meshDeviceMap') || {};
237
+
238
+ // 转换为数组格式
239
+ const devices = Object.keys(deviceMap).map(mac => {
240
+ return {
241
+ mac: mac,
242
+ shortAddr: deviceMap[mac].shortAddr,
243
+ type: deviceMap[mac].type,
244
+ buttons: deviceMap[mac].buttons
245
+ };
246
+ });
247
+
248
+ res.json(devices);
249
+ } catch (err) {
250
+ RED.log.error(`获取Mesh设备列表失败: ${err.message}`);
251
+ res.status(500).json({error: err.message});
252
+ }
253
+ });
254
+
9
255
  // 串口列表API - 支持Windows、Linux、macOS所有串口设备
10
256
  RED.httpAdmin.get('/modbus-slave-switch/serialports', async function(req, res) {
11
257
  try {
@@ -87,9 +333,15 @@ module.exports = function(RED) {
87
333
  mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
88
334
  // 开关面板配置
89
335
  switchBrand: config.switchBrand || "symi", // 面板品牌(默认亖米)
90
- buttonType: config.buttonType || "switch", // 按钮类型:switch=开关模式,scene=场景模式
336
+ buttonType: config.buttonType || "switch", // 按钮类型:switch=开关模式,scene=场景模式,mesh=Mesh模式
91
337
  switchId: parseInt(config.switchId) || 0, // 开关ID(0-255,物理面板地址)
92
338
  buttonNumber: parseInt(config.buttonNumber) || 1, // 按钮编号(1-8)
339
+ // Mesh模式配置
340
+ meshMacAddress: config.meshMacAddress || "", // Mesh设备MAC地址
341
+ meshShortAddress: parseInt(config.meshShortAddress) || 0, // Mesh设备短地址
342
+ meshButtonNumber: parseInt(config.meshButtonNumber) || 1, // Mesh按键编号(1-6)
343
+ meshTotalButtons: parseInt(config.meshTotalButtons) || 1, // Mesh开关总路数(1-6)
344
+ // 目标继电器配置
93
345
  targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
94
346
  targetCoilNumber: modbusCoilNumber // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
95
347
  };
@@ -101,6 +353,28 @@ module.exports = function(RED) {
101
353
  node.lastMqttErrorLog = 0; // MQTT错误日志时间
102
354
  node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
103
355
 
356
+ // Mesh模式状态缓存
357
+ node.meshCurrentStates = null; // Mesh设备当前状态(用于保持其他路不变)
358
+
359
+ // Mesh模式:从持久化存储更新短地址(如果MAC地址对应的短地址发生变化)
360
+ if (node.config.buttonType === 'mesh' && node.config.meshMacAddress) {
361
+ initMeshStorage().then(async () => {
362
+ try {
363
+ const deviceMap = await storage.getItem('meshDeviceMap') || {};
364
+ const savedDevice = deviceMap[node.config.meshMacAddress];
365
+
366
+ if (savedDevice && savedDevice.shortAddr !== node.config.meshShortAddress) {
367
+ node.log(`Mesh设备短地址已更新: ${node.config.meshMacAddress} 从 ${node.config.meshShortAddress} 更新为 ${savedDevice.shortAddr}`);
368
+ node.config.meshShortAddress = savedDevice.shortAddr;
369
+ }
370
+ } catch (err) {
371
+ node.warn(`更新Mesh设备短地址失败: ${err.message}`);
372
+ }
373
+ }).catch(err => {
374
+ node.warn(`初始化Mesh存储失败: ${err.message}`);
375
+ });
376
+ }
377
+
104
378
  // 命令队列(处理多个按键同时按下)- 带时间戳
105
379
  node.commandQueue = [];
106
380
  node.isProcessingCommand = false;
@@ -310,7 +584,13 @@ module.exports = function(RED) {
310
584
  // 处理RS-485接收到的数据
311
585
  node.handleRs485Data = function(data) {
312
586
  try {
313
- // 解析轻量级协议帧
587
+ // 如果是Mesh模式,使用Mesh协议解析
588
+ if (node.config.buttonType === 'mesh') {
589
+ node.handleMeshData(data);
590
+ return;
591
+ }
592
+
593
+ // 解析轻量级协议帧(RS-485模式)
314
594
  const frame = protocol.parseFrame(data);
315
595
  if (!frame) {
316
596
  return; // 静默忽略无效帧
@@ -379,7 +659,60 @@ module.exports = function(RED) {
379
659
  node.error(`解析RS-485数据失败: ${err.message}`);
380
660
  }
381
661
  };
382
-
662
+
663
+ // 处理Mesh协议数据
664
+ node.handleMeshData = function(data) {
665
+ try {
666
+ // 解析Mesh状态事件
667
+ const event = meshProtocol.parseStatusEvent(data);
668
+ if (!event) {
669
+ return; // 静默忽略无效帧
670
+ }
671
+
672
+ // 检查是否是我们监听的Mesh设备
673
+ if (event.shortAddr !== node.config.meshShortAddress) {
674
+ return; // 不是我们的设备,忽略
675
+ }
676
+
677
+ // 检查消息类型
678
+ if (event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH &&
679
+ event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH_6) {
680
+ return; // 不是开关状态,忽略
681
+ }
682
+
683
+ // 获取按钮状态
684
+ if (!event.states || event.states.length < node.config.meshButtonNumber) {
685
+ return; // 状态数据不完整
686
+ }
687
+
688
+ const buttonState = event.states[node.config.meshButtonNumber - 1];
689
+ if (buttonState === null) {
690
+ return; // 状态未知,忽略
691
+ }
692
+
693
+ // 全局防抖:防止多个节点重复处理同一个按键
694
+ const debounceKey = `mesh-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
695
+ const now = Date.now();
696
+ const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
697
+
698
+ // 全局防抖:200ms内只触发一次
699
+ if (now - lastTriggerTime < 200) {
700
+ return; // 静默忽略重复触发
701
+ }
702
+ globalDebounceCache.set(debounceKey, now);
703
+
704
+ // 更新当前状态缓存(用于后续控制时保持其他路不变)
705
+ node.meshCurrentStates = event.states;
706
+
707
+ // 发送命令到继电器
708
+ node.debug(`Mesh开关${buttonState ? 'ON' : 'OFF'}: MAC=${node.config.meshMacAddress} 按键${node.config.meshButtonNumber}`);
709
+ node.sendMqttCommand(buttonState);
710
+
711
+ } catch (err) {
712
+ node.error(`解析Mesh数据失败: ${err.message}`);
713
+ }
714
+ };
715
+
383
716
  // 发送命令到继电器(支持两种模式:MQTT模式和内部事件模式)
384
717
  node.sendMqttCommand = function(state) {
385
718
  // 模式1:MQTT模式(通过MQTT发送命令,由主站节点统一处理)
@@ -534,36 +867,54 @@ module.exports = function(RED) {
534
867
  }
535
868
 
536
869
  const state = item.state;
537
-
870
+
538
871
  try {
539
- // 使用初始化时计算的deviceAddr和channel
540
- // 当物理按键按下时,会更新为实际的deviceAddr和channel
541
- const deviceAddr = node.buttonDeviceAddr;
542
- const channel = node.buttonChannel;
543
-
544
- // 根据按钮类型选择协议类型
545
- // 开关模式:使用SET协议(0x03),面板LED需要接收SET指令
546
- // 场景模式:使用REPORT协议(0x04),面板LED需要接收REPORT指令
547
872
  let command;
548
- if (node.config.buttonType === 'scene') {
549
- // 场景模式:使用REPORT协议
550
- command = protocol.buildSingleLightReport(
551
- node.config.switchId, // 本地地址(面板地址)
552
- deviceAddr, // 设备地址(从按键事件中获取)
553
- channel, // 通道(从按键事件中获取)
554
- state
873
+
874
+ if (node.config.buttonType === 'mesh') {
875
+ // Mesh模式:发送Mesh控制帧
876
+ command = meshProtocol.buildSwitchControlFrame(
877
+ node.config.meshShortAddress,
878
+ node.config.meshButtonNumber,
879
+ node.config.meshTotalButtons,
880
+ state,
881
+ node.meshCurrentStates || null
555
882
  );
883
+
884
+ if (!command) {
885
+ node.error(`构建Mesh控制帧失败`);
886
+ continue;
887
+ }
556
888
  } else {
557
- // 开关模式(默认):使用SET协议
558
- command = protocol.buildSingleLightCommand(
559
- node.config.switchId, // 本地地址(面板地址)
560
- deviceAddr, // 设备地址(从按键事件中获取)
561
- channel, // 通道(从按键事件中获取)
562
- state
563
- );
889
+ // RS-485模式:使用轻量级协议
890
+ // 使用初始化时计算的deviceAddr和channel
891
+ // 当物理按键按下时,会更新为实际的deviceAddr和channel
892
+ const deviceAddr = node.buttonDeviceAddr;
893
+ const channel = node.buttonChannel;
894
+
895
+ // 根据按钮类型选择协议类型
896
+ // 开关模式:使用SET协议(0x03),面板LED需要接收SET指令
897
+ // 场景模式:使用REPORT协议(0x04),面板LED需要接收REPORT指令
898
+ if (node.config.buttonType === 'scene') {
899
+ // 场景模式:使用REPORT协议
900
+ command = protocol.buildSingleLightReport(
901
+ node.config.switchId, // 本地地址(面板地址)
902
+ deviceAddr, // 设备地址(从按键事件中获取)
903
+ channel, // 通道(从按键事件中获取)
904
+ state
905
+ );
906
+ } else {
907
+ // 开关模式(默认):使用SET协议
908
+ command = protocol.buildSingleLightCommand(
909
+ node.config.switchId, // 本地地址(面板地址)
910
+ deviceAddr, // 设备地址(从按键事件中获取)
911
+ channel, // 通道(从按键事件中获取)
912
+ state
913
+ );
914
+ }
564
915
  }
565
916
 
566
- // 发送到RS-485总线(使用共享连接配置)
917
+ // 发送到RS-485总线或Mesh网关(使用共享连接配置)
567
918
  if (node.serialPortConfig) {
568
919
  node.serialPortConfig.write(command, (err) => {
569
920
  if (err) {
@@ -575,11 +926,17 @@ module.exports = function(RED) {
575
926
 
576
927
  // 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
577
928
  const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
578
- node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
929
+ if (node.config.buttonType === 'mesh') {
930
+ node.log(`Mesh LED反馈已发送:MAC=${node.config.meshMacAddress} 按钮${node.config.meshButtonNumber} = ${state ? 'ON' : 'OFF'} [${hexStr}]`);
931
+ } else {
932
+ const deviceAddr = node.buttonDeviceAddr;
933
+ const channel = node.buttonChannel;
934
+ node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
935
+ }
579
936
  }
580
937
  });
581
938
  } else {
582
- node.warn('RS-485连接未配置');
939
+ node.warn('连接未配置');
583
940
  }
584
941
 
585
942
  // 队列间隔20ms(优化速度,确保200个节点能快速反馈)
@@ -29,6 +29,10 @@ module.exports = function(RED) {
29
29
  node.reconnectTimer = null; // 重连定时器
30
30
  node.reconnectAttempts = 0; // 重连尝试次数
31
31
 
32
+ // 错误日志限流(避免断网时产生大量日志)
33
+ node.lastErrorLog = 0; // 上次错误日志时间
34
+ node.errorLogInterval = 60 * 1000; // 错误日志间隔:60秒
35
+
32
36
  // 数据监听器列表(每个从站开关节点注册一个)
33
37
  node.dataListeners = [];
34
38
 
@@ -81,15 +85,23 @@ module.exports = function(RED) {
81
85
  }
82
86
  });
83
87
 
84
- // 监听错误
88
+ // 监听错误(限流日志)
85
89
  node.connection.on('error', (err) => {
86
- node.error(`TCP连接错误: ${err.message}`);
90
+ const now = Date.now();
91
+ if (now - node.lastErrorLog > node.errorLogInterval) {
92
+ node.error(`TCP连接错误: ${err.message}`);
93
+ node.lastErrorLog = now;
94
+ }
87
95
  // 不在这里重连,在close事件中统一处理
88
96
  });
89
97
 
90
98
  // 监听关闭(统一的重连入口)
91
99
  node.connection.on('close', (hadError) => {
92
- node.log(`TCP连接已关闭${hadError ? '(有错误)' : ''}`);
100
+ const now = Date.now();
101
+ if (now - node.lastErrorLog > node.errorLogInterval) {
102
+ node.log(`TCP连接已关闭${hadError ? '(有错误)' : ''}`);
103
+ node.lastErrorLog = now;
104
+ }
93
105
 
94
106
  // 清理连接对象
95
107
  if (node.connection) {
@@ -103,7 +115,11 @@ module.exports = function(RED) {
103
115
  const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000); // 指数退避,最大60秒
104
116
  node.reconnectAttempts++;
105
117
 
106
- node.log(`${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
118
+ const nowLog = Date.now();
119
+ if (nowLog - node.lastErrorLog > node.errorLogInterval) {
120
+ node.log(`${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
121
+ node.lastErrorLog = nowLog;
122
+ }
107
123
 
108
124
  node.reconnectTimer = setTimeout(() => {
109
125
  node.reconnectTimer = null;
@@ -164,15 +180,23 @@ module.exports = function(RED) {
164
180
  node.isOpening = false;
165
181
 
166
182
  if (err) {
167
- node.error(`串口打开失败: ${err.message}`);
168
-
183
+ const now = Date.now();
184
+ if (now - node.lastErrorLog > node.errorLogInterval) {
185
+ node.error(`串口打开失败: ${err.message}`);
186
+ node.lastErrorLog = now;
187
+ }
188
+
169
189
  // 打开失败时也要触发重连(如果有监听器在使用)
170
190
  if (!node.isClosing && node.dataListeners.length > 0) {
171
191
  const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000);
172
192
  node.reconnectAttempts++;
173
-
174
- node.log(`${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
175
-
193
+
194
+ const nowLog = Date.now();
195
+ if (nowLog - node.lastErrorLog > node.errorLogInterval) {
196
+ node.log(`${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
197
+ node.lastErrorLog = nowLog;
198
+ }
199
+
176
200
  node.reconnectTimer = setTimeout(() => {
177
201
  node.reconnectTimer = null;
178
202
  node.openSerialConnection();
@@ -191,25 +215,37 @@ module.exports = function(RED) {
191
215
  try {
192
216
  listener(data);
193
217
  } catch (err) {
194
- node.error(`数据监听器错误: ${err.message}`);
218
+ const now = Date.now();
219
+ if (now - node.lastErrorLog > node.errorLogInterval) {
220
+ node.error(`数据监听器错误: ${err.message}`);
221
+ node.lastErrorLog = now;
222
+ }
195
223
  }
196
224
  });
197
225
  });
198
226
 
199
- // 监听错误
227
+ // 监听错误(限流日志)
200
228
  node.connection.on('error', (err) => {
201
- node.error(`串口错误: ${err.message}`);
229
+ const now = Date.now();
230
+ if (now - node.lastErrorLog > node.errorLogInterval) {
231
+ node.error(`串口错误: ${err.message}`);
232
+ node.lastErrorLog = now;
233
+ }
202
234
  // 不在这里重连,在close事件中统一处理
203
235
  });
204
236
 
205
237
  // 监听关闭(统一的重连入口)
206
238
  node.connection.on('close', (err) => {
207
- if (err) {
208
- node.log(`串口已关闭(错误: ${err.message})`);
209
- } else {
210
- node.log('串口已关闭');
239
+ const now = Date.now();
240
+ if (now - node.lastErrorLog > node.errorLogInterval) {
241
+ if (err) {
242
+ node.log(`串口已关闭(错误: ${err.message})`);
243
+ } else {
244
+ node.log('串口已关闭');
245
+ }
246
+ node.lastErrorLog = now;
211
247
  }
212
-
248
+
213
249
  // 清理连接对象
214
250
  if (node.connection) {
215
251
  try {
@@ -219,14 +255,18 @@ module.exports = function(RED) {
219
255
  }
220
256
  node.connection = null;
221
257
  }
222
-
258
+
223
259
  // 自动重连(如果不是主动关闭且有监听器在使用)
224
260
  if (!node.isClosing && node.dataListeners.length > 0) {
225
261
  const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000); // 指数退避,最大60秒
226
262
  node.reconnectAttempts++;
227
-
228
- node.log(`检测到串口断开,${delay/1000}秒后尝试重新连接(第${node.reconnectAttempts}次)...`);
229
-
263
+
264
+ const nowLog = Date.now();
265
+ if (nowLog - node.lastErrorLog > node.errorLogInterval) {
266
+ node.log(`检测到串口断开,${delay/1000}秒后尝试重新连接(第${node.reconnectAttempts}次)...`);
267
+ node.lastErrorLog = nowLog;
268
+ }
269
+
230
270
  node.reconnectTimer = setTimeout(() => {
231
271
  node.reconnectTimer = null;
232
272
  node.openSerialConnection();
@@ -251,6 +291,11 @@ module.exports = function(RED) {
251
291
  }
252
292
  };
253
293
 
294
+ // 获取连接对象(用于Mesh设备发现等场景)
295
+ node.getConnection = function() {
296
+ return node.connection;
297
+ };
298
+
254
299
  // 注册数据监听器
255
300
  node.registerDataListener = function(listener) {
256
301
  if (typeof listener !== 'function') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-modbus",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
5
5
  "main": "nodes/modbus-master.js",
6
6
  "scripts": {