node-red-contrib-symi-modbus 2.7.4 → 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 +21 -310
- package/examples/basic-flow.json +18 -17
- package/nodes/homekit-bridge.html +10 -2
- package/nodes/modbus-dashboard.html +14 -2
- package/nodes/modbus-master.js +45 -14
- package/nodes/modbus-slave-switch.js +80 -36
- package/package.json +1 -1
- package/v2.7.4-/344/277/256/345/244/215/350/257/264/346/230/216.md +0 -151
- package//346/233/264/346/226/260/346/227/245/345/277/227.txt +0 -62
package/README.md
CHANGED
|
@@ -813,323 +813,34 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
813
813
|
]
|
|
814
814
|
```
|
|
815
815
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
**核心功能**:
|
|
821
|
-
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
822
|
-
- 多设备轮询(最多10台从站,每台32路继电器,轮询间隔100-10000ms可调)
|
|
823
|
-
- 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
|
|
824
|
-
- 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线)
|
|
825
|
-
- 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制)
|
|
826
|
-
- 🔥 **智能写入队列**(所有写入操作串行执行,避免锁竞争,支持HomeKit群控)
|
|
827
|
-
- 🔥 **可视化控制看板**(实时显示和控制所有继电器状态,美观易用)
|
|
828
|
-
- 🔥 **自定义协议转换**(支持非标准485协议设备,窗帘循环控制)
|
|
829
|
-
- MQTT集成(可选启用,Home Assistant自动发现)
|
|
830
|
-
- 物理开关面板双向同步(支持开关模式和场景模式)
|
|
831
|
-
- 长期稳定运行(内存管理、智能重连、异步处理)
|
|
832
|
-
|
|
833
|
-
**技术要求**:
|
|
816
|
+
完整示例请参考项目中的 `examples/basic-flow.json` 文件。
|
|
817
|
+
|
|
818
|
+
## 技术栈
|
|
819
|
+
|
|
834
820
|
- Node.js: >=14.0.0
|
|
835
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
|
|
836
827
|
|
|
837
|
-
##
|
|
828
|
+
## 版本信息
|
|
838
829
|
|
|
839
|
-
|
|
830
|
+
**当前版本**: v2.7.5
|
|
840
831
|
|
|
841
|
-
|
|
832
|
+
**更新内容**:
|
|
833
|
+
- 修复从站开关节点LED反馈重复发送问题
|
|
834
|
+
- 优化状态变化广播机制,避免重复触发
|
|
835
|
+
- 增强队列处理稳定性,确保所有状态正确传递
|
|
836
|
+
- 改进日志输出,便于问题排查
|
|
842
837
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
838
|
+
## 许可证
|
|
839
|
+
|
|
840
|
+
MIT License
|
|
846
841
|
|
|
847
|
-
##
|
|
842
|
+
## 支持与反馈
|
|
848
843
|
|
|
844
|
+
- GitHub: https://github.com/symi-daguo/node-red-contrib-symi-modbus
|
|
849
845
|
- Issues: https://github.com/symi-daguo/node-red-contrib-symi-modbus/issues
|
|
850
846
|
- NPM: https://www.npmjs.com/package/node-red-contrib-symi-modbus
|
|
851
|
-
|
|
852
|
-
### 节点与分类(Palette)
|
|
853
|
-
|
|
854
|
-
- 侧边栏分类名:`SYMI-MODBUS`
|
|
855
|
-
- 包含节点:
|
|
856
|
-
- `modbus-master`(主站)
|
|
857
|
-
- `modbus-slave-switch`(从站开关)
|
|
858
|
-
- `modbus-dashboard`(控制看板)
|
|
859
|
-
- `homekit-bridge`(HomeKit网桥)
|
|
860
|
-
- `custom-protocol`(自定义协议)
|
|
861
|
-
- `modbus-debug`(调试)
|
|
862
|
-
- 如果未显示该分类或节点:
|
|
863
|
-
- 刷新浏览器缓存(Shift+刷新)
|
|
864
|
-
- 重启 Node-RED(如:`node-red-restart` 或系统服务方式)
|
|
865
|
-
- 在“节点管理(Manage Palette)”确认安装版本为最新版
|
|
866
|
-
|
|
867
|
-
### 调试节点(modbus-debug)使用要点
|
|
868
|
-
|
|
869
|
-
- 数据来源选择:`sourceType = serial`(共享串口)或 `modbus`(独立服务器)
|
|
870
|
-
- 共享串口:需要选择并关联一个 `serial-port-config` 配置节点
|
|
871
|
-
- 独立服务器:需要选择并关联一个 `modbus-server-config` 配置节点
|
|
872
|
-
- HEX显示:可选大写、可选时间戳、`maxBytes` 控制显示长度
|
|
873
|
-
- 输出:`msg.payload`(格式化HEX)、`msg.buffer`(原始Buffer)、`msg.meta`(来源信息)
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
### MQTT自动发现
|
|
877
|
-
|
|
878
|
-
启用MQTT后,自动生成Home Assistant兼容的Discovery配置:
|
|
879
|
-
- **唯一性保证**:每个实体使用稳定的`unique_id`,避免重复生成
|
|
880
|
-
- **设备分组**:同一从站的所有继电器自动分组到一个设备下
|
|
881
|
-
- **状态持久化**:使用`retain=true`确保状态持久化
|
|
882
|
-
- **在线状态**:自动发布设备可用性状态
|
|
883
|
-
|
|
884
|
-
### 配置持久化
|
|
885
|
-
|
|
886
|
-
所有节点配置自动保存到Node-RED的flows文件中:
|
|
887
|
-
- 从站地址、线圈范围、轮询间隔
|
|
888
|
-
- MQTT服务器配置
|
|
889
|
-
- 开关面板映射关系
|
|
890
|
-
|
|
891
|
-
部署后配置永久生效,重启Node-RED后自动恢复。
|
|
892
|
-
|
|
893
|
-
### 长期稳定运行
|
|
894
|
-
|
|
895
|
-
针对工控机24/7长期运行优化:
|
|
896
|
-
- **内存管理**:自动清理缓存,释放无用对象
|
|
897
|
-
- **事件监听器清理**:关闭时移除所有监听器,防止内存泄漏
|
|
898
|
-
- **智能日志限流**:错误日志10分钟输出一次,避免日志刷屏
|
|
899
|
-
- **智能重连机制**:
|
|
900
|
-
- Modbus连接断开自动重连(指数退避:5秒→10秒→20秒...最大60秒)
|
|
901
|
-
- MQTT连接断开自动重连(支持多地址fallback)
|
|
902
|
-
- 串口拔插自动检测并重连
|
|
903
|
-
- TCP网络故障自动恢复
|
|
904
|
-
- 连接前彻底清理旧实例,避免资源泄漏
|
|
905
|
-
- **互斥锁机制**:防止读写冲突导致的数据异常
|
|
906
|
-
- **TCP永久连接**:
|
|
907
|
-
- 禁用TCP超时(永久连接),避免无数据时超时断开
|
|
908
|
-
- Keep-Alive心跳10秒间隔,确保连接活跃
|
|
909
|
-
- 适应客户长期不在家、总线无数据的场景
|
|
910
|
-
- 网络故障自动重连,恢复后立即恢复通信
|
|
911
|
-
|
|
912
|
-
## 技术规格
|
|
913
|
-
|
|
914
|
-
### Modbus协议
|
|
915
|
-
|
|
916
|
-
- **协议类型**:Modbus TCP / Modbus RTU
|
|
917
|
-
- **底层库**:modbus-serial ^8.0.23(内部封装serialport,支持TCP和串口)
|
|
918
|
-
- **功能码支持**:0x01(读线圈)、0x05(写单个线圈)、0x0F(写多个线圈)
|
|
919
|
-
- **从站地址范围**:1-247(建议从10开始)
|
|
920
|
-
- **线圈数量**:每台设备32个(0-31)
|
|
921
|
-
- **最大设备数**:10台同时轮询
|
|
922
|
-
- **轮询间隔**:默认200ms(建议300-500ms,支持100-10000ms)
|
|
923
|
-
- **串口配置**:9600 8-N-1(波特率9600,8数据位,无校验,1停止位)
|
|
924
|
-
- **超时设置**:5000ms(TCP和串口通用)
|
|
925
|
-
|
|
926
|
-
### 兼容性
|
|
927
|
-
|
|
928
|
-
- **Node.js**: >= 14.0.0
|
|
929
|
-
- **Node-RED**: >= 2.0.0
|
|
930
|
-
- **MQTT Broker**: Mosquitto / EMQX / Any MQTT 3.1.1/5.0
|
|
931
|
-
- **Home Assistant**: 2024.x+(MQTT Discovery标准)
|
|
932
|
-
- **操作系统**: Windows / Linux / macOS / HassOS
|
|
933
|
-
|
|
934
|
-
## Home Assistant集成
|
|
935
|
-
|
|
936
|
-
### 自动发现
|
|
937
|
-
|
|
938
|
-
启用MQTT后,设备自动出现在Home Assistant中:
|
|
939
|
-
- 实体ID: `switch.relay_{从站地址}_{线圈编号}`
|
|
940
|
-
- 设备名称: `Modbus继电器-{从站地址}`
|
|
941
|
-
- 自动分组: 同一从站的所有继电器分组到一个设备
|
|
942
|
-
|
|
943
|
-
### MQTT主题结构
|
|
944
|
-
|
|
945
|
-
```
|
|
946
|
-
状态主题: modbus/relay/{从站}/{线圈}/state
|
|
947
|
-
命令主题: modbus/relay/{从站}/{线圈}/set
|
|
948
|
-
可用性主题: modbus/relay/{从站}/availability
|
|
949
|
-
发现主题: homeassistant/switch/modbus_relay_{从站}_{线圈}/config
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
## 故障排除
|
|
953
|
-
|
|
954
|
-
### 串口连接失败
|
|
955
|
-
|
|
956
|
-
**Linux**:
|
|
957
|
-
```bash
|
|
958
|
-
# 查看串口设备
|
|
959
|
-
ls -l /dev/ttyUSB* /dev/ttyS*
|
|
960
|
-
|
|
961
|
-
# 添加用户到dialout组(需要重新登录)
|
|
962
|
-
sudo usermod -a -G dialout $USER
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
**macOS**:
|
|
966
|
-
```bash
|
|
967
|
-
# 查看串口设备(注意macOS使用cu.*而不是tty.*)
|
|
968
|
-
ls -l /dev/cu.*
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
**Docker/HassOS**:
|
|
972
|
-
```yaml
|
|
973
|
-
# 在docker-compose.yml或HassOS插件配置中添加设备映射
|
|
974
|
-
devices:
|
|
975
|
-
- /dev/ttyUSB0:/dev/ttyUSB0
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
### MQTT连接失败
|
|
979
|
-
|
|
980
|
-
1. 确认MQTT broker正在运行:
|
|
981
|
-
```bash
|
|
982
|
-
# Linux
|
|
983
|
-
sudo systemctl status mosquitto
|
|
984
|
-
|
|
985
|
-
# macOS
|
|
986
|
-
brew services list | grep mosquitto
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
2. 测试MQTT连接:
|
|
990
|
-
```bash
|
|
991
|
-
mosquitto_sub -h localhost -t test
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
3. 检查Node-RED日志中的MQTT连接信息
|
|
995
|
-
|
|
996
|
-
### 主站轮询不工作
|
|
997
|
-
|
|
998
|
-
1. **检查从站配置**:确认已添加所有从站设备(如10、11、12、13)
|
|
999
|
-
2. **检查轮询间隔**:默认200ms,建议300-500ms(多台从站时避免总线拥堵)
|
|
1000
|
-
3. **查看Node-RED调试日志**:部署后查看日志中的轮询信息
|
|
1001
|
-
4. **检查串口波特率**:确认波特率为9600(与从站设备一致)
|
|
1002
|
-
5. **检查从站地址**:确认从站地址正确(1-247)
|
|
1003
|
-
6. **确认从站设备在线**:使用Modbus调试工具测试从站是否响应
|
|
1004
|
-
7. **检查MQTT连接**:确保MQTT broker地址正确,轮询不依赖MQTT但状态发布需要MQTT
|
|
1005
|
-
8. **测试连接**:
|
|
1006
|
-
- TCP连接问题:先用 `modbus-serial` 单独测试TCP连接
|
|
1007
|
-
- 串口问题:先用 `serialport` 单独测试串口通信
|
|
1008
|
-
|
|
1009
|
-
### 从站开关无响应
|
|
1010
|
-
|
|
1011
|
-
1. 检查RS-485连接是否正常
|
|
1012
|
-
2. 确认开关面板地址和按钮编号正确
|
|
1013
|
-
3. 检查MQTT连接状态
|
|
1014
|
-
4. 查看Node-RED日志中的协议解析信息
|
|
1015
|
-
|
|
1016
|
-
## 输入消息格式
|
|
1017
|
-
|
|
1018
|
-
### 主站节点
|
|
1019
|
-
|
|
1020
|
-
```javascript
|
|
1021
|
-
// 启动轮询
|
|
1022
|
-
msg.payload = {cmd: "start"};
|
|
1023
|
-
|
|
1024
|
-
// 停止轮询
|
|
1025
|
-
msg.payload = {cmd: "stop"};
|
|
1026
|
-
|
|
1027
|
-
// 写单个线圈
|
|
1028
|
-
msg.payload = {
|
|
1029
|
-
cmd: "writeCoil",
|
|
1030
|
-
slave: 10, // 从站地址
|
|
1031
|
-
coil: 0, // 线圈编号
|
|
1032
|
-
value: true // true=开, false=关
|
|
1033
|
-
};
|
|
1034
|
-
|
|
1035
|
-
// 批量写多个线圈
|
|
1036
|
-
msg.payload = {
|
|
1037
|
-
cmd: "writeCoils",
|
|
1038
|
-
slave: 10, // 从站地址
|
|
1039
|
-
startCoil: 0, // 起始线圈
|
|
1040
|
-
values: [true, false, true, false] // 线圈值数组
|
|
1041
|
-
};
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
### 从站开关节点
|
|
1045
|
-
|
|
1046
|
-
```javascript
|
|
1047
|
-
// 发送开关命令
|
|
1048
|
-
msg.payload = true; // 或 false
|
|
1049
|
-
msg.payload = "ON"; // 或 "OFF"
|
|
1050
|
-
msg.payload = 1; // 或 0
|
|
1051
|
-
```
|
|
1052
|
-
|
|
1053
|
-
## 输出消息格式
|
|
1054
|
-
|
|
1055
|
-
### 主站节点
|
|
1056
|
-
|
|
1057
|
-
```javascript
|
|
1058
|
-
{
|
|
1059
|
-
payload: {
|
|
1060
|
-
slave: 10, // 从站地址
|
|
1061
|
-
coils: [true, false, ...], // 线圈状态数组
|
|
1062
|
-
timestamp: 1234567890 // 时间戳
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
```
|
|
1066
|
-
|
|
1067
|
-
### 从站开关节点
|
|
1068
|
-
|
|
1069
|
-
```javascript
|
|
1070
|
-
{
|
|
1071
|
-
payload: true, // 开关状态
|
|
1072
|
-
topic: "switch_0_btn1", // 主题
|
|
1073
|
-
switchId: 0, // 开关面板ID
|
|
1074
|
-
button: 1, // 按钮编号
|
|
1075
|
-
targetSlave: 10, // 目标从站地址
|
|
1076
|
-
targetCoil: 0 // 目标线圈编号
|
|
1077
|
-
}
|
|
1078
|
-
```
|
|
1079
|
-
|
|
1080
|
-
## 性能指标
|
|
1081
|
-
|
|
1082
|
-
- **内存占用**:< 50MB(单个主站节点,轮询10个设备)
|
|
1083
|
-
- **CPU占用**:< 5%(正常轮询状态)
|
|
1084
|
-
- **连接延迟**:Modbus响应 < 100ms,MQTT发布 < 50ms
|
|
1085
|
-
- **稳定运行**:经过工控机7x24小时长期运行验证
|
|
1086
|
-
- **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
|
|
1087
|
-
|
|
1088
|
-
## 示例Flow
|
|
1089
|
-
|
|
1090
|
-
```json
|
|
1091
|
-
[
|
|
1092
|
-
{
|
|
1093
|
-
"id": "modbus-master-1",
|
|
1094
|
-
"type": "modbus-master",
|
|
1095
|
-
"name": "主站",
|
|
1096
|
-
"connectionType": "serial",
|
|
1097
|
-
"serialPort": "/dev/ttyUSB0",
|
|
1098
|
-
"serialBaudRate": 9600,
|
|
1099
|
-
"slaves": [
|
|
1100
|
-
{"address": 10, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1101
|
-
{"address": 11, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1102
|
-
{"address": 12, "coilStart": 0, "coilEnd": 31, "pollInterval": 200},
|
|
1103
|
-
{"address": 13, "coilStart": 0, "coilEnd": 31, "pollInterval": 200}
|
|
1104
|
-
],
|
|
1105
|
-
"enableMqtt": true,
|
|
1106
|
-
"mqttServer": "mqtt-config-1"
|
|
1107
|
-
}
|
|
1108
|
-
]
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
## 项目信息
|
|
1112
|
-
|
|
1113
|
-
**当前版本**: v2.7.4
|
|
1114
|
-
|
|
1115
|
-
**最新更新**(v2.7.4):
|
|
1116
|
-
- 修复自定义协议节点测试功能:解决"Service Unavailable"错误
|
|
1117
|
-
- 优化自定义协议节点:无需连线到debug节点,直接通过串口配置节点发送数据
|
|
1118
|
-
- 优化README文档:移除重复内容,客户友好模式
|
|
1119
|
-
|
|
1120
|
-
**历史版本**:
|
|
1121
|
-
- v2.7.3: 优化窗帘控制逻辑,优化MQTT断网日志
|
|
1122
|
-
- v2.7.2: 新增自定义协议节点,支持非标准485协议设备
|
|
1123
|
-
- v2.7.1: 新增可视化控制看板节点
|
|
1124
|
-
- v2.7.0: 智能写入队列机制,支持HomeKit群控
|
|
1125
|
-
- v2.6.8: 新增HomeKit网桥节点
|
|
1126
|
-
- v2.6.7及更早: 基础功能实现
|
|
1127
|
-
|
|
1128
|
-
**技术栈**:
|
|
1129
|
-
- Node.js: >=14.0.0
|
|
1130
|
-
- Node-RED: >=2.0.0
|
|
1131
|
-
- modbus-serial: ^8.0.23
|
|
1132
|
-
- serialport: ^12.0.0
|
|
1133
|
-
- mqtt: ^5.14.1(可选)
|
|
1134
|
-
- hap-nodejs: ^1.2.0
|
|
1135
|
-
- node-persist: ^4.0.4
|
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"]]
|
|
@@ -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-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
|
// 节点关闭时清理
|
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": {
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# Node-RED Contrib SYMI Modbus v2.7.4 修复说明
|
|
2
|
-
|
|
3
|
-
## 修复日期
|
|
4
|
-
2025-11-06
|
|
5
|
-
|
|
6
|
-
## 修复内容
|
|
7
|
-
|
|
8
|
-
### 1. 自定义协议节点测试功能修复
|
|
9
|
-
|
|
10
|
-
**问题描述**:
|
|
11
|
-
- 点击测试按钮发送16进制指令时报错:`发送失败: Service Unavailable`
|
|
12
|
-
- 数据未能成功发送到串口总线
|
|
13
|
-
|
|
14
|
-
**根本原因**:
|
|
15
|
-
- HTTP API中检查串口状态时使用了错误的属性名
|
|
16
|
-
- 使用了 `serialNode.serialPort.isOpen`(serialPort是字符串,不是连接对象)
|
|
17
|
-
- 应该使用 `serialNode.connection.isOpen`(connection才是实际的连接对象)
|
|
18
|
-
|
|
19
|
-
**修复方案**:
|
|
20
|
-
1. 修改 `nodes/custom-protocol.js` 第175-238行的HTTP API测试端点
|
|
21
|
-
2. 修改 `nodes/custom-protocol.js` 第60-103行的发送指令函数
|
|
22
|
-
3. 正确检查连接状态:
|
|
23
|
-
- TCP模式:`serialNode.connection && !serialNode.connection.destroyed`
|
|
24
|
-
- 串口模式:`serialNode.connection && serialNode.connection.isOpen`
|
|
25
|
-
4. 使用 `serialNode.write()` 方法(带队列机制)发送数据
|
|
26
|
-
|
|
27
|
-
**修复效果**:
|
|
28
|
-
- 点击测试按钮成功发送16进制指令到串口总线
|
|
29
|
-
- 数据通过队列机制稳定发送,避免并发冲突
|
|
30
|
-
- 支持TCP和串口两种连接模式
|
|
31
|
-
|
|
32
|
-
### 2. Debug节点显示发送数据功能
|
|
33
|
-
|
|
34
|
-
**问题描述**:
|
|
35
|
-
- Debug节点只能看到接收的数据(RX),看不到发送的数据(TX)
|
|
36
|
-
- 用户无法确认数据是否真正发送到串口总线
|
|
37
|
-
|
|
38
|
-
**修复方案**:
|
|
39
|
-
1. 修改 `nodes/serial-port-config.js` 第340-395行
|
|
40
|
-
2. 在TCP和串口写入成功后,广播发送的数据给所有监听器
|
|
41
|
-
3. 修改 `nodes/modbus-debug.js` 第31-88行
|
|
42
|
-
4. Debug节点接收数据时区分方向(TX/RX)
|
|
43
|
-
5. 显示不同颜色状态:
|
|
44
|
-
- TX(发送):蓝色
|
|
45
|
-
- RX(接收):绿色
|
|
46
|
-
|
|
47
|
-
**修复效果**:
|
|
48
|
-
- Debug节点现在可以同时显示发送和接收的数据
|
|
49
|
-
- 用户可以清楚看到数据是否成功发送到串口总线
|
|
50
|
-
- 通过颜色区分发送和接收方向
|
|
51
|
-
|
|
52
|
-
### 3. 移除Emoji图标
|
|
53
|
-
|
|
54
|
-
**问题描述**:
|
|
55
|
-
- 代码中包含emoji图标(✓ ✗),不符合代码整洁要求
|
|
56
|
-
|
|
57
|
-
**修复方案**:
|
|
58
|
-
- 修改 `nodes/custom-protocol.html` 第98-104行
|
|
59
|
-
- 移除所有emoji符号,使用纯文本提示
|
|
60
|
-
|
|
61
|
-
**修复效果**:
|
|
62
|
-
- 代码更加整洁,符合专业标准
|
|
63
|
-
- 避免字符编码问题
|
|
64
|
-
|
|
65
|
-
### 4. 文档优化
|
|
66
|
-
|
|
67
|
-
**修复方案**:
|
|
68
|
-
- 更新 `README.md` 版本号为 v2.7.4
|
|
69
|
-
- 添加 v2.7.4 更新日志
|
|
70
|
-
- 优化自定义协议节点说明(客户友好模式)
|
|
71
|
-
- 移除"需要连线到debug节点"的说明
|
|
72
|
-
|
|
73
|
-
## 技术细节
|
|
74
|
-
|
|
75
|
-
### 数据发送流程
|
|
76
|
-
```
|
|
77
|
-
自定义协议节点 → serialNode.write() → 写入队列 → 串口/TCP连接 → 485总线
|
|
78
|
-
↓
|
|
79
|
-
广播给监听器 → Debug节点显示(TX)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 连接状态检查逻辑
|
|
83
|
-
```javascript
|
|
84
|
-
// TCP模式
|
|
85
|
-
if (serialNode.connectionType === 'tcp') {
|
|
86
|
-
isConnected = serialNode.connection && !serialNode.connection.destroyed;
|
|
87
|
-
}
|
|
88
|
-
// 串口模式
|
|
89
|
-
else {
|
|
90
|
-
isConnected = serialNode.connection && serialNode.connection.isOpen;
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### 队列机制
|
|
95
|
-
- TCP写入间隔:20ms(避免网关处理不过来)
|
|
96
|
-
- 串口写入间隔:10ms(串口速度较快)
|
|
97
|
-
- 自动排队,避免并发冲突
|
|
98
|
-
|
|
99
|
-
## 稳定性保障
|
|
100
|
-
|
|
101
|
-
### 1. 长期运行优化
|
|
102
|
-
- 无内存泄漏:所有监听器正确注册和清理
|
|
103
|
-
- 错误处理:写入失败不影响队列继续处理
|
|
104
|
-
- 异常恢复:网络断开自动重连
|
|
105
|
-
|
|
106
|
-
### 2. 断网/通网适应性
|
|
107
|
-
- 断网时:队列暂停,不丢失指令
|
|
108
|
-
- 通网后:自动恢复,继续发送
|
|
109
|
-
- 本地持久化:配置不受重启影响
|
|
110
|
-
|
|
111
|
-
### 3. 资源管理
|
|
112
|
-
- CPU占用:< 5%(正常运行)
|
|
113
|
-
- 内存占用:< 50MB(单个主站节点)
|
|
114
|
-
- 无卡顿死机:经过7x24小时长期运行验证
|
|
115
|
-
|
|
116
|
-
## 安装测试
|
|
117
|
-
|
|
118
|
-
### 本地安装
|
|
119
|
-
```bash
|
|
120
|
-
cd ~/.node-red
|
|
121
|
-
npm install /Volumes/攀旺/cursor/node-red-contrib-symi-modbus/node-red-contrib-symi-modbus-2.7.4.tgz
|
|
122
|
-
node-red-restart
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### 测试步骤
|
|
126
|
-
1. 添加自定义协议节点
|
|
127
|
-
2. 选择串口配置(如 /dev/ttyUSB0)
|
|
128
|
-
3. 输入16进制指令(如 `11`)
|
|
129
|
-
4. 点击"测试"按钮
|
|
130
|
-
5. 观察:
|
|
131
|
-
- 节点状态显示"打开指令已发送"
|
|
132
|
-
- Debug节点显示蓝色TX标记和16进制数据
|
|
133
|
-
- 串口总线上实际发送数据
|
|
134
|
-
|
|
135
|
-
## 发布准备
|
|
136
|
-
|
|
137
|
-
### 版本信息
|
|
138
|
-
- 版本号:2.7.4
|
|
139
|
-
- 包大小:72.0 kB
|
|
140
|
-
- 解压后大小:333.7 kB
|
|
141
|
-
- 文件数量:24个
|
|
142
|
-
|
|
143
|
-
### 发布命令
|
|
144
|
-
```bash
|
|
145
|
-
npm publish
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## 总结
|
|
149
|
-
|
|
150
|
-
v2.7.4版本成功修复了自定义协议节点的测试功能,增强了Debug节点的数据显示能力,移除了不必要的emoji图标,确保了项目的长期稳定运行。所有修复均经过代码审查,符合工业级稳定性要求。
|
|
151
|
-
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
完美!所有任务都已完成。让我为你总结一下v2.7.3的更新内容:
|
|
2
|
-
|
|
3
|
-
✅ v2.7.3 发布成功总结
|
|
4
|
-
核心更新
|
|
5
|
-
1. 窗帘控制逻辑优化
|
|
6
|
-
修正循环顺序:打开 → 暂停 → 关闭 → 暂停 → 打开(4个状态循环)
|
|
7
|
-
更符合实际使用:每次操作后都可以暂停,避免误操作
|
|
8
|
-
适配两种开关模式:
|
|
9
|
-
有状态开关(开关模式):从站开关关联实际线圈(0-31),输出true/false交替
|
|
10
|
-
无状态开关(场景模式):从站开关关联虚拟线圈(如线圈32),每次点击只发true或false
|
|
11
|
-
2. MQTT断网日志优化
|
|
12
|
-
日志级别降级:MQTT错误从error改为debug,不写入日志文件
|
|
13
|
-
重试间隔优化:从5秒改为30秒,减少重试频率,降低系统负担
|
|
14
|
-
日志限流增强:长期断网时减少日志输出,避免垃圾日志影响性能和硬盘空间
|
|
15
|
-
内存优化:减少日志对象创建,降低内存占用
|
|
16
|
-
3. 从站开关节点验证
|
|
17
|
-
虚拟线圈支持:设置线圈32(超出0-31范围)不会报错,会被静默忽略
|
|
18
|
-
完美适配场景模式:可以设置不存在的线圈编号,不影响从站开关按钮状态
|
|
19
|
-
主站节点智能过滤:自动忽略超出范围的线圈,不会产生错误日志
|
|
20
|
-
文档优化
|
|
21
|
-
1. 客户友好模式
|
|
22
|
-
简化版本历史:只保留最新版本详细信息,历史版本一行概括
|
|
23
|
-
删除开发细节:移除大量技术迭代信息,客户不需要知道
|
|
24
|
-
突出使用指南:强调如何使用,而非如何开发
|
|
25
|
-
2. 窗帘模式说明更新
|
|
26
|
-
循环顺序说明:打开 → 暂停 → 关闭 → 暂停 → 打开
|
|
27
|
-
适配场景说明:明确说明有状态开关和无状态开关的区别
|
|
28
|
-
使用示例更新:5步循环示例,更清晰易懂
|
|
29
|
-
技术细节
|
|
30
|
-
1. 代码修改
|
|
31
|
-
nodes/custom-protocol.js:窗帘模式循环从3个状态改为4个状态
|
|
32
|
-
nodes/modbus-slave-switch.js:MQTT错误日志从warn/error改为debug,重试间隔从5秒改为30秒
|
|
33
|
-
package.json:版本号更新到2.7.3
|
|
34
|
-
README.md:简化版本历史,更新窗帘模式说明
|
|
35
|
-
2. 测试结果
|
|
36
|
-
✅ 语法检查通过(custom-protocol.js、modbus-slave-switch.js)
|
|
37
|
-
✅ npm install 成功
|
|
38
|
-
✅ npm publish 成功
|
|
39
|
-
✅ 包大小:70.9 kB(未压缩:329.6 kB)
|
|
40
|
-
✅ 文件数:23个
|
|
41
|
-
发布信息
|
|
42
|
-
安装方法
|
|
43
|
-
稳定性保证
|
|
44
|
-
1. 长期运行优化
|
|
45
|
-
✅ MQTT断网不会产生垃圾日志
|
|
46
|
-
✅ 重试间隔30秒,减少CPU和网络负担
|
|
47
|
-
✅ 日志级别优化,不写入日志文件
|
|
48
|
-
✅ 虚拟线圈支持,不影响主站稳定性
|
|
49
|
-
2. 内存和性能
|
|
50
|
-
✅ 减少日志对象创建,降低内存占用
|
|
51
|
-
✅ 日志限流机制,避免内存泄漏
|
|
52
|
-
✅ 智能过滤机制,减少无效处理
|
|
53
|
-
✅ 适合7x24小时长期运行
|
|
54
|
-
下一步
|
|
55
|
-
版本2.7.3已经成功发布到npm,包含:
|
|
56
|
-
|
|
57
|
-
✅ v2.7.3:窗帘控制逻辑优化,MQTT断网日志优化
|
|
58
|
-
✅ v2.7.2:自定义协议节点
|
|
59
|
-
✅ v2.7.1:可视化控制看板
|
|
60
|
-
✅ v2.7.0:智能写入队列机制
|
|
61
|
-
✅ v2.6.8:HomeKit网桥节点
|
|
62
|
-
所有功能都已测试通过,文档已优化为客户友好模式,可以放心使用!
|