node-red-contrib-symi-mesh 1.6.1 → 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 CHANGED
@@ -1032,7 +1032,7 @@ node-red-contrib-symi-mesh/
1032
1032
 
1033
1033
  ## 更新日志
1034
1034
 
1035
- ### v1.6.0 (2025-12-03)
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.0
1054
+ **版本**: 1.6.2
1055
1055
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1056
- **最后更新**: 2025-12-03
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
@@ -271,32 +271,50 @@ module.exports = function(RED) {
271
271
  node.lastSyncTime = 0;
272
272
  node.pendingVerify = false;
273
273
 
274
+ // RS485连接信息
275
+ const rs485Info = node.rs485Config.connectionType === 'tcp'
276
+ ? `${node.rs485Config.host}:${node.rs485Config.port}`
277
+ : node.rs485Config.serialPort;
278
+
274
279
  if (node.mappings.length === 0) {
275
280
  node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
276
281
  } else {
277
- node.status({ fill: 'yellow', shape: 'ring', text: '连接中...' });
282
+ node.status({ fill: 'yellow', shape: 'ring', text: `连接中 ${rs485Info}...` });
278
283
  }
279
284
 
280
- // 注册到RS485配置节点
281
- node.rs485Config.register(node);
282
-
283
- // 监听RS485连接事件
284
- node.rs485Config.on('connected', () => {
285
- node.status({ fill: 'green', shape: 'dot', text: `已连接 ${node.mappings.length}个映射` });
286
- });
285
+ // 【重要】先绑定事件监听器,再注册到配置节点
286
+ // 定义事件处理函数(用于清理时移除)
287
+ const onRS485Connected = () => {
288
+ node.log(`[RS485 Bridge] 已连接到 ${rs485Info}`);
289
+ node.status({ fill: 'green', shape: 'dot', text: `已连接 ${rs485Info} (${node.mappings.length}个映射)` });
290
+ };
287
291
 
288
- node.rs485Config.on('disconnected', () => {
289
- node.status({ fill: 'yellow', shape: 'ring', text: '已断开' });
290
- });
292
+ const onRS485Disconnected = () => {
293
+ node.warn(`[RS485 Bridge] 已断开 ${rs485Info}`);
294
+ node.status({ fill: 'yellow', shape: 'ring', text: `已断开 ${rs485Info}` });
295
+ };
291
296
 
292
- node.rs485Config.on('error', (err) => {
293
- node.status({ fill: 'red', shape: 'ring', text: '连接错误' });
294
- });
297
+ const onRS485Error = (err) => {
298
+ node.error(`[RS485 Bridge] 连接错误 ${rs485Info}: ${err.message}`);
299
+ node.status({ fill: 'red', shape: 'ring', text: `错误 ${rs485Info}` });
300
+ };
295
301
 
296
- // 监听RS485接收帧
297
- node.rs485Config.on('frame', (frame) => {
302
+ const onRS485Frame = (frame) => {
298
303
  node.parseModbusResponse(frame);
299
- });
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}`);
300
318
 
301
319
  // 查找Mesh设备的映射配置
302
320
  node.findMeshMapping = function(mac, channel) {
@@ -426,23 +444,50 @@ module.exports = function(RED) {
426
444
  // Mesh -> Modbus sync
427
445
  node.syncMeshToModbus = async function(cmd) {
428
446
  const { mapping, registers, state } = cmd;
429
- const stateMapping = {
430
- 'switch': 'switch', 'acSwitch': 'switch',
431
- 'targetTemp': 'targetTemp', 'acTargetTemp': 'targetTemp',
432
- 'acMode': 'mode', 'acFanSpeed': 'fanSpeed',
433
- 'brightness': 'brightness'
434
- };
447
+
448
+ node.log(`[Mesh->RS485] 同步到从机${mapping.address}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
435
449
 
436
450
  for (const [meshKey, value] of Object.entries(state)) {
437
- const regKey = stateMapping[meshKey];
438
- // Only sync if RS485 device has this register (partial sync support)
439
- if (regKey && registers[regKey]) {
440
- try {
441
- await node.writeModbusRegister(mapping.address, registers[regKey], value);
442
- node.debug(`Mesh->RS485@${mapping.address}: ${meshKey}=${value}`);
443
- } catch (err) {
444
- node.error(`RS485写入失败: ${regKey}=${value}, ${err.message}`);
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
+ }
445
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}`);
488
+ }
489
+ } catch (err) {
490
+ node.error(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
446
491
  }
447
492
  }
448
493
 
@@ -460,37 +505,54 @@ module.exports = function(RED) {
460
505
  return;
461
506
  }
462
507
 
463
- const attrMapping = {
464
- 'switch': { attrType: 0x02, param: (v) => Buffer.from([v ? 0x02 : 0x01]) },
465
- 'targetTemp': { attrType: 0x1C, param: (v) => Buffer.from([Math.round(v)]) },
466
- 'mode': {
467
- attrType: 0x16,
468
- param: (v) => {
469
- const map = { 'cool': 0, 'heat': 1, 'fan': 2, 'dry': 3 };
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
- };
508
+ node.log(`[RS485->Mesh] 同步到设备 ${mapping.meshMac}, 通道${mapping.meshChannel || 0}, 状态: ${JSON.stringify(state)}`);
482
509
 
483
510
  for (const [key, value] of Object.entries(state)) {
484
- // 仅在Mesh设备支持此属性时同步
485
- const m = attrMapping[key];
486
- if (m) {
487
- try {
488
- const param = typeof m.param === 'function' ? m.param(value) : m.param;
489
- await node.gateway.sendControl(meshDevice.networkAddress, m.attrType, param);
490
- node.debug(`RS485@${mapping.address}->Mesh: ${key}=${value}`);
491
- } catch (err) {
492
- node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
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 ? '开' : '关'}`);
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}`);
493
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}`);
494
556
  }
