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/src/ComType.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { ComMacro } from './ComMacro';
2
+
3
+
4
+ export class ComType {
5
+ startupDelay: number = 0
6
+
7
+ constructor(
8
+ public name: string,
9
+ public baud: number,
10
+ public macros: {
11
+ init: ComMacro[];
12
+ [operationId: string]: ComMacro[];
13
+ },
14
+ startupDelay?: number
15
+ ) {
16
+ if(startupDelay) {
17
+ this.startupDelay = startupDelay
18
+ }
19
+ }
20
+
21
+ toString() {
22
+ return `ComType(name='${this.name}', baud=${this.baud})`
23
+ + `[${Object.entries(this.macros).map(([m, macros]) => (
24
+ `\n\t${m}: [${macros.map(cm => cm.toString()).join(', ')}]`
25
+ )).join('')}\n]`;
26
+ }
27
+ }
@@ -0,0 +1,204 @@
1
+ import { ComManager } from '../ComManager'
2
+ import { SerialPort } from 'serialport'
3
+ import { ComPort } from '../ComPort'
4
+ import { ComMacro } from '../ComMacro'
5
+ import { ComType } from '../ComType'
6
+
7
+ const MOCK_PORTS = [
8
+ {
9
+ path: '/dev/mock1',
10
+ manufacturer: 'ellipsis',
11
+ serialNumber: '123456',
12
+ pnpId: 'pnpId123',
13
+ locationId: 'location123',
14
+ vendorId: 'EL001',
15
+ productId: 'MOCK001'
16
+ },
17
+ {
18
+ path: '/dev/mock2',
19
+ manufacturer: 'ellipsis',
20
+ serialNumber: '123457',
21
+ pnpId: 'pnpId123',
22
+ locationId: 'location123',
23
+ vendorId: 'EL001',
24
+ productId: 'MOCK001'
25
+ },
26
+ {
27
+ path: '/dev/tty.Bluetooth-Incoming-Port',
28
+ manufacturer: undefined,
29
+ serialNumber: undefined,
30
+ pnpId: undefined,
31
+ locationId: undefined,
32
+ vendorId: undefined,
33
+ productId: undefined
34
+ }
35
+ ]
36
+
37
+ const mockType: ComType = {
38
+ name: 'mock',
39
+ baud: 9600,
40
+ macros: {
41
+ init: [new ComMacro('init', /INITTED/)],
42
+ help: [new ComMacro('help', /SOME INSTRUCTIONS/)]
43
+ }
44
+ }
45
+
46
+ const sleep = (ms: number) => {
47
+ return new Promise((resolve) => setTimeout(resolve, ms))
48
+ }
49
+
50
+ describe('ComManager', () => {
51
+ let comManager: ComManager
52
+
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+ (SerialPort as any).resetMocks();
56
+ (SerialPort as any).setMockPorts(MOCK_PORTS)
57
+ comManager = new ComManager()
58
+ comManager.refreshInterval = -1
59
+ comManager.defaultTimeout = 10
60
+ })
61
+
62
+ afterEach(() => {
63
+ (SerialPort as any).resetMocks()
64
+ })
65
+
66
+ describe('scanPorts', () => {
67
+ it('should find available ports', async () => {
68
+ await comManager.scanPorts()
69
+ expect(comManager.ports.length).toBe(MOCK_PORTS.length)
70
+ expect(comManager.ports[0].path).toBe(MOCK_PORTS[0].path)
71
+ expect(comManager.ports[1].path).toBe(MOCK_PORTS[1].path)
72
+ expect(comManager.ports[2].path).toBe(MOCK_PORTS[2].path)
73
+ })
74
+
75
+ it('should associate types to ports', async () => {
76
+ comManager.types = [mockType]
77
+ await comManager.scanPorts();
78
+
79
+ // Simulate the first port responding to the init macro and the others not responding:
80
+ (SerialPort as any).setNextResponse('INITTED')
81
+
82
+ await comManager.scanPorts()
83
+
84
+ // First should have the type set:
85
+ expect(comManager.ports[0].type).toBe(mockType)
86
+ expect(comManager.ports[1].type).toBeNull()
87
+ expect(comManager.ports[2].type).toBeNull()
88
+
89
+ // Only first has a type and so others should be closed:
90
+ expect(comManager.ports[0].state).toBe('background')
91
+ expect(comManager.ports[1].state).toBe('closed')
92
+ expect(comManager.ports[2].state).toBe('closed')
93
+ })
94
+
95
+ it('should successfully scan ports on init', async () => {
96
+ // Simulate the first port responding to the init macro and the others not responding:
97
+ (SerialPort as any).setNextResponse('INITTED')
98
+ await comManager.init([mockType])
99
+
100
+ // First should have the type set:
101
+ expect(comManager.ports[0].type).toBe(mockType)
102
+ expect(comManager.ports[1].type).toBeNull()
103
+ expect(comManager.ports[2].type).toBeNull()
104
+
105
+ // Only first has a type and so others should be closed:
106
+ expect(comManager.ports[0].state).toBe('background')
107
+ expect(comManager.ports[1].state).toBe('closed')
108
+ expect(comManager.ports[2].state).toBe('closed')
109
+ })
110
+ })
111
+
112
+ describe('get port', () => {
113
+ it('should return the correct port by type and index', async () => {
114
+ // Simulate the first port responding to the init macro and the others not responding:
115
+ (SerialPort as any).setNextResponse('INITTED')
116
+ await comManager.init([mockType])
117
+
118
+ const port = comManager.getComPort('mock', 0)
119
+ expect(port).toBe(comManager.ports[0])
120
+ })
121
+
122
+ it('should throw an error if port not found by index', async () => {
123
+ // Simulate the first port responding to the init macro and the others not responding:
124
+ (SerialPort as any).setNextResponse('INITTED')
125
+ await comManager.init([mockType])
126
+
127
+ expect(() => comManager.getComPort('mock', 99)).toThrow()
128
+ })
129
+
130
+ it('should return the correct port by type and name', async () => {
131
+ // Simulate the first port responding to the init macro and the others not responding:
132
+ (SerialPort as any).setNextResponse('INITTED')
133
+ await comManager.init([mockType])
134
+ comManager.ports[0].name = '123456' // set name to serial number for testing
135
+
136
+ const port = comManager.getComPort('mock', '123456')
137
+ expect(port).toBe(comManager.ports[0])
138
+ })
139
+
140
+ it('should throw an error if port not found by name', async () => {
141
+ // Simulate the first port responding to the init macro and the others not responding:
142
+ (SerialPort as any).setNextResponse('INITTED')
143
+ await comManager.init([mockType])
144
+ comManager.ports[0].name = '123456' // set name to serial number for testing
145
+
146
+ expect(() => comManager.getComPort('mock', 'nonexistent')).toThrow()
147
+ })
148
+ })
149
+
150
+ describe('send', () => {
151
+ it('should send messages to the correct port by index', async () => {
152
+ (SerialPort as any).setNextResponse('INITTED')
153
+ await comManager.init([mockType])
154
+
155
+ const sendPromise = comManager.send('mock', 0, 'help');
156
+ (SerialPort as any).setNextResponse('SOME INSTRUCTIONS') // simulate response from device
157
+ await Promise.resolve(sendPromise) // wait for send to complete
158
+ })
159
+
160
+ it('should send messages to the correct port by name', async () => {
161
+ (SerialPort as any).setNextResponse('INITTED')
162
+ await comManager.init([mockType])
163
+ comManager.ports[0].name = '123456' // set name to serial number for testing
164
+
165
+ const sendPromise = comManager.send('mock', '123456', 'help');
166
+ (SerialPort as any).setNextResponse('SOME INSTRUCTIONS') // simulate response from device
167
+ await Promise.resolve(sendPromise) // wait for send to complete
168
+ })
169
+ })
170
+
171
+ describe('refresh interval', () => {
172
+ it('should refresh the port list at the specified interval', async () => {
173
+ (SerialPort as any).setNextResponse('INITTED')
174
+ comManager.refreshInterval = 500
175
+ await comManager.init([mockType])
176
+ expect(comManager.ports[0].type).toBe(mockType)
177
+
178
+ // Check that the port was correctly initialized:
179
+ const mockPort = comManager.getComPort('mock', 0)
180
+ expect(mockPort).toBeDefined()
181
+ expect(mockPort.type).toBe(mockType);
182
+
183
+ const sendSpy = jest.spyOn(mockPort, 'send');
184
+
185
+ // Make sure the port will be refreshed by receiving a response to the hello message:
186
+ (SerialPort as any).setNextResponse('INITTED')
187
+
188
+ // Wait for refresh to happen - port init should have been sent and port should be open:
189
+ await sleep(comManager.refreshInterval + 200) // add a buffer to make sure the refresh has completed
190
+ expect(sendSpy).toHaveBeenCalledWith('init')
191
+ expect(mockPort.state).toBe('background')
192
+ sendSpy.mockClear()
193
+
194
+ // Wait for next refresh to happen which will not receive a response and should close the port:
195
+ await sleep(comManager.refreshInterval)
196
+ expect(mockPort.state).toBe('closed')
197
+
198
+ // Cancel autorefresh:
199
+ comManager.refreshInterval = -1
200
+ clearInterval((comManager as any).refreshIntervalHandle) // ensure rescans don't run after the test finishes
201
+ })
202
+ })
203
+ })
204
+
@@ -0,0 +1,140 @@
1
+ import { ComMacro } from "../ComMacro"
2
+ import { ComPort } from "../ComPort"
3
+ import { SerialPort } from "serialport"
4
+
5
+ const sleep = (ms: number) => {
6
+ return new Promise((resolve) => setTimeout(resolve, ms))
7
+ }
8
+
9
+ describe('ComPort', () => {
10
+ let comPort: ComPort
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ (SerialPort as any).resetMocks()
15
+ comPort = new ComPort('/dev/serialportMock')
16
+ })
17
+
18
+ afterEach(() => {
19
+ (SerialPort as any).resetMocks()
20
+ })
21
+
22
+ describe('connect', () => {
23
+ it('should open the port successfully', async () => {
24
+ const connectPromise = comPort.connect(9600)
25
+ expect(comPort.state).toBe('connecting')
26
+ await Promise.resolve(connectPromise) // wait for the open callback to be called
27
+ expect(comPort.state).toBe('background')
28
+ })
29
+
30
+ it('should handle open errors', async () => {
31
+ (SerialPort as any).setMockOpenError(new Error('Failed to open port'))
32
+ const connectPromise = comPort.connect(9600)
33
+ await expect(connectPromise).rejects.toThrow('Failed to open port')
34
+ expect(comPort.state).toBe('closed')
35
+ expect(comPort.lastError).toBe('Failed to open port')
36
+ })
37
+
38
+ it('should handle errors after opening', async () => {
39
+ await comPort.connect(9600);
40
+ (comPort as any).port.simulateError(new Error('Port error'))
41
+ expect(comPort.state).toBe('background')
42
+ expect(comPort.lastError).toBe('Port error')
43
+ })
44
+
45
+ it('should open on setting type', async () => {
46
+ const setTypePromise = comPort.setType({
47
+ name: 'serialportMock',
48
+ baud: 9600,
49
+ macros: {
50
+ init: [new ComMacro('init', /INITTED/)]
51
+ }
52
+ })
53
+ expect(comPort.state).toBe('connecting');
54
+ (SerialPort as any).setNextResponse('INITTED') // simulate response from device
55
+ await Promise.resolve(setTypePromise) // wait for setType to complete
56
+ expect(comPort.state).toBe('closed') // should close after init macros run
57
+ })
58
+ })
59
+
60
+ describe('background', () => {
61
+ it('should receive background input', async () => {
62
+ await comPort.connect(9600);
63
+ (comPort as any).port.simulateData('Hello, World!')
64
+ expect(comPort.backgroundBuffer).toContain('Hello, World!')
65
+ })
66
+ })
67
+
68
+ describe('send', () => {
69
+ it('should send data successfully via a macro and receive a response', async () => {
70
+ await comPort.connect(9600);
71
+ const responsePromise = comPort.send([new ComMacro('HELP', /SOME INSTRUCTIONS/)]);
72
+ (SerialPort as any).setNextResponse('SOME INSTRUCTIONS') // simulate response from device
73
+ await Promise.resolve(responsePromise) // wait for the write callback to be called
74
+ })
75
+
76
+ it('should throw a timeout error if response not received in time', async () => {
77
+ comPort.timeout = 500; // set a short timeout for testing
78
+ await comPort.connect(9600);
79
+ const responsePromise = comPort.send([new ComMacro('HELP', /SOME INSTRUCTIONS/)]);
80
+ await expect(responsePromise).rejects.toThrow('Timeout after 500ms while reading from port /dev/serialportMock.')
81
+ })
82
+
83
+ it('should send a macro base on the operationId', async () => {
84
+
85
+ (SerialPort as any).setNextResponse('INITTED') // simulate response from device
86
+ await comPort.setType({
87
+ name: 'serialportMock',
88
+ baud: 9600,
89
+ macros: {
90
+ init: [new ComMacro('INIT', /INITTED/)],
91
+ help: [new ComMacro('HELP', /SOME INSTRUCTIONS/)]
92
+ }
93
+ })
94
+ expect(comPort.type).toBeDefined();
95
+
96
+ (SerialPort as any).setNextResponse('SOME INSTRUCTIONS') // simulate response from device
97
+ await comPort.send('help') // send should auto-connect based on type
98
+ expect(comPort.state).toBe('background') // should return to background after macro completes
99
+ })
100
+ })
101
+
102
+ describe('close', () => {
103
+ it('should close the port successfully', async () => {
104
+ await comPort.connect(9600);
105
+ expect(comPort.state).toBe('background')
106
+ await comPort.close()
107
+ expect(comPort.state).toBe('closed')
108
+ })
109
+ })
110
+
111
+ describe('congestion handling', () => {
112
+ it('should queue sends while busy and execute them sequentially', async () => {
113
+ await comPort.connect(9600);
114
+ expect(comPort.state).toBe('background')
115
+
116
+ // 1. Send two macros in quick succession:
117
+ const sendPromise1 = comPort.send([new ComMacro('CMD1', /RESP1/)])
118
+ const sendPromise2 = comPort.send([new ComMacro('CMD2', /RESP2/)])
119
+ // Note that state may be either busy or background depending on timing.
120
+
121
+ // 2. After a brief delay the state should be 'busy':
122
+ await sleep(50)
123
+ expect(comPort.state).toBe('busy'); // first send should set state to busy
124
+
125
+ // 3. Simulate response for first command:
126
+ (comPort as any).port.simulateData('RESP1')
127
+ await sendPromise1 // wait for first send to complete
128
+ expect(comPort.state).toBe('background'); // should return to background after first macro completes
129
+
130
+ // 4. After another brief delay, the state should again be busy as the second send starts processing:
131
+ await sleep(500)
132
+ expect(comPort.state).toBe('busy'); // second send should now be processing and waiting for response
133
+
134
+ // 5. Simulate response for second command:
135
+ (comPort as any).port.simulateData('RESP2')
136
+ await sendPromise2 // wait for second send to complete
137
+ expect(comPort.state).toBe('background') // should return to background after both macros complete
138
+ })
139
+ })
140
+ })
@@ -0,0 +1,52 @@
1
+ import { ComManager } from '../ComManager'
2
+ import { ComMacro } from '../ComMacro'
3
+
4
+ /**
5
+ * This demo works with the arduino-demo sketch.
6
+ */
7
+ async function demo() {
8
+ const manager = new ComManager()
9
+
10
+ // Initialise the manager with a com type that has some macros.
11
+ // The init macro is required and is used to identify the type of a port when it is connected.
12
+ // The other macros are optional and can be used to define commands that can be sent to the device.
13
+ await manager.init(
14
+ [{
15
+ name: 'myType',
16
+ baud: 9600,
17
+ startupDelay: 2000, // optional delay to wait for device to startup after connecting and before sending init
18
+ macros: {
19
+ // Command to initialise and identify type - must have key 'init':
20
+ 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
21
+
22
+ // Basic command and response pattern - can be named anything:
23
+ getData: [new ComMacro('GET DATA', /CMD DONE$/gm)],
24
+
25
+ // Command with parameters:
26
+ setData: [new ComMacro('SET DATA {val}', /CMD DONE$/gm)],
27
+
28
+ // Command with a sequence of commands and responses:
29
+ deleteData: [
30
+ new ComMacro('DEL DATA', /Are you sure\? \(Y\/N\)$/gm),
31
+ new ComMacro('Y', /CMD DONE$/gm)
32
+ ]
33
+ }
34
+ }]
35
+ )
36
+
37
+ // Send the getData command to the first port of type 'myType':
38
+ let data = await manager.send('myType', 0, 'getData')
39
+ console.log('\nGet data response - ', data[0], '\n') // data is an array of responses matching the regex for the macro
40
+
41
+ // Send the setData command with a parameter:
42
+ await manager.send('myType', 0, 'setData', {val: 'abc123'})
43
+ data = await manager.send('myType', 0, 'getData')
44
+ console.log('\nData changed - ', data[0], '\n')
45
+
46
+ // Delete the data using the deleteData command which has a sequence of commands and responses:
47
+ await manager.send('myType', 0, 'deleteData')
48
+ data = await manager.send('myType', 0, 'getData')
49
+ console.log('\nAfter deletion - ', data[0], '\n')
50
+ }
51
+
52
+ demo()
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "commonjs",
5
+ "lib": ["ES2021"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node",
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "experimentalDecorators": true,
18
+ "emitDecoratorMetadata": true,
19
+ "types": ["node", "jest"]
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
23
+ }