johnny-five-electron
Version:
Temporary fork to support Electron (to be deprecated)
686 lines (552 loc) • 15.6 kB
JavaScript
var IS_TEST_MODE = !!process.env.IS_TEST_MODE;
var Board = require("../lib/board.js");
var Pins = Board.Pins;
var Emitter = require("events").EventEmitter;
var util = require("util");
var Collection = require("../lib/mixins/collection");
var __ = require("../lib/fn.js");
var nanosleep = require("../lib/sleep.js").nano;
var Animation = require("../lib/animation.js");
// Servo instance private data
var priv = new Map();
var Controllers = {
PCA9685: {
REGISTER: {
value: {
PCA9685_MODE1: 0x0,
PCA9685_PRESCALE: 0xFE,
LED0_ON_L: 0x6
}
},
servoWrite: {
value: function(pin, degrees) {
var on, off;
// If same degrees return immediately.
if (this.last && this.last.degrees === degrees) {
return this;
}
on = 0;
off = __.map(degrees, 0, 180, this.pwmRange[0]/4, this.pwmRange[1]/4 );
this.io.i2cWrite(this.address, [this.REGISTER.LED0_ON_L + 4 * pin, on, on >> 8, off, off >> 8]);
}
},
initialize: {
/*
TODO:
Refactor this initialization as an abstract controller
*/
value: function(opts) {
this.address = opts.address || 0x40;
this.pwmRange = opts.pwmRange || [544, 2400];
if (!this.board.Drivers[this.address]) {
this.io.i2cConfig(opts);
this.board.Drivers[this.address] = {
initialized: false
};
// Reset
this.io.i2cWriteReg(this.address, this.REGISTER.PCA9685_MODE1, 0x00);
// Sleep
this.io.i2cWriteReg(this.address, this.REGISTER.PCA9685_MODE1, 0x10);
// Set prescalar
this.io.i2cWriteReg(this.address, this.REGISTER.PCA9685_PRESCALE, 0x70);
// Wake up
this.io.i2cWriteReg(this.address, this.REGISTER.PCA9685_MODE1, 0x00);
// Wait 5 nanoseconds for restart
nanosleep(5);
// Auto-increment
this.io.i2cWriteReg(this.address, this.REGISTER.PCA9685_MODE1, 0xa1);
this.board.Drivers[this.address].initialized = true;
}
}
}
},
Standard: {
initialize: {
value: function(opts) {
// 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",
});
}
if (Array.isArray(opts.pwmRange)) {
this.io.servoConfig(this.pin, opts.pwmRange[0], opts.pwmRange[1]);
} else {
this.io.pinMode(this.pin, this.mode);
}
}
},
servoWrite: {
value: function(pin, degrees) {
// Servo is restricted to integers
degrees |= 0;
// If same degrees return immediately.
if (this.last && this.last.degrees === degrees) {
return this;
}
this.io.servoWrite(this.pin, degrees);
}
}
}
};
/**
* Servo
* @constructor
*
* @param {Object} opts Options: pin, type, id, range
*/
function Servo(opts) {
var history = [];
var pinValue;
var controller;
if (!(this instanceof Servo)) {
return new Servo(opts);
}
pinValue = typeof opts === "object" ? opts.pin : opts;
Board.Component.call(
this, opts = Board.Options(opts)
);
this.range = opts.range || [0, 180];
this.deadband = opts.deadband || [90, 90];
this.fps = opts.fps || 100;
this.offset = opts.offset || 0;
this.mode = this.io.MODES.SERVO;
this.interval = null;
this.value = null;
// 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 (typeof opts.controller === "undefined" && 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 (!Number.isNaN(pinValue) && this.pin !== pinValue) {
this.pin = pinValue;
}
}
// 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
if (opts.isInverted) {
console.warn("The 'isInverted' property has been renamed 'invert'");
}
this.invert = opts.isInverted || opts.invert || false;
// Specification config
this.specs = opts.specs || {
speed: Servo.Continuous.speeds["@5.0V"]
};
// Allow "setup"instructions to come from
// constructor options properties
this.startAt = 90;
// Collect all movement history for this servo
// history = [
// {
// timestamp: Date.now(),
// degrees: degrees
// }
// ];
priv.set(this, {
history: history
});
/**
* Used for adding special controllers (i.e. PCA9685)
**/
controller = typeof opts.controller === "string" ?
Controllers[opts.controller] : Controllers.Standard;
Object.defineProperties(this, Object.assign({}, controller, {
history: {
get: function() {
return history.slice(-5);
}
},
last: {
get: function() {
return history[history.length - 1];
}
},
position: {
get: function() {
return history[history.length - 1].degrees;
}
}
}));
this.initialize(opts);
// 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();
}
}
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
*
* - or -
*
* @param {Object} an Animation() segment config object
*
* @return {Servo} instance
*/
Servo.prototype.to = function(degrees, time, rate) {
var options = {};
var state = priv.get(this);
if (typeof degrees === "object") {
options = {
duration: 1000,
cuePoints: [0, 1.0],
keyFrames: [null, {degrees: typeof degrees.degrees === "number" ? degrees.degrees : this.startAt}],
oncomplete: function() {
this.stop();
process.nextTick(this.emit.bind(this, "move:complete"));
}.bind(this)
};
__.extend(options, degrees);
state.isRunning = true;
state.animation = state.animation || new Animation(this);
state.animation.enqueue(options);
} else {
var target = degrees;
// Enforce limited range of motion
degrees = Board.constrain(degrees, this.range[0], this.range[1]);
degrees += this.offset;
this.value = degrees;
if (this.invert) {
degrees = Board.map(
degrees,
0, 180,
180, 0
);
}
if (typeof time !== "undefined") {
options = {
duration: time,
keyFrames: [null, {degrees: degrees}],
fps: rate || this.fps
};
this.to(options);
} else {
this.servoWrite(this.pin, degrees);
state.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] = {
value: last
};
}
// There are a couple of properties that are device type sepcific
// that we need to convert to something generic
keyFrames.forEach(function(keyFrame) {
if (typeof keyFrame.degrees !== "undefined") {
keyFrame.value = keyFrame.degrees;
}
if (typeof keyFrame.copyDegrees !== "undefined") {
keyFrame.copyValue = keyFrame.copyDegrees;
}
});
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
* @param {Number} time Time to spend in motion.
* @param {Number} rate The rate of the motion transiton
* @return {Object} instance
*/
Servo.prototype.min = function(time, rate) {
return this.to(this.range[0], time, rate);
};
/**
* max Set Servo to maximum degrees, defaults to 180deg
* @param {Number} time Time to spend in motion.
* @param {Number} rate The rate of the motion transiton
* @return {[type]} [description]
*/
Servo.prototype.max = function(time, rate) {
return this.to(this.range[1], time, rate);
};
/**
* center Set Servo to centerpoint, defaults to 90deg
* @param {Number} time Time to spend in motion.
* @param {Number} rate The rate of the motion transiton
* @return {[type]} [description]
*/
Servo.prototype.center = function(time, rate) {
return this.to(Math.abs((this.range[0] + this.range[1]) / 2), time, rate);
};
/**
* 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 options = {
keyFrames: [{degrees: this.range[0]}, {degrees: this.range[1]}],
metronomic: true,
loop: true,
easing: "inOutSine"
};
// If opts is an array, then assume a range was passed
if (Array.isArray(opts)) {
options.keyframes = opts;
} else {
// Otherwise, opts is an object.
__.extend(options, opts);
}
this.to(options);
return this;
};
/**
* stop Stop a moving servo
* @return {[type]} [description]
*/
Servo.prototype.stop = function() {
var state = priv.get(this);
if (state.animation) {
state.animation.stop();
}
if (this.type === "continuous") {
this.to(90);
} else {
clearInterval(this.interval);
}
return this;
};
//
["clockWise", "cw", "counterClockwise", "ccw"].forEach(function(api) {
Servo.prototype[api] = function(rate) {
var range;
rate = rate === undefined ? 1 : rate;
if (this.type !== "continuous") {
this.board.error(
"Servo",
"Servo.prototype." + api + " is only available for continuous servos"
);
}
if (api === "cw" || api === "clockWise") {
range = [rate, 0, 1, this.deadband[1] + 1, this.range[1]];
} else {
range = [rate, 0, 1, this.deadband[0] - 1, this.range[0]];
}
return this.to(__.scale.apply(null, range) | 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
};
/**
* Servos()
* new Servos()
*
* Constructs an Array-like instance of all servos
*/
function Servos(numsOrObjects) {
if (!(this instanceof Servos)) {
return new Servos(numsOrObjects);
}
Object.defineProperty(this, "type", {
value: Servo
});
Collection.call(this, numsOrObjects);
}
Servos.prototype = Object.create(Collection.prototype, {
constructor: {
value: Servos
}
});
/*
* Servos, center()
*
* centers all servos to 90deg
*
* eg. array.center();
* Servos, min()
*
* set all servos to the minimum degrees
* defaults to 0
*
* eg. array.min();
* Servos, max()
*
* set all servos to the maximum degrees
* defaults to 180
*
* eg. array.max();
* Servos, stop()
*
* stop all servos
*
* eg. array.stop();
*/
Object.keys(Servo.prototype).forEach(function(method) {
// Create Servos wrappers for each method listed.
// This will allow us control over all Servo instances
// simultaneously.
Servos.prototype[method] = function() {
var length = this.length;
for (var i = 0; i < length; i++) {
this[i][method].apply(this[i], arguments);
}
return this;
};
});
/**
* Animation.normalize
*
* @param [number || object] keyFrames An array of step values or a keyFrame objects
*/
Servos.prototype[Animation.normalize] = function(keyFrameSet) {
keyFrameSet.forEach(function(keyFrames, index) {
if (keyFrames !== null) {
var servo = this[index];
// If servo is a servoArray then user servo[0] for default values
if (servo instanceof Servos) {
servo = servo[0];
}
var last = servo.last ? servo.last.target : servo.startAt;
// If the first keyFrameSet is null use the current position
if (keyFrames[0] === null) {
keyFrames[0] = {
value: last
};
}
if (Array.isArray(keyFrames)) {
if (keyFrames[0] === null) {
keyFrameSet[index][0] = {
value: last
};
}
}
keyFrames.forEach(function(keyFrame) {
if (keyFrame != null && typeof keyFrame.degrees !== "undefined") {
keyFrame.value = keyFrame.degrees;
}
if (keyFrame != null && typeof keyFrame.copyDegrees !== "undefined") {
keyFrame.copyValue = keyFrame.copyDegrees;
}
});
}
}, this);
return keyFrameSet;
};
/**
* Animation.render
*
* @position [number] array of values to set the servos to
*/
Servos.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;
if (IS_TEST_MODE) {
Servo.purge = function() {
priv.clear();
};
}
// Assign Servos Collection class as static "method" of Servo.
Servo.Array = Servos;
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