node-red-contrib-symi-modbus 1.3.0 → 1.5.4

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.
@@ -50,37 +50,68 @@
50
50
  container.empty();
51
51
 
52
52
  node.slaves.forEach(function(slave, index) {
53
- var slaveRow = $('<div class="slave-row" style="padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">');
53
+ var slaveRow = $('<div class="slave-row" style="padding: 14px; margin: 10px 0; border: 1px solid #e0e0e0; border-radius: 8px; background: linear-gradient(135deg, #ffffff 0%, #f9f9f9 100%); box-shadow: 0 2px 6px rgba(0,0,0,0.08); transition: all 0.3s;">');
54
54
 
55
55
  slaveRow.html(`
56
- <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
57
- <strong style="min-width: 80px;">从站 ${index + 1}:</strong>
58
- <label style="margin: 0 5px;">地址:</label>
59
- <input type="number" class="slave-address" data-index="${index}" value="${slave.address}" min="1" max="247" style="width: 60px;">
60
- <label style="margin: 0 5px;">线圈:</label>
61
- <input type="number" class="slave-coil-start" data-index="${index}" value="${slave.coilStart}" min="0" max="31" style="width: 50px;">
62
- <span>-</span>
63
- <input type="number" class="slave-coil-end" data-index="${index}" value="${slave.coilEnd}" min="0" max="31" style="width: 50px;">
64
- <label style="margin: 0 5px;">间隔(ms):</label>
65
- <input type="number" class="slave-poll-interval" data-index="${index}" value="${slave.pollInterval}" min="10" style="width: 60px;">
66
- <button type="button" class="btn-delete-slave" data-index="${index}" style="background: #d9534f; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer;">删除</button>
56
+ <div style="display: grid; grid-template-columns: auto 1fr auto; gap: 15px; align-items: center;">
57
+ <div style="display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 32px; background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%); color: white; border-radius: 50%; font-weight: bold; font-size: 14px; box-shadow: 0 2px 4px rgba(33,150,243,0.3);">${index + 1}</div>
58
+
59
+ <div style="display: grid; grid-template-columns: repeat(3, auto); gap: 20px; align-items: center;">
60
+ <div style="display: flex; flex-direction: column; gap: 4px;">
61
+ <label style="margin: 0; font-size: 11px; color: #666; font-weight: 500;">从站地址:</label>
62
+ <input type="number" class="slave-address" data-index="${index}" value="${slave.address}" min="1" max="247" style="width: 70px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
63
+ </div>
64
+
65
+ <div style="display: flex; flex-direction: column; gap: 4px;">
66
+ <label style="margin: 0; font-size: 11px; color: #666; font-weight: 500;">线圈范围:</label>
67
+ <div style="display: flex; align-items: center; gap: 5px;">
68
+ <input type="number" class="slave-coil-start" data-index="${index}" value="${slave.coilStart}" min="0" max="31" style="width: 50px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
69
+ <span style="color: #999;">-</span>
70
+ <input type="number" class="slave-coil-end" data-index="${index}" value="${slave.coilEnd}" min="0" max="31" style="width: 50px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
71
+ </div>
72
+ </div>
73
+
74
+ <div style="display: flex; flex-direction: column; gap: 4px;">
75
+ <label style="margin: 0; font-size: 11px; color: #666; font-weight: 500;">轮询间隔:</label>
76
+ <div style="display: flex; align-items: center; gap: 5px;">
77
+ <input type="number" class="slave-poll-interval" data-index="${index}" value="${slave.pollInterval}" min="10" style="width: 70px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
78
+ <span style="font-size: 11px; color: #888;">ms</span>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <button type="button" class="btn-delete-slave" data-index="${index}" style="background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); color: white; border: none; padding: 6px 12px; border-radius: 5px; cursor: pointer; font-size: 12px; box-shadow: 0 2px 4px rgba(244,67,54,0.3); transition: all 0.3s;">
84
+ <i class="fa fa-trash"></i> 删除
85
+ </button>
67
86
  </div>
68
87
  `);
69
88
 
89
+ // 添加hover效果
90
+ slaveRow.hover(
91
+ function() { $(this).css("box-shadow", "0 4px 12px rgba(0,0,0,0.15)"); },
92
+ function() { $(this).css("box-shadow", "0 2px 6px rgba(0,0,0,0.08)"); }
93
+ );
94
+
70
95
  container.append(slaveRow);
71
96
  });
