node-red-contrib-symi-modbus 2.10.20 → 2.10.29
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 +51 -3
- package/lib/lf-led-registry.js +364 -0
- package/nodes/lf-protocol.js +295 -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 +358 -4
- package/nodes/relay-output.html +52 -39
- package/nodes/relay-output.js +46 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,11 +30,11 @@ 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 网关,支持窗帘、杜亚电机及各种子设备。 |
|
|
37
|
-
| **继电器输出** |
|
|
37
|
+
| **继电器输出** | 灵活的联动节点,支持按键触发、延时控制、多种动作逻辑,LF 品牌支持指示灯跟随与睡眠模式。 |
|
|
38
38
|
| **Modbus调试** | 实时监控原始 485 总线报文,方便现场调试和协议分析。 |
|
|
39
39
|
| **自定义协议** | 支持通过 JavaScript 编写自定义协议转换逻辑。 |
|
|
40
40
|
| **配置节点** | 包含串口配置、MQTT配置和Modbus服务器配置。 |
|
|
@@ -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,45 @@ 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
|
+
- **待机指示灯**:继电器全部关闭时默认下发 `0x0100`(仅 BIT8 背光),**不会**自动全灭;全灭 `0x0000` 仅用于显式熄灭
|
|
138
|
+
- **继电器输出(LF)**:可选 **指示灯跟随状态**——勾选则按键灯与绑定线圈同步;不勾选则按键后瞬时亮灯,本按键关联的全部继电器动作完成后恢复待机背光
|
|
139
|
+
- **睡眠模式(LF)**:继电器输出节点勾选 **睡眠模式** 后,该按键触发的继电器动作完成时面板全部指示灯熄灭(含背光);任意面板按键按下后自动退出睡眠,恢复正常显示
|
|
140
|
+
- **RCU 模式**:首次 LED 控制自动写 `0x1003=0x0006` 并保存 `0x100F`
|
|
141
|
+
- **多键同步**:同面板任意按键/继电器变化,经 `scheduleLfPanelLedSync` **面板级防抖** 后只发 **一条** `0x1008` 帧(非逐节点、非逐线圈各发一帧)
|
|
142
|
+
|
|
143
|
+
**面板地址 1 · 变背光 · 单键亮(测试码):**
|
|
144
|
+
|
|
145
|
+
| 操作 | Hex |
|
|
146
|
+
|------|-----|
|
|
147
|
+
| 按键1亮 | `01 06 10 08 01 01 CC 98` |
|
|
148
|
+
| 按键2亮 | `01 06 10 08 01 02 8C 99` |
|
|
149
|
+
| 按键3亮 | `01 06 10 08 01 04 0C 9B` |
|
|
150
|
+
| 按键4亮 | `01 06 10 08 01 08 0C 9E` |
|
|
151
|
+
| 按键5亮 | `01 06 10 08 01 10 0C 94` |
|
|
152
|
+
| 按键6亮 | `01 06 10 08 01 20 0C 80` |
|
|
153
|
+
| 按键7亮 | `01 06 10 08 01 40 0C A8` |
|
|
154
|
+
| 按键8亮 | `01 06 10 08 01 80 0C F8` |
|
|
155
|
+
| **待机指示灯** | `01 06 10 08 01 00 0D 58` |
|
|
156
|
+
| **全灭** | `01 06 10 08 00 00 0C C8` |
|
|
157
|
+
|
|
158
|
+
**单键灭(`0x1008` 写剩余 bitmask,非逐键单独灭指令):**
|
|
159
|
+
|
|
160
|
+
| 操作 | Hex |
|
|
161
|
+
|------|-----|
|
|
162
|
+
| 仅灭当前唯一亮键(全灭) | `01 06 10 08 00 00 0C C8` |
|
|
163
|
+
| 灭键1、键2仍亮 | `01 06 10 08 01 02 8C 99` |
|
|
164
|
+
| 灭键2、键1+3仍亮 | `01 06 10 08 01 05 CD 5B` |
|
|
165
|
+
| 键1+3亮 | `01 06 10 08 01 05 CD 5B` |
|
|
166
|
+
| 键1–4全亮 | `01 06 10 08 01 0F 4D 5C` |
|
|
167
|
+
|
|
168
|
+
**修改面板地址**:广播 `FF 06 10 00 00 [新地址] [CRC]`,再向新地址发 `XX 06 10 0F 00 FE [CRC]` 保存。
|
|
169
|
+
|
|
131
170
|
### 6. 进阶功能配置
|
|
132
171
|
|
|
133
172
|
#### HomeKit 接入
|
|
@@ -487,6 +526,15 @@ msg.payload = 1; // 或 0
|
|
|
487
526
|
|
|
488
527
|
### 更新日志
|
|
489
528
|
|
|
529
|
+
#### v2.10.29 (2026-07-09)
|
|
530
|
+
|
|
531
|
+
- **[LF485 开关]** 拉斐 LF 485 弱电开关完整对接:主动收发、RCU 模式自动启用、变背光 BIT8、面板级 `0x1008` 单帧聚合(同面板 8 键只发一条指示灯帧)。
|
|
532
|
+
- **[LF485 睡眠模式]** 继电器输出节点新增 **睡眠模式** 勾选项(仅 LF 品牌):勾选后该按键触发的继电器动作完成时,面板全部指示灯熄灭(`0x1008=0x0000`,含背光);任意面板按键按下后自动退出睡眠,恢复正常显示。
|
|
533
|
+
- **[LF485 指示灯跟随]** 继电器输出节点 LF 品牌新增 **指示灯跟随状态**(默认不跟随):勾选=按键灯与绑定线圈同步;不勾选=瞬时亮灯,同按键全部继电器动作完成后回待机背光。
|
|
534
|
+
- **[LF485 待机背光]** 继电器全关时默认 **待机背光**(`0x1008=0x0100`),不再误发全灭。
|
|
535
|
+
- **[适配]** 从站开关 / 继电器输出 / Mesh 透传节点支持 LF 品牌;`lf-protocol.js` 单元测试覆盖 PDF 范例与用户抓包。
|
|
536
|
+
- **[修复]** `RED.nodes.eachNode` 空指针崩溃防护;补全按键 7/8 从站开关节点;场景键不再误写 Modbus 线圈;移除未使用参数。
|
|
537
|
+
|
|
490
538
|
#### v2.10.20 (2026-06-30)
|
|
491
539
|
|
|
492
540
|
- **[打包]** 修复 npm 发布包误含 `.codebase-memory/` 开发索引的问题,包体积约 158KB;规范 `.npmignore` / `.gitignore`,开发缓存目录不进入发布包。
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LF 面板指示灯聚合状态(从站开关 + 继电器输出共享)
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:同一面板 (serialPortConfigId + switchId) 任意时刻只通过
|
|
7
|
+
* scheduleLfPanelLedSync 汇聚为 **一条** 0x1008 帧;禁止多路径各自立即发送。
|
|
8
|
+
*
|
|
9
|
+
* 待机:0x1008 = 0x0100(仅 BIT8 背光)
|
|
10
|
+
* 全灭:0x0000(仅显式熄灭场景)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let syncCallback = null;
|
|
14
|
+
const lfRelayOutputs = new Map();
|
|
15
|
+
const lfMomentaryButtons = new Map();
|
|
16
|
+
const lfPendingActions = new Map();
|
|
17
|
+
const lfPanelSyncTimers = new Map();
|
|
18
|
+
// 睡眠模式:面板级状态,激活时指示灯全灭(0x0000),任意按键退出
|
|
19
|
+
const lfSleepModePanels = new Set();
|
|
20
|
+
|
|
21
|
+
const BATCH_FALLBACK_MARGIN_MS = 3000;
|
|
22
|
+
const PANEL_LED_DEBOUNCE_MS = 200;
|
|
23
|
+
const PANEL_LED_DEDUPE_MS = 200;
|
|
24
|
+
|
|
25
|
+
function getPanelKey(serialPortConfigId, switchId) {
|
|
26
|
+
return `${serialPortConfigId}:${parseInt(switchId, 10)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 设置面板睡眠模式
|
|
31
|
+
* @param {string} serialPortConfigId 串口配置ID
|
|
32
|
+
* @param {number|string} switchId 面板地址
|
|
33
|
+
* @param {boolean} enabled true=进入睡眠(全灭),false=退出睡眠
|
|
34
|
+
*/
|
|
35
|
+
function setPanelSleepMode(serialPortConfigId, switchId, enabled) {
|
|
36
|
+
const panelKey = getPanelKey(serialPortConfigId, switchId);
|
|
37
|
+
if (enabled) {
|
|
38
|
+
lfSleepModePanels.add(panelKey);
|
|
39
|
+
} else {
|
|
40
|
+
lfSleepModePanels.delete(panelKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 查询面板是否处于睡眠模式
|
|
46
|
+
*/
|
|
47
|
+
function isPanelSleepMode(serialPortConfigId, switchId) {
|
|
48
|
+
return lfSleepModePanels.has(getPanelKey(serialPortConfigId, switchId));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 清除所有睡眠模式状态(节点卸载/重启时调用)
|
|
53
|
+
*/
|
|
54
|
+
function clearAllSleepMode() {
|
|
55
|
+
lfSleepModePanels.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getPendingKey(serialPortConfigId, switchId, buttonNumber) {
|
|
59
|
+
return `${getPanelKey(serialPortConfigId, switchId)}:${parseInt(buttonNumber, 10)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setLfLedSyncCallback(fn) {
|
|
63
|
+
syncCallback = fn;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancelPanelSyncTimer(panelKey) {
|
|
67
|
+
const timer = lfPanelSyncTimers.get(panelKey);
|
|
68
|
+
if (timer) {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
lfPanelSyncTimers.delete(panelKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 面板级 LED 调度:多次请求合并为一次 0x1008 发送
|
|
76
|
+
* @param {object} [options]
|
|
77
|
+
* @param {boolean} [options.immediate] 立即发送(仅按键瞬时亮灯等需要即时反馈的场景)
|
|
78
|
+
* @param {number} [options.debounceMs] 防抖毫秒,默认 200
|
|
79
|
+
*/
|
|
80
|
+
function scheduleLfPanelLedSync(serialPortConfigId, switchId, options) {
|
|
81
|
+
const opts = options || {};
|
|
82
|
+
const panelId = parseInt(switchId, 10);
|
|
83
|
+
const panelKey = getPanelKey(serialPortConfigId, panelId);
|
|
84
|
+
|
|
85
|
+
cancelPanelSyncTimer(panelKey);
|
|
86
|
+
|
|
87
|
+
if (opts.immediate === true) {
|
|
88
|
+
if (typeof syncCallback === "function") {
|
|
89
|
+
syncCallback(serialPortConfigId, panelId);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const debounceMs = opts.debounceMs !== undefined ? opts.debounceMs : PANEL_LED_DEBOUNCE_MS;
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
lfPanelSyncTimers.delete(panelKey);
|
|
97
|
+
if (typeof syncCallback === "function") {
|
|
98
|
+
syncCallback(serialPortConfigId, panelId);
|
|
99
|
+
}
|
|
100
|
+
}, debounceMs);
|
|
101
|
+
lfPanelSyncTimers.set(panelKey, timer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @deprecated 请使用 scheduleLfPanelLedSync;保留兼容入口 */
|
|
105
|
+
function requestLfPanelLedSync(serialPortConfigId, switchId) {
|
|
106
|
+
scheduleLfPanelLedSync(serialPortConfigId, switchId, { debounceMs: PANEL_LED_DEBOUNCE_MS });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function registerLfRelayOutput(node) {
|
|
110
|
+
if (!node || node.switchBrand !== "lf" || node.buttonType === "input") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const configId = node.serialPortConfig || node.id || "global";
|
|
114
|
+
const key = getPanelKey(configId, node.switchId);
|
|
115
|
+
if (!lfRelayOutputs.has(key)) {
|
|
116
|
+
lfRelayOutputs.set(key, new Set());
|
|
117
|
+
}
|
|
118
|
+
lfRelayOutputs.get(key).add(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unregisterLfRelayOutput(node) {
|
|
122
|
+
if (!node) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const configId = node.serialPortConfig || node.id || "global";
|
|
126
|
+
const key = getPanelKey(configId, node.switchId);
|
|
127
|
+
const set = lfRelayOutputs.get(key);
|
|
128
|
+
if (!set) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
set.delete(node);
|
|
132
|
+
if (set.size === 0) {
|
|
133
|
+
lfRelayOutputs.delete(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getRelayOutputsForButton(serialPortConfigId, switchId, buttonNumber) {
|
|
138
|
+
const key = getPanelKey(serialPortConfigId, switchId);
|
|
139
|
+
const set = lfRelayOutputs.get(key);
|
|
140
|
+
if (!set) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const btn = parseInt(buttonNumber, 10);
|
|
144
|
+
return Array.from(set).filter((ro) => parseInt(ro.buttonNumber, 10) === btn);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getMomentaryOutputs(serialPortConfigId, switchId, buttonNumber) {
|
|
148
|
+
return getRelayOutputsForButton(serialPortConfigId, switchId, buttonNumber)
|
|
149
|
+
.filter((ro) => ro.lfLedFollowState !== true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasMomentaryRelayOutputs(serialPortConfigId, switchId, buttonNumber) {
|
|
153
|
+
return getMomentaryOutputs(serialPortConfigId, switchId, buttonNumber).length > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function clearMomentary(serialPortConfigId, switchId, buttonNumber) {
|
|
157
|
+
const key = getPanelKey(serialPortConfigId, switchId);
|
|
158
|
+
const active = lfMomentaryButtons.get(key);
|
|
159
|
+
if (!active) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
active.delete(parseInt(buttonNumber, 10));
|
|
163
|
+
if (active.size === 0) {
|
|
164
|
+
lfMomentaryButtons.delete(key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function finishMomentaryBatch(serialPortConfigId, switchId, buttonNumber, pendingKey) {
|
|
169
|
+
const pending = lfPendingActions.get(pendingKey);
|
|
170
|
+
if (!pending) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (pending.fallbackTimer) {
|
|
174
|
+
clearTimeout(pending.fallbackTimer);
|
|
175
|
+
}
|
|
176
|
+
lfPendingActions.delete(pendingKey);
|
|
177
|
+
clearMomentary(serialPortConfigId, switchId, buttonNumber);
|
|
178
|
+
// 批次结束:只调度一次聚合同步(含继电器状态稳定等待)
|
|
179
|
+
scheduleLfPanelLedSync(serialPortConfigId, switchId, { debounceMs: PANEL_LED_DEBOUNCE_MS });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 面板上是否有进行中的瞬时亮灯批次(继电器动作尚未全部完成)
|
|
184
|
+
*/
|
|
185
|
+
function hasActiveMomentaryBatch(serialPortConfigId, switchId) {
|
|
186
|
+
const prefix = `${getPanelKey(serialPortConfigId, switchId)}:`;
|
|
187
|
+
for (const [key, pending] of lfPendingActions.entries()) {
|
|
188
|
+
if (key.startsWith(prefix) && pending.completed < pending.expected) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isMomentaryBatchActive(serialPortConfigId, switchId, buttonNumber) {
|
|
196
|
+
const pendingKey = getPendingKey(serialPortConfigId, switchId, buttonNumber);
|
|
197
|
+
const pending = lfPendingActions.get(pendingKey);
|
|
198
|
+
return !!(pending && pending.completed < pending.expected);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 在 modbus:buttonPressed 派发之前调用
|
|
203
|
+
*/
|
|
204
|
+
function beginLfButtonActionBatch(serialPortConfigId, switchId, buttonNumber) {
|
|
205
|
+
const configId = serialPortConfigId;
|
|
206
|
+
const panelId = parseInt(switchId, 10);
|
|
207
|
+
const btn = parseInt(buttonNumber, 10);
|
|
208
|
+
const panelKey = getPanelKey(configId, panelId);
|
|
209
|
+
const pendingKey = getPendingKey(configId, panelId, btn);
|
|
210
|
+
|
|
211
|
+
const momentaryOutputs = getMomentaryOutputs(configId, panelId, btn);
|
|
212
|
+
if (momentaryOutputs.length === 0) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (lfPendingActions.has(pendingKey)) {
|
|
217
|
+
const old = lfPendingActions.get(pendingKey);
|
|
218
|
+
if (old && old.fallbackTimer) {
|
|
219
|
+
clearTimeout(old.fallbackTimer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!lfMomentaryButtons.has(panelKey)) {
|
|
224
|
+
lfMomentaryButtons.set(panelKey, new Set());
|
|
225
|
+
}
|
|
226
|
+
lfMomentaryButtons.get(panelKey).add(btn);
|
|
227
|
+
|
|
228
|
+
const maxDelay = momentaryOutputs.reduce((max, ro) => Math.max(max, ro.delayMs || 0), 0);
|
|
229
|
+
const pending = {
|
|
230
|
+
expected: momentaryOutputs.length,
|
|
231
|
+
completed: 0,
|
|
232
|
+
maxDelay: maxDelay,
|
|
233
|
+
startedAt: Date.now()
|
|
234
|
+
};
|
|
235
|
+
pending.fallbackTimer = setTimeout(() => {
|
|
236
|
+
if (lfPendingActions.has(pendingKey)) {
|
|
237
|
+
finishMomentaryBatch(configId, panelId, btn, pendingKey);
|
|
238
|
+
}
|
|
239
|
+
}, maxDelay + BATCH_FALLBACK_MARGIN_MS);
|
|
240
|
+
|
|
241
|
+
lfPendingActions.set(pendingKey, pending);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ensureLfButtonActionBatch(serialPortConfigId, switchId, buttonNumber) {
|
|
246
|
+
const pendingKey = getPendingKey(serialPortConfigId, switchId, buttonNumber);
|
|
247
|
+
if (lfPendingActions.has(pendingKey)) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return beginLfButtonActionBatch(serialPortConfigId, switchId, buttonNumber);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function onLfRelayActionComplete(node) {
|
|
254
|
+
if (!node || node.switchBrand !== "lf" || node.lfLedFollowState === true) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const configId = node.serialPortConfig || node.id || "global";
|
|
258
|
+
const btn = parseInt(node.buttonNumber, 10);
|
|
259
|
+
const pendingKey = getPendingKey(configId, node.switchId, btn);
|
|
260
|
+
const pending = lfPendingActions.get(pendingKey);
|
|
261
|
+
if (!pending) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
pending.completed += 1;
|
|
266
|
+
if (pending.completed < pending.expected) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
finishMomentaryBatch(configId, node.switchId, btn, pendingKey);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function computeLfPanelLedMask(serialPortConfigId, switchId, lfProtocol, lfPanelNodes) {
|
|
274
|
+
// 睡眠模式优先:指示灯全灭(0x0000,连背光都灭),任意按键退出后自动恢复
|
|
275
|
+
if (lfSleepModePanels.has(getPanelKey(serialPortConfigId, switchId))) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let keyMask = 0;
|
|
280
|
+
const panelKey = getPanelKey(serialPortConfigId, switchId);
|
|
281
|
+
|
|
282
|
+
const collectFromSlave = function(n) {
|
|
283
|
+
if (!n || !n.config || n.config.switchBrand !== "lf") {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (parseInt(n.config.switchId, 10) !== parseInt(switchId, 10)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!n.serialPortConfig || n.serialPortConfig.id !== serialPortConfigId) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (n.config.syncLight === false) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (n.config.buttonType === "scene") {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (n.currentState) {
|
|
299
|
+
keyMask |= lfProtocol.buttonToMask(n.config.buttonNumber);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const registered = lfPanelNodes.get(panelKey);
|
|
304
|
+
if (registered && registered.size > 0) {
|
|
305
|
+
registered.forEach(collectFromSlave);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const momentary = lfMomentaryButtons.get(panelKey);
|
|
309
|
+
if (momentary) {
|
|
310
|
+
momentary.forEach((btn) => {
|
|
311
|
+
keyMask |= lfProtocol.buttonToMask(btn);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const relaySet = lfRelayOutputs.get(panelKey);
|
|
316
|
+
if (relaySet) {
|
|
317
|
+
const followByButton = new Map();
|
|
318
|
+
relaySet.forEach((ro) => {
|
|
319
|
+
if (ro.lfLedFollowState !== true) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const btn = parseInt(ro.buttonNumber, 10);
|
|
323
|
+
const lit = ro.currentCoilState === true;
|
|
324
|
+
if (!followByButton.has(btn)) {
|
|
325
|
+
followByButton.set(btn, false);
|
|
326
|
+
}
|
|
327
|
+
if (lit) {
|
|
328
|
+
followByButton.set(btn, true);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
followByButton.forEach((lit, btn) => {
|
|
332
|
+
if (lit) {
|
|
333
|
+
keyMask |= lfProtocol.buttonToMask(btn);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (keyMask === 0) {
|
|
339
|
+
return lfProtocol.LED_STANDBY_MASK;
|
|
340
|
+
}
|
|
341
|
+
return lfProtocol.finalizeLedMask(keyMask, true);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
PANEL_LED_DEBOUNCE_MS,
|
|
346
|
+
PANEL_LED_DEDUPE_MS,
|
|
347
|
+
setLfLedSyncCallback,
|
|
348
|
+
scheduleLfPanelLedSync,
|
|
349
|
+
requestLfPanelLedSync,
|
|
350
|
+
registerLfRelayOutput,
|
|
351
|
+
unregisterLfRelayOutput,
|
|
352
|
+
beginLfButtonActionBatch,
|
|
353
|
+
ensureLfButtonActionBatch,
|
|
354
|
+
onLfRelayActionComplete,
|
|
355
|
+
hasActiveMomentaryBatch,
|
|
356
|
+
isMomentaryBatchActive,
|
|
357
|
+
hasMomentaryRelayOutputs,
|
|
358
|
+
getMomentaryOutputs,
|
|
359
|
+
computeLfPanelLedMask,
|
|
360
|
+
getPanelKey,
|
|
361
|
+
setPanelSleepMode,
|
|
362
|
+
isPanelSleepMode,
|
|
363
|
+
clearAllSleepMode
|
|
364
|
+
};
|