node-red-contrib-symi-mesh 1.3.1 → 1.6.1
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 +219 -48
- package/examples/basic-example.json +151 -0
- package/lib/device-manager.js +109 -23
- package/lib/mqtt-helper.js +25 -4
- package/lib/protocol.js +22 -14
- package/lib/tcp-client.js +15 -13
- package/nodes/rs485-debug.html +238 -0
- package/nodes/rs485-debug.js +220 -0
- package/nodes/symi-485-bridge.html +376 -0
- package/nodes/symi-485-bridge.js +776 -0
- package/nodes/symi-485-config.html +125 -0
- package/nodes/symi-485-config.js +275 -0
- package/nodes/symi-cloud-sync.html +4 -4
- package/nodes/symi-cloud-sync.js +43 -10
- package/nodes/symi-device.html +1 -0
- package/nodes/symi-device.js +23 -14
- package/nodes/symi-gateway.js +121 -39
- package/nodes/symi-mqtt.js +233 -49
- package/package.json +7 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('rs485-debug', {
|
|
3
|
+
category: 'Symi Mesh',
|
|
4
|
+
color: '#87CEEB',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
rs485Config: { value: '', type: 'symi-485-config', required: true },
|
|
8
|
+
displayMode: { value: 'hex' },
|
|
9
|
+
showTimestamp: { value: true },
|
|
10
|
+
maxMessages: { value: 100 }
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 1,
|
|
14
|
+
icon: 'serial.svg',
|
|
15
|
+
paletteLabel: 'RS485调试',
|
|
16
|
+
label: function() {
|
|
17
|
+
return this.name || 'RS485调试';
|
|
18
|
+
},
|
|
19
|
+
oneditprepare: function() {
|
|
20
|
+
var node = this;
|
|
21
|
+
var autoRefreshInterval = null;
|
|
22
|
+
var autoRefreshEnabled = true;
|
|
23
|
+
|
|
24
|
+
// 设置编辑面板更宽
|
|
25
|
+
var panel = $('#dialog-form').parent();
|
|
26
|
+
if (panel.length && panel.width() < 700) {
|
|
27
|
+
panel.css('width', '800px');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 加载历史消息 - 显示全部100条(增量更新避免闪烁)
|
|
31
|
+
var lastMessageCount = 0;
|
|
32
|
+
var lastMessageTime = '';
|
|
33
|
+
|
|
34
|
+
function loadHistory() {
|
|
35
|
+
if (!node.id) return;
|
|
36
|
+
$.getJSON('/rs485-debug/history/' + node.id, function(messages) {
|
|
37
|
+
var container = $('#debug-history');
|
|
38
|
+
|
|
39
|
+
if (messages.length === 0) {
|
|
40
|
+
if (lastMessageCount !== 0) {
|
|
41
|
+
container.empty();
|
|
42
|
+
container.append('<div class="debug-empty">暂无数据,部署后将显示RS485通信数据</div>');
|
|
43
|
+
lastMessageCount = 0;
|
|
44
|
+
lastMessageTime = '';
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 检查是否有新消息(通过最后一条消息的时间戳判断)
|
|
50
|
+
var latestTime = messages.length > 0 ? messages[messages.length - 1].timestamp : '';
|
|
51
|
+
if (latestTime === lastMessageTime && messages.length === lastMessageCount) {
|
|
52
|
+
return; // 没有新消息,不更新DOM
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 有新消息时才重建列表
|
|
56
|
+
container.empty();
|
|
57
|
+
messages.forEach(function(msg) {
|
|
58
|
+
var dir = msg.direction === 'TX' ? '→ TX' : '← RX';
|
|
59
|
+
var dirClass = msg.direction === 'TX' ? 'tx' : 'rx';
|
|
60
|
+
var html = '<div class="debug-line ' + dirClass + '">' +
|
|
61
|
+
'<span class="time">' + msg.timestampLocal + '</span>' +
|
|
62
|
+
'<span class="dir">' + dir + '</span>' +
|
|
63
|
+
'<span class="hex">' + msg.hex + '</span>';
|
|
64
|
+
if (msg.modbus) {
|
|
65
|
+
html += '<span class="info">从机:' + msg.modbus.slaveAddr + ' ' + msg.modbus.funcName + '</span>';
|
|
66
|
+
}
|
|
67
|
+
html += '</div>';
|
|
68
|
+
container.append(html);
|
|
69
|
+
});
|
|
70
|
+
container.scrollTop(container[0].scrollHeight);
|
|
71
|
+
|
|
72
|
+
// 更新状态
|
|
73
|
+
lastMessageCount = messages.length;
|
|
74
|
+
lastMessageTime = latestTime;
|
|
75
|
+
$('#debug-status').text('已缓存 ' + messages.length + ' 条');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 自动刷新(实时采集)
|
|
80
|
+
function startAutoRefresh() {
|
|
81
|
+
if (autoRefreshInterval) clearInterval(autoRefreshInterval);
|
|
82
|
+
autoRefreshInterval = setInterval(function() {
|
|
83
|
+
if (autoRefreshEnabled) {
|
|
84
|
+
loadHistory();
|
|
85
|
+
}
|
|
86
|
+
}, 500); // 每500ms刷新一次
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stopAutoRefresh() {
|
|
90
|
+
if (autoRefreshInterval) {
|
|
91
|
+
clearInterval(autoRefreshInterval);
|
|
92
|
+
autoRefreshInterval = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 清空历史
|
|
97
|
+
$('#btn-clear-history').on('click', function() {
|
|
98
|
+
$.post('/rs485-debug/clear/' + node.id, function() {
|
|
99
|
+
loadHistory();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 手动刷新
|
|
104
|
+
$('#btn-refresh-history').on('click', loadHistory);
|
|
105
|
+
|
|
106
|
+
// 切换自动刷新
|
|
107
|
+
$('#btn-auto-refresh').on('click', function() {
|
|
108
|
+
autoRefreshEnabled = !autoRefreshEnabled;
|
|
109
|
+
if (autoRefreshEnabled) {
|
|
110
|
+
$(this).addClass('active').find('i').removeClass('fa-pause').addClass('fa-play');
|
|
111
|
+
$(this).find('span').text('实时');
|
|
112
|
+
startAutoRefresh();
|
|
113
|
+
} else {
|
|
114
|
+
$(this).removeClass('active').find('i').removeClass('fa-play').addClass('fa-pause');
|
|
115
|
+
$(this).find('span').text('暂停');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 初始加载并启动自动刷新
|
|
120
|
+
loadHistory();
|
|
121
|
+
startAutoRefresh();
|
|
122
|
+
|
|
123
|
+
// 编辑面板关闭时停止自动刷新
|
|
124
|
+
var originalCancel = RED.editor.cancel;
|
|
125
|
+
var cleanupDone = false;
|
|
126
|
+
function cleanup() {
|
|
127
|
+
if (!cleanupDone) {
|
|
128
|
+
cleanupDone = true;
|
|
129
|
+
stopAutoRefresh();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 监听对话框关闭事件
|
|
133
|
+
$(document).one('editableDialogClose', cleanup);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<script type="text/html" data-template-name="rs485-debug">
|
|
139
|
+
<style>
|
|
140
|
+
.debug-section { margin: 12px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
|
141
|
+
.debug-section h4 { margin: 0 0 10px 0; font-size: 13px; color: #333; }
|
|
142
|
+
#debug-history { max-height: calc(100vh - 400px); min-height: 400px; overflow-y: auto; background: #1e1e1e; border-radius: 4px; padding: 10px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
|
143
|
+
.debug-empty { color: #888; text-align: center; padding: 20px; }
|
|
144
|
+
.debug-line { padding: 3px 0; border-bottom: 1px solid #333; color: #ddd; }
|
|
145
|
+
.debug-line.tx { color: #98c379; }
|
|
146
|
+
.debug-line.rx { color: #61afef; }
|
|
147
|
+
.debug-line .time { color: #888; margin-right: 8px; }
|
|
148
|
+
.debug-line .dir { font-weight: bold; margin-right: 8px; min-width: 40px; display: inline-block; }
|
|
149
|
+
.debug-line .hex { color: #e5c07b; }
|
|
150
|
+
.debug-line .info { color: #c678dd; margin-left: 10px; font-size: 10px; }
|
|
151
|
+
.debug-buttons { margin-top: 8px; }
|
|
152
|
+
.debug-buttons button { margin-right: 8px; }
|
|
153
|
+
</style>
|
|
154
|
+
|
|
155
|
+
<div class="form-row">
|
|
156
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
157
|
+
<input type="text" id="node-input-name" placeholder="RS485调试">
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="form-row">
|
|
161
|
+
<label for="node-input-rs485Config"><i class="fa fa-plug"></i> RS485连接</label>
|
|
162
|
+
<input type="text" id="node-input-rs485Config">
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="form-row">
|
|
166
|
+
<label for="node-input-displayMode"><i class="fa fa-eye"></i> 显示模式</label>
|
|
167
|
+
<select id="node-input-displayMode" style="width:70%">
|
|
168
|
+
<option value="hex">十六进制 (HEX)</option>
|
|
169
|
+
<option value="ascii">ASCII字符</option>
|
|
170
|
+
<option value="both">十六进制 + ASCII</option>
|
|
171
|
+
</select>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="form-row">
|
|
175
|
+
<label for="node-input-showTimestamp"><i class="fa fa-clock-o"></i> 显示时间戳</label>
|
|
176
|
+
<input type="checkbox" id="node-input-showTimestamp" style="width:auto; margin-left:0">
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="form-row">
|
|
180
|
+
<label for="node-input-maxMessages"><i class="fa fa-database"></i> 缓存条数</label>
|
|
181
|
+
<input type="number" id="node-input-maxMessages" min="10" max="1000" style="width:70%">
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="debug-section">
|
|
185
|
+
<h4><i class="fa fa-terminal"></i> 通信数据预览 <span id="debug-status" style="font-weight:normal;color:#888;font-size:11px;"></span></h4>
|
|
186
|
+
<div id="debug-history"></div>
|
|
187
|
+
<div class="debug-buttons">
|
|
188
|
+
<button type="button" id="btn-auto-refresh" class="red-ui-button red-ui-button-small active" style="background:#4CAF50;color:white;">
|
|
189
|
+
<i class="fa fa-play"></i> <span>实时</span>
|
|
190
|
+
</button>
|
|
191
|
+
<button type="button" id="btn-refresh-history" class="red-ui-button red-ui-button-small">
|
|
192
|
+
<i class="fa fa-refresh"></i> 刷新
|
|
193
|
+
</button>
|
|
194
|
+
<button type="button" id="btn-clear-history" class="red-ui-button red-ui-button-small">
|
|
195
|
+
<i class="fa fa-trash"></i> 清空
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="form-tips">
|
|
201
|
+
<p><b>使用说明:</b></p>
|
|
202
|
+
<p>• 自动显示RS485总线上的所有通信数据</p>
|
|
203
|
+
<p>• <span style="color:#98c379">绿色TX</span>:发送数据 | <span style="color:#61afef">蓝色RX</span>:接收数据</p>
|
|
204
|
+
<p>• 输入端可发送十六进制字符串测试(如:01 03 00 00 00 01)</p>
|
|
205
|
+
<p>• 输出端输出格式化的通信日志</p>
|
|
206
|
+
</div>
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<script type="text/html" data-help-name="rs485-debug">
|
|
210
|
+
<p>RS485/Modbus通信调试节点,用于抓取并显示原始字节流</p>
|
|
211
|
+
|
|
212
|
+
<h3>功能</h3>
|
|
213
|
+
<ul>
|
|
214
|
+
<li>实时显示RS485总线通信数据</li>
|
|
215
|
+
<li>自动解析Modbus RTU帧结构</li>
|
|
216
|
+
<li>支持十六进制和ASCII两种显示模式</li>
|
|
217
|
+
<li>可手动发送测试帧</li>
|
|
218
|
+
</ul>
|
|
219
|
+
|
|
220
|
+
<h3>输入</h3>
|
|
221
|
+
<p>发送十六进制字符串或Buffer到RS485总线</p>
|
|
222
|
+
<pre>msg.payload = "01 03 00 00 00 01 84 0A"</pre>
|
|
223
|
+
|
|
224
|
+
<h3>输出</h3>
|
|
225
|
+
<dl>
|
|
226
|
+
<dt>payload</dt><dd>格式化的通信日志字符串</dd>
|
|
227
|
+
<dt>topic</dt><dd>rs485/tx 或 rs485/rx</dd>
|
|
228
|
+
<dt>rs485</dt><dd>完整的通信数据对象(含时间戳、原始数据等)</dd>
|
|
229
|
+
</dl>
|
|
230
|
+
|
|
231
|
+
<h3>Modbus功能码解析</h3>
|
|
232
|
+
<ul>
|
|
233
|
+
<li>0x01 - 读线圈</li>
|
|
234
|
+
<li>0x03 - 读保持寄存器</li>
|
|
235
|
+
<li>0x06 - 写单寄存器</li>
|
|
236
|
+
<li>0x10 - 写多寄存器</li>
|
|
237
|
+
</ul>
|
|
238
|
+
</script>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RS485 Debug Node
|
|
3
|
+
* 用于抓取并显示原始485字节流,以十六进制格式输出
|
|
4
|
+
* 帮助调试串口或TCP网关下的Modbus通信
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = function(RED) {
|
|
8
|
+
function RS485DebugNode(config) {
|
|
9
|
+
RED.nodes.createNode(this, config);
|
|
10
|
+
const node = this;
|
|
11
|
+
|
|
12
|
+
node.name = config.name || '';
|
|
13
|
+
node.rs485Config = RED.nodes.getNode(config.rs485Config);
|
|
14
|
+
node.displayMode = config.displayMode || 'hex'; // hex, ascii, both
|
|
15
|
+
node.showTimestamp = config.showTimestamp !== false;
|
|
16
|
+
node.maxMessages = parseInt(config.maxMessages) || 100;
|
|
17
|
+
|
|
18
|
+
// 消息缓存
|
|
19
|
+
node.messageBuffer = [];
|
|
20
|
+
|
|
21
|
+
if (!node.rs485Config) {
|
|
22
|
+
node.status({ fill: 'red', shape: 'ring', text: '未配置RS485连接' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 更新状态
|
|
27
|
+
function updateStatus() {
|
|
28
|
+
const configName = node.rs485Config.name || `${node.rs485Config.host}:${node.rs485Config.port}`;
|
|
29
|
+
if (node.rs485Config.connected) {
|
|
30
|
+
node.status({ fill: 'green', shape: 'dot', text: `监听中 ${configName} (${node.messageBuffer.length}条)` });
|
|
31
|
+
} else {
|
|
32
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `等待连接 ${configName}...` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 格式化数据为十六进制
|
|
37
|
+
function formatHex(buffer) {
|
|
38
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
39
|
+
buffer = Buffer.from(buffer);
|
|
40
|
+
}
|
|
41
|
+
return buffer.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 格式化数据为ASCII(可打印字符)
|
|
45
|
+
function formatAscii(buffer) {
|
|
46
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
47
|
+
buffer = Buffer.from(buffer);
|
|
48
|
+
}
|
|
49
|
+
let result = '';
|
|
50
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
51
|
+
const byte = buffer[i];
|
|
52
|
+
result += (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.';
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 解析Modbus功能码
|
|
58
|
+
function parseModbusFunction(buffer) {
|
|
59
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 2) return null;
|
|
60
|
+
const funcCode = buffer[1];
|
|
61
|
+
const funcNames = {
|
|
62
|
+
0x01: '读线圈',
|
|
63
|
+
0x02: '读离散输入',
|
|
64
|
+
0x03: '读保持寄存器',
|
|
65
|
+
0x04: '读输入寄存器',
|
|
66
|
+
0x05: '写单线圈',
|
|
67
|
+
0x06: '写单寄存器',
|
|
68
|
+
0x0F: '写多线圈',
|
|
69
|
+
0x10: '写多寄存器'
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
slaveAddr: buffer[0],
|
|
73
|
+
funcCode: funcCode,
|
|
74
|
+
funcName: funcNames[funcCode] || '未知(0x' + funcCode.toString(16).toUpperCase() + ')'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 处理接收到的数据帧
|
|
79
|
+
function handleFrame(frame, direction) {
|
|
80
|
+
const timestamp = new Date();
|
|
81
|
+
const hexData = formatHex(frame);
|
|
82
|
+
const modbusInfo = parseModbusFunction(frame);
|
|
83
|
+
|
|
84
|
+
const msg = {
|
|
85
|
+
timestamp: timestamp.toISOString(),
|
|
86
|
+
timestampLocal: timestamp.toLocaleTimeString('zh-CN', { hour12: false }) + '.' + timestamp.getMilliseconds().toString().padStart(3, '0'),
|
|
87
|
+
direction: direction, // 'TX' or 'RX'
|
|
88
|
+
length: frame.length,
|
|
89
|
+
hex: hexData,
|
|
90
|
+
raw: frame,
|
|
91
|
+
modbus: modbusInfo
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (node.displayMode === 'ascii' || node.displayMode === 'both') {
|
|
95
|
+
msg.ascii = formatAscii(frame);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 添加到缓存
|
|
99
|
+
node.messageBuffer.push(msg);
|
|
100
|
+
if (node.messageBuffer.length > node.maxMessages) {
|
|
101
|
+
node.messageBuffer.shift();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 构建输出payload
|
|
105
|
+
let displayText = '';
|
|
106
|
+
if (node.showTimestamp) {
|
|
107
|
+
displayText += '[' + msg.timestampLocal + '] ';
|
|
108
|
+
}
|
|
109
|
+
displayText += (direction === 'TX' ? '→ TX: ' : '← RX: ');
|
|
110
|
+
displayText += hexData;
|
|
111
|
+
|
|
112
|
+
if (modbusInfo) {
|
|
113
|
+
displayText += ' | 从机:' + modbusInfo.slaveAddr + ' ' + modbusInfo.funcName;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 输出消息
|
|
117
|
+
node.send({
|
|
118
|
+
payload: displayText,
|
|
119
|
+
topic: 'rs485/' + direction.toLowerCase(),
|
|
120
|
+
rs485: msg
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 同时输出到debug面板
|
|
124
|
+
node.debug(displayText);
|
|
125
|
+
updateStatus();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 【重要】先绑定事件监听器,再注册到配置节点
|
|
129
|
+
// 否则如果连接很快建立,事件可能丢失
|
|
130
|
+
|
|
131
|
+
// 监听发送帧
|
|
132
|
+
node.rs485Config.on('tx', function(frame) {
|
|
133
|
+
handleFrame(frame, 'TX');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 监听原始接收数据(显示所有总线数据)
|
|
137
|
+
node.rs485Config.on('data', function(data) {
|
|
138
|
+
handleFrame(data, 'RX');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 连接状态事件
|
|
142
|
+
node.rs485Config.on('connected', function() {
|
|
143
|
+
node.log(`RS485连接已建立: ${node.rs485Config.host}:${node.rs485Config.port}`);
|
|
144
|
+
updateStatus();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
node.rs485Config.on('disconnected', function() {
|
|
148
|
+
node.warn('RS485连接已断开');
|
|
149
|
+
updateStatus();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
node.rs485Config.on('error', function(err) {
|
|
153
|
+
node.error('RS485错误: ' + err.message);
|
|
154
|
+
node.status({ fill: 'red', shape: 'ring', text: '错误: ' + err.message });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 现在注册到配置节点(这会触发连接)
|
|
158
|
+
node.rs485Config.register(node);
|
|
159
|
+
node.log(`已注册到RS485配置: ${node.rs485Config.host}:${node.rs485Config.port}`);
|
|
160
|
+
|
|
161
|
+
// 处理输入消息(手动发送测试帧)
|
|
162
|
+
node.on('input', function(msg) {
|
|
163
|
+
if (msg.payload) {
|
|
164
|
+
let frame;
|
|
165
|
+
if (Buffer.isBuffer(msg.payload)) {
|
|
166
|
+
frame = msg.payload;
|
|
167
|
+
} else if (typeof msg.payload === 'string') {
|
|
168
|
+
// 尝试解析十六进制字符串
|
|
169
|
+
const hexStr = msg.payload.replace(/\s/g, '');
|
|
170
|
+
if (/^[0-9A-Fa-f]+$/.test(hexStr)) {
|
|
171
|
+
frame = Buffer.from(hexStr, 'hex');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (frame && node.rs485Config.connected) {
|
|
176
|
+
node.rs485Config.send(frame);
|
|
177
|
+
handleFrame(frame, 'TX');
|
|
178
|
+
node.log('手动发送: ' + formatHex(frame));
|
|
179
|
+
} else if (!node.rs485Config.connected) {
|
|
180
|
+
node.warn('RS485未连接,无法发送');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 初始状态
|
|
186
|
+
updateStatus();
|
|
187
|
+
|
|
188
|
+
// 清理
|
|
189
|
+
node.on('close', function(done) {
|
|
190
|
+
if (node.rs485Config) {
|
|
191
|
+
node.rs485Config.deregister(node);
|
|
192
|
+
}
|
|
193
|
+
node.messageBuffer = [];
|
|
194
|
+
done();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
RED.nodes.registerType('rs485-debug', RS485DebugNode);
|
|
199
|
+
|
|
200
|
+
// API: 获取消息历史
|
|
201
|
+
RED.httpAdmin.get('/rs485-debug/history/:nodeId', function(req, res) {
|
|
202
|
+
const node = RED.nodes.getNode(req.params.nodeId);
|
|
203
|
+
if (node && node.messageBuffer) {
|
|
204
|
+
res.json(node.messageBuffer);
|
|
205
|
+
} else {
|
|
206
|
+
res.json([]);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// API: 清空消息历史
|
|
211
|
+
RED.httpAdmin.post('/rs485-debug/clear/:nodeId', function(req, res) {
|
|
212
|
+
const node = RED.nodes.getNode(req.params.nodeId);
|
|
213
|
+
if (node) {
|
|
214
|
+
node.messageBuffer = [];
|
|
215
|
+
res.json({ success: true });
|
|
216
|
+
} else {
|
|
217
|
+
res.json({ success: false, error: '节点未找到' });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
};
|