node-red-contrib-symi-modbus 2.10.6 → 2.10.7
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 +7 -0
- package/nodes/lightweight-protocol.js +11 -9
- package/nodes/modbus-slave-switch.js +26 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -961,6 +961,13 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
961
961
|
|
|
962
962
|
## 版本更新说明
|
|
963
963
|
|
|
964
|
+
### v2.10.7 (2026-01-22)
|
|
965
|
+
- **协议模式严格匹配**: 全面适配 Symi 智能面板协议 V1.06。
|
|
966
|
+
- **开关模式 (Switch Mode)**: 严格解析 `0x00` (OFF) 和 `0x01` (ON) 状态码,指示灯状态强制同步(Force Sync),确保面板物理状态与系统状态一致。
|
|
967
|
+
- **场景模式 (Scene Mode)**: 采用 Toggle 逻辑,忽略触发码的具体值(兼容 `0x01` 或 `0x1X`),依靠系统侧的 LED 反馈指令 (`REPORT 0x04`) 来驱动面板指示灯变化。
|
|
968
|
+
- **无猜测逻辑**: 移除了代码中基于操作码的模糊推断,完全遵循用户配置 (`buttonType`) 决定处理逻辑,消除混淆。
|
|
969
|
+
- **Clowire协议回退**: 禁用了 v2.10.6 引入的 Clowire 协议“全关”优化(0x1020=0x0002)。经测试,部分面板在接收全关指令后无法正确同步指示灯状态(LED 不熄灭),因此回退为逐个发送关闭指令的逻辑,确保指示灯状态与继电器状态严格一致。
|
|
970
|
+
|
|
964
971
|
### v2.10.6 (2026-01-17)
|
|
965
972
|
- **多节点LED状态聚合**: 支持多个 `从站开关` 节点配置为同一物理按键(相同 Switch ID 和 Button ID)但控制不同继电器。系统会自动聚合所有关联继电器的状态(OR逻辑),实现“任意开则灯亮,全关则灯灭”的智能反馈,完美支持“一键多控”场景。
|
|
966
973
|
- **Clowire协议深度优化**: 新增支持 Clowire 协议的“全关”指令(0x1020=0x0002)。当检测到某面板下的所有按键状态均为 OFF 时,系统会自动合并发送一条全关指令,替代逐个发送的关闭指令,大幅降低 RS-485 总线通信压力,提升批量关灯时的响应速度。
|
|
@@ -482,22 +482,24 @@ module.exports = {
|
|
|
482
482
|
// 开关模式:0x00=关,0x01=开
|
|
483
483
|
// 场景模式:0x11=按键1,0x12=按键2,0x13=按键3等(高4位=1表示场景模式)
|
|
484
484
|
// 特殊场景模式:0x0F也是场景模式(某些面板使用0x0F表示场景触发)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
|
|
485
|
+
|
|
486
|
+
// 协议层识别的场景模式标记(仅供参考,业务层应优先使用配置)
|
|
487
|
+
const protocolIsScene = (opInfo & 0xF0) === 0x10 || opInfo === 0x0F;
|
|
488
|
+
|
|
489
|
+
// 状态解析:
|
|
490
|
+
// 0x00 -> false (OFF)
|
|
491
|
+
// 0x01 -> true (ON)
|
|
492
|
+
// 其他值 -> true (视为触发信号)
|
|
493
|
+
const state = (opInfo === 0x00) ? false : true;
|
|
491
494
|
|
|
492
495
|
return {
|
|
493
496
|
type: 'single',
|
|
494
497
|
deviceType: frame.deviceType,
|
|
495
498
|
deviceAddr: frame.deviceAddr,
|
|
496
499
|
channel: frame.channel,
|
|
497
|
-
state:
|
|
498
|
-
isSceneMode:
|
|
500
|
+
state: state,
|
|
501
|
+
isSceneMode: protocolIsScene, // 协议层识别的场景模式
|
|
499
502
|
opInfo: opInfo, // 保留原始操作信息
|
|
500
|
-
needFeedback: !!needFeedback, // 是否需要LED反馈
|
|
501
503
|
raw: frame
|
|
502
504
|
};
|
|
503
505
|
} else if (frame.opCode === this.LIGHT_OP_MULTI) {
|
|
@@ -685,7 +685,9 @@ module.exports = function(RED) {
|
|
|
685
685
|
let useGlobalOff = false;
|
|
686
686
|
if (!anyOn) {
|
|
687
687
|
if (representativeNode.config.switchBrand === 'clowire') {
|
|
688
|
-
|
|
688
|
+
// v2.10.7 回退:Clowire 全关指令 (0x1020=0x0002) 在部分面板上无法同步指示灯状态
|
|
689
|
+
// 暂时禁用该优化,改回逐个发送关闭指令
|
|
690
|
+
useGlobalOff = false;
|
|
689
691
|
} else if (representativeNode.config.switchBrand === 'symi') {
|
|
690
692
|
useGlobalOff = false;
|
|
691
693
|
}
|
|
@@ -948,35 +950,29 @@ module.exports = function(RED) {
|
|
|
948
950
|
}
|
|
949
951
|
|
|
950
952
|
if (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber) {
|
|
951
|
-
//
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
953
|
+
// 核心逻辑:模式判断(严格遵循用户配置,去除猜测)
|
|
954
|
+
// 开关模式:按键打开(0x01)和关闭(0x00)是两个不同的协议
|
|
955
|
+
// 场景模式:开关打开和关闭都是发的同一个码(通常是触发码),靠指示灯反馈分辨
|
|
956
|
+
|
|
957
|
+
const isSceneMode = (node.config.buttonType === 'scene');
|
|
958
|
+
const isSwitchMode = (node.config.buttonType === 'switch');
|
|
959
|
+
|
|
960
|
+
// 如果未配置(兼容旧版),则参考协议层的识别结果
|
|
961
|
+
// 但通常 node.config.buttonType 默认为 'switch'
|
|
962
|
+
|
|
956
963
|
// 全局防抖(基于面板ID和按键编号,不包含从站地址)
|
|
957
|
-
// 这样同一个按键事件只会被处理一次LED反馈,但每个节点都会发送MQTT命令
|
|
958
|
-
// 修改:针对命令发送(MQTT/内部事件)的防抖,key 必须包含 targetSlaveAddress,
|
|
959
|
-
// 否则如果配置了多个从站开关节点(对应同一个物理按键但控制不同继电器),
|
|
960
|
-
// 会被误判为重复命令而只触发第一个。
|
|
961
964
|
const ledDebounceKey = getScopedKey(node, `led-${node.config.switchId}-${node.config.buttonNumber}`);
|
|
962
|
-
// 增加 targetCoilNumber 到 key 中,确保同一从站不同线圈也能独立触发(虽然少见)
|
|
963
965
|
const commandDebounceKey = getScopedKey(node, `cmd-${node.config.switchId}-${node.config.buttonNumber}-${node.config.targetSlaveAddress}-${node.config.targetCoilNumber}`);
|
|
964
966
|
const now = Date.now();
|
|
965
967
|
|
|
966
968
|
// 检查命令防抖(每个节点独立)
|
|
967
|
-
// 只有针对完全相同的目标(同一从站、同一线圈)才进行防抖
|
|
968
969
|
const lastCommandTime = globalDebounceCache.get(commandDebounceKey) || 0;
|
|
969
970
|
if (now - lastCommandTime < 200) {
|
|
970
|
-
// node.debug(`忽略重复命令: 面板${node.config.switchId} 按键${node.config.buttonNumber} -> 从站${node.config.targetSlaveAddress} (间隔${now - lastCommandTime}ms)`);
|
|
971
971
|
return;
|
|
972
972
|
}
|
|
973
973
|
globalDebounceCache.set(commandDebounceKey, now);
|
|
974
974
|
|
|
975
975
|
// 检查LED反馈防抖(同一面板同一按键共享)
|
|
976
|
-
// 调试日志:确认按键触发及目标
|
|
977
|
-
// node.debug(`[按键触发] 面板${node.config.switchId} 按键${node.config.buttonNumber} -> 目标从站${node.config.targetSlaveAddress} 线圈${node.config.targetCoilNumber}`);
|
|
978
|
-
|
|
979
|
-
// 多个节点共享同一个物理按键的 LED 反馈,只需要发送一次
|
|
980
976
|
const lastLedTime = globalDebounceCache.get(ledDebounceKey) || 0;
|
|
981
977
|
const shouldSendLed = (now - lastLedTime >= 200);
|
|
982
978
|
if (shouldSendLed) {
|
|
@@ -989,11 +985,12 @@ module.exports = function(RED) {
|
|
|
989
985
|
}
|
|
990
986
|
|
|
991
987
|
if (isSceneMode) {
|
|
992
|
-
//
|
|
993
|
-
|
|
988
|
+
// 场景模式:无论收到什么码(只要是触发),都翻转状态
|
|
989
|
+
// 因为场景模式下“打开和关闭都是发的同一个码”
|
|
990
|
+
node.debug(`[场景触发] 面板${node.config.switchId} 按键${node.config.buttonNumber} (Toggle)`);
|
|
994
991
|
node.currentState = !node.currentState;
|
|
995
992
|
|
|
996
|
-
//
|
|
993
|
+
// 发送按键按下事件
|
|
997
994
|
RED.events.emit('modbus:buttonPressed', {
|
|
998
995
|
switchId: node.config.switchId,
|
|
999
996
|
button: node.config.buttonNumber,
|
|
@@ -1006,12 +1003,9 @@ module.exports = function(RED) {
|
|
|
1006
1003
|
// 发送MQTT命令到继电器
|
|
1007
1004
|
node.sendMqttCommand(node.currentState, true); // isSceneMode = true
|
|
1008
1005
|
|
|
1009
|
-
//
|
|
1010
|
-
// 只有第一个处理的节点发送LED反馈(通过shouldSendLed控制)
|
|
1006
|
+
// 场景模式:必须立即发送LED反馈以更新面板状态
|
|
1011
1007
|
if (shouldSendLed) {
|
|
1012
1008
|
if (node.serialPortConfig && node.serialPortConfig.setFeedbackLock) {
|
|
1013
|
-
// 统一使用 50ms 锁定,防止反馈回显触发误判
|
|
1014
|
-
// 使用 setTimeout 延迟锁定,确保当前帧能被所有并行的节点处理完毕
|
|
1015
1009
|
setTimeout(() => {
|
|
1016
1010
|
node.serialPortConfig.setFeedbackLock(50);
|
|
1017
1011
|
}, 1);
|
|
@@ -1019,11 +1013,14 @@ module.exports = function(RED) {
|
|
|
1019
1013
|
node.sendCommandToPanel(node.currentState);
|
|
1020
1014
|
}
|
|
1021
1015
|
} else {
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1016
|
+
// 开关模式:严格使用协议中的状态 (0x00=OFF, 0x01=ON)
|
|
1017
|
+
// 如果协议发来的是非0/1值(例如0x11),detectButtonPress默认解析为true
|
|
1018
|
+
// 这要求用户必须正确配置面板和节点类型
|
|
1019
|
+
|
|
1020
|
+
node.debug(`[开关动作] 面板${node.config.switchId} 按键${node.config.buttonNumber} = ${buttonEvent.state ? 'ON' : 'OFF'}`);
|
|
1024
1021
|
node.currentState = buttonEvent.state;
|
|
1025
1022
|
|
|
1026
|
-
//
|
|
1023
|
+
// 发送按键按下事件
|
|
1027
1024
|
RED.events.emit('modbus:buttonPressed', {
|
|
1028
1025
|
switchId: node.config.switchId,
|
|
1029
1026
|
button: node.config.buttonNumber,
|
|
@@ -1036,13 +1033,10 @@ module.exports = function(RED) {
|
|
|
1036
1033
|
// 发送MQTT命令到继电器
|
|
1037
1034
|
node.sendMqttCommand(buttonEvent.state, false); // isSceneMode = false
|
|
1038
1035
|
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
// 注意:这里需要确保同步指令发送后,不会再次触发按键逻辑
|
|
1036
|
+
// 开关模式:收到物理按键上报时,强制同步 LED 反馈
|
|
1037
|
+
// 即使状态一致,也发送一次 REPORT 帧,确保面板指示灯与系统状态绝对同步
|
|
1042
1038
|
if (shouldSendLed) {
|
|
1043
1039
|
if (node.serialPortConfig && node.serialPortConfig.setFeedbackLock) {
|
|
1044
|
-
// 延长反馈锁时间至 100ms,确保总线回显被彻底过滤
|
|
1045
|
-
// 使用 setTimeout 延迟锁定,确保当前帧能被所有并行的节点处理完毕
|
|
1046
1040
|
setTimeout(() => {
|
|
1047
1041
|
node.serialPortConfig.setFeedbackLock(100);
|
|
1048
1042
|
}, 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.7",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持Symi/Clowire品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|