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 +45 -38
- package/nodes/lightweight-protocol.js +2 -2
- package/nodes/modbus-master.js +129 -58
- package/nodes/modbus-slave-switch.js +24 -7
- package/package.json +1 -1
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
|
// 单灯按键按下
|
package/nodes/modbus-master.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
780
|
+
return;
|
|
752
781
|
}
|
|
753
782
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
834
|
+
return;
|
|
785
835
|
}
|
|
786
836
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
810
|
-
node.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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": {
|