node-red-contrib-symi-mesh 1.6.0 → 1.6.2
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 +3 -3
- 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 +322 -80
- package/nodes/symi-485-config.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1032,7 +1032,7 @@ node-red-contrib-symi-mesh/
|
|
|
1032
1032
|
|
|
1033
1033
|
## 更新日志
|
|
1034
1034
|
|
|
1035
|
-
### v1.6.
|
|
1035
|
+
### v1.6.2 (2025-12-05)
|
|
1036
1036
|
- **MQTT订阅修复**:修复闭包问题导致的设备MAC映射错误,确保HA实体可控
|
|
1037
1037
|
- **内存泄漏修复**:节点关闭时正确移除gateway事件监听器,防止内存累积
|
|
1038
1038
|
- **三合一面板完善**:空调/新风/地暖控制和状态反馈全面优化
|
|
@@ -1051,8 +1051,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1051
1051
|
## 关于
|
|
1052
1052
|
|
|
1053
1053
|
**作者**: SYMI 亖米
|
|
1054
|
-
**版本**: 1.6.
|
|
1054
|
+
**版本**: 1.6.2
|
|
1055
1055
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1056
|
-
**最后更新**: 2025-12-
|
|
1056
|
+
**最后更新**: 2025-12-05
|
|
1057
1057
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1058
1058
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
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: '地暖',
|
|
@@ -214,32 +271,50 @@ module.exports = function(RED) {
|
|
|
214
271
|
node.lastSyncTime = 0;
|
|
215
272
|
node.pendingVerify = false;
|
|
216
273
|
|
|
274
|
+
// RS485连接信息
|
|
275
|
+
const rs485Info = node.rs485Config.connectionType === 'tcp'
|
|
276
|
+
? `${node.rs485Config.host}:${node.rs485Config.port}`
|
|
277
|
+
: node.rs485Config.serialPort;
|
|
278
|
+
|
|
217
279
|
if (node.mappings.length === 0) {
|
|
218
280
|
node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
|
|
219
281
|
} else {
|
|
220
|
-
node.status({ fill: 'yellow', shape: 'ring', text:
|
|
282
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `连接中 ${rs485Info}...` });
|
|
221
283
|
}
|
|
222
284
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
285
|
+
// 【重要】先绑定事件监听器,再注册到配置节点
|
|
286
|
+
// 定义事件处理函数(用于清理时移除)
|
|
287
|
+
const onRS485Connected = () => {
|
|
288
|
+
node.log(`[RS485 Bridge] 已连接到 ${rs485Info}`);
|
|
289
|
+
node.status({ fill: 'green', shape: 'dot', text: `已连接 ${rs485Info} (${node.mappings.length}个映射)` });
|
|
290
|
+
};
|
|
230
291
|
|
|
231
|
-
|
|
232
|
-
node.
|
|
233
|
-
|
|
292
|
+
const onRS485Disconnected = () => {
|
|
293
|
+
node.warn(`[RS485 Bridge] 已断开 ${rs485Info}`);
|
|
294
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `已断开 ${rs485Info}` });
|
|
295
|
+
};
|
|
234
296
|
|
|
235
|
-
|
|
236
|
-
node.
|
|
237
|
-
|
|
297
|
+
const onRS485Error = (err) => {
|
|
298
|
+
node.error(`[RS485 Bridge] 连接错误 ${rs485Info}: ${err.message}`);
|
|
299
|
+
node.status({ fill: 'red', shape: 'ring', text: `错误 ${rs485Info}` });
|
|
300
|
+
};
|
|
238
301
|
|
|
239
|
-
|
|
240
|
-
node.rs485Config.on('frame', (frame) => {
|
|
302
|
+
const onRS485Frame = (frame) => {
|
|
241
303
|
node.parseModbusResponse(frame);
|
|
242
|
-
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// 绑定事件监听器
|
|
307
|
+
node.rs485Config.on('connected', onRS485Connected);
|
|
308
|
+
node.rs485Config.on('disconnected', onRS485Disconnected);
|
|
309
|
+
node.rs485Config.on('error', onRS485Error);
|
|
310
|
+
node.rs485Config.on('frame', onRS485Frame);
|
|
311
|
+
|
|
312
|
+
// 保存处理函数引用,用于清理
|
|
313
|
+
node._rs485Handlers = { onRS485Connected, onRS485Disconnected, onRS485Error, onRS485Frame };
|
|
314
|
+
|
|
315
|
+
// 现在注册到RS485配置节点(这会触发连接)
|
|
316
|
+
node.rs485Config.register(node);
|
|
317
|
+
node.log(`[RS485 Bridge] 已注册到RS485配置: ${rs485Info}`);
|
|
243
318
|
|
|
244
319
|
// 查找Mesh设备的映射配置
|
|
245
320
|
node.findMeshMapping = function(mac, channel) {
|
|
@@ -369,23 +444,50 @@ module.exports = function(RED) {
|
|
|
369
444
|
// Mesh -> Modbus sync
|
|
370
445
|
node.syncMeshToModbus = async function(cmd) {
|
|
371
446
|
const { mapping, registers, state } = cmd;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
'targetTemp': 'targetTemp', 'acTargetTemp': 'targetTemp',
|
|
375
|
-
'acMode': 'mode', 'acFanSpeed': 'fanSpeed',
|
|
376
|
-
'brightness': 'brightness'
|
|
377
|
-
};
|
|
447
|
+
|
|
448
|
+
node.log(`[Mesh->RS485] 同步到从机${mapping.address}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
|
|
378
449
|
|
|
379
450
|
for (const [meshKey, value] of Object.entries(state)) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
451
|
+
try {
|
|
452
|
+
// 处理开关状态 - Mesh的switch字段对应RS485的switch1/switch2等
|
|
453
|
+
if (meshKey === 'switch' || meshKey === 'acSwitch') {
|
|
454
|
+
// 根据映射中的通道号选择对应的寄存器
|
|
455
|
+
const channel = mapping.meshChannel || 1;
|
|
456
|
+
const switchRegKey = `switch${channel}`;
|
|
457
|
+
const ledRegKey = `led${channel}`;
|
|
458
|
+
|
|
459
|
+
// 同步开关状态
|
|
460
|
+
if (registers[switchRegKey]) {
|
|
461
|
+
const writeValue = value ? (registers[switchRegKey].on || 1) : (registers[switchRegKey].off || 0);
|
|
462
|
+
await node.writeModbusRegister(mapping.address, registers[switchRegKey], writeValue);
|
|
463
|
+
node.log(`[Mesh->RS485] 开关${channel}: ${value ? '开' : '关'} (寄存器0x${registers[switchRegKey].address.toString(16)})`);
|
|
464
|
+
}
|
|
465
|
+
// 同时同步指示灯状态
|
|
466
|
+
if (registers[ledRegKey]) {
|
|
467
|
+
const writeValue = value ? (registers[ledRegKey].on || 1) : (registers[ledRegKey].off || 0);
|
|
468
|
+
await node.writeModbusRegister(mapping.address, registers[ledRegKey], writeValue);
|
|
469
|
+
node.debug(`[Mesh->RS485] 指示灯${channel}: ${value ? '开' : '关'}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 处理温控器属性
|
|
473
|
+
else if ((meshKey === 'targetTemp' || meshKey === 'acTargetTemp') && registers.targetTemp) {
|
|
474
|
+
await node.writeModbusRegister(mapping.address, registers.targetTemp, value);
|
|
475
|
+
node.debug(`[Mesh->RS485] 目标温度: ${value}`);
|
|
476
|
+
}
|
|
477
|
+
else if (meshKey === 'acMode' && registers.mode) {
|
|
478
|
+
await node.writeModbusRegister(mapping.address, registers.mode, value);
|
|
479
|
+
node.debug(`[Mesh->RS485] 模式: ${value}`);
|
|
480
|
+
}
|
|
481
|
+
else if (meshKey === 'acFanSpeed' && registers.fanSpeed) {
|
|
482
|
+
await node.writeModbusRegister(mapping.address, registers.fanSpeed, value);
|
|
483
|
+
node.debug(`[Mesh->RS485] 风速: ${value}`);
|
|
484
|
+
}
|
|
485
|
+
else if (meshKey === 'brightness' && registers.brightness) {
|
|
486
|
+
await node.writeModbusRegister(mapping.address, registers.brightness, value);
|
|
487
|
+
node.debug(`[Mesh->RS485] 亮度: ${value}`);
|
|
388
488
|
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
node.error(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
|
|
389
491
|
}
|
|
390
492
|
}
|
|
391
493
|
|
|
@@ -403,37 +505,54 @@ module.exports = function(RED) {
|
|
|
403
505
|
return;
|
|
404
506
|
}
|
|
405
507
|
|
|
406
|
-
|
|
407
|
-
'switch': { attrType: 0x02, param: (v) => Buffer.from([v ? 0x02 : 0x01]) },
|
|
408
|
-
'targetTemp': { attrType: 0x1C, param: (v) => Buffer.from([Math.round(v)]) },
|
|
409
|
-
'mode': {
|
|
410
|
-
attrType: 0x16,
|
|
411
|
-
param: (v) => {
|
|
412
|
-
const map = { 'cool': 0, 'heat': 1, 'fan': 2, 'dry': 3 };
|
|
413
|
-
return Buffer.from([map[v] !== undefined ? map[v] : 0]);
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
'fanSpeed': {
|
|
417
|
-
attrType: 0x1D,
|
|
418
|
-
param: (v) => {
|
|
419
|
-
const map = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
|
|
420
|
-
return Buffer.from([map[v] !== undefined ? map[v] : 4]);
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
'brightness': { attrType: 0x03, param: (v) => Buffer.from([Math.round(v)]) }
|
|
424
|
-
};
|
|
508
|
+
node.log(`[RS485->Mesh] 同步到设备 ${mapping.meshMac}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
|
|
425
509
|
|
|
426
510
|
for (const [key, value] of Object.entries(state)) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
511
|
+
try {
|
|
512
|
+
// 处理开关类型 (switch1, switch2, ... 或 led1, led2, ...)
|
|
513
|
+
if (key.startsWith('switch')) {
|
|
514
|
+
// 从键名提取通道号,如 switch1 -> 1, switch2 -> 2
|
|
515
|
+
const channelFromKey = parseInt(key.replace('switch', '')) || 1;
|
|
516
|
+
// 使用映射中配置的通道,或从键名获取
|
|
517
|
+
const channel = mapping.meshChannel || channelFromKey;
|
|
518
|
+
|
|
519
|
+
// Mesh开关控制:attrType=0x02, param=[通道, 开/关]
|
|
520
|
+
const onOff = value ? 0x02 : 0x01; // 0x02=开, 0x01=关
|
|
521
|
+
const param = Buffer.from([channel - 1, onOff]); // 通道从0开始
|
|
522
|
+
|
|
523
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
524
|
+
node.log(`[RS485->Mesh] 开关${channel}: ${value ? '开' : '关'}`);
|
|
436
525
|
}
|
|
526
|
+
// 处理指示灯(可选,某些场景需要同步指示灯状态)
|
|
527
|
+
else if (key.startsWith('led')) {
|
|
528
|
+
// 指示灯状态通常不需要同步回Mesh,仅记录
|
|
529
|
+
node.debug(`[RS485] 指示灯${key}: ${value}`);
|
|
530
|
+
}
|
|
531
|
+
// 处理温控器属性
|
|
532
|
+
else if (key === 'targetTemp') {
|
|
533
|
+
const param = Buffer.from([Math.round(value)]);
|
|
534
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
|
|
535
|
+
node.debug(`[RS485->Mesh] 目标温度: ${value}`);
|
|
536
|
+
}
|
|
537
|
+
else if (key === 'mode') {
|
|
538
|
+
const modeMap = { 'cool': 0, 'heat': 1, 'fan': 2, 'dry': 3 };
|
|
539
|
+
const param = Buffer.from([modeMap[value] !== undefined ? modeMap[value] : 0]);
|
|
540
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x16, param);
|
|
541
|
+
node.debug(`[RS485->Mesh] 模式: ${value}`);
|
|
542
|
+
}
|
|
543
|
+
else if (key === 'fanSpeed') {
|
|
544
|
+
const speedMap = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
|
|
545
|
+
const param = Buffer.from([speedMap[value] !== undefined ? speedMap[value] : 4]);
|
|
546
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1D, param);
|
|
547
|
+
node.debug(`[RS485->Mesh] 风速: ${value}`);
|
|
548
|
+
}
|
|
549
|
+
else if (key === 'brightness') {
|
|
550
|
+
const param = Buffer.from([Math.round(value)]);
|
|
551
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x03, param);
|
|
552
|
+
node.debug(`[RS485->Mesh] 亮度: ${value}`);
|
|
553
|
+
}
|
|
554
|
+
} catch (err) {
|
|
555
|
+
node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
437
556
|
}
|
|
438
557
|
}
|
|
439
558
|
|
|
@@ -502,26 +621,55 @@ module.exports = function(RED) {
|
|
|
502
621
|
node.debug(`发送RS485帧: ${frame.toString('hex')}`);
|
|
503
622
|
};
|
|
504
623
|
|
|
505
|
-
// 解析Modbus
|
|
624
|
+
// 解析Modbus响应/上报帧
|
|
506
625
|
node.parseModbusResponse = function(frame) {
|
|
626
|
+
if (frame.length < 6) return;
|
|
627
|
+
|
|
507
628
|
const slaveAddr = frame[0];
|
|
508
629
|
const fc = frame[1];
|
|
509
630
|
|
|
510
631
|
// 查找对应的映射
|
|
511
632
|
const mapping = node.findRS485Mapping(slaveAddr);
|
|
512
|
-
if (!mapping)
|
|
633
|
+
if (!mapping) {
|
|
634
|
+
node.debug(`未找到从机${slaveAddr}的映射配置`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
513
637
|
|
|
514
638
|
const registers = node.getRegistersForMapping(mapping);
|
|
515
|
-
if (!registers)
|
|
639
|
+
if (!registers) {
|
|
640
|
+
node.debug(`未找到设备${mapping.device}的寄存器定义`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
516
643
|
|
|
517
644
|
// 根据功能码解析数据
|
|
518
645
|
let state = {};
|
|
519
|
-
|
|
646
|
+
|
|
647
|
+
if (fc === 0x06 || fc === 0x10) {
|
|
648
|
+
// 写单寄存器响应/写多寄存器响应 - 这是设备主动上报或写响应
|
|
649
|
+
// 格式: 从机地址 + 功能码 + 寄存器地址(2字节) + 值(2字节) + CRC
|
|
650
|
+
const regAddr = frame.readUInt16BE(2);
|
|
651
|
+
const value = frame.readUInt16BE(4);
|
|
652
|
+
|
|
653
|
+
// 查找匹配的寄存器定义
|
|
654
|
+
for (const [key, reg] of Object.entries(registers)) {
|
|
655
|
+
if (reg.address === regAddr) {
|
|
656
|
+
// 处理开关类型
|
|
657
|
+
if (key.startsWith('switch') || key.startsWith('led')) {
|
|
658
|
+
state[key] = value === (reg.on || 1);
|
|
659
|
+
} else if (reg.map) {
|
|
660
|
+
state[key] = reg.map[value] || value;
|
|
661
|
+
} else {
|
|
662
|
+
state[key] = value;
|
|
663
|
+
}
|
|
664
|
+
node.log(`[RS485] 从机${slaveAddr} 寄存器0x${regAddr.toString(16).toUpperCase()}: ${key}=${value}`);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} else if (fc === 0x03 || fc === 0x04) {
|
|
520
669
|
// 读寄存器响应
|
|
521
670
|
const byteCount = frame[2];
|
|
522
671
|
for (let i = 0; i < byteCount / 2; i++) {
|
|
523
672
|
const value = frame.readUInt16BE(3 + i * 2);
|
|
524
|
-
// 根据寄存器映射解析
|
|
525
673
|
for (const [key, reg] of Object.entries(registers)) {
|
|
526
674
|
if (reg.map) {
|
|
527
675
|
state[key] = reg.map[value] || value;
|
|
@@ -530,10 +678,19 @@ module.exports = function(RED) {
|
|
|
530
678
|
}
|
|
531
679
|
}
|
|
532
680
|
}
|
|
681
|
+
} else if (fc === 0x20) {
|
|
682
|
+
// 自定义功能码0x20 - 可能是批量上报
|
|
683
|
+
// 格式: 从机地址 + 0x20 + 起始寄存器(2字节) + 数量(2字节) + 数据... + CRC
|
|
684
|
+
if (frame.length >= 9) {
|
|
685
|
+
const startReg = frame.readUInt16BE(2);
|
|
686
|
+
const count = frame.readUInt16BE(4);
|
|
687
|
+
node.debug(`[RS485] 从机${slaveAddr} 自定义上报: 起始0x${startReg.toString(16)}, 数量${count}`);
|
|
688
|
+
// 暂不处理,记录日志供分析
|
|
689
|
+
}
|
|
533
690
|
}
|
|
534
691
|
|
|
535
692
|
if (Object.keys(state).length > 0) {
|
|
536
|
-
node.log(`[RS485->Mesh] 设备@${slaveAddr}
|
|
693
|
+
node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态变化: ${JSON.stringify(state)}`);
|
|
537
694
|
node.queueCommand({
|
|
538
695
|
direction: 'modbus-to-mesh',
|
|
539
696
|
mapping: mapping,
|
|
@@ -563,15 +720,97 @@ module.exports = function(RED) {
|
|
|
563
720
|
init();
|
|
564
721
|
}
|
|
565
722
|
|
|
723
|
+
// 输出调试信息
|
|
724
|
+
node.outputDebug = function(direction, info) {
|
|
725
|
+
const msg = {
|
|
726
|
+
topic: 'rs485-bridge/' + direction,
|
|
727
|
+
payload: info,
|
|
728
|
+
timestamp: new Date().toISOString()
|
|
729
|
+
};
|
|
730
|
+
node.send(msg);
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// 监听RS485帧事件并输出
|
|
734
|
+
if (node.rs485Config) {
|
|
735
|
+
node.rs485Config.on('frame', (frame) => {
|
|
736
|
+
const slaveAddr = frame[0];
|
|
737
|
+
const fc = frame[1];
|
|
738
|
+
const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
739
|
+
node.outputDebug('rx', {
|
|
740
|
+
direction: 'RX',
|
|
741
|
+
slaveAddr: slaveAddr,
|
|
742
|
+
funcCode: fc,
|
|
743
|
+
hex: hexData,
|
|
744
|
+
raw: frame
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
node.rs485Config.on('tx', (frame) => {
|
|
749
|
+
const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
750
|
+
node.outputDebug('tx', {
|
|
751
|
+
direction: 'TX',
|
|
752
|
+
hex: hexData,
|
|
753
|
+
raw: frame
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 处理输入消息(手动控制)
|
|
759
|
+
node.on('input', function(msg) {
|
|
760
|
+
if (!msg.payload) return;
|
|
761
|
+
|
|
762
|
+
// 支持直接发送Modbus帧
|
|
763
|
+
if (msg.payload.modbusFrame || msg.payload.hex) {
|
|
764
|
+
let frame;
|
|
765
|
+
if (Buffer.isBuffer(msg.payload.modbusFrame)) {
|
|
766
|
+
frame = msg.payload.modbusFrame;
|
|
767
|
+
} else if (typeof msg.payload.hex === 'string') {
|
|
768
|
+
const hexStr = msg.payload.hex.replace(/\s/g, '');
|
|
769
|
+
if (/^[0-9A-Fa-f]+$/.test(hexStr)) {
|
|
770
|
+
frame = Buffer.from(hexStr, 'hex');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (frame && node.rs485Config && node.rs485Config.connected) {
|
|
774
|
+
node.rs485Config.send(frame);
|
|
775
|
+
node.log(`手动发送Modbus帧: ${frame.toString('hex')}`);
|
|
776
|
+
}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 支持通过消息触发同步
|
|
781
|
+
if (msg.payload.sync === 'mesh-to-rs485' && msg.payload.mac) {
|
|
782
|
+
const mapping = node.findMeshMapping(msg.payload.mac, msg.payload.channel || 0);
|
|
783
|
+
if (mapping) {
|
|
784
|
+
const registers = node.getRegistersForMapping(mapping);
|
|
785
|
+
if (registers) {
|
|
786
|
+
node.queueCommand({
|
|
787
|
+
direction: 'mesh-to-modbus',
|
|
788
|
+
mapping: mapping,
|
|
789
|
+
registers: registers,
|
|
790
|
+
state: msg.payload.state || {},
|
|
791
|
+
timestamp: Date.now()
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
566
798
|
// 清理
|
|
567
799
|
node.on('close', (done) => {
|
|
800
|
+
// 移除Mesh网关事件监听器
|
|
568
801
|
node.gateway.removeListener('device-list-complete', init);
|
|
569
802
|
node.gateway.removeListener('device-state-changed', handleMeshStateChange);
|
|
570
803
|
|
|
571
|
-
//
|
|
572
|
-
if (node.rs485Config) {
|
|
804
|
+
// 移除RS485配置节点事件监听器
|
|
805
|
+
if (node.rs485Config && node._rs485Handlers) {
|
|
806
|
+
node.rs485Config.removeListener('connected', node._rs485Handlers.onRS485Connected);
|
|
807
|
+
node.rs485Config.removeListener('disconnected', node._rs485Handlers.onRS485Disconnected);
|
|
808
|
+
node.rs485Config.removeListener('error', node._rs485Handlers.onRS485Error);
|
|
809
|
+
node.rs485Config.removeListener('frame', node._rs485Handlers.onRS485Frame);
|
|
573
810
|
node.rs485Config.deregister(node);
|
|
574
811
|
}
|
|
812
|
+
|
|
813
|
+
node.log('[RS485 Bridge] 节点已清理');
|
|
575
814
|
done();
|
|
576
815
|
});
|
|
577
816
|
}
|
|
@@ -595,13 +834,16 @@ module.exports = function(RED) {
|
|
|
595
834
|
channels = d.channels || d.switchState?.length || 1;
|
|
596
835
|
}
|
|
597
836
|
|
|
598
|
-
// 生成显示名称
|
|
837
|
+
// 生成显示名称 - 使用完整MAC地址(去除冒号)
|
|
838
|
+
const macClean = d.macAddress?.replace(/:/g, '') || '';
|
|
599
839
|
let displayName = d.name;
|
|
600
840
|
if (isSwitch && channels >= 1) {
|
|
601
|
-
//
|
|
841
|
+
// 开关设备:用按键数命名 + 完整MAC地址
|
|
602
842
|
const chName = channelNames[channels] || channels + '键';
|
|
603
|
-
|
|
604
|
-
|
|
843
|
+
displayName = chName + '开关_' + macClean;
|
|
844
|
+
} else {
|
|
845
|
+
// 非开关设备也显示完整MAC
|
|
846
|
+
displayName = (d.name || '设备') + '_' + macClean;
|
|
605
847
|
}
|
|
606
848
|
|
|
607
849
|
return {
|
package/nodes/symi-485-config.js
CHANGED