node-red-contrib-symi-mesh 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -539,9 +539,38 @@ Mesh 四键开关 第2路 ↔ RS485 六键开关 第5路 地址:2 ✓ 支持
539
539
 
540
540
  | 品牌 | 支持设备 |
541
541
  |-----|---------|
542
- | 话语前湾 | 空调、地暖、新风、窗帘、1-8键开关 |
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
 
@@ -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
- mappings.push({
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
  }
@@ -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
  };
@@ -300,7 +324,7 @@ module.exports = function(RED) {
300
324
  };
301
325
 
302
326
  const onRS485Frame = (frame) => {
303
- node.parseModbusResponse(frame);
327
+ node.parseRS485Frame(frame); // 使用新的解析函数,支持自定义码
304
328
  };
305
329
 
306
330
  // 绑定事件监听器
@@ -342,23 +366,52 @@ module.exports = function(RED) {
342
366
 
343
367
  const mac = eventData.device.macAddress;
344
368
  const state = eventData.state || {};
345
- const channel = state.channel || 0;
369
+ // channel可能是0-based或1-based,需要兼容处理
370
+ // Mesh事件中channel通常是0-based,UI配置的meshChannel是1-based
371
+ const eventChannel = state.channel !== undefined ? state.channel : -1;
346
372
 
347
- // 查找匹配的映射
348
- const mapping = node.findMeshMapping(mac, channel);
349
- if (!mapping) return;
373
+ node.debug(`[Mesh事件] MAC=${mac}, channel=${eventChannel}, state=${JSON.stringify(state)}`);
350
374
 
351
- const registers = node.getRegistersForMapping(mapping);
352
- if (!registers) return;
353
-
354
- node.log(`[Mesh->RS485] ${eventData.device.name} 状态变化`);
355
- node.queueCommand({
356
- direction: 'mesh-to-modbus',
357
- mapping: mapping,
358
- registers: registers,
359
- state: state,
360
- timestamp: Date.now()
361
- });
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
+ }
362
415
  };
363
416
 
364
417
  // RS485 device state change handler (event-driven)
@@ -441,48 +494,122 @@ module.exports = function(RED) {
441
494
  // 这是安全机制,不是轮询
442
495
  };
443
496
 
444
- // Mesh -> Modbus sync
497
+ // Mesh -> Modbus/Custom sync
445
498
  node.syncMeshToModbus = async function(cmd) {
446
499
  const { mapping, registers, state } = cmd;
447
500
 
448
- node.log(`[Mesh->RS485] 同步到从机${mapping.address}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
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)}`);
449
554
 
450
555
  for (const [meshKey, value] of Object.entries(state)) {
451
556
  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}`;
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;
458
564
 
459
- // 同步开关状态
460
- if (registers[switchRegKey]) {
565
+ // RS485端使用配置的rs485Channel (对应0x1031-0x1036)
566
+ const switchRegKey = `switch${rs485Channel}`;
567
+
568
+ if (registers && registers[switchRegKey]) {
461
569
  const writeValue = value ? (registers[switchRegKey].on || 1) : (registers[switchRegKey].off || 0);
462
570
  await node.writeModbusRegister(mapping.address, registers[switchRegKey], writeValue);
463
- node.log(`[Mesh->RS485] 开关${channel}: ${value ? '开' : '关'} (寄存器0x${registers[switchRegKey].address.toString(16)})`);
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 ? '开' : '关'}`);
464
586
  }
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 ? '' : ''}`);
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 ? '开' : '关'}`);
470
598
  }
471
599
  }