72
97
 
73
98
  // 更新删除按钮状态(至少保留1个从站)
74
99
  if (node.slaves.length === 1) {
75
- $(".btn-delete-slave").prop("disabled", true).css("opacity", "0.5");
100
+ $(".btn-delete-slave").prop("disabled", true).css("opacity", "0.4").css("cursor", "not-allowed");
76
101
  }
77
102
 
78
103
  // 更新添加按钮状态(最多10个从站)
79
104
  if (node.slaves.length >= 10) {
80
- $("#btn-add-slave").prop("disabled", true);
105
+ $("#btn-add-slave").prop("disabled", true).css("opacity", "0.5").css("cursor", "not-allowed");
81
106
  } else {
82
- $("#btn-add-slave").prop("disabled", false);
107
+ $("#btn-add-slave").prop("disabled", false).css("opacity", "1").css("cursor", "pointer");
83
108
  }
109
+
110
+ // 添加按钮hover效果
111
+ $("#btn-add-slave").hover(
112
+ function() { if (!$(this).prop("disabled")) $(this).css("background", "linear-gradient(135deg, #66bb6a 0%, #5cb85c 100%)"); },
113
+ function() { if (!$(this).prop("disabled")) $(this).css("background", "linear-gradient(135deg, #5cb85c 0%, #4cae4c 100%)"); }
114
+ );
84
115
  }
85
116
 
86
117
  // 添加从站
@@ -167,6 +198,54 @@
167
198
 
168
199
  // 初始化MQTT显示
169
200
  $("#node-input-enableMqtt").trigger("change");
201
+
202
+ // 搜索串口按钮
203
+ $("#btn-search-ports-master").on("click", function() {
204
+ var btn = $(this);
205
+ var inputBox = $("#node-input-serialPort");
206
+ var selectBox = $("#port-list-master");
207
+
208
+ btn.prop("disabled", true).html('<i class="fa fa-spinner fa-spin"></i> 搜索中');
209
+
210
+ $.ajax({
211
+ url: 'modbus-master/serialports',
212
+ type: 'GET',
213
+ success: function(ports) {
214
+ selectBox.empty().append('<option value="">-- 选择检测到的串口 --</option>');
215
+
216
+ if (ports.length === 0) {
217
+ selectBox.append('<option disabled>未找到可用串口</option>');
218
+ RED.notify("未找到可用串口,请手动输入串口路径", "warning");
219
+ } else {
220
+ ports.forEach(function(port) {
221
+ var label = port.comName;
222
+ if (port.manufacturer && port.manufacturer !== '未知设备') {
223
+ label += ' - ' + port.manufacturer;
224
+ }
225
+ selectBox.append('<option value="' + port.comName + '">' + label + '</option>');
226
+ });
227
+ // 显示下拉框,隐藏输入框
228
+ inputBox.hide();
229
+ selectBox.show();
230
+ }
231
+
232
+ btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
233
+ },
234
+ error: function() {
235
+ RED.notify("搜索串口失败", "error");
236
+ btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
237
+ }
238
+ });
239
+ });
240
+
241
+ // 选择串口(下拉选择)
242
+ $("#port-list-master").on("change", function() {
243
+ var selectedPort = $(this).val();
244
+ if (selectedPort) {
245
+ $("#node-input-serialPort").val(selectedPort).show();
246
+ $(this).hide();
247
+ }
248
+ });
170
249
  },
