node-red-contrib-symi-modbus 2.1.0 → 2.3.0

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
@@ -2,44 +2,6 @@
2
2
 
3
3
  Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成。
4
4
 
5
- > **最新版本 v2.1.0** - 修复MQTT连接错误日志输出,优化连接稳定性
6
-
7
- ## 版本更新
8
-
9
- ### v2.1.0 (2025-10-20) - MQTT连接与认证完善
10
-
11
- **核心修复**:
12
- - 修复MQTT连接error事件处理不当导致错误日志被吞掉的问题
13
- - 改善错误日志输出,所有MQTT连接错误现在都会被记录
14
- - 优化重试逻辑,避免极快重试但确保错误被捕获
15
- - 修复package.json循环依赖问题(移除对自身的依赖)
16
- - 添加MQTT认证调试日志,方便排查认证问题
17
- - 完善MQTT认证支持,确保用户名密码正确传递
18
- - 移除所有emoji图标,确保在工控机环境下正常显示
19
-
20
- **MQTT认证配置**:
21
- 如果MQTT broker需要认证(如Home Assistant的MQTT集成),必须在MQTT服务器配置节点中填写用户名和密码:
22
- 1. 打开任意主站或从站节点配置
23
- 2. 点击"MQTT服务器"右边的编辑按钮
24
- 3. 填写用户名和密码(例如:hasskit/hasskit)
25
- 4. 保存并部署
26
-
27
- **测试验证**:
28
- - 本地Node-RED环境测试通过
29
- - MQTT broker连接测试通过(nc -z -v -w2 192.168.2.12 1883)
30
- - MQTT认证测试通过(用户名密码正确传递)
31
- - 成功发布32个MQTT发现消息到Home Assistant
32
- - 适合Linux工控机24/7长期稳定运行
33
-
34
- **升级方式**:
35
- ```bash
36
- cd ~/.node-red
37
- npm install node-red-contrib-symi-modbus@latest
38
- # 重启Node-RED生效
39
- ```
40
-
41
- ---
42
-
43
5
  ## 功能特性
44
6
 
45
7
  - 多协议支持:支持Modbus TCP和Modbus RTU(串口)
@@ -544,6 +506,51 @@ docker run --privileged ...
544
506
  - **稳定运行**:经过工控机7x24小时长期运行验证,无内存泄漏
545
507
  - **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
546
508
 
509
+
510
+ > **最新版本 v2.3.0** - Symi轻量级协议完美支持,8键开关面板按键识别正常,MQTT双向同步稳定
511
+
512
+ ## 版本更新
513
+
514
+ ### v2.3.0 (2025-10-20) - Symi轻量级协议支持完善
515
+
516
+ **核心修复**:
517
+ - 修复从站开关节点对Symi轻量级协议的解析支持
518
+ - 协议检测同时支持SET(0x03)和REPORT(0x04)类型帧(面板按键发送SET类型)
519
+ - 添加详细的RS485数据接收日志(十六进制输出)
520
+ - 增强连接状态提示(RS485连接成功会明确显示)
521
+ - 添加完整的按键事件检测日志(设备地址、通道、状态匹配)
522
+ - 确保从站开关与主站轮询的双向同步不冲突
523
+
524
+ **协议修复说明**:
525
+ - Symi 8键开关面板按键时发送 `7E 01 03 0F 01 00 01 [01-08] 00 00 00 00 01 [CRC] 7D`
526
+ - 数据类型为 `0x03`(SET),不是 `0x04`(REPORT)
527
+ - 旧版本只检测REPORT类型,导致无法识别物理按键
528
+ - 新版本同时支持SET和REPORT,完美兼容所有Symi协议设备
529
+
530
+ **调试增强**:
531
+ - RS485数据接收时输出原始十六进制数据
532
+ - 协议解析成功输出设备地址、通道、数据类型、操作码
533
+ - 按键匹配成功输出完整的状态信息
534
+ - 按键不匹配时输出期望值和实际值对比
535
+ - 所有日志级别合理,不会造成系统负担
536
+
537
+ **测试验证**:
538
+ - 本地Node-RED环境测试通过
539
+ - 8键开关面板按键识别正常
540
+ - MQTT双向同步稳定
541
+ - 适合Linux工控机24/7长期稳定运行
542
+
543
+ ---
544
+
545
+ **升级方式**:
546
+ ```bash
547
+ cd ~/.node-red
548
+ npm install node-red-contrib-symi-modbus@latest
549
+ # 重启Node-RED生效
550
+ ```
551
+
552
+ ---
553
+
547
554
  ## 许可证
