node-red-contrib-symi-modbus 2.7.4 → 2.7.6

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;
@@ -118,6 +392,12 @@ module.exports = function(RED) {
118
392
  value: false
119
393
  };
120
394
 
395
+ // 防止LED反馈重复发送:记录最后一次LED反馈的时间戳和值
396
+ node.lastLedFeedback = {
397
+ timestamp: 0,
398
+ value: false
399
+ };
400
+
121
401
  // 节点初始化标志(用于静默初始化期间的警告)
122
402
  node.isInitializing = true;
123
403
 
@@ -304,7 +584,13 @@ module.exports = function(RED) {
304
584
  // 处理RS-485接收到的数据
305
585
  node.handleRs485Data = function(data) {
306
586
  try {
307
- // 解析轻量级协议帧
587
+ // 如果是Mesh模式,使用Mesh协议解析
588
+ if (node.config.buttonType === 'mesh') {
589
+ node.handleMeshData(data);
590
+ return;
591
+ }
592
+
593
+ // 解析轻量级协议帧(RS-485模式)
308
594
  const frame = protocol.parseFrame(data);
309
595
  if (!frame) {
310
596
  return; // 静默忽略无效帧
@@ -373,7 +659,60 @@ module.exports = function(RED) {
373
659
  node.error(`解析RS-485数据失败: ${err.message}`);
374
660
  }
375
661
  };
376
-
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
+
377
716
  // 发送命令到继电器(支持两种模式:MQTT模式和内部事件模式)
378
717
  node.sendMqttCommand = function(state) {
379
718
  // 模式1:MQTT模式(通过MQTT发送命令,由主站节点统一处理)
@@ -469,10 +808,26 @@ module.exports = function(RED) {
469
808
  return;
470
809
  }
471
810
 
472
- // 清理过期队列项(超过3秒)
811
+ // 防止重复发送:如果状态相同且时间间隔小于100ms,跳过
473
812
  const now = Date.now();
813
+ const timeSinceLastFeedback = now - node.lastLedFeedback.timestamp;
814
+ const stateChanged = (state !== node.lastLedFeedback.value);
815
+
816
+ if (!stateChanged && timeSinceLastFeedback < 100) {
817
+ node.debug(`跳过重复LED反馈(状态未变化,间隔${timeSinceLastFeedback}ms)`);
818
+ return;
819
+ }
820
+
821
+ // 清理过期队列项(超过3秒)
474
822
  node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
475
823
 
824
+ // 检查队列中是否已有相同状态的反馈(去重)
825
+ const hasSameState = node.ledFeedbackQueue.some(item => item.state === state);
826
+ if (hasSameState) {
827
+ node.debug(`队列中已有相同状态的LED反馈,跳过添加`);
828
+ return;
829
+ }
830
+
476
831
  // 加入LED反馈队列(带时间戳)
477
832
  // 注意:这里不指定协议类型,在发送时根据情况选择
478
833
  node.ledFeedbackQueue.push({ state, timestamp: now });
@@ -481,83 +836,107 @@ module.exports = function(RED) {
481
836
  node.processLedFeedbackQueue();
482
837
  };
483
838
 
484
- // 处理LED反馈队列(基于面板ID的固定延迟)
839
+ // 处理LED反馈队列(20ms间隔串行发送)
485
840
  node.processLedFeedbackQueue = async function() {
486
841
  if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
487
842
  return;
488
843
  }
489
-
844
+
490
845
  // 初始化期间不处理LED反馈(避免部署时大量LED同时发送)
491
846
  if (node.isInitializing) {
492
847
  return;
493
848
  }
494
-
849
+
495
850
  node.isProcessingLedFeedback = true;
496
-
851
+
497
852
  // 清理过期队列项
498
853
  const now = Date.now();
499
854
  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));
507
- }
508
-
855
+
856
+ // 记录队列开始处理时间
857
+ const queueStartTime = Date.now();
858
+ const queueLength = node.ledFeedbackQueue.length;
859
+
509
860
  while (node.ledFeedbackQueue.length > 0) {
510
861
  const item = node.ledFeedbackQueue.shift();
511
-
862
+
512
863
  // 再次检查是否过期
513
864
  if (Date.now() - item.timestamp >= node.queueTimeout) {
514
865
  node.warn(`丢弃过期LED反馈(${Date.now() - item.timestamp}ms)`);
515
866
  continue;
516
867
  }
517
-
868
+
518
869
  const state = item.state;
519
-
870
+
520
871
  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
872
  let command;
530
- if (node.config.buttonType === 'scene') {
531
- // 场景模式:使用REPORT协议
532
- command = protocol.buildSingleLightReport(
533
- node.config.switchId, // 本地地址(面板地址)
534
- deviceAddr, // 设备地址(从按键事件中获取)
535
- channel, // 通道(从按键事件中获取)
536
- 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
537
882
  );
883
+
884
+ if (!command) {
885
+ node.error(`构建Mesh控制帧失败`);
886
+ continue;
887
+ }
538
888
  } else {
539
- // 开关模式(默认):使用SET协议
540
- command = protocol.buildSingleLightCommand(
541
- node.config.switchId, // 本地地址(面板地址)
542
- deviceAddr, // 设备地址(从按键事件中获取)
543
- channel, // 通道(从按键事件中获取)
544
- state
545
- );
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
+ }
546
915
  }
547
916
 
548
- // 发送到RS-485总线(使用共享连接配置)
917
+ // 发送到RS-485总线或Mesh网关(使用共享连接配置)
549
918
  if (node.serialPortConfig) {
550
919
  node.serialPortConfig.write(command, (err) => {
551
920
  if (err) {
552
921
  node.error(`LED反馈失败: ${err.message}`);
553
922
  } else {
923
+ // 记录最后一次LED反馈的时间戳和值
924
+ node.lastLedFeedback.timestamp = Date.now();
925
+ node.lastLedFeedback.value = state;
926
+
554
927
  // 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
555
928
  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}]`);
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
+ }
557
936
  }
558
937
  });
559
938
  } else {
560
- node.warn('RS-485连接未配置');
939
+ node.warn('连接未配置');
561
940
  }
562
941
 
563
942
  // 队列间隔20ms(优化速度,确保200个节点能快速反馈)
@@ -567,9 +946,16 @@ module.exports = function(RED) {
567
946
  }
568
947
  } catch (err) {
569
948
  node.error(`发送LED反馈失败: ${err.message}`);
949
+ // LED反馈失败不中断队列,继续处理下一个
570
950
  }
571
951
  }
572
-
952
+
953
+ // 队列处理完成,输出统计信息
954
+ const queueDuration = Date.now() - queueStartTime;
955
+ if (queueLength > 1) {
956
+ node.debug(`LED反馈队列处理完成:${queueLength}个任务,耗时${queueDuration}ms`);
957
+ }
958
+
573
959
  node.isProcessingLedFeedback = false;
574
960
  };
575
961
 
@@ -842,35 +1228,50 @@ module.exports = function(RED) {
842
1228
 
843
1229
  // 处理输入消息
844
1230
  node.on('input', function(msg) {
845
- if (!node.mqttClient || !node.mqttClient.connected) {
846
- node.warn('MQTT未连接');
847
- return;
848
- }
849
-
850
1231
  let value = false;
851
-
1232
+
852
1233
  // 解析输入值
853
1234
  if (typeof msg.payload === 'boolean') {
854
1235
  value = msg.payload;
855
1236
  } else if (typeof msg.payload === 'string') {
856
- value = (msg.payload.toLowerCase() === 'on' ||
857
- msg.payload.toLowerCase() === 'true' ||
1237
+ value = (msg.payload.toLowerCase() === 'on' ||
1238
+ msg.payload.toLowerCase() === 'true' ||
858
1239
  msg.payload === '1');
859
1240
  } else if (typeof msg.payload === 'number') {
860
1241
  value = (msg.payload !== 0);
861
1242
  }
862
-
863
- // 发布命令到MQTT
864
- const command = value ? 'ON' : 'OFF';
865
- node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
866
- if (err) {
867
- node.error(`发布命令失败: ${err.message}`);
868
- } else {
869
- node.log(`发送命令: ${command}${node.commandTopic}`);
870
- node.currentState = value;
871
- node.updateStatus();
872
- }
1243
+
1244
+ // 更新当前状态
1245
+ node.currentState = value;
1246
+
1247
+ // 输出消息到debug节点(无论是否使用MQTT)
1248
+ node.send({
1249
+ payload: value,
1250
+ topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
1251
+ switchId: node.config.switchId,
1252
+ button: node.config.buttonNumber,
1253
+ targetSlave: node.config.targetSlaveAddress,
1254
+ targetCoil: node.config.targetCoilNumber,
1255
+ source: 'input'
873
1256
  });
1257
+
1258
+ // 如果启用MQTT,发布命令到MQTT
1259
+ if (node.mqttClient && node.mqttClient.connected) {
1260
+ const command = value ? 'ON' : 'OFF';
1261
+ node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
1262
+ if (err) {
1263
+ node.error(`发布命令失败: ${err.message}`);
1264
+ } else {
1265
+ node.log(`发送命令: ${command} 到 ${node.commandTopic}`);
1266
+ }
1267
+ });
1268
+ } else {
1269
+ // 本地模式:通过内部事件发送命令
1270
+ node.sendMqttCommand(value);
1271
+ }
1272
+
1273
+ // 更新状态显示
1274
+ node.updateStatus();
874
1275
  });
875
1276
 
876
1277
  // 节点关闭时清理