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.
- package/README.md +189 -24
- package/nodes/modbus-master.html +168 -53
- package/nodes/modbus-master.js +86 -5
- package/nodes/modbus-slave-switch.html +158 -70
- package/nodes/modbus-slave-switch.js +81 -6
- package/package.json +2 -2
package/nodes/modbus-master.html
CHANGED
|
@@ -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:
|
|
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:
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
|
186
|
-
|
|
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="
|
|
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
|
-
<
|
|
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:
|
|
249
|
-
<i class="fa fa-server"></i>
|
|
250
|
-
<span style="font-size:
|
|
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:
|
|
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:
|
|
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: #
|
|
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>
|
package/nodes/modbus-master.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
462
|
+
|
|
463
|
+
// 连接断开也使用限流日志
|
|
464
|
+
if (shouldLog) {
|
|
465
|
+
node.warn('所有从站都失败,检测到连接断开,尝试重连...');
|
|
466
|
+
}
|
|
467
|
+
|
|
387
468
|
node.isConnected = false;
|
|
388
469
|
node.stopPolling();
|
|
389
470
|
|