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.
- package/README.md +104 -309
- package/examples/basic-flow.json +18 -17
- package/nodes/homekit-bridge.html +44 -22
- package/nodes/homekit-bridge.js +17 -0
- package/nodes/mesh-protocol.js +286 -0
- package/nodes/modbus-dashboard.html +75 -27
- package/nodes/modbus-dashboard.js +17 -0
- package/nodes/modbus-master.js +45 -14
- package/nodes/modbus-slave-switch.html +195 -11
- package/nodes/modbus-slave-switch.js +466 -65
- package/nodes/serial-port-config.js +66 -21
- package/package.json +1 -1
- package/v2.7.4-/344/277/256/345/244/215/350/257/264/346/230/216.md +0 -151
- package//346/233/264/346/226/260/346/227/245/345/277/227.txt +0 -62
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
// 节点关闭时清理
|