@thi.ng/axidraw 1.1.38 → 1.1.40

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/axidraw.js CHANGED
@@ -9,7 +9,9 @@ import { DIN_A3_LANDSCAPE } from "@thi.ng/units/constants/paper-sizes";
9
9
  import { convert, div, Quantity } from "@thi.ng/units/unit";
10
10
  import { inch } from "@thi.ng/units/units/length";
11
11
  import { abs2 } from "@thi.ng/vectors/abs";
12
- import { ZERO2, } from "@thi.ng/vectors/api";
12
+ import {
13
+ ZERO2
14
+ } from "@thi.ng/vectors/api";
13
15
  import { clamp2 } from "@thi.ng/vectors/clamp";
14
16
  import { maddN2 } from "@thi.ng/vectors/maddn";
15
17
  import { mag } from "@thi.ng/vectors/mag";
@@ -17,383 +19,390 @@ import { mulN2 } from "@thi.ng/vectors/muln";
17
19
  import { set2 } from "@thi.ng/vectors/set";
18
20
  import { zero } from "@thi.ng/vectors/setn";
19
21
  import { sub2 } from "@thi.ng/vectors/sub";
20
- import { AxiDrawState, } from "./api.js";
22
+ import {
23
+ AxiDrawState
24
+ } from "./api.js";
21
25
  import { complete, HOME, OFF, ON, PEN, UP } from "./commands.js";
22
26
  import { AxiDrawControl } from "./control.js";
23
27
  import { SERIAL_PORT } from "./serial.js";