171
250
  oneditsave: function() {
172
251
  // 保存从站配置到节点
@@ -176,14 +255,24 @@
176
255
  </script>
177
256
 
178
257
  <script type="text/html" data-template-name="modbus-master">
258
+ <!-- 基本配置 -->
179
259
  <div class="form-row">
180
260
  <label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
181
261
  <input type="text" id="node-input-name" placeholder="Modbus主站">
182
262
  </div>
183
263
 
264
+ <!-- 连接配置区域 -->
265
+ <hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
184
266
  <div class="form-row">
185
- <label for="node-input-connectionType"><i class="fa fa-plug"></i> 连接类型</label>
186
- <select id="node-input-connectionType">
267
+ <label style="width: 100%; margin-bottom: 8px;">
268
+ <i class="fa fa-plug" style="color: #2196f3;"></i>
269
+ <span style="font-size: 14px; font-weight: 600; color: #333;">连接配置</span>
270
+ </label>
271
+ </div>
272
+
273
+ <div class="form-row">
274
+ <label for="node-input-connectionType" style="width: 110px;"><i class="fa fa-exchange"></i> 连接类型</label>
275
+ <select id="node-input-connectionType" style="width: calc(70% - 110px);">
187
276
  <option value="tcp">TCP/IP</option>
188
277
  <option value="serial">串口</option>
189
278
  </select>
@@ -191,88 +280,114 @@
191
280
 
192
281
  <!-- TCP配置 -->
193
282
  <div class="form-row form-row-tcp">
194
- <label for="node-input-tcpHost"><i class="fa fa-server"></i> TCP主机</label>
195
- <input type="text" id="node-input-tcpHost" placeholder="127.0.0.1">
283
+ <label for="node-input-tcpHost" style="width: 110px;"><i class="fa fa-server"></i> TCP主机</label>
284
+ <input type="text" id="node-input-tcpHost" placeholder="192.168.1.100" style="width: calc(70% - 110px);">
285
+ <div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
286
+ Modbus TCP服务器IP地址
287
+ </div>
196
288
  </div>
197
289
 
198
290
  <div class="form-row form-row-tcp">
199
- <label for="node-input-tcpPort"><i class="fa fa-plug"></i> TCP端口</label>
200
- <input type="number" id="node-input-tcpPort" placeholder="502">
291
+ <label for="node-input-tcpPort" style="width: 110px;"><i class="fa fa-plug"></i> TCP端口</label>
292
+ <input type="number" id="node-input-tcpPort" placeholder="502" style="width: 100px;">
293
+ <span style="margin-left: 10px; font-size: 12px; color: #666;">默认 502</span>
201
294
  </div>
202
295
 
203
296
  <!-- 串口配置 -->
204
297
  <div class="form-row form-row-serial">
205
- <label for="node-input-serialPort"><i class="fa fa-terminal"></i> 串口</label>
206
- <input type="text" id="node-input-serialPort" placeholder="COM1">
298
+ <label for="node-input-serialPort" style="width: 110px;"><i class="fa fa-terminal"></i> 串口</label>
299
+ <div style="display: inline-block; width: calc(70% - 110px);">
300
+ <div style="display: flex; gap: 5px; align-items: center;">
301
+ <input type="text" id="node-input-serialPort" placeholder="COM1, /dev/ttyUSB0, /dev/ttyS1" style="flex: 1; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
302
+ <select id="port-list-master" style="flex: 1; padding: 5px; font-family: monospace; font-size: 12px; border: 1px solid #ccc; border-radius: 4px; display: none;">
303
+ <option value="">-- 选择检测到的串口 --</option>
304
+ </select>
305
+ <button type="button" id="btn-search-ports-master" style="padding: 6px 12px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; white-space: nowrap; transition: background 0.3s;">
306
+ <i class="fa fa-search"></i> 搜索
307
+ </button>
308
+ </div>
309
+ <div style="font-size: 11px; color: #888; margin-top: 3px;">
310
+ 支持COM1、/dev/ttyUSB0、/dev/ttyS1等串口设备
311
+ </div>
312
+ </div>
207
313
  </div>
208
314
 
209
315
  <div class="form-row form-row-serial">
210
- <label for="node-input-serialBaudRate"><i class="fa fa-tachometer"></i> 波特率</label>
211
- <select id="node-input-serialBaudRate">
316
+ <label for="node-input-serialBaudRate" style="width: 110px;"><i class="fa fa-tachometer"></i> 波特率</label>
317
+ <select id="node-input-serialBaudRate" style="width: 150px;">
212
318
  <option value="9600">9600</option>
213
319
  <option value="19200">19200</option>
214
320
  <option value="38400">38400</option>
215
321
  <option value="57600">57600</option>
216
322
  <option value="115200">115200</option>
217
323
  </select>
324
+ <span style="margin-left: 10px; font-size: 11px; color: #888;">8-N-1固定配置</span>
218
325
  </div>
219
326
 
220
- <div class="form-row form-row-serial">
221
- <label for="node-input-serialDataBits"><i class="fa fa-database"></i> 数据位</label>
222
- <select id="node-input-serialDataBits">
327
+ <div class="form-row form-row-serial" style="display: none;">
328
+ <label for="node-input-serialDataBits" style="width: 110px;"><i class="fa fa-database"></i> 数据位</label>
329
+ <select id="node-input-serialDataBits" style="width: 100px;">
223
330
  <option value="7">7</option>
224
- <option value="8">8</option>
331
+ <option value="8" selected>8</option>
225
332
  </select>
226
333
  </div>
227
334
 
228
- <div class="form-row form-row-serial">
229
- <label for="node-input-serialStopBits"><i class="fa fa-stop"></i> 停止位</label>
230
- <select id="node-input-serialStopBits">
231
- <option value="1">1</option>
335
+ <div class="form-row form-row-serial" style="display: none;">
336
+ <label for="node-input-serialStopBits" style="width: 110px;"><i class="fa fa-stop"></i> 停止位</label>
337
+ <select id="node-input-serialStopBits" style="width: 100px;">
338
+ <option value="1" selected>1</option>
232
339
  <option value="2">2</option>
233
340
  </select>
234
341
  </div>
235
342
 
236
- <div class="form-row form-row-serial">
237
- <label for="node-input-serialParity"><i class="fa fa-check"></i> 校验位</label>
238
- <select id="node-input-serialParity">
239
- <option value="none">无</option>
343
+ <div class="form-row form-row-serial" style="display: none;">
344
+ <label for="node-input-serialParity" style="width: 110px;"><i class="fa fa-check"></i> 校验位</label>
345
+ <select id="node-input-serialParity" style="width: 100px;">
346
+ <option value="none" selected>无</option>
240
347
  <option value="even">偶校验</option>
241
348
  <option value="odd">奇校验</option>
242
349
  </select>
243
350
  </div>
244
351
 
245
352
  <!-- 从站配置 -->
246
- <hr>
353
+ <hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
247
354
  <div class="form-row">
248
- <label style="width: 100%; margin-bottom: 10px;">
249
- <i class="fa fa-server"></i> 从站设备配置
250
- <span style="font-size: 11px; color: #999; margin-left: 10px;">(最多10台,默认从地址10=0x0A开始)</span>
355
+ <label style="width: 100%; margin-bottom: 8px;">
356
+ <i class="fa fa-server" style="color: #4caf50;"></i>
357
+ <span style="font-size: 14px; font-weight: 600; color: #333;">从站设备配置</span>
251
358
  </label>
359
+ <div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%); border-left: 4px solid #2196f3; border-radius: 4px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
360
+ <strong>配置说明:</strong>最多添加10台设备,地址从10开始递增(10=0x0A, 11=0x0B...),推荐轮询间隔≥200ms
361
+ </div>
252
362
  <div id="slave-list-container" style="width: 100%; margin-top: 10px;">
