node-red-contrib-symi-modbus 2.7.9 → 2.8.1
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 +28 -31
- package/nodes/custom-protocol.html +1 -1
- package/nodes/homekit-bridge.html +1 -1
- package/nodes/modbus-dashboard.html +1 -1
- package/nodes/modbus-debug.html +1 -1
- package/nodes/modbus-master.html +1 -1
- package/nodes/modbus-master.js +2 -2
- package/nodes/modbus-slave-switch.html +1 -1
- package/nodes/modbus-slave-switch.js +16 -2
- package/nodes/serial-port-config.js +40 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -251,8 +251,8 @@ node-red-restart
|
|
|
251
251
|
1. **HomeKit编组群控**:
|
|
252
252
|
- 在HomeKit中创建房间或编组,可同时控制多个继电器
|
|
253
253
|
- 例如:创建"客厅"编组,包含10个灯光开关,一键全开/全关
|
|
254
|
-
-
|
|
255
|
-
- 10个继电器同时动作仅需约
|
|
254
|
+
- 智能优先队列机制确保触发源面板最快响应,其他继电器按序执行
|
|
255
|
+
- 10个继电器同时动作仅需约400ms(40ms间隔×10),触发源面板优先反馈
|
|
256
256
|
|
|
257
257
|
2. **场景联动**:
|
|
258
258
|
- 支持HomeKit场景(如"回家模式"、"离家模式")
|
|
@@ -267,7 +267,8 @@ node-red-restart
|
|
|
267
267
|
- 长期稳定运行,反复控制不会造成内存增加、卡顿或死机
|
|
268
268
|
|
|
269
269
|
4. **技术细节**:
|
|
270
|
-
- 写入队列间隔:
|
|
270
|
+
- 写入队列间隔:40ms(确保总线稳定,优化指示灯反馈)
|
|
271
|
+
- 智能优先队列:触发源面板优先处理(500ms优先窗口)
|
|
271
272
|
- 轮询恢复时间:20ms(快速响应)
|
|
272
273
|
- 锁等待超时:100ms(快速检测异常)
|
|
273
274
|
- 队列自动处理,无需手动干预
|
|
@@ -883,53 +884,49 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
883
884
|
|
|
884
885
|
## 版本信息
|
|
885
886
|
|
|
886
|
-
**当前版本**: v2.
|
|
887
|
+
**当前版本**: v2.8.1
|
|
887
888
|
|
|
888
|
-
### v2.
|
|
889
|
+
### v2.8.1 (2025-11-11)
|
|
889
890
|
|
|
890
891
|
**重要更新**:
|
|
891
|
-
-
|
|
892
|
-
-
|
|
893
|
-
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
**重要修复**:
|
|
900
|
-
- 修复LED反馈队列处理逻辑:移除本地队列,直接使用全局队列(serial-port-config统一管理)
|
|
901
|
-
- 确保所有LED反馈按顺序串行发送,间隔20ms(TCP)或10ms(串口)
|
|
902
|
-
- 修复多从站继电器同时变化时的LED反馈遗漏问题
|
|
903
|
-
- 确保双控/多控场景下所有开关面板都能收到完整的状态反馈
|
|
904
|
-
|
|
905
|
-
**队列处理机制**(全局统一队列):
|
|
892
|
+
- 优化指示灯反馈间隔为40ms(从30ms调整),进一步提升总线稳定性
|
|
893
|
+
- 实现智能优先队列机制:触发场景的开关面板优先获得LED反馈
|
|
894
|
+
- 提升用户体验:操作的面板最快响应,其他面板按序反馈
|
|
895
|
+
|
|
896
|
+
**智能优先队列机制**:
|
|
897
|
+
- **触发源优先**:当某个开关触发场景时,该开关面板的LED反馈会优先处理
|
|
898
|
+
- **优先窗口**:触发后500ms内,该面板的所有LED反馈都会插入到队列前面
|
|
899
|
+
- **用户体验优化**:用户操作的面板会最快看到反馈,其他面板即使慢一点也不会被察觉
|
|
906
900
|
- **全局队列**:所有从站开关节点共享同一个全局队列(由serial-port-config管理)
|
|
907
|
-
- **串行发送**:所有LED
|
|
901
|
+
- **串行发送**:所有LED反馈按优先级和加入顺序依次发送,间隔40ms(TCP和串口统一)
|
|
908
902
|
- **完整性保证**:所有LED反馈都能正确发送,不会遗漏
|
|
909
903
|
- **去重机制**:每个节点50ms内不重复发送相同状态,避免总线拥堵
|
|
910
|
-
- **性能优化**:400个按键(50个8键开关)反馈耗时约8秒(20ms×400)
|
|
911
904
|
|
|
912
905
|
**典型场景说明**:
|
|
913
906
|
1. **场景1:单个继电器控制**
|
|
914
|
-
- 按下开关 → 继电器动作 → LED
|
|
907
|
+
- 按下开关 → 设置触发源 → 继电器动作 → LED反馈优先加入队列 → 40ms后发送
|
|
908
|
+
- 该面板的LED反馈会优先处理,响应最快
|
|
915
909
|
|
|
916
910
|
2. **场景2:批量控制(如17通道全开,16个继电器)**
|
|
917
|
-
- 触发17通道 → 1-16路继电器全开
|
|
911
|
+
- 触发17通道 → 设置触发源(操作的面板) → 1-16路继电器全开
|
|
918
912
|
- 假设每个继电器绑定2个开关面板 = 32个LED反馈
|
|
919
|
-
-
|
|
913
|
+
- 触发源面板的LED反馈优先处理(最快响应)
|
|
914
|
+
- 其他面板按加入队列的顺序依次发送,耗时约1280ms(40ms×32)
|
|
920
915
|
|
|
921
916
|
3. **场景3:快速切换(如17通道全开后立即18通道全关)**
|
|
922
|
-
- 17通道触发 → 1-16路全开 →
|
|
923
|
-
- 18通道触发 → 1-16路全关 →
|
|
924
|
-
-
|
|
917
|
+
- 17通道触发 → 设置触发源 → 1-16路全开 → 触发源面板优先反馈
|
|
918
|
+
- 18通道触发 → 更新触发源 → 1-16路全关 → 新触发源面板优先反馈
|
|
919
|
+
- 全局队列按优先级和顺序处理,确保所有面板最终显示正确状态
|
|
925
920
|
|
|
926
921
|
4. **场景4:双控开关(1个继电器绑定2个不同ID的开关面板)**
|
|
927
922
|
- 继电器状态变化 → 2个LED反馈加入全局队列
|
|
928
|
-
-
|
|
923
|
+
- 如果是其中一个面板触发的,该面板优先反馈
|
|
924
|
+
- 另一个面板按顺序发送,间隔40ms
|
|
929
925
|
|
|
930
926
|
5. **场景5:大规模部署(50个8键开关 = 400个按键)**
|
|
931
|
-
- 批量控制触发 → 所有LED反馈加入全局队列
|
|
932
|
-
-
|
|
927
|
+
- 批量控制触发 → 设置触发源 → 所有LED反馈加入全局队列
|
|
928
|
+
- 触发源面板优先反馈(用户最快看到响应)
|
|
929
|
+
- 其他面板按部署时的节点顺序依次发送,耗时约16秒(40ms×400)
|
|
933
930
|
- 确保总线稳定,不会冲突或丢失
|
|
934
931
|
|
|
935
932
|
## 许可证
|
package/nodes/modbus-debug.html
CHANGED
package/nodes/modbus-master.html
CHANGED
package/nodes/modbus-master.js
CHANGED
|
@@ -150,7 +150,7 @@ module.exports = function(RED) {
|
|
|
150
150
|
// 写入队列机制(确保所有写入操作串行执行,避免锁竞争)
|
|
151
151
|
node.writeQueue = []; // 写入队列
|
|
152
152
|
node.isProcessingWrite = false; // 是否正在处理写入队列
|
|
153
|
-
node.writeQueueInterval =
|
|
153
|
+
node.writeQueueInterval = 40; // 写入队列处理间隔(40ms,确保总线稳定,避免数据丢失)
|
|
154
154
|
|
|
155
155
|
// 定期清理机制(每小时清理一次,防止内存泄漏)
|
|
156
156
|
node.cleanupTimer = setInterval(() => {
|
|
@@ -1197,7 +1197,7 @@ module.exports = function(RED) {
|
|
|
1197
1197
|
node.warn(`队列任务失败,继续处理下一个任务: ${err.message}`);
|
|
1198
1198
|
}
|
|
1199
1199
|
|
|
1200
|
-
// 等待一段时间再处理下一个任务(
|
|
1200
|
+
// 等待一段时间再处理下一个任务(40ms间隔,确保总线稳定)
|
|
1201
1201
|
if (node.writeQueue.length > 0) {
|
|
1202
1202
|
await new Promise(resolve => setTimeout(resolve, node.writeQueueInterval));
|
|
1203
1203
|
}
|
|
@@ -634,6 +634,11 @@ module.exports = function(RED) {
|
|
|
634
634
|
}
|
|
635
635
|
globalDebounceCache.set(debounceKey, now);
|
|
636
636
|
|
|
637
|
+
// 设置触发源(用于优先队列)
|
|
638
|
+
if (node.serialPortConfig && typeof node.serialPortConfig.setTriggerSource === 'function') {
|
|
639
|
+
node.serialPortConfig.setTriggerSource(node.config.switchId);
|
|
640
|
+
}
|
|
641
|
+
|
|
637
642
|
if (isSceneMode) {
|
|
638
643
|
node.debug(`场景触发: 面板${node.config.switchId} 按键${node.config.buttonNumber} (opInfo=0x${buttonEvent.opInfo.toString(16).toUpperCase()})`);
|
|
639
644
|
// 场景模式:切换状态(每次触发时翻转)
|
|
@@ -695,6 +700,11 @@ module.exports = function(RED) {
|
|
|
695
700
|
// 更新当前状态缓存(用于后续控制时保持其他路不变)
|
|
696
701
|
node.meshCurrentStates = event.states;
|
|
697
702
|
|
|
703
|
+
// 设置触发源(用于优先队列)- Mesh使用短地址作为switchId
|
|
704
|
+
if (node.serialPortConfig && typeof node.serialPortConfig.setTriggerSource === 'function') {
|
|
705
|
+
node.serialPortConfig.setTriggerSource(node.config.meshShortAddress);
|
|
706
|
+
}
|
|
707
|
+
|
|
698
708
|
// 发送命令到继电器
|
|
699
709
|
node.debug(`Mesh开关${buttonState ? 'ON' : 'OFF'}: MAC=${node.config.meshMacAddress} 按键${node.config.meshButtonNumber}`);
|
|
700
710
|
node.sendMqttCommand(buttonState);
|
|
@@ -859,7 +869,11 @@ module.exports = function(RED) {
|
|
|
859
869
|
}
|
|
860
870
|
}
|
|
861
871
|
|
|
862
|
-
// 直接发送到全局队列(由serial-port-config统一管理,
|
|
872
|
+
// 直接发送到全局队列(由serial-port-config统一管理,40ms间隔串行发送)
|
|
873
|
+
// 传递优先级和switchId,触发源的面板会优先处理
|
|
874
|
+
const priority = 0; // 默认优先级
|
|
875
|
+
const switchId = node.config.buttonType === 'mesh' ? node.config.meshShortAddress : node.config.switchId;
|
|
876
|
+
|
|
863
877
|
node.serialPortConfig.write(command, (err) => {
|
|
864
878
|
if (err) {
|
|
865
879
|
node.error(`LED反馈失败: ${err.message}`);
|
|
@@ -874,7 +888,7 @@ module.exports = function(RED) {
|
|
|
874
888
|
node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
|
|
875
889
|
}
|
|
876
890
|
}
|
|
877
|
-
});
|
|
891
|
+
}, priority, switchId);
|
|
878
892
|
};
|
|
879
893
|
|
|
880
894
|
// 更新节点状态显示
|
|
@@ -40,6 +40,10 @@ module.exports = function(RED) {
|
|
|
40
40
|
node.writeQueue = [];
|
|
41
41
|
node.isWriting = false;
|
|
42
42
|
|
|
43
|
+
// 触发源跟踪(用于优先队列)
|
|
44
|
+
node.triggerSource = null; // 当前触发场景的开关ID
|
|
45
|
+
node.triggerSourceTime = 0; // 触发时间戳
|
|
46
|
+
|
|
43
47
|
// 打开TCP连接
|
|
44
48
|
node.openTcpConnection = function() {
|
|
45
49
|
if (node.connection && !node.connection.destroyed) {
|
|
@@ -347,10 +351,38 @@ module.exports = function(RED) {
|
|
|
347
351
|
}
|
|
348
352
|
};
|
|
349
353
|
|
|
350
|
-
//
|
|
351
|
-
node.
|
|
354
|
+
// 设置触发源(用于优先队列)
|
|
355
|
+
node.setTriggerSource = function(switchId) {
|
|
356
|
+
node.triggerSource = switchId;
|
|
357
|
+
node.triggerSourceTime = Date.now();
|
|
358
|
+
// 500ms后清除触发源(避免长时间影响队列)
|
|
359
|
+
setTimeout(() => {
|
|
360
|
+
if (Date.now() - node.triggerSourceTime >= 500) {
|
|
361
|
+
node.triggerSource = null;
|
|
362
|
+
}
|
|
363
|
+
}, 500);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 写入数据(带队列机制,防止并发冲突,支持优先级)
|
|
367
|
+
node.write = function(data, callback, priority, switchId) {
|
|
352
368
|
// 加入写入队列
|
|
353
|
-
|
|
369
|
+
const queueItem = { data, callback, priority: priority || 0, switchId };
|
|
370
|
+
|
|
371
|
+
// 如果有优先级或者是触发源,插入到队列前面
|
|
372
|
+
if (priority > 0 || (switchId && switchId === node.triggerSource)) {
|
|
373
|
+
// 找到第一个非优先级项的位置
|
|
374
|
+
let insertIndex = 0;
|
|
375
|
+
for (let i = 0; i < node.writeQueue.length; i++) {
|
|
376
|
+
if (node.writeQueue[i].priority === 0 && node.writeQueue[i].switchId !== node.triggerSource) {
|
|
377
|
+
insertIndex = i;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
insertIndex = i + 1;
|
|
381
|
+
}
|
|
382
|
+
node.writeQueue.splice(insertIndex, 0, queueItem);
|
|
383
|
+
} else {
|
|
384
|
+
node.writeQueue.push(queueItem);
|
|
385
|
+
}
|
|
354
386
|
|
|
355
387
|
// 启动队列处理
|
|
356
388
|
node.processWriteQueue();
|
|
@@ -405,9 +437,9 @@ module.exports = function(RED) {
|
|
|
405
437
|
});
|
|
406
438
|
});
|
|
407
439
|
|
|
408
|
-
// TCP写入间隔(
|
|
440
|
+
// TCP写入间隔(40ms,确保指示灯反馈稳定)
|
|
409
441
|
if (node.writeQueue.length > 0) {
|
|
410
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
442
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
411
443
|
}
|
|
412
444
|
} else {
|
|
413
445
|
if (!node.connection.isOpen) {
|
|
@@ -439,9 +471,9 @@ module.exports = function(RED) {
|
|
|
439
471
|
});
|
|
440
472
|
});
|
|
441
473
|
|
|
442
|
-
// 串口写入间隔(
|
|
474
|
+
// 串口写入间隔(40ms,确保指示灯反馈稳定)
|
|
443
475
|
if (node.writeQueue.length > 0) {
|
|
444
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
476
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
445
477
|
}
|
|
446
478
|
}
|
|
447
479
|
} catch (err) {
|
|
@@ -513,7 +545,7 @@ module.exports = function(RED) {
|
|
|
513
545
|
RED.nodes.registerType('serial-port-config', SerialPortConfigNode);
|
|
514
546
|
|
|
515
547
|
// 提供串口搜索API
|
|
516
|
-
RED.httpAdmin.get('/serial-ports', async (
|
|
548
|
+
RED.httpAdmin.get('/serial-ports', async (_req, res) => {
|
|
517
549
|
try {
|
|
518
550
|
const { SerialPort } = require('serialport');
|
|
519
551
|
const ports = await SerialPort.list();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|