node-red-contrib-symi-mesh 1.7.1 → 1.7.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.
@@ -29,24 +29,45 @@
29
29
  }
30
30
  }).trigger('change');
31
31
 
32
- // 加载串口列表
33
- function loadSerialPorts() {
34
- $.getJSON('/symi-rs485-bridge/serial-ports', function(ports) {
35
- var select = $('#node-config-input-serialPort');
36
- var currentVal = select.val() || node.serialPort;
37
- select.empty();
38
- select.append('<option value="">-- 选择串口 --</option>');
39
- ports.forEach(function(p) {
40
- var label = p.path;
41
- if (p.manufacturer) label += ' (' + p.manufacturer + ')';
42
- var sel = (p.path === currentVal) ? ' selected' : '';
43
- select.append('<option value="' + p.path + '"' + sel + '>' + label + '</option>');
44
- });
32
+ // 串口搜索
33
+ $('#btn-refresh-ports').on('click', function() {
34
+ var $btn = $(this);
35
+ var $select = $('#serial-port-select');
36
+ $btn.prop('disabled', true);
37
+ $select.empty().append('<option value="">搜索中...</option>').show();
38
+
39
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
40
+ $select.empty();
41
+ if (ports && ports.length > 0) {
42
+ $select.append('<option value="">-- 选择串口 --</option>');
43
+ ports.forEach(function(p) {
44
+ var label = p.path;
45
+ if (p.manufacturer) label += ' (' + p.manufacturer + ')';
46
+ var sel = (p.path === node.serialPort) ? ' selected' : '';
47
+ $select.append('<option value="' + p.path + '"' + sel + '>' + label + '</option>');
48
+ });
49
+ } else {
50
+ $select.append('<option value="">未发现串口</option>');
51
+ }
52
+ $btn.prop('disabled', false);
53
+ }).fail(function() {
54
+ $select.empty().append('<option value="">搜索失败</option>');
55
+ $btn.prop('disabled', false);
45
56
  });
57
+ });
58
+
59
+ // 串口选择
60
+ $('#serial-port-select').on('change', function() {
61
+ var val = $(this).val();
62
+ if (val) {
63
+ $('#node-config-input-serialPort').val(val);
64
+ }
65
+ });
66
+
67
+ // 初始化时如果已有串口配置,显示在下拉框
68
+ if (node.serialPort) {
69
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
46
70
  }
47
-
48
- $('#btn-refresh-ports').on('click', loadSerialPorts);
49
- loadSerialPorts();
50
71
  }
51
72
  });
52
73
  </script>
@@ -67,11 +88,13 @@
67
88
 
68
89
  <div class="serial-config">
69
90
  <div class="form-row">
70
- <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口</label>
71
- <select id="node-config-input-serialPort" style="width:60%"></select>
72
- <button type="button" id="btn-refresh-ports" class="red-ui-button" style="margin-left:5px">
73
- <i class="fa fa-refresh"></i>
74
- </button>
91
+ <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口路径</label>
92
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
93
+ <button type="button" id="btn-refresh-ports" class="red-ui-button" style="margin-left:5px">搜索串口</button>
94
+ </div>
95
+ <div class="form-row">
96
+ <label>&nbsp;</label>
97
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
75
98
  </div>
76
99
 
77
100
  <div class="form-row">