495
557
  }
496
558
 
@@ -559,26 +621,55 @@ module.exports = function(RED) {
559
621
  node.debug(`发送RS485帧: ${frame.toString('hex')}`);
560
622
  };
561
623
 
562
- // 解析Modbus响应
624
+ // 解析Modbus响应/上报帧
563
625
  node.parseModbusResponse = function(frame) {
626
+ if (frame.length < 6) return;
627
+
564
628
  const slaveAddr = frame[0];
565
629
  const fc = frame[1];
566
630
 
567
631
  // 查找对应的映射
568
632
  const mapping = node.findRS485Mapping(slaveAddr);
569
- if (!mapping) return;
633
+ if (!mapping) {
634
+ node.debug(`未找到从机${slaveAddr}的映射配置`);
635
+ return;
636
+ }
570
637
 
571
638
  const registers = node.getRegistersForMapping(mapping);
572
- if (!registers) return;
639
+ if (!registers) {
640
+ node.debug(`未找到设备${mapping.device}的寄存器定义`);
641
+ return;
642
+ }
573
643
 
574
644
  // 根据功能码解析数据
575
645
  let state = {};
576
- if (fc === 0x03 || fc === 0x04) {
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) {
577
669
  // 读寄存器响应
578
670
  const byteCount = frame[2];
579
671
  for (let i = 0; i < byteCount / 2; i++) {
580
672
  const value = frame.readUInt16BE(3 + i * 2);
581
- // 根据寄存器映射解析
582
673
  for (const [key, reg] of Object.entries(registers)) {
583
674
  if (reg.map) {
584
675
  state[key] = reg.map[value] || value;
@@ -587,10 +678,19 @@ module.exports = function(RED) {
587
678
  }
588
679
  }
589
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
+ }
590
690
  }
591
691
 
592
692
  if (Object.keys(state).length > 0) {
593
- node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态: ${JSON.stringify(state)}`);
693
+ node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态变化: ${JSON.stringify(state)}`);
594
694
  node.queueCommand({
595
695
  direction: 'modbus-to-mesh',
596
696
  mapping: mapping,
@@ -697,13 +797,20 @@ module.exports = function(RED) {
697
797
 
698
798
  // 清理
699
799
  node.on('close', (done) => {
800
+ // 移除Mesh网关事件监听器
700
801
  node.gateway.removeListener('device-list-complete', init);
701
802
  node.gateway.removeListener('device-state-changed', handleMeshStateChange);
702
803
 
703
- // 注销RS485配置节点
704
- 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);
705
810
  node.rs485Config.deregister(node);
706
811
  }
812
+
813
+ node.log('[RS485 Bridge] 节点已清理');
707
814
  done();
708
815
  });
709
816
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "index.js",
6
6
  "scripts": {