node-red-contrib-symi-modbus 2.6.4 → 2.6.6
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 +334 -15
- package/nodes/modbus-debug.html +95 -0
- package/nodes/modbus-debug.js +199 -0
- package/nodes/modbus-master.html +1 -17
- package/nodes/modbus-master.js +5 -5
- package/nodes/modbus-slave-switch.html +23 -3
- package/nodes/modbus-slave-switch.js +24 -16
- package/package.json +59 -55
package/README.md
CHANGED
|
@@ -246,6 +246,34 @@ node-red-restart
|
|
|
246
246
|
- 从站上报的数据得到优先处理
|
|
247
247
|
- 系统响应迅速,状态同步及时
|
|
248
248
|
|
|
249
|
+
## 调试节点(modbus-debug)
|
|
250
|
+
|
|
251
|
+
用于抓取并显示原始RS485字节流数据(HEX),帮助定位串口或TCP网关下的Modbus通信问题。
|
|
252
|
+
|
|
253
|
+
- 数据来源:
|
|
254
|
+
- 共享连接:`serial-port-config`(推荐,复用主站/从站的同一连接)
|
|
255
|
+
- 独立连接:`modbus-server-config`(直接连接TCP或串口)
|
|
256
|
+
- 输出内容:
|
|
257
|
+
- `msg.payload`:格式化的HEX字符串(可选大写)
|
|
258
|
+
- `msg.buffer`:原始Buffer数据
|
|
259
|
+
- `msg.meta`:来源信息与连接参数(串口或TCP详情)
|
|
260
|
+
- `msg.timestamp`:时间戳(可选)
|
|
261
|
+
- 显示控制:
|
|
262
|
+
- `maxBytes`:限制显示字节数,超过会截断并在`meta.truncated`标记
|
|
263
|
+
|
|
264
|
+
使用步骤:
|
|
265
|
+
1. 将 `modbus-debug` 节点拖入画布。
|
|
266
|
+
2. 选择“数据来源”:
|
|
267
|
+
- 选“共享串口配置(serial-port-config)”以复用主站/从站连接;或
|
|
268
|
+
- 选“Modbus服务器(modbus-server-config)”进行独立直连。
|
|
269
|
+
3. 可选:勾选“大写HEX”、勾选“时间戳”、设置“最大字节数”。
|
|
270
|
+
4. 部署后,节点自动输出捕获到的原始数据。
|
|
271
|
+
|
|
272
|
+
典型用法:
|
|
273
|
+
- 联调TCP转RS485网关时,观察上行/下行原始数据帧是否完整。
|
|
274
|
+
- 排查波特率/数据位/校验位配置是否正确(串口模式)。
|
|
275
|
+
- 与 `modbus-master` 或 `modbus-slave-switch` 同时使用,定位现场设备通信异常。
|
|
276
|
+
|
|
249
277
|
### MQTT自动发现
|
|
250
278
|
|
|
251
279
|
启用MQTT后,自动生成Home Assistant兼容的Discovery配置:
|
|
@@ -479,7 +507,7 @@ msg.payload = 1; // 或 0
|
|
|
479
507
|
|
|
480
508
|
## 项目信息
|
|
481
509
|
|
|
482
|
-
**版本**: v2.6.
|
|
510
|
+
**版本**: v2.6.6
|
|
483
511
|
|
|
484
512
|
**核心功能**:
|
|
485
513
|
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
@@ -499,24 +527,34 @@ msg.payload = 1; // 或 0
|
|
|
499
527
|
- Node.js: >=14.0.0
|
|
500
528
|
- Node-RED: >=2.0.0
|
|
501
529
|
|
|
502
|
-
**最新更新(v2.6.
|
|
530
|
+
**最新更新(v2.6.6)**:
|
|
531
|
+
- **🔥 彻底解决MQTT日志刷屏问题**:
|
|
532
|
+
- 局域网IP检测优化:配置192.168.x.x等IP后不再尝试fallback地址
|
|
533
|
+
- 高频MQTT日志改为debug级别:broker候选、认证、重连等
|
|
534
|
+
- 默认不输出到日志文件和调试窗口
|
|
535
|
+
- 配置局域网IP后立即连接,不产生多余日志
|
|
536
|
+
- **日志输出优化**:
|
|
537
|
+
- 连接成功/失败:仍使用log(重要信息)
|
|
538
|
+
- 重连尝试、fallback地址、认证信息:改为debug(调试信息)
|
|
539
|
+
- 仅启用debug模式时才在调试窗口显示
|
|
540
|
+
- 彻底解决日志刷屏和硬盘占用问题
|
|
541
|
+
|
|
542
|
+
**v2.6.5更新**:
|
|
543
|
+
- **🔥 修复MQTT报错问题**:从站开关节点新增"启用MQTT"勾选框
|
|
544
|
+
- 默认不启用MQTT,不会尝试连接
|
|
545
|
+
- 本地模式和MQTT模式自由切换
|
|
546
|
+
|
|
547
|
+
**v2.6.4更新**:
|
|
503
548
|
- **🔥 日志优化**:大幅减少日志输出,保证长期稳定运行
|
|
504
|
-
- 高频操作日志改为debug
|
|
505
|
-
-
|
|
506
|
-
-
|
|
507
|
-
- 完善的内存清理机制,防止内存泄漏
|
|
508
|
-
- **适合工控机长期运行**:
|
|
509
|
-
- 无debug节点时不产生日志
|
|
510
|
-
- 不会因日志增加硬盘占用
|
|
511
|
-
- 不会因日志增加内存占用
|
|
512
|
-
- 断网/MQTT断开也不会持续报错
|
|
549
|
+
- 高频操作日志改为debug级别
|
|
550
|
+
- 默认不输出到日志文件
|
|
551
|
+
- 完善的内存清理机制
|
|
513
552
|
|
|
514
553
|
**v2.6.3更新**:
|
|
515
554
|
- **🔥 MQTT可选配置**:完全兼容无MQTT环境
|
|
516
|
-
-
|
|
517
|
-
- MQTT模式:可选接入
|
|
518
|
-
-
|
|
519
|
-
- 状态显示:清晰区分当前运行模式
|
|
555
|
+
- 本地模式:纯串口通信
|
|
556
|
+
- MQTT模式:可选接入HA
|
|
557
|
+
- 智能切换和状态显示
|
|
520
558
|
|
|
521
559
|
**性能优化**:
|
|
522
560
|
- 轮询间隔优化:修复间隔计算逻辑,确保每个从站使用正确的轮询间隔
|
|
@@ -535,3 +573,284 @@ msg.payload = 1; // 或 0
|
|
|
535
573
|
**支持**:
|
|
536
574
|
- Issues: https://github.com/symi-daguo/node-red-contrib-symi-modbus/issues
|
|
537
575
|
- NPM: https://www.npmjs.com/package/node-red-contrib-symi-modbus
|
|
576
|
+
|
|
577
|
+
### 节点与分类(Palette)
|
|
578
|
+
|
|
579
|
+
- 侧边栏分类名:`SYMI-MODBUS`
|
|
580
|
+
- 包含节点:`modbus-master`(主站)、`modbus-slave-switch`(从站开关)、`modbus-debug`(调试)
|
|
581
|
+
- 如果未显示该分类或节点:
|
|
582
|
+
- 刷新浏览器缓存(Shift+刷新)
|
|
583
|
+
- 重启 Node-RED(如:`node-red-restart` 或系统服务方式)
|
|
584
|
+
- 在“节点管理(Manage Palette)”确认安装版本为 `v2.6.6`
|
|
585
|
+
|
|
586
|
+
### 调试节点(modbus-debug)使用要点
|
|
587
|
+
|
|
588
|
+
- 数据来源选择:`sourceType = serial`(共享串口)或 `modbus`(独立服务器)
|
|
589
|
+
- 共享串口:需要选择并关联一个 `serial-port-config` 配置节点
|
|
590
|
+
- 独立服务器:需要选择并关联一个 `modbus-server-config` 配置节点
|
|
591
|
+
- HEX显示:可选大写、可选时间戳、`maxBytes` 控制显示长度
|
|
592
|
+
- 输出:`msg.payload`(格式化HEX)、`msg.buffer`(原始Buffer)、`msg.meta`(来源信息)
|
|
593
|
+
|
|
594
|
+
### v2.6.6 重要更新
|
|
595
|
+
|
|
596
|
+
- 调试节点配置验证修复:根据所选数据来源类型(`serial`/`modbus`)动态校验,避免误报为“配置不正确”
|
|
597
|
+
- 统一侧边栏分类:所有节点统一归类到 `SYMI-MODBUS`
|
|
598
|
+
- 文档更新:补充节点分类、调试节点要点与常见问题处理
|
|
599
|
+
|
|
600
|
+
典型用法:
|
|
601
|
+
- 联调TCP转RS485网关时,观察上行/下行原始数据帧是否完整。
|
|
602
|
+
- 排查波特率/数据位/校验位配置是否正确(串口模式)。
|
|
603
|
+
- 与 `modbus-master` 或 `modbus-slave-switch` 同时使用,定位现场设备通信异常。
|
|
604
|
+
|
|
605
|
+
### MQTT自动发现
|
|
606
|
+
|
|
607
|
+
启用MQTT后,自动生成Home Assistant兼容的Discovery配置:
|
|
608
|
+
- **唯一性保证**:每个实体使用稳定的`unique_id`,避免重复生成
|
|
609
|
+
- **设备分组**:同一从站的所有继电器自动分组到一个设备下
|
|
610
|
+
- **状态持久化**:使用`retain=true`确保状态持久化
|
|
611
|
+
- **在线状态**:自动发布设备可用性状态
|
|
612
|
+
|
|
613
|
+
### 配置持久化
|
|
614
|
+
|
|
615
|
+
所有节点配置自动保存到Node-RED的flows文件中:
|
|
616
|
+
- 从站地址、线圈范围、轮询间隔
|
|
617
|
+
- MQTT服务器配置
|
|
618
|
+
- 开关面板映射关系
|
|
619
|
+
|
|
620
|
+
部署后配置永久生效,重启Node-RED后自动恢复。
|
|
621
|
+
|
|
622
|
+
### 长期稳定运行
|
|
623
|
+
|
|
624
|
+
针对工控机24/7长期运行优化:
|
|
625
|
+
- **内存管理**:自动清理缓存,释放无用对象
|
|
626
|
+
- **事件监听器清理**:关闭时移除所有监听器,防止内存泄漏
|
|
627
|
+
- **智能日志限流**:错误日志10分钟输出一次,避免日志刷屏
|
|
628
|
+
- **智能重连机制**:
|
|
629
|
+
- Modbus连接断开自动重连(指数退避:5秒→10秒→20秒...最大60秒)
|
|
630
|
+
- MQTT连接断开自动重连(支持多地址fallback)
|
|
631
|
+
- 串口拔插自动检测并重连
|
|
632
|
+
- TCP网络故障自动恢复
|
|
633
|
+
- 连接前彻底清理旧实例,避免资源泄漏
|
|
634
|
+
- **互斥锁机制**:防止读写冲突导致的数据异常
|
|
635
|
+
- **Keep-Alive心跳**:TCP连接启用30秒心跳检测
|
|
636
|
+
|
|
637
|
+
## 技术规格
|
|
638
|
+
|
|
639
|
+
### Modbus协议
|
|
640
|
+
|
|
641
|
+
- **协议类型**:Modbus TCP / Modbus RTU
|
|
642
|
+
- **底层库**:modbus-serial ^8.0.23(内部封装serialport,支持TCP和串口)
|
|
643
|
+
- **功能码支持**:0x01(读线圈)、0x05(写单个线圈)、0x0F(写多个线圈)
|
|
644
|
+
- **从站地址范围**:1-247(建议从10开始)
|
|
645
|
+
- **线圈数量**:每台设备32个(0-31)
|
|
646
|
+
- **最大设备数**:10台同时轮询
|
|
647
|
+
- **轮询间隔**:默认200ms(建议300-500ms,支持100-10000ms)
|
|
648
|
+
- **串口配置**:9600 8-N-1(波特率9600,8数据位,无校验,1停止位)
|
|
649
|
+
- **超时设置**:5000ms(TCP和串口通用)
|
|
650
|
+
|
|
651
|
+
### 兼容性
|
|
652
|
+
|
|
653
|
+
- **Node.js**: >= 14.0.0
|
|
654
|
+
- **Node-RED**: >= 2.0.0
|
|
655
|
+
- **MQTT Broker**: Mosquitto / EMQX / Any MQTT 3.1.1/5.0
|
|
656
|
+
- **Home Assistant**: 2024.x+(MQTT Discovery标准)
|
|
657
|
+
- **操作系统**: Windows / Linux / macOS / HassOS
|
|
658
|
+
|
|
659
|
+
## Home Assistant集成
|
|
660
|
+
|
|
661
|
+
### 自动发现
|
|
662
|
+
|
|
663
|
+
启用MQTT后,设备自动出现在Home Assistant中:
|
|
664
|
+
- 实体ID: `switch.relay_{从站地址}_{线圈编号}`
|
|
665
|
+
- 设备名称: `Modbus继电器-{从站地址}`
|
|
666
|
+
- 自动分组: 同一从站的所有继电器分组到一个设备
|
|
667
|
+
|
|
668
|
+
### MQTT主题结构
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
状态主题: modbus/relay/{从站}/{线圈}/state
|
|
672
|
+
命令主题: modbus/relay/{从站}/{线圈}/set
|
|
673
|
+
可用性主题: modbus/relay/{从站}/availability
|
|
674
|
+
发现主题: homeassistant/switch/modbus_relay_{从站}_{线圈}/config
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## 故障排除
|
|
678
|
+
|
|
679
|
+
### 串口连接失败
|
|
680
|
+
|
|
681
|
+
**Linux**:
|
|
682
|
+
```bash
|
|
683
|
+
# 查看串口设备
|
|
684
|
+
ls -l /dev/ttyUSB* /dev/ttyS*
|
|
685
|
+
|
|
686
|
+
# 添加用户到dialout组(需要重新登录)
|
|
687
|
+
sudo usermod -a -G dialout $USER
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**macOS**:
|
|
691
|
+
```bash
|
|
692
|
+
# 查看串口设备(注意macOS使用cu.*而不是tty.*)
|
|
693
|
+
ls -l /dev/cu.*
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Docker/HassOS**:
|
|
697
|
+
```yaml
|
|
698
|
+
# 在docker-compose.yml或HassOS插件配置中添加设备映射
|
|
699
|
+
devices:
|
|
700
|
+
- /dev/ttyUSB0:/dev/ttyUSB0
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### MQTT连接失败
|
|
704
|
+
|
|
705
|
+
1. 确认MQTT broker正在运行:
|
|
706
|
+
```bash
|
|
707
|
+
# Linux
|
|
708
|
+
sudo systemctl status mosquitto
|
|
709
|
+
|
|
710
|
+
# macOS
|
|
711
|
+
brew services list | grep mosquitto
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
2. 测试MQTT连接:
|
|
715
|
+
```bash
|
|
716
|
+
mosquitto_sub -h localhost -t test
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
3. 检查Node-RED日志中的MQTT连接信息
|
|
720
|
+
|
|
721
|
+
### 主站轮询不工作
|
|
722
|
+
|
|
723
|
+
1. **检查从站配置**:确认已添加所有从站设备(如10、11、12、13)
|
|
724
|
+
2. **检查轮询间隔**:默认200ms,建议300-500ms(多台从站时避免总线拥堵)
|
|
725
|
+
3. **查看Node-RED调试日志**:部署后查看日志中的轮询信息
|
|
726
|
+
4. **检查串口波特率**:确认波特率为9600(与从站设备一致)
|
|
727
|
+
5. **检查从站地址**:确认从站地址正确(1-247)
|
|
728
|
+
6. **确认从站设备在线**:使用Modbus调试工具测试从站是否响应
|
|
729
|
+
7. **检查MQTT连接**:确保MQTT broker地址正确,轮询不依赖MQTT但状态发布需要MQTT
|
|
730
|
+
8. **测试连接**:
|
|
731
|
+
- TCP连接问题:先用 `modbus-serial` 单独测试TCP连接
|
|
732
|
+
- 串口问题:先用 `serialport` 单独测试串口通信
|
|
733
|
+
|
|
734
|
+
### 从站开关无响应
|
|
735
|
+
|
|
736
|
+
1. 检查RS-485连接是否正常
|
|
737
|
+
2. 确认开关面板地址和按钮编号正确
|
|
738
|
+
3. 检查MQTT连接状态
|
|
739
|
+
4. 查看Node-RED日志中的协议解析信息
|
|
740
|
+
|
|
741
|
+
## 输入消息格式
|
|
742
|
+
|
|
743
|
+
### 主站节点
|
|
744
|
+
|
|
745
|
+
```javascript
|
|
746
|
+
// 启动轮询
|
|
747
|
+
msg.payload = {cmd: "start"};
|
|
748
|
+
|
|
749
|
+
// 停止轮询
|
|
750
|
+
msg.payload = {cmd: "stop"};
|
|
751
|
+
|
|
752
|
+
// 写单个线圈
|
|
753
|
+
msg.payload = {
|
|
754
|
+
cmd: "writeCoil",
|
|
755
|
+
slave: 10, // 从站地址
|
|
756
|
+
coil: 0, // 线圈编号
|
|
757
|
+
value: true // true=开, false=关
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// 批量写多个线圈
|
|
761
|
+
msg.payload = {
|
|
762
|
+
cmd: "writeCoils",
|
|
763
|
+
slave: 10, // 从站地址
|
|
764
|
+
startCoil: 0, // 起始线圈
|
|
765
|
+
values: [true, false, true, false] // 线圈值数组
|
|
766
|
+
};
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### 从站开关节点
|
|
770
|
+
|
|
771
|
+
```javascript
|
|
772
|
+
// 发送开关命令
|
|
773
|
+
msg.payload = true; // 或 false
|
|
774
|
+
msg.payload = "ON"; // 或 "OFF"
|
|
775
|
+
msg.payload = 1; // 或 0
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## 输出消息格式
|
|
779
|
+
|
|
780
|
+
### 主站节点
|
|
781
|
+
|
|
782
|
+
```javascript
|
|
783
|
+
{
|
|
784
|
+
payload: {
|
|
785
|
+
slave: 10, // 从站地址
|
|
786
|
+
coils: [true, false, ...], // 线圈状态数组
|
|
787
|
+
timestamp: 1234567890 // 时间戳
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### 从站开关节点
|
|
793
|
+
|
|
794
|
+
```javascript
|
|
795
|
+
{
|
|
796
|
+
payload: true, // 开关状态
|
|
797
|
+
topic: "switch_0_btn1", // 主题
|
|
798
|
+
switchId: 0, // 开关面板ID
|
|
799
|
+
button: 1, // 按钮编号
|
|
800
|
+
targetSlave: 10, // 目标从站地址
|
|
801
|
+
targetCoil: 0 // 目标线圈编号
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
## 性能指标
|
|
806
|
+
|
|
807
|
+
- **内存占用**:< 50MB(单个主站节点,轮询10个设备)
|
|
808
|
+
- **CPU占用**:< 5%(正常轮询状态)
|
|
809
|
+
- **连接延迟**:Modbus响应 < 100ms,MQTT发布 < 50ms
|
|
810
|
+
- **稳定运行**:经过工控机7x24小时长期运行验证
|
|
811
|
+
- **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
|
|
812
|
+
|
|
813
|
+
## 示例Flow
|
|
814
|
+
|
|
815
|
+
```json
|
|
816
|
+
[
|
|
817
|
+
{
|
|
818
|
+
"id": "modbus-master-1",
|
|
819
|
+
"type": "modbus-master",
|
|
820
|
+
"name": "主站",
|
|
821
|
+
"connectionType": "serial",
|
|
822
|
+
"serialPort": "/dev/ttyUSB0",
|
|
823
|
+
"serialBaudRate": 9600,
|
|
824
|
+
"slaves": [
|
|
825
|
+
{"address": 10, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
826
|
+
{"address": 11, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
827
|
+
{"address": 12, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
828
|
+
{"address": 13, "coilStart": 0, "coilEnd": 31, "pollInterval": 200}
|
|
829
|
+
],
|
|
830
|
+
"enableMqtt": true,
|
|
831
|
+
"mqttServer": "mqtt-config-1"
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## 项目信息
|
|
837
|
+
|
|
838
|
+
**版本**: v2.6.6
|
|
839
|
+
|
|
840
|
+
**核心功能**:
|
|
841
|
+
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
842
|
+
- 多设备轮询(最多10台从站,每台32路继电器,轮询间隔100-10000ms可调)
|
|
843
|
+
- Symi私有协议自动识别(支持两种485开关控制方式)
|
|
844
|
+
- 智能轮询暂停机制(从站上报时自动暂停,处理完成后恢复)
|
|
845
|
+
- 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
|
|
846
|
+
- MQTT集成(可选启用,Home Assistant自动发现,实体唯一性保证,QoS=0高性能发布)
|
|
847
|
+
- 物理开关面板双向同步(亖米协议支持,LED反馈同步)
|
|
848
|
+
- 共享连接架构(多个从站开关节点共享同一个串口/TCP连接,支持500+节点)
|
|
849
|
+
- 长期稳定运行(内存管理、智能重连、错误日志限流、异步MQTT发布)
|
|
850
|
+
|
|
851
|
+
**技术栈**:
|
|
852
|
+
- modbus-serial: ^8.0.23(内部封装serialport,支持TCP和串口)
|
|
853
|
+
- serialport: ^12.0.0(原生串口通信)
|
|
854
|
+
- mqtt: ^5.14.1(最新稳定版,可选依赖)
|
|
855
|
+
- Node.js: >=14.0.0
|
|
856
|
+
- Node-RED: >=2.0.0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('modbus-debug', {
|
|
3
|
+
category: 'SYMI-MODBUS',
|
|
4
|
+
color: '#C0DEED',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
sourceType: { value: 'serial' }, // serial | modbus
|
|
8
|
+
serialPortConfig: {
|
|
9
|
+
value: '',
|
|
10
|
+
type: 'serial-port-config',
|
|
11
|
+
required: false,
|
|
12
|
+
validate: function(v) {
|
|
13
|
+
var t = $("#node-input-sourceType").val() || this.sourceType || 'serial';
|
|
14
|
+
if (t === 'serial') return !!v; // 串口模式需要选择配置
|
|
15
|
+
return true; // 非串口模式不要求
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
modbusServer: {
|
|
19
|
+
value: '',
|
|
20
|
+
type: 'modbus-server-config',
|
|
21
|
+
required: false,
|
|
22
|
+
validate: function(v) {
|
|
23
|
+
var t = $("#node-input-sourceType").val() || this.sourceType || 'serial';
|
|
24
|
+
if (t === 'modbus') return !!v; // 独立Modbus服务器模式需要选择配置
|
|
25
|
+
return true; // 非modbus模式不要求
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
uppercase: { value: true },
|
|
29
|
+
includeTimestamp: { value: true },
|
|
30
|
+
maxBytes: { value: 0 }
|
|
31
|
+
},
|
|
32
|
+
inputs: 1,
|
|
33
|
+
outputs: 1,
|
|
34
|
+
icon: 'font-awesome/fa-eye',
|
|
35
|
+
label: function() { return this.name || 'modbus-debug'; },
|
|
36
|
+
oneditprepare: function() {
|
|
37
|
+
function toggleSources() {
|
|
38
|
+
var type = $("#node-input-sourceType").val();
|
|
39
|
+
if (type === 'serial') {
|
|
40
|
+
$("#serial-select-wrap").show();
|
|
41
|
+
$("#modbus-select-wrap").hide();
|
|
42
|
+
} else {
|
|
43
|
+
$("#serial-select-wrap").hide();
|
|
44
|
+
$("#modbus-select-wrap").show();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
$("#node-input-sourceType").on('change', toggleSources);
|
|
48
|
+
toggleSources();
|
|
49
|
+
},
|
|
50
|
+
oneditsave: function() {}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<script type="text/x-red" data-template-name="modbus-debug">
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
57
|
+
<input type="text" id="node-input-name" placeholder="原始485调试">
|
|
58
|
+
</div>
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-sourceType"><i class="fa fa-exchange"></i> 数据来源</label>
|
|
61
|
+
<select id="node-input-sourceType">
|
|
62
|
+
<option value="serial">共享串口配置 (serial-port-config)</option>
|
|
63
|
+
<option value="modbus">Modbus服务器配置 (modbus-server-config)</option>
|
|
64
|
+
</select>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-row" id="serial-select-wrap">
|
|
67
|
+
<label for="node-input-serialPortConfig"><i class="fa fa-plug"></i> 串口配置</label>
|
|
68
|
+
<input type="text" id="node-input-serialPortConfig">
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-row" id="modbus-select-wrap" style="display:none">
|
|
71
|
+
<label for="node-input-modbusServer"><i class="fa fa-server"></i> Modbus服务器</label>
|
|
72
|
+
<input type="text" id="node-input-modbusServer">
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-input-uppercase"><i class="fa fa-text-height"></i> 大写HEX</label>
|
|
76
|
+
<input type="checkbox" id="node-input-uppercase" style="width:auto" checked>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="form-row">
|
|
79
|
+
<label for="node-input-includeTimestamp"><i class="fa fa-clock-o"></i> 包含时间戳</label>
|
|
80
|
+
<input type="checkbox" id="node-input-includeTimestamp" style="width:auto" checked>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-row">
|
|
83
|
+
<label for="node-input-maxBytes"><i class="fa fa-filter"></i> 显示最大字节数 (0为不限制)</label>
|
|
84
|
+
<input type="number" id="node-input-maxBytes" min="0" step="1" placeholder="0">
|
|
85
|
+
</div>
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<script type="text/x-red" data-help-name="modbus-debug">
|
|
89
|
+
<p>modbus-debug 节点用于抓取并显示原始485字节流,以十六进制格式输出,帮助调试串口或TCP网关下的Modbus通信。</p>
|
|
90
|
+
<ul>
|
|
91
|
+
<li><b>数据来源</b>:选择共享的 <code>serial-port-config</code>(推荐,复用主站/从站的同一连接),或选择 <code>modbus-server-config</code>(独立直连TCP/串口)。</li>
|
|
92
|
+
<li><b>输出</b>:<code>msg.payload</code> 为格式化HEX字符串;<code>msg.buffer</code>为原始Buffer;<code>msg.meta</code>包含来源与连接信息;若开启则包含 <code>msg.timestamp</code>。</li>
|
|
93
|
+
<li><b>显示最大字节数</b>:为避免控制台过载,可设置最大显示长度,超过则截断并在 meta.truncated 标记。</li>
|
|
94
|
+
</ul>
|
|
95
|
+
</script>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const net = require("net");
|
|
5
|
+
let SerialPort;
|
|
6
|
+
try { SerialPort = require("serialport").SerialPort || require("serialport"); } catch (e) { SerialPort = null; }
|
|
7
|
+
|
|
8
|
+
function bufferToHex(buffer, uppercase) {
|
|
9
|
+
const hex = buffer.toString("hex");
|
|
10
|
+
const grouped = hex.match(/.{1,2}/g) || [];
|
|
11
|
+
const str = grouped.join(" ");
|
|
12
|
+
return uppercase ? str.toUpperCase() : str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ModbusDebugNode(config) {
|
|
16
|
+
RED.nodes.createNode(this, config);
|
|
17
|
+
const node = this;
|
|
18
|
+
|
|
19
|
+
node.name = config.name || "原始485调试";
|
|
20
|
+
node.sourceType = config.sourceType || "serial"; // serial | modbus
|
|
21
|
+
node.serialPortConfig = RED.nodes.getNode(config.serialPortConfig);
|
|
22
|
+
node.modbusServer = RED.nodes.getNode(config.modbusServer);
|
|
23
|
+
node.uppercase = config.uppercase !== false; // 默认大写显示
|
|
24
|
+
node.includeTimestamp = config.includeTimestamp !== false; // 默认包含时间戳
|
|
25
|
+
node.maxBytes = parseInt(config.maxBytes) || 0; // 0 表示不截断
|
|
26
|
+
|
|
27
|
+
// 本地独立连接(当来源选择 modbus-server-config 时使用)
|
|
28
|
+
node.localConnection = null;
|
|
29
|
+
node.localConnType = null; // tcp | serial
|
|
30
|
+
|
|
31
|
+
const sendHexMsg = (data) => {
|
|
32
|
+
if (!data || !Buffer.isBuffer(data) || data.length === 0) return;
|
|
33
|
+
|
|
34
|
+
let buf = data;
|
|
35
|
+
if (node.maxBytes > 0 && buf.length > node.maxBytes) {
|
|
36
|
+
buf = buf.subarray(0, node.maxBytes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hex = bufferToHex(buf, node.uppercase);
|
|
40
|
+
const meta = {
|
|
41
|
+
length: data.length,
|
|
42
|
+
displayedLength: buf.length,
|
|
43
|
+
truncated: node.maxBytes > 0 && data.length > node.maxBytes,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 来源信息
|
|
47
|
+
if (node.sourceType === "serial" && node.serialPortConfig) {
|
|
48
|
+
meta.source = "serial-port-config";
|
|
49
|
+
meta.connectionType = node.serialPortConfig.connectionType;
|
|
50
|
+
if (node.serialPortConfig.connectionType === "tcp") {
|
|
51
|
+
meta.tcpHost = node.serialPortConfig.tcpHost;
|
|
52
|
+
meta.tcpPort = node.serialPortConfig.tcpPort;
|
|
53
|
+
} else {
|
|
54
|
+
meta.serialPort = node.serialPortConfig.serialPort;
|
|
55
|
+
meta.baudRate = node.serialPortConfig.baudRate;
|
|
56
|
+
meta.dataBits = node.serialPortConfig.dataBits;
|
|
57
|
+
meta.stopBits = node.serialPortConfig.stopBits;
|
|
58
|
+
meta.parity = node.serialPortConfig.parity;
|
|
59
|
+
}
|
|
60
|
+
} else if (node.sourceType === "modbus" && node.modbusServer) {
|
|
61
|
+
meta.source = "modbus-server-config";
|
|
62
|
+
meta.connectionType = node.modbusServer.connectionType;
|
|
63
|
+
if (node.modbusServer.connectionType === "tcp") {
|
|
64
|
+
meta.tcpHost = node.modbusServer.tcpHost;
|
|
65
|
+
meta.tcpPort = node.modbusServer.tcpPort;
|
|
66
|
+
} else {
|
|
67
|
+
meta.serialPort = node.modbusServer.serialPort;
|
|
68
|
+
meta.baudRate = node.modbusServer.serialBaudRate;
|
|
69
|
+
meta.dataBits = node.modbusServer.serialDataBits;
|
|
70
|
+
meta.stopBits = node.modbusServer.serialStopBits;
|
|
71
|
+
meta.parity = node.modbusServer.serialParity;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const msg = { payload: hex, buffer: data, meta };
|
|
76
|
+
if (node.includeTimestamp) msg.timestamp = Date.now();
|
|
77
|
+
|
|
78
|
+
node.send(msg);
|
|
79
|
+
node.status({ fill: "green", shape: "dot", text: `RX ${data.length}B` });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// 选择来源:共享串口配置 或 独立连接到 Modbus 服务器配置
|
|
83
|
+
const startListening = () => {
|
|
84
|
+
if (node.sourceType === "serial") {
|
|
85
|
+
if (!node.serialPortConfig) {
|
|
86
|
+
node.status({ fill: "red", shape: "ring", text: "未选择串口配置" });
|
|
87
|
+
node.error("未配置RS-485连接,请在节点中选择 serial-port-config 配置节点");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// 使用共享连接
|
|
91
|
+
try {
|
|
92
|
+
node.serialPortConfig.registerDataListener(sendHexMsg);
|
|
93
|
+
const desc = node.serialPortConfig.connectionType === "tcp"
|
|
94
|
+
? `TCP ${node.serialPortConfig.tcpHost}:${node.serialPortConfig.tcpPort}`
|
|
95
|
+
: `串口 ${node.serialPortConfig.serialPort} @ ${node.serialPortConfig.baudRate}bps`;
|
|
96
|
+
node.log(`modbus-debug 监听共享连接:${desc}`);
|
|
97
|
+
node.status({ fill: "blue", shape: "ring", text: "监听中" });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
node.error(`注册数据监听器失败: ${err.message}`);
|
|
100
|
+
node.status({ fill: "red", shape: "ring", text: "监听失败" });
|
|
101
|
+
}
|
|
102
|
+
} else if (node.sourceType === "modbus") {
|
|
103
|
+
if (!node.modbusServer) {
|
|
104
|
+
node.status({ fill: "red", shape: "ring", text: "未选择Modbus服务器" });
|
|
105
|
+
node.error("未配置Modbus服务器,请在节点中选择 modbus-server-config 配置节点");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 独立建立原始连接以抓取数据
|
|
110
|
+
if (node.modbusServer.connectionType === "tcp") {
|
|
111
|
+
try {
|
|
112
|
+
node.localConnType = "tcp";
|
|
113
|
+
node.localConnection = new net.Socket();
|
|
114
|
+
node.localConnection.setKeepAlive(true, 30000);
|
|
115
|
+
node.localConnection.connect(node.modbusServer.tcpPort, node.modbusServer.tcpHost, () => {
|
|
116
|
+
node.log(`modbus-debug 已连接TCP:${node.modbusServer.tcpHost}:${node.modbusServer.tcpPort}`);
|
|
117
|
+
node.status({ fill: "blue", shape: "dot", text: "TCP监听中" });
|
|
118
|
+
});
|
|
119
|
+
node.localConnection.on("data", sendHexMsg);
|
|
120
|
+
node.localConnection.on("error", (err) => {
|
|
121
|
+
node.warn(`TCP监听错误: ${err.message}`);
|
|
122
|
+
node.status({ fill: "red", shape: "ring", text: "TCP错误" });
|
|
123
|
+
});
|
|
124
|
+
node.localConnection.on("close", () => {
|
|
125
|
+
node.status({ fill: "grey", shape: "ring", text: "TCP已关闭" });
|
|
126
|
+
});
|
|
127
|
+
} catch (e) {
|
|
128
|
+
node.error(`TCP初始化失败: ${e.message}`);
|
|
129
|
+
node.status({ fill: "red", shape: "ring", text: "TCP初始化失败" });
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// 串口
|
|
133
|
+
if (!SerialPort) {
|
|
134
|
+
node.error("未找到 serialport 模块,无法打开串口");
|
|
135
|
+
node.status({ fill: "red", shape: "ring", text: "serialport缺失" });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
node.localConnType = "serial";
|
|
140
|
+
node.localConnection = new SerialPort({
|
|
141
|
+
path: node.modbusServer.serialPort,
|
|
142
|
+
baudRate: node.modbusServer.serialBaudRate || 9600,
|
|
143
|
+
dataBits: node.modbusServer.serialDataBits || 8,
|
|
144
|
+
stopBits: node.modbusServer.serialStopBits || 1,
|
|
145
|
+
parity: node.modbusServer.serialParity || "none",
|
|
146
|
+
autoOpen: true,
|
|
147
|
+
});
|
|
148
|
+
node.localConnection.on("open", () => {
|
|
149
|
+
node.log(`modbus-debug 已打开串口:${node.modbusServer.serialPort}`);
|
|
150
|
+
node.status({ fill: "blue", shape: "dot", text: "串口监听中" });
|
|
151
|
+
});
|
|
152
|
+
node.localConnection.on("data", sendHexMsg);
|
|
153
|
+
node.localConnection.on("error", (err) => {
|
|
154
|
+
node.warn(`串口监听错误: ${err.message}`);
|
|
155
|
+
node.status({ fill: "red", shape: "ring", text: "串口错误" });
|
|
156
|
+
});
|
|
157
|
+
node.localConnection.on("close", () => {
|
|
158
|
+
node.status({ fill: "grey", shape: "ring", text: "串口已关闭" });
|
|
159
|
+
});
|
|
160
|
+
} catch (e) {
|
|
161
|
+
node.error(`串口初始化失败: ${e.message}`);
|
|
162
|
+
node.status({ fill: "red", shape: "ring", text: "串口初始化失败" });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
startListening();
|
|
169
|
+
|
|
170
|
+
// 输入不做处理(预留写入功能),当前为纯监听输出节点
|
|
171
|
+
node.on("input", function(msg, send, done) { done(); });
|
|
172
|
+
|
|
173
|
+
// 关闭时清理
|
|
174
|
+
node.on("close", function(done) {
|
|
175
|
+
try {
|
|
176
|
+
if (node.sourceType === "serial" && node.serialPortConfig && typeof node.serialPortConfig.unregisterDataListener === "function") {
|
|
177
|
+
node.serialPortConfig.unregisterDataListener(sendHexMsg);
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
node.warn(`注销监听器时出错: ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
if (node.localConnection) {
|
|
184
|
+
if (node.localConnType === "tcp") {
|
|
185
|
+
try { node.localConnection.destroy(); } catch (e) { /* ignore */ }
|
|
186
|
+
} else if (node.localConnType === "serial") {
|
|
187
|
+
try { node.localConnection.close(); } catch (e) { /* ignore */ }
|
|
188
|
+
}
|
|
189
|
+
node.localConnection = null;
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {
|
|
192
|
+
node.warn(`关闭本地连接时出错: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
done();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
RED.nodes.registerType("modbus-debug", ModbusDebugNode);
|
|
199
|
+
};
|
package/nodes/modbus-master.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('modbus-master', {
|
|
3
|
-
category: '
|
|
3
|
+
category: 'SYMI-MODBUS',
|
|
4
4
|
color: '#3FADB5',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value: "Modbus主站"},
|
|
@@ -35,8 +35,6 @@
|
|
|
35
35
|
}];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// 移除旧的连接类型切换逻辑(现在使用服务器配置节点)
|
|
39
|
-
|
|
40
38
|
// 渲染从站列表
|
|
41
39
|
function renderSlaveList() {
|
|
42
40
|
var container = $("#slave-list-container");
|
|
@@ -79,7 +77,6 @@
|
|
|
79
77
|
</div>
|
|
80
78
|
`);
|
|
81
79
|
|
|
82
|
-
// 添加hover效果
|
|
83
80
|
slaveRow.hover(
|
|
84
81
|
function() { $(this).css("box-shadow", "0 4px 12px rgba(0,0,0,0.15)"); },
|
|
85
82
|
function() { $(this).css("box-shadow", "0 2px 6px rgba(0,0,0,0.08)"); }
|
|
@@ -88,33 +85,28 @@
|
|
|
88
85
|
container.append(slaveRow);
|
|
89
86
|
});
|
|
90
87
|
|
|
91
|
-
// 更新删除按钮状态(至少保留1个从站)
|
|
92
88
|
if (node.slaves.length === 1) {
|
|
93
89
|
$(".btn-delete-slave").prop("disabled", true).css("opacity", "0.4").css("cursor", "not-allowed");
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
// 更新添加按钮状态(最多10个从站)
|
|
97
92
|
if (node.slaves.length >= 10) {
|
|
98
93
|
$("#btn-add-slave").prop("disabled", true).css("opacity", "0.5").css("cursor", "not-allowed");
|
|
99
94
|
} else {
|
|
100
95
|
$("#btn-add-slave").prop("disabled", false).css("opacity", "1").css("cursor", "pointer");
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
// 添加按钮hover效果
|
|
104
98
|
$("#btn-add-slave").hover(
|
|
105
99
|
function() { if (!$(this).prop("disabled")) $(this).css("background", "linear-gradient(135deg, #66bb6a 0%, #5cb85c 100%)"); },
|
|
106
100
|
function() { if (!$(this).prop("disabled")) $(this).css("background", "linear-gradient(135deg, #5cb85c 0%, #4cae4c 100%)"); }
|
|
107
101
|
);
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
// 添加从站
|
|
111
104
|
$("#btn-add-slave").on("click", function() {
|
|
112
105
|
if (node.slaves.length >= 10) {
|
|
113
106
|
RED.notify("最多只能添加10个从站", "warning");
|
|
114
107
|
return;
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
// 计算下一个从站地址(递增)
|
|
118
110
|
var lastAddress = node.slaves[node.slaves.length - 1].address;
|
|
119
111
|
var nextAddress = lastAddress + 1;
|
|
120
112
|
if (nextAddress > 247) nextAddress = 1;
|
|
@@ -129,7 +121,6 @@
|
|
|
129
121
|
renderSlaveList();
|
|
130
122
|
});
|
|
131
123
|
|
|
132
|
-
// 删除从站
|
|
133
124
|
$(document).on("click", ".btn-delete-slave", function() {
|
|
134
125
|
var index = parseInt($(this).data("index"));
|
|
135
126
|
if (node.slaves.length > 1) {
|
|
@@ -140,7 +131,6 @@
|
|
|
140
131
|
}
|
|
141
132
|
});
|
|
142
133
|
|
|
143
|
-
// 更新从站数据
|
|
144
134
|
$(document).on("change", ".slave-address, .slave-coil-start, .slave-coil-end, .slave-poll-interval", function() {
|
|
145
135
|
var index = parseInt($(this).data("index"));
|
|
146
136
|
var className = $(this).attr("class").split(" ")[0];
|
|
@@ -162,10 +152,8 @@
|
|
|
162
152
|
}
|
|
163
153
|
});
|
|
164
154
|
|
|
165
|
-
// 初始化渲染
|
|
166
155
|
renderSlaveList();
|
|
167
156
|
|
|
168
|
-
// MQTT开关
|
|
169
157
|
$("#node-input-enableMqtt").on("change", function() {
|
|
170
158
|
if ($(this).is(":checked")) {
|
|
171
159
|
$(".form-row-mqtt").show();
|
|
@@ -174,24 +162,20 @@
|
|
|
174
162
|
}
|
|
175
163
|
});
|
|
176
164
|
|
|
177
|
-
// 初始化MQTT显示
|
|
178
165
|
$("#node-input-enableMqtt").trigger("change");
|
|
179
166
|
},
|
|
180
167
|
oneditsave: function() {
|
|
181
|
-
// 保存从站配置到节点
|
|
182
168
|
this.slaves = this.slaves || [];
|
|
183
169
|
}
|
|
184
170
|
});
|
|
185
171
|
</script>
|
|
186
172
|
|
|
187
173
|
<script type="text/html" data-template-name="modbus-master">
|
|
188
|
-
<!-- 基本配置 -->
|
|
189
174
|
<div class="form-row">
|
|
190
175
|
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
191
176
|
<input type="text" id="node-input-name" placeholder="Modbus主站">
|
|
192
177
|
</div>
|
|
193
178
|
|
|
194
|
-
<!-- Modbus服务器配置 -->
|
|
195
179
|
<div class="form-row">
|
|
196
180
|
<label for="node-input-modbusServer"><i class="fa fa-server"></i> Modbus服务器</label>
|
|
197
181
|
<input type="text" id="node-input-modbusServer" placeholder="选择或添加Modbus服务器">
|
package/nodes/modbus-master.js
CHANGED
|
@@ -389,8 +389,8 @@ module.exports = function(RED) {
|
|
|
389
389
|
let currentCandidateIndex = 0;
|
|
390
390
|
let lastConnectAttempt = 0;
|
|
391
391
|
|
|
392
|
-
node.
|
|
393
|
-
node.
|
|
392
|
+
node.debug(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
|
|
393
|
+
node.debug(`正在连接MQTT broker: ${brokerCandidates[0]}`);
|
|
394
394
|
|
|
395
395
|
const options = {
|
|
396
396
|
clientId: `modbus_master_${Math.random().toString(16).substring(2, 10)}`,
|
|
@@ -403,7 +403,7 @@ module.exports = function(RED) {
|
|
|
403
403
|
if (node.config.mqttUsername) {
|
|
404
404
|
options.username = node.config.mqttUsername;
|
|
405
405
|
options.password = node.config.mqttPassword;
|
|
406
|
-
node.
|
|
406
|
+
node.debug(`MQTT认证: 用户名=${node.config.mqttUsername}`);
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
// 尝试连接函数
|
|
@@ -427,7 +427,7 @@ module.exports = function(RED) {
|
|
|
427
427
|
|
|
428
428
|
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
429
429
|
if (brokerUrl !== brokerCandidates[0]) {
|
|
430
|
-
node.
|
|
430
|
+
node.debug(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// 异步发送设备发现消息(避免阻塞事件循环)
|
|
@@ -491,7 +491,7 @@ module.exports = function(RED) {
|
|
|
491
491
|
|
|
492
492
|
// 5秒后重试第一个地址
|
|
493
493
|
setTimeout(() => {
|
|
494
|
-
node.
|
|
494
|
+
node.debug('重试连接MQTT broker...');
|
|
495
495
|
tryConnect(brokerCandidates[0]);
|
|
496
496
|
}, 5000);
|
|
497
497
|
} else {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('modbus-slave-switch', {
|
|
3
|
-
category: '
|
|
3
|
+
category: 'SYMI-MODBUS',
|
|
4
4
|
color: '#E9967A',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value: "从站开关"},
|
|
7
7
|
// RS-485连接配置(共享配置节点)
|
|
8
8
|
serialPortConfig: {value: "", type: "serial-port-config", required: true},
|
|
9
9
|
// MQTT配置(可选)
|
|
10
|
+
enableMqtt: {value: false}, // 默认不启用MQTT
|
|
10
11
|
mqttServer: {value: "", type: "mqtt-server-config", required: false},
|
|
11
12
|
// 开关面板配置
|
|
12
13
|
switchBrand: {value: "symi"}, // 品牌选择
|
|
@@ -24,6 +25,19 @@
|
|
|
24
25
|
// 显示时直接使用用户输入的路数(1-32)
|
|
25
26
|
const coilDisplay = this.targetCoilNumber || 1;
|
|
26
27
|
return this.name || `开关${this.switchId}-按钮${this.buttonNumber} → 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
|
|
28
|
+
},
|
|
29
|
+
oneditprepare: function() {
|
|
30
|
+
// MQTT开关控制显示/隐藏
|
|
31
|
+
$("#node-input-enableMqtt").on("change", function() {
|
|
32
|
+
if ($(this).is(":checked")) {
|
|
33
|
+
$(".form-row-mqtt-slave").show();
|
|
34
|
+
} else {
|
|
35
|
+
$(".form-row-mqtt-slave").hide();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 初始化显示状态
|
|
40
|
+
$("#node-input-enableMqtt").trigger("change");
|
|
27
41
|
}
|
|
28
42
|
});
|
|
29
43
|
</script>
|
|
@@ -64,13 +78,19 @@
|
|
|
64
78
|
<span style="margin-left: 8px; padding: 2px 8px; background: #e3f2fd; color: #1976d2; border-radius: 3px; font-size: 11px; font-weight: 500;">可选</span>
|
|
65
79
|
</label>
|
|
66
80
|
<div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%); border-left: 4px solid #2196f3; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
|
67
|
-
<strong>💡 提示:</strong
|
|
81
|
+
<strong>💡 提示:</strong>不启用MQTT则使用<strong>本地模式</strong>(通过连线控制),启用MQTT则使用<strong>MQTT模式</strong>(可接入Home Assistant)
|
|
68
82
|
</div>
|
|
69
83
|
</div>
|
|
70
84
|
|
|
71
85
|
<div class="form-row">
|
|
86
|
+
<label for="node-input-enableMqtt" style="width: 110px;"><i class="fa fa-toggle-on"></i> 启用MQTT</label>
|
|
87
|
+
<input type="checkbox" id="node-input-enableMqtt" style="width: auto; margin-left: 5px; transform: scale(1.3); cursor: pointer;">
|
|
88
|
+
<span style="margin-left: 15px; font-size: 11px; color: #666; font-style: italic;">勾选后切换到MQTT模式</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="form-row form-row-mqtt-slave">
|
|
72
92
|
<label for="node-input-mqttServer" style="width: 110px;"><i class="fa fa-server"></i> MQTT服务器</label>
|
|
73
|
-
<input type="text" id="node-input-mqttServer" placeholder="
|
|
93
|
+
<input type="text" id="node-input-mqttServer" placeholder="选择MQTT服务器配置" style="width: calc(70% - 110px);">
|
|
74
94
|
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
75
95
|
选择已配置的MQTT服务器(需与主站节点使用同一配置)
|
|
76
96
|
</div>
|
|
@@ -79,8 +79,9 @@ module.exports = function(RED) {
|
|
|
79
79
|
|
|
80
80
|
// 配置参数
|
|
81
81
|
node.config = {
|
|
82
|
-
//
|
|
83
|
-
|
|
82
|
+
// MQTT配置
|
|
83
|
+
enableMqtt: config.enableMqtt !== undefined ? config.enableMqtt : false, // 是否启用MQTT(默认false)
|
|
84
|
+
mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "",
|
|
84
85
|
mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
|
|
85
86
|
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
86
87
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
|
|
@@ -164,7 +165,7 @@ module.exports = function(RED) {
|
|
|
164
165
|
|
|
165
166
|
// 如果是局域网IP,不启用fallback(用户明确指定了IP)
|
|
166
167
|
if (isLanIp) {
|
|
167
|
-
node.
|
|
168
|
+
node.debug(`检测到局域网IP配置 ${host},只使用此地址`);
|
|
168
169
|
return candidates;
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -509,27 +510,27 @@ module.exports = function(RED) {
|
|
|
509
510
|
node.updateStatus = function() {
|
|
510
511
|
const rs485Status = node.isRs485Connected ? 'OK' : 'ERR';
|
|
511
512
|
const state = node.currentState ? 'ON' : 'OFF';
|
|
512
|
-
const
|
|
513
|
+
const mqttEnabled = node.config.enableMqtt === true;
|
|
513
514
|
const mqttConnected = node.mqttClient && node.mqttClient.connected;
|
|
514
515
|
|
|
515
516
|
// RS485连接正常
|
|
516
517
|
if (node.isRs485Connected) {
|
|
517
|
-
if (!
|
|
518
|
-
//
|
|
518
|
+
if (!mqttEnabled) {
|
|
519
|
+
// 本地模式(未启用MQTT)
|
|
519
520
|
node.status({
|
|
520
521
|
fill: node.currentState ? "green" : "blue",
|
|
521
522
|
shape: "dot",
|
|
522
|
-
text: `本地模式
|
|
523
|
+
text: `本地模式 ${state}`
|
|
523
524
|
});
|
|
524
525
|
} else if (mqttConnected) {
|
|
525
|
-
// MQTT
|
|
526
|
+
// MQTT模式(已启用且已连接)
|
|
526
527
|
node.status({
|
|
527
528
|
fill: node.currentState ? "green" : "grey",
|
|
528
529
|
shape: "dot",
|
|
529
530
|
text: `MQTT模式 ${state}`
|
|
530
531
|
});
|
|
531
532
|
} else {
|
|
532
|
-
// MQTT
|
|
533
|
+
// MQTT已启用但未连接(使用本地模式)
|
|
533
534
|
node.status({
|
|
534
535
|
fill: node.currentState ? "green" : "blue",
|
|
535
536
|
shape: "ring",
|
|
@@ -548,10 +549,17 @@ module.exports = function(RED) {
|
|
|
548
549
|
|
|
549
550
|
// 连接MQTT(带智能重试和fallback)
|
|
550
551
|
node.connectMqtt = function() {
|
|
552
|
+
// 检查是否启用MQTT
|
|
553
|
+
if (!node.config.enableMqtt) {
|
|
554
|
+
node.log('MQTT未启用 - 使用本地模式(通过Node-RED连线控制)');
|
|
555
|
+
node.log('提示:将此节点连线到主站节点,即可实现本地控制');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
551
559
|
// 验证MQTT broker配置
|
|
552
560
|
if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
|
|
553
|
-
node.
|
|
554
|
-
node.
|
|
561
|
+
node.warn('MQTT已启用但broker地址未配置 - 使用本地模式');
|
|
562
|
+
node.warn('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
|
|
555
563
|
return;
|
|
556
564
|
}
|
|
557
565
|
|
|
@@ -560,8 +568,8 @@ module.exports = function(RED) {
|
|
|
560
568
|
let currentCandidateIndex = 0;
|
|
561
569
|
let lastConnectAttempt = 0;
|
|
562
570
|
|
|
563
|
-
node.
|
|
564
|
-
node.
|
|
571
|
+
node.debug(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
|
|
572
|
+
node.debug(`正在连接MQTT broker: ${brokerCandidates[0]}`);
|
|
565
573
|
|
|
566
574
|
const options = {
|
|
567
575
|
clientId: `modbus_switch_${node.id}`,
|
|
@@ -574,7 +582,7 @@ module.exports = function(RED) {
|
|
|
574
582
|
if (node.config.mqttUsername) {
|
|
575
583
|
options.username = node.config.mqttUsername;
|
|
576
584
|
options.password = node.config.mqttPassword;
|
|
577
|
-
node.
|
|
585
|
+
node.debug(`MQTT认证: 用户名=${node.config.mqttUsername}`);
|
|
578
586
|
}
|
|
579
587
|
|
|
580
588
|
// 尝试连接函数
|
|
@@ -598,7 +606,7 @@ module.exports = function(RED) {
|
|
|
598
606
|
|
|
599
607
|
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
600
608
|
if (brokerUrl !== brokerCandidates[0]) {
|
|
601
|
-
node.
|
|
609
|
+
node.debug(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
602
610
|
}
|
|
603
611
|
|
|
604
612
|
node.updateStatus();
|
|
@@ -662,7 +670,7 @@ module.exports = function(RED) {
|
|
|
662
670
|
|
|
663
671
|
// 5秒后重试第一个地址
|
|
664
672
|
setTimeout(() => {
|
|
665
|
-
node.
|
|
673
|
+
node.debug('重试连接MQTT broker...');
|
|
666
674
|
tryConnect(brokerCandidates[0]);
|
|
667
675
|
}, 5000);
|
|
668
676
|
} else {
|
package/package.json
CHANGED
|
@@ -1,55 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
|
-
"main": "nodes/modbus-master.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"node-red",
|
|
11
|
-
"modbus",
|
|
12
|
-
"modbus-tcp",
|
|
13
|
-
"modbus-rtu",
|
|
14
|
-
"relay",
|
|
15
|
-
"mqtt",
|
|
16
|
-
"automation",
|
|
17
|
-
"home-assistant",
|
|
18
|
-
"smart-home",
|
|
19
|
-
"iot",
|
|
20
|
-
"industrial",
|
|
21
|
-
"plc"
|
|
22
|
-
],
|
|
23
|
-
"author": {
|
|
24
|
-
"name": "symi-daguo",
|
|
25
|
-
"email": "symi@example.com"
|
|
26
|
-
},
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=14.0.0",
|
|
30
|
-
"npm": ">=6.0.0"
|
|
31
|
-
},
|
|
32
|
-
"node-red": {
|
|
33
|
-
"version": ">=2.0.0",
|
|
34
|
-
"nodes": {
|
|
35
|
-
"modbus-master": "nodes/modbus-master.js",
|
|
36
|
-
"modbus-slave-switch": "nodes/modbus-slave-switch.js",
|
|
37
|
-
"modbus-server-config": "nodes/modbus-server-config.js",
|
|
38
|
-
"mqtt-server-config": "nodes/mqtt-server-config.js",
|
|
39
|
-
"serial-port-config": "nodes/serial-port-config.js"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-symi-modbus",
|
|
3
|
+
"version": "2.6.6",
|
|
4
|
+
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
|
+
"main": "nodes/modbus-master.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"node-red",
|
|
11
|
+
"modbus",
|
|
12
|
+
"modbus-tcp",
|
|
13
|
+
"modbus-rtu",
|
|
14
|
+
"relay",
|
|
15
|
+
"mqtt",
|
|
16
|
+
"automation",
|
|
17
|
+
"home-assistant",
|
|
18
|
+
"smart-home",
|
|
19
|
+
"iot",
|
|
20
|
+
"industrial",
|
|
21
|
+
"plc"
|
|
22
|
+
],
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "symi-daguo",
|
|
25
|
+
"email": "symi@example.com"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.0.0",
|
|
30
|
+
"npm": ">=6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"node-red": {
|
|
33
|
+
"version": ">=2.0.0",
|
|
34
|
+
"nodes": {
|
|
35
|
+
"modbus-master": "nodes/modbus-master.js",
|
|
36
|
+
"modbus-slave-switch": "nodes/modbus-slave-switch.js",
|
|
37
|
+
"modbus-server-config": "nodes/modbus-server-config.js",
|
|
38
|
+
"mqtt-server-config": "nodes/mqtt-server-config.js",
|
|
39
|
+
"serial-port-config": "nodes/serial-port-config.js",
|
|
40
|
+
"modbus-debug": "nodes/modbus-debug.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"modbus-serial": "^8.0.23",
|
|
45
|
+
"mqtt": "^5.14.1",
|
|
46
|
+
"serialport": "^12.0.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/symi-daguo/node-red-contrib-symi-modbus.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/symi-daguo/node-red-contrib-symi-modbus/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/symi-daguo/node-red-contrib-symi-modbus#readme",
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"node-red": "^4.1.1"
|
|
58
|
+
}
|
|
59
|
+
}
|