node-red-contrib-symi-mesh 1.6.1 → 1.6.3
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 +34 -5
- package/nodes/symi-485-bridge.html +91 -4
- package/nodes/symi-485-bridge.js +477 -87
- package/nodes/symi-485-config.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -539,9 +539,38 @@ Mesh 四键开关 第2路 ↔ RS485 六键开关 第5路 地址:2 ✓ 支持
|
|
|
539
539
|
|
|
540
540
|
| 品牌 | 支持设备 |
|
|
541
541
|
|-----|---------|
|
|
542
|
-
| 话语前湾 |
|
|
542
|
+
| 话语前湾 | 空调(客厅/主卧/次卧1/次卧2)、地暖、新风、窗帘、1-8键开关 |
|
|
543
|
+
| 通用Modbus | 各类标准Modbus设备 |
|
|
544
|
+
| 自定义协议 | 任意485码匹配(开关/窗帘/场景) |
|
|
543
545
|
|
|
544
|
-
|
|
546
|
+
### 自定义协议模式
|
|
547
|
+
|
|
548
|
+
当内置协议无法满足需求时,可使用"自定义协议"模式,手动录入RS485十六进制码进行双向匹配:
|
|
549
|
+
|
|
550
|
+
#### 自定义开关
|
|
551
|
+
- **打开码**:当Mesh开关打开时发送此码,收到此码时触发Mesh开关打开
|
|
552
|
+
- **关闭码**:当Mesh开关关闭时发送此码,收到此码时触发Mesh开关关闭
|
|
553
|
+
|
|
554
|
+
#### 自定义窗帘
|
|
555
|
+
- **打开码**:Mesh窗帘打开 ↔ RS485打开命令
|
|
556
|
+
- **关闭码**:Mesh窗帘关闭 ↔ RS485关闭命令
|
|
557
|
+
- **停止码**:Mesh窗帘停止 ↔ RS485停止命令
|
|
558
|
+
|
|
559
|
+
#### 配置示例
|
|
560
|
+
```
|
|
561
|
+
品牌: 自定义协议
|
|
562
|
+
类型: 自定义窗帘
|
|
563
|
+
打开码: 55 02 02 03 01 49 44
|
|
564
|
+
关闭码: 55 02 02 03 02 09 45
|
|
565
|
+
停止码: 55 02 02 03 03 C8 85
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
#### 工作原理
|
|
569
|
+
1. **Mesh→RS485**:Mesh设备状态变化时,发送对应的自定义码
|
|
570
|
+
2. **RS485→Mesh**:收到的RS485帧与自定义码匹配,触发对应Mesh设备
|
|
571
|
+
3. **防死循环**:500ms内不重复触发,避免Mesh→RS485→Mesh循环
|
|
572
|
+
|
|
573
|
+
> 自定义码支持任意长度的十六进制字符串,支持空格分隔(如 `55 01 01` 或 `550101`)
|
|
545
574
|
|
|
546
575
|
### 注意事项
|
|
547
576
|
|
|
@@ -1032,7 +1061,7 @@ node-red-contrib-symi-mesh/
|
|
|
1032
1061
|
|
|
1033
1062
|
## 更新日志
|
|
1034
1063
|
|
|
1035
|
-
### v1.6.
|
|
1064
|
+
### v1.6.2 (2025-12-05)
|
|
1036
1065
|
- **MQTT订阅修复**:修复闭包问题导致的设备MAC映射错误,确保HA实体可控
|
|
1037
1066
|
- **内存泄漏修复**:节点关闭时正确移除gateway事件监听器,防止内存累积
|
|
1038
1067
|
- **三合一面板完善**:空调/新风/地暖控制和状态反馈全面优化
|
|
@@ -1051,8 +1080,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1051
1080
|
## 关于
|
|
1052
1081
|
|
|
1053
1082
|
**作者**: SYMI 亖米
|
|
1054
|
-
**版本**: 1.6.
|
|
1083
|
+
**版本**: 1.6.2
|
|
1055
1084
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1056
|
-
**最后更新**: 2025-12-
|
|
1085
|
+
**最后更新**: 2025-12-05
|
|
1057
1086
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1058
1087
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
mappings.forEach(function(m, idx) {
|
|
162
|
-
var row = $('<div class="mapping-row" data-idx="' + idx + '"></div>');
|
|
162
|
+
var row = $('<div class="mapping-row" data-idx="' + idx + '" style="flex-wrap:wrap;"></div>');
|
|
163
163
|
row.html(
|
|
164
164
|
'<div class="mesh-col">' +
|
|
165
165
|
' <select class="mesh-select">' + getMeshOptions(m.meshMac) + '</select>' +
|
|
@@ -179,6 +179,10 @@
|
|
|
179
179
|
'<div class="del-col"><button type="button" class="red-ui-button red-ui-button-small btn-remove" title="删除"><i class="fa fa-times"></i></button></div>'
|
|
180
180
|
);
|
|
181
181
|
container.append(row);
|
|
182
|
+
// 如果是自定义协议,显示自定义码输入框
|
|
183
|
+
if (m.brand === 'custom' && m.device) {
|
|
184
|
+
updateCustomCodesVisibility(row, m.brand, m.device, idx);
|
|
185
|
+
}
|
|
182
186
|
});
|
|
183
187
|
|
|
184
188
|
bindEvents();
|
|
@@ -194,9 +198,11 @@
|
|
|
194
198
|
var brandId = $(this).val();
|
|
195
199
|
row.find('.device-select').html(getDeviceOptions(brandId, ''));
|
|
196
200
|
row.find('.rs485-ch-wrap').empty();
|
|
201
|
+
row.find('.custom-codes-row').remove(); // 移除自定义码区域
|
|
197
202
|
mappings[idx].brand = brandId;
|
|
198
203
|
mappings[idx].device = '';
|
|
199
204
|
mappings[idx].rs485Channel = 1;
|
|
205
|
+
mappings[idx].customCodes = {};
|
|
200
206
|
});
|
|
201
207
|
|
|
202
208
|
container.find('.device-select').off('change').on('change', function() {
|
|
@@ -208,6 +214,8 @@
|
|
|
208
214
|
// 更新RS485按键选择
|
|
209
215
|
row.find('.rs485-ch-wrap').html(getRS485ChannelOptions(brandId, deviceId, 1));
|
|
210
216
|
mappings[idx].rs485Channel = 1;
|
|
217
|
+
// 显示/隐藏自定义码输入区域
|
|
218
|
+
updateCustomCodesVisibility(row, brandId, deviceId, idx);
|
|
211
219
|
bindEvents(); // 重新绑定新添加的元素事件
|
|
212
220
|
});
|
|
213
221
|
|
|
@@ -242,11 +250,73 @@
|
|
|
242
250
|
mappings.splice(idx, 1);
|
|
243
251
|
renderMappings();
|
|
244
252
|
});
|
|
253
|
+
|
|
254
|
+
// 自定义码输入事件
|
|
255
|
+
container.find('.custom-code-on').off('change').on('change', function() {
|
|
256
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
257
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
258
|
+
mappings[idx].customCodes.on = $(this).val();
|
|
259
|
+
});
|
|
260
|
+
container.find('.custom-code-off').off('change').on('change', function() {
|
|
261
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
262
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
263
|
+
mappings[idx].customCodes.off = $(this).val();
|
|
264
|
+
});
|
|
265
|
+
container.find('.custom-code-open').off('change').on('change', function() {
|
|
266
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
267
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
268
|
+
mappings[idx].customCodes.open = $(this).val();
|
|
269
|
+
});
|
|
270
|
+
container.find('.custom-code-close').off('change').on('change', function() {
|
|
271
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
272
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
273
|
+
mappings[idx].customCodes.close = $(this).val();
|
|
274
|
+
});
|
|
275
|
+
container.find('.custom-code-stop').off('change').on('change', function() {
|
|
276
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
277
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
278
|
+
mappings[idx].customCodes.stop = $(this).val();
|
|
279
|
+
});
|
|
280
|
+
container.find('.custom-code-trigger').off('change').on('change', function() {
|
|
281
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
282
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
283
|
+
mappings[idx].customCodes.trigger = $(this).val();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 更新自定义码输入区域的显示
|
|
288
|
+
function updateCustomCodesVisibility(row, brandId, deviceId, idx) {
|
|
289
|
+
row.find('.custom-codes-row').remove();
|
|
290
|
+
if (brandId !== 'custom') return;
|
|
291
|
+
|
|
292
|
+
var codes = mappings[idx].customCodes || {};
|
|
293
|
+
var html = '<div class="custom-codes-row" style="width:100%;padding:8px;margin-top:6px;background:#fff8e1;border:1px dashed #ffa726;border-radius:4px;">';
|
|
294
|
+
html += '<div style="font-size:11px;color:#f57c00;margin-bottom:6px;"><i class="fa fa-code"></i> 自定义RS485码(十六进制,如: 55 01 01 03 01 B9 00)</div>';
|
|
295
|
+
|
|
296
|
+
if (deviceId === 'custom_switch') {
|
|
297
|
+
html += '<div style="display:flex;gap:8px;flex-wrap:wrap;">';
|
|
298
|
+
html += '<label style="font-size:11px;">打开码: <input type="text" class="custom-code-on" value="' + (codes.on || '') + '" placeholder="55 01 01..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
299
|
+
html += '<label style="font-size:11px;">关闭码: <input type="text" class="custom-code-off" value="' + (codes.off || '') + '" placeholder="55 01 02..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
300
|
+
html += '</div>';
|
|
301
|
+
} else if (deviceId === 'custom_curtain') {
|
|
302
|
+
html += '<div style="display:flex;gap:8px;flex-wrap:wrap;">';
|
|
303
|
+
html += '<label style="font-size:11px;">打开码: <input type="text" class="custom-code-open" value="' + (codes.open || '') + '" placeholder="55 02 02 03 01..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
304
|
+
html += '<label style="font-size:11px;">关闭码: <input type="text" class="custom-code-close" value="' + (codes.close || '') + '" placeholder="55 02 02 03 02..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
305
|
+
html += '<label style="font-size:11px;">停止码: <input type="text" class="custom-code-stop" value="' + (codes.stop || '') + '" placeholder="55 02 02 03 03..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
306
|
+
html += '</div>';
|
|
307
|
+
} else if (deviceId === 'custom_scene') {
|
|
308
|
+
html += '<div style="display:flex;gap:8px;">';
|
|
309
|
+
html += '<label style="font-size:11px;">触发码: <input type="text" class="custom-code-trigger" value="' + (codes.trigger || '') + '" placeholder="55 01 01..." style="width:200px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
|
|
310
|
+
html += '</div>';
|
|
311
|
+
}
|
|
312
|
+
html += '</div>';
|
|
313
|
+
row.append(html);
|
|
314
|
+
bindEvents();
|
|
245
315
|
}
|
|
246
316
|
|
|
247
317
|
// 添加新映射
|
|
248
318
|
$('#btn-add-mapping').on('click', function() {
|
|
249
|
-
mappings.push({ meshMac: '', meshChannel: 0, brand: '', device: '', address: 1, rs485Channel: 1 });
|
|
319
|
+
mappings.push({ meshMac: '', meshChannel: 0, brand: '', device: '', address: 1, rs485Channel: 1, customCodes: {} });
|
|
250
320
|
renderMappings();
|
|
251
321
|
});
|
|
252
322
|
|
|
@@ -256,14 +326,31 @@
|
|
|
256
326
|
oneditsave: function() {
|
|
257
327
|
var mappings = [];
|
|
258
328
|
$('#mapping-list .mapping-row').each(function() {
|
|
259
|
-
|
|
329
|
+
var m = {
|
|
260
330
|
meshMac: $(this).find('.mesh-select').val() || '',
|
|
261
331
|
meshChannel: parseInt($(this).find('.mesh-channel').val()) || 1,
|
|
262
332
|
brand: $(this).find('.brand-select').val() || '',
|
|
263
333
|
device: $(this).find('.device-select').val() || '',
|
|
264
334
|
address: parseInt($(this).find('.addr-input').val()) || 1,
|
|
265
335
|
rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1
|
|
266
|
-
}
|
|
336
|
+
};
|
|
337
|
+
// 保存自定义码
|
|
338
|
+
if (m.brand === 'custom') {
|
|
339
|
+
m.customCodes = {};
|
|
340
|
+
var on = $(this).find('.custom-code-on').val();
|
|
341
|
+
var off = $(this).find('.custom-code-off').val();
|
|
342
|
+
var open = $(this).find('.custom-code-open').val();
|
|
343
|
+
var close = $(this).find('.custom-code-close').val();
|
|
344
|
+
var stop = $(this).find('.custom-code-stop').val();
|
|
345
|
+
var trigger = $(this).find('.custom-code-trigger').val();
|
|
346
|
+
if (on) m.customCodes.on = on;
|
|
347
|
+
if (off) m.customCodes.off = off;
|
|
348
|
+
if (open) m.customCodes.open = open;
|
|
349
|
+
if (close) m.customCodes.close = close;
|
|
350
|
+
if (stop) m.customCodes.stop = stop;
|
|
351
|
+
if (trigger) m.customCodes.trigger = trigger;
|
|
352
|
+
}
|
|
353
|
+
mappings.push(m);
|
|
267
354
|
});
|
|
268
355
|
this.mappings = JSON.stringify(mappings);
|
|
269
356
|
}
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -234,6 +234,30 @@ module.exports = function(RED) {
|
|
|
234
234
|
'fresh_air': { name: '新风', type: 'fan', registers: { switch: { address: 0x0000, type: 'coil' }, fanSpeed: { address: 0x0001, type: 'holding' } } },
|
|
235
235
|
'scene': { name: '场景', type: 'scene', registers: { trigger: { address: 0x0000, type: 'holding' } } }
|
|
236
236
|
}
|
|
237
|
+
},
|
|
238
|
+
// ===== 自定义协议 - 用户可录入任意RS485码 =====
|
|
239
|
+
'custom': {
|
|
240
|
+
name: '自定义协议',
|
|
241
|
+
devices: {
|
|
242
|
+
'custom_switch': {
|
|
243
|
+
name: '自定义开关',
|
|
244
|
+
type: 'switch',
|
|
245
|
+
customMode: true, // 标记为自定义模式
|
|
246
|
+
// 用户需要在映射中配置: customCodes.on, customCodes.off
|
|
247
|
+
},
|
|
248
|
+
'custom_curtain': {
|
|
249
|
+
name: '自定义窗帘',
|
|
250
|
+
type: 'cover',
|
|
251
|
+
customMode: true,
|
|
252
|
+
// 用户需要在映射中配置: customCodes.open, customCodes.close, customCodes.stop
|
|
253
|
+
},
|
|
254
|
+
'custom_scene': {
|
|
255
|
+
name: '自定义场景',
|
|
256
|
+
type: 'scene',
|
|
257
|
+
customMode: true,
|
|
258
|
+
// 用户需要在映射中配置: customCodes.trigger
|
|
259
|
+
}
|
|
260
|
+
}
|
|
237
261
|
}
|
|
238
262
|
}
|
|
239
263
|
};
|
|
@@ -271,32 +295,50 @@ module.exports = function(RED) {
|
|
|
271
295
|
node.lastSyncTime = 0;
|
|
272
296
|
node.pendingVerify = false;
|
|
273
297
|
|
|
298
|
+
// RS485连接信息
|
|
299
|
+
const rs485Info = node.rs485Config.connectionType === 'tcp'
|
|
300
|
+
? `${node.rs485Config.host}:${node.rs485Config.port}`
|
|
301
|
+
: node.rs485Config.serialPort;
|
|
302
|
+
|
|
274
303
|
if (node.mappings.length === 0) {
|
|
275
304
|
node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
|
|
276
305
|
} else {
|
|
277
|
-
node.status({ fill: 'yellow', shape: 'ring', text:
|
|
306
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `连接中 ${rs485Info}...` });
|
|
278
307
|
}
|
|
279
308
|
|
|
280
|
-
//
|
|
281
|
-
|
|
309
|
+
// 【重要】先绑定事件监听器,再注册到配置节点
|
|
310
|
+
// 定义事件处理函数(用于清理时移除)
|
|
311
|
+
const onRS485Connected = () => {
|
|
312
|
+
node.log(`[RS485 Bridge] 已连接到 ${rs485Info}`);
|
|
313
|
+
node.status({ fill: 'green', shape: 'dot', text: `已连接 ${rs485Info} (${node.mappings.length}个映射)` });
|
|
314
|
+
};
|
|
282
315
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
node.status({ fill: '
|
|
286
|
-
}
|
|
316
|
+
const onRS485Disconnected = () => {
|
|
317
|
+
node.warn(`[RS485 Bridge] 已断开 ${rs485Info}`);
|
|
318
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `已断开 ${rs485Info}` });
|
|
319
|
+
};
|
|
287
320
|
|
|
288
|
-
|
|
289
|
-
node.
|
|
290
|
-
|
|
321
|
+
const onRS485Error = (err) => {
|
|
322
|
+
node.error(`[RS485 Bridge] 连接错误 ${rs485Info}: ${err.message}`);
|
|
323
|
+
node.status({ fill: 'red', shape: 'ring', text: `错误 ${rs485Info}` });
|
|
324
|
+
};
|
|
291
325
|
|
|
292
|
-
|
|
293
|
-
node.
|
|
294
|
-
}
|
|
326
|
+
const onRS485Frame = (frame) => {
|
|
327
|
+
node.parseRS485Frame(frame); // 使用新的解析函数,支持自定义码
|
|
328
|
+
};
|
|
295
329
|
|
|
296
|
-
//
|
|
297
|
-
node.rs485Config.on('
|
|
298
|
-
|
|
299
|
-
|
|
330
|
+
// 绑定事件监听器
|
|
331
|
+
node.rs485Config.on('connected', onRS485Connected);
|
|
332
|
+
node.rs485Config.on('disconnected', onRS485Disconnected);
|
|
333
|
+
node.rs485Config.on('error', onRS485Error);
|
|
334
|
+
node.rs485Config.on('frame', onRS485Frame);
|
|
335
|
+
|
|
336
|
+
// 保存处理函数引用,用于清理
|
|
337
|
+
node._rs485Handlers = { onRS485Connected, onRS485Disconnected, onRS485Error, onRS485Frame };
|
|
338
|
+
|
|
339
|
+
// 现在注册到RS485配置节点(这会触发连接)
|
|
340
|
+
node.rs485Config.register(node);
|
|
341
|
+
node.log(`[RS485 Bridge] 已注册到RS485配置: ${rs485Info}`);
|
|
300
342
|
|
|
301
343
|
// 查找Mesh设备的映射配置
|
|
302
344
|
node.findMeshMapping = function(mac, channel) {
|
|
@@ -324,23 +366,52 @@ module.exports = function(RED) {
|
|
|
324
366
|
|
|
325
367
|
const mac = eventData.device.macAddress;
|
|
326
368
|
const state = eventData.state || {};
|
|
327
|
-
|
|
369
|
+
// channel可能是0-based或1-based,需要兼容处理
|
|
370
|
+
// Mesh事件中channel通常是0-based,UI配置的meshChannel是1-based
|
|
371
|
+
const eventChannel = state.channel !== undefined ? state.channel : -1;
|
|
328
372
|
|
|
329
|
-
|
|
330
|
-
const mapping = node.findMeshMapping(mac, channel);
|
|
331
|
-
if (!mapping) return;
|
|
373
|
+
node.debug(`[Mesh事件] MAC=${mac}, channel=${eventChannel}, state=${JSON.stringify(state)}`);
|
|
332
374
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
mapping
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
375
|
+
// 查找匹配的映射 - 遍历所有映射找到MAC匹配的
|
|
376
|
+
for (const mapping of node.mappings) {
|
|
377
|
+
if (mapping.meshMac !== mac) continue;
|
|
378
|
+
|
|
379
|
+
// 通道匹配:meshChannel=0表示匹配所有,否则需要匹配具体通道
|
|
380
|
+
// UI的meshChannel是1-based,事件的channel可能是0-based
|
|
381
|
+
const configChannel = mapping.meshChannel || 1;
|
|
382
|
+
const matchChannel = (eventChannel === -1) || // 无channel信息,匹配所有
|
|
383
|
+
(eventChannel === configChannel) || // 1-based匹配
|
|
384
|
+
(eventChannel === configChannel - 1); // 0-based匹配
|
|
385
|
+
|
|
386
|
+
if (!matchChannel) continue;
|
|
387
|
+
|
|
388
|
+
const registers = node.getRegistersForMapping(mapping);
|
|
389
|
+
|
|
390
|
+
node.log(`[Mesh->RS485] ${eventData.device.name} 通道${configChannel} 状态变化: ${JSON.stringify(state)}`);
|
|
391
|
+
|
|
392
|
+
// 输出调试信息到节点输出端口
|
|
393
|
+
node.send({
|
|
394
|
+
topic: 'mesh-state-change',
|
|
395
|
+
payload: {
|
|
396
|
+
direction: 'Mesh→RS485',
|
|
397
|
+
device: eventData.device.name,
|
|
398
|
+
mac: mac,
|
|
399
|
+
channel: configChannel,
|
|
400
|
+
state: state
|
|
401
|
+
},
|
|
402
|
+
timestamp: new Date().toISOString()
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
node.queueCommand({
|
|
406
|
+
direction: 'mesh-to-modbus',
|
|
407
|
+
mapping: mapping,
|
|
408
|
+
registers: registers,
|
|
409
|
+
state: state,
|
|
410
|
+
timestamp: Date.now()
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
break; // 找到匹配的映射就停止
|
|
414
|
+
}
|
|
344
415
|
};
|
|
345
416
|
|
|
346
417
|
// RS485 device state change handler (event-driven)
|
|
@@ -423,35 +494,148 @@ module.exports = function(RED) {
|
|
|
423
494
|
// 这是安全机制,不是轮询
|
|
424
495
|
};
|
|
425
496
|
|
|
426
|
-
// Mesh -> Modbus sync
|
|
497
|
+
// Mesh -> Modbus/Custom sync
|
|
427
498
|
node.syncMeshToModbus = async function(cmd) {
|
|
428
499
|
const { mapping, registers, state } = cmd;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
500
|
+
|
|
501
|
+
// 记录发送时间(用于防死循环)
|
|
502
|
+
node.lastMeshToRS485Time = Date.now();
|
|
503
|
+
|
|
504
|
+
// ===== 自定义协议模式 =====
|
|
505
|
+
if (mapping.brand === 'custom' && mapping.customCodes) {
|
|
506
|
+
const codes = mapping.customCodes;
|
|
507
|
+
|
|
508
|
+
node.log(`[Mesh->自定义] 设备${mapping.device}, 状态: ${JSON.stringify(state)}`);
|
|
509
|
+
|
|
510
|
+
// 开关类型
|
|
511
|
+
if (mapping.device === 'custom_switch') {
|
|
512
|
+
for (const [key, value] of Object.entries(state)) {
|
|
513
|
+
if (key === 'switch' || key === 'acSwitch') {
|
|
514
|
+
const hexCode = value ? codes.on : codes.off;
|
|
515
|
+
if (hexCode) {
|
|
516
|
+
await node.sendCustomCode(hexCode);
|
|
517
|
+
node.log(`[Mesh->自定义] 开关: ${value ? '开' : '关'}, 发送: ${hexCode}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// 窗帘类型
|
|
523
|
+
else if (mapping.device === 'custom_curtain') {
|
|
524
|
+
for (const [key, value] of Object.entries(state)) {
|
|
525
|
+
if (key === 'position' || key === 'action') {
|
|
526
|
+
let hexCode = null;
|
|
527
|
+
if (value === 'open' || value === 100) hexCode = codes.open;
|
|
528
|
+
else if (value === 'close' || value === 0) hexCode = codes.close;
|
|
529
|
+
else if (value === 'stop') hexCode = codes.stop;
|
|
530
|
+
|
|
531
|
+
if (hexCode) {
|
|
532
|
+
await node.sendCustomCode(hexCode);
|
|
533
|
+
node.log(`[Mesh->自定义] 窗帘: ${value}, 发送: ${hexCode}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// 场景类型
|
|
539
|
+
else if (mapping.device === 'custom_scene') {
|
|
540
|
+
if (state.trigger && codes.trigger) {
|
|
541
|
+
await node.sendCustomCode(codes.trigger);
|
|
542
|
+
node.log(`[Mesh->自定义] 场景触发, 发送: ${codes.trigger}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
node.status({ fill: 'green', shape: 'dot', text: `自定义同步 ${node.mappings.length}个` });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ===== 标准Modbus协议模式 =====
|
|
551
|
+
const rs485Channel = mapping.rs485Channel || 1; // RS485端的通道号
|
|
552
|
+
const meshChannel = mapping.meshChannel || 1; // Mesh端的通道号
|
|
553
|
+
node.log(`[Mesh->RS485] 同步到从机${mapping.address}, Mesh通道${meshChannel}->RS485通道${rs485Channel}, 状态: ${JSON.stringify(state)}`);
|
|
435
554
|
|
|
436
555
|
for (const [meshKey, value] of Object.entries(state)) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
556
|
+
try {
|
|
557
|
+
// 处理开关状态 - Mesh的switch_N字段对应RS485的switchN (0x1031-0x1036)
|
|
558
|
+
// 只处理按键寄存器,不处理指示灯寄存器
|
|
559
|
+
const switchMatch = meshKey.match(/^switch_(\d+)$/);
|
|
560
|
+
if (switchMatch) {
|
|
561
|
+
const meshSwitchChannel = parseInt(switchMatch[1]);
|
|
562
|
+
// 只处理配置的Mesh通道
|
|
563
|
+
if (meshSwitchChannel !== meshChannel) continue;
|
|
564
|
+
|
|
565
|
+
// RS485端使用配置的rs485Channel (对应0x1031-0x1036)
|
|
566
|
+
const switchRegKey = `switch${rs485Channel}`;
|
|
567
|
+
|
|
568
|
+
if (registers && registers[switchRegKey]) {
|
|
569
|
+
const writeValue = value ? (registers[switchRegKey].on || 1) : (registers[switchRegKey].off || 0);
|
|
570
|
+
await node.writeModbusRegister(mapping.address, registers[switchRegKey], writeValue);
|
|
571
|
+
|
|
572
|
+
// 输出发送的帧信息
|
|
573
|
+
node.send({
|
|
574
|
+
topic: 'rs485-tx',
|
|
575
|
+
payload: {
|
|
576
|
+
direction: 'Mesh→RS485',
|
|
577
|
+
slaveAddr: mapping.address,
|
|
578
|
+
register: `0x${registers[switchRegKey].address.toString(16).toUpperCase()}`,
|
|
579
|
+
value: writeValue,
|
|
580
|
+
action: value ? '开' : '关'
|
|
581
|
+
},
|
|
582
|
+
timestamp: new Date().toISOString()
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
node.log(`[Mesh->RS485] 从机${mapping.address} 按键${rs485Channel} (0x${registers[switchRegKey].address.toString(16)}): ${value ? '开' : '关'}`);
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 兼容旧格式: switch, acSwitch
|
|
591
|
+
if (meshKey === 'switch' || meshKey === 'acSwitch') {
|
|
592
|
+
const switchRegKey = `switch${rs485Channel}`;
|
|
593
|
+
|
|
594
|
+
if (registers && registers[switchRegKey]) {
|
|
595
|
+
const writeValue = value ? (registers[switchRegKey].on || 1) : (registers[switchRegKey].off || 0);
|
|
596
|
+
await node.writeModbusRegister(mapping.address, registers[switchRegKey], writeValue);
|
|
597
|
+
node.log(`[Mesh->RS485] 从机${mapping.address} 按键${rs485Channel}: ${value ? '开' : '关'}`);
|
|
598
|
+
}
|
|
445
599
|
}
|
|
600
|
+
else if ((meshKey === 'targetTemp' || meshKey === 'acTargetTemp') && registers && registers.targetTemp) {
|
|
601
|
+
await node.writeModbusRegister(mapping.address, registers.targetTemp, value);
|
|
602
|
+
node.debug(`[Mesh->RS485] 目标温度: ${value}`);
|
|
603
|
+
}
|
|
604
|
+
else if (meshKey === 'acMode' && registers && registers.mode) {
|
|
605
|
+
await node.writeModbusRegister(mapping.address, registers.mode, value);
|
|
606
|
+
node.debug(`[Mesh->RS485] 模式: ${value}`);
|
|
607
|
+
}
|
|
608
|
+
else if (meshKey === 'acFanSpeed' && registers && registers.fanSpeed) {
|
|
609
|
+
await node.writeModbusRegister(mapping.address, registers.fanSpeed, value);
|
|
610
|
+
node.debug(`[Mesh->RS485] 风速: ${value}`);
|
|
611
|
+
}
|
|
612
|
+
else if (meshKey === 'brightness' && registers && registers.brightness) {
|
|
613
|
+
await node.writeModbusRegister(mapping.address, registers.brightness, value);
|
|
614
|
+
node.debug(`[Mesh->RS485] 亮度: ${value}`);
|
|
615
|
+
}
|
|
616
|
+
} catch (err) {
|
|
617
|
+
node.error(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
|
|
446
618
|
}
|
|
447
619
|
}
|
|
448
620
|
|
|
449
621
|
node.status({ fill: 'green', shape: 'dot', text: `同步 ${node.mappings.length}个映射` });
|
|
450
622
|
};
|
|
451
623
|
|
|
452
|
-
//
|
|
624
|
+
// 发送自定义十六进制码
|
|
625
|
+
node.sendCustomCode = async function(hexCode) {
|
|
626
|
+
if (!hexCode) return;
|
|
627
|
+
const hexStr = hexCode.replace(/\s/g, '');
|
|
628
|
+
if (!/^[0-9A-Fa-f]+$/.test(hexStr)) {
|
|
629
|
+
node.warn(`无效的十六进制码: ${hexCode}`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const frame = Buffer.from(hexStr, 'hex');
|
|
633
|
+
await node.sendRS485Frame(frame);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Modbus/Custom -> Mesh sync
|
|
453
637
|
node.syncModbusToMesh = async function(cmd) {
|
|
454
|
-
const { mapping, registers, state } = cmd;
|
|
638
|
+
const { mapping, registers, state, customMode } = cmd;
|
|
455
639
|
|
|
456
640
|
// 查找Mesh设备
|
|
457
641
|
const meshDevice = node.gateway.getDevice(mapping.meshMac);
|
|
@@ -460,38 +644,85 @@ module.exports = function(RED) {
|
|
|
460
644
|
return;
|
|
461
645
|
}
|
|
462
646
|
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return Buffer.from([map[v] !== undefined ? map[v] : 0]);
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
'fanSpeed': {
|
|
474
|
-
attrType: 0x1D,
|
|
475
|
-
param: (v) => {
|
|
476
|
-
const map = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
|
|
477
|
-
return Buffer.from([map[v] !== undefined ? map[v] : 4]);
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
'brightness': { attrType: 0x03, param: (v) => Buffer.from([Math.round(v)]) }
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
for (const [key, value] of Object.entries(state)) {
|
|
484
|
-
// 仅在Mesh设备支持此属性时同步
|
|
485
|
-
const m = attrMapping[key];
|
|
486
|
-
if (m) {
|
|
647
|
+
const channel = mapping.meshChannel || 1;
|
|
648
|
+
|
|
649
|
+
// ===== 自定义协议模式 =====
|
|
650
|
+
if (customMode || mapping.brand === 'custom') {
|
|
651
|
+
node.log(`[自定义->Mesh] 设备${mapping.meshMac}, 通道${channel}, 状态: ${JSON.stringify(state)}`);
|
|
652
|
+
|
|
653
|
+
for (const [key, value] of Object.entries(state)) {
|
|
487
654
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
655
|
+
// 开关类型
|
|
656
|
+
if (key === 'switch') {
|
|
657
|
+
const onOff = value ? 0x02 : 0x01;
|
|
658
|
+
const param = Buffer.from([channel - 1, onOff]);
|
|
659
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
660
|
+
node.log(`[自定义->Mesh] 开关${channel}: ${value ? '开' : '关'}`);
|
|
661
|
+
}
|
|
662
|
+
// 窗帘类型
|
|
663
|
+
else if (key === 'action' || key === 'position') {
|
|
664
|
+
let action = 0x00; // 停止
|
|
665
|
+
if (value === 'open') action = 0x01; // 打开
|
|
666
|
+
else if (value === 'close') action = 0x02; // 关闭
|
|
667
|
+
else if (value === 'stop') action = 0x00; // 停止
|
|
668
|
+
|
|
669
|
+
const param = Buffer.from([action]);
|
|
670
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x04, param);
|
|
671
|
+
node.log(`[自定义->Mesh] 窗帘: ${value}`);
|
|
672
|
+
}
|
|
491
673
|
} catch (err) {
|
|
492
674
|
node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
493
675
|
}
|
|
494
676
|
}
|
|
677
|
+
|
|
678
|
+
node.status({ fill: 'blue', shape: 'dot', text: `自定义同步 ${node.mappings.length}个` });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ===== 标准Modbus协议模式 =====
|
|
683
|
+
node.log(`[RS485->Mesh] 同步到设备 ${mapping.meshMac}, 通道${channel}, 状态: ${JSON.stringify(state)}`);
|
|
684
|
+
|
|
685
|
+
for (const [key, value] of Object.entries(state)) {
|
|
686
|
+
try {
|
|
687
|
+
// 处理开关类型 (switch1, switch2, ... 或 led1, led2, ...)
|
|
688
|
+
if (key.startsWith('switch')) {
|
|
689
|
+
const channelFromKey = parseInt(key.replace('switch', '')) || 1;
|
|
690
|
+
const ch = mapping.meshChannel || channelFromKey;
|
|
691
|
+
|
|
692
|
+
const onOff = value ? 0x02 : 0x01;
|
|
693
|
+
const param = Buffer.from([ch - 1, onOff]);
|
|
694
|
+
|
|
695
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
696
|
+
node.log(`[RS485->Mesh] 开关${ch}: ${value ? '开' : '关'}`);
|
|
697
|
+
}
|
|
698
|
+
else if (key.startsWith('led')) {
|
|
699
|
+
node.debug(`[RS485] 指示灯${key}: ${value}`);
|
|
700
|
+
}
|
|
701
|
+
else if (key === 'targetTemp') {
|
|
702
|
+
const param = Buffer.from([Math.round(value)]);
|
|
703
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
|
|
704
|
+
node.debug(`[RS485->Mesh] 目标温度: ${value}`);
|
|
705
|
+
}
|
|
706
|
+
else if (key === 'mode') {
|
|
707
|
+
const modeMap = { 'cool': 0, 'heat': 1, 'fan': 2, 'dry': 3 };
|
|
708
|
+
const param = Buffer.from([modeMap[value] !== undefined ? modeMap[value] : 0]);
|
|
709
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x16, param);
|
|
710
|
+
node.debug(`[RS485->Mesh] 模式: ${value}`);
|
|
711
|
+
}
|
|
712
|
+
else if (key === 'fanSpeed') {
|
|
713
|
+
const speedMap = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
|
|
714
|
+
const param = Buffer.from([speedMap[value] !== undefined ? speedMap[value] : 4]);
|
|
715
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1D, param);
|
|
716
|
+
node.debug(`[RS485->Mesh] 风速: ${value}`);
|
|
717
|
+
}
|
|
718
|
+
else if (key === 'brightness') {
|
|
719
|
+
const param = Buffer.from([Math.round(value)]);
|
|
720
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x03, param);
|
|
721
|
+
node.debug(`[RS485->Mesh] 亮度: ${value}`);
|
|
722
|
+
}
|
|
723
|
+
} catch (err) {
|
|
724
|
+
node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
725
|
+
}
|
|
495
726
|
}
|
|
496
727
|
|
|
497
728
|
node.status({ fill: 'blue', shape: 'dot', text: `同步 ${node.mappings.length}个映射` });
|
|
@@ -555,30 +786,160 @@ module.exports = function(RED) {
|
|
|
555
786
|
node.warn('RS485未连接,无法发送数据');
|
|
556
787
|
return;
|
|
557
788
|
}
|
|
558
|
-
|
|
559
|
-
|
|
789
|
+
try {
|
|
790
|
+
const hexStr = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
791
|
+
node.log(`[RS485 TX] 发送帧: ${hexStr}`);
|
|
792
|
+
|
|
793
|
+
await node.rs485Config.send(frame);
|
|
794
|
+
|
|
795
|
+
// 输出到节点端口
|
|
796
|
+
node.send({
|
|
797
|
+
topic: 'rs485-frame-tx',
|
|
798
|
+
payload: {
|
|
799
|
+
direction: 'TX',
|
|
800
|
+
hex: hexStr,
|
|
801
|
+
slaveAddr: frame[0],
|
|
802
|
+
funcCode: frame[1]
|
|
803
|
+
},
|
|
804
|
+
timestamp: new Date().toISOString()
|
|
805
|
+
});
|
|
806
|
+
} catch (err) {
|
|
807
|
+
node.error(`RS485发送失败: ${err.message}`);
|
|
808
|
+
}
|
|
560
809
|
};
|
|
561
810
|
|
|
562
|
-
//
|
|
811
|
+
// 解析原始RS485帧 - 支持标准Modbus和自定义码匹配
|
|
812
|
+
node.parseRS485Frame = function(frame) {
|
|
813
|
+
if (frame.length < 4) return;
|
|
814
|
+
|
|
815
|
+
const hexStr = frame.toString('hex').toUpperCase();
|
|
816
|
+
const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
|
|
817
|
+
|
|
818
|
+
// 首先检查自定义码匹配(遍历所有映射)
|
|
819
|
+
for (const mapping of node.mappings) {
|
|
820
|
+
if (mapping.brand === 'custom' && mapping.customCodes) {
|
|
821
|
+
const codes = mapping.customCodes;
|
|
822
|
+
let matchedAction = null;
|
|
823
|
+
|
|
824
|
+
// 开关类型:匹配on/off
|
|
825
|
+
if (mapping.device === 'custom_switch') {
|
|
826
|
+
if (codes.on && hexStr.includes(codes.on.replace(/\s/g, '').toUpperCase())) {
|
|
827
|
+
matchedAction = { switch: true };
|
|
828
|
+
} else if (codes.off && hexStr.includes(codes.off.replace(/\s/g, '').toUpperCase())) {
|
|
829
|
+
matchedAction = { switch: false };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// 窗帘类型:匹配open/close/stop
|
|
833
|
+
else if (mapping.device === 'custom_curtain') {
|
|
834
|
+
if (codes.open && hexStr.includes(codes.open.replace(/\s/g, '').toUpperCase())) {
|
|
835
|
+
matchedAction = { position: 'open', action: 'open' };
|
|
836
|
+
} else if (codes.close && hexStr.includes(codes.close.replace(/\s/g, '').toUpperCase())) {
|
|
837
|
+
matchedAction = { position: 'close', action: 'close' };
|
|
838
|
+
} else if (codes.stop && hexStr.includes(codes.stop.replace(/\s/g, '').toUpperCase())) {
|
|
839
|
+
matchedAction = { position: 'stop', action: 'stop' };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// 场景类型:匹配trigger
|
|
843
|
+
else if (mapping.device === 'custom_scene') {
|
|
844
|
+
if (codes.trigger && hexStr.includes(codes.trigger.replace(/\s/g, '').toUpperCase())) {
|
|
845
|
+
matchedAction = { trigger: true };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (matchedAction) {
|
|
850
|
+
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
851
|
+
if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 500) {
|
|
852
|
+
node.debug(`[防循环] 忽略刚刚同步的帧: ${hexFormatted}`);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
node.log(`[自定义码匹配] ${mapping.device}: ${JSON.stringify(matchedAction)}, 帧: ${hexFormatted}`);
|
|
857
|
+
|
|
858
|
+
// 输出调试信息到节点输出端口
|
|
859
|
+
node.send({
|
|
860
|
+
topic: 'custom-code-match',
|
|
861
|
+
payload: {
|
|
862
|
+
direction: 'RS485→Mesh(自定义)',
|
|
863
|
+
device: mapping.device,
|
|
864
|
+
meshMac: mapping.meshMac,
|
|
865
|
+
action: matchedAction,
|
|
866
|
+
hex: hexFormatted
|
|
867
|
+
},
|
|
868
|
+
timestamp: new Date().toISOString()
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
node.queueCommand({
|
|
872
|
+
direction: 'modbus-to-mesh',
|
|
873
|
+
mapping: mapping,
|
|
874
|
+
registers: null,
|
|
875
|
+
state: matchedAction,
|
|
876
|
+
customMode: true,
|
|
877
|
+
timestamp: Date.now()
|
|
878
|
+
});
|
|
879
|
+
return; // 匹配到自定义码就不再继续
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// 标准Modbus解析
|
|
885
|
+
node.parseModbusResponse(frame);
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// 解析Modbus响应/上报帧(标准Modbus协议)
|
|
563
889
|
node.parseModbusResponse = function(frame) {
|
|
890
|
+
if (frame.length < 6) return;
|
|
891
|
+
|
|
564
892
|
const slaveAddr = frame[0];
|
|
565
893
|
const fc = frame[1];
|
|
566
894
|
|
|
567
895
|
// 查找对应的映射
|
|
568
896
|
const mapping = node.findRS485Mapping(slaveAddr);
|
|
569
|
-
if (!mapping)
|
|
897
|
+
if (!mapping) {
|
|
898
|
+
node.debug(`未找到从机${slaveAddr}的映射配置`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 自定义模式不走标准Modbus解析
|
|
903
|
+
if (mapping.brand === 'custom') return;
|
|
570
904
|
|
|
571
905
|
const registers = node.getRegistersForMapping(mapping);
|
|
572
|
-
if (!registers)
|
|
906
|
+
if (!registers) {
|
|
907
|
+
node.debug(`未找到设备${mapping.device}的寄存器定义`);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
912
|
+
if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 500) {
|
|
913
|
+
node.debug(`[防循环] 忽略刚刚同步的Modbus帧`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
573
916
|
|
|
574
917
|
// 根据功能码解析数据
|
|
575
918
|
let state = {};
|
|
576
|
-
|
|
577
|
-
|
|
919
|
+
|
|
920
|
+
if (fc === 0x06 || fc === 0x10) {
|
|
921
|
+
// 写单寄存器响应/写多寄存器响应 - 这是设备主动上报或写响应
|
|
922
|
+
const regAddr = frame.readUInt16BE(2);
|
|
923
|
+
const value = frame.readUInt16BE(4);
|
|
924
|
+
|
|
925
|
+
// 查找匹配的寄存器定义
|
|
926
|
+
for (const [key, reg] of Object.entries(registers)) {
|
|
927
|
+
if (reg.address === regAddr) {
|
|
928
|
+
if (key.startsWith('switch') || key.startsWith('led')) {
|
|
929
|
+
state[key] = value === (reg.on || 1);
|
|
930
|
+
} else if (reg.map) {
|
|
931
|
+
state[key] = reg.map[value] || value;
|
|
932
|
+
} else {
|
|
933
|
+
state[key] = value;
|
|
934
|
+
}
|
|
935
|
+
node.log(`[RS485] 从机${slaveAddr} 寄存器0x${regAddr.toString(16).toUpperCase()}: ${key}=${value}`);
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} else if (fc === 0x03 || fc === 0x04) {
|
|
578
940
|
const byteCount = frame[2];
|
|
579
941
|
for (let i = 0; i < byteCount / 2; i++) {
|
|
580
942
|
const value = frame.readUInt16BE(3 + i * 2);
|
|
581
|
-
// 根据寄存器映射解析
|
|
582
943
|
for (const [key, reg] of Object.entries(registers)) {
|
|
583
944
|
if (reg.map) {
|
|
584
945
|
state[key] = reg.map[value] || value;
|
|
@@ -587,10 +948,32 @@ module.exports = function(RED) {
|
|
|
587
948
|
}
|
|
588
949
|
}
|
|
589
950
|
}
|
|
951
|
+
} else if (fc === 0x20) {
|
|
952
|
+
if (frame.length >= 9) {
|
|
953
|
+
const startReg = frame.readUInt16BE(2);
|
|
954
|
+
const count = frame.readUInt16BE(4);
|
|
955
|
+
node.debug(`[RS485] 从机${slaveAddr} 自定义上报: 起始0x${startReg.toString(16)}, 数量${count}`);
|
|
956
|
+
}
|
|
590
957
|
}
|
|
591
958
|
|
|
592
959
|
if (Object.keys(state).length > 0) {
|
|
593
|
-
node.log(`[RS485->Mesh] 设备@${slaveAddr}
|
|
960
|
+
node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态变化: ${JSON.stringify(state)}`);
|
|
961
|
+
|
|
962
|
+
// 输出调试信息到节点输出端口
|
|
963
|
+
node.send({
|
|
964
|
+
topic: 'rs485-state-change',
|
|
965
|
+
payload: {
|
|
966
|
+
direction: 'RS485→Mesh',
|
|
967
|
+
slaveAddr: slaveAddr,
|
|
968
|
+
funcCode: fc,
|
|
969
|
+
brand: mapping.brand,
|
|
970
|
+
device: mapping.device,
|
|
971
|
+
meshMac: mapping.meshMac,
|
|
972
|
+
state: state
|
|
973
|
+
},
|
|
974
|
+
timestamp: new Date().toISOString()
|
|
975
|
+
});
|
|
976
|
+
|
|
594
977
|
node.queueCommand({
|
|
595
978
|
direction: 'modbus-to-mesh',
|
|
596
979
|
mapping: mapping,
|
|
@@ -697,13 +1080,20 @@ module.exports = function(RED) {
|
|
|
697
1080
|
|
|
698
1081
|
// 清理
|
|
699
1082
|
node.on('close', (done) => {
|
|
1083
|
+
// 移除Mesh网关事件监听器
|
|
700
1084
|
node.gateway.removeListener('device-list-complete', init);
|
|
701
1085
|
node.gateway.removeListener('device-state-changed', handleMeshStateChange);
|
|
702
1086
|
|
|
703
|
-
//
|
|
704
|
-
if (node.rs485Config) {
|
|
1087
|
+
// 移除RS485配置节点事件监听器
|
|
1088
|
+
if (node.rs485Config && node._rs485Handlers) {
|
|
1089
|
+
node.rs485Config.removeListener('connected', node._rs485Handlers.onRS485Connected);
|
|
1090
|
+
node.rs485Config.removeListener('disconnected', node._rs485Handlers.onRS485Disconnected);
|
|
1091
|
+
node.rs485Config.removeListener('error', node._rs485Handlers.onRS485Error);
|
|
1092
|
+
node.rs485Config.removeListener('frame', node._rs485Handlers.onRS485Frame);
|
|
705
1093
|
node.rs485Config.deregister(node);
|
|
706
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
node.log('[RS485 Bridge] 节点已清理');
|
|
707
1097
|
done();
|
|
708
1098
|
});
|
|
709
1099
|
}
|
package/nodes/symi-485-config.js
CHANGED
|
@@ -114,6 +114,9 @@ module.exports = function(RED) {
|
|
|
114
114
|
|
|
115
115
|
node.client.on('connect', () => {
|
|
116
116
|
node.connected = true;
|
|
117
|
+
// 启用TCP keep-alive,防止连接超时
|
|
118
|
+
node.client.setKeepAlive(true, 30000); // 30秒心跳
|
|
119
|
+
node.client.setNoDelay(true); // 禁用Nagle算法,立即发送
|
|
117
120
|
node.warn(`[RS485] TCP已连接: ${node.host}:${node.port}`);
|
|
118
121
|
node.emit('connected');
|
|
119
122
|
});
|