@@ -159,7 +159,7 @@ module.exports = function(RED) {
159
159
  node.client.on('error', (err) => {
160
160
  // AggregateError特殊处理(Node.js 18+的IPv4/IPv6连接失败)
161
161
  if (err.name === 'AggregateError' || err.errors) {
162
- node.warn(`RS485 TCP连接失败: 无法连接到 ${node.host}:${node.port}`);
162
+ node.debug(`RS485 TCP连接失败: 无法连接到 ${node.host}:${node.port}`);
163
163
  } else {
164
164
  node.error(`RS485 TCP错误: ${err.message}`);
165
165
  }
@@ -213,18 +213,20 @@ module.exports = function(RED) {
213
213
  node.connected = false;
214
214
  };
215
215
 
216
- // 处理接收数据(支持Modbus RTU和杜亚协议)
216
+ // 处理接收数据(支持Modbus RTU、杜亚协议和自定义码)
217
217
  node.handleData = function(data) {
218
+ const hexIn = data.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
219
+ node.debug(`[RS485 RX] 收到数据: ${hexIn} (${data.length}字节)`);
218
220
  node.receiveBuffer = Buffer.concat([node.receiveBuffer, data]);
219
221
 
220
222
  // 防止缓冲区过大(最大8KB)
221
223
  if (node.receiveBuffer.length > 8192) {
222
224
  node.receiveBuffer = node.receiveBuffer.subarray(-2048);
223
- node.warn('接收缓冲区溢出,已截断');
225
+ node.debug('接收缓冲区溢出,已截断');
224
226
  }
225
227
 
226
- // 最小帧长度为4字节
227
- while (node.receiveBuffer.length >= 4) {
228
+ // 最小帧长度为2字节
229
+ while (node.receiveBuffer.length >= 2) {
228
230
  // ===== 杜亚协议检测 (55开头) =====
229
231
  if (node.receiveBuffer[0] === 0x55 && node.receiveBuffer.length >= 7) {
230
232
  // 杜亚窗帘协议:55 [地址高] [地址低] 03 [数据] [CRC16低] [CRC16高]
@@ -255,16 +257,52 @@ module.exports = function(RED) {
255
257
  break;
256
258
  }
257
259
 
260
+ // ===== SYMI协议检测 (7E开头7D结尾) =====
261
+ if (node.receiveBuffer[0] === 0x7E) {
262
+ // 查找结束符7D
263
+ let endIdx = -1;
264
+ for (let i = 1; i < node.receiveBuffer.length; i++) {
265
+ if (node.receiveBuffer[i] === 0x7D) {
266
+ endIdx = i;
267
+ break;
268
+ }
269
+ }
270
+ if (endIdx > 0) {
271
+ const frame = node.receiveBuffer.subarray(0, endIdx + 1);
272
+ node.receiveBuffer = node.receiveBuffer.subarray(endIdx + 1);
273
+ node.emit('frame', frame);
274
+ continue;
275
+ }
276
+ // 等待更多数据
277
+ break;
278
+ }
279
+
258
280
  // ===== 标准Modbus RTU协议 =====
259
281
  const frameLen = node.getFrameLength(node.receiveBuffer);
260
- if (frameLen === 0 || node.receiveBuffer.length < frameLen) break;
261
-
262
- const frame = node.receiveBuffer.subarray(0, frameLen);
263
- node.receiveBuffer = node.receiveBuffer.subarray(frameLen);
282
+ if (frameLen > 0 && node.receiveBuffer.length >= frameLen) {
283
+ const frame = node.receiveBuffer.subarray(0, frameLen);
284
+ node.receiveBuffer = node.receiveBuffer.subarray(frameLen);
285
+
286
+ if (node.validateCRC(frame)) {
287
+ node.emit('frame', frame);
288
+ }
289
+ continue;
290
+ }
264
291
 
265
- if (node.validateCRC(frame)) {
266
- node.emit('frame', frame);
292
+ // 无法识别为标准协议,启动超时处理(用于自定义码)
293
+ if (!node._frameTimeout) {
294
+ node._frameTimeout = setTimeout(() => {
295
+ node._frameTimeout = null;
296
+ if (node.receiveBuffer.length > 0) {
297
+ const frame = Buffer.from(node.receiveBuffer);
298
+ const hexStr = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
299
+ node.debug(`[RS485] 自定义帧超时发出: ${hexStr} (${frame.length}字节)`);
300
+ node.receiveBuffer = Buffer.alloc(0);
301
+ node.emit('frame', frame);
302
+ }
303
+ }, 50);
267
304
  }
305
+ break;
268
306
  }
269
307
  };
270
308
 
@@ -18,6 +18,8 @@
18
18
  inputs: 1,
19
19
  outputs: 1,
20
20
  icon: 'font-awesome/fa-cloud-download',
21
+ align: 'left',
22
+ paletteLabel: '云端同步',
21
23
  label: function() {
22
24
  return this.name || '云端同步';
23
25
  },
@@ -15,13 +15,15 @@
15
15
  },
16
16
  inputs: 1,
17
17
  outputs: 1,
18
- icon: 'bridge.png',
18
+ icon: 'font-awesome/fa-cube',
19
+ align: 'left',
20
+ paletteLabel: '设备控制',
19
21
  label: function() {
20
22
  if (this.name) return this.name;
21
23
  if (this.deviceMac && this.channel > 1) {
22
- return 'Symi设备-' + this.channel + '路';
24
+ return '设备控制-' + this.channel + '路';
23
25
  }
24
- return 'Symi设备';
26
+ return '设备控制';
25
27
  },
26
28
  labelStyle: function() {
27
29
  return this.name ? 'node_label_italic' : '';
@@ -15,6 +15,8 @@
15
15
  : `Symi Gateway (${this.serialPort})`);
16
16
  },
17
17
  oneditprepare: function() {
18
+ var node = this;
19
+
18
20
  $('#node-config-input-connectionType').on('change', function() {
19
21
  if ($(this).val() === 'tcp') {
20
22
  $('.tcp-config').show();
@@ -24,6 +26,47 @@
24
26
  $('.serial-config').show();
25
27
  }
26
28
  }).trigger('change');
29
+
30
+ // 串口自动发现
31
+ $('#serial-discover-btn').on('click', function() {
32
+ var $btn = $(this);
33
+ var $select = $('#serial-port-select');
34
+ $btn.prop('disabled', true).text('搜索中...');
35
+ $select.empty().append('<option value="">搜索中...</option>');
36
+
37
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
38
+ $select.empty();
39
+ if (ports && ports.length > 0) {
40
+ $select.append('<option value="">-- 选择串口 --</option>');
41
+ ports.forEach(function(port) {
42
+ var label = port.path;
43
+ if (port.manufacturer) label += ' (' + port.manufacturer + ')';
44
+ var selected = (node.serialPort === port.path) ? 'selected' : '';
45
+ $select.append('<option value="' + port.path + '" ' + selected + '>' + label + '</option>');
46
+ });
47
+ $select.show();
48
+ } else {
49
+ $select.append('<option value="">未发现串口</option>');
50
+ }
51
+ $btn.prop('disabled', false).text('搜索串口');
52
+ }).fail(function() {
53
+ $select.empty().append('<option value="">搜索失败</option>');
54
+ $btn.prop('disabled', false).text('搜索串口');
55
+ });
56
+ });
57
+
58
+ // 串口选择
59
+ $('#serial-port-select').on('change', function() {
60
+ var val = $(this).val();
61
+ if (val) {
62
+ $('#node-config-input-serialPort').val(val);
63
+ }
64
+ });
65
+
66
+ // 初始化时如果已有串口配置,显示在下拉框
67
+ if (node.serialPort) {
68
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
69
+ }
27
70
  }
28
71
  });
