node-red-contrib-symi-modbus 2.10.20 → 2.10.27
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 +44 -2
- package/nodes/lf-protocol.js +285 -0
- package/nodes/mesh-transparent-node.html +1 -0
- package/nodes/mesh-transparent-node.js +3 -0
- package/nodes/modbus-slave-switch.html +35 -2
- package/nodes/modbus-slave-switch.js +243 -4
- package/nodes/relay-output.html +1 -0
- package/nodes/relay-output.js +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成。
|
|
|
30
30
|
| 节点名称 | 功能描述 |
|
|
31
31
|
| :--- | :--- |
|
|
32
32
|
| **Modbus主站** | 核心调度中心,负责协议解析、多设备轮询和状态管理。 |
|
|
33
|
-
| **从站开关** | 监听物理面板(Symi/Clowire
|
|
33
|
+
| **从站开关** | 监听物理面板(Symi/Clowire/LF拉斐)按键事件并联动继电器,支持LED同步。 |
|
|
34
34
|
| **HomeKit网桥** | 一键将所有 Modbus 继电器接入 Apple HomeKit,支持自定义名称。 |
|
|
35
35
|
| **控制看板** | 在 Node-RED 编辑器内直接控制和查看所有继电器的实时状态。 |
|
|
36
36
|
| **Mesh透传** | 适配 Symi 蓝牙 Mesh 网关,支持窗帘、杜亚电机及各种子设备。 |
|
|
@@ -117,7 +117,7 @@ node-red-restart
|
|
|
117
117
|
1. 拖拽 **从站开关** 节点到流程画布
|
|
118
118
|
2. 选择刚创建的RS-485连接配置
|
|
119
119
|
3. 配置开关面板信息:
|
|
120
|
-
- 面板品牌: `Symi` / `Clowire
|
|
120
|
+
- 面板品牌: `Symi` / `Clowire`(克伦威尔)/ `LF`(拉斐)
|
|
121
121
|
- 开关ID: 物理面板地址 (0-255)
|
|
122
122
|
- 按钮编号: 按键编号 (1-8)
|
|
123
123
|
- 按钮类型:
|
|
@@ -128,6 +128,41 @@ node-red-restart
|
|
|
128
128
|
- 目标线圈编号: `0`
|
|
129
129
|
5. 部署流程
|
|
130
130
|
|
|
131
|
+
#### LF(拉斐)485 弱电开关
|
|
132
|
+
|
|
133
|
+
- **串口**:9600 8N1,指令间隔 ≥50ms(由 `serial-port-config` 写入队列保证)
|
|
134
|
+
- **面板地址**:1–42(出厂默认 2),开关 ID 填十进制地址
|
|
135
|
+
- **按键上报**(主动发送 8 字节):`[地址][03][00][02][绝对键值][按键掩码][CRC]`
|
|
136
|
+
- **指示灯控制**:写寄存器 `0x1008` 一次写入完整 bitmask(变背光产品高字节 BIT8=1,节点自动处理)
|
|
137
|
+
- **RCU 模式**:首次 LED 控制自动写 `0x1003=0x0006` 并保存 `0x100F`
|
|
138
|
+
- **多键同步**:同面板 8 个从站开关节点共享状态,**只发一条** `0x1008` 帧(复用写入队列 / 200ms 防抖 / 面板级去重)
|
|
139
|
+
|
|
140
|
+
**面板地址 1 · 变背光 · 单键亮(测试码):**
|
|
141
|
+
|
|
142
|
+
| 操作 | Hex |
|
|
143
|
+
|------|-----|
|
|
144
|
+
| 按键1亮 | `01 06 10 08 01 01 CC 98` |
|
|
145
|
+
| 按键2亮 | `01 06 10 08 01 02 8C 99` |
|
|
146
|
+
| 按键3亮 | `01 06 10 08 01 04 0C 9B` |
|
|
147
|
+
| 按键4亮 | `01 06 10 08 01 08 0C 9E` |
|
|
148
|
+
| 按键5亮 | `01 06 10 08 01 10 0C 94` |
|
|
149
|
+
| 按键6亮 | `01 06 10 08 01 20 0C 80` |
|
|
150
|
+
| 按键7亮 | `01 06 10 08 01 40 0C A8` |
|
|
151
|
+
| 按键8亮 | `01 06 10 08 01 80 0C F8` |
|
|
152
|
+
| **全灭** | `01 06 10 08 00 00 0C C8` |
|
|
153
|
+
|
|
154
|
+
**单键灭(`0x1008` 写剩余 bitmask,非逐键单独灭指令):**
|
|
155
|
+
|
|
156
|
+
| 操作 | Hex |
|
|
157
|
+
|------|-----|
|
|
158
|
+
| 仅灭当前唯一亮键(全灭) | `01 06 10 08 00 00 0C C8` |
|
|
159
|
+
| 灭键1、键2仍亮 | `01 06 10 08 01 02 8C 99` |
|
|
160
|
+
| 灭键2、键1+3仍亮 | `01 06 10 08 01 05 CD 5B` |
|
|
161
|
+
| 键1+3亮 | `01 06 10 08 01 05 CD 5B` |
|
|
162
|
+
| 键1–4全亮 | `01 06 10 08 01 0F 4D 5C` |
|
|
163
|
+
|
|
164
|
+
**修改面板地址**:广播 `FF 06 10 00 00 [新地址] [CRC]`,再向新地址发 `XX 06 10 0F 00 FE [CRC]` 保存。
|
|
165
|
+
|
|
131
166
|
### 6. 进阶功能配置
|
|
132
167
|
|
|
133
168
|
#### HomeKit 接入
|
|
@@ -487,6 +522,13 @@ msg.payload = 1; // 或 0
|
|
|
487
522
|
|
|
488
523
|
### 更新日志
|
|
489
524
|
|
|
525
|
+
#### v2.10.27 (2026-07-09)
|
|
526
|
+
|
|
527
|
+
- **[LF485 开关]** 生产发布:拉斐 LF 485 弱电开关完整对接(主动收发、RCU 模式、变背光 BIT8、面板级 0x1008 单帧聚合)。
|
|
528
|
+
- **[LF485 开关]** `sendLfPanelLedSync` 同面板 8 键只发一条指示灯帧;复用 `serial-port-config` 50ms 队列、200ms 防抖、粘包解析与全局缓存。
|
|
529
|
+
- **[适配]** 从站开关 / 继电器输出 / Mesh 透传节点支持 LF 品牌;`lf-protocol.js` 单元测试覆盖 PDF 范例与用户抓包。
|
|
530
|
+
- **[打包]** npm 包审计:不含 `doc/`、PDF、开发缓存;兼容 Node-RED ≥2.0、Node.js ≥14。
|
|
531
|
+
|
|
490
532
|
#### v2.10.20 (2026-06-30)
|
|
491
533
|
|
|
492
534
|
- **[打包]** 修复 npm 发布包误含 `.codebase-memory/` 开发索引的问题,包体积约 158KB;规范 `.npmignore` / `.gitignore`,开发缓存目录不进入发布包。
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LF485 通讯协议(拉斐 Laffey 智能酒店版)
|
|
3
|
+
* 参考:LF485通讯协议智能酒店版(轮询)V3.0
|
|
4
|
+
*
|
|
5
|
+
* 开关面板主动发送帧(8字节):
|
|
6
|
+
* [地址][03][00][02][绝对键值][按键位掩码][CRC_L][CRC_H]
|
|
7
|
+
*
|
|
8
|
+
* 按键位掩码 BIT0~BIT7 对应按键 1~8
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
CMD_READ: 0x03,
|
|
13
|
+
CMD_WRITE: 0x06,
|
|
14
|
+
|
|
15
|
+
REG_PANEL_ADDR: 0x1000,
|
|
16
|
+
REG_PANEL_CONFIG: 0x1003,
|
|
17
|
+
REG_LED_CONTROL: 0x1008,
|
|
18
|
+
REG_BUTTON_VALUE: 0x100B,
|
|
19
|
+
REG_SAVE_CONFIG: 0x100F,
|
|
20
|
+
|
|
21
|
+
DEFAULT_PANEL_ADDR: 0x02,
|
|
22
|
+
ACTIVE_FRAME_LEN: 8,
|
|
23
|
+
|
|
24
|
+
// 0x1003 工作模式位定义(PDF V3.0)
|
|
25
|
+
CONFIG_DEMO_MODE: 0x0001, // BIT0 演示模式(本地控制,非 RCU)
|
|
26
|
+
CONFIG_RCU_MODE: 0x0002, // BIT1 RCU 模式(主机控制指示灯,必须开启)
|
|
27
|
+
CONFIG_ACTIVE_SEND: 0x0004, // BIT2 按键主动发送
|
|
28
|
+
CONFIG_SENSOR_ENABLE: 0x0008, // BIT3 感应使能
|
|
29
|
+
CONFIG_BACKLIGHT_DELAY: 0x0010, // BIT4 背光延时灭
|
|
30
|
+
CONFIG_RELEASE_SEND: 0x0020, // BIT5 按键松开发送
|
|
31
|
+
|
|
32
|
+
// RCU + 主动发送(酒店/智能家居常用组合)
|
|
33
|
+
CONFIG_RCU_ACTIVE: 0x0006,
|
|
34
|
+
|
|
35
|
+
// 0x1008 变背光类产品需 BIT8 才允许 LED 显示
|
|
36
|
+
LED_BACKLIGHT_ENABLE: 0x0100,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Modbus CRC16(低字节在前)
|
|
40
|
+
*/
|
|
41
|
+
calculateCRC16: function(buffer, len) {
|
|
42
|
+
let crc = 0xFFFF;
|
|
43
|
+
const end = len === undefined ? buffer.length : len;
|
|
44
|
+
for (let i = 0; i < end; i++) {
|
|
45
|
+
crc ^= buffer[i];
|
|
46
|
+
for (let j = 0; j < 8; j++) {
|
|
47
|
+
if (crc & 0x0001) {
|
|
48
|
+
crc = (crc >> 1) ^ 0xA001;
|
|
49
|
+
} else {
|
|
50
|
+
crc >>= 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return crc;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
verifyCRC: function(buffer) {
|
|
58
|
+
if (!buffer || buffer.length < 3) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const calc = this.calculateCRC16(buffer, buffer.length - 2);
|
|
62
|
+
const recv = buffer[buffer.length - 2] | (buffer[buffer.length - 1] << 8);
|
|
63
|
+
return calc === recv;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 绝对键值 = (面板地址 - 1) * 6 + 键编号
|
|
68
|
+
*/
|
|
69
|
+
calcAbsoluteKey: function(panelAddr, buttonNumber) {
|
|
70
|
+
return ((panelAddr - 1) * 6) + buttonNumber;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 按键编号 -> 位掩码(1~8)
|
|
75
|
+
*/
|
|
76
|
+
buttonToMask: function(buttonNumber) {
|
|
77
|
+
const n = parseInt(buttonNumber, 10);
|
|
78
|
+
if (n < 1 || n > 8) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return 1 << (n - 1);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 位掩码 -> 按键编号(仅支持单键按下)
|
|
86
|
+
*/
|
|
87
|
+
maskToButton: function(mask) {
|
|
88
|
+
let m = mask & 0xFF;
|
|
89
|
+
if (m === 0 || (m & (m - 1)) !== 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
let bit = 0;
|
|
93
|
+
while (m > 1) {
|
|
94
|
+
m >>= 1;
|
|
95
|
+
bit++;
|
|
96
|
+
}
|
|
97
|
+
return bit + 1;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
isLfActiveFrame: function(buffer) {
|
|
101
|
+
if (!buffer || buffer.length !== this.ACTIVE_FRAME_LEN) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (buffer[1] !== this.CMD_READ) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (buffer[2] !== 0x00 || buffer[3] !== 0x02) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return this.verifyCRC(buffer);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 解析主动发送按键帧
|
|
115
|
+
*/
|
|
116
|
+
parseActiveButtonFrame: function(buffer) {
|
|
117
|
+
if (!this.isLfActiveFrame(buffer)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const panelAddr = buffer[0];
|
|
121
|
+
const absKey = buffer[4];
|
|
122
|
+
const buttonMask = buffer[5];
|
|
123
|
+
const buttonNumber = this.maskToButton(buttonMask);
|
|
124
|
+
if (!buttonNumber) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
panelAddr,
|
|
129
|
+
absKey,
|
|
130
|
+
buttonMask,
|
|
131
|
+
buttonNumber,
|
|
132
|
+
pressed: true,
|
|
133
|
+
type: "button"
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
parseAllFrames: function(buffer) {
|
|
138
|
+
const events = [];
|
|
139
|
+
if (!buffer || buffer.length < this.ACTIVE_FRAME_LEN) {
|
|
140
|
+
return events;
|
|
141
|
+
}
|
|
142
|
+
let offset = 0;
|
|
143
|
+
while (offset + this.ACTIVE_FRAME_LEN <= buffer.length) {
|
|
144
|
+
const slice = buffer.slice(offset, offset + this.ACTIVE_FRAME_LEN);
|
|
145
|
+
if (this.isLfActiveFrame(slice)) {
|
|
146
|
+
const event = this.parseActiveButtonFrame(slice);
|
|
147
|
+
if (event) {
|
|
148
|
+
events.push(event);
|
|
149
|
+
}
|
|
150
|
+
offset += this.ACTIVE_FRAME_LEN;
|
|
151
|
+
} else {
|
|
152
|
+
offset++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return events;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 写寄存器 0x1008 控制状态指示灯(RCU 模式)
|
|
160
|
+
* @param {number} panelAddr 面板地址 1-42
|
|
161
|
+
* @param {number} ledMask 指示灯位掩码 BIT0~BIT7 对应按键1~8
|
|
162
|
+
*/
|
|
163
|
+
buildLedMaskCommand: function(panelAddr, ledMask) {
|
|
164
|
+
const buffer = Buffer.alloc(8);
|
|
165
|
+
buffer[0] = panelAddr & 0xFF;
|
|
166
|
+
buffer[1] = this.CMD_WRITE;
|
|
167
|
+
buffer[2] = 0x10;
|
|
168
|
+
buffer[3] = 0x08;
|
|
169
|
+
buffer[4] = (ledMask >> 8) & 0xFF;
|
|
170
|
+
buffer[5] = ledMask & 0xFF;
|
|
171
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
172
|
+
buffer[6] = crc & 0xFF;
|
|
173
|
+
buffer[7] = (crc >> 8) & 0xFF;
|
|
174
|
+
return buffer;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
buildLedControlCommand: function(panelAddr, buttonNumber, state, useBacklight) {
|
|
178
|
+
const bit = this.buttonToMask(buttonNumber);
|
|
179
|
+
const buttonMask = state ? bit : 0;
|
|
180
|
+
return this.buildAggregatedLedMaskCommand(panelAddr, buttonMask, useBacklight);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
buildTurnOffAllLedsCommand: function(panelAddr) {
|
|
184
|
+
return this.buildLedMaskCommand(panelAddr, 0x0000);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 聚合按键位掩码并应用变背光 BIT8(PDF:变背光产品 BIT8=1 才显示 LED)
|
|
189
|
+
* @param {number} buttonMask BIT0~7 按键状态
|
|
190
|
+
* @param {boolean} useBacklight 变背光面板(默认 true)
|
|
191
|
+
*/
|
|
192
|
+
finalizeLedMask: function(buttonMask, useBacklight) {
|
|
193
|
+
let mask = buttonMask & 0xFF;
|
|
194
|
+
const backlight = useBacklight !== false;
|
|
195
|
+
if (backlight && mask !== 0) {
|
|
196
|
+
mask |= this.LED_BACKLIGHT_ENABLE;
|
|
197
|
+
}
|
|
198
|
+
return mask;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
buildAggregatedLedMaskCommand: function(panelAddr, buttonMask, useBacklight) {
|
|
202
|
+
return this.buildLedMaskCommand(panelAddr, this.finalizeLedMask(buttonMask, useBacklight));
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 清除单个按键位(0x1008 写完整掩码,灭某一键需写剩余键状态)
|
|
207
|
+
* @param {number} currentButtonMask 当前按键位 BIT0~7
|
|
208
|
+
* @param {number} buttonNumber 要熄灭的按键 1~8
|
|
209
|
+
*/
|
|
210
|
+
clearButtonFromMask: function(currentButtonMask, buttonNumber) {
|
|
211
|
+
const bit = this.buttonToMask(buttonNumber);
|
|
212
|
+
return (currentButtonMask & 0xFF) & (~bit & 0xFF);
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
buildSingleButtonOffCommand: function(panelAddr, buttonNumber, currentButtonMask, useBacklight) {
|
|
216
|
+
const newMask = this.clearButtonFromMask(currentButtonMask || 0, buttonNumber);
|
|
217
|
+
return this.buildAggregatedLedMaskCommand(panelAddr, newMask, useBacklight);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 修改面板地址(写 0x1000),修改后需写 0x100F 保存
|
|
222
|
+
*/
|
|
223
|
+
buildSetPanelAddressCommand: function(currentAddr, newAddr, useBroadcast) {
|
|
224
|
+
const addr = useBroadcast ? 0xFF : (currentAddr & 0xFF);
|
|
225
|
+
const buffer = Buffer.alloc(8);
|
|
226
|
+
buffer[0] = addr;
|
|
227
|
+
buffer[1] = this.CMD_WRITE;
|
|
228
|
+
buffer[2] = 0x10;
|
|
229
|
+
buffer[3] = 0x00;
|
|
230
|
+
buffer[4] = 0x00;
|
|
231
|
+
buffer[5] = newAddr & 0xFF;
|
|
232
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
233
|
+
buffer[6] = crc & 0xFF;
|
|
234
|
+
buffer[7] = (crc >> 8) & 0xFF;
|
|
235
|
+
return buffer;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 配置面板工作模式(0x1003)
|
|
240
|
+
* bit1=RCU模式 bit2=按键主动发送
|
|
241
|
+
*/
|
|
242
|
+
buildSetPanelConfigCommand: function(panelAddr, configValue) {
|
|
243
|
+
const buffer = Buffer.alloc(8);
|
|
244
|
+
buffer[0] = panelAddr & 0xFF;
|
|
245
|
+
buffer[1] = this.CMD_WRITE;
|
|
246
|
+
buffer[2] = 0x10;
|
|
247
|
+
buffer[3] = 0x03;
|
|
248
|
+
buffer[4] = (configValue >> 8) & 0xFF;
|
|
249
|
+
buffer[5] = configValue & 0xFF;
|
|
250
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
251
|
+
buffer[6] = crc & 0xFF;
|
|
252
|
+
buffer[7] = (crc >> 8) & 0xFF;
|
|
253
|
+
return buffer;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
buildSaveConfigCommand: function(panelAddr) {
|
|
257
|
+
const buffer = Buffer.alloc(8);
|
|
258
|
+
buffer[0] = panelAddr & 0xFF;
|
|
259
|
+
buffer[1] = this.CMD_WRITE;
|
|
260
|
+
buffer[2] = 0x10;
|
|
261
|
+
buffer[3] = 0x0F;
|
|
262
|
+
buffer[4] = 0x00;
|
|
263
|
+
buffer[5] = 0xFE;
|
|
264
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
265
|
+
buffer[6] = crc & 0xFF;
|
|
266
|
+
buffer[7] = (crc >> 8) & 0xFF;
|
|
267
|
+
return buffer;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 启用 RCU 模式 + 按键主动发送(PDF:BIT1=RCU 控制指示灯,BIT2=主动发送)
|
|
272
|
+
* 例:02 06 10 03 00 06 FD 3B
|
|
273
|
+
*/
|
|
274
|
+
buildEnableRcuModeCommand: function(panelAddr) {
|
|
275
|
+
return this.buildSetPanelConfigCommand(panelAddr, this.CONFIG_RCU_ACTIVE);
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 变背光类产品:按键1亮(需 BIT8=1 才显示 LED)
|
|
280
|
+
* 例:02 06 10 08 01 01 CC AB
|
|
281
|
+
*/
|
|
282
|
+
buildLedControlCommandBacklight: function(panelAddr, buttonNumber, state) {
|
|
283
|
+
return this.buildLedControlCommand(panelAddr, buttonNumber, state, true);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
@@ -393,6 +393,7 @@
|
|
|
393
393
|
<select id="node-input-switchBrand" style="width: 200px;">
|
|
394
394
|
<option value="symi">亖米(Symi)</option>
|
|
395
395
|
<option value="clowire">Clowire(克伦威尔)</option>
|
|
396
|
+
<option value="lf">LF(拉斐)</option>
|
|
396
397
|
</select>
|
|
397
398
|
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">支持1-8键开关</span>
|
|
398
399
|
</div>
|
|
@@ -3,6 +3,7 @@ module.exports = function(RED) {
|
|
|
3
3
|
const meshProtocol = require("./mesh-protocol")(RED);
|
|
4
4
|
const protocol = require("./lightweight-protocol");
|
|
5
5
|
const clowireProtocol = require("./clowire-protocol");
|
|
6
|
+
const lfProtocol = require("./lf-protocol");
|
|
6
7
|
const dooyaProtocol = require("./dooya-protocol");
|
|
7
8
|
|
|
8
9
|
function MeshTransparentNode(config) {
|
|
@@ -243,6 +244,8 @@ module.exports = function(RED) {
|
|
|
243
244
|
let command = null;
|
|
244
245
|
if (node.switchBrand === "clowire") {
|
|
245
246
|
command = clowireProtocol.buildLedControlCommand(node.switchId, node.buttonNumber, state);
|
|
247
|
+
} else if (node.switchBrand === "lf") {
|
|
248
|
+
command = lfProtocol.buildLedControlCommand(node.switchId, node.buttonNumber, state);
|
|
246
249
|
} else {
|
|
247
250
|
// Symi: 使用 REPORT (0x04) 协议同步状态,不触发动作
|
|
248
251
|
const targetAddr = node.panelAddr || node.switchId || 1;
|
|
@@ -42,7 +42,12 @@
|
|
|
42
42
|
// 开关模式和场景模式显示
|
|
43
43
|
const coilDisplay = this.targetCoilNumber || 1;
|
|
44
44
|
const btnLabel = this.buttonNumber == 15 ? '背光灯' : `按钮${this.buttonNumber}`;
|
|
45
|
-
|
|
45
|
+
let brandLabel = '开关';
|
|
46
|
+
if (this.switchBrand === 'clowire') {
|
|
47
|
+
brandLabel = 'Clowire';
|
|
48
|
+
} else if (this.switchBrand === 'lf') {
|
|
49
|
+
brandLabel = 'LF';
|
|
50
|
+
}
|
|
46
51
|
return this.name || `${brandLabel}${this.switchId}-${btnLabel} → 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
|
|
47
52
|
}
|
|
48
53
|
},
|
|
@@ -63,15 +68,38 @@
|
|
|
63
68
|
}
|
|
64
69
|
});
|
|
65
70
|
|
|
71
|
+
function updateBrandUi() {
|
|
72
|
+
const brand = $("#node-input-switchBrand").val();
|
|
73
|
+
const backlightOpt = $("#node-input-buttonNumber option[value='15']");
|
|
74
|
+
if (brand === 'lf') {
|
|
75
|
+
$("#node-input-switchId").attr("min", 1).attr("max", 42);
|
|
76
|
+
$("#lf-brand-hint").show();
|
|
77
|
+
backlightOpt.hide();
|
|
78
|
+
if ($("#node-input-buttonNumber").val() === '15') {
|
|
79
|
+
$("#node-input-buttonNumber").val(1);
|
|
80
|
+
$("#backlight-hint").hide();
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
$("#node-input-switchId").attr("min", 0).attr("max", 255);
|
|
84
|
+
$("#lf-brand-hint").hide();
|
|
85
|
+
backlightOpt.show();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// 品牌切换控制
|
|
67
90
|
$("#node-input-switchBrand").on("change", function() {
|
|
68
91
|
const brand = $(this).val();
|
|
69
92
|
if (brand === 'clowire') {
|
|
70
|
-
// Clowire默认地址是2
|
|
71
93
|
if ($("#node-input-switchId").val() === '0') {
|
|
72
94
|
$("#node-input-switchId").val(2);
|
|
73
95
|
}
|
|
96
|
+
} else if (brand === 'lf') {
|
|
97
|
+
const sid = $("#node-input-switchId").val();
|
|
98
|
+
if (sid === '0' || sid === '2' || sid === '') {
|
|
99
|
+
$("#node-input-switchId").val(1);
|
|
100
|
+
}
|
|
74
101
|
}
|
|
102
|
+
updateBrandUi();
|
|
75
103
|
});
|
|
76
104
|
|
|
77
105
|
// 按钮类型切换控制显示/隐藏
|
|
@@ -212,6 +240,7 @@
|
|
|
212
240
|
// 初始化显示状态
|
|
213
241
|
$("#node-input-enableMqtt").trigger("change");
|
|
214
242
|
$("#node-input-buttonType").trigger("change");
|
|
243
|
+
updateBrandUi();
|
|
215
244
|
|
|
216
245
|
// 始终加载已保存的Mesh设备列表(无论当前是什么模式)
|
|
217
246
|
// 这样切换到Mesh模式时,列表已经加载好了
|
|
@@ -321,10 +350,14 @@
|
|
|
321
350
|
<select id="node-input-switchBrand" style="width: 200px;">
|
|
322
351
|
<option value="symi">亖米(Symi)</option>
|
|
323
352
|
<option value="clowire">Clowire(克伦威尔)</option>
|
|
353
|
+
<option value="lf">LF(拉斐)</option>
|
|
324
354
|
</select>
|
|
325
355
|
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">支持1-8键开关</span>
|
|
326
356
|
</div>
|
|
327
357
|
|
|
358
|
+
<div id="lf-brand-hint" style="display: none; font-size: 11px; color: #888; margin: -8px 0 8px 110px;">
|
|
359
|
+
LF485:9600 8N1,地址 1-42;RCU 模式 + 变背光 BIT8 自动控制 0x1008 指示灯
|
|
360
|
+
</div>
|
|
328
361
|
|
|
329
362
|
|
|
330
363
|
<div class="form-row" style="display: flex; align-items: center;">
|
|
@@ -3,6 +3,7 @@ module.exports = function(RED) {
|
|
|
3
3
|
const mqtt = require("mqtt");
|
|
4
4
|
const protocol = require("./lightweight-protocol");
|
|
5
5
|
const clowireProtocol = require("./clowire-protocol");
|
|
6
|
+
const lfProtocol = require("./lf-protocol");
|
|
6
7
|
const meshProtocol = require("./mesh-protocol")(RED);
|
|
7
8
|
const mqttHelper = require("./mqtt-helper");
|
|
8
9
|
const storage = require("node-persist");
|
|
@@ -23,6 +24,143 @@ module.exports = function(RED) {
|
|
|
23
24
|
} = createSlaveSwitchCaches(RED);
|
|
24
25
|
|
|
25
26
|
startGlobalDebounceCacheCleanup();
|
|
27
|
+
|
|
28
|
+
// LF 面板指示灯聚合掩码(同一485总线上多节点共享)
|
|
29
|
+
const lfPanelLedMask = new Map();
|
|
30
|
+
// LF 面板上次已下发的 0x1008 掩码(每面板一条,避免多节点重复发送)
|
|
31
|
+
const lfPanelLastSentMask = new Map();
|
|
32
|
+
// LF 面板 RCU 模式已配置标记(PDF:0x1003 BIT1=1 才能由主机控制 0x1008 指示灯)
|
|
33
|
+
const lfRcuConfiguredPanels = new Set();
|
|
34
|
+
|
|
35
|
+
function ensureLfRcuMode(node, panelId) {
|
|
36
|
+
if (!node.serialPortConfig || typeof node.serialPortConfig.write !== "function") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const configId = node.serialPortConfig.id || node.id;
|
|
40
|
+
const rcuKey = `${configId}:${panelId}`;
|
|
41
|
+
if (lfRcuConfiguredPanels.has(rcuKey)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
lfRcuConfiguredPanels.add(rcuKey);
|
|
45
|
+
|
|
46
|
+
const cfgCmd = lfProtocol.buildEnableRcuModeCommand(panelId);
|
|
47
|
+
const saveCmd = lfProtocol.buildSaveConfigCommand(panelId);
|
|
48
|
+
node.log(`[LF RCU] 面板${panelId} 自动启用 RCU 模式(0x1003=0x0006) 并保存,之后主机可控制 0x1008 指示灯`);
|
|
49
|
+
|
|
50
|
+
node.serialPortConfig.write(cfgCmd, (err) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
node.log(`[LF RCU] 写入 0x1003 失败: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}, 4, panelId);
|
|
55
|
+
node.serialPortConfig.write(saveCmd, (err) => {
|
|
56
|
+
if (err) {
|
|
57
|
+
node.log(`[LF RCU] 写入 0x100F 保存失败: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}, 4, panelId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function computeLfPanelLedMask(serialPortConfigId, switchId) {
|
|
63
|
+
let mask = 0;
|
|
64
|
+
RED.nodes.eachNode(function(n) {
|
|
65
|
+
if (n.type !== "modbus-slave-switch") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (n.config.switchBrand !== "lf") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (parseInt(n.config.switchId, 10) !== parseInt(switchId, 10)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!n.serialPortConfig || n.serialPortConfig.id !== serialPortConfigId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (n.config.syncLight === false) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (n.currentState) {
|
|
81
|
+
mask |= lfProtocol.buttonToMask(n.config.buttonNumber);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return lfProtocol.finalizeLedMask(mask, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function updateLfRelayStateCache(node, state) {
|
|
88
|
+
const relayStateKey = getScopedKey(node, `relay-state-${node.config.targetSlaveAddress}-${node.config.targetCoilNumber}`);
|
|
89
|
+
if (!globalButtonRelayStates.has(relayStateKey)) {
|
|
90
|
+
globalButtonRelayStates.set(relayStateKey, new Map());
|
|
91
|
+
}
|
|
92
|
+
globalButtonRelayStates.get(relayStateKey).set(node.id, state);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* LF 面板 LED 同步:聚合同面板全部按键状态,只发一条 0x1008 写寄存器帧
|
|
97
|
+
*/
|
|
98
|
+
function sendLfPanelLedSync(callerNode, isTriggerSource) {
|
|
99
|
+
if (!callerNode || !callerNode.config || callerNode.config.switchBrand !== "lf") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (callerNode.config.syncLight === false) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (!callerNode.serialPortConfig || !callerNode.serialPortConfig.connection) {
|
|
106
|
+
if (!callerNode.isInitializing && !callerNode.isClosing) {
|
|
107
|
+
callerNode.log("RS-485连接未建立,无法发送LF指示灯反馈");
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const panelId = parseInt(callerNode.config.switchId, 10);
|
|
113
|
+
const configId = callerNode.serialPortConfig.id || callerNode.id;
|
|
114
|
+
const ledKey = `${configId}:${panelId}`;
|
|
115
|
+
|
|
116
|
+
ensureLfRcuMode(callerNode, panelId);
|
|
117
|
+
|
|
118
|
+
const mask = computeLfPanelLedMask(configId, panelId);
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const lastSent = lfPanelLastSentMask.get(ledKey);
|
|
121
|
+
if (!isTriggerSource && lastSent && lastSent.mask === mask && (now - lastSent.timestamp) < 50) {
|
|
122
|
+
callerNode.debug(`[LF LED] 跳过重复 面板${panelId} 掩码=0x${mask.toString(16).padStart(4, "0")}`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const command = mask === 0
|
|
127
|
+
? lfProtocol.buildTurnOffAllLedsCommand(panelId)
|
|
128
|
+
: lfProtocol.buildLedMaskCommand(panelId, mask);
|
|
129
|
+
|
|
130
|
+
lfPanelLastSentMask.set(ledKey, { mask, timestamp: now });
|
|
131
|
+
lfPanelLedMask.set(ledKey, mask);
|
|
132
|
+
|
|
133
|
+
RED.nodes.eachNode(function(n) {
|
|
134
|
+
if (n.type !== "modbus-slave-switch") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (n.config.switchBrand !== "lf") {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (parseInt(n.config.switchId, 10) !== panelId) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!n.serialPortConfig || n.serialPortConfig.id !== configId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
n.lastSentLedState.value = n.currentState;
|
|
147
|
+
n.lastSentLedState.timestamp = now;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (typeof callerNode.serialPortConfig.setFeedbackLock === "function") {
|
|
151
|
+
callerNode.serialPortConfig.setFeedbackLock(150);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hexStr = command.toString("hex").toUpperCase().match(/.{1,2}/g).join(" ");
|
|
155
|
+
callerNode.debug(`[LF LED] 面板${panelId} 聚合掩码=0x${mask.toString(16).padStart(4, "0")} 单帧=[${hexStr}]`);
|
|
156
|
+
|
|
157
|
+
callerNode.serialPortConfig.write(command, (err) => {
|
|
158
|
+
if (err) {
|
|
159
|
+
callerNode.log(`LF LED反馈失败: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}, 3, panelId);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
26
164
|
// 初始化Mesh设备持久化存储
|
|
27
165
|
const meshPersistDir = path.join(RED.settings.userDir || os.homedir() + "/.node-red", "mesh-devices-persist");
|
|
28
166
|
|
|
@@ -313,7 +451,7 @@ module.exports = function(RED) {
|
|
|
313
451
|
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
314
452
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
|
|
315
453
|
// 开关面板配置
|
|
316
|
-
switchBrand: config.switchBrand || "symi", //
|
|
454
|
+
switchBrand: config.switchBrand || "symi", // 面板品牌(symi/clowire/lf)
|
|
317
455
|
buttonType: config.buttonType || "switch", // 按钮类型:switch=开关模式,scene=场景模式,mesh=Mesh模式
|
|
318
456
|
syncLight: config.syncLight !== false, // 指示灯同步:默认开启
|
|
319
457
|
switchId: parseInt(config.switchId) || 0, // 开关ID(0-255,物理面板地址)
|
|
@@ -794,12 +932,16 @@ module.exports = function(RED) {
|
|
|
794
932
|
representativeNode.debug(`检查面板按键状态时出错: ${err.message}`);
|
|
795
933
|
}
|
|
796
934
|
|
|
797
|
-
//
|
|
935
|
+
// LF:聚合同面板全部按键,只发一条 0x1008
|
|
936
|
+
if (representativeNode.config.switchBrand === "lf") {
|
|
937
|
+
sendLfPanelLedSync(representativeNode, false);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Clowire/Symi 协议优化:如果该面板所有按键都为 OFF,则发送一条“全关”指令
|
|
798
942
|
let useGlobalOff = false;
|
|
799
943
|
if (!anyOn) {
|
|
800
944
|
if (representativeNode.config.switchBrand === "clowire") {
|
|
801
|
-
// v2.10.7 回退:Clowire 全关指令 (0x1020=0x0002) 在部分面板上无法同步指示灯状态
|
|
802
|
-
// 暂时禁用该优化,改回逐个发送关闭指令
|
|
803
945
|
useGlobalOff = false;
|
|
804
946
|
} else if (representativeNode.config.switchBrand === "symi") {
|
|
805
947
|
useGlobalOff = false;
|
|
@@ -944,6 +1086,17 @@ module.exports = function(RED) {
|
|
|
944
1086
|
}
|
|
945
1087
|
}
|
|
946
1088
|
}
|
|
1089
|
+
} else if (node.config.switchBrand === "lf") {
|
|
1090
|
+
// LF485 拉斐面板:固定8字节主动发送帧
|
|
1091
|
+
while (node.rs485Buffer.length >= lfProtocol.ACTIVE_FRAME_LEN) {
|
|
1092
|
+
const slice = node.rs485Buffer.slice(0, lfProtocol.ACTIVE_FRAME_LEN);
|
|
1093
|
+
if (lfProtocol.isLfActiveFrame(slice)) {
|
|
1094
|
+
node.handleLfData(slice);
|
|
1095
|
+
node.rs485Buffer = node.rs485Buffer.slice(lfProtocol.ACTIVE_FRAME_LEN);
|
|
1096
|
+
} else {
|
|
1097
|
+
node.rs485Buffer = node.rs485Buffer.slice(1);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
947
1100
|
} else if (node.config.switchBrand === "clowire") {
|
|
948
1101
|
// Clowire 协议拼包逻辑 (克伦威尔)
|
|
949
1102
|
while (node.rs485Buffer.length >= 9) {
|
|
@@ -1439,6 +1592,80 @@ module.exports = function(RED) {
|
|
|
1439
1592
|
}
|
|
1440
1593
|
};
|
|
1441
1594
|
|
|
1595
|
+
// 处理LF485拉斐面板协议数据
|
|
1596
|
+
node.handleLfData = function(data) {
|
|
1597
|
+
try {
|
|
1598
|
+
const frames = Buffer.isBuffer(data) ? [data] : [data];
|
|
1599
|
+
for (const frame of frames) {
|
|
1600
|
+
const event = lfProtocol.parseActiveButtonFrame(frame);
|
|
1601
|
+
if (!event) {
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
if (event.panelAddr !== parseInt(node.config.switchId, 10)) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (event.buttonNumber !== parseInt(node.config.buttonNumber, 10)) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const commandDebounceKey = getScopedKey(node, `lf-cmd-${node.config.switchId}-${node.config.buttonNumber}-${node.config.targetSlaveAddress}-${node.config.targetCoilNumber}`);
|
|
1612
|
+
const ledDebounceKey = getScopedKey(node, `lf-panel-led-${node.config.switchId}`);
|
|
1613
|
+
const now = Date.now();
|
|
1614
|
+
|
|
1615
|
+
const lastCommandTime = globalDebounceCache.get(commandDebounceKey) || 0;
|
|
1616
|
+
if (now - lastCommandTime < 500) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
globalDebounceCache.set(commandDebounceKey, now);
|
|
1620
|
+
|
|
1621
|
+
const lastLedTime = globalDebounceCache.get(ledDebounceKey) || 0;
|
|
1622
|
+
const shouldSendLed = (now - lastLedTime >= 300);
|
|
1623
|
+
if (shouldSendLed) {
|
|
1624
|
+
globalDebounceCache.set(ledDebounceKey, now);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
if (node.serialPortConfig && typeof node.serialPortConfig.setTriggerSource === "function") {
|
|
1628
|
+
node.serialPortConfig.setTriggerSource(node.config.switchId);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
node.debug(`[LF按键] 面板${node.config.switchId} 按键${node.config.buttonNumber} 按下`);
|
|
1632
|
+
|
|
1633
|
+
node.currentState = !node.currentState;
|
|
1634
|
+
|
|
1635
|
+
updateLfRelayStateCache(node, node.currentState);
|
|
1636
|
+
|
|
1637
|
+
RED.events.emit("modbus:buttonPressed", {
|
|
1638
|
+
switchId: node.config.switchId,
|
|
1639
|
+
button: node.config.buttonNumber,
|
|
1640
|
+
value: node.currentState,
|
|
1641
|
+
brand: "lf",
|
|
1642
|
+
type: "scene",
|
|
1643
|
+
serialPortConfigId: node.serialPortConfig ? node.serialPortConfig.id : null
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
node.sendMqttCommand(node.currentState);
|
|
1647
|
+
|
|
1648
|
+
if (shouldSendLed) {
|
|
1649
|
+
setTimeout(() => {
|
|
1650
|
+
sendLfPanelLedSync(node, true);
|
|
1651
|
+
}, 200);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
node.send({
|
|
1655
|
+
payload: node.currentState,
|
|
1656
|
+
topic: `lf_switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
1657
|
+
switchId: node.config.switchId,
|
|
1658
|
+
button: node.config.buttonNumber,
|
|
1659
|
+
brand: "lf",
|
|
1660
|
+
targetSlave: node.config.targetSlaveAddress,
|
|
1661
|
+
targetCoil: node.config.targetCoilNumber
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
node.log(`处理LF协议数据失败: ${err.message}`);
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1442
1669
|
// 处理Clowire协议数据
|
|
1443
1670
|
node.handleClowireData = function(data) {
|
|
1444
1671
|
try {
|
|
@@ -1997,6 +2224,9 @@ module.exports = function(RED) {
|
|
|
1997
2224
|
|
|
1998
2225
|
const hexStr = command.toString("hex").toUpperCase().match(/.{1,2}/g).join(" ");
|
|
1999
2226
|
node.debug(`[Clowire LED] 控制帧已构建:[${hexStr}]`);
|
|
2227
|
+
} else if (node.config.switchBrand === "lf") {
|
|
2228
|
+
sendLfPanelLedSync(node, isTriggerSource);
|
|
2229
|
+
return;
|
|
2000
2230
|
} else {
|
|
2001
2231
|
// RS-485模式:使用轻量级协议
|
|
2002
2232
|
// 修正:设备地址应为面板地址 (switchId)
|
|
@@ -2479,6 +2709,15 @@ module.exports = function(RED) {
|
|
|
2479
2709
|
node.sendMqttCommand(value);
|
|
2480
2710
|
}
|
|
2481
2711
|
|
|
2712
|
+
// 本地输入同步面板指示灯(无需 Modbus 主站在线)
|
|
2713
|
+
if (node.config.syncLight && node.config.buttonType !== "mesh") {
|
|
2714
|
+
if (node.config.switchBrand === "lf") {
|
|
2715
|
+
sendLfPanelLedSync(node, false);
|
|
2716
|
+
} else {
|
|
2717
|
+
node.sendCommandToPanel(value, false);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2482
2721
|
// 更新状态显示
|
|
2483
2722
|
node.updateStatus();
|
|
2484
2723
|
});
|
package/nodes/relay-output.html
CHANGED
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
<select id="node-input-switchBrand" style="width: 200px;">
|
|
183
183
|
<option value="symi">亖米(Symi)</option>
|
|
184
184
|
<option value="clowire">Clowire(克伦威尔)</option>
|
|
185
|
+
<option value="lf">LF(拉斐)</option>
|
|
185
186
|
</select>
|
|
186
187
|
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">支持1-8键开关</span>
|
|
187
188
|
</div>
|
package/nodes/relay-output.js
CHANGED
|
@@ -98,11 +98,11 @@ module.exports = function(RED) {
|
|
|
98
98
|
|
|
99
99
|
// 获取触发值
|
|
100
100
|
const triggerValue = data.value !== undefined ? data.value : data.payload;
|
|
101
|
-
const
|
|
101
|
+
const isToggleBrand = msgBrand === "clowire" || msgBrand === "lf";
|
|
102
102
|
|
|
103
103
|
// 忽略释放信号
|
|
104
|
-
//
|
|
105
|
-
if (node.ignoreRelease && !triggerValue && !
|
|
104
|
+
// Clowire/LF 协议上报的是“切换状态”,false 也是一次有效按键事件
|
|
105
|
+
if (node.ignoreRelease && !triggerValue && !isToggleBrand) {
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.10.
|
|
4
|
-
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持Symi/Clowire
|
|
3
|
+
"version": "2.10.27",
|
|
4
|
+
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持Symi/Clowire/LF拉斐品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "eslint nodes/*.js lib/*.js --ext .js",
|