UNPKG

johnny-five

Version:

The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!

464 lines (397 loc) 10.5 kB
const Board = require("./board"); const Fn = require("./fn"); const priv = new Map(); const steppers = new Map(); const TAU = Fn.TAU; const MAXSTEPPERS = 6; // correlates with MAXSTEPPERS in firmware class Step { constructor(stepper) { this.rpm = 180; this.direction = -1; this.speed = 0; this.accel = 0; this.decel = 0; this.stepper = stepper; } move(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] ); } } Step.PROPERTIES = ["rpm", "direction", "speed", "accel", "decel"]; Step.DEFAULTS = [180, -1, 0, 0, 0]; function MotorPins(pins) { let k = 0; pins = pins.slice(); while (pins.length) { this[`motor${++k}`] = pins.shift(); } } function isSupported({pins, MODES}) { return pins.some(({supportedModes}) => supportedModes.includes(MODES.STEPPER)); } /** * 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} options * */ class Stepper { constructor(options) { const params = []; let state; Board.Component.call( this, options = Board.Options(options) ); if (!isSupported(this.io)) { throw new Error( "Stepper is not supported" ); } if (!options.pins) { throw new Error( "Stepper requires a `pins` object or array" ); } if (!options.stepsPerRev) { throw new Error( "Stepper requires a `stepsPerRev` number value" ); } steppers.set(this.board, steppers.get(this.board) || []); this.id = steppers.get(this.board).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 (!options.type) { throw new Error( "Stepper requires a `type` number value (DRIVER, TWO_WIRE)" ); } } if (options.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 (!options.type) { if (this.pins.dir) { options.type = Stepper.TYPE.DRIVER; } else { if (this.pins.motor3) { options.type = Stepper.TYPE.FOUR_WIRE; } else { options.type = Stepper.TYPE.TWO_WIRE; } } } // Initial Stepper config params (same for all 3 types) params.push(this.id, options.type, options.stepsPerRev); if (options.type === Stepper.TYPE.DRIVER) { if (typeof this.pins.dir === "undefined" || typeof this.pins.step === "undefined") { throw new Error( "Stepper.TYPE.DRIVER expects: `pins.dir`, `pins.step`" ); } params.push( this.pins.dir, this.pins.step ); } if (options.type === Stepper.TYPE.TWO_WIRE) { if (typeof this.pins.motor1 === "undefined" || typeof this.pins.motor2 === "undefined") { throw new Error( "Stepper.TYPE.TWO_WIRE expects: `pins.motor1`, `pins.motor2`" ); } params.push( this.pins.motor1, this.pins.motor2 ); } if (options.type === Stepper.TYPE.FOUR_WIRE) { if (typeof this.pins.motor1 === "undefined" || typeof this.pins.motor2 === "undefined" || typeof this.pins.motor3 === "undefined" || typeof this.pins.motor4 === "undefined") { 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((pin) => { this.io.pinMode(pin, this.io.MODES.STEPPER); }); this.io.stepperConfig.apply(this.io, params); steppers.get(this.board).push(this); state = Step.PROPERTIES.reduce((state, key, i) => (state[key] = typeof options[key] !== "undefined" ? options[key] : Step.DEFAULTS[i], state), { isRunning: false, type: options.type, pins: this.pins }); priv.set(this, state); Object.defineProperties(this, { type: { get() { return state.type; } }, pins: { get() { return state.pins; } } }); } /** * 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 */ rpm(rpm) { const state = priv.get(this); if (typeof rpm === "undefined") { return state.rpm; } state.rpm = rpm; state.speed = Math.round(rpm * TAU * 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 */ speed(speed) { const state = priv.get(this); if (typeof speed === "undefined") { return state.speed; } state.speed = speed; state.rpm = Math.round(speed / TAU / 100 * 60); return this; } ccw() { return this.direction(0); } cw() { 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) */ step(stepsOrOpts, callback) { let steps; let step; let state; let params; let isValidStep; steps = typeof stepsOrOpts === "object" ? (stepsOrOpts.steps || 0) : Math.floor(stepsOrOpts); step = new Step(this); state = priv.get(this); params = []; isValidStep = true; function failback(error) { isValidStep = false; if (callback) { callback(error); } } 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((key) => { if (typeof stepsOrOpts[key] !== "undefined") { this[key](stepsOrOpts[key]); } }); } 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((key) => { params.push(step[key] = this[key]()); }); if (steps === 0) { failback( new Error( "Must set a number of steps when calling `step()`" ) ); } if (step.direction < 0) { failback( new Error( "Must set a direction before calling `step()`" ) ); } if (isValidStep) { state.isRunning = true; params.push(complete => { state.isRunning = false; callback(null, complete); }); step.move.apply(step, params); } return this; } } 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 }) } }); ["direction", "accel", "decel"].forEach(prop => { Stepper.prototype[prop] = function(value) { const state = priv.get(this); if (typeof value === "undefined") { return state[prop]; } state[prop] = value; return this; }; }); module.exports = Stepper;