29
72
  </script>
@@ -54,7 +97,12 @@
54
97
 
55
98
  <div class="form-row serial-config">
56
99
  <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口路径</label>
57
- <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3">
100
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
101
+ <button type="button" id="serial-discover-btn" class="red-ui-button" style="margin-left:5px;">搜索串口</button>
102
+ </div>
103
+ <div class="form-row serial-config">
104
+ <label>&nbsp;</label>
105
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
58
106
  </div>
59
107
 
60
108
  <div class="form-row serial-config">
@@ -53,6 +53,13 @@ module.exports = function(RED) {
53
53
  this.deviceListComplete = false;
54
54
  this.isQueryingStates = false; // 标记是否正在查询状态
55
55
 
56
+ // 转发deviceManager的device-state-changed事件(用于0x1C fanMode等直接触发的事件)
57
+ this.deviceManager.on('device-state-changed', (eventData) => {
58
+ if (!this.isQueryingStates) {
59
+ this.emit('device-state-changed', eventData);
60
+ }
61
+ });
62
+
56
63
  // 状态事件处理队列 - 确保场景执行后的大量状态更新能被正确处理
57
64
  this.stateEventQueue = [];
58
65
  this.maxQueueSize = 100; // 最大队列大小,防止内存泄漏
@@ -169,7 +176,7 @@ module.exports = function(RED) {
169
176
  // 规则:53 B0 后面紧跟的第一个窗帘帧就是真实用户控制
170
177
  // 注意:必须用时间戳而不是标记,因为队列中可能有旧帧等待处理
171
178
  this.lastB0Time = Date.now();
172
- this.log(`[控制响应] 0xB0: ${frameHex},记录时间戳`)
179
+ this.debug(`[控制响应] 0xB0: ${frameHex},记录时间戳`)
173
180
  } else if (frame.opcode === 0xB4) {
174
181
  // 场景控制响应
175
182
  if (frame.status === 0) {
@@ -403,7 +410,7 @@ module.exports = function(RED) {
403
410
  // 6-8路开关使用0x45,1-4路使用0x02
404
411
  const msgType = (device.channels >= 6) ? 0x45 : 0x02;
405
412
  queryAttrs = [msgType];
406
- this.log(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels}, msgType=0x${msgType.toString(16).toUpperCase()})`);
413
+ this.debug(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels}, msgType=0x${msgType.toString(16).toUpperCase()})`);
407
414
  } else if (device.deviceType === 9) {
408
415
  queryAttrs = [0x02, 0x0E]; // 插卡取电:开关状态、插卡状态
409
416
  } else if (device.deviceType === 4 || device.deviceType === 0x18) {
@@ -487,7 +494,7 @@ module.exports = function(RED) {
487
494
  const frame = this.protocolHandler.buildDeviceControlFrame(device.networkAddress, 0x10, Buffer.from([0x01]));
488
495
  await this.client.sendFrame(frame, 2);
489
496
  await this.sleep(50);
490
- this.log(`已启用设备${device.name}的状态上报`);
497
+ this.debug(`已启用设备${device.name}的状态上报`);
491
498
  } catch(e) {
492
499
  this.error(`启用设备${device.name}状态上报失败: ${e.message}`);
493
500
  }
@@ -675,6 +682,39 @@ module.exports = function(RED) {
675
682
  res.json([]);
676
683
  }
677
684
  });
