UNPKG

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