node-red-contrib-symi-mesh 1.7.2 → 1.7.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
@@ -1291,6 +1291,8 @@ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间
1291
1291
  | **Symi RS485 Bridge** | RS485设备双向同步 | [RS485/Modbus双向同步](#rs485modbus双向同步) |
1292
1292
  | **Symi KNX Bridge** | KNX系统双向同步 | [KNX双向同步](#knx双向同步) |
1293
1293
  | **Symi KNX-HA Bridge** | KNX与HA实体双向同步 | [KNX-HA双向同步](#knx-ha双向同步) |
1294
+ | **Symi MQTT Sync** | 第三方MQTT品牌设备同步 | v1.7.3新增 |
1295
+ | **Symi MQTT Brand** | 品牌MQTT配置节点 | v1.7.3新增 |
1294
1296
  | **RS485 Debug** | RS485总线数据抓包调试 | [RS485调试节点](#rs485调试节点) |
1295
1297
  | **Symi RS485 Sync** | 两种RS485协议双向同步 | [RS485协议同步](#rs485协议同步) |
1296
1298
 
@@ -1314,12 +1316,15 @@ node-red-contrib-symi-mesh/
1314
1316
  │ ├── symi-485-bridge.js/html # RS485桥接节点
1315
1317
  │ ├── symi-485-config.js/html # RS485配置节点
1316
1318
  │ ├── symi-knx-bridge.js/html # KNX桥接节点
1319
+ │ ├── symi-knx-ha-bridge.js/html # KNX-HA桥接节点
1317
1320
  │ ├── symi-rs485-sync.js/html # RS485协议同步节点
1321
+ │ ├── symi-mqtt-sync.js/html # MQTT品牌同步节点
1322
+ │ ├── symi-mqtt-brand.js/html # 品牌MQTT配置节点
1318
1323
  │ └── rs485-debug.js/html # RS485调试节点
1319
1324
  ├── examples/
1320
1325
  │ ├── basic-example.json # 基础示例
1321
- │ ├── knx-sync-example.json # KNX Function节点示例
1322
- │ └── knx-bridge-example.json # KNX桥接节点示例
1326
+ │ ├── knx-sync-example.json # KNX桥接节点示例
1327
+ │ └── rs485-sync-example.json # RS485协议同步示例
1323
1328
  ├── LICENSE
1324
1329
  ├── README.md
1325
1330
  └── package.json
@@ -1457,6 +1462,52 @@ node-red-contrib-symi-mesh/
1457
1462
 
1458
1463
  ## 更新日志
1459
1464
 
1465
+ ### v1.7.4 (2025-12-24)
1466
+ - **串口配置完善**:完整的串口参数配置,解决HassOS环境串口锁定问题
1467
+ - **完整参数**:波特率、数据位(7/8)、停止位(1/2)、校验位(None/Even/Odd)
1468
+ - **串口解锁**:添加`lock: false`参数,避免HassOS环境下"Cannot lock port"错误
1469
+ - **配置持久化**:所有串口参数自动保存,重启后保持
1470
+ - **统一界面**:symi-gateway和symi-485-config使用相同的串口配置界面
1471
+ - **串口兼容性修复**:兼容serialport v9和v10+,确保HassOS环境正常使用串口
1472
+ - `serial-client.js`:动态检测serialport版本
1473
+ - `symi-485-config.js`:兼容v9/v10+ API
1474
+ - `symi-485-bridge.js`:兼容v9/v10+ API
1475
+ - **KNX Bridge窗帘/调光同步优化**:重新设计步进设备同步逻辑,解决双向控制时乱动问题
1476
+ - **控制锁定机制**:谁先发起控制谁锁定,3秒内忽略另一方的反馈
1477
+ - **窗帘设备**:Mesh控制→锁定→忽略KNX反馈;KNX控制→锁定→忽略Mesh反馈
1478
+ - **调光设备**:同样应用控制锁定机制,避免亮度调节过程中的反馈干扰
1479
+ - **停止解锁**:窗帘stopped动作会解除锁定,允许下一次控制
1480
+ - **位置/亮度同步**:只同步最终值,不同步过程中的步进状态
1481
+
1482
+ ### v1.7.3 (2025-12-24)
1483
+ - **MQTT同步节点**:新增`symi-mqtt-sync`节点实现第三方MQTT品牌设备与Mesh设备双向同步
1484
+ - **双MQTT配置节点架构**:
1485
+ - Mesh MQTT:选择`symi-mqtt`配置节点,获取Mesh设备列表
1486
+ - 品牌MQTT:选择`symi-mqtt-brand`配置节点(支持下拉选择+编辑+添加)
1487
+ - **品牌MQTT配置节点**:新增`symi-mqtt-brand`配置节点
1488
+ - 支持配置MQTT服务器地址、用户名、密码
1489
+ - 支持HYQW协议(项目代码、设备SN)
1490
+ - 自动发现品牌MQTT设备实体
1491
+ - 可扩展支持更多品牌协议
1492
+ - **实体映射**:
1493
+ - 左边选择Mesh设备+通道
1494
+ - 右边选择品牌设备+通道(灯具支持多路)
1495
+ - 相同类型实体一对一映射
1496
+ - **设备类型**:灯具(8)、空调(12)、窗帘(14)、地暖(16)、新风(36)
1497
+ - **双向同步**:MQTT↔Mesh实时状态同步,2秒防抖防死循环
1498
+ - **输入输出端口**:支持连接debug节点调试同步数据
1499
+ - **错误日志限流**:网络故障时每60秒最多记录一次错误
1500
+ - **断线自动重连**:5秒重连间隔
1501
+ - **串口支持优化**:
1502
+ - 所有串口节点支持手动输入+搜索选择
1503
+ - 兼容serialport v9/v10+,兼容HassOS环境
1504
+ - 串口错误日志限流,避免长时间故障时日志爆炸
1505
+ - **稳定性优化**:
1506
+ - 错误日志频率限制,长时间网络故障不影响系统性能
1507
+ - 静默处理非关键错误,生产级稳定性
1508
+ - 内存安全,无调试日志,断电断网恢复后正常工作
1509
+ - 缓存队列处理,符合MQTT协议要求
1510
+
1460
1511
  ### v1.7.2 (2025-12-22)
1461
1512
  - **RS485双向同步节点**:新增`symi-rs485-sync`独立节点实现两种不同RS485协议之间的双向同步
1462
1513
  - 支持中弘VRF网关协议(功能码0x31-0x34控制,0x50查询)
@@ -1551,19 +1602,6 @@ node-red-contrib-symi-mesh/
1551
1602
  - 连接knxUltimate节点,无缝集成
1552
1603
  - 适用于已有KNX系统与HA整合的场景
1553
1604
 
1554
- ### v1.6.8 (2025-12-15)
1555
- - **KNX双向同步修复**:彻底修复米家/面板操作KNX不同步的问题
1556
- - 修复0x45消息类型处理:米家/面板操作发送msgType=0x45,不再限制channels>=6
1557
- - 支持1-4路开关2字节参数解析(米家/面板场景触发格式)
1558
- - 场景执行通知(0x11)后自动查询设备状态
1559
- - **syncLock阻塞修复**:移除handleMeshStateChange中syncLock检查
1560
- - 避免队列处理期间丢失状态变化事件
1561
- - 改用per-device时间戳防回环机制
1562
- - **processQueue健壮性**:添加try/finally确保processing标志正确重置
1563
- - **防死循环参数调整**:LOOP_PREVENTION_MS调整为800ms
1564
- - **同时修复RS485桥接**:应用相同syncLock修复
1565
-
1566
-
1567
1605
  ## 许可证
1568
1606
 
1569
1607
  MIT License
@@ -1575,8 +1613,8 @@ Copyright (c) 2025 SYMI 亖米
1575
1613
  ## 关于
1576
1614
 
1577
1615
  **作者**: SYMI 亖米
1578
- **版本**: 1.7.2
1616
+ **版本**: 1.7.4
1579
1617
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1580
- **最后更新**: 2025-12-23
1618
+ **最后更新**: 2025-12-24
1581
1619
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
1582
1620
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -6,10 +6,13 @@ const EventEmitter = require('events');
6
6
  const { ProtocolHandler } = require('./protocol');
7
7
 
8
8
  class SerialClient extends EventEmitter {
9
- constructor(portPath, baudRate = 115200, logger = console) {
9
+ constructor(portPath, baudRate = 115200, logger = console, options = {}) {
10
10
  super();
11
11
  this.portPath = portPath;
12
12
  this.baudRate = baudRate;
13
+ this.dataBits = options.dataBits || 8;
14
+ this.stopBits = options.stopBits || 1;
15
+ this.parity = options.parity || 'none';
13
16
  this.logger = logger;
14
17
  this.port = null;
15
18
  this.protocolHandler = new ProtocolHandler();
@@ -20,6 +23,23 @@ class SerialClient extends EventEmitter {
20
23
  this.reconnectDelay = 5000;
21
24
  this.reconnectTimer = null;
22
25
  this.autoReconnect = true;
26
+ // 错误日志限流
27
+ this._lastErrorLog = 0;
28
+ this._errorLogInterval = 60000; // 60秒最多记录一次错误
29
+ this._reconnectAttempts = 0;
30
+ }
31
+
32
+ // 限流错误日志
33
+ _logErrorThrottled(msg) {
34
+ const now = Date.now();
35
+ if (now - this._lastErrorLog > this._errorLogInterval) {
36
+ this._lastErrorLog = now;
37
+ if (this._reconnectAttempts > 1) {
38
+ this.logger.warn(`${msg} (已尝试${this._reconnectAttempts}次)`);
39
+ } else {
40
+ this.logger.warn(msg);
41
+ }
42
+ }
23
43
  }
24
44
 
25
45
  async connect() {
@@ -42,15 +62,23 @@ class SerialClient extends EventEmitter {
42
62
  }
43
63
 
44
64
  if (!this.SerialPort) {
45
- this.SerialPort = require('serialport').SerialPort;
65
+ // 兼容serialport v9和v10+
66
+ try {
67
+ // v10+ API
68
+ this.SerialPort = require('serialport').SerialPort;
69
+ } catch (e) {
70
+ // v9 API
71
+ this.SerialPort = require('serialport');
72
+ }
46
73
  }
47
74
 
48
75
  this.port = new this.SerialPort({
49
76
  path: this.portPath,
50
77
  baudRate: this.baudRate,
51
- dataBits: 8,
52
- stopBits: 1,
53
- parity: 'none'
78
+ dataBits: this.dataBits,
79
+ stopBits: this.stopBits,
80
+ parity: this.parity,
81
+ lock: false // 不锁定串口,避免HassOS环境下的锁定问题
54
82
  });
55
83
 
56
84
  return new Promise((resolve, reject) => {
@@ -98,7 +126,7 @@ class SerialClient extends EventEmitter {
98
126
 
99
127
  this.port.on('error', (error) => {
100
128
  clearTimeout(timeout);
101
- this.logger.error('Serial port error:', error.message);
129
+ this._logErrorThrottled(`串口错误: ${error.message}`);
102
130
  this.emit('error', error);
103
131
 
104
132
  if (!resolved && !rejected) {
@@ -153,13 +181,15 @@ class SerialClient extends EventEmitter {
153
181
  if (this.autoReconnect && !this.reconnectTimer) {
154
182
  this.reconnectTimer = setTimeout(() => {
155
183
  this.reconnectTimer = null;
156
- this.logger.log('Attempting to reconnect serial port...');
184
+ this._reconnectAttempts++;
157
185
  this.connect()
158
186
  .then(() => {
159
- this.logger.log('Serial port reconnected successfully');
187
+ this._reconnectAttempts = 0;
188
+ this._lastErrorLog = 0; // 重置错误日志限流
189
+ this.logger.log(`串口重连成功: ${this.portPath}`);
160
190
  })
161
191
  .catch((error) => {
162
- this.logger.error(`Serial port reconnect failed: ${error.message}`);
192
+ this._logErrorThrottled(`串口重连失败: ${error.message}`);
163
193
  // 失败后会自动继续尝试重连(通过handleDisconnect)
164
194
  });
165
195
  }, this.reconnectDelay);
@@ -4,7 +4,13 @@
4
4
  * 事件驱动架构,命令队列顺序处理
5
5
  */
6
6
 
7
- const { SerialPort } = require('serialport');
7
+ // 兼容serialport v9和v10+
8
+ let SerialPort;
9
+ try {
10
+ SerialPort = require('serialport').SerialPort;
11
+ } catch (e) {
12
+ SerialPort = require('serialport');
13
+ }
8
14
 
9
15
  module.exports = function(RED) {
10
16
 
@@ -8,6 +8,8 @@
8
8
  port: { value: 502 },
9
9
  serialPort: { value: '' },
10
10
  baudRate: { value: 9600 },
11
+ dataBits: { value: 8 },
12
+ stopBits: { value: 1 },
11
13
  parity: { value: 'none' }
12
14
  },
13
15
  label: function() {
@@ -29,24 +31,45 @@
29
31
  }
30
32
  }).trigger('change');
31
33
 
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
- });
34
+ // 串口搜索
35
+ $('#btn-refresh-ports').on('click', function() {
36
+ var $btn = $(this);
37
+ var $select = $('#serial-port-select');
38
+ $btn.prop('disabled', true);
39
+ $select.empty().append('<option value="">搜索中...</option>').show();
40
+
41
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
42
+ $select.empty();
43
+ if (ports && ports.length > 0) {
44
+ $select.append('<option value="">-- 选择串口 --</option>');
45
+ ports.forEach(function(p) {
46
+ var label = p.path;
47
+ if (p.manufacturer) label += ' (' + p.manufacturer + ')';
48
+ var sel = (p.path === node.serialPort) ? ' selected' : '';
49
+ $select.append('<option value="' + p.path + '"' + sel + '>' + label + '</option>');
50
+ });
51
+ } else {
52
+ $select.append('<option value="">未发现串口</option>');
53
+ }
54
+ $btn.prop('disabled', false);
55
+ }).fail(function() {
56
+ $select.empty().append('<option value="">搜索失败</option>');
57
+ $btn.prop('disabled', false);
45
58
  });
59
+ });
60
+
61
+ // 串口选择
62
+ $('#serial-port-select').on('change', function() {
63
+ var val = $(this).val();
64
+ if (val) {
65
+ $('#node-config-input-serialPort').val(val);
66
+ }
67
+ });
68
+
69
+ // 初始化时如果已有串口配置,显示在下拉框
70
+ if (node.serialPort) {
71
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
46
72
  }
47
-
48
- $('#btn-refresh-ports').on('click', loadSerialPorts);
49
- loadSerialPorts();
50
73
  }
51
74
  });
52
75
  </script>
@@ -67,11 +90,13 @@
67
90
 
68
91
  <div class="serial-config">
69
92
  <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>
93
+ <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口路径</label>
94
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
95
+ <button type="button" id="btn-refresh-ports" class="red-ui-button" style="margin-left:5px">搜索串口</button>
96
+ </div>
97
+ <div class="form-row">
98
+ <label>&nbsp;</label>
99
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
75
100
  </div>
76
101
 
77
102
  <div class="form-row">
@@ -85,6 +110,22 @@
85
110
  </select>
86
111
  </div>
87
112
 
113
+ <div class="form-row">
114
+ <label for="node-config-input-dataBits"><i class="fa fa-bars"></i> 数据位</label>
115
+ <select id="node-config-input-dataBits">
116
+ <option value="7">7</option>
117
+ <option value="8">8</option>
118
+ </select>
119
+ </div>
120
+
121
+ <div class="form-row">
122
+ <label for="node-config-input-stopBits"><i class="fa fa-stop"></i> 停止位</label>
123
+ <select id="node-config-input-stopBits">
124
+ <option value="1">1</option>
125
+ <option value="2">2</option>
126
+ </select>
127
+ </div>
128
+
88
129
  <div class="form-row">
89
130
  <label for="node-config-input-parity"><i class="fa fa-check-square"></i> 校验位</label>
90
131
  <select id="node-config-input-parity">
@@ -3,7 +3,13 @@
3
3
  * 使用与Mesh网关相同的技术栈(SerialPort/net)
4
4
  */
5
5
 
6
- const { SerialPort } = require('serialport');
6
+ // 兼容serialport v9和v10+
7
+ let SerialPort;
8
+ try {
9
+ SerialPort = require('serialport').SerialPort;
10
+ } catch (e) {
11
+ SerialPort = require('serialport');
12
+ }
7
13
  const net = require('net');
8
14
 
9
15
  // 全局禁用 Happy Eyeballs 算法,防止 AggregateError 导致 Node-RED 崩溃
@@ -50,6 +56,8 @@ module.exports = function(RED) {
50
56
  node.port = parseInt(config.port) || 502;
51
57
  node.serialPort = config.serialPort || '';
52
58
  node.baudRate = parseInt(config.baudRate) || 9600;
59
+ node.dataBits = parseInt(config.dataBits) || 8;
60
+ node.stopBits = parseInt(config.stopBits) || 1;
53
61
  node.parity = config.parity || 'none';
54
62
 
55
63
  // 连接状态
@@ -93,10 +101,11 @@ module.exports = function(RED) {
93
101
  node.client = new SerialPort({
94
102
  path: node.serialPort,
95
103
  baudRate: node.baudRate,
96
- dataBits: 8,
97
- stopBits: 1,
104
+ dataBits: node.dataBits,
105
+ stopBits: node.stopBits,
98
106
  parity: node.parity,
99
- autoOpen: false
107
+ autoOpen: false,
108
+ lock: false // 不锁定串口,避免HassOS环境下的锁定问题
100
109
  });
101
110
 
102
111
  node.client.on('open', () => {
@@ -7,7 +7,10 @@
7
7
  host: { value: '' },
8
8
  port: { value: 4196 },
9
9
  serialPort: { value: '' },
10
- baudRate: { value: 115200 }
10
+ baudRate: { value: 115200 },
11
+ dataBits: { value: 8 },
12
+ stopBits: { value: 1 },
13
+ parity: { value: 'none' }
11
14
  },
12
15
  label: function() {
13
16
  return this.name || (this.connectionType === 'tcp'
@@ -15,6 +18,8 @@
15
18
  : `Symi Gateway (${this.serialPort})`);
16
19
  },
17
20
  oneditprepare: function() {
21
+ var node = this;
22
+
18
23
  $('#node-config-input-connectionType').on('change', function() {
19
24
  if ($(this).val() === 'tcp') {
20
25
  $('.tcp-config').show();
@@ -24,6 +29,47 @@
24
29
  $('.serial-config').show();
25
30
  }
26
31
  }).trigger('change');
32
+
33
+ // 串口自动发现
34
+ $('#serial-discover-btn').on('click', function() {
35
+ var $btn = $(this);
36
+ var $select = $('#serial-port-select');
37
+ $btn.prop('disabled', true).text('搜索中...');
38
+ $select.empty().append('<option value="">搜索中...</option>');
39
+
40
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
41
+ $select.empty();
42
+ if (ports && ports.length > 0) {
43
+ $select.append('<option value="">-- 选择串口 --</option>');
44
+ ports.forEach(function(port) {
45
+ var label = port.path;
46
+ if (port.manufacturer) label += ' (' + port.manufacturer + ')';
47
+ var selected = (node.serialPort === port.path) ? 'selected' : '';
48
+ $select.append('<option value="' + port.path + '" ' + selected + '>' + label + '</option>');
49
+ });
50
+ $select.show();
51
+ } else {
52
+ $select.append('<option value="">未发现串口</option>');
53
+ }
54
+ $btn.prop('disabled', false).text('搜索串口');
55
+ }).fail(function() {
56
+ $select.empty().append('<option value="">搜索失败</option>');
57
+ $btn.prop('disabled', false).text('搜索串口');
58
+ });
59
+ });
60
+
61
+ // 串口选择
62
+ $('#serial-port-select').on('change', function() {
63
+ var val = $(this).val();
64
+ if (val) {
65
+ $('#node-config-input-serialPort').val(val);
66
+ }
67
+ });
68
+
69
+ // 初始化时如果已有串口配置,显示在下拉框
70
+ if (node.serialPort) {
71
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
72
+ }
27
73
  }
28
74
  });
29
75
  </script>
@@ -54,12 +100,48 @@
54
100
 
55
101
  <div class="form-row serial-config">
56
102
  <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">
103
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
104
+ <button type="button" id="serial-discover-btn" class="red-ui-button" style="margin-left:5px;">搜索串口</button>
105
+ </div>
106
+ <div class="form-row serial-config">
107
+ <label>&nbsp;</label>
108
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
58
109
  </div>
59
110
 
60
111
  <div class="form-row serial-config">
61
112
  <label for="node-config-input-baudRate"><i class="fa fa-tachometer"></i> 波特率</label>
62
- <input type="number" id="node-config-input-baudRate" placeholder="115200">
113
+ <select id="node-config-input-baudRate">
114
+ <option value="9600">9600</option>
115
+ <option value="19200">19200</option>
116
+ <option value="38400">38400</option>
117
+ <option value="57600">57600</option>
118
+ <option value="115200">115200</option>
119
+ </select>
120
+ </div>
121
+
122
+ <div class="form-row serial-config">
123
+ <label for="node-config-input-dataBits"><i class="fa fa-bars"></i> 数据位</label>
124
+ <select id="node-config-input-dataBits">
125
+ <option value="7">7</option>
126
+ <option value="8">8</option>
127
+ </select>
128
+ </div>
129
+
130
+ <div class="form-row serial-config">
131
+ <label for="node-config-input-stopBits"><i class="fa fa-stop"></i> 停止位</label>
132
+ <select id="node-config-input-stopBits">
133
+ <option value="1">1</option>
134
+ <option value="2">2</option>
135
+ </select>
136
+ </div>
137
+
138
+ <div class="form-row serial-config">
139
+ <label for="node-config-input-parity"><i class="fa fa-check-square"></i> 校验位</label>
140
+ <select id="node-config-input-parity">
141
+ <option value="none">无 (None)</option>
142
+ <option value="even">偶校验 (Even)</option>
143
+ <option value="odd">奇校验 (Odd)</option>
144
+ </select>
63
145
  </div>
64
146
  </script>
65
147
 
@@ -45,6 +45,9 @@ module.exports = function(RED) {
45
45
  this.port = parseInt(config.port) || 4196;
46
46
  this.serialPort = config.serialPort;
47
47
  this.baudRate = parseInt(config.baudRate) || 115200;
48
+ this.dataBits = parseInt(config.dataBits) || 8;
49
+ this.stopBits = parseInt(config.stopBits) || 1;
50
+ this.parity = config.parity || 'none';
48
51
 
49
52
  this.client = null;
50
53
  this.deviceManager = new DeviceManager(this.context(), this, this.id);
@@ -83,7 +86,11 @@ module.exports = function(RED) {
83
86
  if (this.connectionType === 'tcp') {
84
87
  this.client = new TCPClient(this.host, this.port, this);
85
88
  } else {
86
- this.client = new SerialClient(this.serialPort, this.baudRate, this);
89
+ this.client = new SerialClient(this.serialPort, this.baudRate, this, {
90
+ dataBits: this.dataBits,
91
+ stopBits: this.stopBits,
92
+ parity: this.parity
93
+ });
87
94
  }
88
95
 
89
96
  this.client.on('connected', () => {
@@ -682,6 +689,39 @@ module.exports = function(RED) {
682
689
  res.json([]);
683
690
  }
684
691
  });
692
+
693
+ // 串口自动发现API - 兼容不同版本serialport
694
+ RED.httpAdmin.get('/symi-gateway/serial-ports', async function(req, res) {
695
+ try {
696
+ let ports = [];
697
+ try {
698
+ // 尝试新版API (serialport v10+)
699
+ const { SerialPort } = require('serialport');
700
+ ports = await SerialPort.list();
701
+ } catch (e1) {
702
+ try {
703
+ // 尝试旧版API (serialport v9)
704
+ const SerialPort = require('serialport');
705
+ if (typeof SerialPort.list === 'function') {
706
+ ports = await SerialPort.list();
707
+ }
708
+ } catch (e2) {
709
+ // 静默处理
710
+ }
711
+ }
712
+ // 过滤常见串口设备
713
+ const filteredPorts = (ports || []).filter(function(p) {
714
+ const path = (p.path || '').toLowerCase();
715
+ // 排除蓝牙和内部设备
716
+ if (path.includes('bluetooth') || path.includes('bt-')) return false;
717
+ // 包含USB、tty、COM端口
718
+ return path.includes('usb') || path.includes('tty') || path.includes('com') || path.includes('serial');
719
+ });
720
+ res.json(filteredPorts);
721
+ } catch (e) {
722
+ res.json([]);
723
+ }
724
+ });
685
725
  }
686
726
  };
687
727