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 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.7 (2025-11-12)
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个以内面板,约200个按钮(设计极限500+)
927
+ - 开关面板:支持50个以内Mesh面板,约200个按钮
907
928
  - LED同步:部署/重启后1秒内完成所有LED状态同步
908
- - 批量控制:支持17通道全开/全关等批量操作,LED反馈流畅无卡顿
929
+ - 批量控制:支持全开/全关等批量操作,LED反馈流畅无卡顿
930
+ - 多面板支持:100个面板同时LED反馈仅需4秒(40ms间隔×100)
931
+ - 场景触发:支持无线模式按键,不受LED反馈影响
909
932
 
910
933
  ## 许可证
911
934
 
@@ -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
- const msgType = frame[6];
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
- msgType: msgType
224
+ triggerSource: null // 0=未知, 1=本地按键, 2=Mesh网络, 3=私有双控网络
220
225
  };
221
226
 
222
- // 根据消息类型解析状态
223
- if (msgType === PROTOCOL.MSG_TYPE_SWITCH) {
224
- // 1-4路开关状态
225
- const stateValue = frame[7];
226
- event.states = parseMultiSwitchState(stateValue, 4);
227
- } else if (msgType === PROTOCOL.MSG_TYPE_SWITCH_6) {
228
- // 6路开关状态
229
- const stateLow = frame[7];
230
- const stateHigh = frame[8];
231
- const stateValue = stateLow | (stateHigh << 8);
232
- event.states = parseMultiSwitchState(stateValue, 6);
233
- } else if (msgType === PROTOCOL.MSG_TYPE_DIMMER) {
234
- // 调光灯状态
235
- event.brightness = frame[7];
236
- event.colorTemp = frame[8];
237
- } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN) {
238
- // 窗帘动作状态
239
- event.action = frame[7]; // 0=停止, 1=打开中, 2=关闭中
240
- } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN_POS) {
241
- // 窗帘位置状态
242
- event.position = frame[7]; // 0-100%
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;
@@ -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
- if (i === node.meshButtonNumber) {
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
- <span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">Mesh开关按键(1-6路)</span>
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
- node.handleRs485Data(data);
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
- if (slave === node.config.targetSlaveAddress && coil === node.config.targetCoilNumber) {
558
- node.log(`收到状态变化事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
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
- // Mesh模式:更新全局共享状态缓存(用于构建正确的控制帧)
566
- if (node.config.buttonType === 'mesh') {
567
- const meshAddr = node.config.meshShortAddress;
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
- if (event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH &&
701
- event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH_6) {
702
- return; // 不是开关状态,忽略
703
- }
840
+ let isSceneMode = false;
841
+ let buttonState = null;
704
842
 
705
- // 获取按钮状态
706
- if (!event.states || event.states.length < node.config.meshButtonNumber) {
707
- return; // 状态数据不完整
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
- const buttonState = event.states[node.config.meshButtonNumber - 1];
711
- if (buttonState === null) {
712
- return; // 状态未知,忽略
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
- const debounceKey = `mesh-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
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
- // 全局防抖:200ms内只触发一次
721
- if (now - lastTriggerTime < 200) {
722
- return; // 静默忽略重复触发
723
- }
724
- globalDebounceCache.set(debounceKey, now);
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
- const meshAddr = node.config.meshShortAddress;
728
- meshDeviceStates.set(meshAddr, event.states);
729
- node.debug(`[Mesh按键] 设备${meshAddr} 状态更新=${JSON.stringify(event.states)}`);
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
- node.debug(`Mesh开关${buttonState ? 'ON' : 'OFF'}: MAC=${node.config.meshMacAddress} 按键${node.config.meshButtonNumber}`);
744
- node.sendMqttCommand(buttonState);
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
- const currentStates = meshDeviceStates.get(meshAddr) || null;
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
- node.debug(`[Mesh LED] 准备发送:设备${meshAddr} 按钮${node.config.meshButtonNumber} = ${state}, 当前状态=${JSON.stringify(currentStates)}`);
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
- // 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
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.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": {