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 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
+ }