UNPKG

10.5 kBJavaScriptView Raw
1const Board = require("./board");
2const Fn = require("./fn");
3const priv = new Map();
4const steppers = new Map();
5const TAU = Fn.TAU;
6
7const MAXSTEPPERS = 6; // correlates with MAXSTEPPERS in firmware
8
9
10class Step {
11 constructor(stepper) {
12 this.rpm = 180;
13 this.direction = -1;
14 this.speed = 0;
15 this.accel = 0;
16 this.decel = 0;
17
18 this.stepper = stepper;
19 }
20
21 move(steps, dir, speed, accel, decel, callback) {
22 // Restore the param order... (steps, dir => dir, steps)
23 this.stepper.io.stepperStep.apply(
24 this.stepper.io, [this.stepper.id, dir, steps, speed, accel, decel, callback]
25 );
26 }
27}
28
29Step.PROPERTIES = ["rpm", "direction", "speed", "accel", "decel"];
30Step.DEFAULTS = [180, -1, 0, 0, 0];
31
32
33function MotorPins(pins) {
34 let k = 0;
35 pins = pins.slice();
36 while (pins.length) {
37 this[`motor${++k}`] = pins.shift();
38 }
39}
40
41function isSupported({pins, MODES}) {
42 return pins.some(({supportedModes}) => supportedModes.includes(MODES.STEPPER));
43}
44
45/**
46 * Stepper
47 *
48 * Class for handling steppers using AdvancedFirmata support for asynchronous stepper control
49 *
50 *
51 * five.Stepper({
52 * type: constant, // io.STEPPER.TYPE.*
53 * stepsPerRev: number, // steps to make on revolution of stepper
54 * pins: {
55 * step: number, // pin attached to step pin on driver (used for type DRIVER)
56 * dir: number, // pin attached to direction pin on driver (used for type DRIVER)
57 * motor1: number, // (used for type TWO_WIRE and FOUR_WIRE)
58 * motor2: number, // (used for type TWO_WIRE and FOUR_WIRE)
59 * motor3: number, // (used for type FOUR_WIRE)
60 * motor4: number, // (used for type FOUR_WIRE)
61 * }
62 * });
63 *
64 *
65 * five.Stepper({
66 * type: five.Stepper.TYPE.DRIVER,
67 * stepsPerRev: number,
68 * pins: {
69 * step: number,
70 * dir: number
71 * }
72 * });
73 *
74 * five.Stepper({
75 * type: five.Stepper.TYPE.DRIVER,
76 * stepsPerRev: number,
77 * pins: [ step, dir ]
78 * });
79 *
80 * five.Stepper({
81 * type: five.Stepper.TYPE.TWO_WIRE,
82 * stepsPerRev: number,
83 * pins: {
84 * motor1: number,
85 * motor2: number
86 * }
87 * });
88 *
89 * five.Stepper({
90 * type: five.Stepper.TYPE.TWO_WIRE,
91 * stepsPerRev: number,
92 * pins: [ motor1, motor2 ]
93 * });
94 *
95 * five.Stepper({
96 * type: five.Stepper.TYPE.FOUR_WIRE,
97 * stepsPerRev: number,
98 * pins: {
99 * motor1: number,
100 * motor2: number,
101 * motor3: number,
102 * motor4: number
103 * }
104 * });
105 *
106 * five.Stepper({
107 * type: five.Stepper.TYPE.FOUR_WIRE,
108 * stepsPerRev: number,
109 * pins: [ motor1, motor2, motor3, motor4 ]
110 * });
111 *
112 *
113 * @param {Object} options
114 *
115 */
116
117class Stepper {
118 constructor(options) {
119 const params = [];
120 let state;
121
122 Board.Component.call(
123 this, options = Board.Options(options)
124 );
125
126 if (!isSupported(this.io)) {
127 throw new Error(
128 "Stepper is not supported"
129 );
130 }
131
132 if (!options.pins) {
133 throw new Error(
134 "Stepper requires a `pins` object or array"
135 );
136 }
137
138 if (!options.stepsPerRev) {
139 throw new Error(
140 "Stepper requires a `stepsPerRev` number value"
141 );
142 }
143
144 steppers.set(this.board, steppers.get(this.board) || []);
145 this.id = steppers.get(this.board).length;
146
147 if (this.id >= MAXSTEPPERS) {
148 throw new Error(
149 `Stepper cannot exceed max steppers (${MAXSTEPPERS})`
150 );
151 }
152
153 // Convert an array of pins to the appropriate named pin
154 if (Array.isArray(this.pins)) {
155 if (this.pins.length === 2) {
156 // Using an array of 2 pins requres a TYPE
157 // to disambiguate DRIVER and TWO_WIRE
158 if (!options.type) {
159 throw new Error(
160 "Stepper requires a `type` number value (DRIVER, TWO_WIRE)"
161 );
162 }
163 }
164
165 if (options.type === Stepper.TYPE.DRIVER) {
166 this.pins = {
167 step: this.pins[0],
168 dir: this.pins[1]
169 };
170 } else {
171 this.pins = new MotorPins(this.pins);
172 }
173 }
174
175 // Attempt to guess the type if none is provided
176 if (!options.type) {
177 if (this.pins.dir) {
178 options.type = Stepper.TYPE.DRIVER;
179 } else {
180 if (this.pins.motor3) {
181 options.type = Stepper.TYPE.FOUR_WIRE;
182 } else {
183 options.type = Stepper.TYPE.TWO_WIRE;
184 }
185 }
186 }
187
188
189 // Initial Stepper config params (same for all 3 types)
190 params.push(this.id, options.type, options.stepsPerRev);
191
192
193 if (options.type === Stepper.TYPE.DRIVER) {
194 if (typeof this.pins.dir === "undefined" ||
195 typeof this.pins.step === "undefined") {
196 throw new Error(
197 "Stepper.TYPE.DRIVER expects: `pins.dir`, `pins.step`"
198 );
199 }
200
201 params.push(
202 this.pins.dir, this.pins.step
203 );
204 }
205
206 if (options.type === Stepper.TYPE.TWO_WIRE) {
207 if (typeof this.pins.motor1 === "undefined" ||
208 typeof this.pins.motor2 === "undefined") {
209 throw new Error(
210 "Stepper.TYPE.TWO_WIRE expects: `pins.motor1`, `pins.motor2`"
211 );
212 }
213
214 params.push(
215 this.pins.motor1, this.pins.motor2
216 );
217 }
218
219 if (options.type === Stepper.TYPE.FOUR_WIRE) {
220 if (typeof this.pins.motor1 === "undefined" ||
221 typeof this.pins.motor2 === "undefined" ||
222 typeof this.pins.motor3 === "undefined" ||
223 typeof this.pins.motor4 === "undefined") {
224 throw new Error(
225 "Stepper.TYPE.FOUR_WIRE expects: `pins.motor1`, `pins.motor2`, `pins.motor3`, `pins.motor4`"
226 );
227 }
228
229 params.push(
230 this.pins.motor1, this.pins.motor2, this.pins.motor3, this.pins.motor4
231 );
232 }
233
234 // Iterate the params and set each pin's mode to MODES.STEPPER
235 // Params:
236 // [deviceNum, type, stepsPerRev, dirOrMotor1Pin, stepOrMotor2Pin, motor3Pin, motor4Pin]
237 // The first 3 are required, the remaining 2-4 will be pins
238 params.slice(3).forEach((pin) => {
239 this.io.pinMode(pin, this.io.MODES.STEPPER);
240 });
241
242 this.io.stepperConfig.apply(this.io, params);
243
244 steppers.get(this.board).push(this);
245
246 state = Step.PROPERTIES.reduce((state, key, i) => (state[key] = typeof options[key] !== "undefined" ? options[key] : Step.DEFAULTS[i], state), {
247 isRunning: false,
248 type: options.type,
249 pins: this.pins
250 });
251
252 priv.set(this, state);
253
254 Object.defineProperties(this, {
255 type: {
256 get() {
257 return state.type;
258 }
259 },
260
261 pins: {
262 get() {
263 return state.pins;
264 }
265 }
266 });
267 }
268
269 /**
270 * rpm
271 *
272 * Gets the rpm value or sets the rpm in revs per minute
273 * making an internal conversion to speed in `0.01 * rad/s`
274 *
275 * @param {Number} rpm Revs per minute
276 *
277 * NOTE: *rpm* is optional, if missing
278 * the method will behave like a getter
279 *
280 * @return {Stepper} this Chainable method when used as a setter
281 */
282 rpm(rpm) {
283 const state = priv.get(this);
284
285 if (typeof rpm === "undefined") {
286 return state.rpm;
287 }
288 state.rpm = rpm;
289 state.speed = Math.round(rpm * TAU * 100 / 60);
290 return this;
291 }
292
293 /**
294 * speed
295 *
296 * Gets the speed value or sets the speed in `0.01 * rad/s`
297 * making an internal conversion to rpm
298 *
299 * @param {Number} speed Speed given in 0.01 * rad/s
300 *
301 * NOTE: *speed* is optional, if missing
302 * the method will behave like a getter
303 *
304 * @return {Stepper} this Chainable method when used as a setter
305 */
306 speed(speed) {
307 const state = priv.get(this);
308
309 if (typeof speed === "undefined") {
310 return state.speed;
311 }
312 state.speed = speed;
313 state.rpm = Math.round(speed / TAU / 100 * 60);
314 return this;
315 }
316
317 ccw() {
318 return this.direction(0);
319 }
320
321 cw() {
322 return this.direction(1);
323 }
324
325 /**
326 * step
327 *
328 * Move stepper motor a number of steps and call the callback on completion
329 *
330 * @param {Number} stepsOrOpts Steps to move using current settings for speed, accel, etc.
331 * @param {Object} stepsOrOpts Options object containing any of the following:
332 * stepsOrOpts = {
333 * steps:
334 * rpm:
335 * speed:
336 * direction:
337 * accel:
338 * decel:
339 * }
340 *
341 * NOTE: *steps* is required.
342 *
343 * @param {Function} callback function(err, complete)
344 */
345 step(stepsOrOpts, callback) {
346 let steps;
347 let step;
348 let state;
349 let params;
350 let isValidStep;
351
352 steps = typeof stepsOrOpts === "object" ?
353 (stepsOrOpts.steps || 0) : Math.floor(stepsOrOpts);
354
355 step = new Step(this);
356
357 state = priv.get(this);
358
359 params = [];
360
361 isValidStep = true;
362
363 function failback(error) {
364 isValidStep = false;
365 if (callback) {
366 callback(error);
367 }
368 }
369
370 params.push(steps);
371
372 if (typeof stepsOrOpts === "object") {
373 // If an object of property values has been provided,
374 // call the correlating method with the value argument.
375 Step.PROPERTIES.forEach((key) => {
376 if (typeof stepsOrOpts[key] !== "undefined") {
377 this[key](stepsOrOpts[key]);
378 }
379 });
380 }
381
382 if (!state.speed) {
383 this.rpm(state.rpm);
384 step.speed = this.speed();
385 }
386
387
388 // Ensure that the property params are set in the
389 // correct order, but without rpm
390 Step.PROPERTIES.slice(1).forEach((key) => {
391 params.push(step[key] = this[key]());
392 });
393
394
395 if (steps === 0) {
396 failback(
397 new Error(
398 "Must set a number of steps when calling `step()`"
399 )
400 );
401 }
402
403 if (step.direction < 0) {
404 failback(
405 new Error(
406 "Must set a direction before calling `step()`"
407 )
408 );
409 }
410
411 if (isValidStep) {
412 state.isRunning = true;
413
414 params.push(complete => {
415 state.isRunning = false;
416 callback(null, complete);
417 });
418
419 step.move.apply(step, params);
420 }
421
422 return this;
423 }
424}
425
426Object.defineProperties(Stepper, {
427 TYPE: {
428 value: Object.freeze({
429 DRIVER: 1,
430 TWO_WIRE: 2,
431 FOUR_WIRE: 4
432 })
433 },
434 RUNSTATE: {
435 value: Object.freeze({
436 STOP: 0,
437 ACCEL: 1,
438 DECEL: 2,
439 RUN: 3
440 })
441 },
442 DIRECTION: {
443 value: Object.freeze({
444 CCW: 0,
445 CW: 1
446 })
447 }
448});
449
450["direction", "accel", "decel"].forEach(prop => {
451 Stepper.prototype[prop] = function(value) {
452 const state = priv.get(this);
453
454 if (typeof value === "undefined") {
455 return state[prop];
456 }
457 state[prop] = value;
458 return this;
459 };
460});
461
462
463module.exports = Stepper;