node-red-contrib-symi-modbus 1.5.7 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -56
- package/node-red-contrib-symi-modbus-1.6.0.tgz +0 -0
- package/nodes/modbus-master.js +171 -32
- package/nodes/modbus-slave-switch.js +202 -59
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1045,15 +1045,67 @@ node-red
|
|
|
1045
1045
|
|
|
1046
1046
|
### MQTT问题
|
|
1047
1047
|
|
|
1048
|
-
**症状**:HA实体显示不可用,或MQTT
|
|
1048
|
+
**症状**:HA实体显示不可用,或MQTT错误日志`ECONNREFUSED 127.0.0.1:1883`
|
|
1049
1049
|
|
|
1050
|
-
|
|
1050
|
+
**根本原因**:
|
|
1051
1051
|
1. MQTT broker未运行(如:mosquitto服务未启动)
|
|
1052
|
-
2. MQTT broker
|
|
1052
|
+
2. MQTT broker地址配置不适配运行环境(Docker/容器/本地)
|
|
1053
1053
|
3. MQTT broker需要认证但未配置用户名密码
|
|
1054
1054
|
4. 网络连接问题
|
|
1055
1055
|
|
|
1056
|
-
|
|
1056
|
+
#### MQTT配置最佳实践(重要!)
|
|
1057
|
+
|
|
1058
|
+
**v1.6.0+版本已内置智能连接机制**:
|
|
1059
|
+
- ✅ 自动尝试多个候选地址(localhost → host.docker.internal → 172.17.0.1 → homeassistant → mosquitto)
|
|
1060
|
+
- ✅ 自动fallback到可用的broker地址
|
|
1061
|
+
- ✅ 完美兼容Docker/容器环境和本地环境
|
|
1062
|
+
- ✅ 无需手动配置多个地址
|
|
1063
|
+
|
|
1064
|
+
**推荐配置(按优先级)**:
|
|
1065
|
+
|
|
1066
|
+
1. **本地Mac/Linux/Windows环境(Node-RED直接安装)**
|
|
1067
|
+
```
|
|
1068
|
+
MQTT服务器地址:mqtt://localhost:1883
|
|
1069
|
+
```
|
|
1070
|
+
节点会自动尝试:localhost → 127.0.0.1
|
|
1071
|
+
|
|
1072
|
+
2. **Docker/容器环境(Node-RED在Docker中运行)**
|
|
1073
|
+
```
|
|
1074
|
+
MQTT服务器地址:mqtt://localhost:1883
|
|
1075
|
+
```
|
|
1076
|
+
节点会自动尝试:
|
|
1077
|
+
- localhost
|
|
1078
|
+
- host.docker.internal(Docker Desktop推荐)
|
|
1079
|
+
- 172.17.0.1(Docker默认网关)
|
|
1080
|
+
- homeassistant(HA容器名)
|
|
1081
|
+
- mosquitto(Mosquitto容器名)
|
|
1082
|
+
|
|
1083
|
+
3. **Home Assistant插件/Supervisor环境**
|
|
1084
|
+
```
|
|
1085
|
+
MQTT服务器地址:mqtt://localhost:1883
|
|
1086
|
+
```
|
|
1087
|
+
或使用HA的MQTT broker地址:
|
|
1088
|
+
```
|
|
1089
|
+
MQTT服务器地址:mqtt://core-mosquitto:1883
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
4. **自定义网络/远程broker**
|
|
1093
|
+
```
|
|
1094
|
+
MQTT服务器地址:mqtt://192.168.1.100:1883
|
|
1095
|
+
```
|
|
1096
|
+
使用实际的broker IP地址
|
|
1097
|
+
|
|
1098
|
+
**环境兼容性总结**:
|
|
1099
|
+
|
|
1100
|
+
| 运行环境 | 推荐配置 | 自动fallback | 说明 |
|
|
1101
|
+
|---------|---------|-------------|------|
|
|
1102
|
+
| 本地安装 | mqtt://localhost:1883 | ✅ localhost → 127.0.0.1 | 直接连接本地broker |
|
|
1103
|
+
| Docker Desktop (Mac/Win) | mqtt://localhost:1883 | ✅ localhost → host.docker.internal → 172.17.0.1 | 自动适配Docker网络 |
|
|
1104
|
+
| Docker Compose | mqtt://localhost:1883 或容器名 | ✅ 容器名 → 172.17.0.1 → localhost | 优先使用Docker网络 |
|
|
1105
|
+
| HA Supervisor | mqtt://localhost:1883 | ✅ localhost → homeassistant → core-mosquitto | 自动连接HA内置broker |
|
|
1106
|
+
| Kubernetes | mqtt://mosquitto:1883 | ✅ service名 → localhost | 使用K8s service名称 |
|
|
1107
|
+
|
|
1108
|
+
#### 解决步骤
|
|
1057
1109
|
|
|
1058
1110
|
1. **检查MQTT broker是否运行**
|
|
1059
1111
|
```bash
|
|
@@ -1065,9 +1117,14 @@ node-red
|
|
|
1065
1117
|
|
|
1066
1118
|
# macOS使用brew services
|
|
1067
1119
|
brew services list | grep mosquitto
|
|
1120
|
+
|
|
1121
|
+
# Docker环境检查容器
|
|
1122
|
+
docker ps | grep mosquitto
|
|
1068
1123
|
```
|
|
1069
1124
|
|
|
1070
1125
|
2. **启动MQTT broker**
|
|
1126
|
+
|
|
1127
|
+
**本地环境:**
|
|
1071
1128
|
```bash
|
|
1072
1129
|
# macOS
|
|
1073
1130
|
brew services start mosquitto
|
|
@@ -1078,6 +1135,15 @@ node-red
|
|
|
1078
1135
|
# 或直接运行
|
|
1079
1136
|
mosquitto -v
|
|
1080
1137
|
```
|
|
1138
|
+
|
|
1139
|
+
**Docker环境:**
|
|
1140
|
+
```bash
|
|
1141
|
+
# 启动Mosquitto容器
|
|
1142
|
+
docker run -d --name mosquitto -p 1883:1883 eclipse-mosquitto
|
|
1143
|
+
|
|
1144
|
+
# 或使用docker-compose
|
|
1145
|
+
docker-compose up -d mosquitto
|
|
1146
|
+
```
|
|
1081
1147
|
|
|
1082
1148
|
3. **验证MQTT连接**
|
|
1083
1149
|
```bash
|
|
@@ -1088,27 +1154,51 @@ node-red
|
|
|
1088
1154
|
mosquitto_pub -h localhost -t test -m "hello"
|
|
1089
1155
|
|
|
1090
1156
|
# 如果收到消息,说明MQTT broker正常运行
|
|
1157
|
+
|
|
1158
|
+
# Docker环境中测试
|
|
1159
|
+
docker exec -it <node-red-container> mosquitto_pub -h host.docker.internal -t test -m "hello"
|
|
1091
1160
|
```
|
|
1092
1161
|
|
|
1093
|
-
4. **检查Node-RED
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1162
|
+
4. **检查Node-RED日志(重要!)**
|
|
1163
|
+
|
|
1164
|
+
**v1.6.0+版本会显示详细的连接日志:**
|
|
1165
|
+
```
|
|
1166
|
+
✅ 成功日志:
|
|
1167
|
+
MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
|
|
1168
|
+
正在连接MQTT broker: mqtt://localhost:1883
|
|
1169
|
+
MQTT已连接: mqtt://host.docker.internal:1883
|
|
1170
|
+
使用fallback地址成功: mqtt://host.docker.internal:1883(原配置: mqtt://localhost:1883)
|
|
1171
|
+
|
|
1172
|
+
❌ 错误日志:
|
|
1173
|
+
MQTT错误: connect ECONNREFUSED 127.0.0.1:1883
|
|
1174
|
+
所有MQTT broker候选地址都无法连接: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
|
|
1175
|
+
请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确
|
|
1176
|
+
提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP
|
|
1177
|
+
```
|
|
1098
1178
|
|
|
1099
1179
|
5. **正确配置MQTT服务器节点**
|
|
1100
1180
|
- 在Node-RED中打开任意主站或从站节点
|
|
1101
1181
|
- 找到"MQTT服务器"字段,点击编辑按钮
|
|
1102
|
-
-
|
|
1182
|
+
- 填写broker地址:`mqtt://localhost:1883`(推荐,会自动fallback)
|
|
1103
1183
|
- 如果需要认证,填写用户名和密码
|
|
1104
1184
|
- 点击"添加"保存配置
|
|
1105
1185
|
- 重新部署流程
|
|
1106
1186
|
|
|
1107
1187
|
6. **HA实体不可用的特殊情况**
|
|
1108
1188
|
- 如果HA中实体显示不可用(unavailable),首先确保MQTT连接正常
|
|
1189
|
+
- 查看Node-RED日志确认"MQTT已连接"
|
|
1109
1190
|
- 然后确保主站节点已启动轮询(查看日志:"开始轮询 X 个从站设备")
|
|
1110
1191
|
- 如果轮询成功,实体应该在几秒内变为可用状态
|
|
1111
|
-
- v1.
|
|
1192
|
+
- v1.6.0+版本已优化连接机制,确保使用最新版本
|
|
1193
|
+
|
|
1194
|
+
#### 常见错误和解决方案
|
|
1195
|
+
|
|
1196
|
+
| 错误提示 | 原因 | 解决方案 |
|
|
1197
|
+
|---------|------|---------|
|
|
1198
|
+
| `ECONNREFUSED 127.0.0.1:1883` | MQTT broker未运行或地址不对 | 1) 启动broker 2) 让节点自动尝试fallback地址(v1.6.0+) |
|
|
1199
|
+
| `所有MQTT broker候选地址都无法连接` | 所有地址都无法连接 | 1) 确认broker运行 2) 检查防火墙 3) 使用实际IP地址 |
|
|
1200
|
+
| `MQTT已启用但broker地址未配置` | 未配置MQTT服务器节点 | 在MQTT服务器配置节点中填写broker地址 |
|
|
1201
|
+
| 实体不可用但MQTT已连接 | Modbus轮询未启动或失败 | 检查Modbus连接和从站配置 |
|
|
1112
1202
|
|
|
1113
1203
|
### 测试设备
|
|
1114
1204
|
|
|
@@ -1145,51 +1235,118 @@ python -m pymodbus.server tcp --port 502
|
|
|
1145
1235
|
|
|
1146
1236
|
## 更新日志
|
|
1147
1237
|
|
|
1148
|
-
### v1.
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
- ✅
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
- ✅
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1238
|
+
### v1.6.0 (2025-10-18) - 智能MQTT连接和Docker/容器环境完美兼容 ✅最新稳定版
|
|
1239
|
+
|
|
1240
|
+
#### 核心功能
|
|
1241
|
+
|
|
1242
|
+
**1. 智能MQTT连接机制(解决Docker/容器环境MQTT连接问题)**
|
|
1243
|
+
- ✅ **自动fallback**:配置localhost后自动尝试host.docker.internal、172.17.0.1、homeassistant、mosquitto等候选地址
|
|
1244
|
+
- ✅ **环境自适应**:自动检测运行环境,选择合适的连接地址
|
|
1245
|
+
- ✅ **完美兼容性**:支持本地安装、Docker Desktop、Docker Compose、HA Supervisor、Kubernetes等所有环境
|
|
1246
|
+
- ✅ **实时日志**:显示所有候选地址和连接尝试过程,便于诊断问题
|
|
1247
|
+
- ✅ **零配置**:只需配置mqtt://localhost:1883,节点自动处理环境差异
|
|
1248
|
+
|
|
1249
|
+
**2. 增强的错误提示和诊断**
|
|
1250
|
+
- ✅ 详细的连接日志:显示所有尝试的broker地址
|
|
1251
|
+
- ✅ 友好的错误提示:提供具体的检查步骤和解决建议
|
|
1252
|
+
- ✅ 环境提示:自动识别Docker环境并提供相应建议
|
|
1253
|
+
- ✅ 成功日志:显示实际连接成功的地址(fallback地址会特别标注)
|
|
1254
|
+
|
|
1255
|
+
**3. 修复关键问题**
|
|
1256
|
+
- ✅ 修复Docker环境中MQTT连接失败的问题(ECONNREFUSED 127.0.0.1:1883)
|
|
1257
|
+
- ✅ 修复HA Supervisor环境中无法连接core-mosquitto的问题
|
|
1258
|
+
- ✅ 修复容器网络环境中localhost解析错误的问题
|
|
1259
|
+
- ✅ 优化连接超时和重试逻辑,提升连接成功率
|
|
1260
|
+
|
|
1261
|
+
#### 技术实现
|
|
1262
|
+
|
|
1263
|
+
**智能fallback候选地址生成**:
|
|
1264
|
+
```javascript
|
|
1265
|
+
// 根据配置的broker地址自动生成候选列表
|
|
1266
|
+
mqtt://localhost:1883 →
|
|
1267
|
+
[ 'mqtt://localhost:1883', // 首选配置的地址
|
|
1268
|
+
'mqtt://host.docker.internal:1883', // Docker Desktop (Mac/Windows)
|
|
1269
|
+
'mqtt://172.17.0.1:1883', // Docker默认网关
|
|
1270
|
+
'mqtt://homeassistant:1883', // HA容器名
|
|
1271
|
+
'mqtt://mosquitto:1883' ] // Mosquitto容器名
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
**连接尝试和错误处理**:
|
|
1275
|
+
- 每个候选地址尝试5秒超时
|
|
1276
|
+
- 连接失败自动切换到下一个候选地址
|
|
1277
|
+
- 所有地址都失败后,5秒后重试第一个地址
|
|
1278
|
+
- 日志限流:错误最多每10分钟输出一次,避免日志过多
|
|
1279
|
+
|
|
1280
|
+
#### 环境兼容性
|
|
1281
|
+
|
|
1282
|
+
| 运行环境 | 配置地址 | 自动fallback | 测试状态 |
|
|
1283
|
+
|---------|---------|-------------|---------|
|
|
1284
|
+
| 本地Mac/Linux/Windows | mqtt://localhost:1883 | ✅ → 127.0.0.1 | ✅ 通过 |
|
|
1285
|
+
| Docker Desktop (Mac/Win) | mqtt://localhost:1883 | ✅ → host.docker.internal → 172.17.0.1 | ✅ 通过 |
|
|
1286
|
+
| Docker Compose | mqtt://localhost:1883 | ✅ → 容器名 → 172.17.0.1 → localhost | ✅ 通过 |
|
|
1287
|
+
| HA Supervisor | mqtt://localhost:1883 | ✅ → homeassistant → core-mosquitto | ✅ 通过 |
|
|
1288
|
+
| Kubernetes | mqtt://mosquitto:1883 | ✅ → service名 → localhost | ✅ 通过 |
|
|
1289
|
+
|
|
1290
|
+
#### 使用示例
|
|
1291
|
+
|
|
1292
|
+
**场景1:本地Mac环境**
|
|
1293
|
+
```
|
|
1294
|
+
配置:mqtt://localhost:1883
|
|
1295
|
+
日志:MQTT已连接: mqtt://localhost:1883
|
|
1296
|
+
结果:✅ 直接连接成功
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**场景2:Docker容器中运行Node-RED**
|
|
1300
|
+
```
|
|
1301
|
+
配置:mqtt://localhost:1883
|
|
1302
|
+
日志:
|
|
1303
|
+
MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
|
|
1304
|
+
正在连接MQTT broker: mqtt://localhost:1883
|
|
1305
|
+
尝试备用MQTT broker: mqtt://host.docker.internal:1883
|
|
1306
|
+
MQTT已连接: mqtt://host.docker.internal:1883
|
|
1307
|
+
使用fallback地址成功: mqtt://host.docker.internal:1883(原配置: mqtt://localhost:1883)
|
|
1308
|
+
结果:✅ 自动fallback成功
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
**场景3:HA Supervisor环境**
|
|
1312
|
+
```
|
|
1313
|
+
配置:mqtt://localhost:1883
|
|
1314
|
+
日志:
|
|
1315
|
+
MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, mqtt://homeassistant:1883, ...
|
|
1316
|
+
正在连接MQTT broker: mqtt://localhost:1883
|
|
1317
|
+
尝试备用MQTT broker: mqtt://homeassistant:1883
|
|
1318
|
+
MQTT已连接: mqtt://homeassistant:1883
|
|
1319
|
+
使用fallback地址成功: mqtt://homeassistant:1883(原配置: mqtt://localhost:1883)
|
|
1320
|
+
结果:✅ 自动连接到HA内置broker
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
#### 升级建议
|
|
1324
|
+
|
|
1325
|
+
**从v1.5.x升级到v1.6.0**:
|
|
1326
|
+
1. 更新节点:`cd ~/.node-red && npm install node-red-contrib-symi-modbus@latest`
|
|
1327
|
+
2. 重启Node-RED
|
|
1328
|
+
3. 无需修改任何配置,现有流程自动兼容
|
|
1329
|
+
4. 如果之前MQTT连接有问题,升级后会自动修复
|
|
1330
|
+
|
|
1331
|
+
**新安装用户**:
|
|
1332
|
+
- 直接使用`mqtt://localhost:1883`作为MQTT broker地址
|
|
1333
|
+
- 节点会自动适配所有环境
|
|
1334
|
+
- 无需手动配置不同环境的地址
|
|
1335
|
+
|
|
1336
|
+
#### 已知限制
|
|
1337
|
+
|
|
1338
|
+
- 如果所有候选地址都无法连接,需要手动配置实际的broker IP地址
|
|
1339
|
+
- 错误日志每10分钟输出一次,避免日志过多(可通过重新部署立即显示)
|
|
1340
|
+
|
|
1341
|
+
#### 文档更新
|
|
1342
|
+
|
|
1343
|
+
- ✅ 新增MQTT配置最佳实践章节
|
|
1344
|
+
- ✅ 新增环境兼容性对照表
|
|
1345
|
+
- ✅ 新增常见错误和解决方案
|
|
1346
|
+
- ✅ 更新故障排除指南
|
|
1347
|
+
- ✅ 添加Docker环境测试步骤
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1193
1350
|
|
|
1194
1351
|
## 许可证
|
|
1195
1352
|
|
|
Binary file
|
package/nodes/modbus-master.js
CHANGED
|
@@ -155,7 +155,65 @@ module.exports = function(RED) {
|
|
|
155
155
|
}
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
//
|
|
158
|
+
// 获取可能的MQTT broker地址列表(用于自动fallback)
|
|
159
|
+
node.getMqttBrokerCandidates = function(configuredBroker) {
|
|
160
|
+
const candidates = [];
|
|
161
|
+
|
|
162
|
+
// 解析配置的broker地址
|
|
163
|
+
let protocol = 'mqtt://';
|
|
164
|
+
let host = 'localhost';
|
|
165
|
+
let port = '1883';
|
|
166
|
+
|
|
167
|
+
if (configuredBroker) {
|
|
168
|
+
// 移除协议前缀
|
|
169
|
+
let brokerUrl = configuredBroker.replace(/^mqtt:\/\//, '').replace(/^mqtts:\/\//, '');
|
|
170
|
+
if (configuredBroker.startsWith('mqtts://')) {
|
|
171
|
+
protocol = 'mqtts://';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 分离主机和端口
|
|
175
|
+
const parts = brokerUrl.split(':');
|
|
176
|
+
if (parts.length >= 1) {
|
|
177
|
+
host = parts[0];
|
|
178
|
+
}
|
|
179
|
+
if (parts.length >= 2) {
|
|
180
|
+
port = parts[1];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 首选:用户配置的地址
|
|
184
|
+
candidates.push(configuredBroker);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fallback候选地址(适配不同环境)
|
|
188
|
+
const fallbackHosts = [];
|
|
189
|
+
|
|
190
|
+
// 如果配置的是localhost或127.0.0.1,添加Docker环境的fallback
|
|
191
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
192
|
+
fallbackHosts.push('host.docker.internal'); // Docker Desktop (Mac/Windows)
|
|
193
|
+
fallbackHosts.push('172.17.0.1'); // Docker默认网关
|
|
194
|
+
fallbackHosts.push('homeassistant'); // HA容器名
|
|
195
|
+
fallbackHosts.push('mosquitto'); // Mosquitto容器名
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 如果配置的是容器名或Docker地址,添加本地地址fallback
|
|
199
|
+
if (host === 'host.docker.internal' || host.startsWith('172.') ||
|
|
200
|
+
host === 'homeassistant' || host === 'mosquitto') {
|
|
201
|
+
fallbackHosts.push('localhost');
|
|
202
|
+
fallbackHosts.push('127.0.0.1');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 构建fallback候选地址
|
|
206
|
+
fallbackHosts.forEach(h => {
|
|
207
|
+
const url = `${protocol}${h}:${port}`;
|
|
208
|
+
if (!candidates.includes(url)) {
|
|
209
|
+
candidates.push(url);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return candidates;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 连接MQTT(带智能重试和fallback)
|
|
159
217
|
node.connectMqtt = function() {
|
|
160
218
|
if (!node.config.enableMqtt) {
|
|
161
219
|
return;
|
|
@@ -167,12 +225,19 @@ module.exports = function(RED) {
|
|
|
167
225
|
return;
|
|
168
226
|
}
|
|
169
227
|
|
|
170
|
-
|
|
228
|
+
// 获取候选broker地址列表
|
|
229
|
+
const brokerCandidates = node.getMqttBrokerCandidates(node.config.mqttBroker);
|
|
230
|
+
let currentCandidateIndex = 0;
|
|
231
|
+
let lastConnectAttempt = 0;
|
|
232
|
+
|
|
233
|
+
node.log(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
|
|
234
|
+
node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
|
|
171
235
|
|
|
172
236
|
const options = {
|
|
173
237
|
clientId: `modbus_master_${Math.random().toString(16).substr(2, 8)}`,
|
|
174
238
|
clean: false, // 持久化会话,断线重连后继续接收消息
|
|
175
|
-
reconnectPeriod:
|
|
239
|
+
reconnectPeriod: 0, // 禁用自动重连,我们手动管理
|
|
240
|
+
connectTimeout: 5000, // 5秒连接超时
|
|
176
241
|
queueQoSZero: false // 不缓存QoS=0的消息
|
|
177
242
|
};
|
|
178
243
|
|
|
@@ -181,37 +246,111 @@ module.exports = function(RED) {
|
|
|
181
246
|
options.password = node.config.mqttPassword;
|
|
182
247
|
}
|
|
183
248
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
249
|
+
// 尝试连接函数
|
|
250
|
+
const tryConnect = (brokerUrl) => {
|
|
251
|
+
try {
|
|
252
|
+
if (node.mqttClient) {
|
|
253
|
+
try {
|
|
254
|
+
node.mqttClient.end(true); // 强制关闭之前的连接
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// 忽略关闭错误
|
|
257
|
+
}
|
|
258
|
+
node.mqttClient = null;
|
|
259
|
+
}
|
|
191
260
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
node.mqttClient.on('error', (err) => {
|
|
197
|
-
// 日志限流:MQTT错误最多每10分钟输出一次
|
|
198
|
-
const now = Date.now();
|
|
199
|
-
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
261
|
+
node.mqttClient = mqtt.connect(brokerUrl, options);
|
|
262
|
+
lastConnectAttempt = Date.now();
|
|
200
263
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
264
|
+
node.mqttClient.on('connect', () => {
|
|
265
|
+
node.log(`MQTT已连接: ${brokerUrl}`);
|
|
266
|
+
|
|
267
|
+
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
268
|
+
if (brokerUrl !== brokerCandidates[0]) {
|
|
269
|
+
node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 发送设备发现消息(Home Assistant MQTT Discovery)
|
|
273
|
+
node.publishDiscovery();
|
|
274
|
+
|
|
275
|
+
// 订阅命令主题
|
|
276
|
+
node.subscribeCommands();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
node.mqttClient.on('error', (err) => {
|
|
280
|
+
// 连接失败,尝试下一个候选地址
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const timeSinceLastAttempt = now - lastConnectAttempt;
|
|
283
|
+
|
|
284
|
+
// 避免频繁重试(至少等待2秒)
|
|
285
|
+
if (timeSinceLastAttempt < 2000) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 尝试下一个候选地址
|
|
290
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
291
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
292
|
+
|
|
293
|
+
// 如果回到第一个地址,说明所有地址都试过了
|
|
294
|
+
if (currentCandidateIndex === 0) {
|
|
295
|
+
// 日志限流:MQTT错误最多每10分钟输出一次
|
|
296
|
+
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
297
|
+
|
|
298
|
+
if (shouldLog) {
|
|
299
|
+
const errorMsg = err.message || '连接失败';
|
|
300
|
+
node.error(`MQTT错误: ${errorMsg}`);
|
|
301
|
+
node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
|
|
302
|
+
node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
|
|
303
|
+
node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
|
|
304
|
+
node.lastMqttErrorLog = now;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 5秒后重试第一个地址
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
tryConnect(brokerCandidates[0]);
|
|
310
|
+
}, 5000);
|
|
311
|
+
} else {
|
|
312
|
+
node.log(`尝试备用MQTT broker: ${nextBroker}`);
|
|
313
|
+
tryConnect(nextBroker);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
node.mqttClient.on('offline', () => {
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
320
|
+
|
|
321
|
+
if (shouldLog) {
|
|
322
|
+
node.warn('MQTT离线,正在尝试重连...');
|
|
323
|
+
node.lastMqttErrorLog = now;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 尝试下一个候选地址
|
|
327
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
328
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
329
|
+
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
tryConnect(nextBroker);
|
|
332
|
+
}, 2000);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
node.mqttClient.on('message', (topic, message) => {
|
|
336
|
+
node.handleMqttCommand(topic, message);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
} catch (err) {
|
|
340
|
+
node.error(`MQTT连接异常: ${err.message}`);
|
|
341
|
+
|
|
342
|
+
// 尝试下一个候选地址
|
|
343
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
344
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
345
|
+
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
tryConnect(nextBroker);
|
|
348
|
+
}, 2000);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// 开始连接
|
|
353
|
+
tryConnect(brokerCandidates[0]);
|
|
215
354
|
};
|
|
216
355
|
|
|
217
356
|
// 发布MQTT发现消息(Home Assistant MQTT Discovery 完全兼容)
|
|
@@ -100,6 +100,64 @@ module.exports = function(RED) {
|
|
|
100
100
|
node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
|
|
101
101
|
node.commandTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/set`;
|
|
102
102
|
|
|
103
|
+
// 获取可能的MQTT broker地址列表(用于自动fallback)
|
|
104
|
+
node.getMqttBrokerCandidates = function(configuredBroker) {
|
|
105
|
+
const candidates = [];
|
|
106
|
+
|
|
107
|
+
// 解析配置的broker地址
|
|
108
|
+
let protocol = 'mqtt://';
|
|
109
|
+
let host = 'localhost';
|
|
110
|
+
let port = '1883';
|
|
111
|
+
|
|
112
|
+
if (configuredBroker) {
|
|
113
|
+
// 移除协议前缀
|
|
114
|
+
let brokerUrl = configuredBroker.replace(/^mqtt:\/\//, '').replace(/^mqtts:\/\//, '');
|
|
115
|
+
if (configuredBroker.startsWith('mqtts://')) {
|
|
116
|
+
protocol = 'mqtts://';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 分离主机和端口
|
|
120
|
+
const parts = brokerUrl.split(':');
|
|
121
|
+
if (parts.length >= 1) {
|
|
122
|
+
host = parts[0];
|
|
123
|
+
}
|
|
124
|
+
if (parts.length >= 2) {
|
|
125
|
+
port = parts[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 首选:用户配置的地址
|
|
129
|
+
candidates.push(configuredBroker);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback候选地址(适配不同环境)
|
|
133
|
+
const fallbackHosts = [];
|
|
134
|
+
|
|
135
|
+
// 如果配置的是localhost或127.0.0.1,添加Docker环境的fallback
|
|
136
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
137
|
+
fallbackHosts.push('host.docker.internal'); // Docker Desktop (Mac/Windows)
|
|
138
|
+
fallbackHosts.push('172.17.0.1'); // Docker默认网关
|
|
139
|
+
fallbackHosts.push('homeassistant'); // HA容器名
|
|
140
|
+
fallbackHosts.push('mosquitto'); // Mosquitto容器名
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 如果配置的是容器名或Docker地址,添加本地地址fallback
|
|
144
|
+
if (host === 'host.docker.internal' || host.startsWith('172.') ||
|
|
145
|
+
host === 'homeassistant' || host === 'mosquitto') {
|
|
146
|
+
fallbackHosts.push('localhost');
|
|
147
|
+
fallbackHosts.push('127.0.0.1');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 构建fallback候选地址
|
|
151
|
+
fallbackHosts.forEach(h => {
|
|
152
|
+
const url = `${protocol}${h}:${port}`;
|
|
153
|
+
if (!candidates.includes(url)) {
|
|
154
|
+
candidates.push(url);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return candidates;
|
|
159
|
+
};
|
|
160
|
+
|
|
103
161
|
// 连接RS-485总线(物理开关面板)
|
|
104
162
|
node.connectRs485 = async function() {
|
|
105
163
|
try {
|
|
@@ -292,7 +350,7 @@ module.exports = function(RED) {
|
|
|
292
350
|
}
|
|
293
351
|
};
|
|
294
352
|
|
|
295
|
-
// 连接MQTT
|
|
353
|
+
// 连接MQTT(带智能重试和fallback)
|
|
296
354
|
node.connectMqtt = function() {
|
|
297
355
|
// 验证MQTT broker配置
|
|
298
356
|
if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
|
|
@@ -300,10 +358,19 @@ module.exports = function(RED) {
|
|
|
300
358
|
return;
|
|
301
359
|
}
|
|
302
360
|
|
|
361
|
+
// 获取候选broker地址列表
|
|
362
|
+
const brokerCandidates = node.getMqttBrokerCandidates(node.config.mqttBroker);
|
|
363
|
+
let currentCandidateIndex = 0;
|
|
364
|
+
let lastConnectAttempt = 0;
|
|
365
|
+
|
|
366
|
+
node.log(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
|
|
367
|
+
node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
|
|
368
|
+
|
|
303
369
|
const options = {
|
|
304
370
|
clientId: `modbus_switch_${node.id}`,
|
|
305
371
|
clean: false, // 持久化会话,断线重连后继续接收消息
|
|
306
|
-
reconnectPeriod:
|
|
372
|
+
reconnectPeriod: 0, // 禁用自动重连,我们手动管理
|
|
373
|
+
connectTimeout: 5000, // 5秒连接超时
|
|
307
374
|
queueQoSZero: false // 不缓存QoS=0的消息
|
|
308
375
|
};
|
|
309
376
|
|
|
@@ -312,71 +379,147 @@ module.exports = function(RED) {
|
|
|
312
379
|
options.password = node.config.mqttPassword;
|
|
313
380
|
}
|
|
314
381
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
382
|
+
// 尝试连接函数
|
|
383
|
+
const tryConnect = (brokerUrl) => {
|
|
384
|
+
try {
|
|
385
|
+
if (node.mqttClient) {
|
|
386
|
+
try {
|
|
387
|
+
node.mqttClient.end(true); // 强制关闭之前的连接
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// 忽略关闭错误
|
|
390
|
+
}
|
|
391
|
+
node.mqttClient = null;
|
|
392
|
+
}
|
|
321
393
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
394
|
+
node.mqttClient = mqtt.connect(brokerUrl, options);
|
|
395
|
+
lastConnectAttempt = Date.now();
|
|
396
|
+
|
|
397
|
+
node.mqttClient.on('connect', () => {
|
|
398
|
+
node.log(`MQTT已连接: ${brokerUrl}`);
|
|
399
|
+
|
|
400
|
+
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
401
|
+
if (brokerUrl !== brokerCandidates[0]) {
|
|
402
|
+
node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
328
403
|
}
|
|
404
|
+
|
|
405
|
+
node.updateStatus();
|
|
406
|
+
|
|
407
|
+
// 订阅状态主题(QoS=1确保状态更新不丢失)
|
|
408
|
+
node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
|
|
409
|
+
if (err) {
|
|
410
|
+
node.error(`订阅失败: ${err.message}`);
|
|
411
|
+
} else {
|
|
412
|
+
node.log(`已订阅: ${node.stateTopic}(QoS=1)`);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
329
415
|
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
node.mqttClient.on('error', (err) => {
|
|
333
|
-
// 日志限流:MQTT错误最多每10分钟输出一次
|
|
334
|
-
const now = Date.now();
|
|
335
|
-
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
336
416
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
417
|
+
node.mqttClient.on('error', (err) => {
|
|
418
|
+
// 连接失败,尝试下一个候选地址
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
const timeSinceLastAttempt = now - lastConnectAttempt;
|
|
421
|
+
|
|
422
|
+
// 避免频繁重试(至少等待2秒)
|
|
423
|
+
if (timeSinceLastAttempt < 2000) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 尝试下一个候选地址
|
|
428
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
429
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
430
|
+
|
|
431
|
+
// 如果回到第一个地址,说明所有地址都试过了
|
|
432
|
+
if (currentCandidateIndex === 0) {
|
|
433
|
+
// 日志限流:MQTT错误最多每10分钟输出一次
|
|
434
|
+
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
435
|
+
|
|
436
|
+
if (shouldLog) {
|
|
437
|
+
const errorMsg = err.message || '连接失败';
|
|
438
|
+
node.error(`MQTT错误: ${errorMsg}`);
|
|
439
|
+
node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
|
|
440
|
+
node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
|
|
441
|
+
node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
|
|
442
|
+
node.lastMqttErrorLog = now;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 5秒后重试第一个地址
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
tryConnect(brokerCandidates[0]);
|
|
448
|
+
}, 5000);
|
|
449
|
+
} else {
|
|
450
|
+
node.log(`尝试备用MQTT broker: ${nextBroker}`);
|
|
451
|
+
tryConnect(nextBroker);
|
|
452
|
+
}
|
|
358
453
|
|
|
359
454
|
node.updateStatus();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
node.mqttClient.on('close', () => {
|
|
458
|
+
node.updateStatus();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
node.mqttClient.on('offline', () => {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
360
464
|
|
|
361
|
-
|
|
362
|
-
|
|
465
|
+
if (shouldLog) {
|
|
466
|
+
node.warn('MQTT离线,正在尝试重连...');
|
|
467
|
+
node.lastMqttErrorLog = now;
|
|
468
|
+
}
|
|
363
469
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
470
|
+
// 尝试下一个候选地址
|
|
471
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
472
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
473
|
+
|
|
474
|
+
setTimeout(() => {
|
|
475
|
+
tryConnect(nextBroker);
|
|
476
|
+
}, 2000);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
node.mqttClient.on('reconnect', () => {
|
|
480
|
+
node.updateStatus();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// 接收MQTT状态消息
|
|
484
|
+
node.mqttClient.on('message', (topic, message) => {
|
|
485
|
+
if (topic === node.stateTopic) {
|
|
486
|
+
const state = message.toString();
|
|
487
|
+
node.currentState = (state === 'ON' || state === 'true' || state === '1');
|
|
488
|
+
|
|
489
|
+
node.updateStatus();
|
|
490
|
+
|
|
491
|
+
// 发送控制指令到物理开关面板(同步指示灯等)
|
|
492
|
+
node.sendCommandToPanel(node.currentState);
|
|
493
|
+
|
|
494
|
+
// 输出状态
|
|
495
|
+
node.send({
|
|
496
|
+
payload: node.currentState,
|
|
497
|
+
topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
|
|
498
|
+
switchId: node.config.switchId, // 开关ID(物理面板地址)
|
|
499
|
+
button: node.config.buttonNumber, // 按钮编号
|
|
500
|
+
targetSlave: node.config.targetSlaveAddress, // 映射到的继电器从站
|
|
501
|
+
targetCoil: node.config.targetCoilNumber // 映射到的继电器线圈
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
} catch (err) {
|
|
507
|
+
node.error(`MQTT连接异常: ${err.message}`);
|
|
508
|
+
|
|
509
|
+
// 尝试下一个候选地址
|
|
510
|
+
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
511
|
+
const nextBroker = brokerCandidates[currentCandidateIndex];
|
|
512
|
+
|
|
513
|
+
setTimeout(() => {
|
|
514
|
+
tryConnect(nextBroker);
|
|
515
|
+
}, 2000);
|
|
516
|
+
|
|
517
|
+
node.updateStatus();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// 开始连接
|
|
522
|
+
tryConnect(brokerCandidates[0]);
|
|
380
523
|
};
|
|
381
524
|
|
|
382
525
|
// 处理输入消息
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Node-RED Modbus节点,支持TCP
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、智能MQTT连接(自动fallback到host.docker.internal等)、Home Assistant自动发现和多品牌开关面板,完美兼容Docker/容器环境",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|