ellipsis-com 0.0.1

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,262 @@
1
+ import { SerialPort } from 'serialport'
2
+ import { ComType } from './ComType'
3
+ import { ComPort } from './ComPort'
4
+
5
+ // TODO: Adopt a propper logging system.
6
+ let debug: (...args: any[]) => void
7
+ export function enableDebug(enable = true) {
8
+ if (enable) {
9
+ debug = console.log;
10
+ } else {
11
+ debug = () => { };
12
+ }
13
+ }
14
+ enableDebug(false)
15
+
16
+ /**
17
+ * Maintains a list of ports and types, and handles scanning for new ports and matching them to types.
18
+ */
19
+ export class ComManager {
20
+ ports: ComPort[] = []
21
+ types: ComType[] = []
22
+ refreshInterval = 60000 // 60 seconds
23
+ defaultTimeout: number | undefined = undefined // assigned to ports on creation
24
+
25
+ private refreshIntervalHandle: NodeJS.Timeout | null = null
26
+
27
+ constructor() {}
28
+
29
+ async init(types: ComType[] = []) {
30
+ this.types = types
31
+
32
+ try {
33
+ await this.scanPorts()
34
+ debug("ComManager initialized.")
35
+ } catch( err) {
36
+ console.error("Error initializing ComManager: ", err)
37
+ setTimeout(() => this.init(), 1000) // retry
38
+ }
39
+
40
+ this.scheduleRefresh()
41
+ }
42
+
43
+ private scheduleRefresh() {
44
+ // Clear any current job:
45
+ if(this.refreshIntervalHandle) {
46
+ clearInterval(this.refreshIntervalHandle)
47
+ }
48
+
49
+ // A negative interval disables refreshing:
50
+ if(this.refreshInterval < 0) {
51
+ debug('ComManager auto-refresh disabled.')
52
+ return
53
+ }
54
+
55
+ // Schedule a new job:
56
+ this.refreshIntervalHandle = setTimeout(async () => {
57
+ debug('Rescanning com ports...')
58
+ try {
59
+ this.scanPorts()
60
+ } catch(err) {
61
+ console.error("Error scanning ports: ", err)
62
+ } finally {
63
+ this.scheduleRefresh() // reschedule the next refresh
64
+ }
65
+ }, this.refreshInterval)
66
+ }
67
+
68
+ getComPort(typeName: string, index_name: number | string): ComPort {
69
+ const typePorts = this.ports.filter(p => p.type?.name === typeName)
70
+
71
+ if(typeof index_name === 'string') {
72
+ const port = typePorts.find(p => p.name === index_name)
73
+ if(!port) {
74
+ throw new Error(`Port with name ${index_name} not found for type ${typeName}.`)
75
+ }
76
+ return port
77
+ }
78
+
79
+ const index = index_name as number
80
+
81
+ // Make sure the port exists and has a known type:
82
+ if(index >= typePorts.length) {
83
+ throw new Error(`Port with index ${index} not found. ${typePorts.length} ports found of type ${typeName}.`) // TODO: Create custom not found error class.
84
+ }
85
+
86
+ return typePorts[index]
87
+ }
88
+
89
+ async scanPorts(): Promise<void> {
90
+ debug(`Scanning serial ports...`)
91
+ try {
92
+ const ports = await SerialPort.list()
93
+ await Promise.all(ports.map(async p => {
94
+ // Send hello message to existing ports:
95
+ const existingPort = this.ports.find(existing => existing.path === p.path)
96
+ if(existingPort && existingPort.type) {
97
+ try {
98
+ debug(`Sending hello message to port ${existingPort.path} of type ${existingPort.type?.name}...`);
99
+ await existingPort.send('init')
100
+ debug(`Response received from port ${existingPort.path}.`);
101
+ } catch(err) {
102
+ debug(`Port ${existingPort.path} of type ${existingPort.type?.name} is not responding.`)
103
+
104
+ // Close the port if open:
105
+ if(existingPort.state !== 'closed') {
106
+ await existingPort.close()
107
+ }
108
+ }
109
+
110
+ return
111
+ }
112
+
113
+ // Remove existing ports of unknown type so that we can try again:
114
+ if(existingPort && !existingPort.type) {
115
+ debug(`Removing existing port ${existingPort.path} of unknown type for re-detection.`)
116
+ this.ports = this.ports.filter(p => p.path !== existingPort.path)
117
+ }
118
+
119
+ // Add new port:
120
+ const port = new ComPort(p.path)
121
+ if(this.defaultTimeout) {
122
+ port.timeout = this.defaultTimeout
123
+ }
124
+
125
+ port.path = p.path
126
+ port.manufacturer = p.manufacturer
127
+ port.serialNumber = p.serialNumber
128
+ port.pnpId = p.pnpId
129
+ port.locationId = p.locationId
130
+ port.productId = p.productId
131
+ port.vendorId = p.vendorId
132
+
133
+ this.ports.push(port)
134
+
135
+ // Check if the port supports one of the known com types:
136
+ for(let type of this.types) {
137
+ try {
138
+ // Try to set the type:
139
+ if(!await port.setType(type)) {
140
+ continue
141
+ }
142
+
143
+ // If the type was set without error, then the port
144
+ // supports this type:
145
+ debug(`Com port of type ${type.name} found at path ${port.path}`)
146
+
147
+ // Open the port for background use:
148
+ await port.connect()
149
+
150
+ break
151
+ } catch(err) {
152
+ debug(`Failed to add type to port: ${port.toString()}`)
153
+ console.error(err)
154
+ continue
155
+ }
156
+ }
157
+ }))
158
+ debug(`Com ports: \n\t${this.ports.map(p => p.toString()).join(',\n\t')}`);
159
+ } catch (error: any) {
160
+ console.error(`Error scanning serial ports: ${error.message}`)
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Load the com types from the OpenAPI document.
166
+ * TODO: This method needs to be moved into the bb-com-extension.
167
+ * @returns
168
+ */
169
+ // loadTypes() {
170
+ // if(!this.openapiDoc['x-bb-com-types']) {
171
+ // debug("No com types found in OpenAPI document.")
172
+ // return
173
+ // }
174
+
175
+ // // Iterate over all com types in the OpenAPI document:
176
+ // Object.keys(this.openapiDoc['x-bb-com-types']).forEach( (name: string) => {
177
+ // const comType = this.openapiDoc['x-bb-com-types'][name]
178
+
179
+ // // Ensure the baud rate is a number:
180
+ // if(typeof comType.baud === 'string') {
181
+ // comType.baud = parseInt(comType.baud)
182
+ // }
183
+
184
+ // // Extract all macros that specify this com type by name:
185
+ // const macros = {} as {[method: string]: ComMacro[]}
186
+ // Object.values(this.openapiDoc.paths).forEach( (path: any) => {
187
+ // Object.keys(path)
188
+ // .filter(method => ['get', 'post', 'put', 'patch', 'delete'].includes(method))
189
+ // .filter(method => path[method]['x-bb-com-macro'])
190
+ // .forEach(method => {
191
+ // if(!path[method]['x-bb-com-macro'].commands ||
192
+ // !path[method]['x-bb-com-macro'].responses ||
193
+ // path[method]['x-bb-com-macro'].commands.length !== path[method]['x-bb-com-macro'].responses.length)
194
+ // {
195
+ // throw new Error('x-bb-com-macro commands and responses must be arrays of the same length in openapi.json at path '+path+'/'+method+'.')
196
+ // }
197
+ // if(!path[method].operationId) {
198
+ // throw new Error(`OperationId missing for method ${method} at path ${path} in openapi.json.`)
199
+ // }
200
+ // macros[path[method].operationId] = (path[method]['x-bb-com-macro'].commands as string[]).map((c, i) => (
201
+ // new ComMacro(c, new RegExp(path[method]['x-bb-com-macro'].responses[i].replace(/^\\/|\\/$/g, ''), 'gm'))
202
+ // ))
203
+ // })
204
+ // })
205
+
206
+ // // Init macro:
207
+ // if(!comType.initCommands || comType.initCommands.length === 0) {
208
+ // throw new Error(`Com type '${name}' is missing init commands.`)
209
+ // }
210
+ // if(!comType.initResponses || comType.initResponses.length === 0) {
211
+ // throw new Error(`Com type '${name}' is missing init responses.`)
212
+ // }
213
+ // if(comType.initCommands.length !== comType.initResponses.length) {
214
+ // throw new Error(`Com type '${name}' init commands and responses must be arrays of the same length.`)
215
+ // }
216
+ // macros.init = []
217
+ // comType.initCommands.forEach((cmd: string, i: number) => {
218
+ // // Extract flags and strip slashes if present:
219
+ // let response = comType.initResponses[i] as string
220
+ // let flags = ''
221
+ // if(response.startsWith('/')) {
222
+ // const lastSlash = response.lastIndexOf('/')
223
+ // if(lastSlash < response.length - 1) {
224
+ // flags = response.substring(lastSlash + 1)
225
+ // }
226
+ // response = response.substring(1, lastSlash)
227
+ // }
228
+
229
+ // // Add the macro:
230
+ // macros.init.push(
231
+ // new ComMacro(
232
+ // cmd,
233
+ // new RegExp(response, flags)
234
+ // )
235
+ // )
236
+ // })
237
+
238
+ // // Add the com type:
239
+ // this.types.push(new ComType(
240
+ // name,
241
+ // comType.baud,
242
+ // macros as {[method: string]: ComMacro[], init: ComMacro[]}
243
+ // ))
244
+ // debug(`Loaded com type ${comType.toString()}`);
245
+ // })
246
+ // }
247
+
248
+ async list(type: string, operationId: string, params: {[key: string]: any}): Promise<string[][]> {
249
+ const results: string[][] = []
250
+ const ports = this.ports.filter(p => p.type?.name === type)
251
+ for(let index = 0; index < ports.length; index++) {
252
+ results.push(await this.send(type, index, operationId, params))
253
+ }
254
+ return results
255
+ }
256
+
257
+ async send(type: string, index_name: number|string, operationId: string, params?: {[key: string]: any}): Promise<string[]> {
258
+ const port = this.getComPort(type, index_name)
259
+ const responses = await port.send(operationId, params)
260
+ return responses
261
+ }
262
+ }
package/src/ComPort.ts ADDED
@@ -0,0 +1,422 @@
1
+ import { SerialPort } from 'serialport';
2
+ import { ComMacro } from './ComMacro';
3
+ import { ComType } from './ComType';
4
+
5
+ // TODO: Adopt a propper logging system.
6
+ let debug: (...args: any[]) => void
7
+ export function enableDebug(enable = true) {
8
+ if (enable) {
9
+ debug = console.log;
10
+ } else {
11
+ debug = () => { };
12
+ }
13
+ }
14
+ enableDebug(false)
15
+
16
+ /**
17
+ * Represents a serial communication port. This class is responsible for managing the connection to the port, sending commands, and reading responses. It also
18
+ * maintains the state of the port (e.g. whether it is currently busy or available)
19
+ * and handles incoming data and errors.
20
+ *
21
+ * The port will be busy while waiting for a response to a sent command, and will be available for new commands once the response is received or an error occurs.
22
+ * While waiting for a new command, the port will be in the background state, allowing it to log incoming data without blocking.
23
+ * If an error occurs while waiting for a response, the port will return to the background state and log the error.
24
+ *
25
+ * Valid states: busy, closed, background, connecting.
26
+ */
27
+ export class ComPort {
28
+ private port: SerialPort | null = null
29
+ private readMatch?: RegExp
30
+ private readComplete = false
31
+ private buffer: string = '' // TODO replace with Buffer
32
+
33
+ backgroundBuffer: string[] = [];
34
+
35
+ name: string | null = null
36
+ state: 'busy' | 'closed' | 'background' | 'connecting' = 'closed'
37
+ type: ComType | null = null
38
+ lastError: string | null = null
39
+ timeout: number = 5000 // TODO: Allow setting this at the service level and at the method level.
40
+ lineEnding: string = '\n' // TODO: Allow setting this at the service level and at the method level.
41
+
42
+ // General port info:
43
+ manufacturer?: string
44
+ serialNumber?: string
45
+ pnpId?: string
46
+ locationId?: string
47
+ productId?: string
48
+ vendorId?: string
49
+
50
+ constructor(public path: string) { }
51
+
52
+ toString() {
53
+ return `ComPort(path=${this.path}, state=${this.state}${this.type ? ', type=' : ''}${this.type ? this.type?.name : ''}${this.lastError ? ', lastError=' : ''}${this.lastError ? this.lastError : ''})`;
54
+ }
55
+
56
+ async setType(type: ComType): Promise<boolean> {
57
+ try {
58
+ await this.connect(type.baud); // throws an error if connection fails
59
+ await sleep(type.startupDelay); // wait for device to startup if needed
60
+ await this.send(type.macros.init);
61
+ if (this.readComplete) {
62
+ this.type = type;
63
+ return true;
64
+ } else {
65
+ return false;
66
+ }
67
+ } finally {
68
+ await this.close();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Opens the serial port with either the given baud rate or the baud rate
74
+ * set in the ComPort's type. Once successfully open the port will be in
75
+ * the background state and may be used to write and read.
76
+ * @param baud The Baud rate.
77
+ * @returns A Promise.
78
+ */
79
+ async connect(baud?: number): Promise<void> {
80
+ if (!baud && !this.type) {
81
+ throw new Error('No baud rate set while trying to connect. Either pass the baud rate to the connect function or set a ComType first.');
82
+ }
83
+ if (!baud) {
84
+ baud = this.type!.baud;
85
+ }
86
+ this.state = 'connecting';
87
+
88
+ this.port = new SerialPort({
89
+ path: this.path,
90
+ baudRate: baud,
91
+ autoOpen: false
92
+ });
93
+
94
+ this.port.on('error', this.receiveError.bind(this));
95
+ this.port.on('data', this.receiveData.bind(this));
96
+
97
+ return new Promise<void>((resolve, reject) => {
98
+ this.port!.open((err: Error | null) => {
99
+ if (err) {
100
+ this.state = 'closed';
101
+ this.lastError = err.message;
102
+ reject(err);
103
+ return;
104
+ }
105
+ // this.resetResponse()
106
+ // Successfully opened:
107
+ debug(`Connected to com port ${this.path}.`);
108
+ this.state = 'background';
109
+ resolve();
110
+ });
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Read from the serial port until either the given pattern is
116
+ * matched in the read data or the ComPort's timeout is reached.
117
+ * @param match The RegExp to match against read data.
118
+ * @returns The read data.
119
+ */
120
+ async read(match: RegExp): Promise<string> {
121
+ let time = Date.now()
122
+ this.readMatch = match
123
+ this.buffer = ''
124
+ this.lastError = null
125
+ this.readComplete = false
126
+
127
+ return new Promise<string>((resolve, reject) => {
128
+ const checkCompletion = () => {
129
+ if (this.readComplete) {
130
+ resolve(this.buffer.trim())
131
+ } else if (Date.now() - time > this.timeout) {
132
+ reject(new Error(`Timeout after ${this.timeout}ms while reading from port ${this.path}.`)) // TODO: Create custom timeout error class.
133
+ } else if (this.lastError) {
134
+ reject(new Error(this.lastError))
135
+ } else {
136
+ setTimeout(() => {
137
+ checkCompletion()
138
+ }, 100);
139
+ }
140
+ };
141
+ checkCompletion()
142
+ })
143
+ }
144
+
145
+ /**
146
+ * Writes the given command string to the serial port.
147
+ * @param command The command to write.
148
+ * @returns A Promise.
149
+ */
150
+ async write(command: string): Promise<void> {
151
+ if (!this.port || !this.port.isOpen) {
152
+ throw new Error('Port not open. Please open the port via the connect function before attempting to write.');
153
+ }
154
+
155
+ // Asynchronously write data to the port:
156
+ return new Promise((resolve, reject) => {
157
+ this.port!.write(command + this.lineEnding, (err) => {
158
+ if (err) {
159
+ this.lastError = err.message;
160
+ reject(err);
161
+ return;
162
+ }
163
+ resolve();
164
+ });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Closes the port. If successfully closed, the port's state will
170
+ * be closed.
171
+ * @returns A Promise.
172
+ */
173
+ async close() {
174
+ return new Promise<void>((resolve, reject) => {
175
+ this.port?.close((err) => {
176
+ if (!this.port!.isOpen) {
177
+ this.port = null;
178
+ this.state = 'closed';
179
+ }
180
+ if (err) {
181
+ reject(err);
182
+ } else {
183
+ resolve();
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Attempts to set the port state to busy.
191
+ * If the port is currently closed, it will attempt to connect.
192
+ * If the port is currently connecting, it will wait until the connection is complete.
193
+ * If the port is currently busy, it will wait until the port becomes available.
194
+ * If the port does not become available within a certain time frame, an error will be thrown.
195
+ * @returns
196
+ */
197
+ async lockPort(): Promise<void> {
198
+ // Already available:
199
+ if (this.state === 'background') {
200
+ this.state = 'busy'
201
+ return;
202
+ }
203
+
204
+ // If port is not connected then connect:
205
+ if (this.state === 'closed') {
206
+ debug(`Port ${this.path} is closed - connecting...`);
207
+ await this.connect();
208
+ }
209
+
210
+ // If currently connecting, first wait until connected:
211
+ let attempt = 0;
212
+ while (this.state === 'connecting' && attempt < 10) {
213
+ await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
214
+ attempt++;
215
+ }
216
+ if (this.state === 'connecting') {
217
+ throw new Error(`Timeout while waiting for connection to complete.`);
218
+ }
219
+ debug(`Port ${this.path} connected.`);
220
+
221
+ // If busy, wait until available:
222
+ const MAX_ATTEMPTS = 10;
223
+ attempt = 0;
224
+ while (this.state === 'busy' && attempt < MAX_ATTEMPTS) {
225
+ debug(`Port ${this.path} is busy - waiting...`);
226
+ await new Promise<void>(resolve => setTimeout(() => resolve(), 500));
227
+ attempt++;
228
+ }
229
+ if (this.state === 'busy') {
230
+ throw new Error(`Timeout while waiting for port to become available.`);
231
+ }
232
+
233
+ this.state = 'busy'
234
+ debug(`Port ${this.path} is now available.`);
235
+ }
236
+
237
+ /**
238
+ * Executes the macros for the given method while the responses are matched.
239
+ * After execution the com port's state will be 'background'.
240
+ * @param operationId The operationId name to execute.
241
+ * @param params The parameters to replace in the macro commands.
242
+ * @return An array of the responses from each command.
243
+ */
244
+ async send(operationId: string, params?: { [name: string]: any; }): Promise<string[]>;
245
+
246
+ /**
247
+ * Executes the given macros while the responses are matched.
248
+ * After execution the com port's state will be 'background'.
249
+ * @param macros The array of macros to execute.
250
+ * @param params The parameters to replace in the macro commands.
251
+ * @return An array of the responses from each command.
252
+ */
253
+ async send(macros: ComMacro[], params?: { [name: string]: any; }): Promise<string[]>;
254
+
255
+ async send(macros_operationId: ComMacro[] | string, params?: { [name: string]: any; }): Promise<string[]> {
256
+
257
+ if (typeof macros_operationId === 'string') {
258
+ if (!this.type) {
259
+ throw new Error(`Cannot send to com port ${this.path} by operationId without a type.`);
260
+ }
261
+ debug(`Sending macros for operationId '${macros_operationId}' on port ${this.path}.`);
262
+ return this.send(this.type.macros[macros_operationId], params);
263
+ }
264
+
265
+ if (!macros_operationId || macros_operationId.length === 0) {
266
+ throw new Error(`No macros provided to send to com port ${this.path}.`);
267
+ }
268
+
269
+ let macros = macros_operationId as ComMacro[];
270
+
271
+ // Replace parameters in each macro command:
272
+ if (params) {
273
+ macros = macros.map(m => {
274
+ let command = m.command;
275
+
276
+ // Extract all parameters in the command.
277
+ // These have the form {param}:
278
+ const paramRegex = /{([^}]+)}/gm;
279
+ let match;
280
+ const commandParams: string[] = [];
281
+ while ((match = paramRegex.exec(command)) !== null) {
282
+ // This is necessary to avoid infinite loops with zero-width matches
283
+ if (match.index === paramRegex.lastIndex) {
284
+ paramRegex.lastIndex++;
285
+ }
286
+
287
+ // The result can be accessed through the 'match'-variable.
288
+ match.forEach((m, groupIndex) => {
289
+ if (groupIndex === 1) {
290
+ commandParams.push(m);
291
+ }
292
+ });
293
+ }
294
+
295
+ // Replace each parameter with its value:
296
+ commandParams.forEach(p => {
297
+ const re = new RegExp(`{${p}}`, 'g');
298
+ let val;
299
+
300
+ // Resolve nested parameters:
301
+ if (p.includes('.')) {
302
+ val = params[p.substring(0, p.indexOf('.'))]; // get top-level value
303
+ const keys = p.split('.');
304
+ for (let key of keys) {
305
+ // Check for the property:
306
+ if (val && val.hasOwnProperty(key)) {
307
+ val = val[key as any];
308
+ }
309
+
310
+ // Check if the property has an array index:
311
+ else if (key.endsWith(']') && key.includes('[')) {
312
+ const arrKey = key.substring(0, key.indexOf('[')) as any;
313
+ const indexStr = key.substring(key.indexOf('[') + 1, key.length - 1);
314
+ const index = parseInt(indexStr);
315
+ if (val && val.hasOwnProperty(arrKey) && Array.isArray(val[arrKey]) && val[arrKey].length > index) {
316
+ val = val[arrKey][index];
317
+ }
318
+ }
319
+ // else not found.
320
+ }
321
+ } else {
322
+ val = params[p];
323
+ }
324
+
325
+ command = command.replaceAll(re, val);
326
+ });
327
+
328
+ return new ComMacro(command, m.response);
329
+ });
330
+ }
331
+
332
+ // Execute each macro in turn:
333
+ const responses = [];
334
+ for (let macro of macros) {
335
+ // Wait for port the become available:
336
+ await this.lockPort()
337
+
338
+ // Write the command and wait for the response:
339
+ await this.write(macro.command);
340
+ const response = await this.read(macro.response);
341
+ responses.push(response);
342
+ }
343
+
344
+ return responses;
345
+ }
346
+
347
+ receiveError(err: Error) {
348
+ console.error(`Error on port ${this.path}: ${err.message}`);
349
+ this.lastError = err.message;
350
+
351
+ if (this.port?.isOpen) {
352
+ // If the error did not close the port, then return to background processing:
353
+ this.state = 'background';
354
+ } else {
355
+ // If the error closed the port, then set state to closed
356
+ // and remove the SerialPort if needed:
357
+ this.state = 'closed';
358
+ if (this.port) {
359
+ this.close();
360
+ }
361
+ }
362
+ }
363
+
364
+ receiveData(data: Buffer) {
365
+ // Background Log:
366
+ if (this.state === 'background') {
367
+ const dataStr = data.toString();
368
+ const lines = dataStr.split(/\r?\n/);
369
+
370
+ if (lines.length > 0) {
371
+ // Append to the last entry to ensure incompelte lines are joined:
372
+ if (this.backgroundBuffer.length > 0) {
373
+ this.backgroundBuffer[this.backgroundBuffer.length - 1] += lines[0];
374
+ } else {
375
+ this.backgroundBuffer.push(lines[0]);
376
+ }
377
+
378
+ // Add remaining lines as new entries:
379
+ for (let i = 1; i < lines.length; i++) {
380
+ if (lines[i].trim().length > 0) {
381
+ this.backgroundBuffer.push(lines[i]);
382
+ }
383
+ }
384
+
385
+ // If the data string ends with a line ending, then
386
+ // add an empty line to represent the new line:
387
+ if (dataStr.endsWith('\n') || dataStr.endsWith('\r')) {
388
+ this.backgroundBuffer.push('');
389
+ }
390
+ }
391
+
392
+ // Limit background buffer size:
393
+ if (this.backgroundBuffer.length > 1000) {
394
+ this.backgroundBuffer.splice(this.backgroundBuffer.length - 10, 10);
395
+ }
396
+
397
+ return;
398
+ }
399
+
400
+ // Read until pattern matched:
401
+ if (this.state === 'busy') {
402
+ if (!this.readMatch) {
403
+ this.lastError = 'State was busy reading data but no read RegExp was found to check for completion.'
404
+ this.state = 'background'
405
+ return
406
+ }
407
+ this.buffer += data.toString()
408
+ if (this.readMatch.test(this.buffer.toString().trim())) {
409
+ this.state = 'background'
410
+ this.readComplete = true
411
+ }
412
+ }
413
+ else {
414
+ console.error(`Port ${this.path} received data in unexpected state '${this.state}': ${data.toString()}`);
415
+ }
416
+ }
417
+ }
418
+
419
+ async function sleep(startupDelay: number) {
420
+ return new Promise<void>(resolve => setTimeout(() => resolve(), startupDelay));
421
+ }
422
+