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 CHANGED
@@ -471,8 +471,8 @@ node-red-restart
471
471
  - 连接前彻底清理旧实例,避免资源泄漏
472
472
  - **互斥锁机制**:防止读写冲突导致的数据异常
473
473
  - **TCP永久连接**:
474
- - 禁用TCP超时(永久连接),避免无数据时超时断开
475
- - Keep-Alive心跳10秒间隔,确保连接活跃
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.8 (2026-01-06)
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(克伦威尔)智能面板
@@ -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
- // Modbus客户端
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分钟(降低到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.config.tcpMode || "telnet";
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
- port: node.config.tcpPort
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
- const timeout = node.config.connectionType === "serial" ? 3000 : 2000;
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.8",
4
- "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
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"