node-red-contrib-symi-modbus 2.9.3 → 2.9.5
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 +16 -21
- package/nodes/modbus-slave-switch.html +16 -1
- package/nodes/modbus-slave-switch.js +40 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -888,29 +888,24 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
888
888
|
|
|
889
889
|
## 版本信息
|
|
890
890
|
|
|
891
|
-
**当前版本**: v2.9.
|
|
891
|
+
**当前版本**: v2.9.5 (2025-12-15)
|
|
892
892
|
|
|
893
|
-
**v2.9.
|
|
893
|
+
**v2.9.5 更新内容**:
|
|
894
|
+
- **重要修复**:重启Node-RED时设备自动动作问题
|
|
895
|
+
- 修复重启后继电器会自动动作一次的bug
|
|
896
|
+
- 问题根因:首次轮询(source='init')时modbus-slave-switch向下游发送状态消息,触发下游节点执行控制命令
|
|
897
|
+
- 解决方案:首次轮询时只同步内部状态和LED反馈,不发送消息到下游节点
|
|
898
|
+
- 现在重启Node-RED不会导致任何继电器动作,只会同步指示灯状态
|
|
899
|
+
|
|
900
|
+
**v2.9.4 更新内容**:
|
|
894
901
|
- **重要修复**:场景按钮LED反馈不同步问题
|
|
895
|
-
-
|
|
896
|
-
-
|
|
897
|
-
-
|
|
898
|
-
-
|
|
899
|
-
-
|
|
900
|
-
-
|
|
901
|
-
|
|
902
|
-
**v2.9.2 更新内容**:
|
|
903
|
-
- **稳定性增强**:全局防抖缓存定期清理机制
|
|
904
|
-
- 每10分钟自动清理超过1分钟未使用的防抖记录
|
|
905
|
-
- 防止长期运行时内存缓慢增长
|
|
906
|
-
- **稳定性增强**:Mesh无线模式按键注册清理
|
|
907
|
-
- 节点关闭时正确清理meshWirelessButtons全局Map
|
|
908
|
-
- 避免重新部署时残留无效注册信息
|
|
909
|
-
- **代码审查**:全面检查所有节点代码
|
|
910
|
-
- 确认所有定时器在节点关闭时正确清理
|
|
911
|
-
- 确认所有事件监听器正确注销
|
|
912
|
-
- 确认所有连接资源正确释放
|
|
913
|
-
- 支持断电断网后自动恢复运行
|
|
902
|
+
- 修复场景按钮按下后背光灯不跟随继电器状态变化的bug
|
|
903
|
+
- 问题根因:场景模式下currentState提前更新,导致后续状态变化事件被认为"未变化"而跳过LED反馈
|
|
904
|
+
- 解决方案:场景按钮触发时立即发送LED反馈,不等待状态变化事件
|
|
905
|
+
- **新功能**:按键背光灯选项
|
|
906
|
+
- 在按钮编号下拉框中新增"按键背光灯"选项(通道0x0F)
|
|
907
|
+
- 用于红外感应触发背光灯的联动控制
|
|
908
|
+
- 未选择此选项时,红外感应帧会被正确忽略,避免误触发
|
|
914
909
|
|
|
915
910
|
**核心特性**:
|
|
916
911
|
- 支持Modbus RTU/TCP协议,兼容标准Modbus设备和TCP转RS485网关
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
} else {
|
|
38
38
|
// 开关模式和场景模式显示
|
|
39
39
|
const coilDisplay = this.targetCoilNumber || 1;
|
|
40
|
-
|
|
40
|
+
const btnLabel = this.buttonNumber == 15 ? '背光灯' : `按钮${this.buttonNumber}`;
|
|
41
|
+
return this.name || `开关${this.switchId}-${btnLabel} → 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
oneditprepare: function() {
|
|
@@ -68,6 +69,16 @@
|
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
// 按钮编号切换时显示/隐藏背光灯提示
|
|
73
|
+
$("#node-input-buttonNumber").on("change", function() {
|
|
74
|
+
const buttonNumber = $(this).val();
|
|
75
|
+
if (buttonNumber === '15') {
|
|
76
|
+
$("#backlight-hint").show();
|
|
77
|
+
} else {
|
|
78
|
+
$("#backlight-hint").hide();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
71
82
|
// Mesh设备发现按钮
|
|
72
83
|
$("#btn-discover-mesh").on("click", function() {
|
|
73
84
|
const serialPortConfig = $("#node-input-serialPortConfig").val();
|
|
@@ -297,8 +308,12 @@
|
|
|
297
308
|
<option value="6">按钮 6</option>
|
|
298
309
|
<option value="7">按钮 7</option>
|
|
299
310
|
<option value="8">按钮 8</option>
|
|
311
|
+
<option value="15">按键背光灯</option>
|
|
300
312
|
</select>
|
|
301
313
|
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">面板物理按键</span>
|
|
314
|
+
<div id="backlight-hint" style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px; display: none;">
|
|
315
|
+
<i class="fa fa-info-circle" style="color: #2196f3;"></i> 按键背光灯用于红外感应触发,通道0x0F
|
|
316
|
+
</div>
|
|
302
317
|
</div>
|
|
303
318
|
|
|
304
319
|
<!-- Mesh模式配置 -->
|
|
@@ -442,11 +442,11 @@ module.exports = function(RED) {
|
|
|
442
442
|
node.hasReceivedInitialState = false;
|
|
443
443
|
|
|
444
444
|
// 根据按钮编号计算deviceAddr和channel(用于LED反馈)
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
node.buttonDeviceAddr =
|
|
449
|
-
node.buttonChannel =
|
|
445
|
+
// 8键面板:deviceAddr固定为1,channel直接使用按键编号1-8
|
|
446
|
+
// 这是因为8键面板的帧格式是 deviceAddr=1, channel=1-8,不是按4通道分组
|
|
447
|
+
// 例如:按键7的LED反馈帧是 7E 01 04 0F 01 00 01 07 ...(deviceAddr=1, channel=7)
|
|
448
|
+
node.buttonDeviceAddr = 1;
|
|
449
|
+
node.buttonChannel = node.config.buttonNumber;
|
|
450
450
|
|
|
451
451
|
// MQTT主题(映射到继电器设备)
|
|
452
452
|
node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
|
|
@@ -729,28 +729,42 @@ module.exports = function(RED) {
|
|
|
729
729
|
// RS-485模式或非Mesh模式:只处理自己绑定的线圈
|
|
730
730
|
// 检查状态是否真正变化
|
|
731
731
|
const stateChanged = (node.currentState !== value);
|
|
732
|
+
|
|
733
|
+
// 区分触发源
|
|
734
|
+
const isButtonPress = (triggerSource === 'button-press' || triggerSource === 'scene-trigger');
|
|
735
|
+
const isInit = (data.source === 'init'); // 首次轮询
|
|
736
|
+
const isPolling = (data.source === 'polling'); // 轮询变化
|
|
732
737
|
|
|
733
738
|
// 更新当前状态
|
|
734
739
|
node.currentState = value;
|
|
735
740
|
node.lastStateChange.timestamp = Date.now();
|
|
736
741
|
node.lastStateChange.value = value;
|
|
737
742
|
|
|
738
|
-
if (stateChanged) {
|
|
739
|
-
// RS-485
|
|
743
|
+
if (stateChanged && !isButtonPress) {
|
|
744
|
+
// RS-485模式:状态变化且非按键触发时发送LED反馈
|
|
745
|
+
// 按键触发的LED反馈已在handleRs485Data中发送,避免重复
|
|
740
746
|
node.sendCommandToPanel(value);
|
|
741
747
|
}
|
|
742
748
|
|
|
743
749
|
// 更新节点状态显示
|
|
744
750
|
node.updateStatus();
|
|
745
751
|
|
|
746
|
-
//
|
|
752
|
+
// 首次轮询时只同步内部状态和LED反馈,不发送消息到下游
|
|
753
|
+
// 避免重启Node-RED时触发下游节点导致继电器动作
|
|
754
|
+
if (isInit) {
|
|
755
|
+
node.debug(`首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}(不发送到下游)`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 输出状态消息(仅在非首次轮询时发送)
|
|
747
760
|
node.send({
|
|
748
761
|
payload: value,
|
|
749
762
|
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
750
763
|
switchId: node.config.switchId,
|
|
751
764
|
button: node.config.buttonNumber,
|
|
752
765
|
targetSlave: node.config.targetSlaveAddress,
|
|
753
|
-
targetCoil: node.config.targetCoilNumber
|
|
766
|
+
targetCoil: node.config.targetCoilNumber,
|
|
767
|
+
source: isPolling ? 'polling' : triggerSource // 传递触发源
|
|
754
768
|
});
|
|
755
769
|
}
|
|
756
770
|
};
|
|
@@ -788,35 +802,24 @@ module.exports = function(RED) {
|
|
|
788
802
|
continue; // 静默忽略非按键事件
|
|
789
803
|
}
|
|
790
804
|
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
|
|
805
|
+
// 计算实际按键编号
|
|
806
|
+
// 8键面板:deviceAddr=1时,channel直接就是按键编号1-8
|
|
807
|
+
// 例如:devAddr=1,channel=7→按键7
|
|
808
|
+
// 特殊:channel=0x0F(15)是红外感应触发背光灯
|
|
809
|
+
const actualButtonNumber = (buttonEvent.deviceAddr === 1) ? buttonEvent.channel : (buttonEvent.deviceAddr * 4 - 4 + buttonEvent.channel);
|
|
794
810
|
|
|
795
811
|
// 检查是否是我们监听的开关面板和按钮
|
|
796
812
|
// switchId对应本地地址(物理面板地址)
|
|
797
|
-
// buttonNumber对应实际按键编号(1-8
|
|
813
|
+
// buttonNumber对应实际按键编号(1-8,或15表示背光灯)
|
|
798
814
|
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
815
|
+
// 注意:通道0x0F是红外感应触发背光灯
|
|
816
|
+
// 只有当用户配置了buttonNumber=15(按键背光灯)时才处理该帧
|
|
817
|
+
// 否则忽略红外感应帧(避免误触发普通按键事件)
|
|
818
|
+
if (buttonEvent.channel === 0x0F && node.config.buttonNumber !== 15) {
|
|
819
|
+
continue; // 忽略红外感应帧(仅当未配置为背光灯模式时)
|
|
820
|
+
}
|
|
804
821
|
|
|
805
|
-
if (
|
|
806
|
-
// 只有开关模式才更新deviceAddr和channel为实际值
|
|
807
|
-
// 场景模式保持使用配置计算的值,因为场景帧的通道(如0x0F)是特殊标识,不是LED反馈通道
|
|
808
|
-
if (!isSceneModeFrame && node.config.buttonType !== 'scene') {
|
|
809
|
-
const oldDeviceAddr = node.buttonDeviceAddr;
|
|
810
|
-
const oldChannel = node.buttonChannel;
|
|
811
|
-
node.buttonDeviceAddr = buttonEvent.deviceAddr;
|
|
812
|
-
node.buttonChannel = buttonEvent.channel;
|
|
813
|
-
|
|
814
|
-
// 输出调试日志,对比计算值和实际值
|
|
815
|
-
if (oldDeviceAddr !== buttonEvent.deviceAddr || oldChannel !== buttonEvent.channel) {
|
|
816
|
-
node.log(`按键事件更新LED反馈地址:计算值(设备${oldDeviceAddr} 通道${oldChannel}) → 实际值(设备${buttonEvent.deviceAddr} 通道${buttonEvent.channel})`);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
822
|
+
if (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber) {
|
|
820
823
|
// 判断按钮类型:优先使用协议解析结果,其次使用配置
|
|
821
824
|
const isSceneMode = buttonEvent.isSceneMode ||
|
|
822
825
|
node.config.buttonType === 'scene' ||
|
|
@@ -844,6 +847,10 @@ module.exports = function(RED) {
|
|
|
844
847
|
// 场景模式:切换状态(每次触发时翻转)
|
|
845
848
|
node.currentState = !node.currentState;
|
|
846
849
|
node.sendMqttCommand(node.currentState);
|
|
850
|
+
|
|
851
|
+
// 场景模式:立即发送LED反馈(修复:不等待状态变化事件)
|
|
852
|
+
// 因为currentState已经更新,后续的coilStateChanged事件会被认为"状态未变化"而跳过
|
|
853
|
+
node.sendCommandToPanel(node.currentState);
|
|
847
854
|
} else {
|
|
848
855
|
// 开关模式:根据状态发送ON/OFF
|
|
849
856
|
node.debug(`开关${buttonEvent.state ? 'ON' : 'OFF'}: 面板${node.config.switchId} 按键${node.config.buttonNumber}`);
|
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.5",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|