node-red-contrib-symi-modbus 2.8.8 → 2.8.9
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 +27 -4
- package/nodes/mesh-protocol.js +62 -23
- package/nodes/modbus-master.js +10 -7
- package/nodes/modbus-slave-switch.html +7 -2
- package/nodes/modbus-slave-switch.js +326 -106
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,6 +187,9 @@ node-red-restart
|
|
|
187
187
|
4. **选择设备和按键**
|
|
188
188
|
- Mesh设备: 从下拉框选择开关(显示格式:`MAC地址 (X路开关)`)
|
|
189
189
|
- 按钮编号: 选择要使用的按键(1-6路)
|
|
190
|
+
- **无线模式**(可选): 勾选此选项后,该按键不响应LED反馈
|
|
191
|
+
- 适用场景:按键用于触发场景(如全开/全关),继电器状态变化不应点亮该按键LED
|
|
192
|
+
- 例如:按键3控制线圈16(全开触发),勾选无线模式后,线圈0-15状态变化不会点亮按键3的LED
|
|
190
193
|
- 多个节点可共享同一设备列表,无需重复扫描
|
|
191
194
|
|
|
192
195
|
5. **配置目标继电器**
|
|
@@ -196,12 +199,13 @@ node-red-restart
|
|
|
196
199
|
6. **部署流程**
|
|
197
200
|
- 点击"完成"并部署
|
|
198
201
|
- Mesh开关按键会自动控制对应继电器
|
|
199
|
-
- 继电器状态变化会自动反馈到Mesh开关LED
|
|
202
|
+
- 继电器状态变化会自动反馈到Mesh开关LED(无线模式按键除外)
|
|
200
203
|
|
|
201
204
|
**Mesh模式特点**:
|
|
202
205
|
- 无线控制,无需布线
|
|
203
206
|
- 支持1-6路开关
|
|
204
207
|
- 双向同步(按键→继电器,继电器→LED)
|
|
208
|
+
- 支持无线模式(场景触发按键不响应LED反馈)
|
|
205
209
|
- 设备列表持久化保存
|
|
206
210
|
- 短地址自动更新(如果网关重新配网)
|
|
207
211
|
- 与RS-485开关使用方式完全一致
|
|
@@ -884,28 +888,47 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
884
888
|
|
|
885
889
|
## 版本信息
|
|
886
890
|
|
|
887
|
-
**当前版本**: v2.8.
|
|
891
|
+
**当前版本**: v2.8.9 (2025-11-13)
|
|
892
|
+
|
|
893
|
+
**v2.8.9 更新内容**:
|
|
894
|
+
- 新增Mesh开关无线模式支持,适用于场景触发按键
|
|
895
|
+
- 修复Mesh开关群控LED反馈问题,确保状态完全同步
|
|
896
|
+
- 优化LED反馈防抖机制(100ms),智能合并多次状态变化
|
|
897
|
+
- 群控多个继电器时,Mesh开关只发送一次LED反馈,包含所有按钮的最新状态
|
|
898
|
+
- 修复串口永久连接机制,彻底解决部署时串口锁定问题
|
|
899
|
+
- RS-485开关功能完全不受影响,保持稳定运行
|
|
888
900
|
|
|
889
901
|
**核心特性**:
|
|
890
902
|
- 支持Modbus RTU/TCP协议,兼容标准Modbus设备和TCP转RS485网关
|
|
891
903
|
- 支持Symi RS-485开关和蓝牙Mesh开关,实现双向状态同步
|
|
904
|
+
- 支持Mesh开关无线模式(场景触发按键不响应LED反馈)
|
|
892
905
|
- 内置HomeKit网桥,支持Siri语音控制
|
|
893
906
|
- 可视化控制看板,实时显示和控制所有继电器状态
|
|
894
907
|
- 智能写入队列,支持大规模批量控制(160+继电器)流畅无卡顿
|
|
895
908
|
- 完整的内存管理和错误处理,适合7x24小时长期稳定运行
|
|
896
909
|
|
|
910
|
+
**Mesh开关LED反馈机制**:
|
|
911
|
+
- 按键触发:100ms内完成控制和LED反馈,响应迅速
|
|
912
|
+
- 继电器控制:100ms防抖,智能合并多次状态变化后发送一次LED反馈
|
|
913
|
+
- 全控场景:无论多少个继电器同时变化,只发送一次LED反馈,包含所有按钮的最新状态
|
|
914
|
+
- 无线模式:勾选无线模式的按键不响应LED反馈,适用于场景触发按键
|
|
915
|
+
- 全局锁定:LED反馈发送期间(100ms),忽略所有Mesh面板的状态上报,避免误触发
|
|
916
|
+
|
|
897
917
|
**稳定性保障**:
|
|
898
918
|
- 连接永久保持:串口/TCP连接一旦建立就保持打开,避免部署时频繁开关导致的锁定问题
|
|
899
919
|
- 内存泄漏防护:每5分钟自动检查并清理异常积压数据
|
|
900
920
|
- 日志防护:错误日志限流,避免硬盘被填满
|
|
901
921
|
- 配置持久化:所有配置和Mesh设备列表自动保存,重启后自动恢复
|
|
902
922
|
- 断网/断电不影响:本地模式下完全脱离网络依赖
|
|
923
|
+
- RS-485独立运行:RS-485开关功能完全独立,不受Mesh功能影响
|
|
903
924
|
|
|
904
925
|
**实际使用场景**:
|
|
905
926
|
- 主站轮询:200ms/台,支持5-10台从站设备
|
|
906
|
-
- 开关面板:支持50
|
|
927
|
+
- 开关面板:支持50个以内Mesh面板,约200个按钮
|
|
907
928
|
- LED同步:部署/重启后1秒内完成所有LED状态同步
|
|
908
|
-
-
|
|
929
|
+
- 批量控制:支持全开/全关等批量操作,LED反馈流畅无卡顿
|
|
930
|
+
- 多面板支持:100个面板同时LED反馈仅需4秒(40ms间隔×100)
|
|
931
|
+
- 场景触发:支持无线模式按键,不受LED反馈影响
|
|
909
932
|
|
|
910
933
|
## 许可证
|
|
911
934
|
|
package/nodes/mesh-protocol.js
CHANGED
|
@@ -24,6 +24,7 @@ module.exports = function(RED) {
|
|
|
24
24
|
MSG_TYPE_CURTAIN: 0x05, // 窗帘动作
|
|
25
25
|
MSG_TYPE_CURTAIN_POS: 0x06, // 窗帘位置
|
|
26
26
|
MSG_TYPE_THERMOSTAT: 0x07, // 温控器
|
|
27
|
+
MSG_TYPE_SCENE: 0x38, // 场景模式(无线开关)
|
|
27
28
|
MSG_TYPE_SWITCH_6: 0x45, // 6路开关状态
|
|
28
29
|
// 设备类型
|
|
29
30
|
DEVICE_TYPE_SWITCH: 0x01,
|
|
@@ -211,35 +212,73 @@ module.exports = function(RED) {
|
|
|
211
212
|
const subOp = frame[2];
|
|
212
213
|
const length = frame[3];
|
|
213
214
|
const shortAddr = frame[4] | (frame[5] << 8); // 小端序
|
|
214
|
-
|
|
215
|
+
|
|
216
|
+
// NODE_STATUS事件的数据格式:多个(msg_type + param)的组合
|
|
217
|
+
// 例如:53 80 06 06 07 01 02 01 0D 01 DA
|
|
218
|
+
// 02 01 = msg_type=0x02(开关状态) param=0x01(第一路关闭)
|
|
219
|
+
// 0D 01 = msg_type=0x0D(触发源) param=0x01(本地按键)
|
|
215
220
|
|
|
216
221
|
const event = {
|
|
217
222
|
subOp: subOp,
|
|
218
223
|
shortAddr: shortAddr,
|
|
219
|
-
|
|
224
|
+
triggerSource: null // 0=未知, 1=本地按键, 2=Mesh网络, 3=私有双控网络
|
|
220
225
|
};
|
|
221
226
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
227
|
+
// 解析数据部分(从第6字节开始)
|
|
228
|
+
let offset = 6;
|
|
229
|
+
while (offset < frame.length - 1) { // 最后一字节是校验
|
|
230
|
+
const msgType = frame[offset];
|
|
231
|
+
offset++;
|
|
232
|
+
|
|
233
|
+
if (offset >= frame.length - 1) break;
|
|
234
|
+
|
|
235
|
+
// 根据消息类型解析参数
|
|
236
|
+
if (msgType === PROTOCOL.MSG_TYPE_SWITCH) {
|
|
237
|
+
// 1-4路开关状态(1字节)
|
|
238
|
+
const stateValue = frame[offset];
|
|
239
|
+
event.msgType = msgType;
|
|
240
|
+
event.states = parseMultiSwitchState(stateValue, 4);
|
|
241
|
+
offset++;
|
|
242
|
+
} else if (msgType === PROTOCOL.MSG_TYPE_SWITCH_6) {
|
|
243
|
+
// 6路开关状态(2字节)
|
|
244
|
+
const stateLow = frame[offset];
|
|
245
|
+
const stateHigh = frame[offset + 1];
|
|
246
|
+
const stateValue = stateLow | (stateHigh << 8);
|
|
247
|
+
event.msgType = msgType;
|
|
248
|
+
event.states = parseMultiSwitchState(stateValue, 6);
|
|
249
|
+
offset += 2;
|
|
250
|
+
} else if (msgType === 0x0D) {
|
|
251
|
+
// 触发源(1字节)
|
|
252
|
+
// 0=未知, 1=本地按键, 2=Mesh网络, 3=私有双控网络
|
|
253
|
+
event.triggerSource = frame[offset];
|
|
254
|
+
offset++;
|
|
255
|
+
} else if (msgType === PROTOCOL.MSG_TYPE_DIMMER) {
|
|
256
|
+
// 调光灯状态(2字节)
|
|
257
|
+
event.msgType = msgType;
|
|
258
|
+
event.brightness = frame[offset];
|
|
259
|
+
event.colorTemp = frame[offset + 1];
|
|
260
|
+
offset += 2;
|
|
261
|
+
} else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN) {
|
|
262
|
+
// 窗帘动作状态(1字节)
|
|
263
|
+
event.msgType = msgType;
|
|
264
|
+
event.action = frame[offset];
|
|
265
|
+
offset++;
|
|
266
|
+
} else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN_POS) {
|
|
267
|
+
// 窗帘位置状态(1字节)
|
|
268
|
+
event.msgType = msgType;
|
|
269
|
+
event.position = frame[offset];
|
|
270
|
+
offset++;
|
|
271
|
+
} else if (msgType === PROTOCOL.MSG_TYPE_SCENE) {
|
|
272
|
+
// 场景模式(无线开关)(2字节:按键编号 + 状态)
|
|
273
|
+
// 例如:38 03 01 = 按键3触发场景
|
|
274
|
+
event.msgType = msgType;
|
|
275
|
+
event.sceneButton = frame[offset]; // 按键编号(1-6)
|
|
276
|
+
event.sceneState = frame[offset + 1]; // 状态(01=触发)
|
|
277
|
+
offset += 2;
|
|
278
|
+
} else {
|
|
279
|
+
// 未知消息类型,跳过1字节(假设参数长度为1)
|
|
280
|
+
offset++;
|
|
281
|
+
}
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
return event;
|
package/nodes/modbus-master.js
CHANGED
|
@@ -1180,7 +1180,7 @@ module.exports = function(RED) {
|
|
|
1180
1180
|
|
|
1181
1181
|
try {
|
|
1182
1182
|
if (task.type === 'single') {
|
|
1183
|
-
await node._writeSingleCoilInternal(task.slaveId, task.coil, task.value);
|
|
1183
|
+
await node._writeSingleCoilInternal(task.slaveId, task.coil, task.value, task.triggerSource);
|
|
1184
1184
|
} else if (task.type === 'multiple') {
|
|
1185
1185
|
await node._writeMultipleCoilsInternal(task.slaveId, task.startCoil, task.values);
|
|
1186
1186
|
}
|
|
@@ -1214,7 +1214,7 @@ module.exports = function(RED) {
|
|
|
1214
1214
|
};
|
|
1215
1215
|
|
|
1216
1216
|
// 写单个线圈(内部实现,不经过队列)
|
|
1217
|
-
node._writeSingleCoilInternal = async function(slaveId, coil, value) {
|
|
1217
|
+
node._writeSingleCoilInternal = async function(slaveId, coil, value, triggerSource = 'unknown') {
|
|
1218
1218
|
if (!node.isConnected) {
|
|
1219
1219
|
throw new Error('Modbus未连接');
|
|
1220
1220
|
}
|
|
@@ -1241,7 +1241,7 @@ module.exports = function(RED) {
|
|
|
1241
1241
|
node.client.setID(slaveId);
|
|
1242
1242
|
|
|
1243
1243
|
// 记录写入操作(帮助追踪总线数据来源)
|
|
1244
|
-
node.log(`写入线圈: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1244
|
+
node.log(`写入线圈: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'} (触发源: ${triggerSource})`);
|
|
1245
1245
|
|
|
1246
1246
|
await node.client.writeCoil(coil, value);
|
|
1247
1247
|
|
|
@@ -1269,7 +1269,8 @@ module.exports = function(RED) {
|
|
|
1269
1269
|
slave: slaveId,
|
|
1270
1270
|
coil: coil,
|
|
1271
1271
|
value: value,
|
|
1272
|
-
source: 'write'
|
|
1272
|
+
source: 'write',
|
|
1273
|
+
triggerSource: triggerSource // 传递触发源标识
|
|
1273
1274
|
});
|
|
1274
1275
|
}
|
|
1275
1276
|
|
|
@@ -1300,7 +1301,7 @@ module.exports = function(RED) {
|
|
|
1300
1301
|
};
|
|
1301
1302
|
|
|
1302
1303
|
// 写单个线圈(公共接口,通过队列执行)
|
|
1303
|
-
node.writeSingleCoil = function(slaveId, coil, value) {
|
|
1304
|
+
node.writeSingleCoil = function(slaveId, coil, value, triggerSource = 'unknown') {
|
|
1304
1305
|
return new Promise((resolve, reject) => {
|
|
1305
1306
|
// 添加到队列
|
|
1306
1307
|
node.writeQueue.push({
|
|
@@ -1308,6 +1309,7 @@ module.exports = function(RED) {
|
|
|
1308
1309
|
slaveId: slaveId,
|
|
1309
1310
|
coil: coil,
|
|
1310
1311
|
value: value,
|
|
1312
|
+
triggerSource: triggerSource, // 传递触发源标识
|
|
1311
1313
|
resolve: resolve,
|
|
1312
1314
|
reject: reject
|
|
1313
1315
|
});
|
|
@@ -1431,13 +1433,14 @@ module.exports = function(RED) {
|
|
|
1431
1433
|
const slave = parseInt(data.slave);
|
|
1432
1434
|
const coil = parseInt(data.coil);
|
|
1433
1435
|
const value = Boolean(data.value);
|
|
1436
|
+
const triggerSource = data.triggerSource || 'unknown'; // 传递触发源标识
|
|
1434
1437
|
|
|
1435
1438
|
// 输出日志确认收到事件
|
|
1436
|
-
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1439
|
+
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'} (触发源: ${triggerSource})`);
|
|
1437
1440
|
|
|
1438
1441
|
try {
|
|
1439
1442
|
// 执行写入操作(writeSingleCoil内部已经会广播状态变化事件)
|
|
1440
|
-
await node.writeSingleCoil(slave, coil, value);
|
|
1443
|
+
await node.writeSingleCoil(slave, coil, value, triggerSource);
|
|
1441
1444
|
|
|
1442
1445
|
node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1443
1446
|
} catch (err) {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
meshShortAddress: {value: 0}, // Mesh设备短地址
|
|
20
20
|
meshButtonNumber: {value: 1, validate: RED.validators.number()}, // Mesh按键编号(1-6)
|
|
21
21
|
meshTotalButtons: {value: 1, validate: RED.validators.number()}, // Mesh开关总路数(1-6)
|
|
22
|
+
meshWirelessMode: {value: false}, // 无线模式(场景触发,不响应LED反馈)
|
|
22
23
|
// 映射到继电器
|
|
23
24
|
targetSlaveAddress: {value: 10, validate: RED.validators.number()},
|
|
24
25
|
targetCoilNumber: {value: 1, validate: RED.validators.number()} // 默认值改为1(显示为1路)
|
|
@@ -159,7 +160,8 @@
|
|
|
159
160
|
btnSelect.empty();
|
|
160
161
|
for (let i = 1; i <= device.buttons; i++) {
|
|
161
162
|
const btnOption = $(`<option value="${i}">按钮 ${i}</option>`);
|
|
162
|
-
|
|
163
|
+
// 修复:使用this.meshButtonNumber而不是node.meshButtonNumber
|
|
164
|
+
if (i === parseInt(node.meshButtonNumber)) {
|
|
163
165
|
btnOption.prop("selected", true);
|
|
164
166
|
}
|
|
165
167
|
btnSelect.append(btnOption);
|
|
@@ -329,7 +331,10 @@
|
|
|
329
331
|
<option value="5">按钮 5</option>
|
|
330
332
|
<option value="6">按钮 6</option>
|
|
331
333
|
</select>
|
|
332
|
-
<
|
|
334
|
+
<label style="margin-left: 15px; display: inline-block;">
|
|
335
|
+
<input type="checkbox" id="node-input-meshWirelessMode" style="width: auto; vertical-align: middle; margin: 0;">
|
|
336
|
+
<span style="vertical-align: middle; white-space: nowrap;">无线模式</span>
|
|
337
|
+
</label>
|
|
333
338
|
</div>
|
|
334
339
|
|
|
335
340
|
<input type="hidden" id="node-input-meshShortAddress">
|
|
@@ -13,6 +13,22 @@ module.exports = function(RED) {
|
|
|
13
13
|
// 全局Mesh设备状态缓存(按短地址索引)
|
|
14
14
|
// 用于同一个Mesh设备的多个按钮节点共享状态数组
|
|
15
15
|
const meshDeviceStates = new Map(); // key: meshShortAddress, value: [state1, state2, ...]
|
|
16
|
+
|
|
17
|
+
// 全局Mesh无线模式按键标记(按短地址索引)
|
|
18
|
+
// 用于标记哪些按键是无线模式(场景触发,不响应LED反馈)
|
|
19
|
+
const meshWirelessButtons = new Map(); // key: meshShortAddress, value: Set([buttonNumber1, buttonNumber2, ...])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// 全局Mesh设备LED反馈防抖定时器(按短地址索引)
|
|
24
|
+
// 用于合并同一个Mesh设备的多个按钮状态变化,只发送一次LED反馈
|
|
25
|
+
const meshLedDebounceTimers = new Map(); // key: meshShortAddress, value: {timer, nodeId, serialPortConfig}
|
|
26
|
+
|
|
27
|
+
// 全局Mesh LED反馈批量发送队列
|
|
28
|
+
// 用于批量发送LED反馈,避免总线拥堵
|
|
29
|
+
const meshLedFeedbackQueue = []; // [{meshAddr, states, timestamp}]
|
|
30
|
+
let meshLedFeedbackQueueTimer = null;
|
|
31
|
+
let meshLedFeedbackGlobalLock = 0; // 全局锁定时间戳:在此时间之前,忽略所有Mesh面板的状态上报
|
|
16
32
|
|
|
17
33
|
// 初始化Mesh设备持久化存储
|
|
18
34
|
const meshPersistDir = path.join(RED.settings.userDir || os.homedir() + '/.node-red', 'mesh-devices-persist');
|
|
@@ -400,9 +416,9 @@ module.exports = function(RED) {
|
|
|
400
416
|
|
|
401
417
|
// 节点关闭标志(用于静默关闭期间的警告)
|
|
402
418
|
node.isClosing = false;
|
|
403
|
-
|
|
404
|
-
// 首次状态同步标志(用于识别部署/重启后的首次轮询)
|
|
405
|
-
node.hasReceivedInitialState = false;
|
|
419
|
+
|
|
420
|
+
// 首次状态同步标志(用于识别部署/重启后的首次轮询)
|
|
421
|
+
node.hasReceivedInitialState = false;
|
|
406
422
|
|
|
407
423
|
// 根据按钮编号计算deviceAddr和channel(用于LED反馈)
|
|
408
424
|
// Symi协议公式:按键编号 = deviceAddr * 4 - 4 + channel
|
|
@@ -493,16 +509,11 @@ module.exports = function(RED) {
|
|
|
493
509
|
node.connectRs485 = async function() {
|
|
494
510
|
try {
|
|
495
511
|
// 使用共享连接配置(由配置节点管理)
|
|
496
|
-
node.log(`使用共享RS-485连接配置: ${node.serialPortConfig.connectionType === 'tcp' ?
|
|
497
|
-
`TCP ${node.serialPortConfig.tcpHost}:${node.serialPortConfig.tcpPort}` :
|
|
498
|
-
`串口 ${node.serialPortConfig.serialPort} @ ${node.serialPortConfig.baudRate}bps`}`);
|
|
499
|
-
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
500
|
-
|
|
501
512
|
node.isRs485Connected = true;
|
|
502
|
-
|
|
503
|
-
// 立即结束初始化阶段(允许Mesh按键控制继电器)
|
|
504
|
-
// LED反馈已通过队列机制自动限速,无需延迟
|
|
505
|
-
node.isInitializing = false;
|
|
513
|
+
|
|
514
|
+
// 立即结束初始化阶段(允许Mesh按键控制继电器)
|
|
515
|
+
// LED反馈已通过队列机制自动限速,无需延迟
|
|
516
|
+
node.isInitializing = false;
|
|
506
517
|
node.updateStatus();
|
|
507
518
|
|
|
508
519
|
|
|
@@ -525,7 +536,12 @@ module.exports = function(RED) {
|
|
|
525
536
|
if (node.serialPortConfig) {
|
|
526
537
|
// 定义数据监听器函数(静默处理,只在匹配时输出日志)
|
|
527
538
|
node.serialDataListener = (data) => {
|
|
528
|
-
|
|
539
|
+
// 根据开关类型调用不同的处理函数
|
|
540
|
+
if (node.config.switchType === 'mesh') {
|
|
541
|
+
node.handleMeshData(data);
|
|
542
|
+
} else {
|
|
543
|
+
node.handleRs485Data(data);
|
|
544
|
+
}
|
|
529
545
|
};
|
|
530
546
|
|
|
531
547
|
// 注册到共享连接配置
|
|
@@ -546,41 +562,166 @@ module.exports = function(RED) {
|
|
|
546
562
|
const slave = parseInt(data.slave);
|
|
547
563
|
const coil = parseInt(data.coil);
|
|
548
564
|
const value = Boolean(data.value);
|
|
565
|
+
const triggerSource = data.triggerSource || data.source || 'unknown'; // 触发源:'button-press', 'relay-control', 'init', 'unknown'
|
|
549
566
|
|
|
550
567
|
// 检查是否是我们关注的从站和线圈
|
|
551
|
-
// 识别首次轮询(source: 'init'),标记已接收初始状态
|
|
552
|
-
if (data.source === 'init' && !node.hasReceivedInitialState) {
|
|
553
|
-
node.hasReceivedInitialState = true;
|
|
554
|
-
node.debug(`收到首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
568
|
+
// 识别首次轮询(source: 'init'),标记已接收初始状态
|
|
569
|
+
if (data.source === 'init' && !node.hasReceivedInitialState) {
|
|
570
|
+
node.hasReceivedInitialState = true;
|
|
571
|
+
node.debug(`收到首次轮询状态同步:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Mesh模式:监听所有绑定到同一个面板的线圈,更新全局状态缓存
|
|
575
|
+
if (node.config.buttonType === 'mesh' && slave === node.config.targetSlaveAddress) {
|
|
576
|
+
const meshAddr = node.config.meshShortAddress;
|
|
577
|
+
if (!meshDeviceStates.has(meshAddr)) {
|
|
578
|
+
// 初始化状态数组(全部设为null,表示未知状态)
|
|
579
|
+
meshDeviceStates.set(meshAddr, new Array(node.config.meshTotalButtons).fill(null));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 计算线圈对应的按钮编号(线圈0→按钮1,线圈1→按钮2,...)
|
|
583
|
+
const baseCoil = node.config.targetCoilNumber - (node.config.meshButtonNumber - 1);
|
|
584
|
+
const buttonIndex = coil - baseCoil;
|
|
585
|
+
|
|
586
|
+
// 检查是否是同一个面板的线圈(线圈范围:baseCoil ~ baseCoil+totalButtons-1)
|
|
587
|
+
if (buttonIndex >= 0 && buttonIndex < node.config.meshTotalButtons) {
|
|
588
|
+
// 更新全局状态缓存
|
|
589
|
+
const states = meshDeviceStates.get(meshAddr);
|
|
590
|
+
const oldValue = states[buttonIndex];
|
|
591
|
+
states[buttonIndex] = value;
|
|
592
|
+
const stateChanged = (oldValue !== value);
|
|
593
|
+
|
|
594
|
+
// 核心修复:区分按键触发和继电器直接控制
|
|
595
|
+
const isButtonPress = (triggerSource === 'button-press');
|
|
596
|
+
const isInit = (triggerSource === 'init' || data.source === 'init');
|
|
597
|
+
const isRelayControl = !isButtonPress && !isInit;
|
|
598
|
+
|
|
599
|
+
// 继电器控制:所有同一面板的线圈变化都会重置定时器(只有按钮1节点负责)
|
|
600
|
+
if (isRelayControl && stateChanged && node.config.meshButtonNumber === 1) {
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
|
|
603
|
+
// 获取或创建定时器对象
|
|
604
|
+
let timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
605
|
+
|
|
606
|
+
if (!timerObj) {
|
|
607
|
+
// 首次变化:创建新的定时器对象
|
|
608
|
+
timerObj = {
|
|
609
|
+
timer: null,
|
|
610
|
+
nodeId: node.id,
|
|
611
|
+
serialPortConfig: node.serialPortConfig,
|
|
612
|
+
firstChangeTime: now, // 记录第一次变化的时间
|
|
613
|
+
changeCount: 0 // 记录变化次数
|
|
614
|
+
};
|
|
615
|
+
meshLedDebounceTimers.set(meshAddr, timerObj);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 增加变化计数
|
|
619
|
+
timerObj.changeCount++;
|
|
620
|
+
|
|
621
|
+
// 清除之前的定时器
|
|
622
|
+
if (timerObj.timer) {
|
|
623
|
+
clearTimeout(timerObj.timer);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 设置新的定时器:100ms内如果没有新的状态变化,才发送LED反馈
|
|
627
|
+
const capturedNode = node;
|
|
628
|
+
timerObj.timer = setTimeout(() => {
|
|
629
|
+
// 检查是否是当前节点设置的定时器
|
|
630
|
+
const currentTimerObj = meshLedDebounceTimers.get(meshAddr);
|
|
631
|
+
if (currentTimerObj && currentTimerObj.nodeId === capturedNode.id) {
|
|
632
|
+
meshLedDebounceTimers.delete(meshAddr);
|
|
633
|
+
const currentStates = meshDeviceStates.get(meshAddr);
|
|
634
|
+
|
|
635
|
+
// 计算全局锁定时间:100ms(足够发送LED反馈,但不影响下一次按键)
|
|
636
|
+
const lockDuration = 100;
|
|
637
|
+
meshLedFeedbackGlobalLock = Date.now() + lockDuration;
|
|
638
|
+
|
|
639
|
+
// 发送LED反馈到物理开关面板(使用最新的完整状态)
|
|
640
|
+
const button1State = currentStates[0]; // 按钮1的状态
|
|
641
|
+
capturedNode.sendCommandToPanel(button1State);
|
|
642
|
+
}
|
|
643
|
+
}, 100);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 只有当前节点绑定的线圈变化时,才更新节点状态并触发按键LED反馈
|
|
647
|
+
if (coil === node.config.targetCoilNumber) {
|
|
648
|
+
// 更新当前节点状态
|
|
649
|
+
node.currentState = value;
|
|
650
|
+
node.lastStateChange.timestamp = Date.now();
|
|
651
|
+
node.lastStateChange.value = value;
|
|
652
|
+
|
|
653
|
+
if (stateChanged) {
|
|
654
|
+
if (isButtonPress) {
|
|
655
|
+
// 按键触发:立即发送整个面板LED状态(快速响应)
|
|
656
|
+
node.debug(`[LED反馈] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 按键触发,立即发送整个面板LED状态`);
|
|
657
|
+
|
|
658
|
+
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报(缩短锁定时间,避免影响下一次按键)
|
|
659
|
+
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
660
|
+
|
|
661
|
+
// 立即发送LED反馈(整个面板)
|
|
662
|
+
node.sendCommandToPanel(value);
|
|
663
|
+
} else if (isInit) {
|
|
664
|
+
// 首次轮询:发送LED反馈(同步初始状态)
|
|
665
|
+
// 使用防抖机制,避免重复发送
|
|
666
|
+
if (!meshLedDebounceTimers.has(meshAddr)) {
|
|
667
|
+
const capturedNode = node;
|
|
668
|
+
const timer = setTimeout(() => {
|
|
669
|
+
const timerObj = meshLedDebounceTimers.get(meshAddr);
|
|
670
|
+
if (timerObj && timerObj.nodeId === capturedNode.id) {
|
|
671
|
+
meshLedDebounceTimers.delete(meshAddr);
|
|
672
|
+
const currentStates = meshDeviceStates.get(meshAddr);
|
|
673
|
+
capturedNode.debug(`[LED反馈] Mesh${meshAddr} 首次轮询,发送整个面板LED状态: ${JSON.stringify(currentStates)}`);
|
|
674
|
+
|
|
675
|
+
// 设置全局锁定:100ms内忽略所有Mesh面板的状态上报
|
|
676
|
+
meshLedFeedbackGlobalLock = Date.now() + 100;
|
|
677
|
+
|
|
678
|
+
capturedNode.sendCommandToPanel(value);
|
|
679
|
+
}
|
|
680
|
+
}, 200); // 200ms防抖时间,等待所有初始状态收集完毕
|
|
681
|
+
|
|
682
|
+
meshLedDebounceTimers.set(meshAddr, {
|
|
683
|
+
timer: timer,
|
|
684
|
+
nodeId: node.id,
|
|
685
|
+
serialPortConfig: node.serialPortConfig
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
node.debug(`[LED防抖] Mesh${meshAddr} 按钮${node.config.meshButtonNumber}: 首次轮询,设置防抖定时器(200ms)`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 更新节点状态显示
|
|
694
|
+
node.updateStatus();
|
|
695
|
+
|
|
696
|
+
// 输出状态消息(立即输出,不等待防抖)
|
|
697
|
+
node.send({
|
|
698
|
+
payload: value,
|
|
699
|
+
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
700
|
+
switchId: node.config.switchId,
|
|
701
|
+
button: node.config.buttonNumber,
|
|
702
|
+
targetSlave: node.config.targetSlaveAddress,
|
|
703
|
+
targetCoil: node.config.targetCoilNumber
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} else if (slave === node.config.targetSlaveAddress && coil === node.config.targetCoilNumber) {
|
|
708
|
+
// RS-485模式或非Mesh模式:只处理自己绑定的线圈
|
|
709
|
+
// 检查状态是否真正变化
|
|
710
|
+
const stateChanged = (node.currentState !== value);
|
|
559
711
|
|
|
560
712
|
// 更新当前状态
|
|
561
713
|
node.currentState = value;
|
|
562
714
|
node.lastStateChange.timestamp = Date.now();
|
|
563
715
|
node.lastStateChange.value = value;
|
|
564
716
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (!meshDeviceStates.has(meshAddr)) {
|
|
569
|
-
// 初始化状态数组(全部设为null,表示未知状态)
|
|
570
|
-
meshDeviceStates.set(meshAddr, new Array(node.config.meshTotalButtons).fill(null));
|
|
571
|
-
}
|
|
572
|
-
// 更新对应按钮的状态
|
|
573
|
-
const states = meshDeviceStates.get(meshAddr);
|
|
574
|
-
states[node.config.meshButtonNumber - 1] = value;
|
|
575
|
-
node.debug(`[Mesh状态更新] 设备${meshAddr} 按钮${node.config.meshButtonNumber} = ${value}, 完整状态=${JSON.stringify(states)}`);
|
|
717
|
+
if (stateChanged) {
|
|
718
|
+
// RS-485模式:立即发送LED反馈(不需要防抖)
|
|
719
|
+
node.sendCommandToPanel(value);
|
|
576
720
|
}
|
|
577
721
|
|
|
578
722
|
// 更新节点状态显示
|
|
579
723
|
node.updateStatus();
|
|
580
724
|
|
|
581
|
-
// 发送LED反馈到物理开关面板
|
|
582
|
-
node.sendCommandToPanel(value);
|
|
583
|
-
|
|
584
725
|
// 输出状态消息
|
|
585
726
|
node.send({
|
|
586
727
|
payload: value,
|
|
@@ -595,7 +736,6 @@ module.exports = function(RED) {
|
|
|
595
736
|
|
|
596
737
|
// 注册内部事件监听器
|
|
597
738
|
RED.events.on('modbus:coilStateChanged', node.stateChangeListener);
|
|
598
|
-
node.log('已注册状态变化监听器(用于LED反馈)');
|
|
599
739
|
};
|
|
600
740
|
|
|
601
741
|
// 处理RS-485接收到的数据
|
|
@@ -697,36 +837,98 @@ module.exports = function(RED) {
|
|
|
697
837
|
}
|
|
698
838
|
|
|
699
839
|
// 检查消息类型
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return; // 不是开关状态,忽略
|
|
703
|
-
}
|
|
840
|
+
let isSceneMode = false;
|
|
841
|
+
let buttonState = null;
|
|
704
842
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
843
|
+
if (event.msgType === meshProtocol.PROTOCOL.MSG_TYPE_SCENE) {
|
|
844
|
+
// 场景模式(无线开关):触发继电器控制,但不响应LED反馈
|
|
845
|
+
if (event.sceneButton !== node.config.meshButtonNumber) {
|
|
846
|
+
return; // 不是我们的按键,忽略
|
|
847
|
+
}
|
|
848
|
+
isSceneMode = true;
|
|
849
|
+
|
|
850
|
+
// 场景模式:每次触发都翻转状态(点动模式)
|
|
851
|
+
buttonState = !node.currentState;
|
|
852
|
+
node.debug(`[Mesh场景] 设备${event.shortAddr} 按键${event.sceneButton} 触发场景,翻转状态为${buttonState}`);
|
|
853
|
+
} else if (event.msgType === meshProtocol.PROTOCOL.MSG_TYPE_SWITCH ||
|
|
854
|
+
event.msgType === meshProtocol.PROTOCOL.MSG_TYPE_SWITCH_6) {
|
|
855
|
+
// 开关模式:正常处理状态
|
|
856
|
+
// 获取按钮状态
|
|
857
|
+
if (!event.states || event.states.length < node.config.meshButtonNumber) {
|
|
858
|
+
node.warn(`[Mesh事件] 按键${node.config.meshButtonNumber} 状态数据不完整,states=${JSON.stringify(event.states)}`);
|
|
859
|
+
return; // 状态数据不完整
|
|
860
|
+
}
|
|
709
861
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
862
|
+
buttonState = event.states[node.config.meshButtonNumber - 1];
|
|
863
|
+
if (buttonState === null) {
|
|
864
|
+
return; // 状态未知,忽略
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
return; // 不是开关或场景消息,忽略
|
|
713
868
|
}
|
|
714
869
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const now = Date.now();
|
|
718
|
-
const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
|
|
870
|
+
const meshAddr = node.config.meshShortAddress;
|
|
871
|
+
let now = Date.now();
|
|
719
872
|
|
|
720
|
-
//
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
873
|
+
// 场景模式:跳过LED反馈锁和状态缓存检查,直接触发继电器
|
|
874
|
+
if (isSceneMode) {
|
|
875
|
+
// 场景模式:全局防抖(200ms内只触发一次)
|
|
876
|
+
const debounceKey = `mesh-scene-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
|
|
877
|
+
const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
|
|
725
878
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
879
|
+
if (now - lastTriggerTime < 200) {
|
|
880
|
+
node.debug(`[Mesh场景] 全局防抖忽略:设备${meshAddr} 按键${node.config.meshButtonNumber} (${now - lastTriggerTime}ms内)`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
globalDebounceCache.set(debounceKey, now);
|
|
884
|
+
|
|
885
|
+
node.debug(`[Mesh场景] 设备${meshAddr} 按键${node.config.meshButtonNumber} 触发继电器控制: ${buttonState}`);
|
|
886
|
+
|
|
887
|
+
// 跳过后续的状态缓存检查,直接发送继电器控制命令
|
|
888
|
+
} else {
|
|
889
|
+
// 开关模式:正常处理LED反馈锁和状态缓存
|
|
890
|
+
|
|
891
|
+
// 第一步:检查全局LED反馈锁(最高优先级)
|
|
892
|
+
// 在批量LED反馈发送期间,忽略所有Mesh面板的状态上报
|
|
893
|
+
if (now < meshLedFeedbackGlobalLock) {
|
|
894
|
+
// 静默忽略:LED反馈批量发送期间的状态上报
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// 第二步:获取或初始化全局共享状态缓存
|
|
899
|
+
if (!meshDeviceStates.has(meshAddr)) {
|
|
900
|
+
// 第一次收到状态,初始化为全null数组
|
|
901
|
+
meshDeviceStates.set(meshAddr, new Array(event.states.length).fill(null));
|
|
902
|
+
}
|
|
903
|
+
const previousStates = meshDeviceStates.get(meshAddr);
|
|
904
|
+
const previousButtonState = previousStates[node.config.meshButtonNumber - 1];
|
|
905
|
+
|
|
906
|
+
// 第三步:检查当前按键状态是否变化(关键:防止所有节点都被触发)
|
|
907
|
+
const buttonStateChanged = (previousButtonState !== buttonState);
|
|
908
|
+
|
|
909
|
+
if (!buttonStateChanged) {
|
|
910
|
+
// 状态未变化,忽略(这样可以避免所有节点都被触发)
|
|
911
|
+
node.debug(`[Mesh事件] 按键${node.config.meshButtonNumber}状态未变化(${buttonState}),忽略`);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// 第四步:全局防抖:防止多个节点重复处理同一个按键
|
|
916
|
+
const debounceKey = `mesh-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
|
|
917
|
+
now = Date.now();
|
|
918
|
+
const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
|
|
919
|
+
|
|
920
|
+
// 全局防抖:200ms内只触发一次
|
|
921
|
+
if (now - lastTriggerTime < 200) {
|
|
922
|
+
node.debug(`[Mesh事件] 全局防抖忽略:设备${meshAddr} 按键${node.config.meshButtonNumber} (${now - lastTriggerTime}ms内)`);
|
|
923
|
+
return; // 静默忽略重复触发
|
|
924
|
+
}
|
|
925
|
+
globalDebounceCache.set(debounceKey, now);
|
|
926
|
+
|
|
927
|
+
// 第五步:更新全局共享状态缓存(只更新当前按键的状态)
|
|
928
|
+
previousStates[node.config.meshButtonNumber - 1] = buttonState;
|
|
929
|
+
meshDeviceStates.set(meshAddr, previousStates);
|
|
930
|
+
node.debug(`[Mesh按键] 设备${meshAddr} 按键${node.config.meshButtonNumber} 状态变化: ${previousButtonState} → ${buttonState}`);
|
|
931
|
+
}
|
|
730
932
|
|
|
731
933
|
// 初始化期间不发送控制命令(避免重启时Mesh开关状态覆盖继电器状态)
|
|
732
934
|
if (node.isInitializing) {
|
|
@@ -740,8 +942,9 @@ module.exports = function(RED) {
|
|
|
740
942
|
}
|
|
741
943
|
|
|
742
944
|
// 发送命令到继电器
|
|
743
|
-
|
|
744
|
-
node.
|
|
945
|
+
const modeLabel = isSceneMode ? 'Mesh场景' : 'Mesh按键';
|
|
946
|
+
node.debug(`[${modeLabel}] 发送命令到继电器:从站${node.config.targetSlaveAddress} 线圈${node.config.targetCoilNumber} = ${buttonState ? 'ON' : 'OFF'}`);
|
|
947
|
+
node.sendMqttCommand(buttonState, isSceneMode);
|
|
745
948
|
|
|
746
949
|
} catch (err) {
|
|
747
950
|
node.error(`解析Mesh数据失败: ${err.message}`);
|
|
@@ -749,7 +952,7 @@ module.exports = function(RED) {
|
|
|
749
952
|
};
|
|
750
953
|
|
|
751
954
|
// 发送命令到继电器(支持两种模式:MQTT模式和内部事件模式)
|
|
752
|
-
node.sendMqttCommand = function(state) {
|
|
955
|
+
node.sendMqttCommand = function(state, isSceneMode = false) {
|
|
753
956
|
// 模式1:MQTT模式(通过MQTT发送命令,由主站节点统一处理)
|
|
754
957
|
if (node.config.enableMqtt && node.mqttClient && node.mqttClient.connected) {
|
|
755
958
|
// 直接发送MQTT命令(不使用队列,立即发送)
|
|
@@ -775,11 +978,11 @@ module.exports = function(RED) {
|
|
|
775
978
|
coil: node.config.targetCoilNumber,
|
|
776
979
|
value: state,
|
|
777
980
|
source: 'slave-switch',
|
|
981
|
+
triggerSource: isSceneMode ? 'scene-trigger' : 'button-press', // 场景模式使用scene-trigger,开关模式使用button-press
|
|
778
982
|
nodeId: node.id
|
|
779
983
|
});
|
|
780
984
|
|
|
781
|
-
//
|
|
782
|
-
node.log(`内部事件模式:发送命令到从站${node.config.targetSlaveAddress} 线圈${node.config.targetCoilNumber} = ${state ? 'ON' : 'OFF'}`);
|
|
985
|
+
// 内部事件模式:静默发送(不输出日志)
|
|
783
986
|
};
|
|
784
987
|
|
|
785
988
|
// 处理命令队列(防止多个按键同时按下造成冲突)
|
|
@@ -847,22 +1050,12 @@ module.exports = function(RED) {
|
|
|
847
1050
|
return;
|
|
848
1051
|
}
|
|
849
1052
|
|
|
850
|
-
// 智能LED反馈控制:首次轮询时允许发送,通过队列自动限速
|
|
851
|
-
// 移除固定5秒延迟,部署/重启后立即同步LED状态(1秒内完成)
|
|
852
|
-
// 队列机制已有40ms间隔,不会造成总线拥堵
|
|
1053
|
+
// 智能LED反馈控制:首次轮询时允许发送,通过队列自动限速
|
|
1054
|
+
// 移除固定5秒延迟,部署/重启后立即同步LED状态(1秒内完成)
|
|
1055
|
+
// 队列机制已有40ms间隔,不会造成总线拥堵
|
|
853
1056
|
|
|
854
1057
|
const now = Date.now();
|
|
855
1058
|
|
|
856
|
-
// 防止重复发送:如果状态相同且时间间隔小于50ms,跳过
|
|
857
|
-
if (node.lastSentLedState.value === state && (now - node.lastSentLedState.timestamp) < 50) {
|
|
858
|
-
node.debug(`跳过重复LED反馈(状态未变化,间隔${now - node.lastSentLedState.timestamp}ms)`);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// 更新最后发送状态
|
|
863
|
-
node.lastSentLedState.value = state;
|
|
864
|
-
node.lastSentLedState.timestamp = now;
|
|
865
|
-
|
|
866
1059
|
// 构建LED反馈协议帧
|
|
867
1060
|
let command;
|
|
868
1061
|
|
|
@@ -870,9 +1063,40 @@ module.exports = function(RED) {
|
|
|
870
1063
|
// Mesh模式:发送Mesh控制帧
|
|
871
1064
|
// 从全局共享状态中获取当前设备的完整状态
|
|
872
1065
|
const meshAddr = node.config.meshShortAddress;
|
|
873
|
-
|
|
1066
|
+
let currentStates = meshDeviceStates.get(meshAddr) || null;
|
|
1067
|
+
|
|
1068
|
+
// 无线模式过滤:将null状态和无线模式按键的LED状态强制设为OFF
|
|
1069
|
+
// 复制状态数组,避免修改全局状态
|
|
1070
|
+
if (currentStates) {
|
|
1071
|
+
const wirelessButtons = meshWirelessButtons.get(meshAddr);
|
|
1072
|
+
node.debug(`[无线模式检查] 设备${meshAddr} 无线按键=${wirelessButtons ? Array.from(wirelessButtons) : '无'}`);
|
|
1073
|
+
currentStates = currentStates.map((s, idx) => {
|
|
1074
|
+
// null状态强制设为false
|
|
1075
|
+
if (s === null) return false;
|
|
1076
|
+
// 无线模式按键强制设为false(不响应LED反馈)
|
|
1077
|
+
const buttonNum = idx + 1;
|
|
1078
|
+
if (wirelessButtons && wirelessButtons.has(buttonNum)) {
|
|
1079
|
+
node.debug(`[无线模式过滤] 按键${buttonNum} LED强制设为OFF`);
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
return s;
|
|
1083
|
+
});
|
|
1084
|
+
node.debug(`[LED反馈] 过滤后状态=${JSON.stringify(currentStates)}`);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// 防止重复发送:比较完整状态数组,如果相同且时间间隔小于50ms,跳过
|
|
1088
|
+
const statesStr = JSON.stringify(currentStates);
|
|
1089
|
+
if (node.lastSentLedState.statesStr === statesStr && (now - node.lastSentLedState.timestamp) < 50) {
|
|
1090
|
+
node.debug(`跳过重复LED反馈(状态未变化,间隔${now - node.lastSentLedState.timestamp}ms)`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
874
1093
|
|
|
875
|
-
|
|
1094
|
+
// 更新最后发送状态
|
|
1095
|
+
node.lastSentLedState.value = state;
|
|
1096
|
+
node.lastSentLedState.statesStr = statesStr;
|
|
1097
|
+
node.lastSentLedState.timestamp = now;
|
|
1098
|
+
|
|
1099
|
+
node.debug(`[Mesh LED发送] 设备${meshAddr} 按钮${node.config.meshButtonNumber} = ${state}, 完整状态=${JSON.stringify(currentStates)}`);
|
|
876
1100
|
|
|
877
1101
|
command = meshProtocol.buildSwitchControlFrame(
|
|
878
1102
|
node.config.meshShortAddress,
|
|
@@ -923,15 +1147,7 @@ module.exports = function(RED) {
|
|
|
923
1147
|
if (err) {
|
|
924
1148
|
node.error(`LED反馈失败: ${err.message}`);
|
|
925
1149
|
} else {
|
|
926
|
-
//
|
|
927
|
-
const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
|
|
928
|
-
if (node.config.buttonType === 'mesh') {
|
|
929
|
-
node.log(`Mesh LED反馈已发送:MAC=${node.config.meshMacAddress} 按钮${node.config.meshButtonNumber} = ${state ? 'ON' : 'OFF'} [${hexStr}]`);
|
|
930
|
-
} else {
|
|
931
|
-
const deviceAddr = node.buttonDeviceAddr;
|
|
932
|
-
const channel = node.buttonChannel;
|
|
933
|
-
node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
|
|
934
|
-
}
|
|
1150
|
+
// LED反馈发送成功(静默,不输出日志)
|
|
935
1151
|
}
|
|
936
1152
|
}, priority, switchId);
|
|
937
1153
|
};
|
|
@@ -981,8 +1197,6 @@ module.exports = function(RED) {
|
|
|
981
1197
|
node.connectMqtt = function() {
|
|
982
1198
|
// 检查是否启用MQTT
|
|
983
1199
|
if (!node.config.enableMqtt) {
|
|
984
|
-
node.log('MQTT未启用 - 使用内部事件模式(免连线通信)');
|
|
985
|
-
node.log('提示:物理开关面板按键会通过内部事件自动发送到主站节点');
|
|
986
1200
|
return;
|
|
987
1201
|
}
|
|
988
1202
|
|
|
@@ -1032,12 +1246,6 @@ module.exports = function(RED) {
|
|
|
1032
1246
|
|
|
1033
1247
|
node.mqttClient.on('connect', () => {
|
|
1034
1248
|
node.mqttConnected = true;
|
|
1035
|
-
node.log(`MQTT已连接: ${brokerUrl}`);
|
|
1036
|
-
|
|
1037
|
-
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
1038
|
-
if (brokerUrl !== brokerCandidates[0]) {
|
|
1039
|
-
node.debug(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
1040
|
-
}
|
|
1041
1249
|
|
|
1042
1250
|
node.updateStatus();
|
|
1043
1251
|
|
|
@@ -1045,8 +1253,6 @@ module.exports = function(RED) {
|
|
|
1045
1253
|
node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
|
|
1046
1254
|
if (err) {
|
|
1047
1255
|
node.error(`订阅失败: ${err.message}`);
|
|
1048
|
-
} else {
|
|
1049
|
-
node.log(`已订阅: ${node.stateTopic}(QoS=1)`);
|
|
1050
1256
|
}
|
|
1051
1257
|
});
|
|
1052
1258
|
});
|
|
@@ -1238,8 +1444,6 @@ module.exports = function(RED) {
|
|
|
1238
1444
|
node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
|
|
1239
1445
|
if (err) {
|
|
1240
1446
|
node.error(`发布命令失败: ${err.message}`);
|
|
1241
|
-
} else {
|
|
1242
|
-
node.log(`发送命令: ${command} 到 ${node.commandTopic}`);
|
|
1243
1447
|
}
|
|
1244
1448
|
});
|
|
1245
1449
|
} else {
|
|
@@ -1254,13 +1458,22 @@ module.exports = function(RED) {
|
|
|
1254
1458
|
// 节点关闭时清理
|
|
1255
1459
|
node.on('close', function(done) {
|
|
1256
1460
|
node.isClosing = true;
|
|
1257
|
-
|
|
1461
|
+
|
|
1258
1462
|
// 清除重连定时器
|
|
1259
1463
|
if (node.reconnectTimer) {
|
|
1260
1464
|
clearTimeout(node.reconnectTimer);
|
|
1261
1465
|
node.reconnectTimer = null;
|
|
1262
1466
|
}
|
|
1263
|
-
|
|
1467
|
+
|
|
1468
|
+
// 清除全局Mesh LED防抖定时器(如果是Mesh模式)
|
|
1469
|
+
if (node.config.buttonType === 'mesh') {
|
|
1470
|
+
const meshAddr = node.config.meshShortAddress;
|
|
1471
|
+
if (meshLedDebounceTimers.has(meshAddr)) {
|
|
1472
|
+
clearTimeout(meshLedDebounceTimers.get(meshAddr).timer);
|
|
1473
|
+
meshLedDebounceTimers.delete(meshAddr);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1264
1477
|
// 清理队列(释放内存)
|
|
1265
1478
|
node.commandQueue = [];
|
|
1266
1479
|
node.frameBuffer = Buffer.alloc(0);
|
|
@@ -1269,7 +1482,6 @@ module.exports = function(RED) {
|
|
|
1269
1482
|
if (node.serialPortConfig && node.serialDataListener) {
|
|
1270
1483
|
try {
|
|
1271
1484
|
node.serialPortConfig.unregisterDataListener(node.serialDataListener);
|
|
1272
|
-
node.log('RS-485数据监听器已注销');
|
|
1273
1485
|
} catch (err) {
|
|
1274
1486
|
node.warn(`注销RS-485监听器时出错: ${err.message}`);
|
|
1275
1487
|
}
|
|
@@ -1280,7 +1492,6 @@ module.exports = function(RED) {
|
|
|
1280
1492
|
if (node.stateChangeListener) {
|
|
1281
1493
|
RED.events.removeListener('modbus:coilStateChanged', node.stateChangeListener);
|
|
1282
1494
|
node.stateChangeListener = null;
|
|
1283
|
-
node.log('状态变化监听器已移除');
|
|
1284
1495
|
}
|
|
1285
1496
|
|
|
1286
1497
|
// 关闭MQTT连接
|
|
@@ -1290,7 +1501,6 @@ module.exports = function(RED) {
|
|
|
1290
1501
|
// 移除所有监听器(防止内存泄漏)
|
|
1291
1502
|
node.mqttClient.removeAllListeners();
|
|
1292
1503
|
node.mqttClient.end(false, () => {
|
|
1293
|
-
node.log('MQTT连接已关闭');
|
|
1294
1504
|
node.mqttClient = null;
|
|
1295
1505
|
node.mqttConnected = false;
|
|
1296
1506
|
done();
|
|
@@ -1313,6 +1523,16 @@ module.exports = function(RED) {
|
|
|
1313
1523
|
});
|
|
1314
1524
|
|
|
1315
1525
|
// 初始化
|
|
1526
|
+
// 如果是Mesh无线模式,注册到全局Map
|
|
1527
|
+
if (node.config.buttonType === 'mesh' && node.config.meshWirelessMode === true) {
|
|
1528
|
+
const meshAddr = node.config.meshShortAddress;
|
|
1529
|
+
if (!meshWirelessButtons.has(meshAddr)) {
|
|
1530
|
+
meshWirelessButtons.set(meshAddr, new Set());
|
|
1531
|
+
}
|
|
1532
|
+
meshWirelessButtons.get(meshAddr).add(node.config.meshButtonNumber);
|
|
1533
|
+
node.log(`[无线模式] 注册按键:设备${meshAddr} 按键${node.config.meshButtonNumber}`);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1316
1536
|
node.updateStatus();
|
|
1317
1537
|
node.connectRs485(); // 连接RS-485总线
|
|
1318
1538
|
node.connectMqtt(); // 连接MQTT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.9",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|