685
+
686
+ // 串口自动发现API - 兼容不同版本serialport
687
+ RED.httpAdmin.get('/symi-gateway/serial-ports', async function(req, res) {
688
+ try {
689
+ let ports = [];
690
+ try {
691
+ // 尝试新版API (serialport v10+)
692
+ const { SerialPort } = require('serialport');
693
+ ports = await SerialPort.list();
694
+ } catch (e1) {
695
+ try {
696
+ // 尝试旧版API (serialport v9)
697
+ const SerialPort = require('serialport');
698
+ if (typeof SerialPort.list === 'function') {
699
+ ports = await SerialPort.list();
700
+ }
701
+ } catch (e2) {
702
+ // 静默处理
703
+ }
704
+ }
705
+ // 过滤常见串口设备
706
+ const filteredPorts = (ports || []).filter(function(p) {
707
+ const path = (p.path || '').toLowerCase();
708
+ // 排除蓝牙和内部设备
709
+ if (path.includes('bluetooth') || path.includes('bt-')) return false;
710
+ // 包含USB、tty、COM端口
711
+ return path.includes('usb') || path.includes('tty') || path.includes('com') || path.includes('serial');
712
+ });
713
+ res.json(filteredPorts);
714
+ } catch (e) {
715
+ res.json([]);
716
+ }
717
+ });
678
718
  }
679
719
  };
680
720
 
@@ -11,9 +11,10 @@
11
11
  inputs: 1,
12
12
  outputs: 2,
13
13
  outputLabels: ['KNX输出', '调试信息'],
14
- icon: 'bridge.svg',
15
- label: function() { return this.name || 'KNX Bridge'; },
14
+ icon: 'font-awesome/fa-sitemap',
15
+ align: 'left',
16
16
  paletteLabel: 'KNX桥接',