472
- // 处理温控器属性
473
- else if ((meshKey === 'targetTemp' || meshKey === 'acTargetTemp') && registers.targetTemp) {
600
+ else if ((meshKey === 'targetTemp' || meshKey === 'acTargetTemp') && registers && registers.targetTemp) {
474
601
  await node.writeModbusRegister(mapping.address, registers.targetTemp, value);
475
602
  node.debug(`[Mesh->RS485] 目标温度: ${value}`);
476
603
  }
477
- else if (meshKey === 'acMode' && registers.mode) {
604
+ else if (meshKey === 'acMode' && registers && registers.mode) {
478
605
  await node.writeModbusRegister(mapping.address, registers.mode, value);
479
606
  node.debug(`[Mesh->RS485] 模式: ${value}`);
480
607
  }
481
- else if (meshKey === 'acFanSpeed' && registers.fanSpeed) {
608
+ else if (meshKey === 'acFanSpeed' && registers && registers.fanSpeed) {
482
609
  await node.writeModbusRegister(mapping.address, registers.fanSpeed, value);
483
610
  node.debug(`[Mesh->RS485] 风速: ${value}`);
484
611
  }
485
- else if (meshKey === 'brightness' && registers.brightness) {
612
+ else if (meshKey === 'brightness' && registers && registers.brightness) {
486
613
  await node.writeModbusRegister(mapping.address, registers.brightness, value);
487
614
  node.debug(`[Mesh->RS485] 亮度: ${value}`);
488
615
  }
@@ -494,9 +621,21 @@ module.exports = function(RED) {
494
621
  node.status({ fill: 'green', shape: 'dot', text: `同步 ${node.mappings.length}个映射` });
495
622
  };
496
623
 
497
- // Modbus -> Mesh sync
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
498
637
  node.syncModbusToMesh = async function(cmd) {
499
- const { mapping, registers, state } = cmd;
638
+ const { mapping, registers, state, customMode } = cmd;
500
639
 
501
640
  // 查找Mesh设备
502
641
  const meshDevice = node.gateway.getDevice(mapping.meshMac);
@@ -505,30 +644,60 @@ module.exports = function(RED) {
505
644
  return;
506
645
  }
507
646
 
508
- node.log(`[RS485->Mesh] 同步到设备 ${mapping.meshMac}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
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)) {
654
+ try {
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
+ }
673
+ } catch (err) {
674
+ node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
675
+ }
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)}`);
509
684
 
510
685
  for (const [key, value] of Object.entries(state)) {
511
686
  try {
512
687
  // 处理开关类型 (switch1, switch2, ... 或 led1, led2, ...)
513
688
  if (key.startsWith('switch')) {
514
- // 从键名提取通道号,如 switch1 -> 1, switch2 -> 2
515
689
  const channelFromKey = parseInt(key.replace('switch', '')) || 1;
516
- // 使用映射中配置的通道,或从键名获取
517
- const channel = mapping.meshChannel || channelFromKey;
690
+ const ch = mapping.meshChannel || channelFromKey;
518
691
 
519
- // Mesh开关控制:attrType=0x02, param=[通道, 开/关]
520
- const onOff = value ? 0x02 : 0x01; // 0x02=开, 0x01=关
521
- const param = Buffer.from([channel - 1, onOff]); // 通道从0开始
692
+ const onOff = value ? 0x02 : 0x01;
693
+ const param = Buffer.from([ch - 1, onOff]);
522
694
 
523
695
  await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
524
- node.log(`[RS485->Mesh] 开关${channel}: ${value ? '开' : '关'}`);
696
+ node.log(`[RS485->Mesh] 开关${ch}: ${value ? '开' : '关'}`);
525
697
  }
526
- // 处理指示灯(可选,某些场景需要同步指示灯状态)
527
698
  else if (key.startsWith('led')) {
528
- // 指示灯状态通常不需要同步回Mesh,仅记录
529
699
  node.debug(`[RS485] 指示灯${key}: ${value}`);
530
700
  }
531
- // 处理温控器属性
532
701
  else if (key === 'targetTemp') {
533
702
  const param = Buffer.from([Math.round(value)]);
534
703
  await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
@@ -617,11 +786,106 @@ module.exports = function(RED) {
617
786
  node.warn('RS485未连接,无法发送数据');
618
787
  return;
619
788
  }
620
- await node.rs485Config.send(frame);
621
- node.debug(`发送RS485帧: ${frame.toString('hex')}`);
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
+ }
622
809
  };
623
810
 
624
- // 解析Modbus响应/上报帧
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协议)
625
889
  node.parseModbusResponse = function(frame) {
626
890
  if (frame.length < 6) return;
627
891
 
@@ -635,25 +899,32 @@ module.exports = function(RED) {
635
899
  return;
636
900
  }
637
901
 
902
+ // 自定义模式不走标准Modbus解析
903
+ if (mapping.brand === 'custom') return;
904
+
638
905
  const registers = node.getRegistersForMapping(mapping);
639
906
  if (!registers) {
640
907
  node.debug(`未找到设备${mapping.device}的寄存器定义`);
641
908
  return;
642
909
  }
643
910
 
911
+ // 防死循环:检查是否刚刚从Mesh发送过来
912
+ if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 500) {
913
+ node.debug(`[防循环] 忽略刚刚同步的Modbus帧`);
914
+ return;
915
+ }
916
+
644
917
  // 根据功能码解析数据
645
918
  let state = {};
646
919
 
647
920
  if (fc === 0x06 || fc === 0x10) {
648
921
  // 写单寄存器响应/写多寄存器响应 - 这是设备主动上报或写响应
649
- // 格式: 从机地址 + 功能码 + 寄存器地址(2字节) + 值(2字节) + CRC
650
922
  const regAddr = frame.readUInt16BE(2);
651
923
  const value = frame.readUInt16BE(4);
652
924
 
653
925
  // 查找匹配的寄存器定义
654
926
  for (const [key, reg] of Object.entries(registers)) {
655
927
  if (reg.address === regAddr) {
656
- // 处理开关类型
657
928
  if (key.startsWith('switch') || key.startsWith('led')) {
658
929
  state[key] = value === (reg.on || 1);
659
930
  } else if (reg.map) {
@@ -666,7 +937,6 @@ module.exports = function(RED) {
666
937
  }
667
938
  }
668
939
  } else if (fc === 0x03 || fc === 0x04) {
669
- // 读寄存器响应
670
940
  const byteCount = frame[2];
671
941
  for (let i = 0; i < byteCount / 2; i++) {
672
942
  const value = frame.readUInt16BE(3 + i * 2);
@@ -679,18 +949,31 @@ module.exports = function(RED) {
679
949
  }
680
950
  }
681
951
  } else if (fc === 0x20) {
682
- // 自定义功能码0x20 - 可能是批量上报
683
- // 格式: 从机地址 + 0x20 + 起始寄存器(2字节) + 数量(2字节) + 数据... + CRC
684
952
  if (frame.length >= 9) {
685
953
  const startReg = frame.readUInt16BE(2);
686
954
  const count = frame.readUInt16BE(4);
687
955
  node.debug(`[RS485] 从机${slaveAddr} 自定义上报: 起始0x${startReg.toString(16)}, 数量${count}`);
688
- // 暂不处理,记录日志供分析
689
956
  }
690
957
  }
691
958
 
692
959
  if (Object.keys(state).length > 0) {
693
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
+
694
977
  node.queueCommand({
695
978
  direction: 'modbus-to-mesh',
696
979
  mapping: mapping,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "index.js",
6
6
  "scripts": {