node-red-contrib-symi-mesh 1.2.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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Serial Client for Symi Gateway
3
+ */
4
+
5
+ const EventEmitter = require('events');
6
+ const { ProtocolHandler } = require('./protocol');
7
+
8
+ class SerialClient extends EventEmitter {
9
+ constructor(portPath, baudRate = 115200, logger = console) {
10
+ super();
11
+ this.portPath = portPath;
12
+ this.baudRate = baudRate;
13
+ this.logger = logger;
14
+ this.port = null;
15
+ this.protocolHandler = new ProtocolHandler();
16
+ this.connected = false;
17
+ this.commandQueue = [];
18
+ this.processingQueue = false;
19
+ this.SerialPort = null;
20
+ this.reconnectDelay = 5000;
21
+ this.reconnectTimer = null;
22
+ this.autoReconnect = true;
23
+ }
24
+
25
+ async connect() {
26
+ try {
27
+ if (this.connected) {
28
+ return Promise.resolve();
29
+ }
30
+
31
+ // 清理之前的端口
32
+ if (this.port) {
33
+ try {
34
+ if (this.port.isOpen) {
35
+ this.port.close();
36
+ }
37
+ this.port.removeAllListeners();
38
+ } catch (e) {
39
+ // 忽略清理错误
40
+ }
41
+ this.port = null;
42
+ }
43
+
44
+ if (!this.SerialPort) {
45
+ this.SerialPort = require('serialport').SerialPort;
46
+ }
47
+
48
+ this.port = new this.SerialPort({
49
+ path: this.portPath,
50
+ baudRate: this.baudRate,
51
+ dataBits: 8,
52
+ stopBits: 1,
53
+ parity: 'none'
54
+ });
55
+
56
+ return new Promise((resolve, reject) => {
57
+ let resolved = false;
58
+ let rejected = false;
59
+
60
+ const timeout = setTimeout(() => {
61
+ if (!resolved && !rejected) {
62
+ rejected = true;
63
+ this.logger.error('Serial port connection timeout');
64
+ if (this.port && this.port.isOpen) {
65
+ this.port.close();
66
+ }
67
+ this.handleDisconnect();
68
+ reject(new Error('Serial port connection timeout'));
69
+ }
70
+ }, 10000);
71
+
72
+ this.port.on('open', () => {
73
+ if (!resolved && !rejected) {
74
+ resolved = true;
75
+ clearTimeout(timeout);
76
+ this.connected = true;
77
+ this.logger.log(`Serial port opened: ${this.portPath} at ${this.baudRate} baud`);
78
+ this.emit('connected');
79
+
80
+ if (!this.processingQueue) {
81
+ this.startQueueProcessor();
82
+ }
83
+
84
+ resolve();
85
+ }
86
+ });
87
+
88
+ this.port.on('data', (data) => {
89
+ try {
90
+ const frames = this.protocolHandler.addData(data);
91
+ frames.forEach(frame => {
92
+ this.emit('frame', frame);
93
+ });
94
+ } catch (error) {
95
+ this.logger.error('Error parsing frame:', error);
96
+ }
97
+ });
98
+
99
+ this.port.on('error', (error) => {
100
+ clearTimeout(timeout);
101
+ this.logger.error('Serial port error:', error.message);
102
+ this.emit('error', error);
103
+
104
+ if (!resolved && !rejected) {
105
+ rejected = true;
106
+ this.handleDisconnect();
107
+ reject(error);
108
+ }
109
+ });
110
+
111
+ this.port.on('close', () => {
112
+ clearTimeout(timeout);
113
+ this.logger.log('Serial port closed');
114
+
115
+ if (!resolved && !rejected) {
116
+ rejected = true;
117
+ reject(new Error('Serial port closed during connect'));
118
+ }
119
+
120
+ this.handleDisconnect();
121
+ });
122
+ });
123
+ } catch (error) {
124
+ this.logger.error('Failed to open serial port:', error);
125
+ this.handleDisconnect();
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ handleDisconnect() {
131
+ const wasConnected = this.connected;
132
+ this.connected = false;
133
+
134
+ // 清理端口
135
+ if (this.port) {
136
+ try {
137
+ if (this.port.isOpen) {
138
+ this.port.close();
139
+ }
140
+ this.port.removeAllListeners();
141
+ } catch (e) {
142
+ // 忽略清理错误
143
+ }
144
+ this.port = null;
145
+ }
146
+
147
+ // 触发断连事件
148
+ if (wasConnected) {
149
+ this.emit('disconnected');
150
+ }
151
+
152
+ // 自动重连
153
+ if (this.autoReconnect && !this.reconnectTimer) {
154
+ this.reconnectTimer = setTimeout(() => {
155
+ this.reconnectTimer = null;
156
+ this.logger.log('Attempting to reconnect serial port...');
157
+ this.connect()
158
+ .then(() => {
159
+ this.logger.log('Serial port reconnected successfully');
160
+ })
161
+ .catch((error) => {
162
+ this.logger.error(`Serial port reconnect failed: ${error.message}`);
163
+ // 失败后会自动继续尝试重连(通过handleDisconnect)
164
+ });
165
+ }, this.reconnectDelay);
166
+ }
167
+ }
168
+
169
+ disconnect() {
170
+ this.autoReconnect = false;
171
+
172
+ if (this.reconnectTimer) {
173
+ clearTimeout(this.reconnectTimer);
174
+ this.reconnectTimer = null;
175
+ }
176
+
177
+ return new Promise((resolve) => {
178
+ if (this.port && this.port.isOpen) {
179
+ this.port.close(() => {
180
+ this.connected = false;
181
+ try {
182
+ this.port.removeAllListeners();
183
+ } catch (e) {
184
+ // 忽略错误
185
+ }
186
+ this.port = null;
187
+ this.commandQueue = [];
188
+ this.processingQueue = false;
189
+ resolve();
190
+ });
191
+ } else {
192
+ if (this.port) {
193
+ try {
194
+ this.port.removeAllListeners();
195
+ } catch (e) {
196
+ // 忽略错误
197
+ }
198
+ this.port = null;
199
+ }
200
+ this.commandQueue = [];
201
+ this.processingQueue = false;
202
+ resolve();
203
+ }
204
+ });
205
+ }
206
+
207
+ setAutoReconnect(enabled) {
208
+ this.autoReconnect = enabled;
209
+ }
210
+
211
+ sendFrame(frame, priority = 1) {
212
+ return new Promise((resolve, reject) => {
213
+ if (!this.connected) {
214
+ reject(new Error('Serial port not connected'));
215
+ return;
216
+ }
217
+
218
+ const command = { frame, priority, resolve, reject, retries: 0 };
219
+
220
+ if (priority === 0) {
221
+ this.commandQueue.unshift(command);
222
+ } else {
223
+ this.commandQueue.push(command);
224
+ }
225
+
226
+ if (!this.processingQueue) {
227
+ this.startQueueProcessor();
228
+ }
229
+ });
230
+ }
231
+
232
+ startQueueProcessor() {
233
+ if (this.processingQueue) return;
234
+
235
+ this.processingQueue = true;
236
+ this.processQueue();
237
+ }
238
+
239
+ async processQueue() {
240
+ while (this.commandQueue.length > 0 && this.connected) {
241
+ const command = this.commandQueue.shift();
242
+
243
+ try {
244
+ await this.sendFrameDirect(command.frame);
245
+ command.resolve(true);
246
+ } catch (error) {
247
+ command.retries++;
248
+ if (command.retries < 3) {
249
+ this.commandQueue.unshift(command);
250
+ } else {
251
+ command.reject(error);
252
+ }
253
+ }
254
+
255
+ await this.sleep(50);
256
+ }
257
+
258
+ this.processingQueue = false;
259
+ }
260
+
261
+ sendFrameDirect(frame) {
262
+ return new Promise((resolve, reject) => {
263
+ if (!this.connected || !this.port) {
264
+ reject(new Error('Serial port not connected'));
265
+ return;
266
+ }
267
+
268
+ this.port.write(frame, (error) => {
269
+ if (error) {
270
+ reject(error);
271
+ } else {
272
+ this.port.drain(() => {
273
+ resolve();
274
+ });
275
+ }
276
+ });
277
+ });
278
+ }
279
+
280
+ sleep(ms) {
281
+ return new Promise(resolve => setTimeout(resolve, ms));
282
+ }
283
+ }
284
+
285
+ module.exports = SerialClient;
286
+
@@ -0,0 +1,262 @@
1
+ /**
2
+ * TCP Client for Symi Gateway
3
+ */
4
+
5
+ const net = require('net');
6
+ const EventEmitter = require('events');
7
+ const { ProtocolHandler } = require('./protocol');
8
+
9
+ class TCPClient extends EventEmitter {
10
+ constructor(host, port, logger = console) {
11
+ super();
12
+ this.host = host;
13
+ this.port = port || 4196;
14
+ this.logger = logger;
15
+ this.client = null;
16
+ this.protocolHandler = new ProtocolHandler();
17
+ this.connected = false;
18
+ this.reconnectDelay = 5000;
19
+ this.reconnectTimer = null;
20
+ this.autoReconnect = true;
21
+ this.commandQueue = [];
22
+ this.processingQueue = false;
23
+ }
24
+
25
+ connect() {
26
+ return new Promise((resolve, reject) => {
27
+ if (this.connected) {
28
+ resolve();
29
+ return;
30
+ }
31
+
32
+ // 清理之前的客户端
33
+ if (this.client) {
34
+ try {
35
+ this.client.removeAllListeners();
36
+ this.client.destroy();
37
+ } catch (e) {
38
+ // 忽略清理错误
39
+ }
40
+ }
41
+
42
+ this.client = new net.Socket();
43
+ this.client.setKeepAlive(true, 30000);
44
+
45
+ let resolved = false;
46
+ let rejected = false;
47
+
48
+ const timeout = setTimeout(() => {
49
+ if (!resolved && !rejected) {
50
+ rejected = true;
51
+ if (this.client) {
52
+ this.client.destroy();
53
+ }
54
+ this.logger.error('Connection timeout');
55
+ this.handleDisconnect();
56
+ reject(new Error('Connection timeout'));
57
+ }
58
+ }, 10000);
59
+
60
+ this.client.connect(this.port, this.host, () => {
61
+ if (!resolved && !rejected) {
62
+ resolved = true;
63
+ clearTimeout(timeout);
64
+ this.connected = true;
65
+ this.logger.log(`Connected to gateway at ${this.host}:${this.port}`);
66
+ this.emit('connected');
67
+
68
+ if (!this.processingQueue) {
69
+ this.startQueueProcessor();
70
+ }
71
+
72
+ resolve();
73
+ }
74
+ });
75
+
76
+ this.client.on('data', (data) => {
77
+ try {
78
+ // 添加原始数据日志
79
+ // const dataHex = data.toString('hex').toUpperCase();
80
+ // this.logger.log(`[TCP Raw Data] 收到${data.length}字节: ${dataHex}`);
81
+
82
+ const frames = this.protocolHandler.addData(data);
83
+ // this.logger.log(`[TCP Parse] 解析出${frames.length}个帧`);
84
+
85
+ frames.forEach(frame => {
86
+ this.emit('frame', frame);
87
+ });
88
+ } catch (error) {
89
+ this.logger.error('Error parsing frame:', error);
90
+ this.logger.error('Raw data:', data.toString('hex'));
91
+ }
92
+ });
93
+
94
+ this.client.on('error', (error) => {
95
+ clearTimeout(timeout);
96
+ this.logger.error('TCP client error:', error.message);
97
+
98
+ // 确保错误不会导致uncaught exception
99
+ this.emit('error', error);
100
+
101
+ if (!resolved && !rejected) {
102
+ rejected = true;
103
+ this.handleDisconnect();
104
+ reject(error);
105
+ }
106
+ });
107
+
108
+ this.client.on('close', () => {
109
+ clearTimeout(timeout);
110
+ this.logger.log('Connection closed');
111
+
112
+ if (!resolved && !rejected) {
113
+ rejected = true;
114
+ reject(new Error('Connection closed during connect'));
115
+ }
116
+
117
+ this.handleDisconnect();
118
+ });
119
+ });
120
+ }
121
+
122
+ handleDisconnect() {
123
+ const wasConnected = this.connected;
124
+ this.connected = false;
125
+
126
+ // 清理客户端
127
+ if (this.client) {
128
+ try {
129
+ this.client.removeAllListeners();
130
+ this.client.destroy();
131
+ } catch (e) {
132
+ // 忽略清理错误
133
+ }
134
+ this.client = null;
135
+ }
136
+
137
+ // 触发断连事件
138
+ if (wasConnected) {
139
+ this.emit('disconnected');
140
+ }
141
+
142
+ // 自动重连
143
+ if (this.autoReconnect && !this.reconnectTimer) {
144
+ this.reconnectTimer = setTimeout(() => {
145
+ this.reconnectTimer = null;
146
+ this.logger.log('Attempting to reconnect...');
147
+ this.connect()
148
+ .then(() => {
149
+ this.logger.log('Reconnected successfully');
150
+ })
151
+ .catch((error) => {
152
+ this.logger.error(`Reconnect failed: ${error.message}`);
153
+ // 失败后会自动继续尝试重连(通过handleDisconnect)
154
+ });
155
+ }, this.reconnectDelay);
156
+ }
157
+ }
158
+
159
+ disconnect() {
160
+ this.autoReconnect = false;
161
+
162
+ if (this.reconnectTimer) {
163
+ clearTimeout(this.reconnectTimer);
164
+ this.reconnectTimer = null;
165
+ }
166
+
167
+ if (this.client) {
168
+ try {
169
+ this.client.removeAllListeners();
170
+ this.client.destroy();
171
+ } catch (e) {
172
+ // 忽略错误
173
+ }
174
+ this.client = null;
175
+ }
176
+
177
+ this.connected = false;
178
+ this.commandQueue = [];
179
+ this.processingQueue = false;
180
+ }
181
+
182
+ sendFrame(frame, priority = 1) {
183
+ return new Promise((resolve, reject) => {
184
+ if (!this.connected) {
185
+ reject(new Error('Not connected'));
186
+ return;
187
+ }
188
+
189
+ const command = { frame, priority, resolve, reject, retries: 0 };
190
+
191
+ if (priority === 0) {
192
+ this.commandQueue.unshift(command);
193
+ } else {
194
+ this.commandQueue.push(command);
195
+ }
196
+
197
+ if (!this.processingQueue) {
198
+ this.startQueueProcessor();
199
+ }
200
+ });
201
+ }
202
+
203
+ startQueueProcessor() {
204
+ if (this.processingQueue) return;
205
+
206
+ this.processingQueue = true;
207
+ this.processQueue();
208
+ }
209
+
210
+ async processQueue() {
211
+ while (this.commandQueue.length > 0 && this.connected) {
212
+ const command = this.commandQueue.shift();
213
+
214
+ try {
215
+ await this.sendFrameDirect(command.frame);
216
+ command.resolve(true);
217
+ } catch (error) {
218
+ command.retries++;
219
+ if (command.retries < 3) {
220
+ this.commandQueue.unshift(command);
221
+ } else {
222
+ command.reject(error);
223
+ }
224
+ }
225
+
226
+ await this.sleep(50);
227
+ }
228
+
229
+ this.processingQueue = false;
230
+ }
231
+
232
+ sendFrameDirect(frame) {
233
+ return new Promise((resolve, reject) => {
234
+ if (!this.connected || !this.client) {
235
+ reject(new Error('Not connected'));
236
+ return;
237
+ }
238
+
239
+ // 添加发送日志
240
+ const frameHex = frame.toString('hex').toUpperCase();
241
+ this.logger.log(`[TCP Frame] 发送: ${frameHex}`);
242
+
243
+ this.client.write(frame, (error) => {
244
+ if (error) {
245
+ reject(error);
246
+ } else {
247
+ resolve();
248
+ }
249
+ });
250
+ });
251
+ }
252
+
253
+ sleep(ms) {
254
+ return new Promise(resolve => setTimeout(resolve, ms));
255
+ }
256
+
257
+ setAutoReconnect(enabled) {
258
+ this.autoReconnect = enabled;
259
+ }
260
+ }
261
+
262
+ module.exports = TCPClient;