node-red-contrib-symi-mesh 1.6.0 → 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/nodes/rs485-debug.html +77 -7
- package/nodes/rs485-debug.js +11 -6
- package/nodes/symi-485-bridge.html +19 -5
- package/nodes/symi-485-bridge.js +148 -13
- package/nodes/symi-485-config.js +3 -0
- package/package.json +1 -1
package/nodes/rs485-debug.html
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
oneditprepare: function() {
|
|
20
20
|
var node = this;
|
|
21
|
+
var autoRefreshInterval = null;
|
|
22
|
+
var autoRefreshEnabled = true;
|
|
21
23
|
|
|
22
24
|
// 设置编辑面板更宽
|
|
23
25
|
var panel = $('#dialog-form').parent();
|
|
@@ -25,17 +27,34 @@
|
|
|
25
27
|
panel.css('width', '800px');
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
// 加载历史消息
|
|
30
|
+
// 加载历史消息 - 显示全部100条(增量更新避免闪烁)
|
|
31
|
+
var lastMessageCount = 0;
|
|
32
|
+
var lastMessageTime = '';
|
|
33
|
+
|
|
29
34
|
function loadHistory() {
|
|
30
35
|
if (!node.id) return;
|
|
31
36
|
$.getJSON('/rs485-debug/history/' + node.id, function(messages) {
|
|
32
37
|
var container = $('#debug-history');
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
if (messages.length === 0) {
|
|
35
|
-
|
|
40
|
+
if (lastMessageCount !== 0) {
|
|
41
|
+
container.empty();
|
|
42
|
+
container.append('<div class="debug-empty">暂无数据,部署后将显示RS485通信数据</div>');
|
|
43
|
+
lastMessageCount = 0;
|
|
44
|
+
lastMessageTime = '';
|
|
45
|
+
}
|
|
36
46
|
return;
|
|
37
47
|
}
|
|
38
|
-
|
|
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) {
|
|
39
58
|
var dir = msg.direction === 'TX' ? '→ TX' : '← RX';
|
|
40
59
|
var dirClass = msg.direction === 'TX' ? 'tx' : 'rx';
|
|
41
60
|
var html = '<div class="debug-line ' + dirClass + '">' +
|
|
@@ -49,9 +68,31 @@
|
|
|
49
68
|
container.append(html);
|
|
50
69
|
});
|
|
51
70
|
container.scrollTop(container[0].scrollHeight);
|
|
71
|
+
|
|
72
|
+
// 更新状态
|
|
73
|
+
lastMessageCount = messages.length;
|
|
74
|
+
lastMessageTime = latestTime;
|
|
75
|
+
$('#debug-status').text('已缓存 ' + messages.length + ' 条');
|
|
52
76
|
});
|
|
53
77
|
}
|
|
54
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
|
+
|
|
55
96
|
// 清空历史
|
|
56
97
|
$('#btn-clear-history').on('click', function() {
|
|
57
98
|
$.post('/rs485-debug/clear/' + node.id, function() {
|
|
@@ -59,11 +100,37 @@
|
|
|
59
100
|
});
|
|
60
101
|
});
|
|
61
102
|
|
|
62
|
-
//
|
|
103
|
+
// 手动刷新
|
|
63
104
|
$('#btn-refresh-history').on('click', loadHistory);
|
|
64
105
|
|
|
65
|
-
//
|
|
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
|
+
// 初始加载并启动自动刷新
|
|
66
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);
|
|
67
134
|
}
|
|
68
135
|
});
|
|
69
136
|
</script>
|
|
@@ -115,9 +182,12 @@
|
|
|
115
182
|
</div>
|
|
116
183
|
|
|
117
184
|
<div class="debug-section">
|
|
118
|
-
<h4><i class="fa fa-terminal"></i>
|
|
185
|
+
<h4><i class="fa fa-terminal"></i> 通信数据预览 <span id="debug-status" style="font-weight:normal;color:#888;font-size:11px;"></span></h4>
|
|
119
186
|
<div id="debug-history"></div>
|
|
120
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>
|
|
121
191
|
<button type="button" id="btn-refresh-history" class="red-ui-button red-ui-button-small">
|
|
122
192
|
<i class="fa fa-refresh"></i> 刷新
|
|
123
193
|
</button>
|
package/nodes/rs485-debug.js
CHANGED
|
@@ -23,15 +23,13 @@ module.exports = function(RED) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// 注册到RS485配置节点
|
|
27
|
-
node.rs485Config.register(node);
|
|
28
|
-
|
|
29
26
|
// 更新状态
|
|
30
27
|
function updateStatus() {
|
|
28
|
+
const configName = node.rs485Config.name || `${node.rs485Config.host}:${node.rs485Config.port}`;
|
|
31
29
|
if (node.rs485Config.connected) {
|
|
32
|
-
node.status({ fill: 'green', shape: 'dot', text:
|
|
30
|
+
node.status({ fill: 'green', shape: 'dot', text: `监听中 ${configName} (${node.messageBuffer.length}条)` });
|
|
33
31
|
} else {
|
|
34
|
-
node.status({ fill: 'yellow', shape: 'ring', text:
|
|
32
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `等待连接 ${configName}...` });
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
@@ -127,6 +125,9 @@ module.exports = function(RED) {
|
|
|
127
125
|
updateStatus();
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
// 【重要】先绑定事件监听器,再注册到配置节点
|
|
129
|
+
// 否则如果连接很快建立,事件可能丢失
|
|
130
|
+
|
|
130
131
|
// 监听发送帧
|
|
131
132
|
node.rs485Config.on('tx', function(frame) {
|
|
132
133
|
handleFrame(frame, 'TX');
|
|
@@ -139,7 +140,7 @@ module.exports = function(RED) {
|
|
|
139
140
|
|
|
140
141
|
// 连接状态事件
|
|
141
142
|
node.rs485Config.on('connected', function() {
|
|
142
|
-
node.log(
|
|
143
|
+
node.log(`RS485连接已建立: ${node.rs485Config.host}:${node.rs485Config.port}`);
|
|
143
144
|
updateStatus();
|
|
144
145
|
});
|
|
145
146
|
|
|
@@ -153,6 +154,10 @@ module.exports = function(RED) {
|
|
|
153
154
|
node.status({ fill: 'red', shape: 'ring', text: '错误: ' + err.message });
|
|
154
155
|
});
|
|
155
156
|
|
|
157
|
+
// 现在注册到配置节点(这会触发连接)
|
|
158
|
+
node.rs485Config.register(node);
|
|
159
|
+
node.log(`已注册到RS485配置: ${node.rs485Config.host}:${node.rs485Config.port}`);
|
|
160
|
+
|
|
156
161
|
// 处理输入消息(手动发送测试帧)
|
|
157
162
|
node.on('input', function(msg) {
|
|
158
163
|
if (msg.payload) {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
rs485Config: { value: '', type: 'symi-485-config', required: true },
|
|
9
9
|
mappings: { value: '[]' }
|
|
10
10
|
},
|
|
11
|
-
inputs:
|
|
12
|
-
outputs:
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
13
|
icon: 'bridge.png',
|
|
14
14
|
paletteLabel: 'RS485桥接',
|
|
15
15
|
label: function() {
|
|
@@ -344,19 +344,33 @@
|
|
|
344
344
|
<li>命令队列顺序处理</li>
|
|
345
345
|
<li>防循环保护机制</li>
|
|
346
346
|
<li>断电重启自动恢复</li>
|
|
347
|
+
<li>支持多个桥接节点共享同一Mesh网关</li>
|
|
348
|
+
<li>输出端口可连接debug节点查看通信数据</li>
|
|
347
349
|
</ul>
|
|
348
350
|
|
|
349
351
|
<h3>配置说明</h3>
|
|
350
352
|
<dl>
|
|
351
|
-
<dt>Mesh网关</dt><dd>选择Symi Mesh
|
|
352
|
-
<dt>RS485连接</dt><dd>选择RS485
|
|
353
|
+
<dt>Mesh网关</dt><dd>选择Symi Mesh网关节点(可多个桥接节点共享)</dd>
|
|
354
|
+
<dt>RS485连接</dt><dd>选择RS485连接配置(不同TCP端口独立配置)</dd>
|
|
353
355
|
<dt>实体映射</dt><dd>配置Mesh与RS485实体的对应关系</dd>
|
|
354
356
|
</dl>
|
|
355
357
|
|
|
358
|
+
<h3>输入</h3>
|
|
359
|
+
<p>支持手动发送Modbus帧或触发同步:</p>
|
|
360
|
+
<pre>msg.payload = { hex: "01 06 10 31 00 01" } // 发送十六进制帧</pre>
|
|
361
|
+
<pre>msg.payload = { sync: "mesh-to-rs485", mac: "xxx", state: {...} }</pre>
|
|
362
|
+
|
|
363
|
+
<h3>输出</h3>
|
|
364
|
+
<p>输出RS485通信数据供调试:</p>
|
|
365
|
+
<dl>
|
|
366
|
+
<dt>topic</dt><dd>rs485-bridge/rx 或 rs485-bridge/tx</dd>
|
|
367
|
+
<dt>payload</dt><dd>包含方向、从机地址、功能码、十六进制数据</dd>
|
|
368
|
+
</dl>
|
|
369
|
+
|
|
356
370
|
<h3>同步规则</h3>
|
|
357
371
|
<ul>
|
|
358
372
|
<li>只同步双方都支持的功能点</li>
|
|
359
|
-
<li
|
|
373
|
+
<li>开关协议:按键0x1031-0x1036,指示灯0x1021-0x1026</li>
|
|
360
374
|
<li>开关设备需指定具体按键</li>
|
|
361
375
|
</ul>
|
|
362
376
|
</script>
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -15,12 +15,15 @@ module.exports = function(RED) {
|
|
|
15
15
|
name: '话语前湾',
|
|
16
16
|
devices: {
|
|
17
17
|
// ===== 开关类型 (A4B3协议头,FC=06写单寄存器) =====
|
|
18
|
+
// 按键: 0x1031-0x1036 (按键1-6)
|
|
19
|
+
// 指示灯: 0x1021-0x1026 (指示灯1-6)
|
|
18
20
|
'switch_1': {
|
|
19
21
|
name: '一键开关',
|
|
20
22
|
type: 'switch',
|
|
21
23
|
channels: 1,
|
|
22
24
|
registers: {
|
|
23
|
-
switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 }
|
|
25
|
+
switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
|
|
26
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 }
|
|
24
27
|
}
|
|
25
28
|
},
|
|
26
29
|
'switch_2': {
|
|
@@ -29,7 +32,9 @@ module.exports = function(RED) {
|
|
|
29
32
|
channels: 2,
|
|
30
33
|
registers: {
|
|
31
34
|
switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
|
|
32
|
-
switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 }
|
|
35
|
+
switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
|
|
36
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
|
|
37
|
+
led2: { address: 0x1022, type: 'holding', on: 1, off: 0 }
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
'switch_3': {
|
|
@@ -39,7 +44,10 @@ module.exports = function(RED) {
|
|
|
39
44
|
registers: {
|
|
40
45
|
switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
|
|
41
46
|
switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
|
|
42
|
-
switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 }
|
|
47
|
+
switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
|
|
48
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
|
|
49
|
+
led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
|
|
50
|
+
led3: { address: 0x1023, type: 'holding', on: 1, off: 0 }
|
|
43
51
|
}
|
|
44
52
|
},
|
|
45
53
|
'switch_4': {
|
|
@@ -50,7 +58,11 @@ module.exports = function(RED) {
|
|
|
50
58
|
switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
|
|
51
59
|
switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
|
|
52
60
|
switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
|
|
53
|
-
switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 }
|
|
61
|
+
switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 },
|
|
62
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
|
|
63
|
+
led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
|
|
64
|
+
led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
|
|
65
|
+
led4: { address: 0x1024, type: 'holding', on: 1, off: 0 }
|
|
54
66
|
}
|
|
55
67
|
},
|
|
56
68
|
'switch_6': {
|
|
@@ -63,7 +75,13 @@ module.exports = function(RED) {
|
|
|
63
75
|
switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
|
|
64
76
|
switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 },
|
|
65
77
|
switch5: { address: 0x1035, type: 'holding', on: 1, off: 0 },
|
|
66
|
-
switch6: { address: 0x1036, type: 'holding', on: 1, off: 0 }
|
|
78
|
+
switch6: { address: 0x1036, type: 'holding', on: 1, off: 0 },
|
|
79
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
|
|
80
|
+
led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
|
|
81
|
+
led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
|
|
82
|
+
led4: { address: 0x1024, type: 'holding', on: 1, off: 0 },
|
|
83
|
+
led5: { address: 0x1025, type: 'holding', on: 1, off: 0 },
|
|
84
|
+
led6: { address: 0x1026, type: 'holding', on: 1, off: 0 }
|
|
67
85
|
}
|
|
68
86
|
},
|
|
69
87
|
'switch_8': {
|
|
@@ -78,7 +96,15 @@ module.exports = function(RED) {
|
|
|
78
96
|
switch5: { address: 0x1035, type: 'holding', on: 1, off: 0 },
|
|
79
97
|
switch6: { address: 0x1036, type: 'holding', on: 1, off: 0 },
|
|
80
98
|
switch7: { address: 0x1037, type: 'holding', on: 1, off: 0 },
|
|
81
|
-
switch8: { address: 0x1038, type: 'holding', on: 1, off: 0 }
|
|
99
|
+
switch8: { address: 0x1038, type: 'holding', on: 1, off: 0 },
|
|
100
|
+
led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
|
|
101
|
+
led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
|
|
102
|
+
led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
|
|
103
|
+
led4: { address: 0x1024, type: 'holding', on: 1, off: 0 },
|
|
104
|
+
led5: { address: 0x1025, type: 'holding', on: 1, off: 0 },
|
|
105
|
+
led6: { address: 0x1026, type: 'holding', on: 1, off: 0 },
|
|
106
|
+
led7: { address: 0x1027, type: 'holding', on: 1, off: 0 },
|
|
107
|
+
led8: { address: 0x1028, type: 'holding', on: 1, off: 0 }
|
|
82
108
|
}
|
|
83
109
|
},
|
|
84
110
|
// ===== 调光类型 =====
|
|
@@ -111,10 +137,11 @@ module.exports = function(RED) {
|
|
|
111
137
|
}
|
|
112
138
|
},
|
|
113
139
|
// ===== 空调 (A5B5协议头) =====
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
// 模式: 1=制热, 2=制冷, 4=送风, 8=除湿
|
|
141
|
+
// 风速: 1=低风, 2=中风, 3=高风
|
|
142
|
+
'ac_living': {
|
|
143
|
+
name: '客厅空调',
|
|
116
144
|
type: 'climate',
|
|
117
|
-
// 注意:不同空调寄存器地址不同,这里用基址,实际地址=基址+偏移
|
|
118
145
|
registers: {
|
|
119
146
|
switch: { address: 0x0FA0, type: 'holding', on: 1, off: 0 },
|
|
120
147
|
mode: { address: 0x0FA1, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
|
|
@@ -122,6 +149,36 @@ module.exports = function(RED) {
|
|
|
122
149
|
fanSpeed: { address: 0x0FA3, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
|
|
123
150
|
}
|
|
124
151
|
},
|
|
152
|
+
'ac_bedroom2_1': {
|
|
153
|
+
name: '次卧1空调',
|
|
154
|
+
type: 'climate',
|
|
155
|
+
registers: {
|
|
156
|
+
switch: { address: 0x0FA4, type: 'holding', on: 1, off: 0 },
|
|
157
|
+
mode: { address: 0x0FA5, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
|
|
158
|
+
targetTemp: { address: 0x0FA6, type: 'holding' },
|
|
159
|
+
fanSpeed: { address: 0x0FA7, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
'ac_bedroom2_2': {
|
|
163
|
+
name: '次卧2空调',
|
|
164
|
+
type: 'climate',
|
|
165
|
+
registers: {
|
|
166
|
+
switch: { address: 0x0FA8, type: 'holding', on: 1, off: 0 },
|
|
167
|
+
mode: { address: 0x0FA9, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
|
|
168
|
+
targetTemp: { address: 0x0FAA, type: 'holding' },
|
|
169
|
+
fanSpeed: { address: 0x0FAB, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
'ac_master': {
|
|
173
|
+
name: '主卧空调',
|
|
174
|
+
type: 'climate',
|
|
175
|
+
registers: {
|
|
176
|
+
switch: { address: 0x0FAC, type: 'holding', on: 1, off: 0 },
|
|
177
|
+
mode: { address: 0x0FAD, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
|
|
178
|
+
targetTemp: { address: 0x0FAE, type: 'holding' },
|
|
179
|
+
fanSpeed: { address: 0x0FAF, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
125
182
|
// ===== 地暖 (A3B3协议头) =====
|
|
126
183
|
'floor_heating': {
|
|
127
184
|
name: '地暖',
|
|
@@ -563,6 +620,81 @@ module.exports = function(RED) {
|
|
|
563
620
|
init();
|
|
564
621
|
}
|
|
565
622
|
|
|
623
|
+
// 输出调试信息
|
|
624
|
+
node.outputDebug = function(direction, info) {
|
|
625
|
+
const msg = {
|
|
626
|
+
topic: 'rs485-bridge/' + direction,
|
|
627
|
+
payload: info,
|
|
628
|
+
timestamp: new Date().toISOString()
|
|
629
|
+
};
|
|
630
|
+
node.send(msg);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// 监听RS485帧事件并输出
|
|
634
|
+
if (node.rs485Config) {
|
|
635
|
+
node.rs485Config.on('frame', (frame) => {
|
|
636
|
+
const slaveAddr = frame[0];
|
|
637
|
+
const fc = frame[1];
|
|
638
|
+
const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
639
|
+
node.outputDebug('rx', {
|
|
640
|
+
direction: 'RX',
|
|
641
|
+
slaveAddr: slaveAddr,
|
|
642
|
+
funcCode: fc,
|
|
643
|
+
hex: hexData,
|
|
644
|
+
raw: frame
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
node.rs485Config.on('tx', (frame) => {
|
|
649
|
+
const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
650
|
+
node.outputDebug('tx', {
|
|
651
|
+
direction: 'TX',
|
|
652
|
+
hex: hexData,
|
|
653
|
+
raw: frame
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 处理输入消息(手动控制)
|
|
659
|
+
node.on('input', function(msg) {
|
|
660
|
+
if (!msg.payload) return;
|
|
661
|
+
|
|
662
|
+
// 支持直接发送Modbus帧
|
|
663
|
+
if (msg.payload.modbusFrame || msg.payload.hex) {
|
|
664
|
+
let frame;
|
|
665
|
+
if (Buffer.isBuffer(msg.payload.modbusFrame)) {
|
|
666
|
+
frame = msg.payload.modbusFrame;
|
|
667
|
+
} else if (typeof msg.payload.hex === 'string') {
|
|
668
|
+
const hexStr = msg.payload.hex.replace(/\s/g, '');
|
|
669
|
+
if (/^[0-9A-Fa-f]+$/.test(hexStr)) {
|
|
670
|
+
frame = Buffer.from(hexStr, 'hex');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (frame && node.rs485Config && node.rs485Config.connected) {
|
|
674
|
+
node.rs485Config.send(frame);
|
|
675
|
+
node.log(`手动发送Modbus帧: ${frame.toString('hex')}`);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 支持通过消息触发同步
|
|
681
|
+
if (msg.payload.sync === 'mesh-to-rs485' && msg.payload.mac) {
|
|
682
|
+
const mapping = node.findMeshMapping(msg.payload.mac, msg.payload.channel || 0);
|
|
683
|
+
if (mapping) {
|
|
684
|
+
const registers = node.getRegistersForMapping(mapping);
|
|
685
|
+
if (registers) {
|
|
686
|
+
node.queueCommand({
|
|
687
|
+
direction: 'mesh-to-modbus',
|
|
688
|
+
mapping: mapping,
|
|
689
|
+
registers: registers,
|
|
690
|
+
state: msg.payload.state || {},
|
|
691
|
+
timestamp: Date.now()
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
566
698
|
// 清理
|
|
567
699
|
node.on('close', (done) => {
|
|
568
700
|
node.gateway.removeListener('device-list-complete', init);
|
|
@@ -595,13 +727,16 @@ module.exports = function(RED) {
|
|
|
595
727
|
channels = d.channels || d.switchState?.length || 1;
|
|
596
728
|
}
|
|
597
729
|
|
|
598
|
-
// 生成显示名称
|
|
730
|
+
// 生成显示名称 - 使用完整MAC地址(去除冒号)
|
|
731
|
+
const macClean = d.macAddress?.replace(/:/g, '') || '';
|
|
599
732
|
let displayName = d.name;
|
|
600
733
|
if (isSwitch && channels >= 1) {
|
|
601
|
-
//
|
|
734
|
+
// 开关设备:用按键数命名 + 完整MAC地址
|
|
602
735
|
const chName = channelNames[channels] || channels + '键';
|
|
603
|
-
|
|
604
|
-
|
|
736
|
+
displayName = chName + '开关_' + macClean;
|
|
737
|
+
} else {
|
|
738
|
+
// 非开关设备也显示完整MAC
|
|
739
|
+
displayName = (d.name || '设备') + '_' + macClean;
|
|
605
740
|
}
|
|
606
741
|
|
|
607
742
|
return {
|
package/nodes/symi-485-config.js
CHANGED