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
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ellipsis-com
|
|
2
|
+
|
|
3
|
+
Serial communication management with command/response macros defined by regular expressions. The library wraps a serial port, executes macros, and exposes a manager for discovering and interacting with devices.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- Connect to serial ports with a configured baud rate
|
|
7
|
+
- Send command macros and wait for regex-matched responses
|
|
8
|
+
- Manage multiple devices via a central manager
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Build
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Test
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Basic usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { ComManager } from '../ComManager'
|
|
32
|
+
import { ComMacro } from '../ComMacro'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* This demo works with the arduino-demo sketch.
|
|
36
|
+
*/
|
|
37
|
+
async function demo() {
|
|
38
|
+
const manager = new ComManager()
|
|
39
|
+
|
|
40
|
+
// Initialise the manager with a com type that has some macros.
|
|
41
|
+
// The init macro is required and is used to identify the type of a port when it is connected.
|
|
42
|
+
// The other macros are optional and can be used to define commands that can be sent to the device.
|
|
43
|
+
await manager.init(
|
|
44
|
+
[{
|
|
45
|
+
name: 'myType',
|
|
46
|
+
baud: 9600,
|
|
47
|
+
startupDelay: 2000, // optional delay to wait for device to startup after connecting and before sending init
|
|
48
|
+
macros: {
|
|
49
|
+
// Command to initialise and identify type - must have key 'init':
|
|
50
|
+
init: [new ComMacro('INFO', /MOCK\sDEVICE\sV\d+.?\d*[\n\r]+STATUS: OK[\n\r]+CMD DONE$/gm)], // regex to identify device as being of myType
|
|
51
|
+
|
|
52
|
+
// Basic command and response pattern - can be named anything:
|
|
53
|
+
getData: [new ComMacro('GET DATA', /CMD DONE$/gm)],
|
|
54
|
+
|
|
55
|
+
// Command with parameters:
|
|
56
|
+
setData: [new ComMacro('SET DATA {val}', /CMD DONE$/gm)],
|
|
57
|
+
|
|
58
|
+
// Command with a sequence of commands and responses:
|
|
59
|
+
deleteData: [
|
|
60
|
+
new ComMacro('DEL DATA', /Are you sure\? \(Y\/N\)$/gm),
|
|
61
|
+
new ComMacro('Y', /CMD DONE$/gm)
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// Send the getData command to the first port of type 'myType':
|
|
68
|
+
let data = await manager.send('myType', 0, 'getData')
|
|
69
|
+
console.log('\nGet data response - ', data[0], '\n') // data is an array of responses matching the regex for the macro
|
|
70
|
+
|
|
71
|
+
// Send the setData command with a parameter:
|
|
72
|
+
await manager.send('myType', 0, 'setData', {val: 'abc123'})
|
|
73
|
+
data = await manager.send('myType', 0, 'getData')
|
|
74
|
+
console.log('\nData changed - ', data[0], '\n')
|
|
75
|
+
|
|
76
|
+
// Delete the data using the deleteData command which has a sequence of commands and responses:
|
|
77
|
+
await manager.send('myType', 0, 'deleteData')
|
|
78
|
+
data = await manager.send('myType', 0, 'getData')
|
|
79
|
+
console.log('\nAfter deletion - ', data[0], '\n')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
demo()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Project structure
|
|
86
|
+
- src/ComPort.ts: serial port wrapper with read/write/macro support
|
|
87
|
+
- src/ComType.ts: device type definition and macro configuration
|
|
88
|
+
- src/ComMacro.ts: command/response macro definition
|
|
89
|
+
- src/ComManager.ts: device discovery and access by type
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
ISC
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
export class SerialPort extends EventEmitter {
|
|
4
|
+
private _isOpen: boolean = false;
|
|
5
|
+
public path: string;
|
|
6
|
+
public baudRate: number;
|
|
7
|
+
|
|
8
|
+
static mockPortsList: any[] = [];
|
|
9
|
+
static mockWriteResponse: Error | null = null;
|
|
10
|
+
static mockOpenResponse: Error | null = null;
|
|
11
|
+
static mockCloseResponse: Error | null = null;
|
|
12
|
+
static nextResponse: string | null = null;
|
|
13
|
+
|
|
14
|
+
constructor(options: { path: string; baudRate: number; autoOpen?: boolean }) {
|
|
15
|
+
super();
|
|
16
|
+
this.path = options.path;
|
|
17
|
+
this.baudRate = options.baudRate;
|
|
18
|
+
|
|
19
|
+
if (options.autoOpen !== false) {
|
|
20
|
+
setTimeout(() => this.open(() => {}), 0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isOpen(): boolean {
|
|
25
|
+
return this._isOpen;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
open(callback: (err: Error | null) => void): void {
|
|
29
|
+
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
if (SerialPort.mockOpenResponse) {
|
|
32
|
+
callback(SerialPort.mockOpenResponse)
|
|
33
|
+
SerialPort.mockOpenResponse = null; // reset after use
|
|
34
|
+
} else {
|
|
35
|
+
this._isOpen = true;
|
|
36
|
+
callback(null);
|
|
37
|
+
}
|
|
38
|
+
}, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
write(data: string, callback?: (err: Error | null) => void): void {
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
if (callback) {
|
|
44
|
+
callback(SerialPort.mockWriteResponse)
|
|
45
|
+
SerialPort.mockWriteResponse = null; // reset after use
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if(SerialPort.nextResponse) {
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
if(SerialPort.nextResponse) {
|
|
51
|
+
this.simulateData(SerialPort.nextResponse)
|
|
52
|
+
SerialPort.nextResponse = null; // reset after use
|
|
53
|
+
}
|
|
54
|
+
}, 0);
|
|
55
|
+
}
|
|
56
|
+
}, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close(callback?: (err: Error | null) => void): void {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
this._isOpen = false;
|
|
62
|
+
if (callback) {
|
|
63
|
+
callback(SerialPort.mockCloseResponse)
|
|
64
|
+
SerialPort.mockCloseResponse = null; // reset after use
|
|
65
|
+
}
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Method to simulate receiving data
|
|
70
|
+
simulateData(data: string | Buffer): void {
|
|
71
|
+
if(data === null) {
|
|
72
|
+
throw new Error('Data cannot be null')
|
|
73
|
+
}
|
|
74
|
+
this.emit('data', Buffer.from(data));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Method to simulate errors
|
|
78
|
+
simulateError(error: Error): void {
|
|
79
|
+
this.emit('error', error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static async list(): Promise<any[]> {
|
|
83
|
+
/**
|
|
84
|
+
PortInfo {
|
|
85
|
+
path: string;
|
|
86
|
+
manufacturer: string | undefined;
|
|
87
|
+
serialNumber: string | undefined;
|
|
88
|
+
pnpId: string | undefined;
|
|
89
|
+
locationId: string | undefined;
|
|
90
|
+
productId: string | undefined;
|
|
91
|
+
vendorId: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
*/
|
|
94
|
+
return Promise.resolve(SerialPort.mockPortsList);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper methods for tests
|
|
98
|
+
static resetMocks(): void {
|
|
99
|
+
SerialPort.mockPortsList = [];
|
|
100
|
+
SerialPort.mockWriteResponse = null;
|
|
101
|
+
SerialPort.mockOpenResponse = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static setMockPorts(ports: any[]): void {
|
|
105
|
+
SerialPort.mockPortsList = ports;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static setMockWriteError(error: Error | null): void {
|
|
109
|
+
SerialPort.mockWriteResponse = error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static setMockOpenError(error: Error | null): void {
|
|
113
|
+
SerialPort.mockOpenResponse = error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static setNextResponse(data: string) {
|
|
117
|
+
SerialPort.nextResponse = data;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
String storedData = "Initial Data";
|
|
2
|
+
bool awaitingDeleteConfirm = false;
|
|
3
|
+
|
|
4
|
+
void setup() {
|
|
5
|
+
Serial.begin(9600);
|
|
6
|
+
Serial.println("DEVICE READY");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
void loop() {
|
|
10
|
+
if (Serial.available() > 0) {
|
|
11
|
+
String input = Serial.readStringUntil('\n');
|
|
12
|
+
input.trim(); // Remove whitespace/line endings
|
|
13
|
+
|
|
14
|
+
// Handle Delete Confirmation State
|
|
15
|
+
if (awaitingDeleteConfirm) {
|
|
16
|
+
handleDeleteConfirmation(input);
|
|
17
|
+
} else {
|
|
18
|
+
processCommand(input);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void processCommand(String cmd) {
|
|
24
|
+
if (cmd == "INFO") {
|
|
25
|
+
Serial.println("MOCK DEVICE V1.0");
|
|
26
|
+
Serial.println("STATUS: OK");
|
|
27
|
+
finish();
|
|
28
|
+
}
|
|
29
|
+
else if (cmd == "GET DATA") {
|
|
30
|
+
Serial.print("DATA: ");
|
|
31
|
+
Serial.println(storedData);
|
|
32
|
+
finish();
|
|
33
|
+
}
|
|
34
|
+
else if (cmd.startsWith("SET DATA ")) {
|
|
35
|
+
// Extract everything after "SET DATA "
|
|
36
|
+
storedData = cmd.substring(9);
|
|
37
|
+
Serial.println("DATA UPDATED");
|
|
38
|
+
finish();
|
|
39
|
+
}
|
|
40
|
+
else if (cmd == "DEL DATA") {
|
|
41
|
+
Serial.println("Are you sure? (Y/N)");
|
|
42
|
+
awaitingDeleteConfirm = true;
|
|
43
|
+
// Note: No 'finish()' here because we are waiting for user input
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
Serial.println("ERROR: UNKNOWN COMMAND");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void handleDeleteConfirmation(String response) {
|
|
51
|
+
response.toUpperCase();
|
|
52
|
+
if (response == "Y") {
|
|
53
|
+
storedData = "";
|
|
54
|
+
Serial.println("DATA DELETED");
|
|
55
|
+
awaitingDeleteConfirm = false;
|
|
56
|
+
finish();
|
|
57
|
+
} else if (response == "N") {
|
|
58
|
+
Serial.println("DELETE ABORTED");
|
|
59
|
+
awaitingDeleteConfirm = false;
|
|
60
|
+
finish();
|
|
61
|
+
} else {
|
|
62
|
+
Serial.println("INVALID RESPONSE. PLEASE ENTER Y OR N:");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void finish() {
|
|
67
|
+
Serial.println("CMD DONE");
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComMacro.d.ts","sourceRoot":"","sources":["../src/ComMacro.ts"],"names":[],"mappings":"AACA,qBAAa,QAAQ;IAEN,OAAO,EAAE,MAAM;IACf,QAAQ,EAAE,MAAM;gBADhB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM;IAG3B,QAAQ;CAGX"}
|
package/dist/ComMacro.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComMacro = void 0;
|
|
4
|
+
class ComMacro {
|
|
5
|
+
constructor(command, response) {
|
|
6
|
+
this.command = command;
|
|
7
|
+
this.response = response;
|
|
8
|
+
}
|
|
9
|
+
toString() {
|
|
10
|
+
return `ComMacro(command='${this.command}', response=${this.response.toString()})`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.ComMacro = ComMacro;
|
|
14
|
+
//# sourceMappingURL=ComMacro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComMacro.js","sourceRoot":"","sources":["../src/ComMacro.ts"],"names":[],"mappings":";;;AACA,MAAa,QAAQ;IACjB,YACW,OAAe,EACf,QAAgB;QADhB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;IACvB,CAAC;IAEL,QAAQ;QACJ,OAAO,qBAAqB,IAAI,CAAC,OAAO,eAAe,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC;IACvF,CAAC;CACJ;AATD,4BASC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ComType } from './ComType';
|
|
2
|
+
import { ComPort } from './ComPort';
|
|
3
|
+
export declare function enableDebug(enable?: boolean): void;
|
|
4
|
+
/**
|
|
5
|
+
* Maintains a list of ports and types, and handles scanning for new ports and matching them to types.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ComManager {
|
|
8
|
+
ports: ComPort[];
|
|
9
|
+
types: ComType[];
|
|
10
|
+
refreshInterval: number;
|
|
11
|
+
defaultTimeout: number | undefined;
|
|
12
|
+
private refreshIntervalHandle;
|
|
13
|
+
constructor();
|
|
14
|
+
init(types?: ComType[]): Promise<void>;
|
|
15
|
+
private scheduleRefresh;
|
|
16
|
+
getComPort(typeName: string, index_name: number | string): ComPort;
|
|
17
|
+
scanPorts(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Load the com types from the OpenAPI document.
|
|
20
|
+
* TODO: This method needs to be moved into the bb-com-extension.
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
23
|
+
list(type: string, operationId: string, params: {
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}): Promise<string[][]>;
|
|
26
|
+
send(type: string, index_name: number | string, operationId: string, params?: {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}): Promise<string[]>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ComManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComManager.d.ts","sourceRoot":"","sources":["../src/ComManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAgB,WAAW,CAAC,MAAM,UAAO,QAMxC;AAGD;;GAEG;AACH,qBAAa,UAAU;IACrB,KAAK,EAAE,OAAO,EAAE,CAAK;IACrB,KAAK,EAAE,OAAO,EAAE,CAAK;IACrB,eAAe,SAAQ;IACvB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAY;IAE9C,OAAO,CAAC,qBAAqB,CAA8B;;IAIrD,IAAI,CAAC,KAAK,GAAE,OAAO,EAAO;IAchC,OAAO,CAAC,eAAe;IAyBvB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO;IAqB5D,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IA2EhC;;;;OAIG;IAgFG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAS1F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAK3H"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComManager = void 0;
|
|
4
|
+
exports.enableDebug = enableDebug;
|
|
5
|
+
const serialport_1 = require("serialport");
|
|
6
|
+
const ComPort_1 = require("./ComPort");
|
|
7
|
+
// TODO: Adopt a propper logging system.
|
|
8
|
+
let debug;
|
|
9
|
+
function enableDebug(enable = true) {
|
|
10
|
+
if (enable) {
|
|
11
|
+
debug = console.log;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
debug = () => { };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
enableDebug(false);
|
|
18
|
+
/**
|
|
19
|
+
* Maintains a list of ports and types, and handles scanning for new ports and matching them to types.
|
|
20
|
+
*/
|
|
21
|
+
class ComManager {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.ports = [];
|
|
24
|
+
this.types = [];
|
|
25
|
+
this.refreshInterval = 60000; // 60 seconds
|
|
26
|
+
this.defaultTimeout = undefined; // assigned to ports on creation
|
|
27
|
+
this.refreshIntervalHandle = null;
|
|
28
|
+
}
|
|
29
|
+
async init(types = []) {
|
|
30
|
+
this.types = types;
|
|
31
|
+
try {
|
|
32
|
+
await this.scanPorts();
|
|
33
|
+
debug("ComManager initialized.");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error("Error initializing ComManager: ", err);
|
|
37
|
+
setTimeout(() => this.init(), 1000); // retry
|
|
38
|
+
}
|
|
39
|
+
this.scheduleRefresh();
|
|
40
|
+
}
|
|
41
|
+
scheduleRefresh() {
|
|
42
|
+
// Clear any current job:
|
|
43
|
+
if (this.refreshIntervalHandle) {
|
|
44
|
+
clearInterval(this.refreshIntervalHandle);
|
|
45
|
+
}
|
|
46
|
+
// A negative interval disables refreshing:
|
|
47
|
+
if (this.refreshInterval < 0) {
|
|
48
|
+
debug('ComManager auto-refresh disabled.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Schedule a new job:
|
|
52
|
+
this.refreshIntervalHandle = setTimeout(async () => {
|
|
53
|
+
debug('Rescanning com ports...');
|
|
54
|
+
try {
|
|
55
|
+
this.scanPorts();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error("Error scanning ports: ", err);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
this.scheduleRefresh(); // reschedule the next refresh
|
|
62
|
+
}
|
|
63
|
+
}, this.refreshInterval);
|
|
64
|
+
}
|
|
65
|
+
getComPort(typeName, index_name) {
|
|
66
|
+
const typePorts = this.ports.filter(p => p.type?.name === typeName);
|
|
67
|
+
if (typeof index_name === 'string') {
|
|
68
|
+
const port = typePorts.find(p => p.name === index_name);
|
|
69
|
+
if (!port) {
|
|
70
|
+
throw new Error(`Port with name ${index_name} not found for type ${typeName}.`);
|
|
71
|
+
}
|
|
72
|
+
return port;
|
|
73
|
+
}
|
|
74
|
+
const index = index_name;
|
|
75
|
+
// Make sure the port exists and has a known type:
|
|
76
|
+
if (index >= typePorts.length) {
|
|
77
|
+
throw new Error(`Port with index ${index} not found. ${typePorts.length} ports found of type ${typeName}.`); // TODO: Create custom not found error class.
|
|
78
|
+
}
|
|
79
|
+
return typePorts[index];
|
|
80
|
+
}
|
|
81
|
+
async scanPorts() {
|
|
82
|
+
debug(`Scanning serial ports...`);
|
|
83
|
+
try {
|
|
84
|
+
const ports = await serialport_1.SerialPort.list();
|
|
85
|
+
await Promise.all(ports.map(async (p) => {
|
|
86
|
+
// Send hello message to existing ports:
|
|
87
|
+
const existingPort = this.ports.find(existing => existing.path === p.path);
|
|
88
|
+
if (existingPort && existingPort.type) {
|
|
89
|
+
try {
|
|
90
|
+
debug(`Sending hello message to port ${existingPort.path} of type ${existingPort.type?.name}...`);
|
|
91
|
+
await existingPort.send('init');
|
|
92
|
+
debug(`Response received from port ${existingPort.path}.`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
debug(`Port ${existingPort.path} of type ${existingPort.type?.name} is not responding.`);
|
|
96
|
+
// Close the port if open:
|
|
97
|
+
if (existingPort.state !== 'closed') {
|
|
98
|
+
await existingPort.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Remove existing ports of unknown type so that we can try again:
|
|
104
|
+
if (existingPort && !existingPort.type) {
|
|
105
|
+
debug(`Removing existing port ${existingPort.path} of unknown type for re-detection.`);
|
|
106
|
+
this.ports = this.ports.filter(p => p.path !== existingPort.path);
|
|
107
|
+
}
|
|
108
|
+
// Add new port:
|
|
109
|
+
const port = new ComPort_1.ComPort(p.path);
|
|
110
|
+
if (this.defaultTimeout) {
|
|
111
|
+
port.timeout = this.defaultTimeout;
|
|
112
|
+
}
|
|
113
|
+
port.path = p.path;
|
|
114
|
+
port.manufacturer = p.manufacturer;
|
|
115
|
+
port.serialNumber = p.serialNumber;
|
|
116
|
+
port.pnpId = p.pnpId;
|
|
117
|
+
port.locationId = p.locationId;
|
|
118
|
+
port.productId = p.productId;
|
|
119
|
+
port.vendorId = p.vendorId;
|
|
120
|
+
this.ports.push(port);
|
|
121
|
+
// Check if the port supports one of the known com types:
|
|
122
|
+
for (let type of this.types) {
|
|
123
|
+
try {
|
|
124
|
+
// Try to set the type:
|
|
125
|
+
if (!await port.setType(type)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// If the type was set without error, then the port
|
|
129
|
+
// supports this type:
|
|
130
|
+
debug(`Com port of type ${type.name} found at path ${port.path}`);
|
|
131
|
+
// Open the port for background use:
|
|
132
|
+
await port.connect();
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
debug(`Failed to add type to port: ${port.toString()}`);
|
|
137
|
+
console.error(err);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
debug(`Com ports: \n\t${this.ports.map(p => p.toString()).join(',\n\t')}`);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error(`Error scanning serial ports: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Load the com types from the OpenAPI document.
|
|
150
|
+
* TODO: This method needs to be moved into the bb-com-extension.
|
|
151
|
+
* @returns
|
|
152
|
+
*/
|
|
153
|
+
// loadTypes() {
|
|
154
|
+
// if(!this.openapiDoc['x-bb-com-types']) {
|
|
155
|
+
// debug("No com types found in OpenAPI document.")
|
|
156
|
+
// return
|
|
157
|
+
// }
|
|
158
|
+
// // Iterate over all com types in the OpenAPI document:
|
|
159
|
+
// Object.keys(this.openapiDoc['x-bb-com-types']).forEach( (name: string) => {
|
|
160
|
+
// const comType = this.openapiDoc['x-bb-com-types'][name]
|
|
161
|
+
// // Ensure the baud rate is a number:
|
|
162
|
+
// if(typeof comType.baud === 'string') {
|
|
163
|
+
// comType.baud = parseInt(comType.baud)
|
|
164
|
+
// }
|
|
165
|
+
// // Extract all macros that specify this com type by name:
|
|
166
|
+
// const macros = {} as {[method: string]: ComMacro[]}
|
|
167
|
+
// Object.values(this.openapiDoc.paths).forEach( (path: any) => {
|
|
168
|
+
// Object.keys(path)
|
|
169
|
+
// .filter(method => ['get', 'post', 'put', 'patch', 'delete'].includes(method))
|
|
170
|
+
// .filter(method => path[method]['x-bb-com-macro'])
|
|
171
|
+
// .forEach(method => {
|
|
172
|
+
// if(!path[method]['x-bb-com-macro'].commands ||
|
|
173
|
+
// !path[method]['x-bb-com-macro'].responses ||
|
|
174
|
+
// path[method]['x-bb-com-macro'].commands.length !== path[method]['x-bb-com-macro'].responses.length)
|
|
175
|
+
// {
|
|
176
|
+
// throw new Error('x-bb-com-macro commands and responses must be arrays of the same length in openapi.json at path '+path+'/'+method+'.')
|
|
177
|
+
// }
|
|
178
|
+
// if(!path[method].operationId) {
|
|
179
|
+
// throw new Error(`OperationId missing for method ${method} at path ${path} in openapi.json.`)
|
|
180
|
+
// }
|
|
181
|
+
// macros[path[method].operationId] = (path[method]['x-bb-com-macro'].commands as string[]).map((c, i) => (
|
|
182
|
+
// new ComMacro(c, new RegExp(path[method]['x-bb-com-macro'].responses[i].replace(/^\\/|\\/$/g, ''), 'gm'))
|
|
183
|
+
// ))
|
|
184
|
+
// })
|
|
185
|
+
// })
|
|
186
|
+
// // Init macro:
|
|
187
|
+
// if(!comType.initCommands || comType.initCommands.length === 0) {
|
|
188
|
+
// throw new Error(`Com type '${name}' is missing init commands.`)
|
|
189
|
+
// }
|
|
190
|
+
// if(!comType.initResponses || comType.initResponses.length === 0) {
|
|
191
|
+
// throw new Error(`Com type '${name}' is missing init responses.`)
|
|
192
|
+
// }
|
|
193
|
+
// if(comType.initCommands.length !== comType.initResponses.length) {
|
|
194
|
+
// throw new Error(`Com type '${name}' init commands and responses must be arrays of the same length.`)
|
|
195
|
+
// }
|
|
196
|
+
// macros.init = []
|
|
197
|
+
// comType.initCommands.forEach((cmd: string, i: number) => {
|
|
198
|
+
// // Extract flags and strip slashes if present:
|
|
199
|
+
// let response = comType.initResponses[i] as string
|
|
200
|
+
// let flags = ''
|
|
201
|
+
// if(response.startsWith('/')) {
|
|
202
|
+
// const lastSlash = response.lastIndexOf('/')
|
|
203
|
+
// if(lastSlash < response.length - 1) {
|
|
204
|
+
// flags = response.substring(lastSlash + 1)
|
|
205
|
+
// }
|
|
206
|
+
// response = response.substring(1, lastSlash)
|
|
207
|
+
// }
|
|
208
|
+
// // Add the macro:
|
|
209
|
+
// macros.init.push(
|
|
210
|
+
// new ComMacro(
|
|
211
|
+
// cmd,
|
|
212
|
+
// new RegExp(response, flags)
|
|
213
|
+
// )
|
|
214
|
+
// )
|
|
215
|
+
// })
|
|
216
|
+
// // Add the com type:
|
|
217
|
+
// this.types.push(new ComType(
|
|
218
|
+
// name,
|
|
219
|
+
// comType.baud,
|
|
220
|
+
// macros as {[method: string]: ComMacro[], init: ComMacro[]}
|
|
221
|
+
// ))
|
|
222
|
+
// debug(`Loaded com type ${comType.toString()}`);
|
|
223
|
+
// })
|
|
224
|
+
// }
|
|
225
|
+
async list(type, operationId, params) {
|
|
226
|
+
const results = [];
|
|
227
|
+
const ports = this.ports.filter(p => p.type?.name === type);
|
|
228
|
+
for (let index = 0; index < ports.length; index++) {
|
|
229
|
+
results.push(await this.send(type, index, operationId, params));
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
async send(type, index_name, operationId, params) {
|
|
234
|
+
const port = this.getComPort(type, index_name);
|
|
235
|
+
const responses = await port.send(operationId, params);
|
|
236
|
+
return responses;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.ComManager = ComManager;
|
|
240
|
+
//# sourceMappingURL=ComManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComManager.js","sourceRoot":"","sources":["../src/ComManager.ts"],"names":[],"mappings":"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"}
|