node-red-contrib-symi-modbus 2.9.8 → 2.9.9
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 +13 -3
- package/nodes/modbus-master.js +72 -24
- package/nodes/modbus-server-config.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -471,8 +471,8 @@ node-red-restart
|
|
|
471
471
|
- 连接前彻底清理旧实例,避免资源泄漏
|
|
472
472
|
- **互斥锁机制**:防止读写冲突导致的数据异常
|
|
473
473
|
- **TCP永久连接**:
|
|
474
|
-
|
|
475
|
-
|
|
474
|
+
- 禁用TCP超时(永久连接),避免无数据时超时断开
|
|
475
|
+
- Keep-Alive心跳10秒间隔,确保连接活跃
|
|
476
476
|
- 适应客户长期不在家、总线无数据的场景
|
|
477
477
|
- 网络故障自动重连,恢复后立即恢复通信
|
|
478
478
|
|
|
@@ -952,7 +952,17 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
952
952
|
|
|
953
953
|
## 版本信息
|
|
954
954
|
|
|
955
|
-
**当前版本**: v2.9.
|
|
955
|
+
**当前版本**: v2.9.9 (2026-01-08)
|
|
956
|
+
|
|
957
|
+
**v2.9.9 更新内容**:
|
|
958
|
+
- **深度优化多 TCP 主站并发**:
|
|
959
|
+
- **独立主站标识**:为每个 TCP 主站生成唯一标识,支持多个 TCP 连接独立运行,互不干扰
|
|
960
|
+
- **全局实例管理**:建立全局实例表,解决多节点并发时的资源抢占和卡顿问题
|
|
961
|
+
- **TCP 参数调优**:
|
|
962
|
+
- 降低 TCP 超时时间至 1000ms,提升异常响应速度
|
|
963
|
+
- 启用 Keep-Alive(10秒心跳),确保长连接稳定性
|
|
964
|
+
- 开启 NoDelay,禁用 Nagle 算法,显著降低指令发送延迟
|
|
965
|
+
- **稳定性增强**:完善节点关闭时的资源释放逻辑,防止内存泄漏和连接残留
|
|
956
966
|
|
|
957
967
|
**v2.9.8 更新内容**:
|
|
958
968
|
- **新增品牌支持**:Clowire(克伦威尔)智能面板
|
package/nodes/modbus-master.js
CHANGED
|
@@ -3,7 +3,11 @@ module.exports = function(RED) {
|
|
|
3
3
|
const ModbusRTU = require("modbus-serial");
|
|
4
4
|
const mqtt = require("mqtt");
|
|
5
5
|
const protocol = require("./lightweight-protocol");
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// 全局主站实例注册表(用于多主站独立运行,避免互相干扰)
|
|
8
|
+
// 每个主站节点有独立的Modbus客户端实例,互不影响
|
|
9
|
+
const masterInstances = new Map();
|
|
10
|
+
|
|
7
11
|
// 串口列表API - 支持Windows、Linux、macOS所有串口设备
|
|
8
12
|
RED.httpAdmin.get('/modbus-master/serialports', async function(req, res) {
|
|
9
13
|
try {
|
|
@@ -90,13 +94,16 @@ module.exports = function(RED) {
|
|
|
90
94
|
function ModbusMasterNode(config) {
|
|
91
95
|
RED.nodes.createNode(this, config);
|
|
92
96
|
const node = this;
|
|
93
|
-
|
|
97
|
+
|
|
98
|
+
// 生成唯一的主站标识(用于多主站独立运行)
|
|
99
|
+
node.masterId = `master_${node.id}`;
|
|
100
|
+
|
|
94
101
|
// 获取Modbus服务器配置节点
|
|
95
102
|
node.modbusServerConfig = RED.nodes.getNode(config.modbusServer);
|
|
96
|
-
|
|
103
|
+
|
|
97
104
|
// 获取MQTT服务器配置节点
|
|
98
105
|
node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
|
|
99
|
-
|
|
106
|
+
|
|
100
107
|
// 配置参数(从Modbus服务器配置节点读取)
|
|
101
108
|
node.config = {
|
|
102
109
|
connectionType: node.modbusServerConfig ? (node.modbusServerConfig.connectionType || "tcp") : "tcp",
|
|
@@ -120,13 +127,19 @@ module.exports = function(RED) {
|
|
|
120
127
|
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
121
128
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay"
|
|
122
129
|
};
|
|
123
|
-
|
|
130
|
+
|
|
124
131
|
// 验证Modbus服务器配置
|
|
125
132
|
if (!node.modbusServerConfig) {
|
|
126
133
|
node.warn('未配置Modbus服务器,将使用默认配置');
|
|
127
134
|
}
|
|
128
|
-
|
|
129
|
-
//
|
|
135
|
+
|
|
136
|
+
// 记录主站配置信息(用于调试多主站问题)
|
|
137
|
+
const connInfo = node.config.connectionType === "tcp"
|
|
138
|
+
? `TCP ${node.config.tcpHost}:${node.config.tcpPort}`
|
|
139
|
+
: `串口 ${node.config.serialPort}`;
|
|
140
|
+
node.log(`主站初始化: ${node.masterId}, 连接: ${connInfo}`);
|
|
141
|
+
|
|
142
|
+
// Modbus客户端(每个主站独立的客户端实例)
|
|
130
143
|
node.client = new ModbusRTU();
|
|
131
144
|
node.isConnected = false;
|
|
132
145
|
node.pollTimer = null;
|
|
@@ -139,7 +152,7 @@ module.exports = function(RED) {
|
|
|
139
152
|
node.isClosing = false;
|
|
140
153
|
node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
|
|
141
154
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
142
|
-
node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5
|
|
155
|
+
node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5分钟
|
|
143
156
|
node.consecutiveErrors = {}; // 记录每个从站的连续错误次数
|
|
144
157
|
node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
|
|
145
158
|
node.lastWriteTime = {}; // 记录每个从站的最后写入时间
|
|
@@ -152,6 +165,15 @@ module.exports = function(RED) {
|
|
|
152
165
|
node.isProcessingWrite = false; // 是否正在处理写入队列
|
|
153
166
|
node.writeQueueInterval = 50; // 写入队列处理间隔(50ms,厂家推荐间隔,确保总线稳定)
|
|
154
167
|
|
|
168
|
+
// 注册到全局主站实例表(用于调试和监控)
|
|
169
|
+
masterInstances.set(node.masterId, {
|
|
170
|
+
nodeId: node.id,
|
|
171
|
+
connection: connInfo,
|
|
172
|
+
slaves: node.config.slaves.map(s => s.address),
|
|
173
|
+
startTime: Date.now()
|
|
174
|
+
});
|
|
175
|
+
node.log(`已注册主站实例,当前主站数量: ${masterInstances.size}`);
|
|
176
|
+
|
|
155
177
|
// 定期清理机制(每小时清理一次,防止内存泄漏)
|
|
156
178
|
node.cleanupTimer = setInterval(() => {
|
|
157
179
|
// 清理过期的错误日志记录
|
|
@@ -244,7 +266,7 @@ module.exports = function(RED) {
|
|
|
244
266
|
throw new Error('TCP主机地址未配置,请在节点配置中填写Modbus服务器IP地址');
|
|
245
267
|
}
|
|
246
268
|
|
|
247
|
-
const tcpMode = node.
|
|
269
|
+
const tcpMode = node.modbusServerConfig ? (node.modbusServerConfig.tcpMode || "telnet") : "telnet";
|
|
248
270
|
const modeNames = {
|
|
249
271
|
"telnet": "Telnet ASCII",
|
|
250
272
|
"rtu": "RTU over TCP",
|
|
@@ -253,24 +275,35 @@ module.exports = function(RED) {
|
|
|
253
275
|
|
|
254
276
|
node.log(`正在连接TCP网关(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}...`);
|
|
255
277
|
|
|
278
|
+
// TCP连接选项(优化多主站并发性能)
|
|
279
|
+
const tcpOptions = {
|
|
280
|
+
port: node.config.tcpPort,
|
|
281
|
+
// 注意:modbus-serial内部会设置socket选项
|
|
282
|
+
};
|
|
283
|
+
|
|
256
284
|
if (tcpMode === "telnet") {
|
|
257
|
-
await node.client.connectTelnet(node.config.tcpHost,
|
|
258
|
-
port: node.config.tcpPort
|
|
259
|
-
});
|
|
285
|
+
await node.client.connectTelnet(node.config.tcpHost, tcpOptions);
|
|
260
286
|
} else if (tcpMode === "rtu") {
|
|
261
|
-
await node.client.connectTcpRTUBuffered(node.config.tcpHost,
|
|
262
|
-
port: node.config.tcpPort
|
|
263
|
-
});
|
|
287
|
+
await node.client.connectTcpRTUBuffered(node.config.tcpHost, tcpOptions);
|
|
264
288
|
} else {
|
|
265
|
-
await node.client.connectTCP(node.config.tcpHost,
|
|
266
|
-
|
|
267
|
-
|
|
289
|
+
await node.client.connectTCP(node.config.tcpHost, tcpOptions);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 优化TCP socket设置(提升多主站并发性能)
|
|
293
|
+
if (node.client._client) {
|
|
294
|
+
// 启用TCP Keep-Alive,10秒间隔
|
|
295
|
+
node.client._client.setKeepAlive(true, 10000);
|
|
296
|
+
// 禁用Nagle算法,减少延迟
|
|
297
|
+
node.client._client.setNoDelay(true);
|
|
298
|
+
// 设置socket超时为0(永不超时,由Modbus层控制超时)
|
|
299
|
+
node.client._client.setTimeout(0);
|
|
300
|
+
node.log('TCP socket优化: Keep-Alive=10s, NoDelay=true');
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
node.log(`TCP网关连接成功(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}`);
|
|
271
304
|
} else {
|
|
272
305
|
node.log(`正在连接串口: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps...`);
|
|
273
|
-
|
|
306
|
+
|
|
274
307
|
await node.client.connectRTUBuffered(node.config.serialPort, {
|
|
275
308
|
baudRate: node.config.serialBaudRate || 9600,
|
|
276
309
|
dataBits: node.config.serialDataBits || 8,
|
|
@@ -278,12 +311,14 @@ module.exports = function(RED) {
|
|
|
278
311
|
parity: node.config.serialParity || 'none',
|
|
279
312
|
lock: false // 禁用串口锁定,解决HassOS中"Cannot lock port"错误
|
|
280
313
|
});
|
|
281
|
-
|
|
314
|
+
|
|
282
315
|
node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
283
316
|
}
|
|
284
317
|
|
|
285
|
-
//
|
|
286
|
-
|
|
318
|
+
// 设置Modbus超时时间
|
|
319
|
+
// TCP模式:降低到1000ms,避免一个主站超时阻塞其他主站太久
|
|
320
|
+
// 串口模式:保持2000ms,串口通信需要更长时间
|
|
321
|
+
const timeout = node.config.connectionType === "serial" ? 2000 : 1000;
|
|
287
322
|
node.client.setTimeout(timeout);
|
|
288
323
|
node.log(`Modbus超时设置: ${timeout}ms`);
|
|
289
324
|
|
|
@@ -293,6 +328,13 @@ module.exports = function(RED) {
|
|
|
293
328
|
node.warn(`串口错误(已忽略): ${err.message}`);
|
|
294
329
|
});
|
|
295
330
|
}
|
|
331
|
+
|
|
332
|
+
// TCP模式:设置socket错误处理器
|
|
333
|
+
if (node.client._client) {
|
|
334
|
+
node.client._client.on('error', (err) => {
|
|
335
|
+
node.warn(`TCP socket错误(已忽略): ${err.message}`);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
296
338
|
|
|
297
339
|
node.isConnected = true;
|
|
298
340
|
node.reconnectAttempts = 0; // 重置重连计数
|
|
@@ -1525,6 +1567,12 @@ module.exports = function(RED) {
|
|
|
1525
1567
|
node.isClosing = true;
|
|
1526
1568
|
node.stopPolling();
|
|
1527
1569
|
|
|
1570
|
+
// 从全局主站实例表中移除
|
|
1571
|
+
if (node.masterId) {
|
|
1572
|
+
masterInstances.delete(node.masterId);
|
|
1573
|
+
node.log(`已注销主站实例 ${node.masterId},剩余主站数量: ${masterInstances.size}`);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1528
1576
|
// 移除内部事件监听器
|
|
1529
1577
|
if (node.internalEventHandler) {
|
|
1530
1578
|
RED.events.removeListener('modbus:writeCoil', node.internalEventHandler);
|
|
@@ -1553,13 +1601,13 @@ module.exports = function(RED) {
|
|
|
1553
1601
|
if (node.config.enableMqtt && node.mqttClient && node.mqttClient.connected) {
|
|
1554
1602
|
node.publishOfflineStatus();
|
|
1555
1603
|
}
|
|
1556
|
-
|
|
1604
|
+
|
|
1557
1605
|
// 清理设备状态缓存(释放内存)
|
|
1558
1606
|
node.deviceStates = {};
|
|
1559
1607
|
node.lastErrorLog = {};
|
|
1560
1608
|
node.lastWriteTime = {};
|
|
1561
1609
|
node.lastMqttErrorLog = 0;
|
|
1562
|
-
|
|
1610
|
+
|
|
1563
1611
|
// 关闭Modbus连接
|
|
1564
1612
|
if (node.client) {
|
|
1565
1613
|
try {
|
|
@@ -4,6 +4,7 @@ module.exports = function(RED) {
|
|
|
4
4
|
function ModbusServerConfigNode(config) {
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
6
6
|
this.connectionType = config.connectionType;
|
|
7
|
+
this.tcpMode = config.tcpMode || "telnet"; // TCP模式:telnet/rtu/tcp
|
|
7
8
|
this.tcpHost = config.tcpHost;
|
|
8
9
|
this.tcpPort = parseInt(config.tcpPort) || 502;
|
|
9
10
|
this.serialPort = config.serialPort;
|
|
@@ -12,7 +13,7 @@ module.exports = function(RED) {
|
|
|
12
13
|
this.serialStopBits = parseInt(config.serialStopBits) || 1;
|
|
13
14
|
this.serialParity = config.serialParity || "none";
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
RED.nodes.registerType("modbus-server-config", ModbusServerConfigNode);
|
|
17
18
|
};
|
|
18
19
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.9.
|
|
4
|
-
"description": "Node-RED Modbus节点,支持TCP
|
|
3
|
+
"version": "2.9.9",
|
|
4
|
+
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|