node-red-contrib-symi-modbus 2.9.12 → 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 +38 -60
- package/nodes/modbus-master.html +1 -1
- package/nodes/modbus-slave-switch.html +16 -3
- package/nodes/modbus-slave-switch.js +72 -135
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,24 +11,22 @@ Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成,
|
|
|
11
11
|
- **双模式运行**:
|
|
12
12
|
- **本地模式**:纯串口/TCP通信,断网也能稳定运行,无需MQTT
|
|
13
13
|
- **MQTT模式**:可选接入Home Assistant等第三方平台
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- **Symi
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- **稳定可靠**:完整的内存管理、错误处理、断线重连,适合7x24小时长期运行
|
|
31
|
-
- **总线数据过滤**:自动忽略总线上的无关数据,只处理本节点相关的数据
|
|
14
|
+
- **工业级稳定性**:
|
|
15
|
+
- **多主站隔离**:采用实例级隔离机制,支持无限个主站和从站节点共存,配置互不干扰
|
|
16
|
+
- **智能分包/粘包处理**:内置1KB环形缓冲区和协议分析器,完美处理RS-485总线的分包、粘包问题
|
|
17
|
+
- **内存安全**:严格的内存管理策略,长时间运行内存不泄露,自动清理过期缓存
|
|
18
|
+
- **死锁防护**:多级看门狗机制,自动检测并恢复控制死循环,确保生产环境不卡顿
|
|
19
|
+
- **Symi/Clowire生态集成**:
|
|
20
|
+
- **多协议支持**:完美支持Symi和Clowire(克伦威尔)双品牌协议,自动识别
|
|
21
|
+
- **蓝牙Mesh深度集成**:支持Symi蓝牙Mesh网关V1.3.1协议,实现无线开关秒级响应
|
|
22
|
+
- **设备持久化**:Mesh设备列表自动保存到磁盘,断电/重启/断网后配置不丢失
|
|
23
|
+
- **高级控制功能**:
|
|
24
|
+
- **门禁联动过滤**:继电器节点支持门禁ID过滤,实现精确的门禁联动控制(v2.9.12新增)
|
|
25
|
+
- **HomeKit网桥**:一键接入Apple HomeKit,支持Siri语音控制,状态实时同步
|
|
26
|
+
- **智能写入队列**:所有写入操作串行执行,支持HomeKit群控160个继电器同时动作
|
|
27
|
+
- **可视化运维**:
|
|
28
|
+
- **控制看板**:实时显示所有继电器状态,支持手动控制
|
|
29
|
+
- **调试模式**:详细的通信日志,支持十六进制报文监控
|
|
32
30
|
|
|
33
31
|
## 快速开始
|
|
34
32
|
|
|
@@ -952,25 +950,31 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
952
950
|
|
|
953
951
|
## 版本历史
|
|
954
952
|
|
|
955
|
-
|
|
956
|
-
-
|
|
957
|
-
-
|
|
958
|
-
-
|
|
959
|
-
-
|
|
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,确保在继电器频繁动作时总线通信的有序与稳定。
|
|
960
958
|
|
|
961
|
-
|
|
962
|
-
-
|
|
963
|
-
-
|
|
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
|
+
- **拼包算法优化**:进一步解决工控机串口高频分包问题。
|
|
964
973
|
|
|
965
974
|
**v2.9.10 更新内容**:
|
|
966
|
-
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
- **状态栏增强**:将具体错误信息(如“拒绝连接”、“串口不存在”)直接显示在节点状态文字中,无需查看日志即可掌握连接状况。
|
|
970
|
-
- **多主站配置隔离修复**:
|
|
971
|
-
- 修复了多个Modbus主站节点在编辑时配置互相干扰的问题(通过Scoped Event Handler实现)
|
|
972
|
-
- 确保每个主站节点的从站列表配置完全独立,互不影响
|
|
973
|
-
- **从站开关主站关联**:
|
|
975
|
+
- **日志静默优化**:重连期间不再刷屏调试面板,错误信息显示在节点状态栏。
|
|
976
|
+
- **多主站配置隔离**:修复多个主站节点配置互相干扰的问题。
|
|
977
|
+
- **稳定性增强**:优化了内存管理和重连机制。
|
|
974
978
|
- **新增关联主站功能**:从站开关节点新增"关联主站"配置项
|
|
975
979
|
- **多主站支持**:支持选择特定的主站节点,解决多主站环境下相同从站地址冲突的问题
|
|
976
980
|
- **智能过滤**:从站开关只响应关联主站的状态更新,避免误触发
|
|
@@ -1029,32 +1033,6 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
1029
1033
|
- 断电断网恢复后自动重连,正常工作
|
|
1030
1034
|
- 无调试数据输出,适合生产环境长期运行
|
|
1031
1035
|
|
|
1032
|
-
**v2.9.6 更新内容**:
|
|
1033
|
-
- **重要修复**:485开关场景按钮的CRC校验兼容性问题
|
|
1034
|
-
- 修复不同按键编号的场景按钮因CRC校验值不同而被错误拒绝的问题
|
|
1035
|
-
- 问题根因:不同按键编号(如按钮3、按钮4)会产生不同的CRC校验值,严格校验导致某些厂家的485开关帧被丢弃
|
|
1036
|
-
- 解决方案:对于按键事件帧(SET/REPORT类型),采用宽松的CRC校验策略
|
|
1037
|
-
- 宽松策略:如果CRC不匹配,但帧头、帧尾、数据长度都正确,仍然解析该帧
|
|
1038
|
-
- 现在1-8路所有按键的场景按钮都能正常解析处理,兼容不同厂家的485开关
|
|
1039
|
-
- 测试通过:按钮1-8全部测试通过,包括错误CRC的宽松模式测试
|
|
1040
|
-
|
|
1041
|
-
**v2.9.5 更新内容**:
|
|
1042
|
-
- **重要修复**:重启Node-RED时设备自动动作问题
|
|
1043
|
-
- 修复重启后继电器会自动动作一次的bug
|
|
1044
|
-
- 问题根因:首次轮询(source='init')时modbus-slave-switch向下游发送状态消息,触发下游节点执行控制命令
|
|
1045
|
-
- 解决方案:首次轮询时只同步内部状态和LED反馈,不发送消息到下游节点
|
|
1046
|
-
- 现在重启Node-RED不会导致任何继电器动作,只会同步指示灯状态
|
|
1047
|
-
|
|
1048
|
-
**v2.9.4 更新内容**:
|
|
1049
|
-
- **重要修复**:场景按钮LED反馈不同步问题
|
|
1050
|
-
- 修复场景按钮按下后背光灯不跟随继电器状态变化的bug
|
|
1051
|
-
- 问题根因:场景模式下currentState提前更新,导致后续状态变化事件被认为"未变化"而跳过LED反馈
|
|
1052
|
-
- 解决方案:场景按钮触发时立即发送LED反馈,不等待状态变化事件
|
|
1053
|
-
- **新功能**:按键背光灯选项
|
|
1054
|
-
- 在按钮编号下拉框中新增"按键背光灯"选项(通道0x0F)
|
|
1055
|
-
- 用于红外感应触发背光灯的联动控制
|
|
1056
|
-
- 未选择此选项时,红外感应帧会被正确忽略,避免误触发
|
|
1057
|
-
|
|
1058
1036
|
**核心特性**:
|
|
1059
1037
|
- 支持Modbus RTU/TCP协议,兼容标准Modbus设备和TCP转RS485网关
|
|
1060
1038
|
- 支持Symi RS-485开关和蓝牙Mesh开关,实现双向状态同步
|
package/nodes/modbus-master.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// 映射到继电器
|
|
26
26
|
targetSlaveAddress: {value: 10, validate: RED.validators.number()},
|
|
27
27
|
targetCoilNumber: {value: 1, validate: RED.validators.number()}, // 默认值改为1(显示为1路)
|
|
28
|
-
modbusMaster: {value: ""} // 关联的主站节点
|
|
28
|
+
modbusMaster: {value: "", required: true} // 关联的主站节点
|
|
29
29
|
},
|
|
30
30
|
inputs: 1,
|
|
31
31
|
outputs: 1,
|
|
@@ -448,12 +448,25 @@
|
|
|
448
448
|
<p>Modbus从站开关节点,将物理开关面板的按钮映射到Modbus继电器设备,通过MQTT实现控制。</p>
|
|
449
449
|
|
|
450
450
|
<h3>工作原理</h3>
|
|
451
|
-
<p>本节点实现物理开关面板(RS-485)到Modbus继电器的映射:</p>
|
|
451
|
+
<p>本节点实现物理开关面板(RS-485或蓝牙Mesh)到Modbus继电器的映射:</p>
|
|
452
452
|
<ul>
|
|
453
|
-
<li><strong
|
|
453
|
+
<li><strong>物理面板(RS-485)</strong>:开关ID(0-255)+ 按钮编号(1-8)</li>
|
|
454
|
+
<li><strong>Mesh开关(蓝牙)</strong>:MAC地址 + 按钮编号(1-6)</li>
|
|
454
455
|
<li><strong>映射到</strong>:Modbus从站地址(10-19)+ 线圈编号(0-31)</li>
|
|
455
456
|
<li><strong>通过MQTT</strong>:内置MQTT客户端,无需连线到主站节点</li>
|
|
456
457
|
</ul>
|
|
458
|
+
|
|
459
|
+
<h3>模式选择</h3>
|
|
460
|
+
<dl class="message-properties">
|
|
461
|
+
<dt>按钮类型<span class="property-type">select</span></dt>
|
|
462
|
+
<dd>
|
|
463
|
+
<ul>
|
|
464
|
+
<li><strong>开关按钮</strong>:RS-485有线开关,带状态反馈</li>
|
|
465
|
+
<li><strong>场景按钮</strong>:RS-485场景面板,无状态反馈</li>
|
|
466
|
+
<li><strong>Mesh开关</strong>:蓝牙Mesh无线开关,支持设备发现</li>
|
|
467
|
+
</ul>
|
|
468
|
+
</dd>
|
|
469
|
+
</dl>
|
|
457
470
|
|
|
458
471
|
<h3>MQTT配置</h3>
|
|
459
472
|
<dl class="message-properties">
|
|
@@ -571,12 +571,9 @@ module.exports = function(RED) {
|
|
|
571
571
|
if (node.serialPortConfig) {
|
|
572
572
|
// 定义数据监听器函数(静默处理,只在匹配时输出日志)
|
|
573
573
|
node.serialDataListener = (data) => {
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
} else {
|
|
578
|
-
node.handleRs485Data(data);
|
|
579
|
-
}
|
|
574
|
+
// 所有品牌/协议统一使用 handleRs485Data 进行拼包解析
|
|
575
|
+
// 内部会根据 buttonType/switchBrand 自动选择解析算法,解决沾包分包问题
|
|
576
|
+
node.handleRs485Data(data);
|
|
580
577
|
};
|
|
581
578
|
|
|
582
579
|
// 注册到共享连接配置
|
|
@@ -612,171 +609,111 @@ module.exports = function(RED) {
|
|
|
612
609
|
node.debug(`收到首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
613
610
|
}
|
|
614
611
|
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
+
}
|
|
621
638
|
}
|
|
622
639
|
|
|
623
|
-
|
|
624
|
-
const baseCoil = node.config.targetCoilNumber - (node.config.meshButtonNumber - 1);
|
|
625
|
-
const buttonIndex = coil - baseCoil;
|
|
626
|
-
|
|
627
|
-
// 检查是否是同一个面板的线圈(线圈范围:baseCoil ~ baseCoil+totalButtons-1)
|
|
628
|
-
if (buttonIndex >= 0 && buttonIndex < node.config.meshTotalButtons) {
|
|
629
|
-
// 更新全局状态缓存
|
|
630
|
-
const states = meshDeviceStates.get(meshAddr);
|
|
640
|
+
if (buttonIndex >= 0 && buttonIndex < states.length) {
|
|
631
641
|
const oldValue = states[buttonIndex];
|
|
632
642
|
states[buttonIndex] = value;
|
|
633
643
|
const stateChanged = (oldValue !== value);
|
|
634
644
|
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
const isInit = (triggerSource === 'init' || data.source === 'init');
|
|
638
|
-
const isRelayControl = !isButtonPress && !isInit;
|
|
639
|
-
|
|
640
|
-
// 继电器控制:所有同一面板的线圈变化都会重置定时器(只有按钮1节点负责)
|
|
641
|
-
if (isRelayControl && stateChanged && node.config.meshButtonNumber === 1) {
|
|
645
|
+
// 继电器控制:面板关联的任何线圈变化都会触发LED同步
|
|
646
|
+
if (isRelayControl && stateChanged) {
|
|
642
647
|
const now = Date.now();
|
|
643
|
-
|
|
644
|
-
// 获取或创建定时器对象
|
|
645
|
-
let timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
648
|
+
let timerObj = meshLedDebounceTimers.get(panelKey);
|
|
646
649
|
|
|
647
650
|
if (!timerObj) {
|
|
648
|
-
// 首次变化:创建新的定时器对象
|
|
649
651
|
timerObj = {
|
|
650
652
|
timer: null,
|
|
651
653
|
nodeId: node.id,
|
|
652
654
|
serialPortConfig: node.serialPortConfig,
|
|
653
|
-
firstChangeTime: now,
|
|
654
|
-
changeCount: 0
|
|
655
|
+
firstChangeTime: now,
|
|
656
|
+
changeCount: 0
|
|
655
657
|
};
|
|
656
|
-
meshLedDebounceTimers.set(
|
|
658
|
+
meshLedDebounceTimers.set(panelKey, timerObj);
|
|
657
659
|
}
|
|
658
660
|
|
|
659
|
-
// 增加变化计数
|
|
660
661
|
timerObj.changeCount++;
|
|
662
|
+
if (timerObj.timer) clearTimeout(timerObj.timer);
|
|
661
663
|
|
|
662
|
-
// 清除之前的定时器
|
|
663
|
-
if (timerObj.timer) {
|
|
664
|
-
clearTimeout(timerObj.timer);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// 设置新的定时器:100ms内如果没有新的状态变化,才发送LED反馈
|
|
668
664
|
const capturedNode = node;
|
|
669
665
|
timerObj.timer = setTimeout(() => {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// 发送LED反馈到物理开关面板(使用最新的完整状态)
|
|
681
|
-
const button1State = currentStates[0]; // 按钮1的状态
|
|
682
|
-
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);
|
|
683
676
|
}
|
|
684
|
-
},
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// 只有当前节点绑定的线圈变化时,才更新节点状态并触发按键LED反馈
|
|
688
|
-
if (coil === node.config.targetCoilNumber) {
|
|
689
|
-
// 更新当前节点状态
|
|
690
|
-
node.currentState = value;
|
|
691
|
-
node.lastStateChange.timestamp = Date.now();
|
|
692
|
-
node.lastStateChange.value = value;
|
|
693
|
-
|
|
694
|
-
if (stateChanged) {
|
|
695
|
-
if (isButtonPress) {
|
|
696
|
-
// 按键触发:立即发送整个面板LED状态(快速响应)
|
|
697
|
-
node.debug(`[LED反馈] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 按键触发,立即发送整个面板LED状态`);
|
|
698
|
-
|
|
699
|
-
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报(缩短锁定时间,避免影响下一次按键)
|
|
700
|
-
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
701
|
-
|
|
702
|
-
// 立即发送LED反馈(整个面板)
|
|
703
|
-
node.sendCommandToPanel(value);
|
|
704
|
-
} else if (isInit) {
|
|
705
|
-
// 首次轮询:发送LED反馈(同步初始状态)
|
|
706
|
-
// 使用防抖机制,避免重复发送
|
|
707
|
-
if (!meshLedDebounceTimers.has(meshAddr)) {
|
|
708
|
-
const capturedNode = node;
|
|
709
|
-
const timer = setTimeout(() => {
|
|
710
|
-
const timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
711
|
-
if (timerObj && timerObj.nodeId === capturedNode.id) {
|
|
712
|
-
meshLedDebounceTimers.delete(meshAddr);
|
|
713
|
-
const currentStates = meshDeviceStates.get(meshAddr);
|
|
714
|
-
capturedNode.debug(`[LED反馈] Mesh${meshAddr} 首次轮询,发送整个面板LED状态: ${JSON.stringify(currentStates)}`);
|
|
715
|
-
|
|
716
|
-
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报
|
|
717
|
-
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
718
|
-
|
|
719
|
-
capturedNode.sendCommandToPanel(value);
|
|
720
|
-
}
|
|
721
|
-
}, 200); // 200ms防抖时间,等待所有初始状态收集完毕
|
|
722
|
-
|
|
723
|
-
meshLedDebounceTimers.set(meshAddr, {
|
|
724
|
-
timer: timer,
|
|
725
|
-
nodeId: node.id,
|
|
726
|
-
serialPortConfig: node.serialPortConfig
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
node.debug(`[LED防抖] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 首次轮询,设置防抖定时器(200ms)`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// 更新节点状态显示
|
|
735
|
-
node.updateStatus();
|
|
736
|
-
|
|
737
|
-
// 输出状态消息(立即输出,不等待防抖)
|
|
738
|
-
node.send({
|
|
739
|
-
payload: value,
|
|
740
|
-
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
741
|
-
switchId: node.config.switchId,
|
|
742
|
-
button: node.config.buttonNumber,
|
|
743
|
-
targetSlave: node.config.targetSlaveAddress,
|
|
744
|
-
targetCoil: node.config.targetCoilNumber
|
|
745
|
-
});
|
|
677
|
+
}, 50); // 50ms防抖
|
|
746
678
|
}
|
|
747
679
|
}
|
|
748
|
-
}
|
|
749
|
-
// RS-485模式或非Mesh模式:只处理自己绑定的线圈
|
|
750
|
-
// 检查状态是否真正变化
|
|
751
|
-
const stateChanged = (node.currentState !== value);
|
|
752
|
-
|
|
753
|
-
// 区分触发源
|
|
754
|
-
const isButtonPress = (triggerSource === 'button-press' || triggerSource === 'scene-trigger');
|
|
755
|
-
const isInit = (data.source === 'init'); // 首次轮询
|
|
756
|
-
const isPolling = (data.source === 'polling'); // 轮询变化
|
|
680
|
+
}
|
|
757
681
|
|
|
758
|
-
|
|
682
|
+
// 只有当前节点绑定的线圈变化时,才更新节点状态并处理后续逻辑
|
|
683
|
+
if (slave === node.config.targetSlaveAddress && coil === node.config.targetCoilNumber) {
|
|
684
|
+
const stateChanged = (node.currentState !== value);
|
|
759
685
|
node.currentState = value;
|
|
760
686
|
node.lastStateChange.timestamp = Date.now();
|
|
761
687
|
node.lastStateChange.value = value;
|
|
762
688
|
|
|
763
|
-
if (stateChanged
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
}
|
|
767
708
|
}
|
|
768
709
|
|
|
769
|
-
// 更新节点状态显示
|
|
770
710
|
node.updateStatus();
|
|
771
711
|
|
|
772
|
-
// 首次轮询时只同步内部状态和LED反馈,不发送消息到下游
|
|
773
|
-
// 避免重启Node-RED时触发下游节点导致继电器动作
|
|
774
712
|
if (isInit) {
|
|
775
713
|
node.debug(`首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}(不发送到下游)`);
|
|
776
714
|
return;
|
|
777
715
|
}
|
|
778
716
|
|
|
779
|
-
// 输出状态消息(仅在非首次轮询时发送)
|
|
780
717
|
node.send({
|
|
781
718
|
payload: value,
|
|
782
719
|
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
@@ -784,7 +721,7 @@ module.exports = function(RED) {
|
|
|
784
721
|
button: node.config.buttonNumber,
|
|
785
722
|
targetSlave: node.config.targetSlaveAddress,
|
|
786
723
|
targetCoil: node.config.targetCoilNumber,
|
|
787
|
-
source:
|
|
724
|
+
source: (data.source === 'polling') ? 'polling' : triggerSource
|
|
788
725
|
});
|
|
789
726
|
}
|
|
790
727
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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": {
|
|
7
|
-
"
|
|
7
|
+
"lint": "echo \"No linting configured\"",
|
|
8
|
+
"test": "echo \"No tests configured\""
|
|
8
9
|
},
|
|
9
10
|
"keywords": [
|
|
10
11
|
"node-red",
|