node-red-contrib-symi-modbus 2.9.9 → 2.9.10
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 +17 -1
- package/nodes/custom-protocol.js +11 -11
- package/nodes/homekit-bridge.js +8 -8
- package/nodes/modbus-dashboard.js +1 -1
- package/nodes/modbus-debug.js +5 -5
- package/nodes/modbus-master.html +2 -2
- package/nodes/modbus-master.js +82 -39
- package/nodes/modbus-slave-switch.html +23 -1
- package/nodes/modbus-slave-switch.js +43 -25
- package/nodes/serial-port-config.js +60 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -952,7 +952,23 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
952
952
|
|
|
953
953
|
## 版本信息
|
|
954
954
|
|
|
955
|
-
**当前版本**: v2.9.
|
|
955
|
+
**当前版本**: v2.9.10 (2026-01-08)
|
|
956
|
+
|
|
957
|
+
**v2.9.10 更新内容**:
|
|
958
|
+
- **日志系统优化(解决日志占用问题)**:
|
|
959
|
+
- **彻底静默重连日志**:将 TCP/串口连接过程中的 `node.error` 降级为 `node.log` 或 `node.debug`,不再发送到 Node-RED 调试面板(Debug Tab),彻底解决重连期间日志刷屏问题。
|
|
960
|
+
- **智能过滤**:相同的连接错误在重试期间不再重复输出日志(每 10 分钟仅后台提醒一次)。
|
|
961
|
+
- **状态栏增强**:将具体错误信息(如“拒绝连接”、“串口不存在”)直接显示在节点状态文字中,无需查看日志即可掌握连接状况。
|
|
962
|
+
- **多主站配置隔离修复**:
|
|
963
|
+
- 修复了多个Modbus主站节点在编辑时配置互相干扰的问题(通过Scoped Event Handler实现)
|
|
964
|
+
- 确保每个主站节点的从站列表配置完全独立,互不影响
|
|
965
|
+
- **从站开关主站关联**:
|
|
966
|
+
- **新增关联主站功能**:从站开关节点新增"关联主站"配置项
|
|
967
|
+
- **多主站支持**:支持选择特定的主站节点,解决多主站环境下相同从站地址冲突的问题
|
|
968
|
+
- **智能过滤**:从站开关只响应关联主站的状态更新,避免误触发
|
|
969
|
+
- **事件机制优化**:
|
|
970
|
+
- 优化内部事件总线,携带主站ID信息,实现精确的消息路由
|
|
971
|
+
- 增强`modbus-master`和`modbus-slave-switch`之间的免连线通信稳定性
|
|
956
972
|
|
|
957
973
|
**v2.9.9 更新内容**:
|
|
958
974
|
- **深度优化多 TCP 主站并发**:
|
package/nodes/custom-protocol.js
CHANGED
|
@@ -21,7 +21,7 @@ module.exports = function(RED) {
|
|
|
21
21
|
// 获取串口配置节点
|
|
22
22
|
var serialNode = RED.nodes.getNode(node.config.serialConfig);
|
|
23
23
|
if (!serialNode) {
|
|
24
|
-
node.
|
|
24
|
+
node.log('未找到串口配置节点');
|
|
25
25
|
node.status({fill: "red", shape: "ring", text: "未配置串口"});
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
@@ -38,21 +38,21 @@ module.exports = function(RED) {
|
|
|
38
38
|
|
|
39
39
|
// 确保是偶数长度
|
|
40
40
|
if (hex.length % 2 !== 0) {
|
|
41
|
-
node.
|
|
41
|
+
node.log('16进制字符串长度必须为偶数: ' + hexString);
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// 限制48字节
|
|
46
46
|
if (hex.length > 96) {
|
|
47
47
|
hex = hex.substring(0, 96);
|
|
48
|
-
node.
|
|
48
|
+
node.log('指令长度超过48字节,已截断');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// 转换为Buffer
|
|
52
52
|
var buffer = Buffer.from(hex, 'hex');
|
|
53
53
|
return buffer;
|
|
54
54
|
} catch (err) {
|
|
55
|
-
node.
|
|
55
|
+
node.log('16进制字符串转换失败: ' + err.message);
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -60,13 +60,13 @@ module.exports = function(RED) {
|
|
|
60
60
|
// 发送指令到串口
|
|
61
61
|
function sendCommand(buffer, cmdName) {
|
|
62
62
|
if (!buffer) {
|
|
63
|
-
node.
|
|
63
|
+
node.log('指令为空,跳过发送');
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 直接通过串口配置节点发送数据
|
|
68
68
|
if (!serialNode || !serialNode.connection) {
|
|
69
|
-
node.
|
|
69
|
+
node.log('串口连接未建立');
|
|
70
70
|
node.status({fill: "red", shape: "ring", text: "连接未建立"});
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
@@ -80,7 +80,7 @@ module.exports = function(RED) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (!isConnected) {
|
|
83
|
-
node.
|
|
83
|
+
node.log('串口/TCP连接未打开');
|
|
84
84
|
node.status({fill: "red", shape: "ring", text: "连接未打开"});
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
@@ -88,7 +88,7 @@ module.exports = function(RED) {
|
|
|
88
88
|
// 使用串口配置节点的write方法(带队列机制)
|
|
89
89
|
serialNode.write(buffer, function(err) {
|
|
90
90
|
if (err) {
|
|
91
|
-
node.
|
|
91
|
+
node.log('发送失败: ' + err.message);
|
|
92
92
|
node.status({fill: "red", shape: "ring", text: "发送失败"});
|
|
93
93
|
} else {
|
|
94
94
|
node.log(cmdName + '指令已发送: ' + buffer.toString('hex').toUpperCase());
|
|
@@ -108,7 +108,7 @@ module.exports = function(RED) {
|
|
|
108
108
|
|
|
109
109
|
// 只接受布尔值
|
|
110
110
|
if (typeof value !== 'boolean') {
|
|
111
|
-
node.
|
|
111
|
+
node.log('输入必须为true/false,当前类型: ' + typeof value);
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -126,7 +126,7 @@ module.exports = function(RED) {
|
|
|
126
126
|
// 获取当前指令
|
|
127
127
|
var currentCmd = commands[node.curtainStateIndex];
|
|
128
128
|
if (!currentCmd || !currentCmd.hex) {
|
|
129
|
-
node.
|
|
129
|
+
node.log('窗帘模式缺少' + currentCmd.name + '指令配置');
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -144,7 +144,7 @@ module.exports = function(RED) {
|
|
|
144
144
|
var hexString = value ? node.config.openCmd : node.config.closeCmd;
|
|
145
145
|
|
|
146
146
|
if (!hexString) {
|
|
147
|
-
node.
|
|
147
|
+
node.log(cmdName + '指令未配置');
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
|
package/nodes/homekit-bridge.js
CHANGED
|
@@ -54,7 +54,7 @@ module.exports = function(RED) {
|
|
|
54
54
|
// 获取主站节点
|
|
55
55
|
node.masterNode = RED.nodes.getNode(node.config.masterNodeId);
|
|
56
56
|
if (!node.masterNode) {
|
|
57
|
-
node.
|
|
57
|
+
node.log("未找到主站节点");
|
|
58
58
|
node.status({fill: "red", shape: "dot", text: "主站节点未找到"});
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -62,7 +62,7 @@ module.exports = function(RED) {
|
|
|
62
62
|
// 从主站节点的 config.slaves 获取从站配置
|
|
63
63
|
const slaves = node.masterNode.config ? node.masterNode.config.slaves : null;
|
|
64
64
|
if (!slaves || slaves.length === 0) {
|
|
65
|
-
node.
|
|
65
|
+
node.log("主站节点未配置从站");
|
|
66
66
|
node.status({fill: "red", shape: "dot", text: "主站未配置从站"});
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
@@ -82,8 +82,8 @@ module.exports = function(RED) {
|
|
|
82
82
|
node.log(`端口: ${node.config.port}`);
|
|
83
83
|
|
|
84
84
|
} catch (err) {
|
|
85
|
-
node.
|
|
86
|
-
node.status({fill: "red", shape: "
|
|
85
|
+
node.log(`初始化异常: ${err.message}`);
|
|
86
|
+
node.status({fill: "red", shape: "ring", text: `初始化失败: ${err.message}`});
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -198,7 +198,7 @@ module.exports = function(RED) {
|
|
|
198
198
|
node.log(`配件名称已更新: ${accessory._slaveAddr}_${accessory._coil} -> ${newName}`);
|
|
199
199
|
}
|
|
200
200
|
} catch (err) {
|
|
201
|
-
node.
|
|
201
|
+
node.log(`更新配件名称失败: ${err.message}`);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -259,7 +259,7 @@ module.exports = function(RED) {
|
|
|
259
259
|
node.log(`状态同步到HomeKit: 从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
260
260
|
}
|
|
261
261
|
} catch (err) {
|
|
262
|
-
node.
|
|
262
|
+
node.log(`状态变化监听器错误: ${err.message}`);
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
|
|
@@ -287,7 +287,7 @@ module.exports = function(RED) {
|
|
|
287
287
|
});
|
|
288
288
|
node.log(`已更新 ${updateCount} 个配件名称`);
|
|
289
289
|
} catch (err) {
|
|
290
|
-
node.
|
|
290
|
+
node.log(`批量更新配件名称失败: ${err.message}`);
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
@@ -307,7 +307,7 @@ module.exports = function(RED) {
|
|
|
307
307
|
node.bridge.unpublish();
|
|
308
308
|
node.log("HomeKit网桥已停止");
|
|
309
309
|
} catch (err) {
|
|
310
|
-
node.
|
|
310
|
+
node.log(`停止网桥时出错: ${err.message}`);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
package/nodes/modbus-debug.js
CHANGED
|
@@ -92,7 +92,7 @@ module.exports = function(RED) {
|
|
|
92
92
|
if (node.sourceType === "serial") {
|
|
93
93
|
if (!node.serialPortConfig) {
|
|
94
94
|
node.status({ fill: "red", shape: "ring", text: "未选择串口配置" });
|
|
95
|
-
node.
|
|
95
|
+
node.log("未配置RS-485连接,请在节点中选择 serial-port-config 配置节点");
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
// 使用共享连接
|
|
@@ -104,14 +104,14 @@ module.exports = function(RED) {
|
|
|
104
104
|
node.log(`modbus-debug 监听共享连接:${desc}`);
|
|
105
105
|
node.status({ fill: "blue", shape: "ring", text: "监听中" });
|
|
106
106
|
} catch (err) {
|
|
107
|
-
node.
|
|
107
|
+
node.log(`注册数据监听器失败: ${err.message}`);
|
|
108
108
|
node.status({ fill: "red", shape: "ring", text: "监听失败" });
|
|
109
109
|
}
|
|
110
110
|
} else if (node.sourceType === "modbus") {
|
|
111
111
|
// 已废弃:不再支持独立连接到Modbus服务器(会导致串口冲突)
|
|
112
112
|
// 请使用 "serial" 源类型并选择 serial-port-config 配置节点
|
|
113
113
|
node.status({ fill: "red", shape: "ring", text: "请使用serial源类型" });
|
|
114
|
-
node.
|
|
114
|
+
node.log("不再支持独立连接到Modbus服务器(会导致串口冲突),请将源类型改为 'serial' 并选择 serial-port-config 配置节点");
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
};
|
|
@@ -128,7 +128,7 @@ module.exports = function(RED) {
|
|
|
128
128
|
node.serialPortConfig.unregisterDataListener(sendHexMsg);
|
|
129
129
|
}
|
|
130
130
|
} catch (e) {
|
|
131
|
-
node.
|
|
131
|
+
node.log(`注销监听器时出错: ${e.message}`);
|
|
132
132
|
}
|
|
133
133
|
try {
|
|
134
134
|
if (node.localConnection) {
|
|
@@ -140,7 +140,7 @@ module.exports = function(RED) {
|
|
|
140
140
|
node.localConnection = null;
|
|
141
141
|
}
|
|
142
142
|
} catch (e) {
|
|
143
|
-
node.
|
|
143
|
+
node.log(`关闭本地连接时出错: ${e.message}`);
|
|
144
144
|
}
|
|
145
145
|
done();
|
|
146
146
|
});
|
package/nodes/modbus-master.html
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
renderSlaveList();
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
$(
|
|
125
|
+
$("#slave-list-container").on("click", ".btn-delete-slave", function() {
|
|
126
126
|
var index = parseInt($(this).data("index"));
|
|
127
127
|
if (node.slaves.length > 1) {
|
|
128
128
|
node.slaves.splice(index, 1);
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
$(
|
|
135
|
+
$("#slave-list-container").on("change", ".slave-address, .slave-coil-start, .slave-coil-end, .slave-poll-interval", function() {
|
|
136
136
|
var index = parseInt($(this).data("index"));
|
|
137
137
|
var className = $(this).attr("class").split(" ")[0];
|
|
138
138
|
var value = parseInt($(this).val());
|
package/nodes/modbus-master.js
CHANGED
|
@@ -130,7 +130,7 @@ module.exports = function(RED) {
|
|
|
130
130
|
|
|
131
131
|
// 验证Modbus服务器配置
|
|
132
132
|
if (!node.modbusServerConfig) {
|
|
133
|
-
node.
|
|
133
|
+
node.log('未配置Modbus服务器,将使用默认配置');
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// 记录主站配置信息(用于调试多主站问题)
|
|
@@ -150,6 +150,7 @@ module.exports = function(RED) {
|
|
|
150
150
|
node.mqttClient = null;
|
|
151
151
|
node.mqttConnected = false; // MQTT连接状态
|
|
152
152
|
node.isClosing = false;
|
|
153
|
+
node.lastConnectError = ""; // 记录最后一次连接错误,用于日志限流
|
|
153
154
|
node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
|
|
154
155
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
155
156
|
node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5分钟
|
|
@@ -197,7 +198,18 @@ module.exports = function(RED) {
|
|
|
197
198
|
|
|
198
199
|
// 更新节点状态显示
|
|
199
200
|
node.updateNodeStatus = function() {
|
|
200
|
-
|
|
201
|
+
let modbusStatus = node.isConnected ? "Modbus-OK" : "Modbus-ERR";
|
|
202
|
+
|
|
203
|
+
// 如果连接失败,尝试显示简短错误信息
|
|
204
|
+
if (!node.isConnected && node.lastConnectError) {
|
|
205
|
+
// 提取错误关键词,避免状态栏过长
|
|
206
|
+
let shortErr = node.lastConnectError;
|
|
207
|
+
if (shortErr.includes("ECONNREFUSED")) shortErr = "拒绝连接";
|
|
208
|
+
if (shortErr.includes("ENOENT") || shortErr.includes("No such file")) shortErr = "串口不存在";
|
|
209
|
+
if (shortErr.includes("EACCES")) shortErr = "权限不足";
|
|
210
|
+
if (shortErr.includes("timeout")) shortErr = "连接超时";
|
|
211
|
+
modbusStatus = `ERR: ${shortErr}`;
|
|
212
|
+
}
|
|
201
213
|
|
|
202
214
|
// 如果MQTT未启用,显示"本地模式"
|
|
203
215
|
let mqttStatus;
|
|
@@ -252,7 +264,7 @@ module.exports = function(RED) {
|
|
|
252
264
|
});
|
|
253
265
|
});
|
|
254
266
|
} catch (err) {
|
|
255
|
-
node.
|
|
267
|
+
node.log(`关闭旧连接时出错: ${err.message}`);
|
|
256
268
|
}
|
|
257
269
|
}
|
|
258
270
|
|
|
@@ -325,14 +337,14 @@ module.exports = function(RED) {
|
|
|
325
337
|
// 设置错误处理器,防止未捕获的错误导致进程崩溃
|
|
326
338
|
if (node.client._port) {
|
|
327
339
|
node.client._port.on('error', (err) => {
|
|
328
|
-
node.
|
|
340
|
+
node.log(`串口错误(已忽略): ${err.message}`);
|
|
329
341
|
});
|
|
330
342
|
}
|
|
331
343
|
|
|
332
344
|
// TCP模式:设置socket错误处理器
|
|
333
345
|
if (node.client._client) {
|
|
334
346
|
node.client._client.on('error', (err) => {
|
|
335
|
-
node.
|
|
347
|
+
node.log(`TCP socket错误(已忽略): ${err.message}`);
|
|
336
348
|
});
|
|
337
349
|
}
|
|
338
350
|
|
|
@@ -370,8 +382,22 @@ module.exports = function(RED) {
|
|
|
370
382
|
}
|
|
371
383
|
|
|
372
384
|
} catch (err) {
|
|
373
|
-
node.error(`Modbus连接失败: ${err.message}`);
|
|
374
385
|
node.isConnected = false;
|
|
386
|
+
|
|
387
|
+
// 优化:如果错误消息没有变化,且不是第一次重试,则不输出 node.error 到全局日志
|
|
388
|
+
// 这样可以避免 ECONNREFUSED 等持续性错误刷屏
|
|
389
|
+
const errorMsg = err.message || "未知连接错误";
|
|
390
|
+
const isNewError = node.lastConnectError !== errorMsg;
|
|
391
|
+
|
|
392
|
+
if (isNewError || node.reconnectAttempts % 12 === 0) { // 每分钟(约12次5秒重试)输出一次日志
|
|
393
|
+
if (isNewError) {
|
|
394
|
+
node.log(`Modbus连接失败: ${errorMsg}`);
|
|
395
|
+
} else {
|
|
396
|
+
node.debug(`Modbus连接持续失败: ${errorMsg} (已重试 ${node.reconnectAttempts} 次)`);
|
|
397
|
+
}
|
|
398
|
+
node.lastConnectError = errorMsg;
|
|
399
|
+
}
|
|
400
|
+
|
|
375
401
|
node.updateNodeStatus();
|
|
376
402
|
|
|
377
403
|
// 使用指数退避策略重试(5秒、10秒、20秒...最大60秒)
|
|
@@ -473,8 +499,8 @@ module.exports = function(RED) {
|
|
|
473
499
|
|
|
474
500
|
// 验证MQTT broker配置
|
|
475
501
|
if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
|
|
476
|
-
node.
|
|
477
|
-
node.
|
|
502
|
+
node.log('MQTT已启用但broker地址未配置 - 使用纯本地模式');
|
|
503
|
+
node.log('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
|
|
478
504
|
return;
|
|
479
505
|
}
|
|
480
506
|
|
|
@@ -539,11 +565,17 @@ module.exports = function(RED) {
|
|
|
539
565
|
node.mqttClient.on('error', (err) => {
|
|
540
566
|
// 连接失败,尝试下一个候选地址
|
|
541
567
|
const errorMsg = err.message || err.code || '连接失败';
|
|
542
|
-
node.warn(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
|
|
543
568
|
|
|
544
569
|
const now = Date.now();
|
|
545
570
|
const timeSinceLastAttempt = now - lastConnectAttempt;
|
|
546
571
|
|
|
572
|
+
// 减少 MQTT 错误日志频率
|
|
573
|
+
const shouldLogWarn = (now - node.lastMqttErrorLog) > 60000; // 每分钟最多显示一次 warn
|
|
574
|
+
if (shouldLogWarn) {
|
|
575
|
+
node.log(`MQTT连接错误: ${errorMsg} (broker: ${brokerUrl})`);
|
|
576
|
+
node.lastMqttErrorLog = now;
|
|
577
|
+
}
|
|
578
|
+
|
|
547
579
|
// 避免频繁重试(至少等待1秒),但仍要记录错误
|
|
548
580
|
if (timeSinceLastAttempt < 1000) {
|
|
549
581
|
setTimeout(() => {
|
|
@@ -565,20 +597,18 @@ module.exports = function(RED) {
|
|
|
565
597
|
const isSingleIpConfig = brokerCandidates.length === 1;
|
|
566
598
|
|
|
567
599
|
if (isSingleIpConfig) {
|
|
568
|
-
// 局域网IP
|
|
569
|
-
node.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
600
|
+
// 局域网IP配置失败,立即输出错误
|
|
601
|
+
const shouldLogDetailed = (now - node.lastMqttErrorLog) > 60000;
|
|
602
|
+
if (shouldLogDetailed) {
|
|
603
|
+
node.log(`MQTT连接失败: ${errorMsg} (${brokerCandidates[0]})`);
|
|
604
|
+
node.lastMqttErrorLog = now;
|
|
605
|
+
}
|
|
573
606
|
} else {
|
|
574
607
|
// 多个fallback地址都失败,使用日志限流
|
|
575
608
|
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
576
609
|
|
|
577
610
|
if (shouldLog) {
|
|
578
|
-
node.
|
|
579
|
-
node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
|
|
580
|
-
node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
|
|
581
|
-
node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
|
|
611
|
+
node.log(`MQTT错误: ${errorMsg} (所有候选地址均失败)`);
|
|
582
612
|
node.lastMqttErrorLog = now;
|
|
583
613
|
}
|
|
584
614
|
}
|
|
@@ -602,7 +632,7 @@ module.exports = function(RED) {
|
|
|
602
632
|
const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
|
|
603
633
|
|
|
604
634
|
if (shouldLog) {
|
|
605
|
-
node.
|
|
635
|
+
node.log('MQTT离线,正在尝试重连...');
|
|
606
636
|
node.lastMqttErrorLog = now;
|
|
607
637
|
}
|
|
608
638
|
|
|
@@ -620,7 +650,7 @@ module.exports = function(RED) {
|
|
|
620
650
|
});
|
|
621
651
|
|
|
622
652
|
} catch (err) {
|
|
623
|
-
node.
|
|
653
|
+
node.log(`MQTT连接异常: ${err.message}`);
|
|
624
654
|
|
|
625
655
|
// 尝试下一个候选地址
|
|
626
656
|
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
@@ -658,8 +688,8 @@ module.exports = function(RED) {
|
|
|
658
688
|
node.mqttClient.publish(availabilityTopic, 'online', { retain: true });
|
|
659
689
|
|
|
660
690
|
for (let coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
|
|
661
|
-
//
|
|
662
|
-
const uniqueId = `
|
|
691
|
+
// 使用更稳定的唯一ID:主站ID_从站地址_线圈编号(确保全局绝对唯一)
|
|
692
|
+
const uniqueId = `modbus_${node.id}_${slaveId}_${coil}`;
|
|
663
693
|
const objectId = `relay_${slaveId}_${coil}`;
|
|
664
694
|
const discoveryTopic = `homeassistant/switch/${uniqueId}/config`;
|
|
665
695
|
|
|
@@ -728,7 +758,7 @@ module.exports = function(RED) {
|
|
|
728
758
|
// 使用QoS=1订阅,确保命令不丢失
|
|
729
759
|
node.mqttClient.subscribe(commandTopic, { qos: 1 }, (err) => {
|
|
730
760
|
if (err) {
|
|
731
|
-
node.
|
|
761
|
+
node.log(`订阅MQTT命令主题失败: ${err.message}`);
|
|
732
762
|
} else {
|
|
733
763
|
node.log(`已订阅MQTT命令主题: ${commandTopic}(QoS=1)`);
|
|
734
764
|
}
|
|
@@ -759,7 +789,7 @@ module.exports = function(RED) {
|
|
|
759
789
|
try {
|
|
760
790
|
await node.writeSingleCoil(cmd.slaveId, cmd.coil, cmd.value);
|
|
761
791
|
} catch (err) {
|
|
762
|
-
node.
|
|
792
|
+
node.log(`MQTT命令执行失败: 从站${cmd.slaveId} 线圈${cmd.coil} - ${err.message}`);
|
|
763
793
|
}
|
|
764
794
|
}
|
|
765
795
|
};
|
|
@@ -813,7 +843,7 @@ module.exports = function(RED) {
|
|
|
813
843
|
if (err) {
|
|
814
844
|
// 只在首次错误时输出警告
|
|
815
845
|
if (!node.lastMqttPublishError || Date.now() - node.lastMqttPublishError > 60000) {
|
|
816
|
-
node.
|
|
846
|
+
node.log(`发布状态失败: ${stateTopic} - ${err.message}`);
|
|
817
847
|
node.lastMqttPublishError = Date.now();
|
|
818
848
|
}
|
|
819
849
|
}
|
|
@@ -829,7 +859,7 @@ module.exports = function(RED) {
|
|
|
829
859
|
|
|
830
860
|
// 检查从站配置
|
|
831
861
|
if (!node.config.slaves || node.config.slaves.length === 0) {
|
|
832
|
-
node.
|
|
862
|
+
node.log('未配置从站设备,无法启动轮询');
|
|
833
863
|
return;
|
|
834
864
|
}
|
|
835
865
|
|
|
@@ -993,6 +1023,7 @@ module.exports = function(RED) {
|
|
|
993
1023
|
// 后续轮询:只在状态真正改变时广播
|
|
994
1024
|
if (isFirstPoll || oldValue !== newValue) {
|
|
995
1025
|
RED.events.emit('modbus:coilStateChanged', {
|
|
1026
|
+
masterId: node.id,
|
|
996
1027
|
slave: slaveId,
|
|
997
1028
|
coil: coilIndex,
|
|
998
1029
|
value: newValue,
|
|
@@ -1096,7 +1127,7 @@ module.exports = function(RED) {
|
|
|
1096
1127
|
if (isConnectionError) {
|
|
1097
1128
|
// 连接断开,尝试重连
|
|
1098
1129
|
if (shouldLog) {
|
|
1099
|
-
node.
|
|
1130
|
+
node.log('检测到连接断开,尝试重连...');
|
|
1100
1131
|
}
|
|
1101
1132
|
|
|
1102
1133
|
node.isConnected = false;
|
|
@@ -1184,7 +1215,7 @@ module.exports = function(RED) {
|
|
|
1184
1215
|
|
|
1185
1216
|
// 写入线圈(异步,不阻塞)
|
|
1186
1217
|
node.writeSingleCoil(slaveId, coilNumber, state).catch(err => {
|
|
1187
|
-
node.
|
|
1218
|
+
node.log(`Symi按键控制失败: ${err.message}`);
|
|
1188
1219
|
});
|
|
1189
1220
|
|
|
1190
1221
|
// 发送应答帧(REPORT类型0x04,反馈LED状态)
|
|
@@ -1238,7 +1269,7 @@ module.exports = function(RED) {
|
|
|
1238
1269
|
task.reject(err);
|
|
1239
1270
|
}
|
|
1240
1271
|
// 写入失败不中断队列,继续处理下一个任务
|
|
1241
|
-
node.
|
|
1272
|
+
node.log(`队列任务失败,继续处理下一个任务: ${err.message}`);
|
|
1242
1273
|
}
|
|
1243
1274
|
|
|
1244
1275
|
// 等待一段时间再处理下一个任务(40ms间隔,确保总线稳定)
|
|
@@ -1338,7 +1369,7 @@ module.exports = function(RED) {
|
|
|
1338
1369
|
node.pausePolling = false;
|
|
1339
1370
|
node.pollingPausedCount = 0;
|
|
1340
1371
|
|
|
1341
|
-
node.
|
|
1372
|
+
node.log(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
|
|
1342
1373
|
throw err;
|
|
1343
1374
|
}
|
|
1344
1375
|
};
|
|
@@ -1443,7 +1474,7 @@ module.exports = function(RED) {
|
|
|
1443
1474
|
node.pausePolling = false;
|
|
1444
1475
|
node.pollingPausedCount = 0;
|
|
1445
1476
|
|
|
1446
|
-
node.
|
|
1477
|
+
node.log(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
|
|
1447
1478
|
throw err;
|
|
1448
1479
|
}
|
|
1449
1480
|
};
|
|
@@ -1478,6 +1509,18 @@ module.exports = function(RED) {
|
|
|
1478
1509
|
const value = Boolean(data.value);
|
|
1479
1510
|
const triggerSource = data.triggerSource || 'unknown'; // 传递触发源标识
|
|
1480
1511
|
|
|
1512
|
+
// 检查主站关联(如果指定了主站ID,则必须匹配)
|
|
1513
|
+
if (data.masterId && data.masterId !== node.id) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// 检查该主站是否配置了此从站
|
|
1518
|
+
const slaveConfig = node.config.slaves.find(s => s.address === slave);
|
|
1519
|
+
if (!slaveConfig) {
|
|
1520
|
+
// 该主站不管理此从站,忽略
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1481
1524
|
// 输出日志确认收到事件
|
|
1482
1525
|
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'} (触发源: ${triggerSource})`);
|
|
1483
1526
|
|
|
@@ -1487,7 +1530,7 @@ module.exports = function(RED) {
|
|
|
1487
1530
|
|
|
1488
1531
|
node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1489
1532
|
} catch (err) {
|
|
1490
|
-
node.
|
|
1533
|
+
node.log(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
|
|
1491
1534
|
}
|
|
1492
1535
|
};
|
|
1493
1536
|
|
|
@@ -1519,10 +1562,10 @@ module.exports = function(RED) {
|
|
|
1519
1562
|
const value = Boolean(msg.payload.value);
|
|
1520
1563
|
|
|
1521
1564
|
node.writeSingleCoil(slave, coil, value).catch(err => {
|
|
1522
|
-
node.
|
|
1565
|
+
node.log(`连线模式写入失败: ${err.message}`);
|
|
1523
1566
|
});
|
|
1524
1567
|
} else {
|
|
1525
|
-
node.
|
|
1568
|
+
node.log(`writeCoil命令参数不完整: slave=${msg.payload.slave}, coil=${msg.payload.coil}, value=${msg.payload.value}`);
|
|
1526
1569
|
}
|
|
1527
1570
|
break;
|
|
1528
1571
|
|
|
@@ -1535,15 +1578,15 @@ module.exports = function(RED) {
|
|
|
1535
1578
|
node.log(`接收到本地模式批量写入命令: 从站${slave} 起始线圈${startCoil} 共${values.length}个`);
|
|
1536
1579
|
|
|
1537
1580
|
node.writeMultipleCoils(slave, startCoil, values).catch(err => {
|
|
1538
|
-
node.
|
|
1581
|
+
node.log(`本地模式批量写入失败: ${err.message}`);
|
|
1539
1582
|
});
|
|
1540
1583
|
} else {
|
|
1541
|
-
node.
|
|
1584
|
+
node.log(`writeCoils命令参数不完整`);
|
|
1542
1585
|
}
|
|
1543
1586
|
break;
|
|
1544
1587
|
|
|
1545
1588
|
default:
|
|
1546
|
-
node.
|
|
1589
|
+
node.log(`未知命令: ${cmd}`);
|
|
1547
1590
|
}
|
|
1548
1591
|
});
|
|
1549
1592
|
|
|
@@ -1617,7 +1660,7 @@ module.exports = function(RED) {
|
|
|
1617
1660
|
});
|
|
1618
1661
|
}
|
|
1619
1662
|
} catch (err) {
|
|
1620
|
-
node.
|
|
1663
|
+
node.log(`关闭Modbus连接时出错: ${err.message}`);
|
|
1621
1664
|
}
|
|
1622
1665
|
node.client = null; // 释放引用
|
|
1623
1666
|
}
|
|
@@ -1643,7 +1686,7 @@ module.exports = function(RED) {
|
|
|
1643
1686
|
done();
|
|
1644
1687
|
}
|
|
1645
1688
|
} catch (err) {
|
|
1646
|
-
node.
|
|
1689
|
+
node.log(`关闭MQTT连接时出错: ${err.message}`);
|
|
1647
1690
|
node.mqttClient = null;
|
|
1648
1691
|
done();
|
|
1649
1692
|
}
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
meshWirelessMode: {value: false}, // 无线模式(场景触发,不响应LED反馈)
|
|
25
25
|
// 映射到继电器
|
|
26
26
|
targetSlaveAddress: {value: 10, validate: RED.validators.number()},
|
|
27
|
-
targetCoilNumber: {value: 1, validate: RED.validators.number()} // 默认值改为1(显示为1路)
|
|
27
|
+
targetCoilNumber: {value: 1, validate: RED.validators.number()}, // 默认值改为1(显示为1路)
|
|
28
|
+
modbusMaster: {value: ""} // 关联的主站节点
|
|
28
29
|
},
|
|
29
30
|
inputs: 1,
|
|
30
31
|
outputs: 1,
|
|
@@ -209,6 +210,17 @@
|
|
|
209
210
|
// 始终加载已保存的Mesh设备列表(无论当前是什么模式)
|
|
210
211
|
// 这样切换到Mesh模式时,列表已经加载好了
|
|
211
212
|
loadSavedMeshDevices();
|
|
213
|
+
|
|
214
|
+
// 加载所有Modbus主站节点
|
|
215
|
+
const masterSelect = $("#node-input-modbusMaster");
|
|
216
|
+
const currentMasterId = node.modbusMaster;
|
|
217
|
+
|
|
218
|
+
RED.nodes.eachNode(function(n) {
|
|
219
|
+
if (n.type === "modbus-master") {
|
|
220
|
+
const isSelected = n.id === currentMasterId;
|
|
221
|
+
masterSelect.append(`<option value="${n.id}" ${isSelected ? 'selected' : ''}>${n.name || n.id}</option>`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
212
224
|
}
|
|
213
225
|
});
|
|
214
226
|
</script>
|
|
@@ -396,6 +408,16 @@
|
|
|
396
408
|
</div>
|
|
397
409
|
</div>
|
|
398
410
|
|
|
411
|
+
<div class="form-row">
|
|
412
|
+
<label for="node-input-modbusMaster" style="width: 110px;"><i class="fa fa-sitemap"></i> 关联主站</label>
|
|
413
|
+
<select id="node-input-modbusMaster" style="width: calc(70% - 110px);">
|
|
414
|
+
<option value="">所有主站 (不推荐)</option>
|
|
415
|
+
</select>
|
|
416
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
417
|
+
选择该继电器所属的Modbus主站节点,避免多主站冲突
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
399
421
|
<div class="form-row" style="margin-top: 20px; padding: 14px; background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%); border-left: 4px solid #4caf50; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.08);">
|
|
400
422
|
<div style="font-size: 12px; color: #333; line-height: 1.8;">
|
|
401
423
|
<div style="font-weight: 600; color: #2e7d32; margin-bottom: 10px; font-size: 13px;">
|
|
@@ -353,7 +353,7 @@ module.exports = function(RED) {
|
|
|
353
353
|
// 获取RS-485连接配置节点
|
|
354
354
|
node.serialPortConfig = RED.nodes.getNode(config.serialPortConfig);
|
|
355
355
|
if (!node.serialPortConfig) {
|
|
356
|
-
node.
|
|
356
|
+
node.log('未配置RS-485连接,请在节点配置中选择连接配置');
|
|
357
357
|
node.status({fill: "red", shape: "ring", text: "未配置连接"});
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
@@ -385,10 +385,14 @@ module.exports = function(RED) {
|
|
|
385
385
|
meshTotalButtons: parseInt(config.meshTotalButtons) || 1, // Mesh开关总路数(1-6)
|
|
386
386
|
// 目标继电器配置
|
|
387
387
|
targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
|
|
388
|
-
targetCoilNumber: modbusCoilNumber // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
|
|
388
|
+
targetCoilNumber: modbusCoilNumber, // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
|
|
389
|
+
modbusMaster: config.modbusMaster || "" // 关联的主站节点ID
|
|
389
390
|
};
|
|
390
391
|
|
|
391
392
|
node.currentState = false;
|
|
393
|
+
node.isRs485Connected = false;
|
|
394
|
+
node.lastErrorLog = 0;
|
|
395
|
+
node.lastErrorMsg = "";
|
|
392
396
|
node.mqttClient = null;
|
|
393
397
|
node.isClosing = false;
|
|
394
398
|
node.lastTriggerTime = 0; // 上次触发时间(用于场景模式防抖)
|
|
@@ -410,10 +414,10 @@ module.exports = function(RED) {
|
|
|
410
414
|
node.config.meshShortAddress = savedDevice.shortAddr;
|
|
411
415
|
}
|
|
412
416
|
} catch (err) {
|
|
413
|
-
node.
|
|
417
|
+
node.log(`更新Mesh设备短地址失败: ${err.message}`);
|
|
414
418
|
}
|
|
415
419
|
}).catch(err => {
|
|
416
|
-
node.
|
|
420
|
+
node.log(`初始化Mesh存储失败: ${err.message}`);
|
|
417
421
|
});
|
|
418
422
|
}
|
|
419
423
|
|
|
@@ -546,7 +550,13 @@ module.exports = function(RED) {
|
|
|
546
550
|
node.startListeningStateChanges();
|
|
547
551
|
|
|
548
552
|
} catch (err) {
|
|
549
|
-
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
const errMsg = err.message || "";
|
|
555
|
+
if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
|
|
556
|
+
node.log(`RS-485连接异常: ${errMsg}`);
|
|
557
|
+
node.lastErrorLog = now;
|
|
558
|
+
node.lastErrorMsg = errMsg;
|
|
559
|
+
}
|
|
550
560
|
node.isRs485Connected = false;
|
|
551
561
|
node.updateStatus();
|
|
552
562
|
}
|
|
@@ -569,7 +579,7 @@ module.exports = function(RED) {
|
|
|
569
579
|
// 注册到共享连接配置
|
|
570
580
|
node.serialPortConfig.registerDataListener(node.serialDataListener);
|
|
571
581
|
} else {
|
|
572
|
-
node.
|
|
582
|
+
node.log('RS-485连接配置未初始化,无法监听数据');
|
|
573
583
|
}
|
|
574
584
|
};
|
|
575
585
|
|
|
@@ -584,8 +594,14 @@ module.exports = function(RED) {
|
|
|
584
594
|
const slave = parseInt(data.slave);
|
|
585
595
|
const coil = parseInt(data.coil);
|
|
586
596
|
const value = Boolean(data.value);
|
|
597
|
+
const masterId = data.masterId || "";
|
|
587
598
|
const triggerSource = data.triggerSource || data.source || 'unknown'; // 触发源:'button-press', 'relay-control', 'init', 'unknown'
|
|
588
599
|
|
|
600
|
+
// 检查主站关联(如果配置了主站,则只响应对应主站的消息)
|
|
601
|
+
if (node.config.modbusMaster && masterId && node.config.modbusMaster !== masterId) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
589
605
|
// 检查是否是我们关注的从站和线圈
|
|
590
606
|
// 识别首次轮询(source: 'init'),标记已接收初始状态
|
|
591
607
|
if (data.source === 'init' && !node.hasReceivedInitialState) {
|
|
@@ -867,7 +883,7 @@ module.exports = function(RED) {
|
|
|
867
883
|
// 不匹配的节点静默忽略,不输出任何日志
|
|
868
884
|
}
|
|
869
885
|
} catch (err) {
|
|
870
|
-
node.
|
|
886
|
+
node.log(`解析RS-485数据失败: ${err.message}`);
|
|
871
887
|
}
|
|
872
888
|
};
|
|
873
889
|
|
|
@@ -967,7 +983,7 @@ module.exports = function(RED) {
|
|
|
967
983
|
}
|
|
968
984
|
}
|
|
969
985
|
} catch (err) {
|
|
970
|
-
node.
|
|
986
|
+
node.log(`解析Clowire数据失败: ${err.message}`);
|
|
971
987
|
}
|
|
972
988
|
};
|
|
973
989
|
|
|
@@ -1004,7 +1020,7 @@ module.exports = function(RED) {
|
|
|
1004
1020
|
// 开关模式:正常处理状态
|
|
1005
1021
|
// 获取按钮状态
|
|
1006
1022
|
if (!event.states || event.states.length < node.config.meshButtonNumber) {
|
|
1007
|
-
node.
|
|
1023
|
+
node.log(`[Mesh事件] 按键${node.config.meshButtonNumber} 状态数据不完整,states=${JSON.stringify(event.states)}`);
|
|
1008
1024
|
return; // 状态数据不完整
|
|
1009
1025
|
}
|
|
1010
1026
|
|
|
@@ -1098,7 +1114,7 @@ module.exports = function(RED) {
|
|
|
1098
1114
|
node.sendMqttCommand(buttonState, isSceneMode);
|
|
1099
1115
|
|
|
1100
1116
|
} catch (err) {
|
|
1101
|
-
node.
|
|
1117
|
+
node.log(`解析Mesh数据失败: ${err.message}`);
|
|
1102
1118
|
}
|
|
1103
1119
|
};
|
|
1104
1120
|
|
|
@@ -1113,7 +1129,7 @@ module.exports = function(RED) {
|
|
|
1113
1129
|
|
|
1114
1130
|
node.mqttClient.publish(commandTopic, payload, { qos: 1 }, (err) => {
|
|
1115
1131
|
if (err) {
|
|
1116
|
-
node.
|
|
1132
|
+
node.log(`MQTT发送失败: ${err.message}`);
|
|
1117
1133
|
} else {
|
|
1118
1134
|
node.debug(`MQTT模式:发送命令到${commandTopic} = ${payload}`);
|
|
1119
1135
|
}
|
|
@@ -1130,7 +1146,9 @@ module.exports = function(RED) {
|
|
|
1130
1146
|
value: state,
|
|
1131
1147
|
source: 'slave-switch',
|
|
1132
1148
|
triggerSource: isSceneMode ? 'scene-trigger' : 'button-press', // 场景模式使用scene-trigger,开关模式使用button-press
|
|
1133
|
-
nodeId: node.id
|
|
1149
|
+
nodeId: node.id,
|
|
1150
|
+
masterId: node.config.modbusMaster || "", // 指定目标主站ID
|
|
1151
|
+
serialPortConfigId: config.serialPortConfig // 增加串口配置ID,用于主站过滤,防止多主站冲突
|
|
1134
1152
|
});
|
|
1135
1153
|
|
|
1136
1154
|
// 内部事件模式:静默发送(不输出日志)
|
|
@@ -1153,7 +1171,7 @@ module.exports = function(RED) {
|
|
|
1153
1171
|
|
|
1154
1172
|
// 再次检查是否过期
|
|
1155
1173
|
if (Date.now() - item.timestamp >= node.queueTimeout) {
|
|
1156
|
-
node.
|
|
1174
|
+
node.log(`丢弃过期命令(${Date.now() - item.timestamp}ms)`);
|
|
1157
1175
|
continue;
|
|
1158
1176
|
}
|
|
1159
1177
|
|
|
@@ -1179,7 +1197,7 @@ module.exports = function(RED) {
|
|
|
1179
1197
|
await new Promise(resolve => setTimeout(resolve, 40));
|
|
1180
1198
|
}
|
|
1181
1199
|
} catch (err) {
|
|
1182
|
-
node.
|
|
1200
|
+
node.log(`MQTT发送失败: ${err.message}`);
|
|
1183
1201
|
}
|
|
1184
1202
|
}
|
|
1185
1203
|
|
|
@@ -1195,7 +1213,7 @@ module.exports = function(RED) {
|
|
|
1195
1213
|
if (!node.serialPortConfig || !node.serialPortConfig.connection) {
|
|
1196
1214
|
// 初始化期间静默警告
|
|
1197
1215
|
if (!node.isInitializing) {
|
|
1198
|
-
node.
|
|
1216
|
+
node.log('RS-485连接未建立,无法发送指示灯反馈');
|
|
1199
1217
|
}
|
|
1200
1218
|
node.debug(`[sendCommandToPanel] 连接未建立,退出`);
|
|
1201
1219
|
return;
|
|
@@ -1258,7 +1276,7 @@ module.exports = function(RED) {
|
|
|
1258
1276
|
);
|
|
1259
1277
|
|
|
1260
1278
|
if (!command) {
|
|
1261
|
-
node.
|
|
1279
|
+
node.log(`构建Mesh控制帧失败`);
|
|
1262
1280
|
return;
|
|
1263
1281
|
}
|
|
1264
1282
|
|
|
@@ -1286,7 +1304,7 @@ module.exports = function(RED) {
|
|
|
1286
1304
|
);
|
|
1287
1305
|
|
|
1288
1306
|
if (!command) {
|
|
1289
|
-
node.
|
|
1307
|
+
node.log(`构建Clowire LED控制帧失败`);
|
|
1290
1308
|
return;
|
|
1291
1309
|
}
|
|
1292
1310
|
|
|
@@ -1324,7 +1342,7 @@ module.exports = function(RED) {
|
|
|
1324
1342
|
|
|
1325
1343
|
node.serialPortConfig.write(command, (err) => {
|
|
1326
1344
|
if (err) {
|
|
1327
|
-
node.
|
|
1345
|
+
node.log(`LED反馈失败: ${err.message}`);
|
|
1328
1346
|
} else {
|
|
1329
1347
|
// LED反馈发送成功(静默,不输出日志)
|
|
1330
1348
|
}
|
|
@@ -1381,8 +1399,8 @@ module.exports = function(RED) {
|
|
|
1381
1399
|
|
|
1382
1400
|
// 验证MQTT broker配置
|
|
1383
1401
|
if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
|
|
1384
|
-
node.
|
|
1385
|
-
node.
|
|
1402
|
+
node.log('MQTT已启用但broker地址未配置 - 使用本地模式');
|
|
1403
|
+
node.log('提示:请在MQTT服务器配置节点中设置broker地址,或禁用MQTT功能');
|
|
1386
1404
|
return;
|
|
1387
1405
|
}
|
|
1388
1406
|
|
|
@@ -1431,7 +1449,7 @@ module.exports = function(RED) {
|
|
|
1431
1449
|
// 订阅状态主题(QoS=1确保状态更新不丢失)
|
|
1432
1450
|
node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
|
|
1433
1451
|
if (err) {
|
|
1434
|
-
node.
|
|
1452
|
+
node.log(`订阅失败: ${err.message}`);
|
|
1435
1453
|
}
|
|
1436
1454
|
});
|
|
1437
1455
|
});
|
|
@@ -1570,7 +1588,7 @@ module.exports = function(RED) {
|
|
|
1570
1588
|
});
|
|
1571
1589
|
|
|
1572
1590
|
} catch (err) {
|
|
1573
|
-
node.
|
|
1591
|
+
node.log(`MQTT连接异常: ${err.message}`);
|
|
1574
1592
|
|
|
1575
1593
|
// 尝试下一个候选地址
|
|
1576
1594
|
currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
|
|
@@ -1622,7 +1640,7 @@ module.exports = function(RED) {
|
|
|
1622
1640
|
const command = value ? 'ON' : 'OFF';
|
|
1623
1641
|
node.mqttClient.publish(node.commandTopic, command, { qos: 1 }, (err) => {
|
|
1624
1642
|
if (err) {
|
|
1625
|
-
node.
|
|
1643
|
+
node.log(`发布命令失败: ${err.message}`);
|
|
1626
1644
|
}
|
|
1627
1645
|
});
|
|
1628
1646
|
} else {
|
|
@@ -1671,7 +1689,7 @@ module.exports = function(RED) {
|
|
|
1671
1689
|
try {
|
|
1672
1690
|
node.serialPortConfig.unregisterDataListener(node.serialDataListener);
|
|
1673
1691
|
} catch (err) {
|
|
1674
|
-
node.
|
|
1692
|
+
node.log(`注销RS-485监听器时出错: ${err.message}`);
|
|
1675
1693
|
}
|
|
1676
1694
|
node.serialDataListener = null;
|
|
1677
1695
|
}
|
|
@@ -1700,7 +1718,7 @@ module.exports = function(RED) {
|
|
|
1700
1718
|
done();
|
|
1701
1719
|
}
|
|
1702
1720
|
} catch (err) {
|
|
1703
|
-
node.
|
|
1721
|
+
node.log(`关闭MQTT连接时出错: ${err.message}`);
|
|
1704
1722
|
node.mqttClient = null;
|
|
1705
1723
|
node.mqttConnected = false;
|
|
1706
1724
|
done();
|
|
@@ -31,7 +31,8 @@ module.exports = function(RED) {
|
|
|
31
31
|
|
|
32
32
|
// 错误日志限流(避免断网时产生大量日志)
|
|
33
33
|
node.lastErrorLog = 0; // 上次错误日志时间
|
|
34
|
-
node.
|
|
34
|
+
node.lastErrorMsg = ""; // 记录上一次错误消息
|
|
35
|
+
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔提高到10分钟
|
|
35
36
|
|
|
36
37
|
// 数据监听器列表(每个从站开关节点注册一个)
|
|
37
38
|
node.dataListeners = [];
|
|
@@ -52,25 +53,28 @@ module.exports = function(RED) {
|
|
|
52
53
|
try {
|
|
53
54
|
// 检查监听器数量(正常情况下不应超过100个)
|
|
54
55
|
if (node.dataListeners.length > 100) {
|
|
55
|
-
node.
|
|
56
|
+
node.log(`监听器数量异常: ${node.dataListeners.length},可能存在内存泄漏`);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// 检查写入队列长度(正常情况下不应超过1000个)
|
|
59
60
|
if (node.writeQueue.length > 1000) {
|
|
60
|
-
node.
|
|
61
|
+
node.log(`写入队列积压严重: ${node.writeQueue.length},自动清理旧数据`);
|
|
61
62
|
// 只保留最新的100个写入请求
|
|
62
63
|
const removed = node.writeQueue.length - 100;
|
|
63
64
|
node.writeQueue = node.writeQueue.slice(-100);
|
|
64
|
-
node.
|
|
65
|
+
node.log(`已清理${removed}个积压的写入请求`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// 检查帧缓冲区大小(正常情况下不应超过10KB)
|
|
68
69
|
if (node.frameBuffer && node.frameBuffer.length > 10000) {
|
|
69
|
-
node.
|
|
70
|
+
node.log(`帧缓冲区过大: ${node.frameBuffer.length}字节,自动清空`);
|
|
70
71
|
node.frameBuffer = Buffer.alloc(0);
|
|
71
72
|
}
|
|
72
73
|
} catch (err) {
|
|
73
|
-
node.
|
|
74
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
75
|
+
node.log(`内存检查异常: ${err.message}`);
|
|
76
|
+
node.lastErrorLog = now;
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
}, 5 * 60 * 1000); // 每5分钟检查一次
|
|
76
80
|
|
|
@@ -107,25 +111,31 @@ module.exports = function(RED) {
|
|
|
107
111
|
try {
|
|
108
112
|
listener(data);
|
|
109
113
|
} catch (err) {
|
|
110
|
-
node.
|
|
114
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
115
|
+
node.log(`数据监听器异常: ${err.message}`);
|
|
116
|
+
node.lastErrorLog = now;
|
|
117
|
+
}
|
|
111
118
|
}
|
|
112
119
|
});
|
|
113
120
|
});
|
|
114
121
|
|
|
115
122
|
// 监听超时
|
|
116
123
|
node.connection.on('timeout', () => {
|
|
117
|
-
node.
|
|
124
|
+
node.log('TCP连接超时,尝试重连...');
|
|
118
125
|
if (node.connection) {
|
|
119
126
|
node.connection.destroy();
|
|
120
127
|
}
|
|
121
128
|
});
|
|
122
129
|
|
|
123
|
-
//
|
|
130
|
+
// 监听错误(静默处理,避免刷屏)
|
|
124
131
|
node.connection.on('error', (err) => {
|
|
125
132
|
const now = Date.now();
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
const errMsg = err.message || "";
|
|
134
|
+
// 只有错误消息变化,或者间隔时间到了才打印一条 log(不输出到调试面板)
|
|
135
|
+
if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
|
|
136
|
+
node.log(`TCP连接异常: ${errMsg}`);
|
|
128
137
|
node.lastErrorLog = now;
|
|
138
|
+
node.lastErrorMsg = errMsg;
|
|
129
139
|
}
|
|
130
140
|
// 不在这里重连,在close事件中统一处理
|
|
131
141
|
});
|
|
@@ -152,7 +162,7 @@ module.exports = function(RED) {
|
|
|
152
162
|
|
|
153
163
|
const nowLog = Date.now();
|
|
154
164
|
if (nowLog - node.lastErrorLog > node.errorLogInterval) {
|
|
155
|
-
node.
|
|
165
|
+
node.debug(`[SerialPortConfig] ${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
|
|
156
166
|
node.lastErrorLog = nowLog;
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -163,7 +173,11 @@ module.exports = function(RED) {
|
|
|
163
173
|
}
|
|
164
174
|
});
|
|
165
175
|
} catch (err) {
|
|
166
|
-
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
178
|
+
node.log(`TCP连接初始化异常: ${err.message}`);
|
|
179
|
+
node.lastErrorLog = now;
|
|
180
|
+
}
|
|
167
181
|
}
|
|
168
182
|
};
|
|
169
183
|
|
|
@@ -215,9 +229,11 @@ module.exports = function(RED) {
|
|
|
215
229
|
|
|
216
230
|
if (err) {
|
|
217
231
|
const now = Date.now();
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
const errMsg = err.message || "";
|
|
233
|
+
if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
|
|
234
|
+
node.log(`串口打开失败: ${errMsg}`);
|
|
220
235
|
node.lastErrorLog = now;
|
|
236
|
+
node.lastErrorMsg = errMsg;
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
// 打开失败时也要触发重连(如果有监听器在使用)
|
|
@@ -227,7 +243,7 @@ module.exports = function(RED) {
|
|
|
227
243
|
|
|
228
244
|
const nowLog = Date.now();
|
|
229
245
|
if (nowLog - node.lastErrorLog > node.errorLogInterval) {
|
|
230
|
-
node.
|
|
246
|
+
node.debug(`[SerialPortConfig] ${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
|
|
231
247
|
node.lastErrorLog = nowLog;
|
|
232
248
|
}
|
|
233
249
|
|
|
@@ -251,7 +267,7 @@ module.exports = function(RED) {
|
|
|
251
267
|
} catch (err) {
|
|
252
268
|
const now = Date.now();
|
|
253
269
|
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
254
|
-
node.
|
|
270
|
+
node.log(`数据监听器错误: ${err.message}`);
|
|
255
271
|
node.lastErrorLog = now;
|
|
256
272
|
}
|
|
257
273
|
}
|
|
@@ -261,9 +277,11 @@ module.exports = function(RED) {
|
|
|
261
277
|
// 监听错误(限流日志)
|
|
262
278
|
node.connection.on('error', (err) => {
|
|
263
279
|
const now = Date.now();
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
const errMsg = err.message || "";
|
|
281
|
+
if (errMsg !== node.lastErrorMsg || (now - node.lastErrorLog > node.errorLogInterval)) {
|
|
282
|
+
node.log(`串口运行错误: ${errMsg}`);
|
|
266
283
|
node.lastErrorLog = now;
|
|
284
|
+
node.lastErrorMsg = errMsg;
|
|
267
285
|
}
|
|
268
286
|
// 不在这里重连,在close事件中统一处理
|
|
269
287
|
});
|
|
@@ -310,8 +328,12 @@ module.exports = function(RED) {
|
|
|
310
328
|
});
|
|
311
329
|
} catch (err) {
|
|
312
330
|
node.isOpening = false;
|
|
313
|
-
|
|
314
|
-
|
|
331
|
+
const now = Date.now();
|
|
332
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
333
|
+
node.log(`串口初始化异常: ${err.message}`);
|
|
334
|
+
node.lastErrorLog = now;
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
// 初始化失败时也要触发重连
|
|
316
338
|
if (!node.isClosing && node.dataListeners.length > 0) {
|
|
317
339
|
const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000);
|
|
@@ -334,7 +356,7 @@ module.exports = function(RED) {
|
|
|
334
356
|
// 注册数据监听器
|
|
335
357
|
node.registerDataListener = function(listener) {
|
|
336
358
|
if (typeof listener !== 'function') {
|
|
337
|
-
node.
|
|
359
|
+
node.log('监听器必须是函数');
|
|
338
360
|
return;
|
|
339
361
|
}
|
|
340
362
|
|
|
@@ -433,7 +455,11 @@ module.exports = function(RED) {
|
|
|
433
455
|
await new Promise((resolve, reject) => {
|
|
434
456
|
node.connection.write(data, (err) => {
|
|
435
457
|
if (err) {
|
|
436
|
-
|
|
458
|
+
const now = Date.now();
|
|
459
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
460
|
+
node.log(`TCP写入异常: ${err.message}`);
|
|
461
|
+
node.lastErrorLog = now;
|
|
462
|
+
}
|
|
437
463
|
if (callback) callback(err);
|
|
438
464
|
reject(err);
|
|
439
465
|
} else {
|
|
@@ -442,7 +468,7 @@ module.exports = function(RED) {
|
|
|
442
468
|
try {
|
|
443
469
|
listener(data, 'sent');
|
|
444
470
|
} catch (e) {
|
|
445
|
-
node.
|
|
471
|
+
node.log('监听器处理发送数据失败: ' + e.message);
|
|
446
472
|
}
|
|
447
473
|
});
|
|
448
474
|
|
|
@@ -467,7 +493,11 @@ module.exports = function(RED) {
|
|
|
467
493
|
await new Promise((resolve, reject) => {
|
|
468
494
|
node.connection.write(data, (err) => {
|
|
469
495
|
if (err) {
|
|
470
|
-
|
|
496
|
+
const now = Date.now();
|
|
497
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
498
|
+
node.log(`串口写入异常: ${err.message}`);
|
|
499
|
+
node.lastErrorLog = now;
|
|
500
|
+
}
|
|
471
501
|
if (callback) callback(err);
|
|
472
502
|
reject(err);
|
|
473
503
|
} else {
|
|
@@ -476,7 +506,7 @@ module.exports = function(RED) {
|
|
|
476
506
|
try {
|
|
477
507
|
listener(data, 'sent');
|
|
478
508
|
} catch (e) {
|
|
479
|
-
node.
|
|
509
|
+
node.log('监听器处理发送数据失败: ' + e.message);
|
|
480
510
|
}
|
|
481
511
|
});
|
|
482
512
|
|
|
@@ -493,7 +523,7 @@ module.exports = function(RED) {
|
|
|
493
523
|
}
|
|
494
524
|
} catch (err) {
|
|
495
525
|
// 写入失败,继续处理下一个
|
|
496
|
-
node.
|
|
526
|
+
node.log(`写入失败(已跳过): ${err.message}`);
|
|
497
527
|
}
|
|
498
528
|
}
|
|
499
529
|
|
|
@@ -538,7 +568,7 @@ module.exports = function(RED) {
|
|
|
538
568
|
node.connection.destroy();
|
|
539
569
|
}
|
|
540
570
|
} catch (err) {
|
|
541
|
-
node.
|
|
571
|
+
node.log(`关闭TCP连接时出错: ${err.message}`);
|
|
542
572
|
}
|
|
543
573
|
node.connection = null;
|
|
544
574
|
done();
|
|
@@ -548,7 +578,7 @@ module.exports = function(RED) {
|
|
|
548
578
|
if (node.connection.isOpen) {
|
|
549
579
|
node.connection.close((err) => {
|
|
550
580
|
if (err) {
|
|
551
|
-
node.
|
|
581
|
+
node.log(`串口关闭失败: ${err.message}`);
|
|
552
582
|
}
|
|
553
583
|
node.connection = null;
|
|
554
584
|
done();
|
|
@@ -558,7 +588,7 @@ module.exports = function(RED) {
|
|
|
558
588
|
done();
|
|
559
589
|
}
|
|
560
590
|
} catch (err) {
|
|
561
|
-
node.
|
|
591
|
+
node.log(`关闭串口连接时出错: ${err.message}`);
|
|
562
592
|
node.connection = null;
|
|
563
593
|
done();
|
|
564
594
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.10",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|