nova-control-node 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/LICENSE.md +9 -0
- package/README.md +203 -0
- package/dist/nova-control-node.d.ts +53 -0
- package/dist/nova-control-node.js +129 -0
- package/package.json +51 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Andreas Rozek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# nova-control-node
|
|
2
|
+
|
|
3
|
+
Node.js ESM module for controlling the [Creoqode Nova DIY AI Robot](../../README.md) over USB serial via the `serialport` package.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
| requirement | details |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **Node.js 22+** | required at runtime and for the build toolchain. Download from [nodejs.org](https://nodejs.org). |
|
|
12
|
+
| **`serialport` ≥ 12** | runtime dependency — installed automatically with this package. Requires a C++ build toolchain (`node-gyp`) on first install if no pre-built binary is available for your platform. |
|
|
13
|
+
| **USB serial permissions** | on Linux, add your user to the `dialout` group (`sudo usermod -aG dialout $USER`) and re-login. On macOS and Windows no extra configuration is normally required. |
|
|
14
|
+
| **Arduino sketch** | the matching `Nova_SerialController.ino` sketch must be flashed to the robot's Arduino board (baud rate 9600, 8N1). |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm add nova-control-node
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
### Constants
|
|
29
|
+
|
|
30
|
+
| export | type | value | description |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `BaudRate` | `number` | `9600` | baud rate expected by the Arduino sketch |
|
|
33
|
+
| `HomePosition` | `ServoState` | `{ s1:90, s2:90, s3:110, s4:90, s5:95 }` | safe resting position for all five servos |
|
|
34
|
+
| `SafeRange` | `Record<ServoKey, [number, number]>` | see below | per-servo `[min, max]` in degrees |
|
|
35
|
+
|
|
36
|
+
`SafeRange` values:
|
|
37
|
+
|
|
38
|
+
| servo | pin | role | min | max |
|
|
39
|
+
| --- | --- | --- | --- | --- |
|
|
40
|
+
| `s1` | 32 | head front/back | 45 | 135 |
|
|
41
|
+
| `s2` | 34 | head CW/CCW | 10 | 170 |
|
|
42
|
+
| `s3` | 36 | head up/down | 40 | 150 |
|
|
43
|
+
| `s4` | 38 | body rotation | 30 | 180 |
|
|
44
|
+
| `s5` | 40 | secondary head | 20 | 150 |
|
|
45
|
+
|
|
46
|
+
Both `HomePosition` and `SafeRange` are frozen (`Object.isFrozen` returns `true`).
|
|
47
|
+
|
|
48
|
+
### `buildDirectPacket`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
function buildDirectPacket (State:ServoState):Uint8Array
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Assembles a 5-byte direct servo control packet from the given servo state. Each value is clamped to `SafeRange` and rounded to the nearest integer before encoding. The byte order matches the Arduino sketch:
|
|
55
|
+
|
|
56
|
+
| byte | servo |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| 0 | s4 |
|
|
59
|
+
| 1 | s3 |
|
|
60
|
+
| 2 | s2 |
|
|
61
|
+
| 3 | s1 |
|
|
62
|
+
| 4 | s5 |
|
|
63
|
+
|
|
64
|
+
### `openNova`
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
async function openNova (PortPath:string):Promise<NovaController>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Opens the USB serial port at `PortPath` and returns a `NovaController`.
|
|
71
|
+
|
|
72
|
+
`PortPath` examples: `/dev/ttyACM0` or `/dev/ttyUSB0` on Linux/macOS, `COM3` on Windows.
|
|
73
|
+
|
|
74
|
+
The returned promise rejects if the port cannot be opened (e.g. wrong path, permission denied, or device not connected). On success the promise resolves after the 2-second Arduino reset delay that follows every port open.
|
|
75
|
+
|
|
76
|
+
### `NovaController`
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
interface NovaController {
|
|
80
|
+
home ():Promise<void>
|
|
81
|
+
shiftHeadTo (Degrees:number):Promise<void>
|
|
82
|
+
rollHeadTo (Degrees:number):Promise<void>
|
|
83
|
+
pitchHeadTo (Degrees:number):Promise<void>
|
|
84
|
+
liftHeadTo (Degrees:number):Promise<void>
|
|
85
|
+
rotateBodyTo (Degrees:number):Promise<void>
|
|
86
|
+
get State ():ServoState
|
|
87
|
+
set State (Update:ServoUpdate)
|
|
88
|
+
sendServoState ():Promise<void>
|
|
89
|
+
destroy ():void
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| method / property | description |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `home()` | sends all servos to `HomePosition` |
|
|
96
|
+
| `shiftHeadTo(Degrees)` | sets s1 — head forward `> 90°`, back `< 90°` |
|
|
97
|
+
| `rollHeadTo(Degrees)` | sets s2 — head clockwise `> 90°`, counter-clockwise `< 90°` |
|
|
98
|
+
| `pitchHeadTo(Degrees)` | sets s3 — head up `> 110°`, down toward `40°` |
|
|
99
|
+
| `liftHeadTo(Degrees)` | sets s5 — secondary head up/down, range `20°`–`150°` |
|
|
100
|
+
| `rotateBodyTo(Degrees)` | sets s4 — rotates the entire body around the Z-axis |
|
|
101
|
+
| `State` (get) | returns a deep copy of the pending state if any, else the last-sent state |
|
|
102
|
+
| `State` (set) | replaces any pending entry with `Update` merged onto the *last-sent* state (not onto pending); flush with `sendServoState()` |
|
|
103
|
+
| `sendServoState()` | flushes any pending state update to the Arduino |
|
|
104
|
+
| `destroy()` | closes the serial port |
|
|
105
|
+
|
|
106
|
+
`State` reflects what was *sent* (or is pending to be sent) to the Arduino, not the physical servo position — there is no read-back channel in the protocol.
|
|
107
|
+
|
|
108
|
+
Sends are serialised internally: concurrent method calls and `sendServoState()` calls never overlap on the wire. Each write awaits both `Port.write` and `Port.drain` before resolving, ensuring the 5-byte packet is fully flushed to the OS serial buffer. Named methods such as `shiftHeadTo()` *accumulate* changes on top of whatever is already pending; the `State` setter instead *replaces* the pending entry, starting fresh from the last-sent state.
|
|
109
|
+
|
|
110
|
+
### Types
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type ServoKey = 's1' | 's2' | 's3' | 's4' | 's5'
|
|
114
|
+
type ServoState = { [K in ServoKey]:number }
|
|
115
|
+
type ServoUpdate = Partial<ServoState>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
### Basic usage
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { openNova } from 'nova-control-node'
|
|
126
|
+
|
|
127
|
+
const Nova = await openNova('/dev/ttyACM0') // or 'COM3' on Windows
|
|
128
|
+
|
|
129
|
+
await Nova.home()
|
|
130
|
+
await Nova.rotateBodyTo(120)
|
|
131
|
+
await Nova.shiftHeadTo(100)
|
|
132
|
+
await Nova.rollHeadTo(60)
|
|
133
|
+
|
|
134
|
+
console.log(Nova.State)
|
|
135
|
+
// → { s1:100, s2:60, s3:110, s4:120, s5:95 }
|
|
136
|
+
|
|
137
|
+
Nova.destroy()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Two queue strategies
|
|
141
|
+
|
|
142
|
+
**Named methods accumulate** — each call merges its change on top of whatever is already pending:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// none of these three lines sends anything yet
|
|
146
|
+
Nova.shiftHeadTo(80) // pending: { ...home, s1:80 }
|
|
147
|
+
Nova.rollHeadTo(60) // pending: { ...home, s1:80, s2:60 }
|
|
148
|
+
Nova.rotateBodyTo(120) // pending: { ...home, s1:80, s2:60, s4:120 }
|
|
149
|
+
await Nova.sendServoState()
|
|
150
|
+
// sends { s1:80, s2:60, s3:110, s4:120, s5:95 }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**The `State` setter replaces** the pending entry — it starts fresh from the last-sent state, discarding any pending changes:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
Nova.State = { s4:120 }
|
|
157
|
+
Nova.State = { s1:100, s2:60 } // replaces previous; s4 reverts to last-sent
|
|
158
|
+
await Nova.sendServoState()
|
|
159
|
+
// sends { s1:100, s2:60, s3:110, s4:90, s5:95 } ← s4 is 90, not 120
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Error handling
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { openNova } from 'nova-control-node'
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const Nova = await openNova('/dev/ttyACM0')
|
|
169
|
+
await Nova.home()
|
|
170
|
+
Nova.destroy()
|
|
171
|
+
} catch (Signal) {
|
|
172
|
+
console.error('Could not open serial port:', Signal)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Listing available serial ports
|
|
177
|
+
|
|
178
|
+
The `serialport` package provides a utility for enumerating connected devices:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { SerialPort } from 'serialport'
|
|
182
|
+
|
|
183
|
+
const Ports = await SerialPort.list()
|
|
184
|
+
Ports.forEach((Port) => {
|
|
185
|
+
console.log(Port.path, Port.manufacturer ?? '(unknown)')
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Building
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm run build --workspace packages/nova-control-node
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Output is written to `packages/nova-control-node/dist/`.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
[MIT License](../../LICENSE.md) © Andreas Rozek
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const BaudRate = 9600;
|
|
2
|
+
|
|
3
|
+
/**** buildDirectPacket — 5-byte direct servo control packet ****/
|
|
4
|
+
export declare function buildDirectPacket(State: ServoState): Uint8Array;
|
|
5
|
+
|
|
6
|
+
/**** HomePosition ****/
|
|
7
|
+
export declare const HomePosition: Readonly<ServoState>;
|
|
8
|
+
|
|
9
|
+
/**** NovaController ****/
|
|
10
|
+
export declare interface NovaController {
|
|
11
|
+
home(): Promise<void>;
|
|
12
|
+
shiftHeadTo(Degrees: number): Promise<void>;
|
|
13
|
+
rollHeadTo(Degrees: number): Promise<void>;
|
|
14
|
+
pitchHeadTo(Degrees: number): Promise<void>;
|
|
15
|
+
liftHeadTo(Degrees: number): Promise<void>;
|
|
16
|
+
rotateBodyTo(Degrees: number): Promise<void>;
|
|
17
|
+
get State(): ServoState;
|
|
18
|
+
set State(Update: ServoUpdate);
|
|
19
|
+
sendServoState(): Promise<void>;
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**** NovaOptions ****/
|
|
24
|
+
export declare interface NovaOptions {
|
|
25
|
+
StepIntervalMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**** openNova — factory ****/
|
|
29
|
+
export declare function openNova(PortPath: string, Rate?: number, Options?: NovaOptions): Promise<NovaController>;
|
|
30
|
+
|
|
31
|
+
/**** SafeRange ****/
|
|
32
|
+
export declare const SafeRange: Readonly<Record<ServoKey, [number, number]>>;
|
|
33
|
+
|
|
34
|
+
/*******************************************************************************
|
|
35
|
+
* *
|
|
36
|
+
* nova-control-node *
|
|
37
|
+
* *
|
|
38
|
+
*******************************************************************************/
|
|
39
|
+
/**** ServoKey ****/
|
|
40
|
+
export declare type ServoKey = 's1' | 's2' | 's3' | 's4' | 's5';
|
|
41
|
+
|
|
42
|
+
/**** ServoSpeed — °/ms so that the full safe range takes exactly 1 second ****/
|
|
43
|
+
export declare const ServoSpeed: Readonly<Record<ServoKey, number>>;
|
|
44
|
+
|
|
45
|
+
/**** ServoState ****/
|
|
46
|
+
export declare type ServoState = {
|
|
47
|
+
[K in ServoKey]: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**** ServoUpdate ****/
|
|
51
|
+
export declare type ServoUpdate = Partial<ServoState>;
|
|
52
|
+
|
|
53
|
+
export { }
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { SerialPort as e } from "serialport";
|
|
2
|
+
//#region src/nova-control-node.ts
|
|
3
|
+
var t = 9600, n = Object.freeze({
|
|
4
|
+
s1: 90,
|
|
5
|
+
s2: 90,
|
|
6
|
+
s3: 110,
|
|
7
|
+
s4: 90,
|
|
8
|
+
s5: 95
|
|
9
|
+
}), r = Object.freeze({
|
|
10
|
+
s1: [45, 135],
|
|
11
|
+
s2: [10, 170],
|
|
12
|
+
s3: [40, 150],
|
|
13
|
+
s4: [30, 180],
|
|
14
|
+
s5: [20, 150]
|
|
15
|
+
}), i = Object.freeze({
|
|
16
|
+
s1: (r.s1[1] - r.s1[0]) / 1e3,
|
|
17
|
+
s2: (r.s2[1] - r.s2[0]) / 1e3,
|
|
18
|
+
s3: (r.s3[1] - r.s3[0]) / 1e3,
|
|
19
|
+
s4: (r.s4[1] - r.s4[0]) / 1e3,
|
|
20
|
+
s5: (r.s5[1] - r.s5[0]) / 1e3
|
|
21
|
+
});
|
|
22
|
+
function a(e, t) {
|
|
23
|
+
let [n, i] = r[t];
|
|
24
|
+
return Math.max(n, Math.min(i, Math.round(e)));
|
|
25
|
+
}
|
|
26
|
+
function o(e) {
|
|
27
|
+
return new Uint8Array([
|
|
28
|
+
a(e.s4, "s4"),
|
|
29
|
+
a(e.s3, "s3"),
|
|
30
|
+
a(e.s2, "s2"),
|
|
31
|
+
a(e.s1, "s1"),
|
|
32
|
+
a(e.s5, "s5")
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
async function s(t, n) {
|
|
36
|
+
let r = new e({
|
|
37
|
+
path: t,
|
|
38
|
+
baudRate: n,
|
|
39
|
+
autoOpen: !1
|
|
40
|
+
});
|
|
41
|
+
return await new Promise((e, t) => {
|
|
42
|
+
r.open((n) => {
|
|
43
|
+
n == null ? e() : t(n);
|
|
44
|
+
});
|
|
45
|
+
}), await new Promise((e) => setTimeout(e, 2e3)), {
|
|
46
|
+
async write(e) {
|
|
47
|
+
await new Promise((t, n) => {
|
|
48
|
+
r.write(Buffer.from(e), (e) => {
|
|
49
|
+
e == null ? t() : n(e);
|
|
50
|
+
});
|
|
51
|
+
}), await new Promise((e, t) => {
|
|
52
|
+
r.drain((n) => {
|
|
53
|
+
n == null ? e() : t(n);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
destroy() {
|
|
58
|
+
r.close();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function c(e, r = t, a) {
|
|
63
|
+
let c = a?.StepIntervalMs ?? 20, l = await s(e, r), u = { ...n }, d, f = Promise.resolve();
|
|
64
|
+
function p(e) {
|
|
65
|
+
d = {
|
|
66
|
+
...d ?? u,
|
|
67
|
+
...e
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function m() {
|
|
71
|
+
let e = f;
|
|
72
|
+
f = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
await e;
|
|
75
|
+
} catch {}
|
|
76
|
+
for (; d != null;) {
|
|
77
|
+
let e = d, t = !0, n = { ...u };
|
|
78
|
+
for (let r of [
|
|
79
|
+
"s1",
|
|
80
|
+
"s2",
|
|
81
|
+
"s3",
|
|
82
|
+
"s4",
|
|
83
|
+
"s5"
|
|
84
|
+
]) {
|
|
85
|
+
let a = e[r] - u[r], o = c > 0 ? i[r] * c : Infinity;
|
|
86
|
+
Math.abs(a) > o ? (n[r] = u[r] + Math.sign(a) * o, t = !1) : n[r] = e[r];
|
|
87
|
+
}
|
|
88
|
+
t && (d = void 0), u = { ...n }, await l.write(o(n)), t || await new Promise((e) => setTimeout(e, c));
|
|
89
|
+
}
|
|
90
|
+
})(), await f;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
async home() {
|
|
94
|
+
p({ ...n }), await m();
|
|
95
|
+
},
|
|
96
|
+
async shiftHeadTo(e) {
|
|
97
|
+
p({ s1: e }), await m();
|
|
98
|
+
},
|
|
99
|
+
async rollHeadTo(e) {
|
|
100
|
+
p({ s2: e }), await m();
|
|
101
|
+
},
|
|
102
|
+
async pitchHeadTo(e) {
|
|
103
|
+
p({ s3: e }), await m();
|
|
104
|
+
},
|
|
105
|
+
async liftHeadTo(e) {
|
|
106
|
+
p({ s5: e }), await m();
|
|
107
|
+
},
|
|
108
|
+
async rotateBodyTo(e) {
|
|
109
|
+
p({ s4: e }), await m();
|
|
110
|
+
},
|
|
111
|
+
get State() {
|
|
112
|
+
return structuredClone(d ?? u);
|
|
113
|
+
},
|
|
114
|
+
set State(e) {
|
|
115
|
+
d = {
|
|
116
|
+
...u,
|
|
117
|
+
...e
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
async sendServoState() {
|
|
121
|
+
await m();
|
|
122
|
+
},
|
|
123
|
+
destroy() {
|
|
124
|
+
l.destroy();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { t as BaudRate, n as HomePosition, r as SafeRange, i as ServoSpeed, o as buildDirectPacket, c as openNova };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nova-control-node",
|
|
3
|
+
"description": "Control a NOVA DIY Artificial Intelligence Robot by Creoqode from Node.js",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nova",
|
|
8
|
+
"creoqode",
|
|
9
|
+
"robot",
|
|
10
|
+
"serial",
|
|
11
|
+
"node.js"
|
|
12
|
+
],
|
|
13
|
+
"author": "Andreas Rozek",
|
|
14
|
+
"homepage": "https://github.com/rozek/nova-control#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/rozek/nova-control.git",
|
|
18
|
+
"directory": "packages/nova-control-node"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/rozek/nova-control/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"main": "./dist/nova-control-node.js",
|
|
25
|
+
"module": "./dist/nova-control-node.js",
|
|
26
|
+
"types": "./dist/nova-control-node.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/nova-control-node.js",
|
|
30
|
+
"types": "./dist/nova-control-node.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"serialport": "^12.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vite": "^8.0.0",
|
|
43
|
+
"vite-plugin-dts": "^4.5.4",
|
|
44
|
+
"vitest": "^4.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "vite build",
|
|
48
|
+
"test": "vitest",
|
|
49
|
+
"test:run": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|