UNPKG

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