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 CHANGED
@@ -888,29 +888,24 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
888
888
 
889
889
  ## 版本信息
890
890
 
891
- **当前版本**: v2.9.3 (2025-12-15)
891
+ **当前版本**: v2.9.5 (2025-12-15)
892
892
 
893
- **v2.9.3 更新内容**:
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
- - 修复场景按钮帧使用特殊通道(如0x0F)导致LED反馈失效的问题
896
- - 场景模式保持使用配置计算的按键通道,不被帧中的特殊标识覆盖
897
- - 解决"部署后场景按钮可以同步一次,点击其他开关后不再同步"的问题
898
- - **优化**:场景按钮匹配逻辑
899
- - 增加场景模式帧的特殊匹配:通道0x0F或isSceneMode标识
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
- return this.name || `开关${this.switchId}-按钮${this.buttonNumber} 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
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
- // Symi协议公式:按键编号 = deviceAddr * 4 - 4 + channel
446
- // 反推公式:deviceAddr = floor((buttonNumber - 1) / 4) + 1, channel = ((buttonNumber - 1) % 4) + 1
447
- // 这样即使没有物理按键事件,也能正确发送LED反馈
448
- node.buttonDeviceAddr = Math.floor((node.config.buttonNumber - 1) / 4) + 1;
449
- node.buttonChannel = ((node.config.buttonNumber - 1) % 4) + 1;
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模式:立即发送LED反馈(不需要防抖)
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
- // 计算实际按键编号(Symi协议公式)
792
- // 例如:devAddr=1,channel=1→按键1;devAddr=2,channel=1→按键5
793
- const actualButtonNumber = buttonEvent.deviceAddr * 4 - 4 + buttonEvent.channel;
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
- // 重要:场景按钮的帧通道可能是特殊标识(如0x0F),而不是实际按键通道
800
- // 因此需要额外判断:如果是场景模式且localAddr匹配,也应该处理
801
- const isSceneModeFrame = buttonEvent.isSceneMode || buttonEvent.channel === 0x0F;
802
- const isMatchedByButtonNumber = (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber);
803
- const isMatchedBySceneMode = (buttonEvent.raw.localAddr === node.config.switchId && node.config.buttonType === 'scene' && isSceneModeFrame);
815
+ // 注意:通道0x0F是红外感应触发背光灯
816
+ // 只有当用户配置了buttonNumber=15(按键背光灯)时才处理该帧
817
+ // 否则忽略红外感应帧(避免误触发普通按键事件)
818
+ if (buttonEvent.channel === 0x0F && node.config.buttonNumber !== 15) {
819
+ continue; // 忽略红外感应帧(仅当未配置为背光灯模式时)
820
+ }
804
821
 
805
- if (isMatchedByButtonNumber || isMatchedBySceneMode) {
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",
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": {