elero-usb-transmitter-client 1.1.3 → 1.1.4
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 +5 -0
- package/package.json +9 -5
- package/.eslintrc.json +0 -9
- package/.github/workflows/nodejs.yml +0 -27
- package/.prettierrc +0 -6
- package/.vscode/launch.json +0 -22
- package/jest.json +0 -18
- package/src/UsbTransmitterClient.ts +0 -214
- package/src/cli.ts +0 -167
- package/src/domain/constants.ts +0 -52
- package/src/domain/enums.ts +0 -36
- package/src/domain/types.ts +0 -1
- package/src/index.ts +0 -6
- package/src/model/Response.ts +0 -17
- package/test/UsbTransmitterClient.test.ts +0 -39
- package/test/UsbTransmitterClientMock.test.ts +0 -182
package/README.md
CHANGED
|
@@ -84,6 +84,11 @@ await client.close()
|
|
|
84
84
|
|
|
85
85
|
## Changelog
|
|
86
86
|
|
|
87
|
+
### 1.1.4
|
|
88
|
+
|
|
89
|
+
- Added `prepublishOnly` script to ensure fresh build before publish
|
|
90
|
+
- Explicitly added `dist` to `files` list in `package.json`
|
|
91
|
+
|
|
87
92
|
### 1.1.3
|
|
88
93
|
|
|
89
94
|
- Moved `commander` and `inquirer` to `dependencies` (fix runtime error)
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elero-usb-transmitter-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/src/index.js",
|
|
6
6
|
"types": "./dist/src/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest --config jest.json",
|
|
9
9
|
"build": "tsc",
|
|
10
|
-
"setup": "npm install"
|
|
10
|
+
"setup": "npm install",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
11
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
12
16
|
"bin": {
|
|
13
|
-
"elero-usb-transmitter-client": "
|
|
14
|
-
"elero-cli": "
|
|
17
|
+
"elero-usb-transmitter-client": "dist/src/cli.js",
|
|
18
|
+
"elero-cli": "dist/src/cli.js"
|
|
15
19
|
},
|
|
16
20
|
"repository": {
|
|
17
21
|
"type": "git",
|
|
@@ -47,4 +51,4 @@
|
|
|
47
51
|
"lodash": "^4.17.15",
|
|
48
52
|
"serialport": "^13.0.0"
|
|
49
53
|
}
|
|
50
|
-
}
|
|
54
|
+
}
|
package/.eslintrc.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: Node.js CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main, develop ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
strategy:
|
|
15
|
-
matrix:
|
|
16
|
-
node-version: [18.x, 20.x, 22.x]
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v3
|
|
20
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
-
uses: actions/setup-node@v3
|
|
22
|
-
with:
|
|
23
|
-
node-version: ${{ matrix.node-version }}
|
|
24
|
-
cache: 'npm'
|
|
25
|
-
- run: npm ci
|
|
26
|
-
- run: npm run build
|
|
27
|
-
- run: npm test
|
package/.prettierrc
DELETED
package/.vscode/launch.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
// Use IntelliSense to learn about possible attributes.
|
|
3
|
-
// Hover to view descriptions of existing attributes.
|
|
4
|
-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
-
"version": "0.2.0",
|
|
6
|
-
"configurations": [
|
|
7
|
-
{
|
|
8
|
-
"type": "node",
|
|
9
|
-
"name": "vscode-jest-tests",
|
|
10
|
-
"request": "launch",
|
|
11
|
-
"args": [
|
|
12
|
-
"--runInBand",
|
|
13
|
-
"--config=jest.json"
|
|
14
|
-
],
|
|
15
|
-
"cwd": "${workspaceFolder}",
|
|
16
|
-
"console": "integratedTerminal",
|
|
17
|
-
"internalConsoleOptions": "neverOpen",
|
|
18
|
-
"disableOptimisticBPs": true,
|
|
19
|
-
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
package/jest.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"transform": {
|
|
3
|
-
"^.+\\.(t|j)sx?$": "ts-jest"
|
|
4
|
-
},
|
|
5
|
-
"testRegex": "(/test/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
|
6
|
-
"testPathIgnorePatterns": [
|
|
7
|
-
"/node_modules/",
|
|
8
|
-
"/dist/"
|
|
9
|
-
],
|
|
10
|
-
"moduleFileExtensions": [
|
|
11
|
-
"ts",
|
|
12
|
-
"tsx",
|
|
13
|
-
"js",
|
|
14
|
-
"jsx",
|
|
15
|
-
"json",
|
|
16
|
-
"node"
|
|
17
|
-
]
|
|
18
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { SerialPort } from 'serialport'
|
|
2
|
-
import * as _ from 'lodash'
|
|
3
|
-
import {
|
|
4
|
-
BYTE_HEADER,
|
|
5
|
-
BYTE_LENGTH_2,
|
|
6
|
-
RESPONSE_LENGTH_CHECK,
|
|
7
|
-
RESPONSE_LENGTH_SEND,
|
|
8
|
-
BYTE_LENGTH_5,
|
|
9
|
-
RESPONSE_LENGTH_INFO,
|
|
10
|
-
BYTE_LENGTH_4,
|
|
11
|
-
} from './domain/constants'
|
|
12
|
-
import { Response } from './model/Response'
|
|
13
|
-
import { ControlCommand, EasyCommand, InfoData } from './domain/enums'
|
|
14
|
-
import { Mutex } from 'async-mutex'
|
|
15
|
-
|
|
16
|
-
const DEFAULT_BAUDRATE = 38400
|
|
17
|
-
const DEFAULT_BYTESIZE = 8
|
|
18
|
-
const DEFAULT_PARITY = 'none'
|
|
19
|
-
const DEFAULT_STOPBITS = 1
|
|
20
|
-
|
|
21
|
-
const mutex = new Mutex()
|
|
22
|
-
|
|
23
|
-
export class UsbTransmitterClient {
|
|
24
|
-
serialPort: SerialPort<any>
|
|
25
|
-
|
|
26
|
-
constructor(devPath: string) {
|
|
27
|
-
this.serialPort = new SerialPort({
|
|
28
|
-
path: devPath,
|
|
29
|
-
baudRate: DEFAULT_BAUDRATE,
|
|
30
|
-
dataBits: DEFAULT_BYTESIZE,
|
|
31
|
-
parity: DEFAULT_PARITY,
|
|
32
|
-
stopBits: DEFAULT_STOPBITS,
|
|
33
|
-
autoOpen: false,
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public open(): Promise<void> {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
if (!this.serialPort.isOpen) {
|
|
40
|
-
this.serialPort.open((error) => {
|
|
41
|
-
if (error) reject(error)
|
|
42
|
-
this.serialPort.flush((error) => {
|
|
43
|
-
if (error) reject(error)
|
|
44
|
-
resolve()
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public close(): Promise<void> {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
this.serialPort.close((error) => {
|
|
54
|
-
if (error) reject(error)
|
|
55
|
-
resolve()
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
public async checkChannels(): Promise<number[]> {
|
|
61
|
-
const data = [BYTE_HEADER, BYTE_LENGTH_2, EasyCommand.EASY_CHECK]
|
|
62
|
-
const release = await mutex.acquire()
|
|
63
|
-
try {
|
|
64
|
-
await this.sendCommand(data)
|
|
65
|
-
const responseBytes = await this.waitForResponse(RESPONSE_LENGTH_CHECK)
|
|
66
|
-
const response = this.parseResponse(responseBytes)
|
|
67
|
-
return response.activeChannels
|
|
68
|
-
} finally {
|
|
69
|
-
release()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public async getInfo(channel: number): Promise<Response> {
|
|
74
|
-
let lowChannels = (1 << (channel - 1)) & 0xff
|
|
75
|
-
let highChannels = (1 << (channel - 1)) >> 8
|
|
76
|
-
|
|
77
|
-
const data = [
|
|
78
|
-
BYTE_HEADER,
|
|
79
|
-
BYTE_LENGTH_4,
|
|
80
|
-
EasyCommand.EASY_INFO,
|
|
81
|
-
highChannels,
|
|
82
|
-
lowChannels,
|
|
83
|
-
]
|
|
84
|
-
const release = await mutex.acquire()
|
|
85
|
-
try {
|
|
86
|
-
await this.sendCommand(data)
|
|
87
|
-
const responseBytes = await this.waitForResponse(RESPONSE_LENGTH_INFO)
|
|
88
|
-
const response = this.parseResponse(responseBytes)
|
|
89
|
-
return response
|
|
90
|
-
} finally {
|
|
91
|
-
release()
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
public async sendControlCommand(
|
|
96
|
-
channel: number,
|
|
97
|
-
controlCommand: ControlCommand
|
|
98
|
-
): Promise<Response> {
|
|
99
|
-
let lowChannels = (1 << (channel - 1)) & 0xff
|
|
100
|
-
let highChannels = (1 << (channel - 1)) >> 8
|
|
101
|
-
|
|
102
|
-
const data = [
|
|
103
|
-
BYTE_HEADER,
|
|
104
|
-
BYTE_LENGTH_5,
|
|
105
|
-
EasyCommand.EASY_SEND,
|
|
106
|
-
highChannels,
|
|
107
|
-
lowChannels,
|
|
108
|
-
controlCommand,
|
|
109
|
-
]
|
|
110
|
-
const release = await mutex.acquire()
|
|
111
|
-
try {
|
|
112
|
-
await this.sendCommand(data)
|
|
113
|
-
const responseBytes = await this.waitForResponse(RESPONSE_LENGTH_INFO)
|
|
114
|
-
const response = this.parseResponse(responseBytes)
|
|
115
|
-
return response
|
|
116
|
-
} finally {
|
|
117
|
-
release()
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private waitForResponse(length: number): Promise<Buffer> {
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
const timeout = setTimeout(() => {
|
|
124
|
-
cleanup()
|
|
125
|
-
reject(new Error('Timeout waiting for response'))
|
|
126
|
-
}, 2000)
|
|
127
|
-
|
|
128
|
-
const tryRead = () => {
|
|
129
|
-
const buffer = this.serialPort.read(length)
|
|
130
|
-
if (buffer) {
|
|
131
|
-
cleanup()
|
|
132
|
-
resolve(buffer)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const cleanup = () => {
|
|
137
|
-
clearTimeout(timeout)
|
|
138
|
-
this.serialPort.removeListener('readable', tryRead)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
this.serialPort.on('readable', tryRead)
|
|
142
|
-
tryRead()
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private sendCommand(data: number[]): Promise<number> {
|
|
147
|
-
const checksum = this.calculateChecksum(data)
|
|
148
|
-
data.push(checksum)
|
|
149
|
-
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
this.serialPort.flush((error) => {
|
|
152
|
-
if (error) reject(error)
|
|
153
|
-
this.serialPort.write(data, (error: Error | null | undefined) => {
|
|
154
|
-
if (error) reject(error)
|
|
155
|
-
resolve(data.length)
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private readResponseBytes(length: number): string | Buffer | null {
|
|
162
|
-
//Get the serial data from the serial port.
|
|
163
|
-
var response = this.serialPort.read(length)
|
|
164
|
-
return response
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private calculateChecksum(data: number[]): number {
|
|
168
|
-
//Calculate checksum.
|
|
169
|
-
//All the sum of all bytes (Header to CS) must be 0x00.
|
|
170
|
-
const sum = _.sum(data)
|
|
171
|
-
const result = (256 - sum) % 256
|
|
172
|
-
return result
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private getActiveChannels(byte: number, start: number): number[] {
|
|
176
|
-
var channels: number[] = new Array()
|
|
177
|
-
for (let i = 0; i < 9; i++) {
|
|
178
|
-
if (((byte >> i) & 1) == 1) {
|
|
179
|
-
const channel = i + start
|
|
180
|
-
channels.push(channel)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return channels
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private parseResponse(bytes: Buffer): Response {
|
|
187
|
-
const activeHighChannels = this.getActiveChannels(bytes[3], 9)
|
|
188
|
-
const activeLowChannels = this.getActiveChannels(bytes[4], 1)
|
|
189
|
-
const activeChannels = _.concat(activeLowChannels, activeHighChannels)
|
|
190
|
-
const response: Response = {
|
|
191
|
-
header: bytes[0],
|
|
192
|
-
length: bytes[1],
|
|
193
|
-
command: bytes[2],
|
|
194
|
-
activeChannels: activeChannels,
|
|
195
|
-
checksum: -1,
|
|
196
|
-
status: null,
|
|
197
|
-
statusCode: -1,
|
|
198
|
-
}
|
|
199
|
-
if (bytes.length == RESPONSE_LENGTH_CHECK) {
|
|
200
|
-
response.checksum = bytes[5]
|
|
201
|
-
//Easy Ack (the answer on Easy Info)
|
|
202
|
-
} else if (bytes.length == RESPONSE_LENGTH_SEND) {
|
|
203
|
-
if (bytes[5] in InfoData) {
|
|
204
|
-
response.status = bytes[5] as InfoData
|
|
205
|
-
} else {
|
|
206
|
-
response.status = InfoData.INFO_UNKNOWN
|
|
207
|
-
}
|
|
208
|
-
response.checksum = bytes[6]
|
|
209
|
-
} else {
|
|
210
|
-
response.status = InfoData.INFO_UNKNOWN
|
|
211
|
-
}
|
|
212
|
-
return response
|
|
213
|
-
}
|
|
214
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander'
|
|
3
|
-
import * as inquirer from 'inquirer'
|
|
4
|
-
import { UsbTransmitterClient } from './UsbTransmitterClient'
|
|
5
|
-
import { ControlCommand } from './domain/enums'
|
|
6
|
-
import { SerialPort } from 'serialport'
|
|
7
|
-
|
|
8
|
-
const program = new Command()
|
|
9
|
-
let client: UsbTransmitterClient | null = null
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.version('1.0.0')
|
|
13
|
-
.option('-p, --port <path>', 'Path to serial port')
|
|
14
|
-
.parse(process.argv)
|
|
15
|
-
|
|
16
|
-
const options = program.opts()
|
|
17
|
-
|
|
18
|
-
async function main() {
|
|
19
|
-
let portPath = options.port
|
|
20
|
-
|
|
21
|
-
if (!portPath) {
|
|
22
|
-
const ports = await SerialPort.list()
|
|
23
|
-
const portChoices = ports.map((p) => ({ name: `${p.path} ${p.manufacturer || ''}`, value: p.path }))
|
|
24
|
-
|
|
25
|
-
if (portChoices.length === 0) {
|
|
26
|
-
console.error('No serial ports found. Please specify one with --port.')
|
|
27
|
-
process.exit(1)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const answer = await inquirer.prompt([
|
|
31
|
-
{
|
|
32
|
-
type: 'list',
|
|
33
|
-
name: 'port',
|
|
34
|
-
message: 'Select Serial Port',
|
|
35
|
-
choices: portChoices,
|
|
36
|
-
},
|
|
37
|
-
])
|
|
38
|
-
portPath = answer.port
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
client = new UsbTransmitterClient(portPath)
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await client.open()
|
|
45
|
-
console.log(`Connected to ${portPath}`)
|
|
46
|
-
await mainMenu()
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error('Error connecting to device:', error)
|
|
49
|
-
process.exit(1)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function mainMenu() {
|
|
54
|
-
const answer = await inquirer.prompt([
|
|
55
|
-
{
|
|
56
|
-
type: 'list',
|
|
57
|
-
name: 'action',
|
|
58
|
-
message: 'Main Menu',
|
|
59
|
-
choices: [
|
|
60
|
-
{ name: 'Check Channels', value: 'check' },
|
|
61
|
-
{ name: 'Select Channel', value: 'select' },
|
|
62
|
-
new inquirer.Separator(),
|
|
63
|
-
{ name: 'Exit', value: 'exit' },
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
])
|
|
67
|
-
|
|
68
|
-
switch (answer.action) {
|
|
69
|
-
case 'check':
|
|
70
|
-
await checkChannels()
|
|
71
|
-
break
|
|
72
|
-
case 'select':
|
|
73
|
-
await selectChannel()
|
|
74
|
-
break
|
|
75
|
-
case 'exit':
|
|
76
|
-
await client!.close()
|
|
77
|
-
process.exit(0)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function checkChannels() {
|
|
82
|
-
console.log('Checking channels...')
|
|
83
|
-
try {
|
|
84
|
-
const channels = await client!.checkChannels()
|
|
85
|
-
console.log('Active Channels:', channels.join(', '))
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error('Error checking channels:', error)
|
|
88
|
-
}
|
|
89
|
-
await mainMenu()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function selectChannel() {
|
|
93
|
-
const answer = await inquirer.prompt([
|
|
94
|
-
{
|
|
95
|
-
type: 'input',
|
|
96
|
-
name: 'channel',
|
|
97
|
-
message: 'Enter Channel Number (1-9):',
|
|
98
|
-
validate: (input) => {
|
|
99
|
-
const num = parseInt(input, 10)
|
|
100
|
-
if (isNaN(num) || num < 1 || num > 9) {
|
|
101
|
-
return 'Please enter a number between 1 and 9'
|
|
102
|
-
}
|
|
103
|
-
return true
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
])
|
|
107
|
-
|
|
108
|
-
const channel = parseInt(answer.channel, 10)
|
|
109
|
-
await channelMenu(channel)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function channelMenu(channel: number) {
|
|
113
|
-
const answer = await inquirer.prompt([
|
|
114
|
-
{
|
|
115
|
-
type: 'list',
|
|
116
|
-
name: 'action',
|
|
117
|
-
message: `Channel ${channel} Actions`,
|
|
118
|
-
choices: [
|
|
119
|
-
{ name: 'Get Info', value: 'info' },
|
|
120
|
-
{ name: 'Move Up', value: 'up' },
|
|
121
|
-
{ name: 'Move Down', value: 'down' },
|
|
122
|
-
{ name: 'Stop', value: 'stop' },
|
|
123
|
-
new inquirer.Separator(),
|
|
124
|
-
{ name: 'Back to Main Menu', value: 'back' },
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
])
|
|
128
|
-
|
|
129
|
-
if (answer.action === 'back') {
|
|
130
|
-
await mainMenu()
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
if (answer.action === 'info') {
|
|
136
|
-
const info = await client!.getInfo(channel)
|
|
137
|
-
console.log('Channel Info:', info)
|
|
138
|
-
} else {
|
|
139
|
-
let cmd: ControlCommand
|
|
140
|
-
switch (answer.action) {
|
|
141
|
-
case 'up':
|
|
142
|
-
cmd = ControlCommand.up
|
|
143
|
-
break
|
|
144
|
-
case 'down':
|
|
145
|
-
cmd = ControlCommand.down
|
|
146
|
-
break
|
|
147
|
-
case 'stop':
|
|
148
|
-
cmd = ControlCommand.stop
|
|
149
|
-
break
|
|
150
|
-
default:
|
|
151
|
-
throw new Error('Unknown command')
|
|
152
|
-
}
|
|
153
|
-
console.log(`Sending ${answer.action} command to channel ${channel}...`)
|
|
154
|
-
const response = await client!.sendControlCommand(channel, cmd)
|
|
155
|
-
console.log('Response:', response)
|
|
156
|
-
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error('Error executing command:', error)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
await channelMenu(channel)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
main().catch((err) => {
|
|
165
|
-
console.error('Unexpected error:', err)
|
|
166
|
-
process.exit(1)
|
|
167
|
-
})
|
package/src/domain/constants.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Dictionary } from './types'
|
|
2
|
-
|
|
3
|
-
// Default serial connection details.
|
|
4
|
-
export const DEFAULT_BAUDRATE = 38400
|
|
5
|
-
export const DEFAULT_BYTESIZE = 8
|
|
6
|
-
export const DEFAULT_PARITY = 'none'
|
|
7
|
-
export const DEFAULT_STOPBITS = 1
|
|
8
|
-
|
|
9
|
-
// values to bit shift.
|
|
10
|
-
export const HEX_255: number = 0xff
|
|
11
|
-
export const BIT_8: number = 8
|
|
12
|
-
|
|
13
|
-
// Header for all command.
|
|
14
|
-
export const BYTE_HEADER: number = 0xaa
|
|
15
|
-
// command lengths
|
|
16
|
-
export const BYTE_LENGTH_2: number = 0x02
|
|
17
|
-
export const BYTE_LENGTH_4: number = 0x04
|
|
18
|
-
export const BYTE_LENGTH_5: number = 0x05
|
|
19
|
-
|
|
20
|
-
// required response lenth.
|
|
21
|
-
export const RESPONSE_LENGTH_CHECK: number = 6
|
|
22
|
-
// required response lenth.
|
|
23
|
-
export const RESPONSE_LENGTH_SEND: number = 7
|
|
24
|
-
// Required response lenth.
|
|
25
|
-
export const RESPONSE_LENGTH_INFO: number = 7
|
|
26
|
-
// for Serial error handling
|
|
27
|
-
export const NO_SERIAL_RESPONSE: string = ''
|
|
28
|
-
|
|
29
|
-
// Info to receive response.
|
|
30
|
-
export const INFO_UNKNOWN = 'unknown response'
|
|
31
|
-
export const INFO_NO_INFORMATION = 'no information'
|
|
32
|
-
export const INFO_TOP_POSITION_STOP = 'top position stop'
|
|
33
|
-
export const INFO_BOTTOM_POSITION_STOP = 'bottom position stop'
|
|
34
|
-
export const INFO_INTERMEDIATE_POSITION_STOP = 'intermediate position stop'
|
|
35
|
-
export const INFO_TILT_VENTILATION_POS_STOP = 'tilt ventilation position stop'
|
|
36
|
-
export const INFO_BLOCKING = 'blocking'
|
|
37
|
-
export const INFO_OVERHEATED = 'overheated'
|
|
38
|
-
export const INFO_TIMEOUT = 'timeout'
|
|
39
|
-
export const INFO_START_TO_MOVE_UP = 'start to move up'
|
|
40
|
-
export const INFO_START_TO_MOVE_DOWN = 'start to move down'
|
|
41
|
-
export const INFO_MOVING_UP = 'moving up'
|
|
42
|
-
export const INFO_MOVING_DOWN = 'moving down'
|
|
43
|
-
export const INFO_STOPPED_IN_UNDEFINED_POSITION =
|
|
44
|
-
'stopped in undefined position'
|
|
45
|
-
export const INFO_TOP_POS_STOP_WICH_TILT_POS =
|
|
46
|
-
'top position stop wich is tilt position'
|
|
47
|
-
export const INFO_BOTTOM_POS_STOP_WICH_INT_POS =
|
|
48
|
-
'bottom position stop wich is intermediate position'
|
|
49
|
-
export const INFO_SWITCHING_DEVICE_SWITCHED_OFF =
|
|
50
|
-
'switching device switched off'
|
|
51
|
-
export const INFO_SWITCHING_DEVICE_SWITCHED_ON = 'switching device switched on'
|
|
52
|
-
|
package/src/domain/enums.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export enum ControlCommand {
|
|
2
|
-
stop = 0x10,
|
|
3
|
-
up = 0x20,
|
|
4
|
-
ventilationPosTilting = 0x24,
|
|
5
|
-
down = 0x40,
|
|
6
|
-
intermediatePos = 0x44,
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export enum EasyCommand {
|
|
10
|
-
EASY_CHECK = 0x4a,
|
|
11
|
-
EASY_CONFIRM = 0x4b,
|
|
12
|
-
EASY_SEND = 0x4c,
|
|
13
|
-
EASY_ACK = 0x4d,
|
|
14
|
-
EASY_INFO = 0x4e,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export enum InfoData {
|
|
18
|
-
INFO_UNKNOWN = -1,
|
|
19
|
-
INFO_NO_INFORMATION = 0x00,
|
|
20
|
-
INFO_TOP_POSITION_STOP = 0x01,
|
|
21
|
-
INFO_BOTTOM_POSITION_STOP = 0x02,
|
|
22
|
-
INFO_INTERMEDIATE_POSITION_STOP = 0x03,
|
|
23
|
-
INFO_TILT_VENTILATION_POS_STOP = 0x04,
|
|
24
|
-
INFO_BLOCKING = 0x05,
|
|
25
|
-
INFO_OVERHEATED = 0x06,
|
|
26
|
-
INFO_TIMEOUT = 0x07,
|
|
27
|
-
INFO_START_TO_MOVE_UP = 0x08,
|
|
28
|
-
INFO_START_TO_MOVE_DOWN = 0x09,
|
|
29
|
-
INFO_MOVING_UP = 0x0a,
|
|
30
|
-
INFO_MOVING_DOWN = 0x0b,
|
|
31
|
-
INFO_STOPPED_IN_UNDEFINED_POSITION = 0x0d,
|
|
32
|
-
INFO_TOP_POS_STOP_WICH_TILT_POS = 0x0e,
|
|
33
|
-
INFO_BOTTOM_POS_STOP_WICH_INT_POS = 0x0f,
|
|
34
|
-
INFO_SWITCHING_DEVICE_SWITCHED_OFF = 0x10,
|
|
35
|
-
INFO_SWITCHING_DEVICE_SWITCHED_ON = 0x11,
|
|
36
|
-
}
|
package/src/domain/types.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type Dictionary<T> = { [key: number]: T }
|
package/src/index.ts
DELETED
package/src/model/Response.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { EasyCommand, InfoData } from "../domain/enums";
|
|
2
|
-
|
|
3
|
-
export interface Response {
|
|
4
|
-
header: number
|
|
5
|
-
|
|
6
|
-
length: number
|
|
7
|
-
|
|
8
|
-
command: EasyCommand
|
|
9
|
-
|
|
10
|
-
activeChannels: number[]
|
|
11
|
-
|
|
12
|
-
checksum: number
|
|
13
|
-
|
|
14
|
-
status: InfoData | null
|
|
15
|
-
|
|
16
|
-
statusCode: number
|
|
17
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { UsbTransmitterClient } from "../src/UsbTransmitterClient"
|
|
2
|
-
import { ControlCommand, EasyCommand, InfoData } from "../src/domain/enums"
|
|
3
|
-
import * as fs from 'fs'
|
|
4
|
-
|
|
5
|
-
jest.setTimeout(200000)
|
|
6
|
-
|
|
7
|
-
const devPath = '/dev/ttyUSB0'
|
|
8
|
-
const client = new UsbTransmitterClient(devPath)
|
|
9
|
-
|
|
10
|
-
const aktiveChannelsForTest = [1, 2]
|
|
11
|
-
|
|
12
|
-
// Skip tests if hardware not present
|
|
13
|
-
const describeHardware = fs.existsSync(devPath) ? describe : describe.skip
|
|
14
|
-
|
|
15
|
-
describeHardware('Integration Tests (Hardware)', () => {
|
|
16
|
-
|
|
17
|
-
test('checkChannels', async () => {
|
|
18
|
-
await client.open()
|
|
19
|
-
const channels = await client.checkChannels()
|
|
20
|
-
expect(channels).toEqual(aktiveChannelsForTest)
|
|
21
|
-
await client.close()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('getInfo', async () => {
|
|
25
|
-
await client.open()
|
|
26
|
-
const response = await client.getInfo(1)
|
|
27
|
-
console.log(response)
|
|
28
|
-
expect(response.command).toEqual(EasyCommand.EASY_ACK)
|
|
29
|
-
await client.close()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('sendControlCommand', async () => {
|
|
33
|
-
await client.open()
|
|
34
|
-
const response = await client.sendControlCommand(1, ControlCommand.down)
|
|
35
|
-
console.log(response)
|
|
36
|
-
expect(response.status).toEqual(InfoData.INFO_MOVING_DOWN)
|
|
37
|
-
await client.close()
|
|
38
|
-
})
|
|
39
|
-
})
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { UsbTransmitterClient } from '../src/UsbTransmitterClient'
|
|
3
|
-
import { ControlCommand, EasyCommand, InfoData } from '../src/domain/enums'
|
|
4
|
-
import { SerialPort } from 'serialport'
|
|
5
|
-
import { BYTE_HEADER } from '../src/domain/constants'
|
|
6
|
-
|
|
7
|
-
// Mock entire serialport module
|
|
8
|
-
jest.mock('serialport')
|
|
9
|
-
|
|
10
|
-
describe('UsbTransmitterClient (Mocked)', () => {
|
|
11
|
-
let client: UsbTransmitterClient
|
|
12
|
-
let mockSerialPortInstance: any
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
// Reset mocks
|
|
16
|
-
jest.clearAllMocks()
|
|
17
|
-
|
|
18
|
-
// Setup mock instance
|
|
19
|
-
mockSerialPortInstance = {
|
|
20
|
-
isOpen: false,
|
|
21
|
-
open: jest.fn((cb) => {
|
|
22
|
-
mockSerialPortInstance.isOpen = true
|
|
23
|
-
if (cb) cb(null)
|
|
24
|
-
}),
|
|
25
|
-
close: jest.fn((cb) => {
|
|
26
|
-
mockSerialPortInstance.isOpen = false
|
|
27
|
-
if (cb) cb(null)
|
|
28
|
-
}),
|
|
29
|
-
write: jest.fn((data, cb) => {
|
|
30
|
-
if (cb) cb(null)
|
|
31
|
-
}),
|
|
32
|
-
flush: jest.fn((cb) => {
|
|
33
|
-
if (cb) cb(null)
|
|
34
|
-
}),
|
|
35
|
-
once: jest.fn(),
|
|
36
|
-
read: jest.fn(),
|
|
37
|
-
pipe: jest.fn(),
|
|
38
|
-
on: jest.fn(),
|
|
39
|
-
removeListener: jest.fn()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// When new SerialPort() is called, return our mock instance
|
|
43
|
-
; (SerialPort as unknown as jest.Mock).mockImplementation(() => mockSerialPortInstance)
|
|
44
|
-
|
|
45
|
-
client = new UsbTransmitterClient('/dev/ttyMOCKED')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('open() should open the serial port', async () => {
|
|
49
|
-
await client.open()
|
|
50
|
-
expect(mockSerialPortInstance.open).toHaveBeenCalled()
|
|
51
|
-
expect(mockSerialPortInstance.flush).toHaveBeenCalled()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('close() should close the serial port', async () => {
|
|
55
|
-
await client.close()
|
|
56
|
-
expect(mockSerialPortInstance.close).toHaveBeenCalled()
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('checkChannels() calls correct command and parses response', async () => {
|
|
60
|
-
await client.open()
|
|
61
|
-
|
|
62
|
-
// Simulate "readable" event and data read
|
|
63
|
-
mockSerialPortInstance.once.mockImplementation((event: string, cb: Function) => {
|
|
64
|
-
if (event === 'readable') {
|
|
65
|
-
// Trigger the callback immediately to simulate data ready
|
|
66
|
-
cb()
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
// Mock response for check channels (Head, Len, Cmd, Byte3...CS)
|
|
71
|
-
// Response length check is 6 bytes.
|
|
72
|
-
// Byte 3 is bitmap of active channels (1-8). let's say ch 1 and 2 are active (binary 00000011 = 3)
|
|
73
|
-
const responseBuffer = Buffer.from([
|
|
74
|
-
BYTE_HEADER, // 0xAA
|
|
75
|
-
0x04, // Length
|
|
76
|
-
EasyCommand.EASY_CHECK,
|
|
77
|
-
0x00, // High channels (starts at 9)
|
|
78
|
-
0x03, // Low channels (starts at 1, so 1 & 2)
|
|
79
|
-
0x00 // Checksum (ignored for now in mock, or we calculate it if logic is strict)
|
|
80
|
-
])
|
|
81
|
-
// Fix checksum if logic requires it: 256 - sum
|
|
82
|
-
const sum = responseBuffer[0] + responseBuffer[1] + responseBuffer[2] + responseBuffer[3] + responseBuffer[4]
|
|
83
|
-
responseBuffer[5] = (256 - (sum % 256)) % 256
|
|
84
|
-
|
|
85
|
-
mockSerialPortInstance.read.mockReturnValue(responseBuffer)
|
|
86
|
-
|
|
87
|
-
const channels = await client.checkChannels()
|
|
88
|
-
expect(channels).toEqual(expect.arrayContaining([1, 2]))
|
|
89
|
-
expect(mockSerialPortInstance.write).toHaveBeenCalledWith(
|
|
90
|
-
expect.arrayContaining([BYTE_HEADER, 0x02, EasyCommand.EASY_CHECK]),
|
|
91
|
-
expect.any(Function)
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('sendControlCommand() sends correct bytes', async () => {
|
|
96
|
-
await client.open()
|
|
97
|
-
|
|
98
|
-
mockSerialPortInstance.once.mockImplementation((event: string, cb: Function) => {
|
|
99
|
-
if (event === 'readable') cb()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Response for Info (length 7)
|
|
103
|
-
// 0: AA, 1: 05, 2: EASY_SEND, 3: high, 4: low, 5: status, 6: CS
|
|
104
|
-
const responseBuffer = Buffer.from([
|
|
105
|
-
BYTE_HEADER,
|
|
106
|
-
0x05,
|
|
107
|
-
EasyCommand.EASY_SEND,
|
|
108
|
-
0x00,
|
|
109
|
-
0x01, // Channel 1 bit mask
|
|
110
|
-
InfoData.INFO_MOVING_DOWN,
|
|
111
|
-
0x00
|
|
112
|
-
])
|
|
113
|
-
const sum = responseBuffer.slice(0, 6).reduce((a, b) => a + b, 0)
|
|
114
|
-
responseBuffer[6] = (256 - (sum % 256)) % 256
|
|
115
|
-
|
|
116
|
-
mockSerialPortInstance.read.mockReturnValue(responseBuffer)
|
|
117
|
-
|
|
118
|
-
const response = await client.sendControlCommand(1, ControlCommand.down)
|
|
119
|
-
|
|
120
|
-
expect(response.status).toBe(InfoData.INFO_MOVING_DOWN)
|
|
121
|
-
|
|
122
|
-
// Verify write arguments
|
|
123
|
-
// Data: [AA, 05, EASY_SEND, high, low, cmd, CS]
|
|
124
|
-
// Channel 1 -> low=1, high=0
|
|
125
|
-
expect(mockSerialPortInstance.write).toHaveBeenCalledWith(
|
|
126
|
-
expect.arrayContaining([
|
|
127
|
-
BYTE_HEADER,
|
|
128
|
-
0x05,
|
|
129
|
-
EasyCommand.EASY_SEND,
|
|
130
|
-
0,
|
|
131
|
-
1,
|
|
132
|
-
ControlCommand.down
|
|
133
|
-
]),
|
|
134
|
-
expect.any(Function)
|
|
135
|
-
)
|
|
136
|
-
})
|
|
137
|
-
test('getInfo() should handle fragmented packets (reproduction fix)', async () => {
|
|
138
|
-
await client.open()
|
|
139
|
-
|
|
140
|
-
let readableCallback: Function | null = null;
|
|
141
|
-
mockSerialPortInstance.on.mockImplementation((event: string, cb: Function) => {
|
|
142
|
-
if (event === 'readable') {
|
|
143
|
-
readableCallback = cb
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
let readCallCount = 0
|
|
148
|
-
mockSerialPortInstance.read.mockImplementation((len: number) => {
|
|
149
|
-
readCallCount++
|
|
150
|
-
if (readCallCount === 1) {
|
|
151
|
-
return null // Not enough data yet
|
|
152
|
-
}
|
|
153
|
-
// Return dummy response buffer for getInfo call
|
|
154
|
-
const responseBuffer = Buffer.from([
|
|
155
|
-
BYTE_HEADER,
|
|
156
|
-
0x05,
|
|
157
|
-
EasyCommand.EASY_SEND,
|
|
158
|
-
0x00,
|
|
159
|
-
0x01,
|
|
160
|
-
InfoData.INFO_MOVING_DOWN,
|
|
161
|
-
0x00
|
|
162
|
-
])
|
|
163
|
-
const sum = responseBuffer.slice(0, 6).reduce((a, b) => a + b, 0)
|
|
164
|
-
responseBuffer[6] = (256 - (sum % 256)) % 256
|
|
165
|
-
return responseBuffer
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const infoPromise = client.getInfo(1)
|
|
169
|
-
|
|
170
|
-
// Wait a tick to ensure tryRead() ran once and failed
|
|
171
|
-
await new Promise(r => process.nextTick(r))
|
|
172
|
-
|
|
173
|
-
// Now trigger readable event again (simulation of second packet arriving)
|
|
174
|
-
if (readableCallback) {
|
|
175
|
-
(readableCallback as Function)()
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const response = await infoPromise
|
|
179
|
-
expect(response.status).toBe(InfoData.INFO_MOVING_DOWN)
|
|
180
|
-
expect(readCallCount).toBeGreaterThanOrEqual(2)
|
|
181
|
-
})
|
|
182
|
-
})
|