node-red-contrib-symi-modbus 1.0.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 +698 -38
- package/nodes/lightweight-protocol.js +268 -0
- package/nodes/modbus-master.html +169 -72
- package/nodes/modbus-master.js +107 -13
- package/nodes/modbus-slave-switch.html +379 -22
- package/nodes/modbus-slave-switch.js +408 -28
- package/nodes/mqtt-server-config.html +67 -0
- package/nodes/mqtt-server-config.js +18 -0
- package/package.json +4 -3
package/nodes/modbus-master.js
CHANGED
|
@@ -2,11 +2,66 @@ 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);
|
|
8
60
|
const node = this;
|
|
9
61
|
|
|
62
|
+
// 获取MQTT服务器配置节点
|
|
63
|
+
node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
|
|
64
|
+
|
|
10
65
|
// 配置参数
|
|
11
66
|
node.config = {
|
|
12
67
|
connectionType: config.connectionType,
|
|
@@ -24,10 +79,11 @@ module.exports = function(RED) {
|
|
|
24
79
|
pollInterval: 200
|
|
25
80
|
}],
|
|
26
81
|
enableMqtt: config.enableMqtt,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// 从config节点读取MQTT配置
|
|
83
|
+
mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "",
|
|
84
|
+
mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
|
|
85
|
+
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
86
|
+
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay"
|
|
31
87
|
};
|
|
32
88
|
|
|
33
89
|
// Modbus客户端
|
|
@@ -39,6 +95,9 @@ module.exports = function(RED) {
|
|
|
39
95
|
node.deviceStates = {}; // 存储每个设备的状态
|
|
40
96
|
node.mqttClient = null;
|
|
41
97
|
node.isClosing = false;
|
|
98
|
+
node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
|
|
99
|
+
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
100
|
+
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
42
101
|
|
|
43
102
|
// 初始化设备状态(基于从站配置列表)
|
|
44
103
|
node.config.slaves.forEach((slave) => {
|
|
@@ -73,6 +132,10 @@ module.exports = function(RED) {
|
|
|
73
132
|
node.isConnected = true;
|
|
74
133
|
node.status({fill: "green", shape: "dot", text: "已连接"});
|
|
75
134
|
|
|
135
|
+
// 清除错误日志记录(重新部署或重连时允许再次显示错误)
|
|
136
|
+
node.lastErrorLog = {};
|
|
137
|
+
node.lastMqttErrorLog = 0;
|
|
138
|
+
|
|
76
139
|
// 启动轮询
|
|
77
140
|
node.startPolling();
|
|
78
141
|
|
|
@@ -98,7 +161,10 @@ module.exports = function(RED) {
|
|
|
98
161
|
}
|
|
99
162
|
|
|
100
163
|
const options = {
|
|
101
|
-
clientId: `modbus_master_${Math.random().toString(16).substr(2, 8)}
|
|
164
|
+
clientId: `modbus_master_${Math.random().toString(16).substr(2, 8)}`,
|
|
165
|
+
clean: false, // 持久化会话,断线重连后继续接收消息
|
|
166
|
+
reconnectPeriod: 5000, // 5秒自动重连
|
|
167
|
+
queueQoSZero: false // 不缓存QoS=0的消息
|
|
102
168
|
};
|
|
103
169
|
|
|
104
170
|
if (node.config.mqttUsername) {
|
|
@@ -119,7 +185,14 @@ module.exports = function(RED) {
|
|
|
119
185
|
});
|
|
120
186
|
|
|
121
187
|
node.mqttClient.on('error', (err) => {
|
|
122
|
-
|
|
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
|
+
}
|
|
123
196
|
});
|
|
124
197
|
|
|
125
198
|
node.mqttClient.on('message', (topic, message) => {
|
|
@@ -215,11 +288,12 @@ module.exports = function(RED) {
|
|
|
215
288
|
}
|
|
216
289
|
|
|
217
290
|
const commandTopic = `${node.config.mqttBaseTopic}/+/+/set`;
|
|
218
|
-
|
|
291
|
+
// 使用QoS=1订阅,确保命令不丢失
|
|
292
|
+
node.mqttClient.subscribe(commandTopic, { qos: 1 }, (err) => {
|
|
219
293
|
if (err) {
|
|
220
294
|
node.error(`订阅MQTT命令主题失败: ${err.message}`);
|
|
221
295
|
} else {
|
|
222
|
-
node.log(`已订阅MQTT命令主题: ${commandTopic}
|
|
296
|
+
node.log(`已订阅MQTT命令主题: ${commandTopic}(QoS=1)`);
|
|
223
297
|
}
|
|
224
298
|
});
|
|
225
299
|
};
|
|
@@ -252,7 +326,12 @@ module.exports = function(RED) {
|
|
|
252
326
|
const stateTopic = `${node.config.mqttBaseTopic}/${slaveId}/${coil}/state`;
|
|
253
327
|
const payload = value ? 'ON' : 'OFF';
|
|
254
328
|
|
|
255
|
-
|
|
329
|
+
// 使用QoS=1确保消息送达,retain=true确保断线重连后可获取最新状态
|
|
330
|
+
node.mqttClient.publish(stateTopic, payload, { qos: 1, retain: true }, (err) => {
|
|
331
|
+
if (err) {
|
|
332
|
+
node.warn(`发布状态失败: ${stateTopic} - ${err.message}`);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
256
335
|
};
|
|
257
336
|
|
|
258
337
|
// 开始轮询
|
|
@@ -261,6 +340,10 @@ module.exports = function(RED) {
|
|
|
261
340
|
return;
|
|
262
341
|
}
|
|
263
342
|
|
|
343
|
+
// 清除错误日志记录(重新开始轮询时允许显示错误)
|
|
344
|
+
node.lastErrorLog = {};
|
|
345
|
+
node.lastMqttErrorLog = 0;
|
|
346
|
+
|
|
264
347
|
node.log(`开始轮询 ${node.config.slaves.length} 个从站设备`);
|
|
265
348
|
node.currentSlaveIndex = 0;
|
|
266
349
|
|
|
@@ -352,9 +435,15 @@ module.exports = function(RED) {
|
|
|
352
435
|
} catch (err) {
|
|
353
436
|
node.deviceStates[slaveId].error = err.message;
|
|
354
437
|
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
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
|
+
}
|
|
358
447
|
|
|
359
448
|
// 更新状态显示
|
|
360
449
|
const failedCount = Object.values(node.deviceStates).filter(s => s.error).length;
|
|
@@ -370,7 +459,12 @@ module.exports = function(RED) {
|
|
|
370
459
|
(err.message.includes('ECONNRESET') ||
|
|
371
460
|
err.message.includes('ETIMEDOUT') ||
|
|
372
461
|
err.message.includes('ENOTCONN'))) {
|
|
373
|
-
|
|
462
|
+
|
|
463
|
+
// 连接断开也使用限流日志
|
|
464
|
+
if (shouldLog) {
|
|
465
|
+
node.warn('所有从站都失败,检测到连接断开,尝试重连...');
|
|
466
|
+
}
|
|
467
|
+
|
|
374
468
|
node.isConnected = false;
|
|
375
469
|
node.stopPolling();
|
|
376
470
|
|
|
@@ -3,67 +3,424 @@
|
|
|
3
3
|
category: 'modbus',
|
|
4
4
|
color: '#E9967A',
|
|
5
5
|
defaults: {
|
|
6
|
-
name: {value: "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
name: {value: "从站开关"},
|
|
7
|
+
// RS-485总线连接配置
|
|
8
|
+
connectionType: {value: "tcp"},
|
|
9
|
+
tcpHost: {value: "127.0.0.1"},
|
|
10
|
+
tcpPort: {value: 8888},
|
|
11
|
+
serialPort: {value: "COM1"},
|
|
12
|
+
serialBaudRate: {value: 9600},
|
|
13
|
+
serialDataBits: {value: 8},
|
|
14
|
+
serialStopBits: {value: 1},
|
|
15
|
+
serialParity: {value: "none"},
|
|
16
|
+
// MQTT配置
|
|
17
|
+
mqttServer: {value: "", type: "mqtt-server-config"},
|
|
18
|
+
// 开关面板配置
|
|
19
|
+
switchBrand: {value: "symi"}, // 品牌选择
|
|
20
|
+
switchId: {value: 0, validate: RED.validators.number()},
|
|
21
|
+
buttonNumber: {value: 1, validate: RED.validators.number()},
|
|
22
|
+
// 映射到继电器
|
|
23
|
+
targetSlaveAddress: {value: 10, validate: RED.validators.number()},
|
|
24
|
+
targetCoilNumber: {value: 0, validate: RED.validators.number()}
|
|
10
25
|
},
|
|
11
26
|
inputs: 1,
|
|
12
27
|
outputs: 1,
|
|
13
28
|
icon: "light.png",
|
|
14
29
|
label: function() {
|
|
15
|
-
return this.name ||
|
|
30
|
+
return this.name || `开关${this.switchId}-按钮${this.buttonNumber} → 继电器${this.targetSlaveAddress}-${this.targetCoilNumber}`;
|
|
31
|
+
},
|
|
32
|
+
oneditprepare: function() {
|
|
33
|
+
// 切换连接类型时显示/隐藏相关配置
|
|
34
|
+
$("#node-input-connectionType").on("change", function() {
|
|
35
|
+
var connType = $(this).val();
|
|
36
|
+
if (connType === "tcp") {
|
|
37
|
+
$(".form-row-tcp").show();
|
|
38
|
+
$(".form-row-serial").hide();
|
|
39
|
+
} else {
|
|
40
|
+
$(".form-row-tcp").hide();
|
|
41
|
+
$(".form-row-serial").show();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 搜索串口按钮
|
|
46
|
+
$("#btn-search-ports").on("click", function() {
|
|
47
|
+
var btn = $(this);
|
|
48
|
+
var inputBox = $("#node-input-serialPort");
|
|
49
|
+
var selectBox = $("#port-list");
|
|
50
|
+
|
|
51
|
+
btn.prop("disabled", true).html('<i class="fa fa-spinner fa-spin"></i> 搜索中');
|
|
52
|
+
|
|
53
|
+
$.ajax({
|
|
54
|
+
url: 'modbus-slave-switch/serialports',
|
|
55
|
+
type: 'GET',
|
|
56
|
+
success: function(ports) {
|
|
57
|
+
selectBox.empty().append('<option value="">-- 选择检测到的串口 --</option>');
|
|
58
|
+
|
|
59
|
+
if (ports.length === 0) {
|
|
60
|
+
selectBox.append('<option disabled>未找到可用串口</option>');
|
|
61
|
+
RED.notify("未找到可用串口,请手动输入串口路径", "warning");
|
|
62
|
+
} else {
|
|
63
|
+
ports.forEach(function(port) {
|
|
64
|
+
var label = port.comName;
|
|
65
|
+
if (port.manufacturer && port.manufacturer !== '未知设备') {
|
|
66
|
+
label += ' - ' + port.manufacturer;
|
|
67
|
+
}
|
|
68
|
+
selectBox.append('<option value="' + port.comName + '">' + label + '</option>');
|
|
69
|
+
});
|
|
70
|
+
// 显示下拉框,隐藏输入框
|
|
71
|
+
inputBox.hide();
|
|
72
|
+
selectBox.show();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
|
|
76
|
+
},
|
|
77
|
+
error: function() {
|
|
78
|
+
RED.notify("搜索串口失败", "error");
|
|
79
|
+
btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 选择串口(下拉选择)
|
|
85
|
+
$("#port-list").on("change", function() {
|
|
86
|
+
var selectedPort = $(this).val();
|
|
87
|
+
if (selectedPort) {
|
|
88
|
+
$("#node-input-serialPort").val(selectedPort).show();
|
|
89
|
+
$(this).hide();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 初始化显示
|
|
94
|
+
$("#node-input-connectionType").trigger("change");
|
|
16
95
|
}
|
|
17
96
|
});
|
|
18
97
|
</script>
|
|
19
98
|
|
|
20
99
|
<script type="text/html" data-template-name="modbus-slave-switch">
|
|
100
|
+
<!-- 基本配置 -->
|
|
21
101
|
<div class="form-row">
|
|
22
102
|
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
23
|
-
<input type="text" id="node-input-name" placeholder="
|
|
103
|
+
<input type="text" id="node-input-name" placeholder="从站开关">
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- RS-485总线连接配置 -->
|
|
107
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
108
|
+
<div class="form-row">
|
|
109
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
110
|
+
<i class="fa fa-plug" style="color: #ff9800;"></i>
|
|
111
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">RS-485总线连接配置</span>
|
|
112
|
+
</label>
|
|
113
|
+
<div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #fff3cd 0%, #fffbe6 100%); border-left: 4px solid #ffc107; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
|
114
|
+
<strong>总线说明:</strong>连接物理开关面板的RS-485总线,监听按键事件并发送控制指令
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="form-row">
|
|
119
|
+
<label for="node-input-connectionType" style="width: 110px;"><i class="fa fa-exchange"></i> 连接类型</label>
|
|
120
|
+
<select id="node-input-connectionType" style="width: calc(70% - 110px);">
|
|
121
|
+
<option value="tcp">TCP/IP</option>
|
|
122
|
+
<option value="serial">串口</option>
|
|
123
|
+
</select>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- TCP配置 -->
|
|
127
|
+
<div class="form-row form-row-tcp">
|
|
128
|
+
<label for="node-input-tcpHost" style="width: 110px;"><i class="fa fa-server"></i> TCP主机</label>
|
|
129
|
+
<input type="text" id="node-input-tcpHost" placeholder="192.168.1.200" style="width: calc(70% - 110px);">
|
|
130
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
131
|
+
RS-485转TCP网关IP地址
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="form-row form-row-tcp">
|
|
136
|
+
<label for="node-input-tcpPort" style="width: 110px;"><i class="fa fa-plug"></i> TCP端口</label>
|
|
137
|
+
<input type="number" id="node-input-tcpPort" placeholder="8888" style="width: 100px;">
|
|
138
|
+
<span style="margin-left: 10px; font-size: 12px; color: #666;">默认 8888</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- 串口配置 -->
|
|
142
|
+
<div class="form-row form-row-serial">
|
|
143
|
+
<label for="node-input-serialPort" style="width: 110px;"><i class="fa fa-terminal"></i> 串口</label>
|
|
144
|
+
<div style="display: inline-block; width: calc(70% - 110px);">
|
|
145
|
+
<div style="display: flex; gap: 5px; align-items: center;">
|
|
146
|
+
<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;">
|
|
147
|
+
<select id="port-list" style="flex: 1; padding: 5px; font-family: monospace; font-size: 12px; border: 1px solid #ccc; border-radius: 4px; display: none;">
|
|
148
|
+
<option value="">-- 选择检测到的串口 --</option>
|
|
149
|
+
</select>
|
|
150
|
+
<button type="button" id="btn-search-ports" 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;">
|
|
151
|
+
<i class="fa fa-search"></i> 搜索
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
<div style="font-size: 11px; color: #888; margin-top: 3px;">
|
|
155
|
+
支持COM1、/dev/ttyUSB0、/dev/ttyS1等RS-485串口设备
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="form-row form-row-serial">
|
|
161
|
+
<label for="node-input-serialBaudRate" style="width: 110px;"><i class="fa fa-tachometer"></i> 波特率</label>
|
|
162
|
+
<select id="node-input-serialBaudRate" style="width: 150px;">
|
|
163
|
+
<option value="9600">9600</option>
|
|
164
|
+
<option value="19200">19200</option>
|
|
165
|
+
<option value="38400">38400</option>
|
|
166
|
+
<option value="57600">57600</option>
|
|
167
|
+
<option value="115200">115200</option>
|
|
168
|
+
</select>
|
|
169
|
+
<span style="margin-left: 10px; font-size: 11px; color: #888;">8-N-1固定配置(亖米协议)</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="form-row form-row-serial" style="display: none;">
|
|
173
|
+
<label for="node-input-serialDataBits" style="width: 110px;"><i class="fa fa-database"></i> 数据位</label>
|
|
174
|
+
<select id="node-input-serialDataBits" style="width: 100px;">
|
|
175
|
+
<option value="7">7</option>
|
|
176
|
+
<option value="8" selected>8</option>
|
|
177
|
+
</select>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="form-row form-row-serial" style="display: none;">
|
|
181
|
+
<label for="node-input-serialStopBits" style="width: 110px;"><i class="fa fa-stop"></i> 停止位</label>
|
|
182
|
+
<select id="node-input-serialStopBits" style="width: 100px;">
|
|
183
|
+
<option value="1" selected>1</option>
|
|
184
|
+
<option value="2">2</option>
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="form-row form-row-serial" style="display: none;">
|
|
189
|
+
<label for="node-input-serialParity" style="width: 110px;"><i class="fa fa-check"></i> 校验位</label>
|
|
190
|
+
<select id="node-input-serialParity" style="width: 100px;">
|
|
191
|
+
<option value="none" selected>无</option>
|
|
192
|
+
<option value="even">偶校验</option>
|
|
193
|
+
<option value="odd">奇校验</option>
|
|
194
|
+
</select>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- MQTT配置 -->
|
|
198
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
199
|
+
<div class="form-row">
|
|
200
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
201
|
+
<i class="fa fa-cloud" style="color: #9c27b0;"></i>
|
|
202
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">MQTT服务器配置</span>
|
|
203
|
+
</label>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div class="form-row">
|
|
207
|
+
<label for="node-input-mqttServer" style="width: 110px;"><i class="fa fa-server"></i> MQTT服务器</label>
|
|
208
|
+
<input type="text" id="node-input-mqttServer" placeholder="选择或添加MQTT服务器配置" style="width: calc(70% - 110px);">
|
|
209
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
210
|
+
选择已配置的MQTT服务器(需与主站节点使用同一配置)
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- 物理开关面板配置 -->
|
|
215
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
216
|
+
<div class="form-row">
|
|
217
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
218
|
+
<i class="fa fa-toggle-on" style="color: #3f51b5;"></i>
|
|
219
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">物理开关面板配置</span>
|
|
220
|
+
</label>
|
|
24
221
|
</div>
|
|
25
222
|
|
|
26
223
|
<div class="form-row">
|
|
27
|
-
<label for="node-input-
|
|
28
|
-
<
|
|
224
|
+
<label for="node-input-switchBrand" style="width: 110px;"><i class="fa fa-trademark"></i> 面板品牌</label>
|
|
225
|
+
<select id="node-input-switchBrand" style="width: 200px;">
|
|
226
|
+
<option value="symi">亖米(Symi)</option>
|
|
227
|
+
<option value="other1" disabled style="color: #999;">其他品牌1(待开发)</option>
|
|
228
|
+
<option value="other2" disabled style="color: #999;">其他品牌2(待开发)</option>
|
|
229
|
+
</select>
|
|
230
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">支持1-8键开关</span>
|
|
29
231
|
</div>
|
|
30
232
|
|
|
31
233
|
<div class="form-row">
|
|
32
|
-
<label for="node-input-
|
|
33
|
-
<input type="number" id="node-input-
|
|
234
|
+
<label for="node-input-switchId" style="width: 110px;"><i class="fa fa-id-card"></i> 开关ID</label>
|
|
235
|
+
<input type="number" id="node-input-switchId" placeholder="0" min="0" max="255" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
236
|
+
<span style="margin-left: 10px; color: #666; font-size: 12px;">物理面板地址:<strong>0-255</strong></span>
|
|
237
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
238
|
+
RS-485总线上的设备地址标识
|
|
239
|
+
</div>
|
|
34
240
|
</div>
|
|
35
241
|
|
|
36
242
|
<div class="form-row">
|
|
37
|
-
<label for="node-input-
|
|
38
|
-
<
|
|
243
|
+
<label for="node-input-buttonNumber" style="width: 110px;"><i class="fa fa-hand-pointer-o"></i> 按钮编号</label>
|
|
244
|
+
<select id="node-input-buttonNumber" style="width: 150px;">
|
|
245
|
+
<option value="1">按钮 1</option>
|
|
246
|
+
<option value="2">按钮 2</option>
|
|
247
|
+
<option value="3">按钮 3</option>
|
|
248
|
+
<option value="4">按钮 4</option>
|
|
249
|
+
<option value="5">按钮 5</option>
|
|
250
|
+
<option value="6">按钮 6</option>
|
|
251
|
+
<option value="7">按钮 7</option>
|
|
252
|
+
<option value="8">按钮 8</option>
|
|
253
|
+
</select>
|
|
254
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">面板物理按键</span>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- 映射到继电器配置 -->
|
|
258
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
259
|
+
<div class="form-row">
|
|
260
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
261
|
+
<i class="fa fa-arrow-right" style="color: #4caf50;"></i>
|
|
262
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">映射到继电器设备</span>
|
|
263
|
+
</label>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="form-row">
|
|
267
|
+
<label for="node-input-targetSlaveAddress" style="width: 110px;"><i class="fa fa-map-marker"></i> 从站地址</label>
|
|
268
|
+
<input type="number" id="node-input-targetSlaveAddress" placeholder="10" min="1" max="247" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
269
|
+
<span style="margin-left: 10px; color: #666; font-size: 12px;">Modbus继电器:<strong>10-19</strong></span>
|
|
270
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
271
|
+
主站节点中配置的从站设备地址
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="form-row">
|
|
276
|
+
<label for="node-input-targetCoilNumber" style="width: 110px;"><i class="fa fa-plug"></i> 线圈编号</label>
|
|
277
|
+
<input type="number" id="node-input-targetCoilNumber" placeholder="0" min="0" max="31" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
278
|
+
<span style="margin-left: 10px; color: #666; font-size: 12px;">继电器通道:<strong>0-31</strong></span>
|
|
279
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
280
|
+
32路继电器的具体通道编号
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="form-row" style="margin-top: 20px; padding: 14px; background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%); border-left: 4px solid #4caf50; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.08);">
|
|
285
|
+
<div style="font-size: 12px; color: #333; line-height: 1.8;">
|
|
286
|
+
<div style="font-weight: 600; color: #2e7d32; margin-bottom: 10px; font-size: 13px;">
|
|
287
|
+
配置说明
|
|
288
|
+
</div>
|
|
289
|
+
<div style="margin-bottom: 8px; padding-left: 10px;">
|
|
290
|
+
<strong>面板品牌:</strong><span style="color: #555;">亖米协议,支持1-8键开关</span><br>
|
|
291
|
+
<strong>开关ID:</strong><span style="color: #555;">物理面板RS-485地址(0-255)</span><br>
|
|
292
|
+
<strong>按钮编号:</strong><span style="color: #555;">面板按键序号(1-8)</span><br>
|
|
293
|
+
<strong>从站地址:</strong><span style="color: #555;">Modbus继电器地址(10-19)</span><br>
|
|
294
|
+
<strong>线圈编号:</strong><span style="color: #555;">继电器通道号(0-31)</span>
|
|
295
|
+
</div>
|
|
296
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #a5d6a7;">
|
|
297
|
+
<div style="font-size: 11px; color: #2e7d32; font-weight: 500; margin-bottom: 5px;">
|
|
298
|
+
配置示例
|
|
299
|
+
</div>
|
|
300
|
+
<div style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #c8e6c9; font-size: 11px; color: #555;">
|
|
301
|
+
<strong>场景:</strong>亖米开关ID=0,按钮1 → 控制继电器10的线圈0<br>
|
|
302
|
+
<strong>MQTT:</strong><code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px; color: #e91e63; font-family: monospace;">modbus/relay/10/0/set</code>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
39
306
|
</div>
|
|
40
307
|
</script>
|
|
41
308
|
|
|
42
309
|
<script type="text/html" data-help-name="modbus-slave-switch">
|
|
43
|
-
<p>Modbus
|
|
310
|
+
<p>Modbus从站开关节点,将物理开关面板的按钮映射到Modbus继电器设备,通过MQTT实现控制。</p>
|
|
311
|
+
|
|
312
|
+
<h3>工作原理</h3>
|
|
313
|
+
<p>本节点实现物理开关面板(RS-485)到Modbus继电器的映射:</p>
|
|
314
|
+
<ul>
|
|
315
|
+
<li><strong>物理面板</strong>:开关ID(0-255)+ 按钮编号(1-8)</li>
|
|
316
|
+
<li><strong>映射到</strong>:Modbus从站地址(10-19)+ 线圈编号(0-31)</li>
|
|
317
|
+
<li><strong>通过MQTT</strong>:内置MQTT客户端,无需连线到主站节点</li>
|
|
318
|
+
</ul>
|
|
44
319
|
|
|
45
|
-
<h3
|
|
320
|
+
<h3>MQTT配置</h3>
|
|
46
321
|
<dl class="message-properties">
|
|
47
|
-
<dt
|
|
48
|
-
<dd
|
|
322
|
+
<dt>MQTT服务器<span class="property-type">string</span></dt>
|
|
323
|
+
<dd>MQTT Broker地址(如:mqtt://192.168.1.100:1883)</dd>
|
|
49
324
|
|
|
50
|
-
<dt
|
|
51
|
-
<dd
|
|
325
|
+
<dt>用户名/密码<span class="property-type">string</span></dt>
|
|
326
|
+
<dd>MQTT认证信息(可选)</dd>
|
|
52
327
|
|
|
53
|
-
<dt
|
|
54
|
-
<dd
|
|
328
|
+
<dt>基础主题<span class="property-type">string</span></dt>
|
|
329
|
+
<dd>MQTT主题前缀(需与主站节点配置一致,默认:modbus/relay)</dd>
|
|
330
|
+
</dl>
|
|
331
|
+
|
|
332
|
+
<h3>物理开关面板配置</h3>
|
|
333
|
+
<dl class="message-properties">
|
|
334
|
+
<dt>开关ID<span class="property-type">number (0-255)</span></dt>
|
|
335
|
+
<dd>物理开关面板的设备地址(RS-485总线地址)</dd>
|
|
336
|
+
|
|
337
|
+
<dt>按钮编号<span class="property-type">number (1-8)</span></dt>
|
|
338
|
+
<dd>物理面板上的按键编号(每个面板最多8个按钮)</dd>
|
|
339
|
+
</dl>
|
|
340
|
+
|
|
341
|
+
<h3>映射到继电器</h3>
|
|
342
|
+
<dl class="message-properties">
|
|
343
|
+
<dt>目标从站地址<span class="property-type">number (10-247)</span></dt>
|
|
344
|
+
<dd>要控制的Modbus继电器设备地址</dd>
|
|
345
|
+
|
|
346
|
+
<dt>目标线圈编号<span class="property-type">number (0-31)</span></dt>
|
|
347
|
+
<dd>继电器的具体通道(32路继电器)</dd>
|
|
55
348
|
</dl>
|
|
56
349
|
|
|
57
350
|
<h3>输入</h3>
|
|
58
351
|
<dl class="message-properties">
|
|
59
352
|
<dt>payload<span class="property-type">boolean|string|number</span></dt>
|
|
60
|
-
<dd
|
|
353
|
+
<dd>开关命令:
|
|
354
|
+
<ul>
|
|
355
|
+
<li>布尔值:true=开,false=关</li>
|
|
356
|
+
<li>字符串:"ON"/"OFF", "true"/"false", "1"/"0"</li>
|
|
357
|
+
<li>数字:1=开,0=关</li>
|
|
358
|
+
</ul>
|
|
359
|
+
</dd>
|
|
61
360
|
</dl>
|
|
62
361
|
|
|
63
362
|
<h3>输出</h3>
|
|
64
363
|
<dl class="message-properties">
|
|
65
364
|
<dt>payload<span class="property-type">boolean</span></dt>
|
|
66
|
-
<dd
|
|
365
|
+
<dd>当前开关状态(true=开,false=关)</dd>
|
|
366
|
+
|
|
367
|
+
<dt>topic<span class="property-type">string</span></dt>
|
|
368
|
+
<dd>主题:switch_{开关ID}_btn{按钮编号}</dd>
|
|
369
|
+
|
|
370
|
+
<dt>switchId<span class="property-type">number</span></dt>
|
|
371
|
+
<dd>物理开关面板ID(0-255)</dd>
|
|
372
|
+
|
|
373
|
+
<dt>button<span class="property-type">number</span></dt>
|
|
374
|
+
<dd>物理面板按钮编号(1-8)</dd>
|
|
375
|
+
|
|
376
|
+
<dt>targetSlave<span class="property-type">number</span></dt>
|
|
377
|
+
<dd>映射到的继电器从站地址</dd>
|
|
378
|
+
|
|
379
|
+
<dt>targetCoil<span class="property-type">number</span></dt>
|
|
380
|
+
<dd>映射到的继电器线圈编号</dd>
|
|
67
381
|
</dl>
|
|
382
|
+
|
|
383
|
+
<h3>MQTT主题</h3>
|
|
384
|
+
<p>本节点自动订阅和发布以下MQTT主题(基于目标继电器):</p>
|
|
385
|
+
<ul>
|
|
386
|
+
<li><strong>状态主题</strong>:modbus/relay/{目标从站地址}/{目标线圈}/state(订阅)</li>
|
|
387
|
+
<li><strong>命令主题</strong>:modbus/relay/{目标从站地址}/{目标线圈}/set(发布)</li>
|
|
388
|
+
</ul>
|
|
389
|
+
|
|
390
|
+
<h3>使用示例</h3>
|
|
391
|
+
<p><strong>场景1:</strong>开关ID=0(物理面板),按钮1 → 控制继电器10的线圈0</p>
|
|
392
|
+
<ul>
|
|
393
|
+
<li>开关ID:0(物理面板地址)</li>
|
|
394
|
+
<li>按钮编号:1(面板按钮)</li>
|
|
395
|
+
<li>目标从站地址:10(Modbus继电器)</li>
|
|
396
|
+
<li>目标线圈编号:0(继电器通道)</li>
|
|
397
|
+
</ul>
|
|
398
|
+
<p>MQTT主题:</p>
|
|
399
|
+
<ul>
|
|
400
|
+
<li>状态:modbus/relay/10/0/state</li>
|
|
401
|
+
<li>命令:modbus/relay/10/0/set</li>
|
|
402
|
+
</ul>
|
|
403
|
+
|
|
404
|
+
<p><strong>场景2:</strong>开关ID=5,按钮3 → 控制继电器11的线圈15</p>
|
|
405
|
+
<ul>
|
|
406
|
+
<li>开关ID:5</li>
|
|
407
|
+
<li>按钮编号:3</li>
|
|
408
|
+
<li>目标从站地址:11</li>
|
|
409
|
+
<li>目标线圈编号:15</li>
|
|
410
|
+
</ul>
|
|
411
|
+
<p>MQTT主题:</p>
|
|
412
|
+
<ul>
|
|
413
|
+
<li>状态:modbus/relay/11/15/state</li>
|
|
414
|
+
<li>命令:modbus/relay/11/15/set</li>
|
|
415
|
+
</ul>
|
|
416
|
+
|
|
417
|
+
<h3>特点</h3>
|
|
418
|
+
<ul>
|
|
419
|
+
<li>✅ 完全解耦:无需连线到主站节点</li>
|
|
420
|
+
<li>✅ 内置MQTT:自动管理MQTT连接</li>
|
|
421
|
+
<li>✅ 自动重连:连接断开自动恢复</li>
|
|
422
|
+
<li>✅ 状态同步:实时接收设备状态反馈</li>
|
|
423
|
+
<li>✅ 配置持久化:配置自动保存</li>
|
|
424
|
+
</ul>
|
|
68
425
|
</script>
|
|
69
426
|
|