nova-control-command 0.0.8 → 0.0.9
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 +8 -8
- package/dist/nova-control-command.js +107 -87
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -83,22 +83,22 @@ Sends all five servos to their home positions simultaneously. Home positions are
|
|
|
83
83
|
Each command sets exactly one servo and leaves all others at their last-sent positions.
|
|
84
84
|
|
|
85
85
|
```
|
|
86
|
-
nova-control --port <path> shift-to <
|
|
87
|
-
nova-control --port <path> roll-to <
|
|
88
|
-
nova-control --port <path> pitch-to <
|
|
89
|
-
nova-control --port <path> rotate-to <
|
|
90
|
-
nova-control --port <path> lift-to <
|
|
86
|
+
nova-control --port <path> shift-to <angle> [--within-ms <ms>] # head forward (>90°) or back (<90°) — s1
|
|
87
|
+
nova-control --port <path> roll-to <angle> [--within-ms <ms>] # head clockwise (>90°) or counter-clockwise (<90°) — s2
|
|
88
|
+
nova-control --port <path> pitch-to <angle> [--within-ms <ms>] # head up (>110°) or down (<110°) — s3
|
|
89
|
+
nova-control --port <path> rotate-to <angle> [--within-ms <ms>] # body rotation around Z-axis — s4
|
|
90
|
+
nova-control --port <path> lift-to <angle> [--within-ms <ms>] # secondary head lift, range 20°–150° — s5
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
`<
|
|
93
|
+
`<angle>` is a number in degrees. The firmware clamps out-of-range values silently. With `--within-ms`, uses a trapezoidal ramp-up/ramp-down profile to complete the movement in the specified number of milliseconds.
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
97
|
### `move`
|
|
98
98
|
|
|
99
99
|
```
|
|
100
|
-
nova-control --port <path> move [--shift-to <
|
|
101
|
-
[--rotate-to <
|
|
100
|
+
nova-control --port <path> move [--shift-to <angle>] [--roll-to <angle>] [--pitch-to <angle>]
|
|
101
|
+
[--rotate-to <angle>] [--lift-to <angle>] [--within-ms <ms>]
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
Sets multiple servos in a single packet. At least one servo option is required. Servos not mentioned stay at their last-sent positions. With `--within-ms`, uses a trapezoidal ramp-up/ramp-down profile to complete the movement in the specified number of milliseconds. Useful when two or more joints must move simultaneously.
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { fileURLToPath as e } from "node:url";
|
|
3
3
|
import { realpathSync as t } from "node:fs";
|
|
4
4
|
import { Command as n } from "commander";
|
|
5
|
-
import {
|
|
6
|
-
import i from "node
|
|
7
|
-
import a from "node:
|
|
5
|
+
import { z as r } from "zod";
|
|
6
|
+
import { openNova as i } from "nova-control-node";
|
|
7
|
+
import a from "node:readline";
|
|
8
|
+
import o from "node:fs/promises";
|
|
8
9
|
//#region src/CommandTokenizer.ts
|
|
9
|
-
function
|
|
10
|
+
function s(e) {
|
|
10
11
|
let t = [], n = "", r = 0;
|
|
11
12
|
for (; r < e.length;) {
|
|
12
13
|
let i = e[r];
|
|
@@ -35,73 +36,73 @@ function o(e) {
|
|
|
35
36
|
}
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region src/REPL.ts
|
|
38
|
-
async function
|
|
39
|
-
let n = process.stdin.isTTY, r = n ? `\x1b[1m${e}>\x1b[0m ` : `${e}> `,
|
|
39
|
+
async function c(e, t) {
|
|
40
|
+
let n = process.stdin.isTTY, r = n ? `\x1b[1m${e}>\x1b[0m ` : `${e}> `, i = a.createInterface({
|
|
40
41
|
input: process.stdin,
|
|
41
42
|
output: process.stdout,
|
|
42
43
|
terminal: n,
|
|
43
44
|
prompt: r
|
|
44
45
|
});
|
|
45
|
-
n && (process.stdout.write("NOVA interactive shell — type \"help [command]\" for help, \"exit\" to quit\n"),
|
|
46
|
-
for await (let r of
|
|
47
|
-
let
|
|
48
|
-
if (
|
|
49
|
-
n &&
|
|
46
|
+
n && (process.stdout.write("NOVA interactive shell — type \"help [command]\" for help, \"exit\" to quit\n"), i.prompt());
|
|
47
|
+
for await (let r of i) {
|
|
48
|
+
let a = r.trim();
|
|
49
|
+
if (a === "" || a.startsWith("#")) {
|
|
50
|
+
n && i.prompt();
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
|
-
if (
|
|
53
|
-
let
|
|
54
|
-
if (
|
|
55
|
-
n &&
|
|
53
|
+
if (a === "exit" || a === "quit") break;
|
|
54
|
+
let o = s(a);
|
|
55
|
+
if (o.length === 0) {
|
|
56
|
+
n && i.prompt();
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
59
|
try {
|
|
59
|
-
await t(
|
|
60
|
+
await t(o);
|
|
60
61
|
} catch (t) {
|
|
61
62
|
process.stderr.write(`${e}: ${t.message}\n`);
|
|
62
63
|
}
|
|
63
|
-
n &&
|
|
64
|
+
n && i.prompt();
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
i.close();
|
|
66
67
|
}
|
|
67
68
|
//#endregion
|
|
68
69
|
//#region src/ScriptRunner.ts
|
|
69
|
-
async function
|
|
70
|
+
async function l(e, t, n) {
|
|
70
71
|
let r;
|
|
71
72
|
if (t === "-") r = process.stdin;
|
|
72
73
|
else try {
|
|
73
|
-
r = (await
|
|
74
|
+
r = (await o.open(t)).createReadStream();
|
|
74
75
|
} catch {
|
|
75
76
|
return process.stderr.write(`nova-control: cannot open script '${t}'\n`), 2;
|
|
76
77
|
}
|
|
77
|
-
let
|
|
78
|
+
let i = a.createInterface({
|
|
78
79
|
input: r,
|
|
79
80
|
terminal: !1
|
|
80
81
|
}), c = 0;
|
|
81
|
-
for await (let t of
|
|
82
|
+
for await (let t of i) {
|
|
82
83
|
let r = t.trim();
|
|
83
84
|
if (r === "" || r.startsWith("#")) continue;
|
|
84
|
-
let
|
|
85
|
-
if (
|
|
86
|
-
let
|
|
85
|
+
let a = s(r);
|
|
86
|
+
if (a.length === 0) continue;
|
|
87
|
+
let o = 0;
|
|
87
88
|
try {
|
|
88
|
-
|
|
89
|
+
o = await n(a);
|
|
89
90
|
} catch (e) {
|
|
90
|
-
|
|
91
|
+
o = 1, process.stderr.write(`nova-control: ${e.message}\n`);
|
|
91
92
|
}
|
|
92
|
-
if (
|
|
93
|
-
case "stop": return
|
|
93
|
+
if (o !== 0) switch (c = o, e) {
|
|
94
|
+
case "stop": return i.close(), o;
|
|
94
95
|
case "continue": break;
|
|
95
96
|
case "ask":
|
|
96
|
-
if (!await
|
|
97
|
+
if (!await u()) return i.close(), o;
|
|
97
98
|
break;
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
return
|
|
101
|
+
return i.close(), c;
|
|
101
102
|
}
|
|
102
|
-
async function
|
|
103
|
+
async function u() {
|
|
103
104
|
return process.stdin.isTTY ? new Promise((e) => {
|
|
104
|
-
let t =
|
|
105
|
+
let t = a.createInterface({
|
|
105
106
|
input: process.stdin,
|
|
106
107
|
output: process.stdout
|
|
107
108
|
});
|
|
@@ -112,106 +113,125 @@ async function l() {
|
|
|
112
113
|
}
|
|
113
114
|
//#endregion
|
|
114
115
|
//#region src/nova-control-command.ts
|
|
115
|
-
var
|
|
116
|
+
var d = {
|
|
116
117
|
OK: 0,
|
|
117
118
|
GeneralError: 1,
|
|
118
119
|
UsageError: 2
|
|
119
|
-
},
|
|
120
|
+
}, f = class extends Error {
|
|
120
121
|
ExitCode;
|
|
121
|
-
constructor(e, t =
|
|
122
|
+
constructor(e, t = d.GeneralError) {
|
|
122
123
|
super(e), this.name = "NovaCommandError", this.ExitCode = t;
|
|
123
124
|
}
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
}, p = r.coerce.number().finite(), m = r.coerce.number().finite().positive(), h = r.coerce.number().finite().nonnegative();
|
|
126
|
+
function g(e, t) {
|
|
127
|
+
try {
|
|
128
|
+
return p.parse(e);
|
|
129
|
+
} catch {
|
|
130
|
+
throw new f(`${t}: '${e}' is not a valid angle — expected a finite number`, d.UsageError);
|
|
131
|
+
}
|
|
128
132
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
133
|
+
function _(e, t) {
|
|
134
|
+
if (e != null) try {
|
|
135
|
+
return m.parse(e);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new f(`${t}: '--within-ms ${e}' is not valid — expected a positive number`, d.UsageError);
|
|
138
|
+
}
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
var v = "nova-control", y, b = 9600, x = "stop", S;
|
|
141
|
+
async function C() {
|
|
142
|
+
if (y == null) throw new f("--port is required — specify the serial port (e.g. /dev/ttyACM0 or COM3)", d.UsageError);
|
|
143
|
+
return S ??= await i(y, b), S;
|
|
134
144
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
145
|
+
function w() {
|
|
146
|
+
S != null && (S.destroy(), S = void 0);
|
|
137
147
|
}
|
|
138
|
-
function
|
|
148
|
+
function T(e, t = 9600, n = "stop") {
|
|
149
|
+
y = e, b = t, x = n;
|
|
150
|
+
}
|
|
151
|
+
function E() {
|
|
152
|
+
w(), y = void 0, b = 9600, x = "stop";
|
|
153
|
+
}
|
|
154
|
+
function D(e) {
|
|
139
155
|
e.exitOverride(), e.configureOutput({ writeErr: () => {} });
|
|
140
|
-
for (let t of e.commands)
|
|
156
|
+
for (let t of e.commands) D(t);
|
|
141
157
|
}
|
|
142
|
-
function
|
|
143
|
-
let t = new n(
|
|
158
|
+
function O(e = !1) {
|
|
159
|
+
let t = new n(v);
|
|
144
160
|
return t.description("NOVA robot arm CLI").allowUnknownOption(!1).configureOutput({ writeErr: () => {} }), e || (t.option("--port <path>", "serial port path (e.g. /dev/ttyACM0 on Linux/macOS, COM3 on Windows)").option("--baud <rate>", "baud rate (default: 9600)", "9600").option("--on-error <mode>", "script error mode: stop | continue | ask (default: stop)"), t.hook("preAction", (e, t) => {
|
|
145
161
|
let n = t.optsWithGlobals();
|
|
146
|
-
|
|
162
|
+
y = n.port, b = Number(n.baud ?? "9600"), x = n.onError ?? "stop";
|
|
147
163
|
})), t.command("home").description("send all servos to their home positions").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e) => {
|
|
148
|
-
let t = e.withinMs
|
|
149
|
-
await (await
|
|
164
|
+
let t = _(e.withinMs, "home");
|
|
165
|
+
await (await C()).home(t);
|
|
150
166
|
}), t.command("move").description("set one or more servo positions without interrupting the others").option("--shift-to <angle>", "shift head forward (>90°) or back (<90°) — s1").option("--roll-to <angle>", "roll head clockwise (>90°) or counter-clockwise (<90°) — s2").option("--pitch-to <angle>", "pitch head up (>110°) or down (<110°) — s3").option("--rotate-to <angle>", "rotate body around Z-axis — s4").option("--lift-to <angle>", "lift head on secondary axis, range 20°–150° — s5").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e) => {
|
|
151
167
|
let t = {};
|
|
152
|
-
if (e.shiftTo != null && (t.s1 =
|
|
153
|
-
let n = e.withinMs
|
|
154
|
-
await (await
|
|
168
|
+
if (e.shiftTo != null && (t.s1 = g(e.shiftTo, "move")), e.rollTo != null && (t.s2 = g(e.rollTo, "move")), e.pitchTo != null && (t.s3 = g(e.pitchTo, "move")), e.rotateTo != null && (t.s4 = g(e.rotateTo, "move")), e.liftTo != null && (t.s5 = g(e.liftTo, "move")), Object.keys(t).length === 0) throw new f("move: specify at least one servo option (--shift-to, --roll-to, --pitch-to, --rotate-to, --lift-to)", d.UsageError);
|
|
169
|
+
let n = _(e.withinMs, "move");
|
|
170
|
+
await (await C()).moveTo(t, n);
|
|
155
171
|
}), t.command("shift-to").description("shift head forward (>90°) or back (<90°) — s1").argument("<angle>", "target angle in degrees").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e, t) => {
|
|
156
|
-
let n =
|
|
157
|
-
await (await
|
|
172
|
+
let n = g(e, "shift-to"), r = _(t.withinMs, "shift-to");
|
|
173
|
+
await (await C()).shiftHeadTo(n, r);
|
|
158
174
|
}), t.command("roll-to").description("roll head clockwise (>90°) or counter-clockwise (<90°) — s2").argument("<angle>", "target angle in degrees").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e, t) => {
|
|
159
|
-
let n =
|
|
160
|
-
await (await
|
|
175
|
+
let n = g(e, "roll-to"), r = _(t.withinMs, "roll-to");
|
|
176
|
+
await (await C()).rollHeadTo(n, r);
|
|
161
177
|
}), t.command("pitch-to").description("pitch head up (>110°) or down (<110°) — s3").argument("<angle>", "target angle in degrees").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e, t) => {
|
|
162
|
-
let n =
|
|
163
|
-
await (await
|
|
178
|
+
let n = g(e, "pitch-to"), r = _(t.withinMs, "pitch-to");
|
|
179
|
+
await (await C()).pitchHeadTo(n, r);
|
|
164
180
|
}), t.command("rotate-to").description("rotate body around Z-axis — s4").argument("<angle>", "target angle in degrees").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e, t) => {
|
|
165
|
-
let n =
|
|
166
|
-
await (await
|
|
181
|
+
let n = g(e, "rotate-to"), r = _(t.withinMs, "rotate-to");
|
|
182
|
+
await (await C()).rotateBodyTo(n, r);
|
|
167
183
|
}), t.command("lift-to").description("lift head on secondary axis, range 20°–150° — s5").argument("<angle>", "target angle in degrees").option("--within-ms <ms>", "move smoothly over this many milliseconds (trapezoidal ramp)").action(async (e, t) => {
|
|
168
|
-
let n =
|
|
169
|
-
await (await
|
|
184
|
+
let n = g(e, "lift-to"), r = _(t.withinMs, "lift-to");
|
|
185
|
+
await (await C()).liftHeadTo(n, r);
|
|
170
186
|
}), t.command("wait").description("pause for <ms> milliseconds before the next command").argument("<ms>", "duration in milliseconds (non-negative number)").action(async (e) => {
|
|
171
|
-
let t
|
|
172
|
-
|
|
187
|
+
let t;
|
|
188
|
+
try {
|
|
189
|
+
t = h.parse(e);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new f(`wait: invalid duration '${e}' — expected a non-negative number`, d.UsageError);
|
|
192
|
+
}
|
|
173
193
|
await new Promise((e) => setTimeout(e, t));
|
|
174
194
|
}), t.command("state").description("print the current servo state as JSON").action(async () => {
|
|
175
|
-
let e = await
|
|
195
|
+
let e = await C();
|
|
176
196
|
process.stdout.write(JSON.stringify(e.State) + "\n");
|
|
177
197
|
}), e || (t.command("shell").description("start an interactive REPL").action(async () => {
|
|
178
|
-
await
|
|
198
|
+
await c(v, (e) => k(e));
|
|
179
199
|
}), t.option("--script <file>", "run commands from a script file (use - for stdin)").action(async (e) => {
|
|
180
200
|
if (e.script != null) {
|
|
181
|
-
let t = await
|
|
201
|
+
let t = await l(x, e.script, k);
|
|
182
202
|
process.exit(t);
|
|
183
|
-
} else process.stdout.write(t.helpInformation()), process.exit(
|
|
203
|
+
} else process.stdout.write(t.helpInformation()), process.exit(d.OK);
|
|
184
204
|
}), t.addHelpCommand(!0)), t;
|
|
185
205
|
}
|
|
186
|
-
async function
|
|
187
|
-
if (e.length === 0) return
|
|
188
|
-
let t =
|
|
189
|
-
|
|
206
|
+
async function k(e) {
|
|
207
|
+
if (e.length === 0) return d.OK;
|
|
208
|
+
let t = O(!0);
|
|
209
|
+
D(t);
|
|
190
210
|
try {
|
|
191
211
|
return await t.parseAsync([
|
|
192
212
|
"node",
|
|
193
|
-
|
|
213
|
+
v,
|
|
194
214
|
...e
|
|
195
|
-
]),
|
|
215
|
+
]), d.OK;
|
|
196
216
|
} catch (t) {
|
|
197
217
|
let n = t;
|
|
198
|
-
return n.code === "commander.help" || n.code === "commander.helpDisplayed" ?
|
|
218
|
+
return n.code === "commander.help" || n.code === "commander.helpDisplayed" ? d.OK : n.code === "commander.unknownCommand" ? (process.stderr.write(`${v}: unknown command '${e[0]}' — try '${v} help'\n`), d.UsageError) : n.code === "commander.unknownOption" || n.code === "commander.missingArgument" || n.code === "commander.missingMandatoryOptionValue" ? (process.stderr.write(`${v}: ${n.message}\n`), d.UsageError) : t instanceof f ? (process.stderr.write(`${v}: ${t.message}\n`), t.ExitCode) : (process.stderr.write(`${v}: ${t.message ?? String(t)}\n`), d.GeneralError);
|
|
199
219
|
}
|
|
200
220
|
}
|
|
201
|
-
async function
|
|
202
|
-
let e =
|
|
203
|
-
|
|
221
|
+
async function A() {
|
|
222
|
+
let e = O();
|
|
223
|
+
D(e);
|
|
204
224
|
try {
|
|
205
225
|
await e.parseAsync(process.argv);
|
|
206
226
|
} catch (t) {
|
|
207
227
|
let n = t;
|
|
208
|
-
(n.code === "commander.help" || n.code === "commander.helpDisplayed" || n.code === "commander.version") && process.exit(
|
|
228
|
+
(n.code === "commander.help" || n.code === "commander.helpDisplayed" || n.code === "commander.version") && process.exit(d.OK), (n.code === "commander.unknownCommand" || n.code === "commander.unknownOption" || n.code === "commander.missingArgument" || n.code === "commander.missingMandatoryOptionValue") && (process.stderr.write(`${v}: ${n.message}\n\n`), process.stderr.write(e.helpInformation()), process.exit(d.UsageError)), t instanceof f && (process.stderr.write(`${v}: ${t.message}\n`), process.exit(t.ExitCode)), process.stderr.write(`${v}: ${t.message ?? String(t)}\n`), process.exit(d.GeneralError);
|
|
209
229
|
} finally {
|
|
210
|
-
|
|
230
|
+
w();
|
|
211
231
|
}
|
|
212
232
|
}
|
|
213
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
214
|
-
process.stderr.write(`${
|
|
233
|
+
t(process.argv[1]) === e(import.meta.url) && A().catch((e) => {
|
|
234
|
+
process.stderr.write(`${v}: fatal: ${e.message ?? e}\n`), process.exit(d.GeneralError);
|
|
215
235
|
});
|
|
216
236
|
//#endregion
|
|
217
|
-
export {
|
|
237
|
+
export { E as _destroyForTests, T as _setupForTests, k as executeTokens };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nova-control-command",
|
|
3
3
|
"description": "CLI for controlling a NOVA DIY Artificial Intelligence Robot by Creoqode",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"nova",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"commander": "^12.0.0",
|
|
32
|
-
"nova-control-node": "*"
|
|
32
|
+
"nova-control-node": "*",
|
|
33
|
+
"zod": "^4.3.6"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/node": "^22.0.0",
|