17
+ label: function() { return this.name || 'KNX桥接'; },
17
18
  oneditprepare: function() {
18
19
  const node = this;
19
20
  let mappings = [], devices = [], knxEntities = [];
@@ -463,7 +463,7 @@ module.exports = function(RED) {
463
463
  node.queueCommand = function(cmd) {
464
464
  // 队列大小限制
465
465
  if (node.commandQueue.length >= MAX_QUEUE_SIZE) {
466
- node.warn(`[KNX Bridge] 命令队列已满(${MAX_QUEUE_SIZE}),丢弃最旧命令`);
466
+ node.debug(`[KNX Bridge] 命令队列已满(${MAX_QUEUE_SIZE}),丢弃最旧命令`);
467
467
  node.commandQueue.shift();
468
468
  }
469
469
 
@@ -766,7 +766,7 @@ module.exports = function(RED) {
766
766
  }
767
767
 
768
768
  if (!meshDevice) {
769
- node.warn(`[KNX->Mesh] 未找到Mesh设备: ${meshMac}`);
769
+ node.debug(`[KNX->Mesh] 未找到Mesh设备: ${meshMac}`);
770
770
  return;
771
771
  }
772
772
 
@@ -1032,7 +1032,7 @@ module.exports = function(RED) {
1032
1032
  // 收集所有已映射的唯一设备地址
1033
1033
  const mappedAddresses = new Set();
1034
1034
  for (const mapping of node.mappings) {
1035
- const mac = node.normalizeMac(mapping.meshMac);
1035
+ const mac = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
1036
1036
  const device = node.gateway.deviceManager.getDeviceByMac(mac);
1037
1037
  if (device && device.networkAddress) {
1038
1038
  mappedAddresses.add(device.networkAddress);
@@ -11,9 +11,10 @@
11
11
  inputs: 1,
12
12
  outputs: 2,
13
13
  outputLabels: ['KNX输出', '调试信息'],
14
- icon: 'bridge.svg',
15
- label: function() { return this.name || 'KNX-HA桥接'; },
16
- paletteLabel: 'KNX-HA桥接',
14
+ icon: 'font-awesome/fa-home',
15
+ align: 'left',
16
+ paletteLabel: 'KNX HA桥接',
17
+ label: function() { return this.name || 'KNX HA桥接'; },
17
18
  oneditprepare: function() {
18
19
  const node = this;
19
20
  let mappings = [], haEntities = [], knxEntities = [];
@@ -318,7 +318,7 @@ module.exports = function(RED) {
318
318
  node.recordSyncTime('knx-to-ha', key);
319
319
 
320
320
  if (!node.haServer || !node.haServer.credentials) {
321
- node.warn('[KNX->HA] HA服务器未配置');
321
+ node.debug('[KNX->HA] HA服务器未配置');
322
322
  return;
323
323
  }
324
324
 
@@ -328,7 +328,7 @@ module.exports = function(RED) {
328
328
  const token = node.haServer.credentials.access_token;
329
329
 
330
330
  if (!token) {
331
- node.warn('[KNX->HA] HA访问令牌未配置');
331
+ node.debug('[KNX->HA] HA访问令牌未配置');
332
332
  return;
333
333
  }
334
334
 
@@ -0,0 +1,75 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('symi-mqtt-brand', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ brand: { value: 'hyqw' },
7
+ mqttBroker: { value: 'mqtt://localhost:1883', required: true },
8
+ mqttUsername: { value: '' },
9
+ mqttPassword: { value: '' },
10
+ projectCode: { value: '', required: true },
11
+ deviceSn: { value: '', required: true }
12
+ },
13
+ label: function() {
14
+ if (this.name) return this.name;
15
+ if (this.projectCode && this.deviceSn) {
16
+ return 'HYQW: ' + this.projectCode;
17
+ }
18
+ return '品牌MQTT';
19
+ },
20
+ oneditprepare: function() {
21
+ var node = this;
22
+
23
+ // 品牌切换
24
+ function updateBrandUI() {
25
+ var brand = $('#node-config-input-brand').val();
26
+ if (brand === 'hyqw') {
27
+ $('.brand-hyqw-config').show();
28
+ } else {
29
+ $('.brand-hyqw-config').hide();
30
+ }
31
+ }
32
+ $('#node-config-input-brand').on('change', updateBrandUI);
33
+ updateBrandUI();
34
+ }
35
+ });
36
+ </script>
37
+
38
+ <script type="text/html" data-template-name="symi-mqtt-brand">
39
+ <div class="form-row">
40
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> 名称</label>
41
+ <input type="text" id="node-config-input-name" placeholder="如:花语前湾MQTT">
42
+ </div>
43
+
44
+ <div class="form-row">
45
+ <label for="node-config-input-brand"><i class="fa fa-plug"></i> 品牌协议</label>
46
+ <select id="node-config-input-brand">
47
+ <option value="hyqw">HYQW (花语前湾)</option>
48
+ </select>
49
+ </div>
50
+
51
+ <div class="form-row">
52
+ <label for="node-config-input-mqttBroker"><i class="fa fa-server"></i> MQTT地址</label>
53
+ <input type="text" id="node-config-input-mqttBroker" placeholder="mqtt://192.168.1.100:1883">
54
+ </div>
55
+
56
+ <div class="form-row">
57
+ <label for="node-config-input-mqttUsername"><i class="fa fa-user"></i> 用户名</label>
58
+ <input type="text" id="node-config-input-mqttUsername" placeholder="可选">
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <label for="node-config-input-mqttPassword"><i class="fa fa-lock"></i> 密码</label>
63
+ <input type="password" id="node-config-input-mqttPassword" placeholder="可选">
64
+ </div>
65
+
66
+ <div class="form-row brand-hyqw-config">
67
+ <label for="node-config-input-projectCode"><i class="fa fa-folder"></i> 项目代码</label>
68
+ <input type="text" id="node-config-input-projectCode" placeholder="如: SH-485-V22">
69
+ </div>
70
+
71
+ <div class="form-row brand-hyqw-config">
72
+ <label for="node-config-input-deviceSn"><i class="fa fa-barcode"></i> 设备SN</label>
73
+ <input type="text" id="node-config-input-deviceSn" placeholder="如: FB485V222024110500000377">
74
+ </div>
75
+ </script>