node-red-contrib-symi-modbus 2.3.0 → 2.4.0
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 +30 -25
- package/nodes/modbus-slave-switch.js +163 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -507,41 +507,46 @@ docker run --privileged ...
|
|
|
507
507
|
- **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
|
|
508
508
|
|
|
509
509
|
|
|
510
|
-
> **最新版本 v2.
|
|
510
|
+
> **最新版本 v2.4.0** - TCP流式数据帧缓冲、命令队列、LED反馈队列,确保100%稳定控制和反馈
|
|
511
511
|
|
|
512
512
|
## 版本更新
|
|
513
513
|
|
|
514
|
-
### v2.
|
|
514
|
+
### v2.4.0 (2025-10-20) - TCP帧缓冲与命令队列机制
|
|
515
515
|
|
|
516
516
|
**核心修复**:
|
|
517
|
-
-
|
|
518
|
-
-
|
|
519
|
-
-
|
|
520
|
-
-
|
|
521
|
-
-
|
|
522
|
-
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
-
|
|
528
|
-
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
-
|
|
532
|
-
-
|
|
533
|
-
-
|
|
534
|
-
-
|
|
535
|
-
|
|
517
|
+
- 添加TCP流式数据帧缓冲区,正确处理分包和粘包问题
|
|
518
|
+
- 帧边界检测(7E开始,7D结束),确保完整帧解析
|
|
519
|
+
- 添加MQTT命令队列,防止多个按键同时按下造成冲突
|
|
520
|
+
- 添加LED指示灯反馈队列(40ms间隔),避免RS-485总线过载
|
|
521
|
+
- 增强RS485连接状态提示(明确显示TCP/串口连接成功和监听状态)
|
|
522
|
+
- 分离TCP和串口数据处理逻辑(TCP使用缓冲区,串口直接处理)
|
|
523
|
+
|
|
524
|
+
**TCP帧缓冲机制**:
|
|
525
|
+
- TCP是流式协议,一个完整的协议帧可能被分成多个数据包
|
|
526
|
+
- 或者多个帧合并在一个数据包中
|
|
527
|
+
- 使用缓冲区累积数据,查找帧头(0x7E)和帧尾(0x7D)
|
|
528
|
+
- 提取完整帧后再解析,确保CRC校验正确
|
|
529
|
+
|
|
530
|
+
**队列机制**:
|
|
531
|
+
- **命令队列**:多个按键同时按下时,按顺序发送MQTT命令(40ms间隔)
|
|
532
|
+
- **LED反馈队列**:继电器状态反馈到指示灯时,按顺序发送(40ms间隔)
|
|
533
|
+
- 避免MQTT broker和RS-485总线过载
|
|
534
|
+
- 确保每个命令和反馈都被正确处理
|
|
535
|
+
|
|
536
|
+
**工作流程**:
|
|
537
|
+
```
|
|
538
|
+
物理按键 → TCP数据 → 帧缓冲区 → 提取完整帧 → 解析 → 命令队列 → MQTT发布
|
|
539
|
+
继电器状态变化 → MQTT状态 → LED反馈队列 → RS-485总线 → 指示灯同步
|
|
540
|
+
```
|
|
536
541
|
|
|
537
542
|
**测试验证**:
|
|
538
|
-
-
|
|
539
|
-
-
|
|
540
|
-
-
|
|
543
|
+
- TCP网关(192.168.2.110:1031)连接正常
|
|
544
|
+
- 帧缓冲区正确处理分包数据
|
|
545
|
+
- 多个按键同时按下不丢失
|
|
546
|
+
- LED指示灯同步稳定
|
|
541
547
|
- 适合Linux工控机24/7长期稳定运行
|
|
542
548
|
|
|
543
549
|
---
|
|
544
|
-
|
|
545
550
|
**升级方式**:
|
|
546
551
|
```bash
|
|
547
552
|
cd ~/.node-red
|
|
@@ -96,6 +96,17 @@ module.exports = function(RED) {
|
|
|
96
96
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
97
97
|
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
98
98
|
|
|
99
|
+
// TCP帧缓冲区(用于处理分包和粘包)
|
|
100
|
+
node.frameBuffer = Buffer.alloc(0);
|
|
101
|
+
|
|
102
|
+
// 命令队列(处理多个按键同时按下)
|
|
103
|
+
node.commandQueue = [];
|
|
104
|
+
node.isProcessingCommand = false;
|
|
105
|
+
|
|
106
|
+
// 指示灯反馈队列(40ms间隔发送)
|
|
107
|
+
node.ledFeedbackQueue = [];
|
|
108
|
+
node.isProcessingLedFeedback = false;
|
|
109
|
+
|
|
99
110
|
// MQTT主题(映射到继电器设备)
|
|
100
111
|
node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
|
|
101
112
|
node.commandTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/set`;
|
|
@@ -235,20 +246,75 @@ module.exports = function(RED) {
|
|
|
235
246
|
|
|
236
247
|
// 监听RS-485数据
|
|
237
248
|
if (node.config.connectionType === "tcp") {
|
|
238
|
-
// TCP模式:监听socket
|
|
249
|
+
// TCP模式:监听socket数据(需要处理帧边界)
|
|
239
250
|
if (node.rs485Client._port && node.rs485Client._port.socket) {
|
|
240
251
|
const socket = node.rs485Client._port.socket;
|
|
241
252
|
socket.on('data', (data) => {
|
|
242
|
-
node.
|
|
253
|
+
node.handleTcpData(data);
|
|
243
254
|
});
|
|
255
|
+
node.log('TCP数据监听已启动(使用帧缓冲区)');
|
|
244
256
|
}
|
|
245
257
|
} else {
|
|
246
|
-
//
|
|
258
|
+
// 串口模式:监听串口数据(串口通常自动处理帧边界)
|
|
247
259
|
if (node.rs485Client._port) {
|
|
248
260
|
node.rs485Client._port.on('data', (data) => {
|
|
249
261
|
node.handleRs485Data(data);
|
|
250
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);
|
|
251
314
|
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
node.error(`TCP数据处理错误: ${err.message}`);
|
|
317
|
+
node.frameBuffer = Buffer.alloc(0); // 错误时清空缓冲区
|
|
252
318
|
}
|
|
253
319
|
};
|
|
254
320
|
|
|
@@ -307,59 +373,120 @@ module.exports = function(RED) {
|
|
|
307
373
|
}
|
|
308
374
|
};
|
|
309
375
|
|
|
310
|
-
// 发送MQTT
|
|
376
|
+
// 发送MQTT命令到继电器(入队)
|
|
311
377
|
node.sendMqttCommand = function(state) {
|
|
312
378
|
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
313
379
|
node.warn('MQTT未连接,无法发送命令');
|
|
314
380
|
return;
|
|
315
381
|
}
|
|
316
382
|
|
|
317
|
-
|
|
318
|
-
node.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
383
|
+
// 加入命令队列
|
|
384
|
+
node.commandQueue.push(state);
|
|
385
|
+
node.log(`MQTT命令已入队,队列长度: ${node.commandQueue.length}`);
|
|
386
|
+
|
|
387
|
+
// 启动队列处理
|
|
388
|
+
node.processCommandQueue();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// 处理命令队列(防止多个按键同时按下造成冲突)
|
|
392
|
+
node.processCommandQueue = async function() {
|
|
393
|
+
if (node.isProcessingCommand || node.commandQueue.length === 0) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
node.isProcessingCommand = true;
|
|
398
|
+
|
|
399
|
+
while (node.commandQueue.length > 0) {
|
|
400
|
+
const state = node.commandQueue.shift();
|
|
401
|
+
const command = state ? 'ON' : 'OFF';
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await new Promise((resolve, reject) => {
|
|
405
|
+
node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
|
|
406
|
+
if (err) {
|
|
407
|
+
reject(err);
|
|
408
|
+
} else {
|
|
409
|
+
resolve();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
node.log(`MQTT命令已发送: ${command} → ${node.commandTopic}`);
|
|
323
415
|
node.currentState = state;
|
|
324
416
|
node.updateStatus();
|
|
417
|
+
|
|
418
|
+
// 队列间隔40ms(避免MQTT broker过载)
|
|
419
|
+
if (node.commandQueue.length > 0) {
|
|
420
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
node.error(`发布MQTT命令失败: ${err.message}`);
|
|
325
424
|
}
|
|
326
|
-
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
node.isProcessingCommand = false;
|
|
327
428
|
};
|
|
328
429
|
|
|
329
|
-
//
|
|
330
|
-
node.sendCommandToPanel =
|
|
430
|
+
// 发送控制指令到物理开关面板(控制指示灯等)- 入队
|
|
431
|
+
node.sendCommandToPanel = function(state) {
|
|
331
432
|
if (!node.isRs485Connected) {
|
|
332
|
-
node.warn('RS-485
|
|
433
|
+
node.warn('RS-485未连接,无法发送指示灯反馈');
|
|
333
434
|
return;
|
|
334
435
|
}
|
|
335
436
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
437
|
+
// 加入LED反馈队列
|
|
438
|
+
node.ledFeedbackQueue.push(state);
|
|
439
|
+
|
|
440
|
+
// 启动队列处理
|
|
441
|
+
node.processLedFeedbackQueue();
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// 处理LED反馈队列(40ms间隔发送)
|
|
445
|
+
node.processLedFeedbackQueue = async function() {
|
|
446
|
+
if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
node.isProcessingLedFeedback = true;
|
|
451
|
+
|
|
452
|
+
while (node.ledFeedbackQueue.length > 0) {
|
|
453
|
+
const state = node.ledFeedbackQueue.shift();
|
|
345
454
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
node.
|
|
455
|
+
try {
|
|
456
|
+
// 使用轻量级协议构建单灯控制指令
|
|
457
|
+
// localAddr=0x01(本机),deviceAddr=开关ID,channel=按钮编号,state=开关状态
|
|
458
|
+
const command = protocol.buildSingleLightCommand(
|
|
459
|
+
0x01, // 本机地址(网关地址)
|
|
460
|
+
node.config.switchId,
|
|
461
|
+
node.config.buttonNumber,
|
|
462
|
+
state
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// 发送到RS-485总线
|
|
466
|
+
if (node.config.connectionType === "tcp") {
|
|
467
|
+
// TCP模式
|
|
468
|
+
if (node.rs485Client._port && node.rs485Client._port.socket) {
|
|
469
|
+
node.rs485Client._port.socket.write(command);
|
|
470
|
+
node.log(`LED反馈已发送(TCP): 开关${node.config.switchId} 按钮${node.config.buttonNumber} ${state ? 'ON' : 'OFF'}`);
|
|
471
|
+
}
|
|
472
|
+
} 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
|
+
}
|
|
352
478
|
}
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
if (node.
|
|
356
|
-
|
|
357
|
-
node.log(`发送控制指令(串口): 开关${node.config.switchId} 按钮${node.config.buttonNumber} ${state ? 'ON' : 'OFF'}`);
|
|
479
|
+
|
|
480
|
+
// 队列间隔40ms
|
|
481
|
+
if (node.ledFeedbackQueue.length > 0) {
|
|
482
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
358
483
|
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
node.error(`发送LED反馈失败: ${err.message}`);
|
|
359
486
|
}
|
|
360
|
-
} catch (err) {
|
|
361
|
-
node.error(`发送控制指令失败: ${err.message}`);
|
|
362
487
|
}
|
|
488
|
+
|
|
489
|
+
node.isProcessingLedFeedback = false;
|
|
363
490
|
};
|
|
364
491
|
|
|
365
492
|
// 更新节点状态显示
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、智能MQTT连接(自动fallback HassOS/Docker环境)、Home Assistant自动发现和多品牌开关面板,生产级稳定版本",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|