johnny-five
Version:
The JavaScript Arduino Programming Framework.
430 lines (365 loc) • 9.5 kB
JavaScript
var Board = require("../lib/board.js"),
events = require("events"),
util = require("util");
var priv = new Map(),
steppers = [];
var MAXSTEPPERS = 6; // correlates with MAXSTEPPERS in firmware
function Step(stepper) {
this.rpm = 180;
this.direction = -1;
this.speed = 0;
this.accel = 0;
this.decel = 0;
this.stepper = stepper;
}
Step.PROPERTIES = ["rpm", "direction", "speed", "accel", "decel"];
Step.DEFAULTS = [180, -1, 0, 0, 0];
function MotorPins(pins) {
var k = 0;
pins = pins.slice();
while (pins.length) {
this["motor" + (++k)] = pins.shift();
}
}
/**
* Stepper
*
* Class for handling steppers using AdvancedFirmata support for asynchronous stepper control
*
*
* five.Stepper({
* type: constant, // io.STEPPER.TYPE.*
* stepsPerRev: number, // steps to make on revolution of stepper
* pins: {
* step: number, // pin attached to step pin on driver (used for type DRIVER)
* dir: number, // pin attached to direction pin on driver (used for type DRIVER)
* motor1: number, // (used for type TWO_WIRE and FOUR_WIRE)
* motor2: number, // (used for type TWO_WIRE and FOUR_WIRE)
* motor3: number, // (used for type FOUR_WIRE)
* motor4: number, // (used for type FOUR_WIRE)
* }
* });
*
*
* five.Stepper({
* type: five.Stepper.TYPE.DRIVER
* stepsPerRev: number,
* pins: {
* step: number
* dir: number
* }
* });
*
* five.Stepper({
* type: five.Stepper.TYPE.DRIVER
* stepsPerRev: number,
* pins: [ step, dir ]
* });
*
* five.Stepper({
* type: five.Stepper.TYPE.TWO_WIRE
* stepsPerRev: number,
* pins: {
* motor1: number,
* motor2: number
* }
* });
*
* five.Stepper({
* type: five.Stepper.TYPE.TWO_WIRE
* stepsPerRev: number,
* pins: [ motor1, motor2 ]
* });
*
* five.Stepper({
* type: five.Stepper.TYPE.FOUR_WIRE
* stepsPerRev: number,
* pins: {
* motor1: number,
* motor2: number,
* motor3: number,
* motor4: number
* }
* });
*
* five.Stepper({
* type: five.Stepper.TYPE.FOUR_WIRE
* stepsPerRev: number,
* pins: [ motor1, motor2, motor3, motor4 ]
* });
*
*
* @param {Object} opts
*
*/
function Stepper(opts) {
var initial, params = [];
if (!(this instanceof Stepper)) {
return new Stepper(opts);
}
// Initialize a Device instance on a Board
Board.Device.call(
this, opts = Board.Options(opts)
);
if (this.io.firmware.name.indexOf("AdvancedFirmata") === -1) {
throw new Error(
"Stepper requires AdvancedFirmata. https://github.com/soundanalogous/AdvancedFirmata"
);
}
if (!opts.pins) {
throw new Error(
"Stepper requires a `pins` object or array"
);
}
if (!opts.stepsPerRev) {
throw new Error(
"Stepper requires a `stepsPerRev` number value"
);
}
this.id = steppers.length;
if (this.id >= MAXSTEPPERS) {
throw new Error(
"Stepper cannot exceed max steppers (" + MAXSTEPPERS + ")"
);
}
// Convert an array of pins to the appropriate named pin
if (Array.isArray(this.pins)) {
if (this.pins.length === 2) {
// Using an array of 2 pins requres a TYPE
// to disambiguate DRIVER and TWO_WIRE
if (!opts.type) {
throw new Error(
"Stepper requires a `type` number value (DRIVER, TWO_WIRE)"
);
}
}
if (opts.type === Stepper.TYPE.DRIVER) {
this.pins = {
step: this.pins[0],
dir: this.pins[1]
};
} else {
this.pins = new MotorPins(this.pins);
}
}
// Attempt to guess the type if none is provided
if (!opts.type) {
if (this.pins.dir) {
opts.type = Stepper.TYPE.DRIVER;
} else {
if (this.pins.motor3) {
opts.type = Stepper.TYPE.FOUR_WIRE;
} else {
opts.type = Stepper.TYPE.TWO_WIRE;
}
}
}
// Initial Stepper config params (same for all 3 types)
params.push(this.id, opts.type, opts.stepsPerRev);
if (opts.type === Stepper.TYPE.DRIVER) {
if (!this.pins.dir || !this.pins.step) {
throw new Error(
"Stepper.TYPE.DRIVER expects: pins.dir, pins.step"
);
}
params.push(
this.pins.dir, this.pins.step
);
}
if (opts.type === Stepper.TYPE.TWO_WIRE) {
if (!this.pins.motor1 || !this.pins.motor2) {
throw new Error(
"Stepper.TYPE.TWO_WIRE expects: pins.motor1, pins.motor2"
);
}
params.push(
this.pins.motor1, this.pins.motor2
);
}
if (opts.type === Stepper.TYPE.FOUR_WIRE) {
if (!this.pins.motor1 || !this.pins.motor2 || !this.pins.motor3 || !this.pins.motor4) {
throw new Error(
"Stepper.TYPE.FOUR_WIRE expects: pins.motor1, pins.motor2, pins.motor3, pins.motor4"
);
}
params.push(
this.pins.motor1, this.pins.motor2, this.pins.motor3, this.pins.motor4
);
}
// Iterate the params and set each pin's mode to MODES.STEPPER
// Params:
// [deviceNum, type, stepsPerRev, dirOrMotor1Pin, stepOrMotor2Pin, motor3Pin, motor4Pin]
// The first 3 are required, the remaining 2-4 will be pins
params.slice(3).forEach(function(pin) {
this.io.pinMode(pin, this.io.MODES.STEPPER);
}, this);
this.io.stepperConfig.apply(this.io, params);
steppers.push(this);
initial = Step.PROPERTIES.reduce(function(state, key, i) {
return (state[key] = typeof opts[key] !== "undefined" ? opts[key] : Step.DEFAULTS[i], state);
}, {
isRunning: false
});
priv.set(this, initial);
}
Object.defineProperties(Stepper, {
TYPE: {
value: Object.freeze({
DRIVER: 1,
TWO_WIRE: 2,
FOUR_WIRE: 4
})
},
RUNSTATE: {
value: Object.freeze({
STOP: 0,
ACCEL: 1,
DECEL: 2,
RUN: 3
})
},
DIRECTION: {
value: Object.freeze({
CCW: 0,
CW: 1
})
}
});
/**
* rpm
*
* Gets the rpm value or sets the rpm in revs per minute
* making an internal conversion to speed in `0.01 * rad/s`
*
* @param {Number} rpm Revs per minute
*
* NOTE: *rpm* is optional, if missing
* the method will behave like a getter
*
* @return {Stepper} this Chainable method when used as a setter
*/
Stepper.prototype.rpm = function(rpm) {
var state = priv.get(this);
if (typeof rpm === "undefined") {
return state.rpm;
}
state.rpm = rpm;
state.speed = Math.round(rpm * (2 * Math.PI) * 100 / 60);
return this;
};
/**
* speed
*
* Gets the speed value or sets the speed in `0.01 * rad/s`
* making an internal conversion to rpm
*
* @param {Number} speed Speed given in 0.01 * rad/s
*
* NOTE: *speed* is optional, if missing
* the method will behave like a getter
*
* @return {Stepper} this Chainable method when used as a setter
*/
Stepper.prototype.speed = function(speed) {
var state = priv.get(this);
if (typeof speed === "undefined") {
return state.speed;
}
state.speed = speed;
state.rpm = Math.round(speed / (2 * Math.PI) / 100 * 60);
return this;
};
["direction", "accel", "decel"].forEach(function(prop) {
Stepper.prototype[prop] = function(value) {
var state = priv.get(this);
if (typeof value === "undefined") {
return state[prop];
}
state[prop] = value;
return this;
};
});
Stepper.prototype.ccw = function() {
return this.direction(0);
};
Stepper.prototype.cw = function() {
return this.direction(1);
};
/**
* step
*
* Move stepper motor a number of steps and call the callback on completion
*
* @param {Number} stepsOrOpts Steps to move using current settings for speed, accel, etc.
* @param {Object} stepsOrOpts Options object containing any of the following:
* stepsOrOpts = {
* steps:
* rpm:
* speed:
* direction:
* accel:
* decel:
* }
*
* NOTE: *steps* is required.
*
* @param {Function} callback function(err, complete)
*/
Stepper.prototype.step = function(stepsOrOpts, callback) {
var steps, step, state, params, isValidStep;
steps = typeof stepsOrOpts === "object" ?
(stepsOrOpts.steps || 0) : Math.floor(stepsOrOpts);
step = new Step(this);
state = priv.get(this);
params = [];
isValidStep = true;
function failback() {
isValidStep = false;
callback();
}
params.push(steps);
if (typeof stepsOrOpts === "object") {
// If an object of property values has been provided,
// call the correlating method with the value argument.
Step.PROPERTIES.forEach(function(key) {
if (typeof stepsOrOpts[key] !== "undefined") {
this[key](stepsOrOpts[key]);
}
}, this);
}
if (!state.speed) {
this.rpm(state.rpm);
step.speed = this.speed();
}
// Ensure that the property params are set in the
// correct order, but without rpm
Step.PROPERTIES.slice(1).forEach(function(key) {
params.push(step[key] = this[key]());
}, this);
if (steps === 0) {
failback(
new Error("Must set a number of steps")
);
}
if (step.direction < 0) {
failback(
new Error("Must set a direction")
);
}
if (isValidStep) {
state.isRunning = true;
params.push(function(complete) {
state.isRunning = false;
callback(null, complete);
});
step.move.apply(step, params);
}
return this;
};
Step.prototype.move = function(steps, dir, speed, accel, decel, callback) {
// Restore the param order... (steps, dir => dir, steps)
this.stepper.io.stepperStep.apply(
this.stepper.io, [this.stepper.id, dir, steps, speed, accel, decel, callback]
);
};
module.exports = Stepper;