node-red-contrib-symi-modbus 2.5.0 → 2.5.3
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 +314 -494
- package/examples/basic-flow.json +198 -0
- package/nodes/lightweight-protocol.js +35 -1
- package/nodes/modbus-master.html +13 -185
- package/nodes/modbus-master.js +458 -153
- package/nodes/modbus-server-config.html +174 -0
- package/nodes/modbus-server-config.js +18 -0
- package/nodes/modbus-slave-switch.html +11 -153
- package/nodes/modbus-slave-switch.js +250 -219
- package/nodes/serial-port-config.html +191 -0
- package/nodes/serial-port-config.js +270 -0
- package/package.json +8 -5
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
2
|
"use strict";
|
|
3
3
|
const mqtt = require("mqtt");
|
|
4
|
-
const ModbusRTU = require("modbus-serial");
|
|
5
|
-
const net = require("net");
|
|
6
4
|
const protocol = require("./lightweight-protocol");
|
|
7
5
|
|
|
8
6
|
// 串口列表API - 支持Windows、Linux、macOS所有串口设备
|
|
@@ -60,52 +58,61 @@ module.exports = function(RED) {
|
|
|
60
58
|
function ModbusSlaveSwitchNode(config) {
|
|
61
59
|
RED.nodes.createNode(this, config);
|
|
62
60
|
const node = this;
|
|
63
|
-
|
|
61
|
+
|
|
62
|
+
// 获取RS-485连接配置节点
|
|
63
|
+
node.serialPortConfig = RED.nodes.getNode(config.serialPortConfig);
|
|
64
|
+
if (!node.serialPortConfig) {
|
|
65
|
+
node.error('未配置RS-485连接,请在节点配置中选择连接配置');
|
|
66
|
+
node.status({fill: "red", shape: "ring", text: "未配置连接"});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
// 获取MQTT服务器配置节点
|
|
65
71
|
node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
// 配置参数
|
|
68
74
|
node.config = {
|
|
69
|
-
//
|
|
70
|
-
connectionType: config.connectionType,
|
|
71
|
-
tcpHost: config.tcpHost,
|
|
72
|
-
tcpPort: parseInt(config.tcpPort),
|
|
73
|
-
serialPort: config.serialPort,
|
|
74
|
-
serialBaudRate: parseInt(config.serialBaudRate),
|
|
75
|
-
serialDataBits: parseInt(config.serialDataBits),
|
|
76
|
-
serialStopBits: parseInt(config.serialStopBits),
|
|
77
|
-
serialParity: config.serialParity,
|
|
78
|
-
// 从config节点读取MQTT配置
|
|
75
|
+
// 从MQTT配置节点读取MQTT配置
|
|
79
76
|
mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "mqtt://localhost:1883",
|
|
80
77
|
mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
|
|
81
78
|
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
82
79
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
|
|
80
|
+
// 开关面板配置
|
|
83
81
|
switchBrand: config.switchBrand || "symi", // 面板品牌(默认亖米)
|
|
84
82
|
switchId: parseInt(config.switchId) || 0, // 开关ID(0-255,物理面板地址)
|
|
85
83
|
buttonNumber: parseInt(config.buttonNumber) || 1, // 按钮编号(1-8)
|
|
86
84
|
targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
|
|
87
85
|
targetCoilNumber: parseInt(config.targetCoilNumber) || 0 // 目标继电器线圈编号
|
|
88
86
|
};
|
|
89
|
-
|
|
87
|
+
|
|
90
88
|
node.currentState = false;
|
|
91
89
|
node.mqttClient = null;
|
|
92
|
-
node.rs485Client = new ModbusRTU(); // RS-485总线客户端
|
|
93
|
-
node.isRs485Connected = false;
|
|
94
|
-
node.reconnectTimer = null;
|
|
95
90
|
node.isClosing = false;
|
|
96
91
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
97
92
|
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
node.frameBuffer = Buffer.alloc(0);
|
|
101
|
-
|
|
102
|
-
// 命令队列(处理多个按键同时按下)
|
|
93
|
+
|
|
94
|
+
// 命令队列(处理多个按键同时按下)- 带时间戳
|
|
103
95
|
node.commandQueue = [];
|
|
104
96
|
node.isProcessingCommand = false;
|
|
105
|
-
|
|
106
|
-
// 指示灯反馈队列(40ms
|
|
97
|
+
|
|
98
|
+
// 指示灯反馈队列(40ms间隔发送)- 带时间戳
|
|
107
99
|
node.ledFeedbackQueue = [];
|
|
108
100
|
node.isProcessingLedFeedback = false;
|
|
101
|
+
|
|
102
|
+
// 队列超时时间(3秒)
|
|
103
|
+
node.queueTimeout = 3000;
|
|
104
|
+
|
|
105
|
+
// 防死循环:记录最后一次状态变化的时间戳和值
|
|
106
|
+
node.lastStateChange = {
|
|
107
|
+
timestamp: 0,
|
|
108
|
+
value: false
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 节点初始化标志(用于静默初始化期间的警告)
|
|
112
|
+
node.isInitializing = true;
|
|
113
|
+
|
|
114
|
+
// 节点关闭标志(用于静默关闭期间的警告)
|
|
115
|
+
node.isClosing = false;
|
|
109
116
|
|
|
110
117
|
// MQTT主题(映射到继电器设备)
|
|
111
118
|
node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
|
|
@@ -188,184 +195,110 @@ module.exports = function(RED) {
|
|
|
188
195
|
// 连接RS-485总线(物理开关面板)
|
|
189
196
|
node.connectRs485 = async function() {
|
|
190
197
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
port: node.config.tcpPort
|
|
198
|
-
});
|
|
199
|
-
node.log(`RS485连接成功(TCP): ${node.config.tcpHost}:${node.config.tcpPort}`);
|
|
200
|
-
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
201
|
-
} else {
|
|
202
|
-
// 串口连接验证
|
|
203
|
-
if (!node.config.serialPort) {
|
|
204
|
-
throw new Error("串口未配置,请配置串口路径(如:COM1、/dev/ttyUSB0、/dev/ttyS1)");
|
|
205
|
-
}
|
|
206
|
-
await node.rs485Client.connectRTUBuffered(node.config.serialPort, {
|
|
207
|
-
baudRate: node.config.serialBaudRate || 9600,
|
|
208
|
-
dataBits: node.config.serialDataBits || 8,
|
|
209
|
-
stopBits: node.config.serialStopBits || 1,
|
|
210
|
-
parity: node.config.serialParity || 'none'
|
|
211
|
-
});
|
|
212
|
-
node.log(`RS485连接成功(串口): ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
213
|
-
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
node.rs485Client.setTimeout(5000);
|
|
198
|
+
// 使用共享连接配置(由配置节点管理)
|
|
199
|
+
node.log(`使用共享RS-485连接配置: ${node.serialPortConfig.connectionType === 'tcp' ?
|
|
200
|
+
`TCP ${node.serialPortConfig.tcpHost}:${node.serialPortConfig.tcpPort}` :
|
|
201
|
+
`串口 ${node.serialPortConfig.serialPort} @ ${node.serialPortConfig.baudRate}bps`}`);
|
|
202
|
+
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
203
|
+
|
|
217
204
|
node.isRs485Connected = true;
|
|
218
205
|
node.updateStatus();
|
|
219
|
-
|
|
206
|
+
|
|
207
|
+
// 结束初始化阶段(5秒后)- 避免部署时大量LED反馈同时发送
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
node.isInitializing = false;
|
|
210
|
+
// 初始化完成后,处理积累的LED反馈队列
|
|
211
|
+
node.processLedFeedbackQueue();
|
|
212
|
+
}, 5000);
|
|
213
|
+
|
|
220
214
|
// 监听物理开关面板的按键事件
|
|
221
215
|
node.startListeningButtonEvents();
|
|
222
|
-
|
|
216
|
+
|
|
223
217
|
} catch (err) {
|
|
224
218
|
node.error(`RS-485连接失败: ${err.message}`);
|
|
225
219
|
node.isRs485Connected = false;
|
|
226
220
|
node.updateStatus();
|
|
227
|
-
|
|
228
|
-
// 5秒后重试连接(仅在配置有效时)
|
|
229
|
-
if (!node.isClosing && !node.reconnectTimer) {
|
|
230
|
-
// 如果是配置错误,不重试
|
|
231
|
-
if (err.message.includes("未配置")) {
|
|
232
|
-
node.warn("RS-485连接配置无效,请检查配置");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
node.reconnectTimer = setTimeout(() => {
|
|
236
|
-
node.reconnectTimer = null;
|
|
237
|
-
node.connectRs485();
|
|
238
|
-
}, 5000);
|
|
239
|
-
}
|
|
240
221
|
}
|
|
241
222
|
};
|
|
242
223
|
|
|
243
224
|
// 监听物理开关面板的按键事件
|
|
244
225
|
node.startListeningButtonEvents = function() {
|
|
245
226
|
node.log(`开始监听开关面板 ${node.config.switchId} 的按钮 ${node.config.buttonNumber}`);
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
if (node.
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
227
|
+
|
|
228
|
+
// 使用共享连接配置的数据监听器
|
|
229
|
+
if (node.serialPortConfig) {
|
|
230
|
+
// 定义数据监听器函数
|
|
231
|
+
node.serialDataListener = (data) => {
|
|
232
|
+
node.log(`RS-485收到 ${data.length} 字节: ${data.toString('hex').toUpperCase()}`);
|
|
233
|
+
node.handleRs485Data(data);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// 注册到共享连接配置
|
|
237
|
+
node.serialPortConfig.registerDataListener(node.serialDataListener);
|
|
238
|
+
|
|
239
|
+
node.log('RS-485数据监听已启动(共享连接)');
|
|
257
240
|
} else {
|
|
258
|
-
|
|
259
|
-
if (node.rs485Client._port) {
|
|
260
|
-
node.rs485Client._port.on('data', (data) => {
|
|
261
|
-
node.handleRs485Data(data);
|
|
262
|
-
});
|
|
263
|
-
node.log('串口数据监听已启动');
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// 处理TCP流式数据(需要缓冲和帧边界检测)
|
|
269
|
-
node.handleTcpData = function(data) {
|
|
270
|
-
try {
|
|
271
|
-
// 将接收到的数据追加到缓冲区
|
|
272
|
-
node.frameBuffer = Buffer.concat([node.frameBuffer, data]);
|
|
273
|
-
|
|
274
|
-
// 输出缓冲区状态
|
|
275
|
-
node.log(`TCP收到 ${data.length} 字节,缓冲区共 ${node.frameBuffer.length} 字节`);
|
|
276
|
-
|
|
277
|
-
// 从缓冲区提取完整的帧
|
|
278
|
-
while (node.frameBuffer.length > 0) {
|
|
279
|
-
// 查找帧头 0x7E
|
|
280
|
-
const startIdx = node.frameBuffer.indexOf(0x7E);
|
|
281
|
-
|
|
282
|
-
if (startIdx === -1) {
|
|
283
|
-
// 没有找到帧头,清空缓冲区
|
|
284
|
-
node.frameBuffer = Buffer.alloc(0);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (startIdx > 0) {
|
|
289
|
-
// 丢弃帧头之前的无效数据
|
|
290
|
-
node.frameBuffer = node.frameBuffer.slice(startIdx);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// 查找帧尾 0x7D
|
|
294
|
-
const endIdx = node.frameBuffer.indexOf(0x7D, 1);
|
|
295
|
-
|
|
296
|
-
if (endIdx === -1) {
|
|
297
|
-
// 没有找到帧尾,等待更多数据
|
|
298
|
-
if (node.frameBuffer.length > 100) {
|
|
299
|
-
// 缓冲区过大,可能是错误数据,清空
|
|
300
|
-
node.warn('缓冲区过大,清空');
|
|
301
|
-
node.frameBuffer = Buffer.alloc(0);
|
|
302
|
-
}
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// 提取完整的帧(包括帧头和帧尾)
|
|
307
|
-
const frame = node.frameBuffer.slice(0, endIdx + 1);
|
|
308
|
-
|
|
309
|
-
// 从缓冲区移除已处理的帧
|
|
310
|
-
node.frameBuffer = node.frameBuffer.slice(endIdx + 1);
|
|
311
|
-
|
|
312
|
-
// 处理这一帧
|
|
313
|
-
node.handleRs485Data(frame);
|
|
314
|
-
}
|
|
315
|
-
} catch (err) {
|
|
316
|
-
node.error(`TCP数据处理错误: ${err.message}`);
|
|
317
|
-
node.frameBuffer = Buffer.alloc(0); // 错误时清空缓冲区
|
|
241
|
+
node.error('RS-485连接配置未初始化,无法监听数据');
|
|
318
242
|
}
|
|
319
243
|
};
|
|
320
|
-
|
|
244
|
+
|
|
321
245
|
// 处理RS-485接收到的数据
|
|
322
246
|
node.handleRs485Data = function(data) {
|
|
323
247
|
try {
|
|
324
248
|
// 输出原始数据(十六进制)- 方便调试
|
|
325
249
|
const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
|
|
326
250
|
node.log(`RS485收到数据: ${hexData}`);
|
|
327
|
-
|
|
251
|
+
|
|
328
252
|
// 解析轻量级协议帧
|
|
329
253
|
const frame = protocol.parseFrame(data);
|
|
330
254
|
if (!frame) {
|
|
331
255
|
node.warn(`无效的协议帧(CRC校验失败或格式错误): ${hexData}`);
|
|
332
256
|
return;
|
|
333
257
|
}
|
|
334
|
-
|
|
258
|
+
|
|
335
259
|
node.log(`解析成功: 设备地址=${frame.deviceAddr} 通道=${frame.channel} 数据类型=0x${frame.dataType.toString(16).toUpperCase()} 操作码=0x${frame.opCode.toString(16).toUpperCase()}`);
|
|
336
|
-
|
|
260
|
+
|
|
261
|
+
// 忽略 REPORT (0x04) 类型的帧(这是面板对我们指令的确认,不是按键事件)
|
|
262
|
+
// 只处理 SET (0x03) 类型的帧(真正的按键事件)
|
|
263
|
+
if (frame.dataType === 0x04) {
|
|
264
|
+
node.log(`忽略 REPORT 帧(面板确认收到指令)`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
337
268
|
// 检测是否是按键按下事件
|
|
338
269
|
const buttonEvent = protocol.detectButtonPress(frame);
|
|
339
270
|
if (!buttonEvent) {
|
|
340
271
|
node.log(`不是按键事件(设备类型=${frame.deviceType} 数据类型=${frame.dataType} 操作码=${frame.opCode})`);
|
|
341
272
|
return;
|
|
342
273
|
}
|
|
343
|
-
|
|
344
|
-
node.log(`检测到按键事件: 类型=${buttonEvent.type} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
|
|
274
|
+
|
|
275
|
+
node.log(`检测到按键事件: 类型=${buttonEvent.type} 本地地址=${buttonEvent.raw.localAddr} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
|
|
276
|
+
|
|
277
|
+
// 计算实际按键编号(Symi协议公式)
|
|
278
|
+
// 例如:devAddr=1,channel=1→按键1;devAddr=2,channel=1→按键5
|
|
279
|
+
const actualButtonNumber = buttonEvent.deviceAddr * 4 - 4 + buttonEvent.channel;
|
|
280
|
+
|
|
281
|
+
node.log(`实际按键编号: ${actualButtonNumber}(设备${buttonEvent.deviceAddr} × 4 - 4 + 通道${buttonEvent.channel})`);
|
|
282
|
+
|
|
283
|
+
// 检查是否是我们监听的开关面板和按钮
|
|
284
|
+
// switchId对应本地地址(物理面板地址)
|
|
285
|
+
// buttonNumber对应实际按键编号(1-8)
|
|
286
|
+
if (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber) {
|
|
348
287
|
if (buttonEvent.type === 'single') {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// 发送MQTT命令到继电器
|
|
353
|
-
node.sendMqttCommand(buttonEvent.state);
|
|
354
|
-
} else {
|
|
355
|
-
node.log(`按钮编号不匹配: 收到${buttonEvent.channel} 期望${node.config.buttonNumber}`);
|
|
356
|
-
}
|
|
288
|
+
node.log(`匹配成功!面板${node.config.switchId} 按键${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
|
|
289
|
+
// 发送MQTT命令到继电器
|
|
290
|
+
node.sendMqttCommand(buttonEvent.state);
|
|
357
291
|
} else if (buttonEvent.type === 'multi') {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const state = buttonEvent.buttonStates[buttonIndex];
|
|
362
|
-
node.log(`匹配成功!开关${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${state ? 'ON' : 'OFF'}`);
|
|
363
|
-
// 发送MQTT命令到继电器
|
|
364
|
-
node.sendMqttCommand(state);
|
|
365
|
-
}
|
|
292
|
+
node.log(`匹配成功!面板${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
|
|
293
|
+
// 发送MQTT命令到继电器
|
|
294
|
+
node.sendMqttCommand(buttonEvent.state);
|
|
366
295
|
}
|
|
367
296
|
} else {
|
|
368
|
-
|
|
297
|
+
if (buttonEvent.raw.localAddr !== node.config.switchId) {
|
|
298
|
+
node.log(`面板地址不匹配: 收到${buttonEvent.raw.localAddr} 期望${node.config.switchId}`);
|
|
299
|
+
} else {
|
|
300
|
+
node.log(`按键编号不匹配: 收到${actualButtonNumber} 期望${node.config.buttonNumber}`);
|
|
301
|
+
}
|
|
369
302
|
}
|
|
370
303
|
} catch (err) {
|
|
371
304
|
node.error(`解析RS-485数据失败: ${err.message}`);
|
|
@@ -373,19 +306,27 @@ module.exports = function(RED) {
|
|
|
373
306
|
}
|
|
374
307
|
};
|
|
375
308
|
|
|
376
|
-
//
|
|
309
|
+
// 发送命令到继电器(通过MQTT,由主站节点统一处理)
|
|
377
310
|
node.sendMqttCommand = function(state) {
|
|
378
311
|
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
379
312
|
node.warn('MQTT未连接,无法发送命令');
|
|
380
313
|
return;
|
|
381
314
|
}
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
node.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
node.
|
|
315
|
+
|
|
316
|
+
// 直接发送MQTT命令(不使用队列,立即发送)
|
|
317
|
+
// 主站节点会处理去重和防抖
|
|
318
|
+
const commandTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/set`;
|
|
319
|
+
const payload = state ? 'ON' : 'OFF';
|
|
320
|
+
|
|
321
|
+
node.mqttClient.publish(commandTopic, payload, { qos: 1 }, (err) => {
|
|
322
|
+
if (err) {
|
|
323
|
+
node.error(`MQTT命令发送失败: ${err.message}`);
|
|
324
|
+
} else {
|
|
325
|
+
node.log(`MQTT命令已发送: ${payload} → ${commandTopic}`);
|
|
326
|
+
// 不立即发送LED反馈,等待MQTT状态消息确认后再发送
|
|
327
|
+
// 这样可以确保继电器真的动作了,避免重复发送
|
|
328
|
+
}
|
|
329
|
+
});
|
|
389
330
|
};
|
|
390
331
|
|
|
391
332
|
// 处理命令队列(防止多个按键同时按下造成冲突)
|
|
@@ -396,8 +337,20 @@ module.exports = function(RED) {
|
|
|
396
337
|
|
|
397
338
|
node.isProcessingCommand = true;
|
|
398
339
|
|
|
340
|
+
// 清理过期队列项
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
node.commandQueue = node.commandQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
|
|
343
|
+
|
|
399
344
|
while (node.commandQueue.length > 0) {
|
|
400
|
-
const
|
|
345
|
+
const item = node.commandQueue.shift();
|
|
346
|
+
|
|
347
|
+
// 再次检查是否过期
|
|
348
|
+
if (Date.now() - item.timestamp >= node.queueTimeout) {
|
|
349
|
+
node.warn(`丢弃过期命令(${Date.now() - item.timestamp}ms)`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const state = item.state;
|
|
401
354
|
const command = state ? 'ON' : 'OFF';
|
|
402
355
|
|
|
403
356
|
try {
|
|
@@ -429,57 +382,97 @@ module.exports = function(RED) {
|
|
|
429
382
|
|
|
430
383
|
// 发送控制指令到物理开关面板(控制指示灯等)- 入队
|
|
431
384
|
node.sendCommandToPanel = function(state) {
|
|
432
|
-
|
|
433
|
-
|
|
385
|
+
// 检查连接状态
|
|
386
|
+
if (!node.serialPortConfig || !node.serialPortConfig.connection) {
|
|
387
|
+
// 初始化期间静默警告
|
|
388
|
+
if (!node.isInitializing) {
|
|
389
|
+
node.warn('RS-485连接未建立,无法发送指示灯反馈');
|
|
390
|
+
}
|
|
434
391
|
return;
|
|
435
392
|
}
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
393
|
+
|
|
394
|
+
// 清理过期队列项(超过3秒)
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
|
|
397
|
+
|
|
398
|
+
// 加入LED反馈队列(带时间戳)
|
|
399
|
+
// 注意:这里不指定协议类型,在发送时根据情况选择
|
|
400
|
+
node.ledFeedbackQueue.push({ state, timestamp: now });
|
|
401
|
+
|
|
440
402
|
// 启动队列处理
|
|
441
403
|
node.processLedFeedbackQueue();
|
|
442
404
|
};
|
|
443
405
|
|
|
444
|
-
// 处理LED
|
|
406
|
+
// 处理LED反馈队列(基于面板ID的固定延迟)
|
|
445
407
|
node.processLedFeedbackQueue = async function() {
|
|
446
408
|
if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
|
|
447
409
|
return;
|
|
448
410
|
}
|
|
449
411
|
|
|
412
|
+
// 初始化期间不处理LED反馈(避免部署时大量LED同时发送)
|
|
413
|
+
if (node.isInitializing) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
450
417
|
node.isProcessingLedFeedback = true;
|
|
451
418
|
|
|
419
|
+
// 清理过期队列项
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
|
|
422
|
+
|
|
423
|
+
// 基于面板ID的固定延迟,避免多个节点同时写入TCP冲突
|
|
424
|
+
// 面板1=100ms, 面板2=200ms, 面板3=300ms, 面板4=400ms...
|
|
425
|
+
// 这样不同面板的LED反馈永远不会同时发送
|
|
426
|
+
const fixedDelay = node.config.switchId * 100;
|
|
427
|
+
if (fixedDelay > 0) {
|
|
428
|
+
await new Promise(resolve => setTimeout(resolve, fixedDelay));
|
|
429
|
+
}
|
|
430
|
+
|
|
452
431
|
while (node.ledFeedbackQueue.length > 0) {
|
|
453
|
-
const
|
|
432
|
+
const item = node.ledFeedbackQueue.shift();
|
|
433
|
+
|
|
434
|
+
// 再次检查是否过期
|
|
435
|
+
if (Date.now() - item.timestamp >= node.queueTimeout) {
|
|
436
|
+
node.warn(`丢弃过期LED反馈(${Date.now() - item.timestamp}ms)`);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const state = item.state;
|
|
454
441
|
|
|
455
442
|
try {
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
443
|
+
// 根据按键编号反推设备地址和通道(Symi协议)
|
|
444
|
+
// 例如:按键1→devAddr=1,channel=1;按键5→devAddr=2,channel=1
|
|
445
|
+
const deviceAddr = Math.floor((node.config.buttonNumber - 1) / 4) + 1;
|
|
446
|
+
const channel = ((node.config.buttonNumber - 1) % 4) + 1;
|
|
447
|
+
|
|
448
|
+
// 使用轻量级协议构建LED反馈指令(REPORT类型)
|
|
449
|
+
// 协议规定:面板发送0x03(SET)按键事件,主机应该用0x04(REPORT)反馈LED状态
|
|
450
|
+
// localAddr=面板地址,deviceAddr和channel根据按键编号计算
|
|
451
|
+
const command = protocol.buildSingleLightReport(
|
|
452
|
+
node.config.switchId, // 本地地址(面板地址)
|
|
453
|
+
deviceAddr, // 设备地址(根据按键编号计算)
|
|
454
|
+
channel, // 通道(根据按键编号计算)
|
|
462
455
|
state
|
|
463
456
|
);
|
|
464
|
-
|
|
465
|
-
// 发送到RS-485
|
|
466
|
-
if (node.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
457
|
+
|
|
458
|
+
// 发送到RS-485总线(使用共享连接配置)
|
|
459
|
+
if (node.serialPortConfig) {
|
|
460
|
+
node.serialPortConfig.write(command, (err) => {
|
|
461
|
+
if (err) {
|
|
462
|
+
node.error(`LED反馈发送失败: ${err.message}`);
|
|
463
|
+
} else {
|
|
464
|
+
const hexCmd = Array.from(command).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
|
|
465
|
+
const connType = node.serialPortConfig.connectionType === 'tcp' ? 'TCP' : '串口';
|
|
466
|
+
node.log(`LED反馈已发送(${connType}/REPORT): 面板${node.config.switchId} 按键${node.config.buttonNumber} ${state ? 'ON' : 'OFF'} [${hexCmd}]`);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
472
469
|
} else {
|
|
473
|
-
|
|
474
|
-
if (node.rs485Client._port) {
|
|
475
|
-
node.rs485Client._port.write(command);
|
|
476
|
-
node.log(`LED反馈已发送(串口): 开关${node.config.switchId} 按钮${node.config.buttonNumber} ${state ? 'ON' : 'OFF'}`);
|
|
477
|
-
}
|
|
470
|
+
node.warn('RS-485连接未配置,无法发送LED反馈');
|
|
478
471
|
}
|
|
479
|
-
|
|
480
|
-
// 队列间隔
|
|
472
|
+
|
|
473
|
+
// 队列间隔100ms(确保RS-485总线有足够时间处理)
|
|
481
474
|
if (node.ledFeedbackQueue.length > 0) {
|
|
482
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
475
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
483
476
|
}
|
|
484
477
|
} catch (err) {
|
|
485
478
|
node.error(`发送LED反馈失败: ${err.message}`);
|
|
@@ -537,9 +530,7 @@ module.exports = function(RED) {
|
|
|
537
530
|
if (node.config.mqttUsername) {
|
|
538
531
|
options.username = node.config.mqttUsername;
|
|
539
532
|
options.password = node.config.mqttPassword;
|
|
540
|
-
node.log(`MQTT认证: 用户名=${node.config.mqttUsername}
|
|
541
|
-
} else {
|
|
542
|
-
node.warn('MQTT未配置认证信息,如果broker需要认证将会连接失败');
|
|
533
|
+
node.log(`MQTT认证: 用户名=${node.config.mqttUsername}`);
|
|
543
534
|
}
|
|
544
535
|
|
|
545
536
|
// 尝试连接函数
|
|
@@ -558,6 +549,7 @@ module.exports = function(RED) {
|
|
|
558
549
|
lastConnectAttempt = Date.now();
|
|
559
550
|
|
|
560
551
|
node.mqttClient.on('connect', () => {
|
|
552
|
+
node.mqttConnected = true;
|
|
561
553
|
node.log(`MQTT已连接: ${brokerUrl}`);
|
|
562
554
|
|
|
563
555
|
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
@@ -670,13 +662,29 @@ module.exports = function(RED) {
|
|
|
670
662
|
node.mqttClient.on('message', (topic, message) => {
|
|
671
663
|
if (topic === node.stateTopic) {
|
|
672
664
|
const state = message.toString();
|
|
673
|
-
|
|
674
|
-
|
|
665
|
+
const newState = (state === 'ON' || state === 'true' || state === '1');
|
|
666
|
+
|
|
667
|
+
// 防死循环:如果状态在100ms内没有变化,跳过LED反馈
|
|
668
|
+
const now = Date.now();
|
|
669
|
+
const timeSinceLastChange = now - node.lastStateChange.timestamp;
|
|
670
|
+
const stateChanged = (newState !== node.lastStateChange.value);
|
|
671
|
+
|
|
672
|
+
if (!stateChanged && timeSinceLastChange < 100) {
|
|
673
|
+
// 状态未变化且时间间隔太短,跳过LED反馈(避免死循环)
|
|
674
|
+
node.log(`跳过LED反馈(状态未变化,间隔${timeSinceLastChange}ms)`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 更新状态
|
|
679
|
+
node.currentState = newState;
|
|
680
|
+
node.lastStateChange.timestamp = now;
|
|
681
|
+
node.lastStateChange.value = newState;
|
|
682
|
+
|
|
675
683
|
node.updateStatus();
|
|
676
|
-
|
|
684
|
+
|
|
677
685
|
// 发送控制指令到物理开关面板(同步指示灯等)
|
|
678
686
|
node.sendCommandToPanel(node.currentState);
|
|
679
|
-
|
|
687
|
+
|
|
680
688
|
// 输出状态
|
|
681
689
|
node.send({
|
|
682
690
|
payload: node.currentState,
|
|
@@ -751,23 +759,46 @@ module.exports = function(RED) {
|
|
|
751
759
|
node.reconnectTimer = null;
|
|
752
760
|
}
|
|
753
761
|
|
|
754
|
-
//
|
|
755
|
-
|
|
762
|
+
// 清理队列(释放内存)
|
|
763
|
+
node.commandQueue = [];
|
|
764
|
+
node.ledFeedbackQueue = [];
|
|
765
|
+
node.frameBuffer = Buffer.alloc(0);
|
|
766
|
+
|
|
767
|
+
// 注销RS-485数据监听器(不关闭连接,由配置节点管理)
|
|
768
|
+
if (node.serialPortConfig && node.serialDataListener) {
|
|
756
769
|
try {
|
|
757
|
-
node.
|
|
758
|
-
|
|
759
|
-
});
|
|
770
|
+
node.serialPortConfig.unregisterDataListener(node.serialDataListener);
|
|
771
|
+
node.log('RS-485数据监听器已注销');
|
|
760
772
|
} catch (err) {
|
|
761
|
-
node.warn(
|
|
773
|
+
node.warn(`注销RS-485监听器时出错: ${err.message}`);
|
|
762
774
|
}
|
|
775
|
+
node.serialDataListener = null;
|
|
763
776
|
}
|
|
764
777
|
|
|
765
778
|
// 关闭MQTT连接
|
|
766
|
-
if (node.mqttClient
|
|
767
|
-
|
|
768
|
-
node.
|
|
779
|
+
if (node.mqttClient) {
|
|
780
|
+
try {
|
|
781
|
+
if (node.mqttClient.connected) {
|
|
782
|
+
// 移除所有监听器(防止内存泄漏)
|
|
783
|
+
node.mqttClient.removeAllListeners();
|
|
784
|
+
node.mqttClient.end(false, () => {
|
|
785
|
+
node.log('MQTT连接已关闭');
|
|
786
|
+
node.mqttClient = null;
|
|
787
|
+
node.mqttConnected = false;
|
|
788
|
+
done();
|
|
789
|
+
});
|
|
790
|
+
} else {
|
|
791
|
+
node.mqttClient.removeAllListeners();
|
|
792
|
+
node.mqttClient = null;
|
|
793
|
+
node.mqttConnected = false;
|
|
794
|
+
done();
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
node.warn(`关闭MQTT连接时出错: ${err.message}`);
|
|
798
|
+
node.mqttClient = null;
|
|
799
|
+
node.mqttConnected = false;
|
|
769
800
|
done();
|
|
770
|
-
}
|
|
801
|
+
}
|
|
771
802
|
} else {
|
|
772
803
|
done();
|
|
773
804
|
}
|