24
- export const DEFAULT_OPTS = {
25
- serial: SERIAL_PORT,
26
- logger: new ConsoleLogger("axidraw"),
27
- control: new AxiDrawControl(),
28
- refresh: 1000,
29
- bounds: DIN_A3_LANDSCAPE,
30
- home: [0, 0],
31
- unitsPerInch: 25.4,
32
- stepsPerInch: 2032,
33
- speedDown: 4000,
34
- speedUp: 4000,
35
- up: 60,
36
- down: 30,
37
- delayUp: 150,
38
- delayDown: 150,
39
- preDelay: 0,
40
- start: [ON, PEN(), UP()],
41
- stop: [UP(), HOME, OFF],
42
- sigint: true,
28
+ const DEFAULT_OPTS = {
29
+ serial: SERIAL_PORT,
30
+ logger: new ConsoleLogger("axidraw"),
31
+ control: new AxiDrawControl(),
32
+ refresh: 1e3,
33
+ bounds: DIN_A3_LANDSCAPE,
34
+ home: [0, 0],
35
+ unitsPerInch: 25.4,
36
+ stepsPerInch: 2032,
37
+ speedDown: 4e3,
38
+ speedUp: 4e3,
39
+ up: 60,
40
+ down: 30,
41
+ delayUp: 150,
42
+ delayDown: 150,
43
+ preDelay: 0,
44
+ start: [ON, PEN(), UP()],
45
+ stop: [UP(), HOME, OFF],
46
+ sigint: true
43
47
  };
44
- export class AxiDraw {
45
- serial;
46
- opts;
47
- isConnected = false;
48
- isPenDown = false;
49
- penLimits;
50
- penState = [];
51
- pos = [0, 0];
52
- targetPos = [0, 0];
53
- homePos;
54
- scale;
55
- bounds;
56
- constructor(opts = {}) {
57
- this.opts = { ...DEFAULT_OPTS, ...opts };
58
- this.penLimits = [this.opts.down, this.opts.up];
59
- this.scale = this.opts.stepsPerInch / this.opts.unitsPerInch;
60
- this.setHome(this.opts.home);
61
- if (this.opts.bounds) {
62
- this.bounds =
63
- this.opts.bounds instanceof Quantity
64
- ? [
65
- [0, 0],
66
- convert(this.opts.bounds, div(inch, this.opts.stepsPerInch)),
67
- ]
68
- : [
69
- mulN2([], this.opts.bounds[0], this.scale),
70
- mulN2([], this.opts.bounds[1], this.scale),
71
- ];
72
- }
73
- this.save();
74
- }
75
- reset() {
76
- zero(this.pos);
77
- zero(this.targetPos);
78
- this.send("R\r");
79
- return this;
48
+ class AxiDraw {
49
+ serial;
50
+ opts;
51
+ isConnected = false;
52
+ isPenDown = false;
53
+ penLimits;
54
+ penState = [];
55
+ pos = [0, 0];
56
+ targetPos = [0, 0];
57
+ homePos;
58
+ scale;
59
+ bounds;
60
+ constructor(opts = {}) {
61
+ this.opts = { ...DEFAULT_OPTS, ...opts };
62
+ this.penLimits = [this.opts.down, this.opts.up];
63
+ this.scale = this.opts.stepsPerInch / this.opts.unitsPerInch;
64
+ this.setHome(this.opts.home);
65
+ if (this.opts.bounds) {
66
+ this.bounds = this.opts.bounds instanceof Quantity ? [
67
+ [0, 0],
68
+ convert(
69
+ this.opts.bounds,
70
+ div(inch, this.opts.stepsPerInch)
71
+ )
72
+ ] : [
73
+ mulN2([], this.opts.bounds[0], this.scale),
74
+ mulN2([], this.opts.bounds[1], this.scale)
75
+ ];
80
76
  }
81
- /**
82
- * Async function. Attempts to connect to the drawing machine via given
83
- * (partial) serial port path/name, returns true if successful.
84
- *
85
- * @remarks
86
- * First matching port will be used. If `path` is a sting, a port name must
87
- * only start with it in order to be considered a match.
88
- *
89
- * An error is thrown if no matching port could be found.
90
- *
91
- * @param path
92
- */
93
- async connect(path = "/dev/tty.usbmodem") {
94
- const isStr = isString(path);
95
- for (let port of await this.opts.serial.list(path.toString())) {
96
- if ((isStr && port.path.startsWith(path)) ||
97
- (!isStr && path.test(port.path))) {
98
- this.opts.logger.info(`using device: ${port.path}...`);
99
- this.serial = this.opts.serial.ctor(port.path, 38400);
100
- this.isConnected = true;
101
- if (this.opts.sigint) {
102
- this.opts.logger.debug("installing signal handler...");
103
- process.on("SIGINT", this.onSignal.bind(this));
104
- }
105
- return;
106
- }
77
+ this.save();
78
+ }
79
+ reset() {
80
+ zero(this.pos);
81
+ zero(this.targetPos);
82
+ this.send("R\r");
83
+ return this;
84
+ }
85
+ /**
86
+ * Async function. Attempts to connect to the drawing machine via given
87
+ * (partial) serial port path/name, returns true if successful.
88
+ *
89
+ * @remarks
90
+ * First matching port will be used. If `path` is a sting, a port name must
91
+ * only start with it in order to be considered a match.
92
+ *
93
+ * An error is thrown if no matching port could be found.
94
+ *
95
+ * @param path
96
+ */
97
+ async connect(path = "/dev/tty.usbmodem") {
98
+ const isStr = isString(path);
99
+ for (let port of await this.opts.serial.list(path.toString())) {
100
+ if (isStr && port.path.startsWith(path) || !isStr && path.test(port.path)) {
101
+ this.opts.logger.info(`using device: ${port.path}...`);
102
+ this.serial = this.opts.serial.ctor(port.path, 38400);
103
+ this.isConnected = true;
104
+ if (this.opts.sigint) {
105
+ this.opts.logger.debug("installing signal handler...");
106
+ process.on("SIGINT", this.onSignal.bind(this));
107
107
  }
108
- ioerror(`no matching device for ${path}`);
109
- }
110
- disconnect() {
111
- this.serial.close();
108
+ return;
109
+ }
112
110
  }
113
- /**
114
- * Async function. Converts sequence of {@link DrawCommand}s into actual EBB
115
- * commands and sends them via configured serial port to the AxiDraw. If
116
- * `wrap` is enabled (default), the given commands will be automatically
117
- * wrapped with start/stop commands via {@link complete}. Returns object of
118
- * collected {@link Metrics}. If `showMetrics` is enabled (default), the
119
- * metrics will also be written to the configured logger.
120
- *
121
- * @remarks
122
- * This function is async and if using `await` will only return once all
123
- * commands have been processed or cancelled.
124
- *
125
- * The `control` implementation/ provided as part of {@link AxiDrawOpts} can
126
- * be used to pause, resume or cancel the drawing (see
127
- * {@link AxiDrawOpts.control} for details).
128
- *
129
- * Reference:
130
- * - http://evil-mad.github.io/EggBot/ebb.html
131
- *
132
- * @example
133
- * ```ts
134
- * // execute start sequence, draw a triangle, then exec stop sequence
135
- * axi.draw([
136
- * ["start"],
137
- * ...axi.polyline([[50,50], [100,50], [75, 100], [50,50]]),
138
- * ["stop"]
139
- * ]);
140
- * ```
141
- *
142
- * @param commands
143
- * @param wrap
144
- * @param showMetrics
145
- */
146
- async draw(commands, wrap = true, showMetrics = true) {
147
- assert(this.isConnected, "AxiDraw not yet connected, need to call .connect() first");
148
- let t0 = Date.now();
149
- let numCommands = 0;
150
- let penCommands = 0;
151
- let totalDist = 0;
152
- let drawDist = 0;
153
- const $recordDist = (dist) => {
154
- totalDist += dist;
155
- if (this.isPenDown)
156
- drawDist += dist;
157
- };
158
- const { control, logger, preDelay, refresh } = this.opts;
159
- for (let $cmd of wrap ? complete(commands) : commands) {
160
- numCommands++;
161
- if (control) {
162
- let state = control.deref();
163
- if (state === AxiDrawState.PAUSE) {
164
- const penDown = this.isPenDown;
165
- if (penDown)
166
- this.penUp();
167
- do {
168
- await delayed(0, refresh);
169
- } while ((state = control.deref()) === AxiDrawState.PAUSE);
170
- if (state === AxiDrawState.CONTINUE && penDown) {
171
- this.penDown();
172
- }
173
- }
174
- if (state === AxiDrawState.CANCEL) {
175
- this.penUp();
176
- break;
177
- }
178
- }
179
- const [cmd, a, b] = $cmd;
180
- let wait = -1;
181
- let dist;
182
- switch (cmd) {
183
- case "start":
184
- case "stop": {
185
- const metrics = await this.draw(this.opts[cmd], false, false);
186
- numCommands += metrics.commands;
187
- penCommands += metrics.penCommands;
188
- totalDist += metrics.totalDist;
189
- drawDist += metrics.drawDist;
190
- break;
191
- }
192
- case "home":
193
- [wait, dist] = this.home();
194
- $recordDist(dist);
195
- break;
196
- case "reset":
197
- this.reset();
198
- break;
199
- case "on":
200
- this.motorsOn();
201
- break;
202
- case "off":
203
- this.motorsOff();
204
- break;
205
- case "pen":
206
- this.penConfig(a, b);
207
- break;
208
- case "u":
209
- wait = this.penUp(a, b);
210
- penCommands++;
211
- break;
212
- case "d":
213
- wait = this.penDown(a, b);
214
- penCommands++;
215
- break;
216
- case "save":
217
- this.save();
218
- break;
219
- case "restore":
220
- this.restore();
221
- break;
222
- case "w":
223
- wait = a;
224
- break;
225
- case "M":
226
- [wait, dist] = this.moveTo(a, b);
227
- $recordDist(dist);
228
- break;
229
- case "m":
230
- [wait, dist] = this.moveRelative(a, b);
231
- $recordDist(dist);
232
- break;
233
- case "comment":
234
- logger.info(`comment: ${a}`);
235
- break;
236
- default:
237
- unsupported(`unknown command: ${$cmd}`);
238
- }
239
- if (wait > 0) {
240
- wait = Math.max(0, wait - preDelay);
241
- logger.debug(`waiting ${wait}ms...`);
242
- await delayed(0, wait);
243
- }
244
- // restore one-off pen config to current state
245
- if (cmd === "d" && b !== undefined) {
246
- this.sendPenConfig(5, this.penLimits[0]);
247
- }
248
- else if (cmd === "u" && b !== undefined) {
249
- this.sendPenConfig(4, this.penLimits[1]);
250
- }
111
+ ioerror(`no matching device for ${path}`);
112
+ }
113
+ disconnect() {
114
+ this.serial.close();
115
+ }
116
+ /**
117
+ * Async function. Converts sequence of {@link DrawCommand}s into actual EBB
118
+ * commands and sends them via configured serial port to the AxiDraw. If
119
+ * `wrap` is enabled (default), the given commands will be automatically
120
+ * wrapped with start/stop commands via {@link complete}. Returns object of
121
+ * collected {@link Metrics}. If `showMetrics` is enabled (default), the
122
+ * metrics will also be written to the configured logger.
123
+ *
124
+ * @remarks
125
+ * This function is async and if using `await` will only return once all
126
+ * commands have been processed or cancelled.
127
+ *
128
+ * The `control` implementation/ provided as part of {@link AxiDrawOpts} can
129
+ * be used to pause, resume or cancel the drawing (see
130
+ * {@link AxiDrawOpts.control} for details).
131
+ *
132
+ * Reference:
133
+ * - http://evil-mad.github.io/EggBot/ebb.html
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * // execute start sequence, draw a triangle, then exec stop sequence
138
+ * axi.draw([
139
+ * ["start"],
140
+ * ...axi.polyline([[50,50], [100,50], [75, 100], [50,50]]),
141
+ * ["stop"]
142
+ * ]);
143
+ * ```
144
+ *
145
+ * @param commands
146
+ * @param wrap
147
+ * @param showMetrics
148
+ */
149
+ async draw(commands, wrap = true, showMetrics = true) {
150
+ assert(
151
+ this.isConnected,
152
+ "AxiDraw not yet connected, need to call .connect() first"
153
+ );
154
+ let t0 = Date.now();
155
+ let numCommands = 0;
156
+ let penCommands = 0;
157
+ let totalDist = 0;
158
+ let drawDist = 0;
159
+ const $recordDist = (dist) => {
160
+ totalDist += dist;
161
+ if (this.isPenDown)
162
+ drawDist += dist;
163
+ };
164
+ const { control, logger, preDelay, refresh } = this.opts;
165
+ for (let $cmd of wrap ? complete(commands) : commands) {
166
+ numCommands++;
167
+ if (control) {
168
+ let state = control.deref();
169
+ if (state === AxiDrawState.PAUSE) {
170
+ const penDown = this.isPenDown;
171
+ if (penDown)
172
+ this.penUp();
173
+ do {
174
+ await delayed(0, refresh);
175
+ } while ((state = control.deref()) === AxiDrawState.PAUSE);
176
+ if (state === AxiDrawState.CONTINUE && penDown) {
177
+ this.penDown();
178
+ }
251
179
  }
252
- const duration = Date.now() - t0;
253
- if (showMetrics) {
254
- logger.info(`total duration : ${formatDuration(duration)}`);
255
- logger.info(`total commands : ${numCommands}`);
256
- logger.info(`pen up/downs : ${penCommands}`);
257
- logger.info(`total distance : ${totalDist.toFixed(2)}`);
258
- logger.info(`draw distance : ${drawDist.toFixed(2)}`);
180
+ if (state === AxiDrawState.CANCEL) {
181
+ this.penUp();
182
+ break;
259
183
  }
260
- return {
261
- duration,
262
- drawDist,
263
- totalDist,
264
- penCommands,
265
- commands: numCommands,
266
- };
267
- }
268
- /**
269
- * Syntax sugar for drawing a **single** command only, otherwise same as
270
- * {@link AxiDraw.draw}.
271
- *
272
- * @param cmd
273
- */
274
- draw1(cmd) {
275
- return this.draw([cmd], false);
276
- }
277
- motorsOn() {
278
- this.send("EM,1,1\r");
279
- }
280
- motorsOff() {
281
- this.send("EM,0,0\r");
282
- }
283
- save() {
284
- this.opts.logger.debug("saving pen state:", this.penLimits);
285
- this.penState.push(this.penLimits.slice());
286
- }
287
- restore() {
288
- if (this.penState.length < 2) {
289
- this.opts.logger.warn("stack underflow, can't restore pen state");
290
- return;
184
+ }
185
+ const [cmd, a, b] = $cmd;
186
+ let wait = -1;
187
+ let dist;
188
+ switch (cmd) {
189
+ case "start":
190
+ case "stop": {
191
+ const metrics = await this.draw(
192
+ this.opts[cmd],
193
+ false,
194
+ false
195
+ );
196
+ numCommands += metrics.commands;
197
+ penCommands += metrics.penCommands;
198
+ totalDist += metrics.totalDist;
199
+ drawDist += metrics.drawDist;
200
+ break;
291
201
  }
292
- const [down, up] = (this.penLimits = this.penState.pop());
293
- this.sendPenConfig(5, down);
294
- this.sendPenConfig(4, up);
295
- this.opts.logger.debug("restored pen state:", this.penLimits);
296
- }
297
- penConfig(down, up) {
298
- down = down !== undefined ? down : this.opts.down;
299
- this.sendPenConfig(5, down);
300
- this.penLimits[0] = down;
301
- up = up !== undefined ? up : this.opts.up;
302
- this.sendPenConfig(4, up);
303
- this.penLimits[1] = up;
304
- this.send(`SC,10,65535\r`);
202
+ case "home":
203
+ [wait, dist] = this.home();
204
+ $recordDist(dist);
205
+ break;
206
+ case "reset":
207
+ this.reset();
208
+ break;
209
+ case "on":
210
+ this.motorsOn();
211
+ break;
212
+ case "off":
213
+ this.motorsOff();
214
+ break;
215
+ case "pen":
216
+ this.penConfig(a, b);
217
+ break;
218
+ case "u":
219
+ wait = this.penUp(a, b);
220
+ penCommands++;
221
+ break;
222
+ case "d":
223
+ wait = this.penDown(a, b);
224
+ penCommands++;
225
+ break;
226
+ case "save":
227
+ this.save();
228
+ break;
229
+ case "restore":
230
+ this.restore();
231
+ break;
232
+ case "w":
233
+ wait = a;
234
+ break;
235
+ case "M":
236
+ [wait, dist] = this.moveTo(a, b);
237
+ $recordDist(dist);
238
+ break;
239
+ case "m":
240
+ [wait, dist] = this.moveRelative(a, b);
241
+ $recordDist(dist);
242
+ break;
243
+ case "comment":
244
+ logger.info(`comment: ${a}`);
245
+ break;
246
+ default:
247
+ unsupported(`unknown command: ${$cmd}`);
248
+ }
249
+ if (wait > 0) {
250
+ wait = Math.max(0, wait - preDelay);
251
+ logger.debug(`waiting ${wait}ms...`);
252
+ await delayed(0, wait);
253
+ }
254
+ if (cmd === "d" && b !== void 0) {
255
+ this.sendPenConfig(5, this.penLimits[0]);
256
+ } else if (cmd === "u" && b !== void 0) {
257
+ this.sendPenConfig(4, this.penLimits[1]);
258
+ }
305
259
  }
306
- penUp(delay, level) {
307
- if (level !== undefined)
308
- this.sendPenConfig(4, level);
309
- delay = delay !== undefined && delay >= 0 ? delay : this.opts.delayUp;
310
- this.send(`SP,1,${delay}\r`);
311
- this.isPenDown = false;
312
- return delay;
260
+ const duration = Date.now() - t0;
261
+ if (showMetrics) {
262
+ logger.info(`total duration : ${formatDuration(duration)}`);
263
+ logger.info(`total commands : ${numCommands}`);
264
+ logger.info(`pen up/downs : ${penCommands}`);
265
+ logger.info(`total distance : ${totalDist.toFixed(2)}`);
266
+ logger.info(`draw distance : ${drawDist.toFixed(2)}`);
313
267
  }
314
- penDown(delay, level) {
315
- if (level !== undefined)
316
- this.sendPenConfig(5, level);
317
- delay = delay !== undefined && delay >= 0 ? delay : this.opts.delayDown;
318
- this.send(`SP,0,${delay}\r`);
319
- this.isPenDown = true;
320
- return delay;
321
- }
322
- /**
323
- * Sends a "moveto" command (absolute coords). Returns tuple of `[duration,
324
- * distance]` (distance in original/configured units)
325
- *
326
- * @remarks
327
- * Even though this method accepts absolute coords, all AxiDraw movements
328
- * are relative. Depending on pen up/down state, movement speed will be
329
- * either the configured {@link AxiDrawOpts.speedDown} or
330
- * {@link AxiDrawOpts.speedUp}.
331
- *
332
- * @param p
333
- * @param tempo
334
- */
335
- moveTo(p, tempo) {
336
- const { homePos, scale, targetPos } = this;
337
- // apply scale factor: worldspace units -> motor steps, add home offset
338
- maddN2(targetPos, p, scale, homePos);
339
- return this.sendMove(tempo);
340
- }
341
- /**
342
- * Similar to {@link AxiDraw.moveTo}, but using **relative** coordinates.
343
- *
344
- * @param delta
345
- * @param tempo
346
- */
347
- moveRelative(delta, tempo) {
348
- const { pos, scale, targetPos } = this;
349
- // apply scale factor: worldspace units -> motor steps
350
- maddN2(targetPos, delta, scale, pos);
351
- return this.sendMove(tempo);
352
- }
353
- /**
354
- * Syntax sugar for {@link AxiDraw.moveTo}([0, 0]).
355
- */
356
- home() {
357
- return this.moveTo(ZERO2);
358
- }
359
- setHome(pos) {
360
- this.homePos = mulN2([], pos, this.scale);
361
- this.opts.logger.debug("setting home position:", pos);
362
- }
363
- async onSignal() {
364
- this.opts.logger.warn(`SIGNINT received, stop drawing...`);
365
- this.penUp(0);
366
- this.motorsOff();
367
- await delayed(0, 100);
368
- process.exit(1);
369
- }
370
- send(msg) {
371
- this.opts.logger.debug(msg);
372
- this.serial.write(msg);
373
- }
374
- sendMove(tempo = 1) {
375
- const { bounds, pos, scale, targetPos, opts, isPenDown } = this;
376
- if (bounds)
377
- clamp2(null, targetPos, ...bounds);
378
- const delta = sub2([], targetPos, pos);
379
- set2(pos, targetPos);
380
- const maxAxis = Math.max(...abs2([], delta));
381
- const duration = (1000 * maxAxis) /
382
- ((isPenDown ? opts.speedDown : opts.speedUp) * tempo);
383
- this.send(`XM,${duration | 0},${delta[0] | 0},${delta[1] | 0}\r`);
384
- return [duration, mag(delta) / scale];
385
- }
386
- /**
387
- * Sends pen up/down config
388
- *
389
- * @remarks
390
- * Reference:
391
- * - https://github.com/evil-mad/AxiDraw-Processing/blob/80d81a8c897b8a1872b0555af52a8d1b5b13cec4/AxiGen1/AxiGen1.pde#L213
392
- *
393
- * @param id
394
- * @param x
395
- */
396
- sendPenConfig(id, x) {
397
- this.send(`SC,${id},${(7500 + 175 * x) | 0}\r`);
268
+ return {
269
+ duration,
270
+ drawDist,
271
+ totalDist,
272
+ penCommands,
273
+ commands: numCommands
274
+ };
275
+ }
276
+ /**
277
+ * Syntax sugar for drawing a **single** command only, otherwise same as
278
+ * {@link AxiDraw.draw}.
279
+ *
280
+ * @param cmd
281
+ */
282
+ draw1(cmd) {
283
+ return this.draw([cmd], false);
284
+ }
285
+ motorsOn() {
286
+ this.send("EM,1,1\r");
287
+ }
288
+ motorsOff() {
289
+ this.send("EM,0,0\r");
290
+ }
291
+ save() {
292
+ this.opts.logger.debug("saving pen state:", this.penLimits);
293
+ this.penState.push(this.penLimits.slice());
294
+ }
295
+ restore() {
296
+ if (this.penState.length < 2) {
297
+ this.opts.logger.warn("stack underflow, can't restore pen state");
298
+ return;
398
299
  }
300
+ const [down, up] = this.penLimits = this.penState.pop();
301
+ this.sendPenConfig(5, down);
302
+ this.sendPenConfig(4, up);
303
+ this.opts.logger.debug("restored pen state:", this.penLimits);
304
+ }
305
+ penConfig(down, up) {
306
+ down = down !== void 0 ? down : this.opts.down;
307
+ this.sendPenConfig(5, down);
308
+ this.penLimits[0] = down;
309
+ up = up !== void 0 ? up : this.opts.up;
310
+ this.sendPenConfig(4, up);
311
+ this.penLimits[1] = up;
312
+ this.send(`SC,10,65535\r`);
313
+ }
314
+ penUp(delay, level) {
315
+ if (level !== void 0)
316
+ this.sendPenConfig(4, level);
317
+ delay = delay !== void 0 && delay >= 0 ? delay : this.opts.delayUp;
318
+ this.send(`SP,1,${delay}\r`);
319
+ this.isPenDown = false;
320
+ return delay;
321
+ }
322
+ penDown(delay, level) {
323
+ if (level !== void 0)
324
+ this.sendPenConfig(5, level);
325
+ delay = delay !== void 0 && delay >= 0 ? delay : this.opts.delayDown;
326
+ this.send(`SP,0,${delay}\r`);
327
+ this.isPenDown = true;
328
+ return delay;
329
+ }
330
+ /**
331
+ * Sends a "moveto" command (absolute coords). Returns tuple of `[duration,
332
+ * distance]` (distance in original/configured units)
333
+ *
334
+ * @remarks
335
+ * Even though this method accepts absolute coords, all AxiDraw movements
336
+ * are relative. Depending on pen up/down state, movement speed will be
337
+ * either the configured {@link AxiDrawOpts.speedDown} or
338
+ * {@link AxiDrawOpts.speedUp}.
339
+ *
340
+ * @param p
341
+ * @param tempo
342
+ */
343
+ moveTo(p, tempo) {
344
+ const { homePos, scale, targetPos } = this;
345
+ maddN2(targetPos, p, scale, homePos);
346
+ return this.sendMove(tempo);
347
+ }
348
+ /**
349
+ * Similar to {@link AxiDraw.moveTo}, but using **relative** coordinates.
350
+ *
351
+ * @param delta
352
+ * @param tempo
353
+ */
354
+ moveRelative(delta, tempo) {
355
+ const { pos, scale, targetPos } = this;
356
+ maddN2(targetPos, delta, scale, pos);
357
+ return this.sendMove(tempo);
358
+ }
359
+ /**
360
+ * Syntax sugar for {@link AxiDraw.moveTo}([0, 0]).
361
+ */
362
+ home() {
363
+ return this.moveTo(ZERO2);
364
+ }
365
+ setHome(pos) {
366
+ this.homePos = mulN2([], pos, this.scale);
367
+ this.opts.logger.debug("setting home position:", pos);
368
+ }
369
+ async onSignal() {
370
+ this.opts.logger.warn(`SIGNINT received, stop drawing...`);
371
+ this.penUp(0);
372
+ this.motorsOff();
373
+ await delayed(0, 100);
374
+ process.exit(1);
375
+ }
376
+ send(msg) {
377
+ this.opts.logger.debug(msg);
378
+ this.serial.write(msg);
379
+ }
380
+ sendMove(tempo = 1) {
381
+ const { bounds, pos, scale, targetPos, opts, isPenDown } = this;
382
+ if (bounds)
383
+ clamp2(null, targetPos, ...bounds);
384
+ const delta = sub2([], targetPos, pos);
385
+ set2(pos, targetPos);
386
+ const maxAxis = Math.max(...abs2([], delta));
387
+ const duration = 1e3 * maxAxis / ((isPenDown ? opts.speedDown : opts.speedUp) * tempo);
388
+ this.send(`XM,${duration | 0},${delta[0] | 0},${delta[1] | 0}\r`);
389
+ return [duration, mag(delta) / scale];
390
+ }
391
+ /**
392
+ * Sends pen up/down config
393
+ *
394
+ * @remarks
395
+ * Reference:
396
+ * - https://github.com/evil-mad/AxiDraw-Processing/blob/80d81a8c897b8a1872b0555af52a8d1b5b13cec4/AxiGen1/AxiGen1.pde#L213
397
+ *
398
+ * @param id
399
+ * @param x
400
+ */
401
+ sendPenConfig(id, x) {
402
+ this.send(`SC,${id},${7500 + 175 * x | 0}\r`);
403
+ }
399
404
  }
405
+ export {
406
+ AxiDraw,
407
+ DEFAULT_OPTS
408
+ };