node-red-contrib-symi-modbus 2.9.13 → 2.9.14
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 +23 -24
- package/nodes/modbus-slave-switch.js +69 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -950,32 +950,31 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
950
950
|
|
|
951
951
|
## 版本历史
|
|
952
952
|
|
|
953
|
-
|
|
954
|
-
-
|
|
955
|
-
-
|
|
956
|
-
-
|
|
957
|
-
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
-
|
|
961
|
-
-
|
|
962
|
-
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
-
|
|
968
|
-
-
|
|
953
|
+
**v2.9.14 更新内容**:
|
|
954
|
+
- **全协议指示灯同步优化**:重构了从站开关节点(modbus-slave-switch)的状态同步逻辑,将原有的 Mesh 模式高性能 LED 反馈机制扩展至所有协议(Symi、Clowire、RS-485),显著提升了群控场景下指示灯同步的准确率。
|
|
955
|
+
- **非连续线圈配置支持**:优化了继电器控制逻辑,支持按键与非连续、跨从站线圈的灵活绑定,不再受限于线圈序号必须连续的物理约束。
|
|
956
|
+
- **全局并发锁机制改进**:改进了全局 LED 反馈锁(Global Lock)机制,将锁定时间优化为 150ms,并引入了更精准的面板状态缓存(panelKey-based caching),有效解决了高频群控时个别按键指示灯同步失败的问题。
|
|
957
|
+
- **稳定性与防抖增强**:统一了所有协议的指示灯同步防抖时间为 50ms,确保在继电器频繁动作时总线通信的有序与稳定。
|
|
958
|
+
|
|
959
|
+
**v2.9.13 更新内容**:
|
|
960
|
+
- **全量审计与生产环境验证**:确认多主站隔离、内存自动清理和防死锁机制稳定运行。
|
|
961
|
+
- **文档规范化**:修复并补充了从站开关节点的内置帮助文档,增加 Mesh 和无线模式说明。
|
|
962
|
+
- **发布质量保障**:完成严格的本地安装测试和 npm pack 验证。
|
|
963
|
+
|
|
964
|
+
**v2.9.12 更新内容**:
|
|
965
|
+
- **Mesh 持久化**:设备列表自动保存到磁盘,重启不丢失。
|
|
966
|
+
- **RS-485 沾包处理**:重构拼包逻辑,引入循环缓冲区处理沾包、分包问题。
|
|
967
|
+
- **门禁过滤**:继电器输出节点支持门禁 ID 过滤。
|
|
968
|
+
- **事件联动**:修复 `modbus:buttonPressed` 事件,支持开关与继电器输出节点联动。
|
|
969
|
+
|
|
970
|
+
**v2.9.11 更新内容**:
|
|
971
|
+
- **Clowire 支持**:增加对克伦威尔协议的支持。
|
|
972
|
+
- **拼包算法优化**:进一步解决工控机串口高频分包问题。
|
|
969
973
|
|
|
970
974
|
**v2.9.10 更新内容**:
|
|
971
|
-
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
- **状态栏增强**:将具体错误信息(如“拒绝连接”、“串口不存在”)直接显示在节点状态文字中,无需查看日志即可掌握连接状况。
|
|
975
|
-
- **多主站配置隔离修复**:
|
|
976
|
-
- 修复了多个Modbus主站节点在编辑时配置互相干扰的问题(通过Scoped Event Handler实现)
|
|
977
|
-
- 确保每个主站节点的从站列表配置完全独立,互不影响
|
|
978
|
-
- **从站开关主站关联**:
|
|
975
|
+
- **日志静默优化**:重连期间不再刷屏调试面板,错误信息显示在节点状态栏。
|
|
976
|
+
- **多主站配置隔离**:修复多个主站节点配置互相干扰的问题。
|
|
977
|
+
- **稳定性增强**:优化了内存管理和重连机制。
|
|
979
978
|
- **新增关联主站功能**:从站开关节点新增"关联主站"配置项
|
|
980
979
|
- **多主站支持**:支持选择特定的主站节点,解决多主站环境下相同从站地址冲突的问题
|
|
981
980
|
- **智能过滤**:从站开关只响应关联主站的状态更新,避免误触发
|
|
@@ -609,171 +609,111 @@ module.exports = function(RED) {
|
|
|
609
609
|
node.debug(`收到首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
612
|
+
// 核心变量准备
|
|
613
|
+
const isMesh = (node.config.buttonType === 'mesh');
|
|
614
|
+
const panelKey = isMesh ? node.config.meshShortAddress : node.config.switchId;
|
|
615
|
+
const isButtonPress = (triggerSource === 'button-press' || triggerSource === 'scene-trigger');
|
|
616
|
+
const isInit = (triggerSource === 'init' || data.source === 'init');
|
|
617
|
+
const isRelayControl = !isButtonPress && !isInit;
|
|
618
|
+
|
|
619
|
+
// 更新全局状态缓存(用于所有协议的LED反馈优化)
|
|
620
|
+
if (panelKey) {
|
|
621
|
+
if (!meshDeviceStates.has(panelKey)) {
|
|
622
|
+
const totalButtons = isMesh ? node.config.meshTotalButtons : 8; // 非Mesh默认支持8路
|
|
623
|
+
meshDeviceStates.set(panelKey, new Array(totalButtons).fill(null));
|
|
624
|
+
}
|
|
625
|
+
const states = meshDeviceStates.get(panelKey);
|
|
626
|
+
|
|
627
|
+
// 计算按键索引
|
|
628
|
+
let buttonIndex = -1;
|
|
629
|
+
if (isMesh) {
|
|
630
|
+
const baseCoil = node.config.targetCoilNumber - (node.config.meshButtonNumber - 1);
|
|
631
|
+
buttonIndex = coil - baseCoil;
|
|
632
|
+
} else if (slave === node.config.targetSlaveAddress) {
|
|
633
|
+
// RS-485模式:假设线圈是连续的,或者通过targetCoilNumber定位
|
|
634
|
+
// 这里我们优先处理当前节点绑定的按键
|
|
635
|
+
if (coil === node.config.targetCoilNumber) {
|
|
636
|
+
buttonIndex = node.config.buttonNumber - 1;
|
|
637
|
+
}
|
|
618
638
|
}
|
|
619
639
|
|
|
620
|
-
|
|
621
|
-
const baseCoil = node.config.targetCoilNumber - (node.config.meshButtonNumber - 1);
|
|
622
|
-
const buttonIndex = coil - baseCoil;
|
|
623
|
-
|
|
624
|
-
// 检查是否是同一个面板的线圈(线圈范围:baseCoil ~ baseCoil+totalButtons-1)
|
|
625
|
-
if (buttonIndex >= 0 && buttonIndex < node.config.meshTotalButtons) {
|
|
626
|
-
// 更新全局状态缓存
|
|
627
|
-
const states = meshDeviceStates.get(meshAddr);
|
|
640
|
+
if (buttonIndex >= 0 && buttonIndex < states.length) {
|
|
628
641
|
const oldValue = states[buttonIndex];
|
|
629
642
|
states[buttonIndex] = value;
|
|
630
643
|
const stateChanged = (oldValue !== value);
|
|
631
644
|
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
const isInit = (triggerSource === 'init' || data.source === 'init');
|
|
635
|
-
const isRelayControl = !isButtonPress && !isInit;
|
|
636
|
-
|
|
637
|
-
// 继电器控制:所有同一面板的线圈变化都会重置定时器(只有按钮1节点负责)
|
|
638
|
-
if (isRelayControl && stateChanged && node.config.meshButtonNumber === 1) {
|
|
645
|
+
// 继电器控制:面板关联的任何线圈变化都会触发LED同步
|
|
646
|
+
if (isRelayControl && stateChanged) {
|
|
639
647
|
const now = Date.now();
|
|
640
|
-
|
|
641
|
-
// 获取或创建定时器对象
|
|
642
|
-
let timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
648
|
+
let timerObj = meshLedDebounceTimers.get(panelKey);
|
|
643
649
|
|
|
644
650
|
if (!timerObj) {
|
|
645
|
-
// 首次变化:创建新的定时器对象
|
|
646
651
|
timerObj = {
|
|
647
652
|
timer: null,
|
|
648
653
|
nodeId: node.id,
|
|
649
654
|
serialPortConfig: node.serialPortConfig,
|
|
650
|
-
firstChangeTime: now,
|
|
651
|
-
changeCount: 0
|
|
655
|
+
firstChangeTime: now,
|
|
656
|
+
changeCount: 0
|
|
652
657
|
};
|
|
653
|
-
meshLedDebounceTimers.set(
|
|
658
|
+
meshLedDebounceTimers.set(panelKey, timerObj);
|
|
654
659
|
}
|
|
655
660
|
|
|
656
|
-
// 增加变化计数
|
|
657
661
|
timerObj.changeCount++;
|
|
662
|
+
if (timerObj.timer) clearTimeout(timerObj.timer);
|
|
658
663
|
|
|
659
|
-
// 清除之前的定时器
|
|
660
|
-
if (timerObj.timer) {
|
|
661
|
-
clearTimeout(timerObj.timer);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// 设置新的定时器:100ms内如果没有新的状态变化,才发送LED反馈
|
|
665
664
|
const capturedNode = node;
|
|
666
665
|
timerObj.timer = setTimeout(() => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// 发送LED反馈到物理开关面板(使用最新的完整状态)
|
|
678
|
-
const button1State = currentStates[0]; // 按钮1的状态
|
|
679
|
-
capturedNode.sendCommandToPanel(button1State);
|
|
666
|
+
const currentTimerObj = meshLedDebounceTimers.get(panelKey);
|
|
667
|
+
if (currentTimerObj) {
|
|
668
|
+
meshLedDebounceTimers.delete(panelKey);
|
|
669
|
+
|
|
670
|
+
// 设置全局锁定:100ms内忽略面板状态上报
|
|
671
|
+
// 改进:使用更精准的锁定时间,并增加序列号或时间戳校验
|
|
672
|
+
meshLedFeedbackGlobalLock = Date.now() + 150;
|
|
673
|
+
|
|
674
|
+
capturedNode.debug(`[LED反馈] 面板${panelKey} 继电器触发同步`);
|
|
675
|
+
capturedNode.sendCommandToPanel(capturedNode.currentState);
|
|
680
676
|
}
|
|
681
|
-
},
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// 只有当前节点绑定的线圈变化时,才更新节点状态并触发按键LED反馈
|
|
685
|
-
if (coil === node.config.targetCoilNumber) {
|
|
686
|
-
// 更新当前节点状态
|
|
687
|
-
node.currentState = value;
|
|
688
|
-
node.lastStateChange.timestamp = Date.now();
|
|
689
|
-
node.lastStateChange.value = value;
|
|
690
|
-
|
|
691
|
-
if (stateChanged) {
|
|
692
|
-
if (isButtonPress) {
|
|
693
|
-
// 按键触发:立即发送整个面板LED状态(快速响应)
|
|
694
|
-
node.debug(`[LED反馈] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 按键触发,立即发送整个面板LED状态`);
|
|
695
|
-
|
|
696
|
-
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报(缩短锁定时间,避免影响下一次按键)
|
|
697
|
-
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
698
|
-
|
|
699
|
-
// 立即发送LED反馈(整个面板)
|
|
700
|
-
node.sendCommandToPanel(value);
|
|
701
|
-
} else if (isInit) {
|
|
702
|
-
// 首次轮询:发送LED反馈(同步初始状态)
|
|
703
|
-
// 使用防抖机制,避免重复发送
|
|
704
|
-
if (!meshLedDebounceTimers.has(meshAddr)) {
|
|
705
|
-
const capturedNode = node;
|
|
706
|
-
const timer = setTimeout(() => {
|
|
707
|
-
const timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
708
|
-
if (timerObj && timerObj.nodeId === capturedNode.id) {
|
|
709
|
-
meshLedDebounceTimers.delete(meshAddr);
|
|
710
|
-
const currentStates = meshDeviceStates.get(meshAddr);
|
|
711
|
-
capturedNode.debug(`[LED反馈] Mesh${meshAddr} 首次轮询,发送整个面板LED状态: ${JSON.stringify(currentStates)}`);
|
|
712
|
-
|
|
713
|
-
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报
|
|
714
|
-
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
715
|
-
|
|
716
|
-
capturedNode.sendCommandToPanel(value);
|
|
717
|
-
}
|
|
718
|
-
}, 200); // 200ms防抖时间,等待所有初始状态收集完毕
|
|
719
|
-
|
|
720
|
-
meshLedDebounceTimers.set(meshAddr, {
|
|
721
|
-
timer: timer,
|
|
722
|
-
nodeId: node.id,
|
|
723
|
-
serialPortConfig: node.serialPortConfig
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
node.debug(`[LED防抖] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 首次轮询,设置防抖定时器(200ms)`);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// 更新节点状态显示
|
|
732
|
-
node.updateStatus();
|
|
733
|
-
|
|
734
|
-
// 输出状态消息(立即输出,不等待防抖)
|
|
735
|
-
node.send({
|
|
736
|
-
payload: value,
|
|
737
|
-
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
738
|
-
switchId: node.config.switchId,
|
|
739
|
-
button: node.config.buttonNumber,
|
|
740
|
-
targetSlave: node.config.targetSlaveAddress,
|
|
741
|
-
targetCoil: node.config.targetCoilNumber
|
|
742
|
-
});
|
|
677
|
+
}, 50); // 50ms防抖
|
|
743
678
|
}
|
|
744
679
|
}
|
|
745
|
-
}
|
|
746
|
-
// RS-485模式或非Mesh模式:只处理自己绑定的线圈
|
|
747
|
-
// 检查状态是否真正变化
|
|
748
|
-
const stateChanged = (node.currentState !== value);
|
|
749
|
-
|
|
750
|
-
// 区分触发源
|
|
751
|
-
const isButtonPress = (triggerSource === 'button-press' || triggerSource === 'scene-trigger');
|
|
752
|
-
const isInit = (data.source === 'init'); // 首次轮询
|
|
753
|
-
const isPolling = (data.source === 'polling'); // 轮询变化
|
|
680
|
+
}
|
|
754
681
|
|
|
755
|
-
|
|
682
|
+
// 只有当前节点绑定的线圈变化时,才更新节点状态并处理后续逻辑
|
|
683
|
+
if (slave === node.config.targetSlaveAddress && coil === node.config.targetCoilNumber) {
|
|
684
|
+
const stateChanged = (node.currentState !== value);
|
|
756
685
|
node.currentState = value;
|
|
757
686
|
node.lastStateChange.timestamp = Date.now();
|
|
758
687
|
node.lastStateChange.value = value;
|
|
759
688
|
|
|
760
|
-
if (stateChanged
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
689
|
+
if (stateChanged) {
|
|
690
|
+
if (isButtonPress) {
|
|
691
|
+
// 按键触发:立即同步(高优先级)
|
|
692
|
+
meshLedFeedbackGlobalLock = Date.now() + 150;
|
|
693
|
+
node.sendCommandToPanel(value);
|
|
694
|
+
} else if (isInit) {
|
|
695
|
+
// 首次轮询:防抖同步
|
|
696
|
+
if (panelKey && !meshLedDebounceTimers.has(panelKey)) {
|
|
697
|
+
const capturedNode = node;
|
|
698
|
+
const timer = setTimeout(() => {
|
|
699
|
+
if (meshLedDebounceTimers.has(panelKey)) {
|
|
700
|
+
meshLedDebounceTimers.delete(panelKey);
|
|
701
|
+
meshLedFeedbackGlobalLock = Date.now() + 150;
|
|
702
|
+
capturedNode.sendCommandToPanel(value);
|
|
703
|
+
}
|
|
704
|
+
}, 200);
|
|
705
|
+
meshLedDebounceTimers.set(panelKey, { timer, nodeId: node.id });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
764
708
|
}
|
|
765
709
|
|
|
766
|
-
// 更新节点状态显示
|
|
767
710
|
node.updateStatus();
|
|
768
711
|
|
|
769
|
-
// 首次轮询时只同步内部状态和LED反馈,不发送消息到下游
|
|
770
|
-
// 避免重启Node-RED时触发下游节点导致继电器动作
|
|
771
712
|
if (isInit) {
|
|
772
713
|
node.debug(`首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}(不发送到下游)`);
|
|
773
714
|
return;
|
|
774
715
|
}
|
|
775
716
|
|
|
776
|
-
// 输出状态消息(仅在非首次轮询时发送)
|
|
777
717
|
node.send({
|
|
778
718
|
payload: value,
|
|
779
719
|
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
@@ -781,7 +721,7 @@ module.exports = function(RED) {
|
|
|
781
721
|
button: node.config.buttonNumber,
|
|
782
722
|
targetSlave: node.config.targetSlaveAddress,
|
|
783
723
|
targetCoil: node.config.targetCoilNumber,
|
|
784
|
-
source:
|
|
724
|
+
source: (data.source === 'polling') ? 'polling' : triggerSource
|
|
785
725
|
});
|
|
786
726
|
}
|
|
787
727
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.14",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|