253
363
  <!-- 从站列表将在这里动态生成 -->
254
364
  </div>
255
- <button type="button" id="btn-add-slave" style="margin-top: 10px; padding: 6px 12px; background: #5cb85c; color: white; border: none; border-radius: 4px; cursor: pointer;">
256
- <i class="fa fa-plus"></i> 添加从站
365
+ <button type="button" id="btn-add-slave" style="margin-top: 12px; padding: 9px 18px; background: linear-gradient(135deg, #5cb85c 0%, #4cae4c 100%); color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 500; box-shadow: 0 2px 4px rgba(0,0,0,0.15); transition: all 0.3s;">
366
+ <i class="fa fa-plus"></i> 添加从站设备
257
367
  </button>
258
- <div style="font-size: 11px; color: #999; margin-top: 8px;">
259
- 💡 提示:默认地址10(0x0A)、11(0x0B)、12(0x0C)...,轮询间隔推荐200ms
260
- </div>
261
368
  </div>
262
369
 
263
370
  <!-- MQTT配置 -->
264
- <hr>
371
+ <hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
372
+ <div class="form-row">
373
+ <label style="width: 100%; margin-bottom: 8px;">
374
+ <i class="fa fa-cloud" style="color: #ff9800;"></i>
375
+ <span style="font-size: 14px; font-weight: 600; color: #333;">MQTT集成配置</span>
376
+ </label>
377
+ </div>
378
+
265
379
  <div class="form-row">
