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 +3 -3
- package/nodes/symi-485-bridge.js +174 -67
- 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/symi-485-bridge.js
CHANGED
|
@@ -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
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
node.
|
|
290
|
-
|
|
292
|
+
const onRS485Disconnected = () => {
|
|
293
|
+
node.warn(`[RS485 Bridge] 已断开 ${rs485Info}`);
|
|
294
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `已断开 ${rs485Info}` });
|
|
295
|
+
};
|
|
291
296
|
|
|
292
|
-
|
|
293
|
-
node.
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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)
|
|
633
|
+
if (!mapping) {
|
|
634
|
+
node.debug(`未找到从机${slaveAddr}的映射配置`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
570
637
|
|
|
571
638
|
const registers = node.getRegistersForMapping(mapping);
|
|
572
|
-
if (!registers)
|
|
639
|
+
if (!registers) {
|
|
640
|
+
node.debug(`未找到设备${mapping.device}的寄存器定义`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
573
643
|
|
|
574
644
|
// 根据功能码解析数据
|
|
575
645
|
let state = {};
|
|
576
|
-
|
|
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}
|
|
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
|
-
//
|
|
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
|
}
|