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

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,16 +353,37 @@ 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;
107
381
 
108
- // 指示灯反馈队列(40ms间隔发送)- 带时间戳
109
- node.ledFeedbackQueue = [];
110
- node.isProcessingLedFeedback = false;
111
-
112
- // 队列超时时间(3秒)
113
- node.queueTimeout = 3000;
382
+ // LED反馈去重:记录最后一次发送的状态和时间戳(防止重复发送)
383
+ node.lastSentLedState = {
384
+ value: null,
385
+ timestamp: 0
386
+ };
114
387
 
115
388
  // 防死循环:记录最后一次状态变化的时间戳和值
116
389
  node.lastStateChange = {
@@ -224,8 +497,6 @@ module.exports = function(RED) {
224
497
  // 结束初始化阶段(5秒后)- 避免部署时大量LED反馈同时发送
225
498
  setTimeout(() => {
226
499
  node.isInitializing = false;
227
- // 初始化完成后,处理积累的LED反馈队列
228
- node.processLedFeedbackQueue();
229
500
  }, 5000);
230
501
 
231
502
  // 监听物理开关面板的按键事件
@@ -304,7 +575,13 @@ module.exports = function(RED) {
304
575
  // 处理RS-485接收到的数据
305
576
  node.handleRs485Data = function(data) {
306
577
  try {
307
- // 解析轻量级协议帧
578
+ // 如果是Mesh模式,使用Mesh协议解析
579
+ if (node.config.buttonType === 'mesh') {
580
+ node.handleMeshData(data);
581
+ return;
582
+ }
583
+
584
+ // 解析轻量级协议帧(RS-485模式)
308
585
  const frame = protocol.parseFrame(data);
309
586
  if (!frame) {
310
587
  return; // 静默忽略无效帧
@@ -373,7 +650,60 @@ module.exports = function(RED) {
373
650
  node.error(`解析RS-485数据失败: ${err.message}`);
374
651
  }
375
652
  };
376
-
653
+
654
+ // 处理Mesh协议数据
655
+ node.handleMeshData = function(data) {
656
+ try {
657
+ // 解析Mesh状态事件
658
+ const event = meshProtocol.parseStatusEvent(data);
659
+ if (!event) {
660
+ return; // 静默忽略无效帧
661
+ }
662
+
663
+ // 检查是否是我们监听的Mesh设备
664
+ if (event.shortAddr !== node.config.meshShortAddress) {
665
+ return; // 不是我们的设备,忽略
666
+ }
667
+
668
+ // 检查消息类型
669
+ if (event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH &&
670
+ event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH_6) {
671
+ return; // 不是开关状态,忽略
672
+ }
673
+
674
+ // 获取按钮状态
675
+ if (!event.states || event.states.length < node.config.meshButtonNumber) {
676
+ return; // 状态数据不完整
677
+ }
678
+
679
+ const buttonState = event.states[node.config.meshButtonNumber - 1];
680
+ if (buttonState === null) {
681
+ return; // 状态未知,忽略
682
+ }
683
+
684
+ // 全局防抖:防止多个节点重复处理同一个按键
685
+ const debounceKey = `mesh-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
686
+ const now = Date.now();
687
+ const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
688
+
689
+ // 全局防抖:200ms内只触发一次
690
+ if (now - lastTriggerTime < 200) {
691
+ return; // 静默忽略重复触发
692
+ }
693
+ globalDebounceCache.set(debounceKey, now);
694
+
695
+ // 更新当前状态缓存(用于后续控制时保持其他路不变)
696
+ node.meshCurrentStates = event.states;
697
+
698
+ // 发送命令到继电器
699
+ node.debug(`Mesh开关${buttonState ? 'ON' : 'OFF'}: MAC=${node.config.meshMacAddress} 按键${node.config.meshButtonNumber}`);
700
+ node.sendMqttCommand(buttonState);
701
+
702
+ } catch (err) {
703
+ node.error(`解析Mesh数据失败: ${err.message}`);
704
+ }
705
+ };
706
+
377
707
  // 发送命令到继电器(支持两种模式:MQTT模式和内部事件模式)
378
708
  node.sendMqttCommand = function(state) {
379
709
  // 模式1:MQTT模式(通过MQTT发送命令,由主站节点统一处理)
@@ -458,7 +788,8 @@ module.exports = function(RED) {
458
788
  node.isProcessingCommand = false;
459
789
  };
460
790
 
461
- // 发送控制指令到物理开关面板(控制指示灯等)- 入队
791
+ // 发送控制指令到物理开关面板(控制指示灯等)
792
+ // 直接发送到全局队列,由serial-port-config统一管理(20ms间隔串行发送)
462
793
  node.sendCommandToPanel = function(state) {
463
794
  // 检查连接状态
464
795
  if (!node.serialPortConfig || !node.serialPortConfig.connection) {
@@ -469,110 +800,83 @@ module.exports = function(RED) {
469
800
  return;
470
801
  }
471
802
 
472
- // 清理过期队列项(超过3秒)
473
- const now = Date.now();
474
- node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
475
-
476
- // 加入LED反馈队列(带时间戳)
477
- // 注意:这里不指定协议类型,在发送时根据情况选择
478
- node.ledFeedbackQueue.push({ state, timestamp: now });
479
-
480
- // 启动队列处理
481
- node.processLedFeedbackQueue();
482
- };
483
-
484
- // 处理LED反馈队列(基于面板ID的固定延迟)
485
- node.processLedFeedbackQueue = async function() {
486
- if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
487
- return;
488
- }
489
-
490
- // 初始化期间不处理LED反馈(避免部署时大量LED同时发送)
803
+ // 初始化期间不发送LED反馈(避免部署时大量LED同时发送)
491
804
  if (node.isInitializing) {
492
805
  return;
493
806
  }
494
-
495
- node.isProcessingLedFeedback = true;
496
-
497
- // 清理过期队列项
807
+
498
808
  const now = Date.now();
499
- node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
500
-
501
- // 基于面板ID的固定延迟,避免多个节点同时写入TCP冲突
502
- // 面板1=100ms, 面板2=200ms, 面板3=300ms, 面板4=400ms...
503
- // 这样不同面板的LED反馈永远不会同时发送
504
- const fixedDelay = node.config.switchId * 100;
505
- if (fixedDelay > 0) {
506
- await new Promise(resolve => setTimeout(resolve, fixedDelay));
809
+
810
+ // 防止重复发送:如果状态相同且时间间隔小于50ms,跳过
811
+ if (node.lastSentLedState.value === state && (now - node.lastSentLedState.timestamp) < 50) {
812
+ node.debug(`跳过重复LED反馈(状态未变化,间隔${now - node.lastSentLedState.timestamp}ms)`);
813
+ return;
507
814
  }
508
-
509
- while (node.ledFeedbackQueue.length > 0) {
510
- const item = node.ledFeedbackQueue.shift();
511
-
512
- // 再次检查是否过期
513
- if (Date.now() - item.timestamp >= node.queueTimeout) {
514
- node.warn(`丢弃过期LED反馈(${Date.now() - item.timestamp}ms)`);
515
- continue;
815
+
816
+ // 更新最后发送状态
817
+ node.lastSentLedState.value = state;
818
+ node.lastSentLedState.timestamp = now;
819
+
820
+ // 构建LED反馈协议帧
821
+ let command;
822
+
823
+ if (node.config.buttonType === 'mesh') {
824
+ // Mesh模式:发送Mesh控制帧
825
+ command = meshProtocol.buildSwitchControlFrame(
826
+ node.config.meshShortAddress,
827
+ node.config.meshButtonNumber,
828
+ node.config.meshTotalButtons,
829
+ state,
830
+ node.meshCurrentStates || null
831
+ );
832
+
833
+ if (!command) {
834
+ node.error(`构建Mesh控制帧失败`);
835
+ return;
516
836
  }
517
-
518
- const state = item.state;
519
-
520
- try {
521
- // 使用初始化时计算的deviceAddr和channel
522
- // 当物理按键按下时,会更新为实际的deviceAddr和channel
523
- const deviceAddr = node.buttonDeviceAddr;
524
- const channel = node.buttonChannel;
525
-
526
- // 根据按钮类型选择协议类型
527
- // 开关模式:使用SET协议(0x03),面板LED需要接收SET指令
528
- // 场景模式:使用REPORT协议(0x04),面板LED需要接收REPORT指令
529
- let command;
530
- if (node.config.buttonType === 'scene') {
531
- // 场景模式:使用REPORT协议
532
- command = protocol.buildSingleLightReport(
533
- node.config.switchId, // 本地地址(面板地址)
534
- deviceAddr, // 设备地址(从按键事件中获取)
535
- channel, // 通道(从按键事件中获取)
536
- state
537
- );
538
- } else {
539
- // 开关模式(默认):使用SET协议
540
- command = protocol.buildSingleLightCommand(
541
- node.config.switchId, // 本地地址(面板地址)
542
- deviceAddr, // 设备地址(从按键事件中获取)
543
- channel, // 通道(从按键事件中获取)
544
- state
545
- );
546
- }
837
+ } else {
838
+ // RS-485模式:使用轻量级协议
839
+ const deviceAddr = node.buttonDeviceAddr;
840
+ const channel = node.buttonChannel;
547
841
 
548
- // 发送到RS-485总线(使用共享连接配置)
549
- if (node.serialPortConfig) {
550
- node.serialPortConfig.write(command, (err) => {
551
- if (err) {
552
- node.error(`LED反馈失败: ${err.message}`);
553
- } else {
554
- // 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
555
- const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
556
- node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
557
- }
558
- });
559
- } else {
560
- node.warn('RS-485连接未配置');
561
- }
842
+ // 根据按钮类型选择协议类型
843
+ if (node.config.buttonType === 'scene') {
844
+ // 场景模式:使用REPORT协议
845
+ command = protocol.buildSingleLightReport(
846
+ node.config.switchId,
847
+ deviceAddr,
848
+ channel,
849
+ state
850
+ );
851
+ } else {
852
+ // 开关模式(默认):使用SET协议
853
+ command = protocol.buildSingleLightCommand(
854
+ node.config.switchId,
855
+ deviceAddr,
856
+ channel,
857
+ state
858
+ );
859
+ }
860
+ }
562
861
 
563
- // 队列间隔20ms(优化速度,确保200个节点能快速反馈)
564
- // 20ms × 200节点 = 4秒完成所有反馈
565
- if (node.ledFeedbackQueue.length > 0) {
566
- await new Promise(resolve => setTimeout(resolve, 20));
862
+ // 直接发送到全局队列(由serial-port-config统一管理,20ms间隔串行发送)
863
+ node.serialPortConfig.write(command, (err) => {
864
+ if (err) {
865
+ node.error(`LED反馈失败: ${err.message}`);
866
+ } else {
867
+ // 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
868
+ const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
869
+ if (node.config.buttonType === 'mesh') {
870
+ node.log(`Mesh LED反馈已发送:MAC=${node.config.meshMacAddress} 按钮${node.config.meshButtonNumber} = ${state ? 'ON' : 'OFF'} [${hexStr}]`);
871
+ } else {
872
+ const deviceAddr = node.buttonDeviceAddr;
873
+ const channel = node.buttonChannel;
874
+ node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
567
875
  }
568
- } catch (err) {
569
- node.error(`发送LED反馈失败: ${err.message}`);
570
876
  }
571
- }
572
-
573
- node.isProcessingLedFeedback = false;
877
+ });
574
878
  };
575
-
879
+
576
880
  // 更新节点状态显示
577
881
  node.updateStatus = function() {
578
882
  const rs485Status = node.isRs485Connected ? 'OK' : 'ERR';
@@ -691,55 +995,59 @@ module.exports = function(RED) {
691
995
  node.mqttClient.on('error', (err) => {
692
996
  // 连接失败,尝试下一个候选地址
693
997
  const errorMsg = err.message || err.code || '连接失败';
694
- node.warn(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
695
-
696
998
  const now = Date.now();
999
+
1000
+ // 使用日志限流,避免长期断网时产生垃圾日志
1001
+ const shouldLogError = (now - node.lastMqttErrorLog) > node.errorLogInterval;
1002
+ if (shouldLogError) {
1003
+ node.debug(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
1004
+ }
1005
+
697
1006
  const timeSinceLastAttempt = now - lastConnectAttempt;
698
-
699
- // 避免频繁重试(至少等待1秒),但仍要记录错误
1007
+
1008
+ // 避免频繁重试(至少等待1秒)
700
1009
  if (timeSinceLastAttempt < 1000) {
701
1010
  setTimeout(() => {
702
1011
  tryNextBroker();
703
1012
  }, 1000);
704
1013
  return;
705
1014
  }
706
-
1015
+
707
1016
  tryNextBroker();
708
-
1017
+
709
1018
  function tryNextBroker() {
710
1019
  // 尝试下一个候选地址
711
1020
  currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
712
1021
  const nextBroker = brokerCandidates[currentCandidateIndex];
713
-
1022
+
714
1023
  // 如果回到第一个地址,说明所有地址都试过了
715
1024
  if (currentCandidateIndex === 0) {
716
1025
  // 判断是否是局域网IP配置(只有一个候选地址)
717
1026
  const isSingleIpConfig = brokerCandidates.length === 1;
718
-
719
- if (isSingleIpConfig) {
720
- // 局域网IP配置失败,立即输出错误(不受日志限流限制)
721
- node.error(`MQTT连接失败: ${errorMsg}`);
722
- node.error(`无法连接到MQTT broker: ${brokerCandidates[0]}`);
723
- node.error('请检查:1) MQTT broker是否在该地址运行 2) 网络是否连通 3) 端口是否正确');
724
- node.error('提示:可以使用命令测试: telnet 192.168.2.12 1883');
725
- } else {
726
- // 多个fallback地址都失败,使用日志限流
727
- const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
728
-
729
- if (shouldLog) {
730
- node.error(`MQTT错误: ${errorMsg}`);
731
- node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
732
- node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
733
- node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
734
- node.lastMqttErrorLog = now;
1027
+
1028
+ // 使用日志限流,避免长期断网时产生垃圾日志
1029
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
1030
+
1031
+ if (shouldLog) {
1032
+ if (isSingleIpConfig) {
1033
+ // 局域网IP配置失败,使用debug级别(不写入日志文件)
1034
+ node.debug(`MQTT连接失败: ${errorMsg}`);
1035
+ node.debug(`无法连接到MQTT broker: ${brokerCandidates[0]}`);
1036
+ node.debug('请检查:1) MQTT broker是否在该地址运行 2) 网络是否连通 3) 端口是否正确');
1037
+ } else {
1038
+ // 多个fallback地址都失败,使用debug级别
1039
+ node.debug(`MQTT错误: ${errorMsg}`);
1040
+ node.debug(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
1041
+ node.debug('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
735
1042
  }
1043
+ node.lastMqttErrorLog = now;
736
1044
  }
737
-
738
- // 5秒后重试第一个地址
1045
+
1046
+ // 30秒后重试第一个地址(从5秒改为30秒,减少重试频率)
739
1047
  setTimeout(() => {
740
1048
  node.debug('重试连接MQTT broker...');
741
1049
  tryConnect(brokerCandidates[0]);
742
- }, 5000);
1050
+ }, 30000);
743
1051
  } else {
744
1052
  node.debug(`尝试备用MQTT broker: ${nextBroker}`);
745
1053
  setTimeout(() => {
@@ -747,7 +1055,7 @@ module.exports = function(RED) {
747
1055
  }, 500); // 快速尝试下一个地址
748
1056
  }
749
1057
  }
750
-
1058
+
751
1059
  node.updateStatus();
752
1060
  });
753
1061
 
@@ -758,16 +1066,16 @@ module.exports = function(RED) {
758
1066
  node.mqttClient.on('offline', () => {
759
1067
  const now = Date.now();
760
1068
  const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
761
-
1069
+
762
1070
  if (shouldLog) {
763
- node.warn('MQTT离线,正在尝试重连...');
1071
+ node.debug('MQTT离线,正在尝试重连...');
764
1072
  node.lastMqttErrorLog = now;
765
1073
  }
766
-
1074
+
767
1075
  // 尝试下一个候选地址
768
1076
  currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
769
1077
  const nextBroker = brokerCandidates[currentCandidateIndex];
770
-
1078
+
771
1079
  setTimeout(() => {
772
1080
  tryConnect(nextBroker);
773
1081
  }, 2000);
@@ -838,35 +1146,50 @@ module.exports = function(RED) {
838
1146
 
839
1147
  // 处理输入消息
840
1148
  node.on('input', function(msg) {
841
- if (!node.mqttClient || !node.mqttClient.connected) {
842
- node.warn('MQTT未连接');
843
- return;
844
- }
845
-
846
1149
  let value = false;
847
-
1150
+
848
1151
  // 解析输入值
849
1152
  if (typeof msg.payload === 'boolean') {
850
1153
  value = msg.payload;
851
1154
  } else if (typeof msg.payload === 'string') {
852
- value = (msg.payload.toLowerCase() === 'on' ||
853
- msg.payload.toLowerCase() === 'true' ||
1155
+ value = (msg.payload.toLowerCase() === 'on' ||
1156
+ msg.payload.toLowerCase() === 'true' ||
854
1157
  msg.payload === '1');
855
1158
  } else if (typeof msg.payload === 'number') {
856
1159
  value = (msg.payload !== 0);
857
1160
  }
858
-
859
- // 发布命令到MQTT
860
- const command = value ? 'ON' : 'OFF';
861
- node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
862
- if (err) {
863
- node.error(`发布命令失败: ${err.message}`);
864
- } else {
865
- node.log(`发送命令: ${command}${node.commandTopic}`);
866
- node.currentState = value;
867
- node.updateStatus();
868
- }
1161
+
1162
+ // 更新当前状态
1163
+ node.currentState = value;
1164
+
1165
+ // 输出消息到debug节点(无论是否使用MQTT)
1166
+ node.send({
1167
+ payload: value,
1168
+ topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
1169
+ switchId: node.config.switchId,
1170
+ button: node.config.buttonNumber,
1171
+ targetSlave: node.config.targetSlaveAddress,
1172
+ targetCoil: node.config.targetCoilNumber,
1173
+ source: 'input'
869
1174
  });
1175
+
1176
+ // 如果启用MQTT,发布命令到MQTT
1177
+ if (node.mqttClient && node.mqttClient.connected) {
1178
+ const command = value ? 'ON' : 'OFF';
1179
+ node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
1180
+ if (err) {
1181
+ node.error(`发布命令失败: ${err.message}`);
1182
+ } else {
1183
+ node.log(`发送命令: ${command} 到 ${node.commandTopic}`);
1184
+ }
1185
+ });
1186
+ } else {
1187
+ // 本地模式:通过内部事件发送命令
1188
+ node.sendMqttCommand(value);
1189
+ }
1190
+
1191
+ // 更新状态显示
1192
+ node.updateStatus();
870
1193
  });
871
1194
 
872
1195
  // 节点关闭时清理
@@ -881,7 +1204,6 @@ module.exports = function(RED) {
881
1204
 
882
1205
  // 清理队列(释放内存)
883
1206
  node.commandQueue = [];
884
- node.ledFeedbackQueue = [];
885
1207
  node.frameBuffer = Buffer.alloc(0);
886
1208
 
887
1209
  // 注销RS-485数据监听器(不关闭连接,由配置节点管理)