nova-control-command 0.0.7 → 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 +11 -11
- package/dist/nova-control-command.js +114 -93
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -71,10 +71,10 @@ In script and REPL mode the serial connection is opened once on the first servo
|
|
|
71
71
|
### `home`
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
nova-control --port <path> home
|
|
74
|
+
nova-control --port <path> home [--within-ms <ms>]
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Sends all five servos to their home positions simultaneously. Home positions are baked into the firmware (`s1=90°`, `s2=90°`, `s3=110°`, `s4=90°`, `s5=95°`).
|
|
77
|
+
Sends all five servos to their home positions simultaneously. Home positions are baked into the firmware (`s1=90°`, `s2=90°`, `s3=110°`, `s4=90°`, `s5=95°`). With `--within-ms`, uses a trapezoidal ramp-up/ramp-down profile to complete the movement in the specified number of milliseconds.
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
@@ -83,25 +83,25 @@ 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
|
-
Sets multiple servos in a single packet. At least one option is required. Servos not mentioned stay at their last-sent positions. Useful when two or more joints must move simultaneously.
|
|
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.
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -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,105 +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
|
-
|
|
147
|
-
})), t.command("home").description("send all servos to their home positions").action(async () => {
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
y = n.port, b = Number(n.baud ?? "9600"), x = n.onError ?? "stop";
|
|
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) => {
|
|
164
|
+
let t = _(e.withinMs, "home");
|
|
165
|
+
await (await C()).home(t);
|
|
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) => {
|
|
150
167
|
let t = {};
|
|
151
|
-
if (e.shiftTo != null && (t.s1 =
|
|
152
|
-
let n =
|
|
153
|
-
|
|
154
|
-
}), t.command("shift-to").description("shift head forward (>90°) or back (<90°) — s1").argument("<angle>", "target angle in degrees").action(async (e) => {
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
}), t.command("roll-to").description("roll head clockwise (>90°) or counter-clockwise (<90°) — s2").argument("<angle>", "target angle in degrees").action(async (e) => {
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
}), t.command("pitch-to").description("pitch head up (>110°) or down (<110°) — s3").argument("<angle>", "target angle in degrees").action(async (e) => {
|
|
161
|
-
let
|
|
162
|
-
|
|
163
|
-
}), t.command("rotate-to").description("rotate body around Z-axis — s4").argument("<angle>", "target angle in degrees").action(async (e) => {
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
}), t.command("lift-to").description("lift head on secondary axis, range 20°–150° — s5").argument("<angle>", "target angle in degrees").action(async (e) => {
|
|
167
|
-
let
|
|
168
|
-
|
|
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);
|
|
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) => {
|
|
172
|
+
let n = g(e, "shift-to"), r = _(t.withinMs, "shift-to");
|
|
173
|
+
await (await C()).shiftHeadTo(n, r);
|
|
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) => {
|
|
175
|
+
let n = g(e, "roll-to"), r = _(t.withinMs, "roll-to");
|
|
176
|
+
await (await C()).rollHeadTo(n, r);
|
|
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) => {
|
|
178
|
+
let n = g(e, "pitch-to"), r = _(t.withinMs, "pitch-to");
|
|
179
|
+
await (await C()).pitchHeadTo(n, r);
|
|
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) => {
|
|
181
|
+
let n = g(e, "rotate-to"), r = _(t.withinMs, "rotate-to");
|
|
182
|
+
await (await C()).rotateBodyTo(n, r);
|
|
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) => {
|
|
184
|
+
let n = g(e, "lift-to"), r = _(t.withinMs, "lift-to");
|
|
185
|
+
await (await C()).liftHeadTo(n, r);
|
|
169
186
|
}), t.command("wait").description("pause for <ms> milliseconds before the next command").argument("<ms>", "duration in milliseconds (non-negative number)").action(async (e) => {
|
|
170
|
-
let t
|
|
171
|
-
|
|
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
|
+
}
|
|
172
193
|
await new Promise((e) => setTimeout(e, t));
|
|
173
194
|
}), t.command("state").description("print the current servo state as JSON").action(async () => {
|
|
174
|
-
let e = await
|
|
195
|
+
let e = await C();
|
|
175
196
|
process.stdout.write(JSON.stringify(e.State) + "\n");
|
|
176
197
|
}), e || (t.command("shell").description("start an interactive REPL").action(async () => {
|
|
177
|
-
await
|
|
198
|
+
await c(v, (e) => k(e));
|
|
178
199
|
}), t.option("--script <file>", "run commands from a script file (use - for stdin)").action(async (e) => {
|
|
179
200
|
if (e.script != null) {
|
|
180
|
-
let t = await
|
|
201
|
+
let t = await l(x, e.script, k);
|
|
181
202
|
process.exit(t);
|
|
182
|
-
} else process.stdout.write(t.helpInformation()), process.exit(
|
|
203
|
+
} else process.stdout.write(t.helpInformation()), process.exit(d.OK);
|
|
183
204
|
}), t.addHelpCommand(!0)), t;
|
|
184
205
|
}
|
|
185
|
-
async function
|
|
186
|
-
if (e.length === 0) return
|
|
187
|
-
let t =
|
|
188
|
-
|
|
206
|
+
async function k(e) {
|
|
207
|
+
if (e.length === 0) return d.OK;
|
|
208
|
+
let t = O(!0);
|
|
209
|
+
D(t);
|
|
189
210
|
try {
|
|
190
211
|
return await t.parseAsync([
|
|
191
212
|
"node",
|
|
192
|
-
|
|
213
|
+
v,
|
|
193
214
|
...e
|
|
194
|
-
]),
|
|
215
|
+
]), d.OK;
|
|
195
216
|
} catch (t) {
|
|
196
217
|
let n = t;
|
|
197
|
-
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);
|
|
198
219
|
}
|
|
199
220
|
}
|
|
200
|
-
async function
|
|
201
|
-
let e =
|
|
202
|
-
|
|
221
|
+
async function A() {
|
|
222
|
+
let e = O();
|
|
223
|
+
D(e);
|
|
203
224
|
try {
|
|
204
225
|
await e.parseAsync(process.argv);
|
|
205
226
|
} catch (t) {
|
|
206
227
|
let n = t;
|
|
207
|
-
(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);
|
|
208
229
|
} finally {
|
|
209
|
-
|
|
230
|
+
w();
|
|
210
231
|
}
|
|
211
232
|
}
|
|
212
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
213
|
-
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);
|
|
214
235
|
});
|
|
215
236
|
//#endregion
|
|
216
|
-
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",
|