266
- <label for="node-input-enableMqtt"><i class="fa fa-toggle-on"></i> 启用MQTT</label>
267
- <input type="checkbox" id="node-input-enableMqtt" style="width: auto; margin-left: 10px;">
380
+ <label for="node-input-enableMqtt" style="width: 110px;"><i class="fa fa-toggle-on"></i> 启用MQTT</label>
381
+ <input type="checkbox" id="node-input-enableMqtt" style="width: auto; margin-left: 5px; transform: scale(1.3); cursor: pointer;">
382
+ <span style="margin-left: 15px; font-size: 11px; color: #666; font-style: italic;">用于Home Assistant集成和远程控制</span>
268
383
  </div>
269
384
 
270
385
  <div class="form-row form-row-mqtt">
271
- <label for="node-input-mqttServer"><i class="fa fa-server"></i> MQTT服务器</label>
272
- <input type="text" id="node-input-mqttServer" placeholder="选择或添加MQTT服务器配置">
273
- <div style="font-size: 11px; color: #999; margin-top: 3px; margin-left: 110px;">
386
+ <label for="node-input-mqttServer" style="width: 110px;"><i class="fa fa-server"></i> MQTT服务器</label>
387
+ <input type="text" id="node-input-mqttServer" placeholder="选择或添加MQTT服务器配置" style="width: calc(70% - 110px);">
388
+ <div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px; line-height: 1.5;">
274
389
  选择已配置的MQTT服务器,或点击编辑按钮添加新配置<br>
275
- 所有主站和从站节点可共享此配置
390
+ 所有主站和从站节点可共享此配置,确保使用同一服务器
276
391
  </div>
277
392
  </div>
278
393
  </script>
