node-red-contrib-symi-modbus 2.7.3 → 2.7.5
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 +43 -360
- package/examples/basic-flow.json +18 -17
- package/nodes/custom-protocol.html +3 -3
- package/nodes/custom-protocol.js +64 -28
- package/nodes/homekit-bridge.html +10 -2
- package/nodes/modbus-dashboard.html +14 -2
- package/nodes/modbus-debug.js +10 -2
- package/nodes/modbus-master.js +45 -14
- package/nodes/modbus-slave-switch.js +80 -36
- package/nodes/serial-port-config.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ node-red-restart
|
|
|
56
56
|
**配置方法**:
|
|
57
57
|
1. 主站节点:不启用MQTT或不配置MQTT服务器
|
|
58
58
|
2. 从站开关节点:不配置MQTT服务器
|
|
59
|
-
3.
|
|
59
|
+
3. **无需连线**:主站和从站通过内部事件自动通信
|
|
60
60
|
|
|
61
61
|
**优势**:
|
|
62
62
|
- ✅ 断网也能稳定运行
|
|
@@ -725,63 +725,35 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
725
725
|
|
|
726
726
|
### 自定义协议节点
|
|
727
727
|
|
|
728
|
-
|
|
728
|
+
用于控制非标准Modbus协议的485设备(如窗帘、特殊开关等)。
|
|
729
729
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
- **串口配置**:选择串口配置节点(必填)
|
|
734
|
-
- **打开指令**:16进制打开指令(最多48字节)
|
|
735
|
-
- **关闭指令**:16进制关闭指令(最多48字节)
|
|
736
|
-
- **暂停指令**:16进制暂停指令(仅窗帘模式,最多48字节)
|
|
737
|
-
|
|
738
|
-
**设备类型说明**:
|
|
739
|
-
- **开关模式**:接收`true`发送打开指令,接收`false`发送关闭指令
|
|
740
|
-
- **窗帘模式**:无论收到`true`还是`false`,都触发下一个指令,循环顺序:打开 → 暂停 → 关闭 → 暂停 → 打开...
|
|
741
|
-
- **其他模式**:与开关模式相同,接收`true/false`发送对应指令
|
|
742
|
-
|
|
743
|
-
**功能特性**:
|
|
744
|
-
- **16进制配置**:支持空格分隔的16进制码,自动格式化为大写
|
|
745
|
-
- **字节限制**:每个指令最多48字节,超出自动截断
|
|
746
|
-
- **测试功能**:配置界面可直接点击"测试"按钮发送指令到串口总线
|
|
747
|
-
- **持久化保存**:配置自动保存,重启后自动恢复
|
|
748
|
-
- **连线方式**:从站开关 → 自定义协议 → debug节点
|
|
749
|
-
|
|
750
|
-
**使用示例**:
|
|
751
|
-
1. 在Node-RED中添加自定义协议节点
|
|
752
|
-
2. 选择设备类型(例如:窗帘)
|
|
730
|
+
**配置步骤**:
|
|
731
|
+
1. 添加自定义协议节点到流程画布
|
|
732
|
+
2. 选择设备类型(开关/窗帘/其他)
|
|
753
733
|
3. 选择串口配置节点
|
|
754
|
-
4. 输入16
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
第1次收到true/false → 发送"打开"指令(例如:01 05 00 00 FF 00 8C 3A)
|
|
762
|
-
第2次收到true/false → 发送"暂停"指令(例如:01 05 00 01 FF 00 DD FA)
|
|
763
|
-
第3次收到true/false → 发送"关闭"指令(例如:01 05 00 00 00 00 CD CA)
|
|
764
|
-
第4次收到true/false → 发送"暂停"指令(例如:01 05 00 01 FF 00 DD FA)
|
|
765
|
-
第5次收到true/false → 循环回到"打开"指令
|
|
766
|
-
```
|
|
734
|
+
4. 输入16进制指令(空格分隔,自动格式化为大写)
|
|
735
|
+
- 打开指令:例如 `01 05 00 00 FF 00 8C 3A`
|
|
736
|
+
- 关闭指令:例如 `01 05 00 00 00 00 CD CA`
|
|
737
|
+
- 暂停指令:仅窗帘模式需要,例如 `01 05 00 01 FF 00 DD FA`
|
|
738
|
+
5. 点击"测试"按钮验证指令是否正确发送
|
|
739
|
+
6. 连线:从站开关 → 自定义协议节点
|
|
740
|
+
7. 部署流程
|
|
767
741
|
|
|
768
|
-
|
|
769
|
-
-
|
|
770
|
-
-
|
|
742
|
+
**设备类型说明**:
|
|
743
|
+
- **开关模式**:收到`true`发送打开指令,收到`false`发送关闭指令
|
|
744
|
+
- **窗帘模式**:每次触发循环发送下一个指令(打开 → 暂停 → 关闭 → 暂停 → 打开...)
|
|
745
|
+
- **其他模式**:与开关模式相同
|
|
771
746
|
|
|
772
|
-
|
|
773
|
-
-
|
|
774
|
-
-
|
|
775
|
-
- 16
|
|
776
|
-
- 支持空格、大小写混合输入,自动格式化
|
|
777
|
-
- 窗帘模式内部维护状态索引,自动循环(4个状态:打开→暂停→关闭→暂停)
|
|
747
|
+
**使用场景**:
|
|
748
|
+
- 窗帘控制:支持打开/暂停/关闭循环控制
|
|
749
|
+
- 特殊开关:非标准Modbus协议的485设备
|
|
750
|
+
- 自定义设备:任何需要发送固定16进制指令的设备
|
|
778
751
|
|
|
779
752
|
**注意事项**:
|
|
780
|
-
-
|
|
781
|
-
-
|
|
782
|
-
-
|
|
783
|
-
-
|
|
784
|
-
- 非标准协议设备数量不多时推荐使用连线方式
|
|
753
|
+
- 每个指令最多48字节
|
|
754
|
+
- 窗帘模式需配置三个指令(打开、关闭、暂停)
|
|
755
|
+
- 测试功能需先选择串口配置
|
|
756
|
+
- 无需连线到debug节点,直接通过串口配置节点发送数据
|
|
785
757
|
|
|
786
758
|
## 输出消息格式
|
|
787
759
|
|
|
@@ -841,323 +813,34 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
841
813
|
]
|
|
842
814
|
```
|
|
843
815
|
|
|
844
|
-
|
|
816
|
+
完整示例请参考项目中的 `examples/basic-flow.json` 文件。
|
|
845
817
|
|
|
846
|
-
|
|
818
|
+
## 技术栈
|
|
847
819
|
|
|
848
|
-
**核心功能**:
|
|
849
|
-
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
850
|
-
- 多设备轮询(最多10台从站,每台32路继电器,轮询间隔100-10000ms可调)
|
|
851
|
-
- 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
|
|
852
|
-
- 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线)
|
|
853
|
-
- 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制)
|
|
854
|
-
- 🔥 **智能写入队列**(所有写入操作串行执行,避免锁竞争,支持HomeKit群控)
|
|
855
|
-
- 🔥 **可视化控制看板**(实时显示和控制所有继电器状态,美观易用)
|
|
856
|
-
- 🔥 **自定义协议转换**(支持非标准485协议设备,窗帘循环控制)
|
|
857
|
-
- MQTT集成(可选启用,Home Assistant自动发现)
|
|
858
|
-
- 物理开关面板双向同步(支持开关模式和场景模式)
|
|
859
|
-
- 长期稳定运行(内存管理、智能重连、异步处理)
|
|
860
|
-
|
|
861
|
-
**技术要求**:
|
|
862
820
|
- Node.js: >=14.0.0
|
|
863
821
|
- Node-RED: >=2.0.0
|
|
822
|
+
- modbus-serial: ^8.0.23
|
|
823
|
+
- serialport: ^12.0.0
|
|
824
|
+
- mqtt: ^5.14.1(可选)
|
|
825
|
+
- hap-nodejs: ^1.2.0
|
|
826
|
+
- node-persist: ^4.0.4
|
|
864
827
|
|
|
865
|
-
##
|
|
828
|
+
## 版本信息
|
|
866
829
|
|
|
867
|
-
|
|
830
|
+
**当前版本**: v2.7.5
|
|
868
831
|
|
|
869
|
-
|
|
832
|
+
**更新内容**:
|
|
833
|
+
- 修复从站开关节点LED反馈重复发送问题
|
|
834
|
+
- 优化状态变化广播机制,避免重复触发
|
|
835
|
+
- 增强队列处理稳定性,确保所有状态正确传递
|
|
836
|
+
- 改进日志输出,便于问题排查
|
|
870
837
|
|
|
871
|
-
|
|
872
|
-
- NPM: https://www.npmjs.com/~symi-daguo
|
|
873
|
-
- GitHub: https://github.com/symi-daguo
|
|
838
|
+
## 许可证
|
|
874
839
|
|
|
875
|
-
|
|
840
|
+
MIT License
|
|
841
|
+
|
|
842
|
+
## 支持与反馈
|
|
876
843
|
|
|
844
|
+
- GitHub: https://github.com/symi-daguo/node-red-contrib-symi-modbus
|
|
877
845
|
- Issues: https://github.com/symi-daguo/node-red-contrib-symi-modbus/issues
|
|
878
846
|
- NPM: https://www.npmjs.com/package/node-red-contrib-symi-modbus
|
|
879
|
-
|
|
880
|
-
### 节点与分类(Palette)
|
|
881
|
-
|
|
882
|
-
- 侧边栏分类名:`SYMI-MODBUS`
|
|
883
|
-
- 包含节点:
|
|
884
|
-
- `modbus-master`(主站)
|
|
885
|
-
- `modbus-slave-switch`(从站开关)
|
|
886
|
-
- `modbus-dashboard`(控制看板)
|
|
887
|
-
- `homekit-bridge`(HomeKit网桥)
|
|
888
|
-
- `custom-protocol`(自定义协议)
|
|
889
|
-
- `modbus-debug`(调试)
|
|
890
|
-
- 如果未显示该分类或节点:
|
|
891
|
-
- 刷新浏览器缓存(Shift+刷新)
|
|
892
|
-
- 重启 Node-RED(如:`node-red-restart` 或系统服务方式)
|
|
893
|
-
- 在“节点管理(Manage Palette)”确认安装版本为最新版
|
|
894
|
-
|
|
895
|
-
### 调试节点(modbus-debug)使用要点
|
|
896
|
-
|
|
897
|
-
- 数据来源选择:`sourceType = serial`(共享串口)或 `modbus`(独立服务器)
|
|
898
|
-
- 共享串口:需要选择并关联一个 `serial-port-config` 配置节点
|
|
899
|
-
- 独立服务器:需要选择并关联一个 `modbus-server-config` 配置节点
|
|
900
|
-
- HEX显示:可选大写、可选时间戳、`maxBytes` 控制显示长度
|
|
901
|
-
- 输出:`msg.payload`(格式化HEX)、`msg.buffer`(原始Buffer)、`msg.meta`(来源信息)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
### MQTT自动发现
|
|
905
|
-
|
|
906
|
-
启用MQTT后,自动生成Home Assistant兼容的Discovery配置:
|
|
907
|
-
- **唯一性保证**:每个实体使用稳定的`unique_id`,避免重复生成
|
|
908
|
-
- **设备分组**:同一从站的所有继电器自动分组到一个设备下
|
|
909
|
-
- **状态持久化**:使用`retain=true`确保状态持久化
|
|
910
|
-
- **在线状态**:自动发布设备可用性状态
|
|
911
|
-
|
|
912
|
-
### 配置持久化
|
|
913
|
-
|
|
914
|
-
所有节点配置自动保存到Node-RED的flows文件中:
|
|
915
|
-
- 从站地址、线圈范围、轮询间隔
|
|
916
|
-
- MQTT服务器配置
|
|
917
|
-
- 开关面板映射关系
|
|
918
|
-
|
|
919
|
-
部署后配置永久生效,重启Node-RED后自动恢复。
|
|
920
|
-
|
|
921
|
-
### 长期稳定运行
|
|
922
|
-
|
|
923
|
-
针对工控机24/7长期运行优化:
|
|
924
|
-
- **内存管理**:自动清理缓存,释放无用对象
|
|
925
|
-
- **事件监听器清理**:关闭时移除所有监听器,防止内存泄漏
|
|
926
|
-
- **智能日志限流**:错误日志10分钟输出一次,避免日志刷屏
|
|
927
|
-
- **智能重连机制**:
|
|
928
|
-
- Modbus连接断开自动重连(指数退避:5秒→10秒→20秒...最大60秒)
|
|
929
|
-
- MQTT连接断开自动重连(支持多地址fallback)
|
|
930
|
-
- 串口拔插自动检测并重连
|
|
931
|
-
- TCP网络故障自动恢复
|
|
932
|
-
- 连接前彻底清理旧实例,避免资源泄漏
|
|
933
|
-
- **互斥锁机制**:防止读写冲突导致的数据异常
|
|
934
|
-
- **TCP永久连接**:
|
|
935
|
-
- 禁用TCP超时(永久连接),避免无数据时超时断开
|
|
936
|
-
- Keep-Alive心跳10秒间隔,确保连接活跃
|
|
937
|
-
- 适应客户长期不在家、总线无数据的场景
|
|
938
|
-
- 网络故障自动重连,恢复后立即恢复通信
|
|
939
|
-
|
|
940
|
-
## 技术规格
|
|
941
|
-
|
|
942
|
-
### Modbus协议
|
|
943
|
-
|
|
944
|
-
- **协议类型**:Modbus TCP / Modbus RTU
|
|
945
|
-
- **底层库**:modbus-serial ^8.0.23(内部封装serialport,支持TCP和串口)
|
|
946
|
-
- **功能码支持**:0x01(读线圈)、0x05(写单个线圈)、0x0F(写多个线圈)
|
|
947
|
-
- **从站地址范围**:1-247(建议从10开始)
|
|
948
|
-
- **线圈数量**:每台设备32个(0-31)
|
|
949
|
-
- **最大设备数**:10台同时轮询
|
|
950
|
-
- **轮询间隔**:默认200ms(建议300-500ms,支持100-10000ms)
|
|
951
|
-
- **串口配置**:9600 8-N-1(波特率9600,8数据位,无校验,1停止位)
|
|
952
|
-
- **超时设置**:5000ms(TCP和串口通用)
|
|
953
|
-
|
|
954
|
-
### 兼容性
|
|
955
|
-
|
|
956
|
-
- **Node.js**: >= 14.0.0
|
|
957
|
-
- **Node-RED**: >= 2.0.0
|
|
958
|
-
- **MQTT Broker**: Mosquitto / EMQX / Any MQTT 3.1.1/5.0
|
|
959
|
-
- **Home Assistant**: 2024.x+(MQTT Discovery标准)
|
|
960
|
-
- **操作系统**: Windows / Linux / macOS / HassOS
|
|
961
|
-
|
|
962
|
-
## Home Assistant集成
|
|
963
|
-
|
|
964
|
-
### 自动发现
|
|
965
|
-
|
|
966
|
-
启用MQTT后,设备自动出现在Home Assistant中:
|
|
967
|
-
- 实体ID: `switch.relay_{从站地址}_{线圈编号}`
|
|
968
|
-
- 设备名称: `Modbus继电器-{从站地址}`
|
|
969
|
-
- 自动分组: 同一从站的所有继电器分组到一个设备
|
|
970
|
-
|
|
971
|
-
### MQTT主题结构
|
|
972
|
-
|
|
973
|
-
```
|
|
974
|
-
状态主题: modbus/relay/{从站}/{线圈}/state
|
|
975
|
-
命令主题: modbus/relay/{从站}/{线圈}/set
|
|
976
|
-
可用性主题: modbus/relay/{从站}/availability
|
|
977
|
-
发现主题: homeassistant/switch/modbus_relay_{从站}_{线圈}/config
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
## 故障排除
|
|
981
|
-
|
|
982
|
-
### 串口连接失败
|
|
983
|
-
|
|
984
|
-
**Linux**:
|
|
985
|
-
```bash
|
|
986
|
-
# 查看串口设备
|
|
987
|
-
ls -l /dev/ttyUSB* /dev/ttyS*
|
|
988
|
-
|
|
989
|
-
# 添加用户到dialout组(需要重新登录)
|
|
990
|
-
sudo usermod -a -G dialout $USER
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
**macOS**:
|
|
994
|
-
```bash
|
|
995
|
-
# 查看串口设备(注意macOS使用cu.*而不是tty.*)
|
|
996
|
-
ls -l /dev/cu.*
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
**Docker/HassOS**:
|
|
1000
|
-
```yaml
|
|
1001
|
-
# 在docker-compose.yml或HassOS插件配置中添加设备映射
|
|
1002
|
-
devices:
|
|
1003
|
-
- /dev/ttyUSB0:/dev/ttyUSB0
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
### MQTT连接失败
|
|
1007
|
-
|
|
1008
|
-
1. 确认MQTT broker正在运行:
|
|
1009
|
-
```bash
|
|
1010
|
-
# Linux
|
|
1011
|
-
sudo systemctl status mosquitto
|
|
1012
|
-
|
|
1013
|
-
# macOS
|
|
1014
|
-
brew services list | grep mosquitto
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
2. 测试MQTT连接:
|
|
1018
|
-
```bash
|
|
1019
|
-
mosquitto_sub -h localhost -t test
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
3. 检查Node-RED日志中的MQTT连接信息
|
|
1023
|
-
|
|
1024
|
-
### 主站轮询不工作
|
|
1025
|
-
|
|
1026
|
-
1. **检查从站配置**:确认已添加所有从站设备(如10、11、12、13)
|
|
1027
|
-
2. **检查轮询间隔**:默认200ms,建议300-500ms(多台从站时避免总线拥堵)
|
|
1028
|
-
3. **查看Node-RED调试日志**:部署后查看日志中的轮询信息
|
|
1029
|
-
4. **检查串口波特率**:确认波特率为9600(与从站设备一致)
|
|
1030
|
-
5. **检查从站地址**:确认从站地址正确(1-247)
|
|
1031
|
-
6. **确认从站设备在线**:使用Modbus调试工具测试从站是否响应
|
|
1032
|
-
7. **检查MQTT连接**:确保MQTT broker地址正确,轮询不依赖MQTT但状态发布需要MQTT
|
|
1033
|
-
8. **测试连接**:
|
|
1034
|
-
- TCP连接问题:先用 `modbus-serial` 单独测试TCP连接
|
|
1035
|
-
- 串口问题:先用 `serialport` 单独测试串口通信
|
|
1036
|
-
|
|
1037
|
-
### 从站开关无响应
|
|
1038
|
-
|
|
1039
|
-
1. 检查RS-485连接是否正常
|
|
1040
|
-
2. 确认开关面板地址和按钮编号正确
|
|
1041
|
-
3. 检查MQTT连接状态
|
|
1042
|
-
4. 查看Node-RED日志中的协议解析信息
|
|
1043
|
-
|
|
1044
|
-
## 输入消息格式
|
|
1045
|
-
|
|
1046
|
-
### 主站节点
|
|
1047
|
-
|
|
1048
|
-
```javascript
|
|
1049
|
-
// 启动轮询
|
|
1050
|
-
msg.payload = {cmd: "start"};
|
|
1051
|
-
|
|
1052
|
-
// 停止轮询
|
|
1053
|
-
msg.payload = {cmd: "stop"};
|
|
1054
|
-
|
|
1055
|
-
// 写单个线圈
|
|
1056
|
-
msg.payload = {
|
|
1057
|
-
cmd: "writeCoil",
|
|
1058
|
-
slave: 10, // 从站地址
|
|
1059
|
-
coil: 0, // 线圈编号
|
|
1060
|
-
value: true // true=开, false=关
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
// 批量写多个线圈
|
|
1064
|
-
msg.payload = {
|
|
1065
|
-
cmd: "writeCoils",
|
|
1066
|
-
slave: 10, // 从站地址
|
|
1067
|
-
startCoil: 0, // 起始线圈
|
|
1068
|
-
values: [true, false, true, false] // 线圈值数组
|
|
1069
|
-
};
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
### 从站开关节点
|
|
1073
|
-
|
|
1074
|
-
```javascript
|
|
1075
|
-
// 发送开关命令
|
|
1076
|
-
msg.payload = true; // 或 false
|
|
1077
|
-
msg.payload = "ON"; // 或 "OFF"
|
|
1078
|
-
msg.payload = 1; // 或 0
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
## 输出消息格式
|
|
1082
|
-
|
|
1083
|
-
### 主站节点
|
|
1084
|
-
|
|
1085
|
-
```javascript
|
|
1086
|
-
{
|
|
1087
|
-
payload: {
|
|
1088
|
-
slave: 10, // 从站地址
|
|
1089
|
-
coils: [true, false, ...], // 线圈状态数组
|
|
1090
|
-
timestamp: 1234567890 // 时间戳
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
### 从站开关节点
|
|
1096
|
-
|
|
1097
|
-
```javascript
|
|
1098
|
-
{
|
|
1099
|
-
payload: true, // 开关状态
|
|
1100
|
-
topic: "switch_0_btn1", // 主题
|
|
1101
|
-
switchId: 0, // 开关面板ID
|
|
1102
|
-
button: 1, // 按钮编号
|
|
1103
|
-
targetSlave: 10, // 目标从站地址
|
|
1104
|
-
targetCoil: 0 // 目标线圈编号
|
|
1105
|
-
}
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
## 性能指标
|
|
1109
|
-
|
|
1110
|
-
- **内存占用**:< 50MB(单个主站节点,轮询10个设备)
|
|
1111
|
-
- **CPU占用**:< 5%(正常轮询状态)
|
|
1112
|
-
- **连接延迟**:Modbus响应 < 100ms,MQTT发布 < 50ms
|
|
1113
|
-
- **稳定运行**:经过工控机7x24小时长期运行验证
|
|
1114
|
-
- **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
|
|
1115
|
-
|
|
1116
|
-
## 示例Flow
|
|
1117
|
-
|
|
1118
|
-
```json
|
|
1119
|
-
[
|
|
1120
|
-
{
|
|
1121
|
-
"id": "modbus-master-1",
|
|
1122
|
-
"type": "modbus-master",
|
|
1123
|
-
"name": "主站",
|
|
1124
|
-
"connectionType": "serial",
|
|
1125
|
-
"serialPort": "/dev/ttyUSB0",
|
|
1126
|
-
"serialBaudRate": 9600,
|
|
1127
|
-
"slaves": [
|
|
1128
|
-
{"address": 10, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1129
|
-
{"address": 11, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1130
|
-
{"address": 12, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1131
|
-
{"address": 13, "coilStart": 0, "coilEnd": 31, "pollInterval": 200}
|
|
1132
|
-
],
|
|
1133
|
-
"enableMqtt": true,
|
|
1134
|
-
"mqttServer": "mqtt-config-1"
|
|
1135
|
-
}
|
|
1136
|
-
]
|
|
1137
|
-
```
|
|
1138
|
-
|
|
1139
|
-
## 项目信息
|
|
1140
|
-
|
|
1141
|
-
**当前版本**: v2.7.3
|
|
1142
|
-
|
|
1143
|
-
**最新更新**(v2.7.3):
|
|
1144
|
-
- 优化窗帘控制逻辑:打开 → 暂停 → 关闭 → 暂停 → 打开(循环),更符合实际使用场景
|
|
1145
|
-
- 优化MQTT断网日志:长期断网时减少日志输出,避免垃圾日志影响性能和硬盘空间
|
|
1146
|
-
- 重试间隔优化:从5秒改为30秒,减少重试频率,降低系统负担
|
|
1147
|
-
- 日志级别优化:MQTT错误从error改为debug,不写入日志文件
|
|
1148
|
-
|
|
1149
|
-
**技术栈**:
|
|
1150
|
-
- Node.js: >=14.0.0
|
|
1151
|
-
- Node-RED: >=2.0.0
|
|
1152
|
-
- modbus-serial: ^8.0.23
|
|
1153
|
-
- serialport: ^12.0.0
|
|
1154
|
-
- mqtt: ^5.14.1(可选)
|
|
1155
|
-
- hap-nodejs: ^1.2.0
|
|
1156
|
-
- node-persist: ^4.0.4
|
|
1157
|
-
|
|
1158
|
-
**历史版本**:
|
|
1159
|
-
- v2.7.2: 新增自定义协议节点,支持非标准485协议设备
|
|
1160
|
-
- v2.7.1: 新增可视化控制看板节点
|
|
1161
|
-
- v2.7.0: 智能写入队列机制,支持HomeKit群控
|
|
1162
|
-
- v2.6.8: 新增HomeKit网桥节点
|
|
1163
|
-
- v2.6.7及更早: 基础功能实现
|
package/examples/basic-flow.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "modbus_server_config_1",
|
|
4
|
+
"type": "modbus-server-config",
|
|
5
|
+
"name": "Modbus服务器",
|
|
6
|
+
"connectionType": "tcp",
|
|
7
|
+
"tcpHost": "127.0.0.1",
|
|
8
|
+
"tcpPort": "502"
|
|
9
|
+
},
|
|
2
10
|
{
|
|
3
11
|
"id": "modbus_master_1",
|
|
4
12
|
"type": "modbus-master",
|
|
5
13
|
"name": "Modbus主站",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"slaveCount": "1",
|
|
16
|
-
"coilStart": "0",
|
|
17
|
-
"coilEnd": "31",
|
|
18
|
-
"pollInterval": "100",
|
|
14
|
+
"serverConfig": "modbus_server_config_1",
|
|
15
|
+
"slaves": [
|
|
16
|
+
{
|
|
17
|
+
"address": 10,
|
|
18
|
+
"coilStart": 0,
|
|
19
|
+
"coilEnd": 31,
|
|
20
|
+
"pollInterval": 300
|
|
21
|
+
}
|
|
22
|
+
],
|
|
19
23
|
"enableMqtt": false,
|
|
20
|
-
"
|
|
21
|
-
"mqttUsername": "",
|
|
22
|
-
"mqttPassword": "",
|
|
23
|
-
"mqttBaseTopic": "modbus/relay",
|
|
24
|
+
"mqttConfig": "",
|
|
24
25
|
"x": 320,
|
|
25
26
|
"y": 140,
|
|
26
27
|
"wires": [["debug_1"]]
|
|
@@ -95,13 +95,13 @@
|
|
|
95
95
|
}),
|
|
96
96
|
success: function(result) {
|
|
97
97
|
if (result.success) {
|
|
98
|
-
RED.notify(
|
|
98
|
+
RED.notify(cmdName + '指令已发送: ' + hexString, 'success');
|
|
99
99
|
} else {
|
|
100
|
-
RED.notify('
|
|
100
|
+
RED.notify('发送失败: ' + result.error, 'error');
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
error: function(err) {
|
|
104
|
-
RED.notify('
|
|
104
|
+
RED.notify('发送失败: ' + err.statusText, 'error');
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
}
|
package/nodes/custom-protocol.js
CHANGED
|
@@ -64,20 +64,42 @@ module.exports = function(RED) {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
67
|
+
// 直接通过串口配置节点发送数据
|
|
68
|
+
if (!serialNode || !serialNode.connection) {
|
|
69
|
+
node.error('串口连接未建立');
|
|
70
|
+
node.status({fill: "red", shape: "ring", text: "连接未建立"});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 检查连接状态
|
|
75
|
+
var isConnected = false;
|
|
76
|
+
if (serialNode.connectionType === 'tcp') {
|
|
77
|
+
isConnected = serialNode.connection && !serialNode.connection.destroyed;
|
|
78
|
+
} else {
|
|
79
|
+
isConnected = serialNode.connection && serialNode.connection.isOpen;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isConnected) {
|
|
83
|
+
node.error('串口/TCP连接未打开');
|
|
84
|
+
node.status({fill: "red", shape: "ring", text: "连接未打开"});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 使用串口配置节点的write方法(带队列机制)
|
|
89
|
+
serialNode.write(buffer, function(err) {
|
|
90
|
+
if (err) {
|
|
91
|
+
node.error('发送失败: ' + err.message);
|
|
92
|
+
node.status({fill: "red", shape: "ring", text: "发送失败"});
|
|
93
|
+
} else {
|
|
94
|
+
node.log(cmdName + '指令已发送: ' + buffer.toString('hex').toUpperCase());
|
|
95
|
+
node.status({fill: "green", shape: "dot", text: cmdName + " (" + buffer.length + "字节)"});
|
|
96
|
+
|
|
97
|
+
// 3秒后清除状态
|
|
98
|
+
setTimeout(function() {
|
|
99
|
+
node.status({fill: "blue", shape: "ring", text: "就绪"});
|
|
100
|
+
}, 3000);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
// 处理输入消息
|
|
@@ -180,22 +202,36 @@ module.exports = function(RED) {
|
|
|
180
202
|
}
|
|
181
203
|
var buffer = Buffer.from(hex, 'hex');
|
|
182
204
|
|
|
183
|
-
//
|
|
184
|
-
if (serialNode.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
bytes: buffer.length
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
});
|
|
205
|
+
// 检查连接状态
|
|
206
|
+
if (!serialNode.connection) {
|
|
207
|
+
res.status(503).json({success: false, error: '连接未建立'});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var isConnected = false;
|
|
212
|
+
if (serialNode.connectionType === 'tcp') {
|
|
213
|
+
isConnected = !serialNode.connection.destroyed;
|
|
196
214
|
} else {
|
|
197
|
-
|
|
215
|
+
isConnected = serialNode.connection.isOpen;
|
|
198
216
|
}
|
|
217
|
+
|
|
218
|
+
if (!isConnected) {
|
|
219
|
+
res.status(503).json({success: false, error: '连接未打开'});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 使用串口配置节点的write方法(带队列机制)
|
|
224
|
+
serialNode.write(buffer, function(err) {
|
|
225
|
+
if (err) {
|
|
226
|
+
res.json({success: false, error: err.message});
|
|
227
|
+
} else {
|
|
228
|
+
res.json({
|
|
229
|
+
success: true,
|
|
230
|
+
message: cmdName + '指令已发送',
|
|
231
|
+
bytes: buffer.length
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
199
235
|
} catch (err) {
|
|
200
236
|
res.status(500).json({success: false, error: err.message});
|
|
201
237
|
}
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
renderRelayNameConfig();
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// 添加刷新按钮
|
|
55
|
+
$("#btn-refresh-homekit").on("click", function() {
|
|
56
|
+
renderRelayNameConfig();
|
|
57
|
+
});
|
|
58
|
+
|
|
54
59
|
// 初始渲染
|
|
55
60
|
renderRelayNameConfig();
|
|
56
61
|
|
|
@@ -148,10 +153,13 @@
|
|
|
148
153
|
|
|
149
154
|
<div class="form-row">
|
|
150
155
|
<label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
|
|
151
|
-
<select id="node-input-masterNode" style="width:
|
|
156
|
+
<select id="node-input-masterNode" style="width: 55%;">
|
|
152
157
|
<option value="">请选择主站节点</option>
|
|
153
158
|
</select>
|
|
154
|
-
<
|
|
159
|
+
<button type="button" id="btn-refresh-homekit" class="red-ui-button" style="margin-left: 5px;">
|
|
160
|
+
<i class="fa fa-refresh"></i> 刷新
|
|
161
|
+
</button>
|
|
162
|
+
<div style="font-size: 11px; color: #999; margin-top: 5px;">选择要桥接的Modbus主站节点,点击刷新按钮更新显示</div>
|
|
155
163
|
</div>
|
|
156
164
|
|
|
157
165
|
<hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">
|
|
@@ -222,6 +222,15 @@
|
|
|
222
222
|
}
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
+
// 添加刷新按钮
|
|
226
|
+
$("#btn-refresh-dashboard").on("click", function() {
|
|
227
|
+
stopPolling();
|
|
228
|
+
renderDashboard();
|
|
229
|
+
if (masterNodeSelect.val()) {
|
|
230
|
+
startPolling();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
225
234
|
// 初始渲染
|
|
226
235
|
renderDashboard();
|
|
227
236
|
if (node.masterNode) {
|
|
@@ -244,10 +253,13 @@
|
|
|
244
253
|
|
|
245
254
|
<div class="form-row">
|
|
246
255
|
<label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
|
|
247
|
-
<select id="node-input-masterNode" style="width:
|
|
256
|
+
<select id="node-input-masterNode" style="width: 55%;">
|
|
248
257
|
<option value="">请选择主站节点</option>
|
|
249
258
|
</select>
|
|
250
|
-
<
|
|
259
|
+
<button type="button" id="btn-refresh-dashboard" class="red-ui-button" style="margin-left: 5px;">
|
|
260
|
+
<i class="fa fa-refresh"></i> 刷新
|
|
261
|
+
</button>
|
|
262
|
+
<div style="font-size: 11px; color: #999; margin-top: 5px;">选择要监控的Modbus主站节点,点击刷新按钮更新显示</div>
|
|
251
263
|
</div>
|
|
252
264
|
|
|
253
265
|
<div class="form-row" style="margin-top: 20px;">
|
package/nodes/modbus-debug.js
CHANGED
|
@@ -28,9 +28,12 @@ module.exports = function(RED) {
|
|
|
28
28
|
node.localConnection = null;
|
|
29
29
|
node.localConnType = null; // tcp | serial
|
|
30
30
|
|
|
31
|
-
const sendHexMsg = (data) => {
|
|
31
|
+
const sendHexMsg = (data, direction) => {
|
|
32
32
|
if (!data || !Buffer.isBuffer(data) || data.length === 0) return;
|
|
33
33
|
|
|
34
|
+
// direction: 'received' (接收) 或 'sent' (发送)
|
|
35
|
+
const isSent = direction === 'sent';
|
|
36
|
+
|
|
34
37
|
let buf = data;
|
|
35
38
|
if (node.maxBytes > 0 && buf.length > node.maxBytes) {
|
|
36
39
|
buf = buf.subarray(0, node.maxBytes);
|
|
@@ -41,6 +44,7 @@ module.exports = function(RED) {
|
|
|
41
44
|
length: data.length,
|
|
42
45
|
displayedLength: buf.length,
|
|
43
46
|
truncated: node.maxBytes > 0 && data.length > node.maxBytes,
|
|
47
|
+
direction: isSent ? 'TX' : 'RX'
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
// 来源信息
|
|
@@ -76,7 +80,11 @@ module.exports = function(RED) {
|
|
|
76
80
|
if (node.includeTimestamp) msg.timestamp = Date.now();
|
|
77
81
|
|
|
78
82
|
node.send(msg);
|
|
79
|
-
|
|
83
|
+
|
|
84
|
+
// 状态显示:TX=发送,RX=接收
|
|
85
|
+
const statusText = isSent ? `TX ${data.length}B` : `RX ${data.length}B`;
|
|
86
|
+
const statusColor = isSent ? "blue" : "green";
|
|
87
|
+
node.status({ fill: statusColor, shape: "dot", text: statusText });
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
// 选择来源:共享串口配置 或 独立连接到 Modbus 服务器配置
|
package/nodes/modbus-master.js
CHANGED
|
@@ -1170,6 +1170,10 @@ module.exports = function(RED) {
|
|
|
1170
1170
|
|
|
1171
1171
|
node.isProcessingWrite = true;
|
|
1172
1172
|
|
|
1173
|
+
// 记录队列开始处理时间
|
|
1174
|
+
const queueStartTime = Date.now();
|
|
1175
|
+
const queueLength = node.writeQueue.length;
|
|
1176
|
+
|
|
1173
1177
|
while (node.writeQueue.length > 0) {
|
|
1174
1178
|
const task = node.writeQueue.shift();
|
|
1175
1179
|
|
|
@@ -1189,6 +1193,8 @@ module.exports = function(RED) {
|
|
|
1189
1193
|
if (task.reject) {
|
|
1190
1194
|
task.reject(err);
|
|
1191
1195
|
}
|
|
1196
|
+
// 写入失败不中断队列,继续处理下一个任务
|
|
1197
|
+
node.warn(`队列任务失败,继续处理下一个任务: ${err.message}`);
|
|
1192
1198
|
}
|
|
1193
1199
|
|
|
1194
1200
|
// 等待一段时间再处理下一个任务(20ms间隔,确保总线稳定)
|
|
@@ -1197,6 +1203,12 @@ module.exports = function(RED) {
|
|
|
1197
1203
|
}
|
|
1198
1204
|
}
|
|
1199
1205
|
|
|
1206
|
+
// 队列处理完成,输出统计信息
|
|
1207
|
+
const queueDuration = Date.now() - queueStartTime;
|
|
1208
|
+
if (queueLength > 1) {
|
|
1209
|
+
node.debug(`写入队列处理完成:${queueLength}个任务,耗时${queueDuration}ms`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1200
1212
|
node.isProcessingWrite = false;
|
|
1201
1213
|
};
|
|
1202
1214
|
|
|
@@ -1236,6 +1248,7 @@ module.exports = function(RED) {
|
|
|
1236
1248
|
node.lastWriteTime[slaveId] = Date.now();
|
|
1237
1249
|
|
|
1238
1250
|
// 更新本地状态
|
|
1251
|
+
const oldValue = node.deviceStates[slaveId].coils[coil];
|
|
1239
1252
|
node.deviceStates[slaveId].coils[coil] = value;
|
|
1240
1253
|
|
|
1241
1254
|
node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
@@ -1248,6 +1261,17 @@ module.exports = function(RED) {
|
|
|
1248
1261
|
value: value
|
|
1249
1262
|
});
|
|
1250
1263
|
|
|
1264
|
+
// 只在状态真正改变时广播状态变化事件(用于LED反馈)
|
|
1265
|
+
// 避免重复广播导致LED反馈死循环
|
|
1266
|
+
if (oldValue !== value) {
|
|
1267
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
1268
|
+
slave: slaveId,
|
|
1269
|
+
coil: coil,
|
|
1270
|
+
value: value,
|
|
1271
|
+
source: 'write'
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1251
1275
|
// 释放锁
|
|
1252
1276
|
node.modbusLock = false;
|
|
1253
1277
|
|
|
@@ -1325,14 +1349,29 @@ module.exports = function(RED) {
|
|
|
1325
1349
|
|
|
1326
1350
|
// 更新本地状态
|
|
1327
1351
|
for (let i = 0; i < values.length; i++) {
|
|
1328
|
-
|
|
1352
|
+
const coilIndex = startCoil + i;
|
|
1353
|
+
const oldValue = node.deviceStates[slaveId].coils[coilIndex];
|
|
1354
|
+
const newValue = values[i];
|
|
1355
|
+
|
|
1356
|
+
node.deviceStates[slaveId].coils[coilIndex] = newValue;
|
|
1357
|
+
|
|
1329
1358
|
// 发布到MQTT和触发事件
|
|
1330
|
-
node.publishMqttState(slaveId,
|
|
1359
|
+
node.publishMqttState(slaveId, coilIndex, newValue);
|
|
1331
1360
|
node.emit('stateUpdate', {
|
|
1332
1361
|
slave: slaveId,
|
|
1333
|
-
coil:
|
|
1334
|
-
value:
|
|
1362
|
+
coil: coilIndex,
|
|
1363
|
+
value: newValue
|
|
1335
1364
|
});
|
|
1365
|
+
|
|
1366
|
+
// 只在状态真正改变时广播状态变化事件(用于LED反馈)
|
|
1367
|
+
if (oldValue !== newValue) {
|
|
1368
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
1369
|
+
slave: slaveId,
|
|
1370
|
+
coil: coilIndex,
|
|
1371
|
+
value: newValue,
|
|
1372
|
+
source: 'write'
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1336
1375
|
}
|
|
1337
1376
|
|
|
1338
1377
|
node.debug(`批量写入成功: 从站${slaveId} 起始线圈${startCoil} 共${values.length}个线圈`);
|
|
@@ -1396,18 +1435,10 @@ module.exports = function(RED) {
|
|
|
1396
1435
|
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1397
1436
|
|
|
1398
1437
|
try {
|
|
1399
|
-
//
|
|
1438
|
+
// 执行写入操作(writeSingleCoil内部已经会广播状态变化事件)
|
|
1400
1439
|
await node.writeSingleCoil(slave, coil, value);
|
|
1401
1440
|
|
|
1402
|
-
|
|
1403
|
-
RED.events.emit('modbus:coilStateChanged', {
|
|
1404
|
-
slave: slave,
|
|
1405
|
-
coil: coil,
|
|
1406
|
-
value: value,
|
|
1407
|
-
source: 'master'
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
node.log(`内部事件写入成功,已广播状态变化:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1441
|
+
node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1411
1442
|
} catch (err) {
|
|
1412
1443
|
node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
|
|
1413
1444
|
}
|
|
@@ -118,6 +118,12 @@ module.exports = function(RED) {
|
|
|
118
118
|
value: false
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
// 防止LED反馈重复发送:记录最后一次LED反馈的时间戳和值
|
|
122
|
+
node.lastLedFeedback = {
|
|
123
|
+
timestamp: 0,
|
|
124
|
+
value: false
|
|
125
|
+
};
|
|
126
|
+
|
|
121
127
|
// 节点初始化标志(用于静默初始化期间的警告)
|
|
122
128
|
node.isInitializing = true;
|
|
123
129
|
|
|
@@ -469,10 +475,26 @@ module.exports = function(RED) {
|
|
|
469
475
|
return;
|
|
470
476
|
}
|
|
471
477
|
|
|
472
|
-
//
|
|
478
|
+
// 防止重复发送:如果状态相同且时间间隔小于100ms,跳过
|
|
473
479
|
const now = Date.now();
|
|
480
|
+
const timeSinceLastFeedback = now - node.lastLedFeedback.timestamp;
|
|
481
|
+
const stateChanged = (state !== node.lastLedFeedback.value);
|
|
482
|
+
|
|
483
|
+
if (!stateChanged && timeSinceLastFeedback < 100) {
|
|
484
|
+
node.debug(`跳过重复LED反馈(状态未变化,间隔${timeSinceLastFeedback}ms)`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 清理过期队列项(超过3秒)
|
|
474
489
|
node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
|
|
475
490
|
|
|
491
|
+
// 检查队列中是否已有相同状态的反馈(去重)
|
|
492
|
+
const hasSameState = node.ledFeedbackQueue.some(item => item.state === state);
|
|
493
|
+
if (hasSameState) {
|
|
494
|
+
node.debug(`队列中已有相同状态的LED反馈,跳过添加`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
476
498
|
// 加入LED反馈队列(带时间戳)
|
|
477
499
|
// 注意:这里不指定协议类型,在发送时根据情况选择
|
|
478
500
|
node.ledFeedbackQueue.push({ state, timestamp: now });
|
|
@@ -481,40 +503,36 @@ module.exports = function(RED) {
|
|
|
481
503
|
node.processLedFeedbackQueue();
|
|
482
504
|
};
|
|
483
505
|
|
|
484
|
-
// 处理LED
|
|
506
|
+
// 处理LED反馈队列(20ms间隔串行发送)
|
|
485
507
|
node.processLedFeedbackQueue = async function() {
|
|
486
508
|
if (node.isProcessingLedFeedback || node.ledFeedbackQueue.length === 0) {
|
|
487
509
|
return;
|
|
488
510
|
}
|
|
489
|
-
|
|
511
|
+
|
|
490
512
|
// 初始化期间不处理LED反馈(避免部署时大量LED同时发送)
|
|
491
513
|
if (node.isInitializing) {
|
|
492
514
|
return;
|
|
493
515
|
}
|
|
494
|
-
|
|
516
|
+
|
|
495
517
|
node.isProcessingLedFeedback = true;
|
|
496
|
-
|
|
518
|
+
|
|
497
519
|
// 清理过期队列项
|
|
498
520
|
const now = Date.now();
|
|
499
521
|
node.ledFeedbackQueue = node.ledFeedbackQueue.filter(item => (now - item.timestamp) < node.queueTimeout);
|
|
500
|
-
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (fixedDelay > 0) {
|
|
506
|
-
await new Promise(resolve => setTimeout(resolve, fixedDelay));
|
|
507
|
-
}
|
|
508
|
-
|
|
522
|
+
|
|
523
|
+
// 记录队列开始处理时间
|
|
524
|
+
const queueStartTime = Date.now();
|
|
525
|
+
const queueLength = node.ledFeedbackQueue.length;
|
|
526
|
+
|
|
509
527
|
while (node.ledFeedbackQueue.length > 0) {
|
|
510
528
|
const item = node.ledFeedbackQueue.shift();
|
|
511
|
-
|
|
529
|
+
|
|
512
530
|
// 再次检查是否过期
|
|
513
531
|
if (Date.now() - item.timestamp >= node.queueTimeout) {
|
|
514
532
|
node.warn(`丢弃过期LED反馈(${Date.now() - item.timestamp}ms)`);
|
|
515
533
|
continue;
|
|
516
534
|
}
|
|
517
|
-
|
|
535
|
+
|
|
518
536
|
const state = item.state;
|
|
519
537
|
|
|
520
538
|
try {
|
|
@@ -551,6 +569,10 @@ module.exports = function(RED) {
|
|
|
551
569
|
if (err) {
|
|
552
570
|
node.error(`LED反馈失败: ${err.message}`);
|
|
553
571
|
} else {
|
|
572
|
+
// 记录最后一次LED反馈的时间戳和值
|
|
573
|
+
node.lastLedFeedback.timestamp = Date.now();
|
|
574
|
+
node.lastLedFeedback.value = state;
|
|
575
|
+
|
|
554
576
|
// 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
|
|
555
577
|
const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
|
|
556
578
|
node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
|
|
@@ -567,9 +589,16 @@ module.exports = function(RED) {
|
|
|
567
589
|
}
|
|
568
590
|
} catch (err) {
|
|
569
591
|
node.error(`发送LED反馈失败: ${err.message}`);
|
|
592
|
+
// LED反馈失败不中断队列,继续处理下一个
|
|
570
593
|
}
|
|
571
594
|
}
|
|
572
|
-
|
|
595
|
+
|
|
596
|
+
// 队列处理完成,输出统计信息
|
|
597
|
+
const queueDuration = Date.now() - queueStartTime;
|
|
598
|
+
if (queueLength > 1) {
|
|
599
|
+
node.debug(`LED反馈队列处理完成:${queueLength}个任务,耗时${queueDuration}ms`);
|
|
600
|
+
}
|
|
601
|
+
|
|
573
602
|
node.isProcessingLedFeedback = false;
|
|
574
603
|
};
|
|
575
604
|
|
|
@@ -842,35 +871,50 @@ module.exports = function(RED) {
|
|
|
842
871
|
|
|
843
872
|
// 处理输入消息
|
|
844
873
|
node.on('input', function(msg) {
|
|
845
|
-
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
846
|
-
node.warn('MQTT未连接');
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
874
|
let value = false;
|
|
851
|
-
|
|
875
|
+
|
|
852
876
|
// 解析输入值
|
|
853
877
|
if (typeof msg.payload === 'boolean') {
|
|
854
878
|
value = msg.payload;
|
|
855
879
|
} else if (typeof msg.payload === 'string') {
|
|
856
|
-
value = (msg.payload.toLowerCase() === 'on' ||
|
|
857
|
-
msg.payload.toLowerCase() === 'true' ||
|
|
880
|
+
value = (msg.payload.toLowerCase() === 'on' ||
|
|
881
|
+
msg.payload.toLowerCase() === 'true' ||
|
|
858
882
|
msg.payload === '1');
|
|
859
883
|
} else if (typeof msg.payload === 'number') {
|
|
860
884
|
value = (msg.payload !== 0);
|
|
861
885
|
}
|
|
862
|
-
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
886
|
+
|
|
887
|
+
// 更新当前状态
|
|
888
|
+
node.currentState = value;
|
|
889
|
+
|
|
890
|
+
// 输出消息到debug节点(无论是否使用MQTT)
|
|
891
|
+
node.send({
|
|
892
|
+
payload: value,
|
|
893
|
+
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
894
|
+
switchId: node.config.switchId,
|
|
895
|
+
button: node.config.buttonNumber,
|
|
896
|
+
targetSlave: node.config.targetSlaveAddress,
|
|
897
|
+
targetCoil: node.config.targetCoilNumber,
|
|
898
|
+
source: 'input'
|
|
873
899
|
});
|
|
900
|
+
|
|
901
|
+
// 如果启用MQTT,发布命令到MQTT
|
|
902
|
+
if (node.mqttClient && node.mqttClient.connected) {
|
|
903
|
+
const command = value ? 'ON' : 'OFF';
|
|
904
|
+
node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
|
|
905
|
+
if (err) {
|
|
906
|
+
node.error(`发布命令失败: ${err.message}`);
|
|
907
|
+
} else {
|
|
908
|
+
node.log(`发送命令: ${command} 到 ${node.commandTopic}`);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
} else {
|
|
912
|
+
// 本地模式:通过内部事件发送命令
|
|
913
|
+
node.sendMqttCommand(value);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// 更新状态显示
|
|
917
|
+
node.updateStatus();
|
|
874
918
|
});
|
|
875
919
|
|
|
876
920
|
// 节点关闭时清理
|
|
@@ -345,6 +345,15 @@ module.exports = function(RED) {
|
|
|
345
345
|
if (callback) callback(err);
|
|
346
346
|
reject(err);
|
|
347
347
|
} else {
|
|
348
|
+
// 广播发送的数据给所有监听器(包括debug节点)
|
|
349
|
+
node.dataListeners.forEach(listener => {
|
|
350
|
+
try {
|
|
351
|
+
listener(data, 'sent');
|
|
352
|
+
} catch (e) {
|
|
353
|
+
node.error('监听器处理发送数据失败: ' + e.message);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
348
357
|
if (callback) callback(null);
|
|
349
358
|
resolve();
|
|
350
359
|
}
|
|
@@ -370,6 +379,15 @@ module.exports = function(RED) {
|
|
|
370
379
|
if (callback) callback(err);
|
|
371
380
|
reject(err);
|
|
372
381
|
} else {
|
|
382
|
+
// 广播发送的数据给所有监听器(包括debug节点)
|
|
383
|
+
node.dataListeners.forEach(listener => {
|
|
384
|
+
try {
|
|
385
|
+
listener(data, 'sent');
|
|
386
|
+
} catch (e) {
|
|
387
|
+
node.error('监听器处理发送数据失败: ' + e.message);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
373
391
|
if (callback) callback(null);
|
|
374
392
|
resolve();
|
|
375
393
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|