UNPKG

johnny-five

Version:

The JavaScript Arduino Programming Framework.

690 lines (553 loc) 14.3 kB
var Board = require("../lib/board.js"); var Pins = Board.Pins; var events = require("events"); var Emitter = require("events").EventEmitter; var util = require("util"); var __ = require("../lib/fn.js"); var Animation = require("../lib/animation.js"); // Sensor instance private data var priv = new Map(); var SERVOS = []; /** * Servo * @constructor * * @param {Object} opts Options: pin, type, id, range */ function Servo(opts) { var history = []; var pinValue; if (!(this instanceof Servo)) { return new Servo(opts); } pinValue = typeof opts === "object" ? opts.pin : opts; // Initialize a Device instance on a Board Board.Device.call( this, opts = Board.Options(opts) ); // StandardFirmata on Arduino allows controlling // servos from analog pins. // If we're currently operating with an Arduino // and the user has provided an analog pin name // (eg. "A0", "A5" etc.), parse out the numeric // value and capture the fully qualified analog // pin number. if (Pins.isFirmata(this)) { if (typeof pinValue === "string" && pinValue[0] === "A") { pinValue = this.io.analogPins[+pinValue.slice(1)]; } pinValue = +pinValue; // If the board's default pin normalization // came up with something different, use the // the local value. if (this.pin !== pinValue) { this.pin = pinValue; } } // When in debug mode, if pin is not a PWM pin, emit an error if (opts.debug && !this.board.pins.isServo(this.pin)) { Board.Pins.Error({ pin: this.pin, type: "PWM", via: "Servo", }); } this.id = opts.id || Board.uid(); this.range = opts.range || [0, 180]; this.fps = opts.fps || 100; this.offset = opts.offset || 0; this.mode = this.io.MODES.SERVO; this.interval = null; this.value = 0; // The type of servo determines certain alternate // behaviours in the API this.type = opts.type || "standard"; // Invert the value of all servoWrite operations // eg. 80 => 100, 90 => 90, 0 => 180 this.isInverted = opts.isInverted || false; if (Array.isArray(opts.pwmRange)) { this.io.servoConfig(this.pin, opts.pwmRange[0], opts.pwmRange[1]); } else { this.io.pinMode(this.pin, this.mode); } // Specification config this.specs = opts.specs || { speed: Servo.Continuous.speeds["@5.0V"] }; // Collect all movement history for this servo // history = [ // { // timestamp: Date.now(), // degrees: degrees // } // ]; priv.set(this, { history: history }); // Create a non-writable "last" property // shortcut to access the last servo movement Object.defineProperties(this, { history: { get: function() { return history.slice(-5); } }, last: { get: function() { return history[history.length - 1]; } }, position: { get: function() { return history[history.length - 1].degrees; } } }); // Allow "setup"instructions to come from // constructor options properties this.startAt = 90; // If "startAt" is defined and center is falsy // set servo to min or max degrees if (opts.startAt !== undefined) { this.startAt = opts.startAt; if (!opts.center) { this.to(opts.startAt); } } // If "center" true set servo to 90deg if (opts.center) { this.center(); } // Push this servo into the private // servo instance array. SERVOS.push(this); } util.inherits(Servo, Emitter); /** * to * * Set the servo horn's position to given degree over time. * * @param {Number} degrees Degrees to turn servo to. * @param {Number} time Time to spend in motion. * @param {Number} rate The rate of the motion transiton * * @return {Servo} instance */ Servo.prototype.to = function(degrees, time, rate) { // Servo is restricted to integers degrees |= 0; var target = degrees; degrees += this.offset; // Enforce limited range of motion degrees = Board.constrain(degrees, this.range[0], this.range[1]); this.value = degrees; var last, distance, percent; var isReverse = false; var history = priv.get(this).history; if (this.isInverted) { degrees = Board.map( degrees, this.range[0], this.range[1], this.range[1], this.range[0] ); } // If same degrees, emit "move:complete" and return immediately. if (this.last && this.last.degrees === degrees) { process.nextTick(this.emit.bind(this, "move:complete")); // this.emit("move:complete"); return this; } if (typeof time !== "undefined") { // If rate is not passed, calculate based on time and fps rate = rate || Math.ceil(time / 1000) * this.fps; last = this.last && this.last.degrees || 0; distance = Math.abs(last - degrees); percent = 0; // If steps are limited by Servo resolution if (distance < rate) { rate = distance; } if (this.interval) { clearInterval(this.interval); } if (degrees < last) { isReverse = true; } this.interval = setInterval(function() { var delta = ++percent * (distance / rate); if (isReverse) { delta *= -1; } this.io.servoWrite(this.pin, last + delta); history.push({ timestamp: Date.now(), degrees: last + delta, target: target }); if (percent === rate) { this.emit("move:complete"); clearInterval(this.interval); } }.bind(this), time / rate); } else { this.io.servoWrite(this.pin, degrees); history.push({ timestamp: Date.now(), degrees: degrees, target: target }); } // return this instance return this; }; /** * Animation.normalize * * @param [number || object] keyFrames An array of step values or a keyFrame objects */ Servo.prototype[Animation.normalize] = function(keyFrames) { var last = this.last ? this.last.target : this.startAt; // If user passes null as the first element in keyFrames use current position if (keyFrames[0] === null) { keyFrames[0] = { degrees: last }; } return keyFrames; }; /** * Animation.render * * @position [number] value to set the servo to */ Servo.prototype[Animation.render] = function(position) { return this.to(position[0]); }; /** * step * * Update the servo horn's position by specified degrees (over time) * * @param {Number} degrees Degrees to turn servo to. * @param {Number} time Time to spend in motion. * * @return {Servo} instance */ Servo.prototype.step = function(degrees, time) { return this.to(this.last.target + degrees, time); }; /** * move Alias for Servo.prototype.to */ Servo.prototype.move = function(degrees, time) { console.warn("Servo.prototype.move has been renamed to Servo.prototype.to"); return this.to(degrees, time); }; /** * min Set Servo to minimum degrees, defaults to 0deg * @return {Object} instance */ Servo.prototype.min = function() { return this.to(this.range[0]); }; /** * max Set Servo to maximum degrees, defaults to 180deg * @return {[type]} [description] */ Servo.prototype.max = function() { return this.to(this.range[1]); }; /** * center Set Servo to centerpoint, defaults to 90deg * @return {[type]} [description] */ Servo.prototype.center = function() { return this.to(Math.abs((this.range[0] + this.range[1]) / 2)); }; /** * sweep Sweep the servo between min and max or provided range * @param {Array} range constrain sweep to range * * @param {Object} options Set range or interval. * * @return {[type]} [description] */ Servo.prototype.sweep = function(opts) { var degrees, range = this.range, interval = 100, step = 10; opts = opts || {}; // If opts is an array, then assume a range was passed // // - This implies: // - an interval of 100ms. // - a step of 10 degrees // if (Array.isArray(opts)) { range = opts; } else { // Otherwise, opts is an object. // // - Check for: // - a range, if present use it, otherwise // use the servo's range property. // - an interval, if present use it, otherwise // use the default interval. // - a step, if present use it, otherwise // use the default step // range = opts.range || range; interval = opts.interval || interval; step = opts.step || step; } degrees = range[0]; // If the last recorded movement was not range[0]deg // move the servo to range[0]deg if (this.last && this.last.degrees !== degrees) { this.to(degrees); } if (this.interval) { clearInterval(this.interval); } this.interval = setInterval(function() { var abs; if (degrees >= range[1] || degrees < range[0]) { if (degrees >= range[1]) { this.emit("sweep:half"); } step *= -1; } if (degrees === range[0]) { if (step !== (abs = Math.abs(step))) { this.emit("sweep:full"); step = abs; } } degrees += step; this.to(degrees); }.bind(this), interval); return this; }; /** * stop Stop a moving servo * @return {[type]} [description] */ Servo.prototype.stop = function() { if (this.type === "continuous") { this.to(90); } else { clearInterval(this.interval); } return this; }; /** Speeds for continuous rotation * * Mock directions: A, B * * 0 Full speed A * 89 Slowest speed A * 90 Stopped * 91 Slowest speed B * 180 Fastest speed B * * **/ /** * Degrees to Pulse lengths in ms * Servo.pulse = { * lengths: { * 0: 1, * 90: 1, * 180: 2 * }, * width: 2 / 180 * }; * **/ [{ apis: ["clockWise", "cw"], args: [0, 1, 91, 180] }, { apis: ["counterClockwise", "ccw"], args: [0, 1, 89, 0] }].forEach(function(setup) { var args = setup.args.slice(); setup.apis.forEach(function(api) { Servo.prototype[api] = function(rate) { var copy = args.slice(); rate = rate === undefined ? 1 : rate; if (this.type !== "continuous") { this.board.error( "Servo", "Servo.prototype." + api + " is only available for continuous servos" ); } copy.unshift(rate); return this.to(__.scale.apply(null, copy) | 0); }; }); }); /** * * Static API * * */ Servo.Continuous = function(pinOrOpts) { var opts = {}; if (typeof pinOrOpts === "object") { __.extend(opts, pinOrOpts); } else { opts.pin = pinOrOpts; } opts.type = "continuous"; return new Servo(opts); }; Servo.Continuous.speeds = { // seconds to travel 60 degrees "@4.8V": 0.23, "@5.0V": 0.17, "@6.0V": 0.18 }; /** * Servo.Array() * new Servo.Array() * * Constructs an Array-like instance of all servos */ Servo.Array = function(pins) { if (!(this instanceof Servo.Array)) { return new Servo.Array(pins); } var servos = []; var pinOrServo; if (pins) { while (pins.length) { pinOrServo = pins.shift(); servos.push( typeof pinOrServo === "number" ? new Servo(pinOrServo) : pinOrServo ); } } else { servos = SERVOS.slice(); } servos.forEach(function(servo, index) { this[index] = servo; }, this); this.length = servos.length; }; /** * each Execute callbackFn for each active servo instance * * eg. * array.each(function( servo, index ) { * `this` refers to the current servo instance * }); * * @param {[type]} callbackFn [description] * @return {[type]} [description] */ Servo.Array.prototype.each = function(callbackFn) { var servo, i, length; length = this.length; for (i = 0; i < length; i++) { servo = this[i]; callbackFn.call(servo, servo, i); } return this; }; /** * Servo.Array, center() * * centers all servos to 90deg * * eg. array.center(); * Servo.Array, min() * * set all servos to the minimum degrees * defaults to 0 * * eg. array.min(); * Servo.Array, max() * * set all servos to the maximum degrees * defaults to 180 * * eg. array.max(); * Servo.Array, stop() * * stop all servos * * eg. array.stop(); */ Object.keys(Servo.prototype).forEach(function(method) { // Create Servo.Array wrappers for each method listed. // This will allow us control over all Servo instances // simultaneously. Servo.Array.prototype[method] = function() { var args = [].slice.call(arguments); this.each(function(servo) { Servo.prototype[method].apply(servo, args); }); return this; }; }); /** * Animation.normalize * * @param [number || object] keyFrames An array of step values or a keyFrame objects */ Servo.Array.prototype[Animation.normalize] = function(keyFrames) { keyFrames.forEach(function(keyFrame, index) { if (keyFrame !== null) { var servo = this[index]; // If servo is a servoArray then user servo[0] for default values if (servo instanceof Servo.Array) { servo = servo[0]; } var last = servo.last ? servo.last.target : servo.startAt; // If the first position is null use the current position if (keyFrame[0] === null) { keyFrames[index][0] = { degrees: last }; } if (Array.isArray(keyFrame)) { if (keyFrame[0] === null) { keyFrames[index][0] = { degrees: last }; } } } }, this); return keyFrames; }; /** * Animation.render * * @position [number] array of values to set the servos to */ Servo.Array.prototype[Animation.render] = function(position) { this.each(function(servo, i) { servo.to(position[i]); }); return this; }; // Alias // TODO: Deprecate and REMOVE Servo.prototype.write = Servo.prototype.move; module.exports = Servo; // References // // http://www.societyofrobots.com/actuators_servos.shtml // http://www.parallax.com/Portals/0/Downloads/docs/prod/motors/900-00008-CRServo-v2.2.pdf // http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM // http://servocity.com/html/hs-7980th_servo.html // http://mbed.org/cookbook/Servo // Further API info: // http://www.tinkerforge.com/doc/Software/Bricks/Servo_Brick_Python.html#servo-brick-python-api // http://www.tinkerforge.com/doc/Software/Bricks/Servo_Brick_Java.html#servo-brick-java-api