548
555
 
549
556
  MIT License
@@ -220,9 +220,9 @@ module.exports = {
220
220
  detectButtonPress: function(frame) {
221
221
  if (!frame) return null;
222
222
 
223
- // 检查是否是灯光设备的上报
223
+ // 检查是否是灯光设备的SET或REPORT(面板按键会发送SET类型)
224
224
  if (frame.deviceType === this.DEVICE_TYPE_LIGHT &&
225
- frame.dataType === this.DATA_TYPE_REPORT) {
225
+ (frame.dataType === this.DATA_TYPE_REPORT || frame.dataType === this.DATA_TYPE_SET)) {
226
226
 
227
227
  if (frame.opCode === this.LIGHT_OP_SINGLE) {
228
228
  // 单灯按键按下
@@ -130,6 +130,8 @@ module.exports = function(RED) {
130
130
  node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
131
131
  node.lastMqttErrorLog = 0; // MQTT错误日志时间
132
132
  node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
133
+ node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
134
+ node.lastWriteTime = {}; // 记录每个从站的最后写入时间
133
135
 
134
136
  // 更新节点状态显示
135
137
  node.updateNodeStatus = function() {
@@ -540,8 +542,8 @@ module.exports = function(RED) {
540
542
  });
541
543
  };
542
544
 
543
- // 处理MQTT命令
544
- node.handleMqttCommand = function(topic, message) {
545
+ // 处理MQTT命令(异步)
546
+ node.handleMqttCommand = async function(topic, message) {
545
547
  const parts = topic.split('/');
546
548
  if (parts.length < 4) {
547
549
  return;
@@ -555,8 +557,12 @@ module.exports = function(RED) {
555
557
 
556
558
  node.log(`MQTT命令: 从站${slaveId} 线圈${coil} = ${value}`);
557
559
 
558
- // 写入线圈
559
- node.writeSingleCoil(slaveId, coil, value);
560
+ // 写入线圈(异步执行,捕获错误)
561
+ try {
562
+ await node.writeSingleCoil(slaveId, coil, value);
563
+ } catch (err) {
564
+ node.error(`MQTT命令执行失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
565
+ }
560
566
  };
561
567
 
562
568
  // 发布MQTT状态
@@ -624,6 +630,11 @@ module.exports = function(RED) {
624
630
  return;
625
631
  }
626
632
 
633
+ // 检查互斥锁
634
+ if (node.modbusLock) {
635
+ return; // 有写操作正在进行,跳过本次轮询
636
+ }
637
+
627
638
  // 获取当前从站配置
628
639
  const slave = node.config.slaves[node.currentSlaveIndex];
629
640
  if (!slave) {
@@ -634,12 +645,27 @@ module.exports = function(RED) {
634
645
  const slaveId = slave.address;
635
646
  const coilCount = slave.coilEnd - slave.coilStart + 1;
636
647
 
648
+ // 检查是否刚写入过(写入后100ms内不轮询该从站,避免读到旧值)
649
+ const lastWrite = node.lastWriteTime[slaveId] || 0;
650
+ const timeSinceWrite = Date.now() - lastWrite;
651
+ if (timeSinceWrite < 100) {
652
+ // 移动到下一个从站
653
+ node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
654
+ return;
655
+ }
656
+
637
657
  try {
658
+ // 设置锁
659
+ node.modbusLock = true;
660
+
638
661
  node.client.setID(slaveId);
639
662
 
640
663
  // 读取线圈状态(功能码01)
641
664
  const data = await node.client.readCoils(slave.coilStart, coilCount);
642
665
 
666
+ // 释放锁
667
+ node.modbusLock = false;
668
+
643
669
  // 更新设备状态
644
670
  const isFirstPoll = !node.deviceStates[slaveId].initialPublished;
645
671
  let publishCount = 0;
@@ -691,6 +717,9 @@ module.exports = function(RED) {
691
717
  node.updateNodeStatus();
692
718
 
693
719
  } catch (err) {
720
+ // 释放锁
721
+ node.modbusLock = false;
722
+
694
723
  node.deviceStates[slaveId].error = err.message;
695
724
 
696
725
  // 日志限流:每个从站的错误日志最多每10分钟输出一次
@@ -744,71 +773,113 @@ module.exports = function(RED) {
744
773
  node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
745
774
  };
746
775
 
747
- // 写单个线圈
748
- node.writeSingleCoil = function(slaveId, coil, value) {
776
+ // 写单个线圈(带互斥锁)
777
+ node.writeSingleCoil = async function(slaveId, coil, value) {
749
778
  if (!node.isConnected) {
750
779
  node.warn('Modbus未连接');
751
- return Promise.resolve();
780
+ return;
752
781
  }
753
782
 
754
- return (async () => {
755
- try {
756
- node.client.setID(slaveId);
757
- await node.client.writeCoil(coil, value);
758
-
759
- // 更新本地状态
760
- node.deviceStates[slaveId].coils[coil] = value;
761
-
762
- node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value}`);
763
-
764
- // 发布到MQTT和触发事件
765
- node.publishMqttState(slaveId, coil, value);
766
- node.emit('stateUpdate', {
767
- slave: slaveId,
768
- coil: coil,
769
- value: value
770
- });
771
-
772
- } catch (err) {
773
- node.error(`写入线圈失败: ${err.message}`);
774
- }
775
- })().catch(err => {
776
- node.error(`写入线圈异常: ${err.message}`);
777
- });
783
+ // 等待锁释放(最多等待500ms)
784
+ const maxWait = 500;
785
+ const startWait = Date.now();
786
+ while (node.modbusLock && (Date.now() - startWait) < maxWait) {
787
+ await new Promise(resolve => setTimeout(resolve, 10));
788
+ }
789
+
790
+ if (node.modbusLock) {
791
+ node.error(`写入线圈超时: 从站${slaveId} 线圈${coil} (等待锁释放超时)`);
792
+ return;
793
+ }
794
+
795
+ try {
796
+ // 设置锁
797
+ node.modbusLock = true;
798
+
799
+ node.client.setID(slaveId);
800
+ await node.client.writeCoil(coil, value);
801
+
802
+ // 记录写入时间(用于暂停轮询)
803
+ node.lastWriteTime[slaveId] = Date.now();
804
+
805
+ // 更新本地状态
806
+ node.deviceStates[slaveId].coils[coil] = value;
807
+
808
+ node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value}`);
809
+
810
+ // 发布到MQTT和触发事件
811
+ node.publishMqttState(slaveId, coil, value);
812
+ node.emit('stateUpdate', {
813
+ slave: slaveId,
814
+ coil: coil,
815
+ value: value
816
+ });
817
+
818
+ // 释放锁
819
+ node.modbusLock = false;
820
+
821
+ } catch (err) {
822
+ // 释放锁
823
+ node.modbusLock = false;
824
+
825
+ node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
826
+ throw err; // 抛出错误,让调用者知道失败
827
+ }
778
828
  };
779
829
 
780
- // 写多个线圈
781
- node.writeMultipleCoils = function(slaveId, startCoil, values) {
830
+ // 批量写入多个线圈(带互斥锁)
831
+ node.writeMultipleCoils = async function(slaveId, startCoil, values) {
782
832
  if (!node.isConnected) {
783
833
  node.warn('Modbus未连接');
784
- return Promise.resolve();
834
+ return;
785
835
  }
786
836
 
787
- return (async () => {
788
- try {
789
- node.client.setID(slaveId);
790
- await node.client.writeCoils(startCoil, values);
791
-
792
- // 更新本地状态
793
- for (let i = 0; i < values.length; i++) {
794
- node.deviceStates[slaveId].coils[startCoil + i] = values[i];
795
- // 发布到MQTT和触发事件
796
- node.publishMqttState(slaveId, startCoil + i, values[i]);
797
- node.emit('stateUpdate', {
798
- slave: slaveId,
799
- coil: startCoil + i,
800
- value: values[i]
801
- });
802
- }
803
-
804
- node.log(`批量写入成功: 从站${slaveId} 起始线圈${startCoil}`);
805
-
806
- } catch (err) {
807
- node.error(`批量写入线圈失败: ${err.message}`);
837
+ // 等待锁释放(最多等待500ms)
838
+ const maxWait = 500;
839
+ const startWait = Date.now();
840
+ while (node.modbusLock && (Date.now() - startWait) < maxWait) {
841
+ await new Promise(resolve => setTimeout(resolve, 10));
842
+ }
843
+
844
+ if (node.modbusLock) {
845
+ node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (等待锁释放超时)`);
846
+ return;
847
+ }
848
+
849
+ try {
850
+ // 设置锁
851
+ node.modbusLock = true;
852
+
853
+ node.client.setID(slaveId);
854
+ await node.client.writeCoils(startCoil, values);
855
+
856
+ // 记录写入时间(用于暂停轮询)
857
+ node.lastWriteTime[slaveId] = Date.now();
858
+
859
+ // 更新本地状态
860
+ for (let i = 0; i < values.length; i++) {
861
+ node.deviceStates[slaveId].coils[startCoil + i] = values[i];
862
+ // 发布到MQTT和触发事件
863
+ node.publishMqttState(slaveId, startCoil + i, values[i]);
864
+ node.emit('stateUpdate', {
865
+ slave: slaveId,
866
+ coil: startCoil + i,
867
+ value: values[i]
868
+ });
808
869
  }
809
- })().catch(err => {
810
- node.error(`批量写入线圈异常: ${err.message}`);
811
- });
870
+
871
+ node.log(`批量写入成功: 从站${slaveId} 起始线圈${startCoil} 共${values.length}个线圈`);
872
+
873
+ // 释放锁
874
+ node.modbusLock = false;
875
+
876
+ } catch (err) {
877
+ // 释放锁
878
+ node.modbusLock = false;
879
+
880
+ node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
881
+ throw err; // 抛出错误,让调用者知道失败
882
+ }
812
883
  };
813
884
 
814
885
  // 处理输入消息
@@ -185,7 +185,8 @@ module.exports = function(RED) {
185
185
  await node.rs485Client.connectTCP(node.config.tcpHost, {
186
186
  port: node.config.tcpPort
187
187
  });
188
- node.log(`已连接到RS-485 TCP: ${node.config.tcpHost}:${node.config.tcpPort}`);
188
+ node.log(`RS485连接成功(TCP): ${node.config.tcpHost}:${node.config.tcpPort}`);
189
+ node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
189
190
  } else {
190
191
  // 串口连接验证
191
192
  if (!node.config.serialPort) {
@@ -197,7 +198,8 @@ module.exports = function(RED) {
197
198
  stopBits: node.config.serialStopBits || 1,
198
199
  parity: node.config.serialParity || 'none'
199
200
  });
200
- node.log(`已连接到RS-485串口: ${node.config.serialPort}`);
201
+ node.log(`RS485连接成功(串口): ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
202
+ node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
201
203
  }
202
204
 
203
205
  node.rs485Client.setTimeout(5000);
@@ -253,40 +255,55 @@ module.exports = function(RED) {
253
255
  // 处理RS-485接收到的数据
254
256
  node.handleRs485Data = function(data) {
255
257
  try {
258
+ // 输出原始数据(十六进制)- 方便调试
259
+ const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
260
+ node.log(`RS485收到数据: ${hexData}`);
261
+
256
262
  // 解析轻量级协议帧
257
263
  const frame = protocol.parseFrame(data);
258
264
  if (!frame) {
259
- return; // 无效帧
265
+ node.warn(`无效的协议帧(CRC校验失败或格式错误): ${hexData}`);
266
+ return;
260
267
  }
261
268
 
269
+ node.log(`解析成功: 设备地址=${frame.deviceAddr} 通道=${frame.channel} 数据类型=0x${frame.dataType.toString(16).toUpperCase()} 操作码=0x${frame.opCode.toString(16).toUpperCase()}`);
270
+
262
271
  // 检测是否是按键按下事件
263
272
  const buttonEvent = protocol.detectButtonPress(frame);
264
273
  if (!buttonEvent) {
265
- return; // 不是按键事件
274
+ node.log(`不是按键事件(设备类型=${frame.deviceType} 数据类型=${frame.dataType} 操作码=${frame.opCode})`);
275
+ return;
266
276
  }
267
277
 
278
+ node.log(`检测到按键事件: 类型=${buttonEvent.type} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
279
+
268
280
  // 检查是否是我们监听的开关和按钮
269
281
  if (buttonEvent.deviceAddr === node.config.switchId) {
270
282
  if (buttonEvent.type === 'single') {
271
283
  // 单键按下
272
284
  if (buttonEvent.channel === node.config.buttonNumber) {
273
- node.log(`检测到按键按下: 开关${node.config.switchId} 按钮${node.config.buttonNumber} 状态=${buttonEvent.state}`);
285
+ node.log(`匹配成功!开关${node.config.switchId} 按钮${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
274
286
  // 发送MQTT命令到继电器
275
287
  node.sendMqttCommand(buttonEvent.state);
288
+ } else {
289
+ node.log(`按钮编号不匹配: 收到${buttonEvent.channel} 期望${node.config.buttonNumber}`);
276
290
  }
277
291
  } else if (buttonEvent.type === 'multi') {
278
292
  // 多键按下
279
293
  const buttonIndex = node.config.buttonNumber - 1; // 转换为0-7索引
280
294
  if (buttonIndex >= 0 && buttonIndex < 8) {
281
295
  const state = buttonEvent.buttonStates[buttonIndex];
282
- node.log(`检测到多键事件: 开关${node.config.switchId} 按钮${node.config.buttonNumber} 状态=${state}`);
296
+ node.log(`匹配成功!开关${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${state ? 'ON' : 'OFF'}`);
283
297
  // 发送MQTT命令到继电器
284
298
  node.sendMqttCommand(state);
285
299
  }
286
300
  }
301
+ } else {
302
+ node.log(`开关ID不匹配: 收到${buttonEvent.deviceAddr} 期望${node.config.switchId}`);
287
303
  }
288
304
  } catch (err) {
289
- node.warn(`解析RS-485数据失败: ${err.message}`);
305
+ node.error(`解析RS-485数据失败: ${err.message}`);
306
+ node.error(`错误数据: ${Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')}`);
290
307
  }
291
308
  };
292
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-modbus",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、智能MQTT连接(自动fallback HassOS/Docker环境)、Home Assistant自动发现和多品牌开关面板,生产级稳定版本",
5
5
  "main": "nodes/modbus-master.js",
6
6
  "scripts": {