UNPKG

16.1 kBJavaScriptView Raw
1var Board = require("./board");
2var Pins = Board.Pins;
3var Expander = require("./expander");
4var Emitter = require("events").EventEmitter;
5var util = require("util");
6var Collection = require("./mixins/collection");
7var Fn = require("./fn");
8var Animation = require("./animation");
9
10// Servo instance private data
11var priv = new Map();
12
13var Controllers = {
14 PCA9685: {
15 initialize: {
16 value: function(opts) {
17 var state = priv.get(this);
18
19 this.address = opts.address || 0x40;
20 this.pwmRange = opts.pwmRange || [450, 1850];
21 this.frequency = opts.frequency || 50;
22
23 state.expander = Expander.get({
24 address: this.address,
25 controller: this.controller,
26 bus: this.bus,
27 pwmRange: this.pwmRange,
28 frequency: this.frequency,
29 });
30
31 this.pin = state.expander.normalize(opts.pin);
32 }
33 },
34 update: {
35 writable: true,
36 value: function(microseconds) {
37 var state = priv.get(this);
38 state.expander.servoWrite(this.pin, microseconds);
39 }
40 }
41 },
42 Standard: {
43 initialize: {
44 value: function(opts) {
45
46 // When in debug mode, if pin is not a PWM pin, emit an error
47 if (opts.debug && !this.board.pins.isServo(this.pin)) {
48 Board.Pins.Error({
49 pin: this.pin,
50 type: "PWM",
51 via: "Servo",
52 });
53 }
54
55 if (Array.isArray(opts.pwmRange)) {
56 this.io.servoConfig(this.pin, opts.pwmRange[0], opts.pwmRange[1]);
57 } else {
58 this.io.pinMode(this.pin, this.mode);
59 }
60 }
61 },
62 update: {
63 writable: true,
64 value: function(degrees) {
65
66 // If same degrees return immediately.
67 if (this.last && this.last.degrees === degrees) {
68 return this;
69 }
70
71 // Map value from degreeRange to pwmRange
72 var microseconds = Fn.map(
73 degrees,
74 this.degreeRange[0], this.degreeRange[1],
75 this.pwmRange[0], this.pwmRange[1]
76 );
77
78 // Restrict values to integers
79 microseconds |= 0;
80
81 this.io.servoWrite(this.pin, microseconds);
82 }
83 }
84 }
85};
86
87/**
88 * Servo
89 * @constructor
90 *
91 * @param {Object} opts Options: pin, type, id, range
92 */
93
94function Servo(opts) {
95
96 if (!(this instanceof Servo)) {
97 return new Servo(opts);
98 }
99
100 var history = [];
101 var pinValue = typeof opts === "object" ? opts.pin : opts;
102 var controller = null;
103
104 Board.Component.call(
105 this, opts = Board.Options(opts)
106 );
107
108 this.degreeRange = opts.degreeRange || [0, 180];
109 this.pwmRange = opts.pwmRange || [600, 2400];
110 this.range = opts.range || this.degreeRange;
111 this.deadband = opts.deadband || [90, 90];
112 this.fps = opts.fps || 100;
113 this.offset = opts.offset || 0;
114 this.range = opts.range || [0 - this.offset, 180 - this.offset];
115 this.mode = this.io.MODES.SERVO;
116 this.interval = null;
117 this.value = null;
118
119 // StandardFirmata on Arduino allows controlling
120 // servos from analog pins.
121 // If we're currently operating with an Arduino
122 // and the user has provided an analog pin name
123 // (eg. "A0", "A5" etc.), parse out the numeric
124 // value and capture the fully qualified analog
125 // pin number.
126 if (typeof opts.controller === "undefined" && Pins.isFirmata(this)) {
127 if (typeof pinValue === "string" && pinValue[0] === "A") {
128 pinValue = this.io.analogPins[+pinValue.slice(1)];
129 }
130
131 pinValue = +pinValue;
132
133 // If the board's default pin normalization
134 // came up with something different, use the
135 // the local value.
136 if (!Number.isNaN(pinValue) && this.pin !== pinValue) {
137 this.pin = pinValue;
138 }
139 }
140
141
142 // The type of servo determines certain alternate
143 // behaviours in the API
144 this.type = opts.type || "standard";
145
146 // Invert the value of all servoWrite operations
147 // eg. 80 => 100, 90 => 90, 0 => 180
148 if (opts.isInverted) {
149 console.warn("The 'isInverted' property has been renamed 'invert'");
150 }
151 this.invert = opts.isInverted || opts.invert || false;
152
153
154 // Collect all movement history for this servo
155 // history = [
156 // {
157 // timestamp: Date.now(),
158 // degrees: degrees
159 // }
160 // ];
161
162 // Allow "setup"instructions to come from
163 // constructor options properties
164
165 if (opts.controller && typeof opts.controller === "string") {
166 controller = Controllers[opts.controller.toUpperCase()];
167 } else {
168 controller = opts.controller;
169 }
170
171 if (controller == null) {
172 controller = Controllers.Standard;
173 }
174
175 priv.set(this, {
176 history: history
177 });
178
179 Board.Controller.call(this, controller, opts);
180
181 Object.defineProperties(this, {
182 history: {
183 get: function() {
184 return history.slice(-5);
185 }
186 },
187 last: {
188 get: function() {
189 return history[history.length - 1];
190 }
191 },
192 position: {
193 get: function() {
194 return history.length ? history[history.length - 1].degrees : -1;
195 }
196 }
197 });
198
199 this.initialize(opts);
200
201 // If "startAt" is defined and center is falsy
202 // set servo to min or max degrees
203 if (typeof opts.startAt !== "undefined") {
204 this.startAt = opts.startAt;
205 this.to(opts.startAt);
206 } else {
207 this.startAt = (this.degreeRange[1] - this.degreeRange[0]) / 2 + this.degreeRange[0];
208 }
209
210 // If "center" true set servo to 90deg
211 if (opts.center) {
212 this.center();
213 }
214
215 if (opts.type === "continuous") {
216 this.stop();
217 }
218}
219
220util.inherits(Servo, Emitter);
221
222
223/**
224 * to
225 *
226 * Set the servo horn's position to given degree over time.
227 *
228 * @param {Number} degrees Degrees to turn servo to.
229 * @param {Number} time Time to spend in motion.
230 * @param {Number} rate The rate of the motion transiton
231 *
232 * - or -
233 *
234 * @param {Object} an Animation() segment config object
235 *
236 * @return {Servo} instance
237 */
238
239Servo.prototype.to = function(degrees, time, rate) {
240
241 var state = priv.get(this);
242 var options = {};
243
244 if (typeof degrees === "object") {
245
246 Object.assign(options, degrees);
247
248 options.duration = degrees.duration || degrees.interval || 1000;
249 options.cuePoints = degrees.cuePoints || [0, 1.0];
250 options.keyFrames = degrees.keyFrames || [
251 null,
252 {
253 value: typeof degrees.degrees === "number" ? degrees.degrees : this.startAt
254 }
255 ];
256
257 options.oncomplete = function() {
258 // Enforce async execution for user "oncomplete"
259 process.nextTick(function() {
260 if (typeof degrees.oncomplete === "function") {
261 degrees.oncomplete();
262 }
263 this.emit("move:complete");
264 }.bind(this));
265 }.bind(this);
266
267
268 state.isRunning = true;
269 state.animation = state.animation || new Animation(this);
270 state.animation.enqueue(options);
271
272 } else {
273
274 var target = degrees;
275
276 // Enforce limited range of motion
277 degrees = Fn.constrain(degrees, this.range[0], this.range[1]);
278
279 if (typeof time !== "undefined") {
280
281 options.duration = time;
282 options.keyFrames = [null, {
283 degrees: degrees
284 }];
285 options.fps = rate || this.fps;
286
287 this.to(options);
288
289 } else {
290
291 this.value = degrees;
292
293 degrees += this.offset;
294
295 if (this.invert) {
296 degrees = Fn.map(
297 degrees,
298 this.degreeRange[0], this.degreeRange[1],
299 this.degreeRange[1], this.degreeRange[0]
300 );
301 }
302
303 this.update(degrees);
304
305 if (state.history.length > 5) {
306 state.history.shift();
307 }
308
309 state.history.push({
310 timestamp: Date.now(),
311 degrees: degrees,
312 target: target
313 });
314 }
315 }
316
317 // return this instance
318 return this;
319};
320
321
322/**
323 * Animation.normalize
324 *
325 * @param [number || object] keyFrames An array of step values or a keyFrame objects
326 */
327
328Servo.prototype[Animation.normalize] = function(keyFrames) {
329
330 var last = this.last ? this.last.target : this.startAt;
331
332 // If user passes null as the first element in keyFrames use current position
333 if (keyFrames[0] === null) {
334 keyFrames[0] = {
335 value: last
336 };
337 }
338
339 // If user passes a step as the first element in keyFrames use current position + step
340 if (typeof keyFrames[0] === "number") {
341 keyFrames[0] = {
342 value: last + keyFrames[0]
343 };
344 }
345
346 return keyFrames.map(function(frame) {
347 var value = frame;
348
349 /* istanbul ignore else */
350 if (frame !== null) {
351 // frames that are just numbers represent _step_
352 if (typeof frame === "number") {
353 frame = {
354 step: value,
355 };
356 } else {
357 if (typeof frame.degrees === "number") {
358 frame.value = frame.degrees;
359 delete frame.degrees;
360 }
361 if (typeof frame.copyDegrees === "number") {
362 frame.copyValue = frame.copyDegrees;
363 delete frame.copyDegrees;
364 }
365 }
366
367 /* istanbul ignore else */
368 if (!frame.easing) {
369 frame.easing = "linear";
370 }
371 }
372 return frame;
373 });
374};
375
376/**
377 * Animation.render
378 *
379 * @position [number] value to set the servo to
380 */
381Servo.prototype[Animation.render] = function(position) {
382 return this.to(position[0]);
383};
384
385/**
386 * step
387 *
388 * Update the servo horn's position by specified degrees (over time)
389 *
390 * @param {Number} degrees Degrees to turn servo to.
391 * @param {Number} time Time to spend in motion.
392 *
393 * @return {Servo} instance
394 */
395
396Servo.prototype.step = function(degrees, time) {
397 return this.to(this.last.target + degrees, time);
398};
399
400/**
401 * move Alias for Servo.prototype.to
402 */
403Servo.prototype.move = function(degrees, time) {
404 console.warn("Servo.prototype.move has been renamed to Servo.prototype.to");
405
406 return this.to(degrees, time);
407};
408
409/**
410 * min Set Servo to minimum degrees, defaults to 0deg
411 * @param {Number} time Time to spend in motion.
412 * @param {Number} rate The rate of the motion transiton
413 * @return {Object} instance
414 */
415
416Servo.prototype.min = function(time, rate) {
417 return this.to(this.range[0], time, rate);
418};
419
420/**
421 * max Set Servo to maximum degrees, defaults to 180deg
422 * @param {Number} time Time to spend in motion.
423 * @param {Number} rate The rate of the motion transiton
424 * @return {[type]} [description]
425 */
426Servo.prototype.max = function(time, rate) {
427 return this.to(this.range[1], time, rate);
428};
429
430/**
431 * center Set Servo to centerpoint, defaults to 90deg
432 * @param {Number} time Time to spend in motion.
433 * @param {Number} rate The rate of the motion transiton
434 * @return {[type]} [description]
435 */
436Servo.prototype.center = function(time, rate) {
437 return this.to(Math.abs((this.range[0] + this.range[1]) / 2), time, rate);
438};
439
440/**
441 * home Return Servo to startAt position
442 */
443Servo.prototype.home = function() {
444 return this.to(this.startAt);
445};
446
447/**
448 * sweep Sweep the servo between min and max or provided range
449 * @param {Array} range constrain sweep to range
450 *
451 * @param {Object} options Set range or interval.
452 *
453 * @return {[type]} [description]
454 */
455Servo.prototype.sweep = function(opts) {
456
457 var options = {
458 keyFrames: [{
459 value: this.range[0]
460 }, {
461 value: this.range[1]
462 }],
463 metronomic: true,
464 loop: true,
465 easing: "inOutSine"
466 };
467
468 // If opts is an array, then assume a range was passed
469 if (Array.isArray(opts)) {
470 options.keyFrames = rangeToKeyFrames(opts);
471 } else {
472 if (typeof opts === "object" && opts !== null) {
473 Object.assign(options, opts);
474 /* istanbul ignore else */
475 if (Array.isArray(options.range)) {
476 options.keyFrames = rangeToKeyFrames(options.range);
477 }
478 }
479 }
480
481 return this.to(options);
482};
483
484function rangeToKeyFrames(range) {
485 return range.map(function(value) {
486 return { value: value };
487 });
488}
489
490/**
491 * stop Stop a moving servo
492 * @return {[type]} [description]
493 */
494Servo.prototype.stop = function() {
495 var state = priv.get(this);
496
497 if (state.animation) {
498 state.animation.stop();
499 }
500
501 if (this.type === "continuous") {
502 this.to(
503 this.deadband.reduce(function(a, b) {
504 return Math.round((a + b) / 2);
505 })
506 );
507 } else {
508 clearInterval(this.interval);
509 }
510
511 return this;
512};
513
514//
515["clockWise", "cw", "counterClockwise", "ccw"].forEach(function(api) {
516 Servo.prototype[api] = function(rate) {
517 var range;
518 rate = rate === undefined ? 1 : rate;
519 /* istanbul ignore if */
520 if (this.type !== "continuous") {
521 this.board.error(
522 "Servo",
523 "Servo.prototype." + api + " is only available for continuous servos"
524 );
525 }
526 if (api === "cw" || api === "clockWise") {
527 range = [rate, 0, 1, this.deadband[1] + 1, this.range[1]];
528 } else {
529 range = [rate, 0, 1, this.deadband[0] - 1, this.range[0]];
530 }
531 return this.to(Fn.scale.apply(null, range) | 0);
532 };
533});
534
535
536/**
537 *
538 * Static API
539 *
540 *
541 */
542
543Servo.Continuous = function(pinOrOpts) {
544 var opts = {};
545
546 if (typeof pinOrOpts === "object") {
547 Object.assign(opts, pinOrOpts);
548 } else {
549 opts.pin = pinOrOpts;
550 }
551
552 opts.type = "continuous";
553 return new Servo(opts);
554};
555
556Servo.Continuous.speeds = {
557 // seconds to travel 60 degrees
558 "@4.8V": 0.23,
559 "@5.0V": 0.17,
560 "@6.0V": 0.18
561};
562
563/**
564 * Servos()
565 * new Servos()
566 */
567function Servos(numsOrObjects) {
568 if (!(this instanceof Servos)) {
569 return new Servos(numsOrObjects);
570 }
571
572 Object.defineProperty(this, "type", {
573 value: Servo
574 });
575
576 Collection.call(this, numsOrObjects);
577}
578
579util.inherits(Servos, Collection);
580
581/*
582 * Servos, center()
583 *
584 * centers all servos to 90deg
585 *
586 * eg. array.center();
587
588 * Servos, min()
589 *
590 * set all servos to the minimum degrees
591 * defaults to 0
592 *
593 * eg. array.min();
594
595 * Servos, max()
596 *
597 * set all servos to the maximum degrees
598 * defaults to 180
599 *
600 * eg. array.max();
601
602 * Servos, stop()
603 *
604 * stop all servos
605 *
606 * eg. array.stop();
607 */
608
609Collection.installMethodForwarding(
610 Servos.prototype, Servo.prototype
611);
612
613
614/**
615 * Animation.normalize
616 *
617 * @param [number || object] keyFrames An array of step values or a keyFrame objects
618 */
619Servos.prototype[Animation.normalize] = function(keyFrameSet) {
620 return keyFrameSet.map(function(keyFrames, index) {
621 if (keyFrames !== null && Array.isArray(keyFrames)) {
622 var servo = this[index];
623
624 // If servo is a servoArray then user servo[0] for default values
625 if (servo instanceof Servos) {
626 servo = servo[0];
627 }
628
629 var last = servo.last ? servo.last.target : servo.startAt;
630
631 // If the first keyFrameSet is null use the current position
632 if (keyFrames[0] === null) {
633 keyFrames[0] = {
634 value: last
635 };
636 }
637
638 if (Array.isArray(keyFrames)) {
639 if (keyFrames[0] === null) {
640 keyFrameSet[index][0] = {
641 value: last
642 };
643 }
644 }
645 return this[index][Animation.normalize](keyFrames);
646 }
647
648 if (keyFrames && typeof keyFrames.degrees === "number") {
649 keyFrames.value = keyFrames.degrees;
650 delete keyFrames.degrees;
651 }
652 return keyFrames;
653 }, this);
654};
655
656/**
657 * Animation.render
658 *
659 * @position [number] array of values to set the servos to
660 */
661Servos.prototype[Animation.render] = function(position) {
662 return this.each(function(servo, i) {
663 servo.to(position[i]);
664 });
665};
666
667
668// Assign Servos Collection class as static "method" of Servo.
669// TODO: Eliminate .Array for 1.0.0
670Servo.Array = Servos;
671Servo.Collection = Servos;
672
673// Alias
674// TODO: Deprecate and REMOVE
675Servo.prototype.write = Servo.prototype.move;
676
677/* istanbul ignore else */
678if (!!process.env.IS_TEST_MODE) {
679 Servo.Controllers = Controllers;
680 Servo.purge = function() {
681 priv.clear();
682 };
683}
684
685module.exports = Servo;
686// References
687//
688// http://www.societyofrobots.com/actuators_servos.shtml
689// http://www.parallax.com/Portals/0/Downloads/docs/prod/motors/900-00008-CRServo-v2.2.pdf
690// http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM
691// http://servocity.com/html/hs-7980th_servo.html
692// http://mbed.org/cookbook/Servo
693
694// Further API info:
695// http://www.tinkerforge.com/doc/Software/Bricks/Servo_Brick_Python.html#servo-brick-python-api
696// http://www.tinkerforge.com/doc/Software/Bricks/Servo_Brick_Java.html#servo-brick-java-api