@@ -2,6 +2,58 @@ module.exports = function(RED) {
2
2
  "use strict";
3
3
  const ModbusRTU = require("modbus-serial");
4
4
  const mqtt = require("mqtt");
5
+
6
+ // 串口列表API - 支持Windows、Linux、macOS所有串口设备
7
+ RED.httpAdmin.get('/modbus-master/serialports', async function(req, res) {
8
+ try {
9
+ // 尝试从多个可能的位置获取serialport模块
10
+ let SerialPort;
11
+ try {
12
+ // 优先尝试使用node_modules中的serialport
13
+ SerialPort = require('serialport');
14
+ } catch (e) {
15
+ try {
16
+ // 如果失败,尝试从modbus-serial的依赖中获取
17
+ const ModbusRTU = require('modbus-serial');
18
+ SerialPort = ModbusRTU.SerialPort || require('serialport');
19
+ } catch (e2) {
20
+ // 两种方式都失败,返回空列表
21
+ return res.json([]);
22
+ }
23
+ }
24
+
25
+ // serialport v10+ (使用SerialPort.SerialPort.list)
26
+ if (SerialPort && SerialPort.SerialPort && SerialPort.SerialPort.list) {
27
+ const ports = await SerialPort.SerialPort.list();
28
+ const portList = ports.map(port => ({
29
+ comName: port.path || port.comName,
30
+ manufacturer: port.manufacturer || '未知设备',
31
+ vendorId: port.vendorId || '',
32
+ productId: port.productId || ''
33
+ }));
34
+ return res.json(portList);
35
+ }
36
+
37
+ // serialport v9 (使用SerialPort.list)
38
+ if (SerialPort && SerialPort.list) {
39
+ const ports = await SerialPort.list();
40
+ const portList = ports.map(port => ({
41
+ comName: port.path || port.comName,
42
+ manufacturer: port.manufacturer || '未知设备',
43
+ vendorId: port.vendorId || '',
44
+ productId: port.productId || ''
45
+ }));
46
+ return res.json(portList);
47
+ }
48
+
49
+ // 如果以上方法都不可用,返回空列表
50
+ res.json([]);
51
+ } catch (err) {
52
+ // 发生错误时记录日志并返回空列表
53
+ RED.log.warn(`串口列表获取失败: ${err.message}`);
54
+ res.json([]);
55
+ }
56
+ });
5
57
 
6
58
  function ModbusMasterNode(config) {
7
59
  RED.nodes.createNode(this, config);
@@ -43,6 +95,9 @@ module.exports = function(RED) {
43
95
  node.deviceStates = {}; // 存储每个设备的状态
44
96
  node.mqttClient = null;
45
97
  node.isClosing = false;
98
+ node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
99
+ node.lastMqttErrorLog = 0; // MQTT错误日志时间
100
+ node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
46
101
 
47
102
  // 初始化设备状态(基于从站配置列表)
48
103
  node.config.slaves.forEach((slave) => {
@@ -77,6 +132,10 @@ module.exports = function(RED) {
77
132
  node.isConnected = true;
78
133
  node.status({fill: "green", shape: "dot", text: "已连接"});
79
134
 
135
+ // 清除错误日志记录(重新部署或重连时允许再次显示错误)
136
+ node.lastErrorLog = {};
137
+ node.lastMqttErrorLog = 0;
138
+
80
139
  // 启动轮询
81
140
  node.startPolling();
82
141
 
@@ -126,7 +185,14 @@ module.exports = function(RED) {
126
185
  });
127
186
 
128
187
  node.mqttClient.on('error', (err) => {
129
- node.error(`MQTT错误: ${err.message}`);
188
+ // 日志限流:MQTT错误最多每10分钟输出一次
189
+ const now = Date.now();
190
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
191
+
192
+ if (shouldLog) {
193
+ node.error(`MQTT错误: ${err.message} [此错误将在10分钟后再次显示]`);
194
+ node.lastMqttErrorLog = now;
195
+ }
130
196
  });
131
197
 
132
198
  node.mqttClient.on('message', (topic, message) => {
@@ -274,6 +340,10 @@ module.exports = function(RED) {
274
340
  return;
275
341
  }
276
342
 
343
+ // 清除错误日志记录(重新开始轮询时允许显示错误)
344
+ node.lastErrorLog = {};
345
+ node.lastMqttErrorLog = 0;
346
+
277
347
  node.log(`开始轮询 ${node.config.slaves.length} 个从站设备`);
278
348
  node.currentSlaveIndex = 0;
279
349
 
@@ -365,9 +435,15 @@ module.exports = function(RED) {
365
435
  } catch (err) {
366
436
  node.deviceStates[slaveId].error = err.message;
367
437
 
368
- // 容错机制:单个从站失败不影响其他从站
369
- // 只记录警告,继续轮询下一个从站
370
- node.warn(`轮询从站${slaveId}失败(不影响其他从站): ${err.message}`);
438
+ // 日志限流:每个从站的错误日志最多每10分钟输出一次
439
+ const now = Date.now();
440
+ const lastLogTime = node.lastErrorLog[slaveId] || 0;
441
+ const shouldLog = (now - lastLogTime) > node.errorLogInterval;
442
+
443
+ if (shouldLog) {
444
+ node.warn(`轮询从站${slaveId}失败(不影响其他从站): ${err.message} [此错误将在10分钟后再次显示]`);
445
+ node.lastErrorLog[slaveId] = now;
446
+ }
371
447
 
372
448
  // 更新状态显示
373
449
  const failedCount = Object.values(node.deviceStates).filter(s => s.error).length;
@@ -383,7 +459,12 @@ module.exports = function(RED) {
383
459
  (err.message.includes('ECONNRESET') ||
384
460
  err.message.includes('ETIMEDOUT') ||
385
461
  err.message.includes('ENOTCONN'))) {
386
- node.warn('所有从站都失败,检测到连接断开,尝试重连...');
462
+
463
+ // 连接断开也使用限流日志
464
+ if (shouldLog) {
465
+ node.warn('所有从站都失败,检测到连接断开,尝试重连...');
466
+ }
467
+
387
468
  node.isConnected = false;
388
469
  node.stopPolling();
389
470