node-red-contrib-symi-modbus 2.6.7 → 2.6.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 +286 -360
- package/examples/basic-flow.json +33 -21
- package/nodes/custom-protocol.html +276 -0
- package/nodes/custom-protocol.js +240 -0
- package/nodes/homekit-bridge.html +273 -0
- package/nodes/homekit-bridge.js +346 -0
- package/nodes/mesh-protocol.js +286 -0
- package/nodes/modbus-dashboard.html +444 -0
- package/nodes/modbus-dashboard.js +116 -0
- package/nodes/modbus-debug.js +10 -2
- package/nodes/modbus-master.js +185 -84
- package/nodes/modbus-slave-switch.html +196 -12
- package/nodes/modbus-slave-switch.js +479 -157
- package/nodes/serial-port-config.js +84 -21
- package/package.json +8 -3
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('modbus-dashboard', {
|
|
3
|
+
category: 'SYMI-MODBUS',
|
|
4
|
+
color: '#4CAF50',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: {value: "Modbus控制看板"},
|
|
7
|
+
masterNode: {value: "", required: true}
|
|
8
|
+
},
|
|
9
|
+
inputs: 0,
|
|
10
|
+
outputs: 0,
|
|
11
|
+
icon: "font-awesome/fa-dashboard",
|
|
12
|
+
label: function() {
|
|
13
|
+
return this.name || "Modbus控制看板";
|
|
14
|
+
},
|
|
15
|
+
oneditprepare: function() {
|
|
16
|
+
var node = this;
|
|
17
|
+
var stateCache = {}; // 缓存所有线圈状态
|
|
18
|
+
var relayNamesCache = {}; // 缓存继电器名称
|
|
19
|
+
var pollInterval = null; // 轮询定时器(全局变量,用于清理)
|
|
20
|
+
|
|
21
|
+
// 填充主站节点选择器
|
|
22
|
+
var masterNodeSelect = $("#node-input-masterNode");
|
|
23
|
+
masterNodeSelect.empty();
|
|
24
|
+
masterNodeSelect.append('<option value="">请选择主站节点</option>');
|
|
25
|
+
|
|
26
|
+
RED.nodes.eachNode(function(n) {
|
|
27
|
+
if (n.type === "modbus-master") {
|
|
28
|
+
var label = n.name || `主站 ${n.id.substring(0, 8)}`;
|
|
29
|
+
var selected = (n.id === node.masterNode) ? ' selected' : '';
|
|
30
|
+
masterNodeSelect.append(`<option value="${n.id}"${selected}>${label}</option>`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 加载HomeKit网桥的名称配置
|
|
35
|
+
function loadRelayNames() {
|
|
36
|
+
RED.nodes.eachNode(function(n) {
|
|
37
|
+
if (n.type === "homekit-bridge" && n.masterNode === node.masterNode) {
|
|
38
|
+
if (n.relayNames) {
|
|
39
|
+
relayNamesCache = Object.assign({}, n.relayNames);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 获取继电器名称
|
|
46
|
+
function getRelayName(slaveAddr, coil) {
|
|
47
|
+
var key = slaveAddr + "_" + coil;
|
|
48
|
+
if (relayNamesCache[key]) {
|
|
49
|
+
return relayNamesCache[key];
|
|
50
|
+
}
|
|
51
|
+
var relayType = coil < 16 ? "开关" : "插座";
|
|
52
|
+
return `${relayType}${coil + 1}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 渲染控制面板
|
|
56
|
+
function renderDashboard() {
|
|
57
|
+
var masterNodeId = $("#node-input-masterNode").val();
|
|
58
|
+
var container = $("#dashboard-container");
|
|
59
|
+
container.empty();
|
|
60
|
+
|
|
61
|
+
if (!masterNodeId) {
|
|
62
|
+
container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;">请先选择主站节点</div>');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 显示加载中
|
|
67
|
+
container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>');
|
|
68
|
+
|
|
69
|
+
// 通过HTTP API获取主站节点的最新配置
|
|
70
|
+
$.ajax({
|
|
71
|
+
url: '/modbus-dashboard/master-config/' + masterNodeId,
|
|
72
|
+
method: 'GET',
|
|
73
|
+
success: function(masterConfig) {
|
|
74
|
+
if (!masterConfig || !masterConfig.slaves || masterConfig.slaves.length === 0) {
|
|
75
|
+
container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;">主站节点未配置从站</div>');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 加载继电器名称
|
|
80
|
+
loadRelayNames();
|
|
81
|
+
|
|
82
|
+
// 清空容器
|
|
83
|
+
container.empty();
|
|
84
|
+
|
|
85
|
+
// 遍历所有从站
|
|
86
|
+
masterConfig.slaves.forEach(function(slave) {
|
|
87
|
+
var slaveSection = $('<div class="slave-section">');
|
|
88
|
+
|
|
89
|
+
var slaveHeader = $(`
|
|
90
|
+
<div class="slave-header">
|
|
91
|
+
<span class="slave-title">从站 ${slave.address}</span>
|
|
92
|
+
<span class="slave-info">线圈 ${slave.coilStart}-${slave.coilEnd}</span>
|
|
93
|
+
</div>
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
var coilGrid = $('<div class="coil-grid">');
|
|
97
|
+
|
|
98
|
+
for (var coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
|
|
99
|
+
var key = slave.address + "_" + coil;
|
|
100
|
+
var relayName = getRelayName(slave.address, coil);
|
|
101
|
+
var currentState = stateCache[key] || false;
|
|
102
|
+
var stateClass = currentState ? 'state-on' : 'state-off';
|
|
103
|
+
var stateText = currentState ? 'ON' : 'OFF';
|
|
104
|
+
|
|
105
|
+
var coilItem = $(`
|
|
106
|
+
<div class="coil-item">
|
|
107
|
+
<div class="coil-header">
|
|
108
|
+
<span class="coil-name" title="${relayName}">${relayName}</span>
|
|
109
|
+
<span class="coil-number">线圈${coil}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="coil-control">
|
|
112
|
+
<button class="btn-toggle ${stateClass}" data-slave="${slave.address}" data-coil="${coil}">
|
|
113
|
+
<span class="state-text">${stateText}</span>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
coilGrid.append(coilItem);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
slaveSection.append(slaveHeader);
|
|
123
|
+
slaveSection.append(coilGrid);
|
|
124
|
+
container.append(slaveSection);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 绑定按钮点击事件
|
|
128
|
+
$(".btn-toggle").off("click").on("click", function() {
|
|
129
|
+
var slaveAddr = parseInt($(this).data("slave"));
|
|
130
|
+
var coil = parseInt($(this).data("coil"));
|
|
131
|
+
var key = slaveAddr + "_" + coil;
|
|
132
|
+
var currentState = stateCache[key] || false;
|
|
133
|
+
var newState = !currentState;
|
|
134
|
+
|
|
135
|
+
// 发送控制命令(通过HTTP API)
|
|
136
|
+
sendControlCommand(slaveAddr, coil, newState);
|
|
137
|
+
|
|
138
|
+
// 立即更新UI(乐观更新)
|
|
139
|
+
stateCache[key] = newState;
|
|
140
|
+
updateButtonState($(this), newState);
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
error: function(xhr, status, error) {
|
|
144
|
+
container.html('<div style="padding: 40px; text-align: center; color: #f44336; font-size: 14px;"><i class="fa fa-exclamation-triangle"></i> 加载失败: ' + error + '</div>');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 更新按钮状态
|
|
150
|
+
function updateButtonState(button, state) {
|
|
151
|
+
if (state) {
|
|
152
|
+
button.removeClass('state-off').addClass('state-on');
|
|
153
|
+
button.find('.state-text').text('ON');
|
|
154
|
+
} else {
|
|
155
|
+
button.removeClass('state-on').addClass('state-off');
|
|
156
|
+
button.find('.state-text').text('OFF');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 发送控制命令
|
|
161
|
+
function sendControlCommand(slaveAddr, coil, value) {
|
|
162
|
+
// 通过Node-RED的admin API发送注入命令
|
|
163
|
+
$.ajax({
|
|
164
|
+
url: '/modbus-dashboard/control',
|
|
165
|
+
method: 'POST',
|
|
166
|
+
contentType: 'application/json',
|
|
167
|
+
data: JSON.stringify({
|
|
168
|
+
slave: slaveAddr,
|
|
169
|
+
coil: coil,
|
|
170
|
+
value: value
|
|
171
|
+
}),
|
|
172
|
+
success: function() {
|
|
173
|
+
console.log(`控制命令已发送: 从站${slaveAddr} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
174
|
+
},
|
|
175
|
+
error: function(err) {
|
|
176
|
+
console.error('控制命令发送失败:', err);
|
|
177
|
+
RED.notify('控制命令发送失败', 'error');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 轮询状态更新(每500ms)
|
|
183
|
+
function startPolling() {
|
|
184
|
+
if (pollInterval) {
|
|
185
|
+
clearInterval(pollInterval);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
pollInterval = setInterval(function() {
|
|
189
|
+
var masterNodeId = $("#node-input-masterNode").val();
|
|
190
|
+
if (!masterNodeId) return;
|
|
191
|
+
|
|
192
|
+
$.ajax({
|
|
193
|
+
url: '/modbus-dashboard/state',
|
|
194
|
+
method: 'GET',
|
|
195
|
+
success: function(data) {
|
|
196
|
+
if (data && data.states) {
|
|
197
|
+
// 更新状态缓存
|
|
198
|
+
Object.keys(data.states).forEach(function(key) {
|
|
199
|
+
var oldState = stateCache[key];
|
|
200
|
+
var newState = data.states[key];
|
|
201
|
+
stateCache[key] = newState;
|
|
202
|
+
|
|
203
|
+
// 如果状态变化,更新UI
|
|
204
|
+
if (oldState !== newState) {
|
|
205
|
+
var parts = key.split('_');
|
|
206
|
+
var slaveAddr = parts[0];
|
|
207
|
+
var coil = parts[1];
|
|
208
|
+
var button = $(`.btn-toggle[data-slave="${slaveAddr}"][data-coil="${coil}"]`);
|
|
209
|
+
if (button.length > 0) {
|
|
210
|
+
updateButtonState(button, newState);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
error: function(err) {
|
|
217
|
+
console.error('状态轮询失败:', err);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}, 500);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 停止轮询
|
|
224
|
+
function stopPolling() {
|
|
225
|
+
if (pollInterval) {
|
|
226
|
+
clearInterval(pollInterval);
|
|
227
|
+
pollInterval = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 监听主站节点选择变化
|
|
232
|
+
masterNodeSelect.on("change", function() {
|
|
233
|
+
renderDashboard();
|
|
234
|
+
stopPolling();
|
|
235
|
+
if ($(this).val()) {
|
|
236
|
+
startPolling();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 添加刷新按钮
|
|
241
|
+
$("#btn-refresh-dashboard").on("click", function() {
|
|
242
|
+
stopPolling();
|
|
243
|
+
renderDashboard();
|
|
244
|
+
if (masterNodeSelect.val()) {
|
|
245
|
+
startPolling();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// 初始渲染
|
|
250
|
+
renderDashboard();
|
|
251
|
+
if (node.masterNode) {
|
|
252
|
+
startPolling();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 对话框关闭时停止轮询
|
|
256
|
+
$("#node-dialog-cancel, #node-dialog-ok").on("click", function() {
|
|
257
|
+
stopPolling();
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
oneditcancel: function() {
|
|
261
|
+
// 取消编辑时停止轮询,防止内存泄漏
|
|
262
|
+
if (pollInterval) {
|
|
263
|
+
clearInterval(pollInterval);
|
|
264
|
+
pollInterval = null;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
oneditsave: function() {
|
|
268
|
+
// 保存时停止轮询,防止内存泄漏
|
|
269
|
+
if (pollInterval) {
|
|
270
|
+
clearInterval(pollInterval);
|
|
271
|
+
pollInterval = null;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
oneditdelete: function() {
|
|
275
|
+
// 删除节点时停止轮询,防止内存泄漏
|
|
276
|
+
if (pollInterval) {
|
|
277
|
+
clearInterval(pollInterval);
|
|
278
|
+
pollInterval = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
</script>
|
|
283
|
+
|
|
284
|
+
<script type="text/html" data-template-name="modbus-dashboard">
|
|
285
|
+
<div class="form-row">
|
|
286
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 节点名称</label>
|
|
287
|
+
<input type="text" id="node-input-name" placeholder="Modbus控制看板">
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div class="form-row">
|
|
291
|
+
<label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
|
|
292
|
+
<select id="node-input-masterNode" style="width: 55%;">
|
|
293
|
+
<option value="">请选择主站节点</option>
|
|
294
|
+
</select>
|
|
295
|
+
<button type="button" id="btn-refresh-dashboard" class="red-ui-button" style="margin-left: 5px;">
|
|
296
|
+
<i class="fa fa-refresh"></i> 刷新
|
|
297
|
+
</button>
|
|
298
|
+
<div style="font-size: 11px; color: #999; margin-top: 5px;">选择要监控的Modbus主站节点,点击刷新按钮更新显示</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="form-row" style="margin-top: 20px;">
|
|
302
|
+
<label style="width: 100%; font-weight: bold; margin-bottom: 10px;">
|
|
303
|
+
<i class="fa fa-dashboard"></i> 控制面板
|
|
304
|
+
</label>
|
|
305
|
+
<div id="dashboard-container" style="
|
|
306
|
+
max-height: 700px;
|
|
307
|
+
overflow-y: auto;
|
|
308
|
+
border: 1px solid #ddd;
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
background: #fafafa;
|
|
311
|
+
padding: 10px;
|
|
312
|
+
"></div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<style>
|
|
316
|
+
.slave-section {
|
|
317
|
+
margin-bottom: 20px;
|
|
318
|
+
background: white;
|
|
319
|
+
border-radius: 8px;
|
|
320
|
+
padding: 15px;
|
|
321
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.slave-header {
|
|
325
|
+
display: flex;
|
|
326
|
+
justify-content: space-between;
|
|
327
|
+
align-items: center;
|
|
328
|
+
margin-bottom: 15px;
|
|
329
|
+
padding-bottom: 10px;
|
|
330
|
+
border-bottom: 2px solid #4CAF50;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.slave-title {
|
|
334
|
+
font-weight: bold;
|
|
335
|
+
font-size: 16px;
|
|
336
|
+
color: #333;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.slave-info {
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
color: #666;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.coil-grid {
|
|
345
|
+
display: grid;
|
|
346
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
347
|
+
gap: 10px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.coil-item {
|
|
351
|
+
background: #f9f9f9;
|
|
352
|
+
border: 1px solid #e0e0e0;
|
|
353
|
+
border-radius: 6px;
|
|
354
|
+
padding: 10px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.coil-header {
|
|
358
|
+
display: flex;
|
|
359
|
+
justify-content: space-between;
|
|
360
|
+
align-items: center;
|
|
361
|
+
margin-bottom: 8px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.coil-name {
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
font-size: 13px;
|
|
367
|
+
color: #333;
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
text-overflow: ellipsis;
|
|
370
|
+
white-space: nowrap;
|
|
371
|
+
max-width: 120px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.coil-number {
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
color: #999;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.coil-control {
|
|
380
|
+
display: flex;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.btn-toggle {
|
|
385
|
+
width: 100%;
|
|
386
|
+
padding: 8px 16px;
|
|
387
|
+
border: none;
|
|
388
|
+
border-radius: 4px;
|
|
389
|
+
font-weight: bold;
|
|
390
|
+
font-size: 13px;
|
|
391
|
+
cursor: pointer;
|
|
392
|
+
transition: all 0.2s;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.btn-toggle.state-on {
|
|
396
|
+
background: #4CAF50;
|
|
397
|
+
color: white;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.btn-toggle.state-on:hover {
|
|
401
|
+
background: #45a049;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.btn-toggle.state-off {
|
|
405
|
+
background: #f44336;
|
|
406
|
+
color: white;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.btn-toggle.state-off:hover {
|
|
410
|
+
background: #da190b;
|
|
411
|
+
}
|
|
412
|
+
</style>
|
|
413
|
+
</script>
|
|
414
|
+
|
|
415
|
+
<script type="text/html" data-help-name="modbus-dashboard">
|
|
416
|
+
<p>Modbus控制看板节点,提供可视化界面显示和控制所有从站的继电器状态。</p>
|
|
417
|
+
|
|
418
|
+
<h3>功能特性</h3>
|
|
419
|
+
<ul>
|
|
420
|
+
<li>实时显示所有从站和线圈的状态</li>
|
|
421
|
+
<li>支持直接点击按钮控制继电器开关</li>
|
|
422
|
+
<li>自动同步HomeKit网桥配置的继电器名称</li>
|
|
423
|
+
<li>美观的网格布局,按从站分组显示</li>
|
|
424
|
+
<li>状态实时更新(500ms轮询)</li>
|
|
425
|
+
<li>零额外开销,不参与实际Modbus通信</li>
|
|
426
|
+
</ul>
|
|
427
|
+
|
|
428
|
+
<h3>使用步骤</h3>
|
|
429
|
+
<ol>
|
|
430
|
+
<li>选择已配置的Modbus主站节点</li>
|
|
431
|
+
<li>在配置界面中查看所有继电器状态</li>
|
|
432
|
+
<li>点击按钮即可控制继电器开关</li>
|
|
433
|
+
<li>部署流程后,节点会显示监控状态</li>
|
|
434
|
+
</ol>
|
|
435
|
+
|
|
436
|
+
<h3>注意事项</h3>
|
|
437
|
+
<ul>
|
|
438
|
+
<li>确保主站节点已正确配置并运行</li>
|
|
439
|
+
<li>控制看板只在配置界面打开时才轮询状态</li>
|
|
440
|
+
<li>继电器名称与HomeKit网桥共享,修改需在HomeKit网桥节点中进行</li>
|
|
441
|
+
<li>本节点不参与实际Modbus通信,不会增加主站负担</li>
|
|
442
|
+
</ul>
|
|
443
|
+
</script>
|
|
444
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// 全局状态缓存(所有dashboard节点共享)
|
|
5
|
+
var globalStateCache = {};
|
|
6
|
+
|
|
7
|
+
function ModbusDashboardNode(config) {
|
|
8
|
+
RED.nodes.createNode(this, config);
|
|
9
|
+
var node = this;
|
|
10
|
+
|
|
11
|
+
// 配置
|
|
12
|
+
node.config = {
|
|
13
|
+
name: config.name || "Modbus控制看板",
|
|
14
|
+
masterNode: config.masterNode
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 获取主站节点
|
|
18
|
+
var masterNode = RED.nodes.getNode(node.config.masterNode);
|
|
19
|
+
if (!masterNode) {
|
|
20
|
+
node.error('未找到主站节点');
|
|
21
|
+
node.status({fill: "red", shape: "ring", text: "未配置主站"});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 监听主站状态更新事件
|
|
26
|
+
node.stateUpdateHandler = function(data) {
|
|
27
|
+
if (!data || typeof data !== 'object') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var key = data.slave + "_" + data.coil;
|
|
32
|
+
globalStateCache[key] = data.value;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 注册事件监听器
|
|
36
|
+
masterNode.on('stateUpdate', node.stateUpdateHandler);
|
|
37
|
+
|
|
38
|
+
// 初始化状态缓存(从主站获取当前状态)
|
|
39
|
+
if (masterNode.deviceStates) {
|
|
40
|
+
Object.keys(masterNode.deviceStates).forEach(function(slaveId) {
|
|
41
|
+
var deviceState = masterNode.deviceStates[slaveId];
|
|
42
|
+
if (deviceState && deviceState.coils) {
|
|
43
|
+
deviceState.coils.forEach(function(value, coil) {
|
|
44
|
+
var key = slaveId + "_" + coil;
|
|
45
|
+
globalStateCache[key] = value;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 更新节点状态
|
|
52
|
+
node.status({fill: "green", shape: "dot", text: "监控中"});
|
|
53
|
+
|
|
54
|
+
// 清理
|
|
55
|
+
node.on('close', function() {
|
|
56
|
+
if (masterNode && node.stateUpdateHandler) {
|
|
57
|
+
masterNode.removeListener('stateUpdate', node.stateUpdateHandler);
|
|
58
|
+
}
|
|
59
|
+
node.status({});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
RED.nodes.registerType("modbus-dashboard", ModbusDashboardNode);
|
|
64
|
+
|
|
65
|
+
// HTTP API:获取主站节点配置
|
|
66
|
+
RED.httpAdmin.get('/modbus-dashboard/master-config/:id', function(req, res) {
|
|
67
|
+
var masterNodeId = req.params.id;
|
|
68
|
+
var masterNode = RED.nodes.getNode(masterNodeId);
|
|
69
|
+
|
|
70
|
+
if (!masterNode) {
|
|
71
|
+
res.status(404).json({error: '主站节点不存在'});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 返回主站配置(包括最新的从站列表)
|
|
76
|
+
// 从站列表存储在 node.config.slaves 中
|
|
77
|
+
res.json({
|
|
78
|
+
slaves: (masterNode.config && masterNode.config.slaves) ? masterNode.config.slaves : [],
|
|
79
|
+
relayNames: masterNode.relayNames || {}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// HTTP API:获取状态
|
|
84
|
+
RED.httpAdmin.get('/modbus-dashboard/state', function(req, res) {
|
|
85
|
+
res.json({
|
|
86
|
+
states: globalStateCache
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// HTTP API:发送控制命令
|
|
91
|
+
RED.httpAdmin.post('/modbus-dashboard/control', function(req, res) {
|
|
92
|
+
var slave = parseInt(req.body.slave);
|
|
93
|
+
var coil = parseInt(req.body.coil);
|
|
94
|
+
var value = Boolean(req.body.value);
|
|
95
|
+
|
|
96
|
+
if (isNaN(slave) || isNaN(coil)) {
|
|
97
|
+
res.status(400).json({error: '参数错误'});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 发送内部事件(通过免连线通信机制)
|
|
102
|
+
RED.events.emit('modbus:writeCoil', {
|
|
103
|
+
slave: slave,
|
|
104
|
+
coil: coil,
|
|
105
|
+
value: value,
|
|
106
|
+
source: 'dashboard'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 立即更新缓存(乐观更新)
|
|
110
|
+
var key = slave + "_" + coil;
|
|
111
|
+
globalStateCache[key] = value;
|
|
112
|
+
|
|
113
|
+
res.json({success: true});
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
package/nodes/modbus-debug.js
CHANGED
|
@@ -28,9 +28,12 @@ module.exports = function(RED) {
|
|
|
28
28
|
node.localConnection = null;
|
|
29
29
|
node.localConnType = null; // tcp | serial
|
|
30
30
|
|
|
31
|
-
const sendHexMsg = (data) => {
|
|
31
|
+
const sendHexMsg = (data, direction) => {
|
|
32
32
|
if (!data || !Buffer.isBuffer(data) || data.length === 0) return;
|
|
33
33
|
|
|
34
|
+
// direction: 'received' (接收) 或 'sent' (发送)
|
|
35
|
+
const isSent = direction === 'sent';
|
|
36
|
+
|
|
34
37
|
let buf = data;
|
|
35
38
|
if (node.maxBytes > 0 && buf.length > node.maxBytes) {
|
|
36
39
|
buf = buf.subarray(0, node.maxBytes);
|
|
@@ -41,6 +44,7 @@ module.exports = function(RED) {
|
|
|
41
44
|
length: data.length,
|
|
42
45
|
displayedLength: buf.length,
|
|
43
46
|
truncated: node.maxBytes > 0 && data.length > node.maxBytes,
|
|
47
|
+
direction: isSent ? 'TX' : 'RX'
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
// 来源信息
|
|
@@ -76,7 +80,11 @@ module.exports = function(RED) {
|
|
|
76
80
|
if (node.includeTimestamp) msg.timestamp = Date.now();
|
|
77
81
|
|
|
78
82
|
node.send(msg);
|
|
79
|
-
|
|
83
|
+
|
|
84
|
+
// 状态显示:TX=发送,RX=接收
|
|
85
|
+
const statusText = isSent ? `TX ${data.length}B` : `RX ${data.length}B`;
|
|
86
|
+
const statusColor = isSent ? "blue" : "green";
|
|
87
|
+
node.status({ fill: statusColor, shape: "dot", text: statusText });
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
// 选择来源:共享串口配置 或 独立连接到 Modbus 服务器配置
|