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.
- package/README.md +92 -0
- package/__mocks__/serialport.ts +119 -0
- package/arduino-demo/arduino-demo.ino +68 -0
- package/dist/ComMacro.d.ts +7 -0
- package/dist/ComMacro.d.ts.map +1 -0
- package/dist/ComMacro.js +14 -0
- package/dist/ComMacro.js.map +1 -0
- package/dist/ComManager.d.ts +30 -0
- package/dist/ComManager.d.ts.map +1 -0
- package/dist/ComManager.js +240 -0
- package/dist/ComManager.js.map +1 -0
- package/dist/ComPort.d.ts +96 -0
- package/dist/ComPort.d.ts.map +1 -0
- package/dist/ComPort.js +365 -0
- package/dist/ComPort.js.map +1 -0
- package/dist/ComType.d.ts +16 -0
- package/dist/ComType.d.ts.map +1 -0
- package/dist/ComType.js +20 -0
- package/dist/ComType.js.map +1 -0
- package/dist/test/Demo.d.ts +2 -0
- package/dist/test/Demo.d.ts.map +1 -0
- package/dist/test/Demo.js +44 -0
- package/dist/test/Demo.js.map +1 -0
- package/jest.config.js +18 -0
- package/package.json +33 -0
- package/src/ComMacro.ts +11 -0
- package/src/ComManager.ts +262 -0
- package/src/ComPort.ts +422 -0
- package/src/ComType.ts +27 -0
- package/src/test/ComManager.test.ts +204 -0
- package/src/test/ComPort.test.ts +140 -0
- package/src/test/Demo.ts +52 -0
- package/tsconfig.json +23 -0
|
@@ -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
|
+
|