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!

1,242 lines (1,110 loc) 24.7 kB
const Board = require("./board"); const Collection = require("./mixins/collection"); const Emitter = require("./mixins/emitter"); const EVS = require("./evshield"); const Expander = require("./expander.js"); const Fn = require("./fn"); const Sensor = require("./sensor"); const ShiftRegister = require("./shiftregister"); const priv = new Map(); const registers = new Map(); function registerKey(registerOpts) { return ["clock", "data", "latch"].reduce((accum, key) => `${accum}.${registerOpts[key]}`, ""); } function latch(state, bit, on) { return on ? state |= (1 << bit) : state &= ~(1 << bit); } function updateShiftRegister(motor, dir) { const rKey = registerKey(motor.settings.register); const register = registers.get(motor.board)[rKey]; let latchState = register.value; const bits = priv.get(motor).bits; const forward = dir !== "reverse"; // There are two ShiftRegister bits which we need to change based on the // direction of the motor. These will be the pins that control the HBridge // on the board. They will get flipped high/low based on the current flow // in the HBridge. latchState = latch(latchState, bits.a, forward); latchState = latch(latchState, bits.b, !forward); if (register.value !== latchState) { register.send(latchState); } } const Controllers = { DEFAULT: {}, ShiftRegister: { initialize: { value({register, bits}) { const rKey = registerKey(register); if (!bits || bits.a === undefined || bits.b === undefined) { throw new Error("ShiftRegister Motors MUST contain HBRIDGE bits {a, b}"); } priv.get(this).bits = bits; if (!registers.has(this.board)) { registers.set(this.board, {}); } if (!registers.get(this.board)[rKey]) { registers.get(this.board)[rKey] = new ShiftRegister({ board: this.board, pins: register }); } this.io.pinMode(this.pins.pwm, this.io.MODES.PWM); } }, dir: { value(dir) { this.stop(); updateShiftRegister(this, dir.name); this.direction = dir; process.nextTick(() => this.emit(dir.name)); return this; } } }, PCA9685: { initialize: { value({address, pwmRange, frequency}) { const state = priv.get(this); this.address = address || 0x40; this.pwmRange = pwmRange || [0, 4080]; this.frequency = frequency || 50; state.expander = Expander.get({ address: this.address, controller: this.controller, bus: this.bus, pwmRange: this.pwmRange, frequency: this.frequency, }); Object.keys(this.pins).forEach(pinName => { this.pins[pinName] = state.expander.normalize(this.pins[pinName]); }); } }, setPWM: { writable: true, value(pin, speed) { priv.get(this).expander.analogWrite(pin, speed); } }, setPin: { writable: true, value(pin, value) { priv.get(this).expander.digitalWrite(pin, value); } }, }, PCA9685_Hybrid: { initialize: { value({address, pwmRange, frequency}) { const state = priv.get(this); this.address = address || 0x40; this.pwmRange = pwmRange || [0, 4080]; this.frequency = frequency || 50; state.expander = Expander.get({ address: this.address, controller: "PCA9685", bus: this.bus, pwmRange: this.pwmRange, frequency: this.frequency, }); this.pins.pwm = state.expander.normalize(this.pins.pwm); } }, setPWM: { writable: true, value(pin, speed) { priv.get(this).expander.analogWrite(pin, speed); } }, }, EVS_EV3: { initialize: { value(options) { const state = priv.get(this); state.shield = EVS.shieldPort(options.pin); state.ev3 = new EVS(Object.assign(options, { io: this.io })); this.settings.pins = { pwm: options.pin, dir: options.pin, }; } }, setPWM: { value(pin, value) { const state = priv.get(this); const register = state.shield.motor === EVS.M1 ? EVS.SPEED_M1 : EVS.SPEED_M2; let speed = Fn.scale(value, 0, 255, 0, 100) | 0; if (value === 0) { state.ev3.write(state.shield, EVS.COMMAND, EVS.Motor_Reset); } else { if (!this.direction.value) { speed = -speed; } const data = [ // 0-100 speed, // Duration (0 is forever) 0, // Command B 0, // Command A EVS.CONTROL_SPEED | EVS.CONTROL_GO ]; state.ev3.write(state.shield, register, data); } } }, setPin: { value(pin, value) { this.setPWM(this.pin, value); } }, validatePins: { value() { // Check for required pins if (typeof this.settings.pin === "undefined") { throw new Error("Pin must be defined"); } } } }, GROVE_I2C_MOTOR_DRIVER: { REGISTER: { value: { ADDRESS: 0x0F, } }, COMMANDS: { value: { SET_SPEED: 0x82, SET_PWM_FREQUENCY: 0x84, SET_DIRECTION: 0xAA, NOOP: 0x01, } }, initialize: { value(options) { const state = priv.get(this); let shared = priv.get("GROVE_I2C_MOTOR_DRIVER"); if (!shared) { shared = { direction: { A: 0x01, B: 0x01, }, speed: { A: 0, B: 0, } }; priv.set("GROVE_I2C_MOTOR_DRIVER", shared); } state.shared = shared; state.pin = options.pin.toUpperCase(); this.settings.pins = { pwm: options.pin, dir: options.pin, }; this.address = options.address || this.REGISTER.ADDRESS; options.address = this.address; this.io.i2cConfig(options); } }, setPWM: { value(pin, value) { const state = priv.get(this); const speed = Board.constrain(value, 0, 255) | 0; state.shared.speed[state.pin] = speed; this.io.i2cWrite(this.address, [ this.COMMANDS.SET_SPEED, state.shared.speed.A, state.shared.speed.B, ]); } }, setPin: { value(pin, value) { const state = priv.get(this); // DIR_CCW = 0x02 // DIR_CW = 0x01 state.shared.direction[state.pin] = value ? 0x01 : 0x02; const a = state.shared.direction.A & 0x03; const b = state.shared.direction.B & 0x03; const direction = (b << 2) | a; this.io.i2cWrite(this.address, [ this.COMMANDS.SET_DIRECTION, direction, this.COMMANDS.NOOP, ]); } }, validatePins: { value() { // Check for required pins if (typeof this.settings.pin === "undefined") { throw new Error("Pin must be defined"); } } } } }; // Aliases // // NXT motors have the exact same control commands as EV3 motors Controllers.EVS_NXT = Controllers.EVS_EV3; const Devices = { NONDIRECTIONAL: { pins: { get() { if (this.settings.pin) { return { pwm: this.settings.pin }; } else { return this.settings.pins || {}; } } }, dir: { writable: true, configurable: true, value(speed) { speed = speed || this.speed(); return this; } }, resume: { value() { const speed = this.speed(); this.speed({ speed }); return this; } } }, DIRECTIONAL: { pins: { get() { if (Array.isArray(this.settings.pins)) { return { pwm: this.settings.pins[0], dir: this.settings.pins[1] }; } else { return this.settings.pins; } } }, dir: { writable: true, configurable: true, value(dir) { this.stop(); this.setPin(this.pins.dir, dir.value); this.direction = dir; process.nextTick(() => this.emit(dir.name)); return this; } } }, CDIR: { pins: { get() { if (Array.isArray(this.settings.pins)) { return { pwm: this.settings.pins[0], dir: this.settings.pins[1], cdir: this.settings.pins[2] }; } else { return this.settings.pins; } } }, dir: { value(dir) { this.stop(); this.direction = dir; this.setPin(this.pins.cdir, 1 ^ dir.value); this.setPin(this.pins.dir, dir.value); process.nextTick(() => this.emit(dir.name)); return this; } }, brake: { value(duration) { this.speed({ speed: 0, saveSpeed: false }); this.setPin(this.pins.dir, 1, 127); this.setPin(this.pins.cdir, 1, 128, 127); this.speed({ speed: 255, saveSpeed: false, braking: true }); process.nextTick(() => this.emit("brake")); if (duration) { this.board.wait(duration, () => this.stop()); } return this; } } } }; /** * Motor * @constructor * * @param {Object} options Options: pin|pins{pwm, dir[, cdir]}, device, controller, current * @param {Number} pin A single pin for basic * @param {Array} pins A two or three digit array of pins [pwm, dir]|[pwm, dir, cdir] * * * Initializing "Hobby Motors" * * new five.Motor(9); * * ...is the same as... * * new five.Motor({ * pin: 9 * }); * * * Initializing 2 pin, Bi-Directional DC Motors: * * new five.Motor([ 3, 12 ]); * * ...is the same as... * * new five.Motor({ * pins: [ 3, 12 ] * }); * * ...is the same as... * * new five.Motor({ * pins: { * pwm: 3, * dir: 12 * } * }); * * * Initializing 3 pin, I2C PCA9685 Motor Controllers: * i.e. The Adafruit Motor Shield V2 * * new five.Motor({ * pins: [ 8, 9, 10 ], * controller: "PCA9685", * address: 0x60 * }); * * * Initializing 3 pin, Bi-Directional DC Motors: * * new five.Motor([ 3, 12, 11 ]); * * ...is the same as... * * new five.Motor({ * pins: [ 3, 12, 11 ] * }); * * ...is the same as... * * new five.Motor({ * pins: { * pwm: 3, * dir: 12, * cdir: 11 * } * }); * * * Initializing Bi-Directional DC Motors with brake: * * new five.Motor({ * pins: { * pwm: 3, * dir: 12, * brake: 11 * } * }); * * * Initializing Bi-Directional DC Motors with current sensing pins: * See Sensor.js for details on options * * new five.Motor({ * pins: [3, 12], * current: { * pin: "A0", * freq: 250, * range: [0, 2000] * } * }); * * * Initializing Bi-Directional DC Motors with inverted speed for reverse: * Most likely used for non-commercial H-Bridge controllers * * new five.Motor({ * pins: [3, 12], * invertPWM: true * }); * */ class Motor extends Emitter { constructor(options) { super(); var device, controller, state; Board.Component.call( this, options = Board.Options(options) ); this.settings = options; // Derive device based on pins passed if (typeof this.settings.device === "undefined") { this.settings.device = "DIRECTIONAL"; if (typeof this.settings.pins === "undefined" && typeof this.settings.register !== "object") { this.settings.device = "NONDIRECTIONAL"; } if (this.settings.pins) { if (Array.isArray(this.settings.pins)) { this.settings.device = ["NONDIRECTIONAL", "DIRECTIONAL", "CDIR"][this.settings.pins.length - 1]; } else { if (typeof this.settings.pins.dir === "undefined") { this.settings.device = "NONDIRECTIONAL"; } else { this.settings.device = "DIRECTIONAL"; } if (typeof this.settings.pins.cdir !== "undefined") { this.settings.device = "CDIR"; } } } } if (typeof this.settings.controller === "string" && (this.settings.controller.startsWith("EVS") || this.settings.controller.startsWith("GROVE_I2C"))) { this.settings.device = "DIRECTIONAL"; } // Allow users to pass in custom device types device = typeof this.settings.device === "string" ? Devices[this.settings.device] : this.settings.device; this.threshold = typeof this.settings.threshold !== "undefined" ? this.settings.threshold : 30; this.invertPWM = typeof this.settings.invertPWM !== "undefined" ? this.settings.invertPWM : false; Object.defineProperties(this, device); if (this.settings.register) { this.settings.controller = "ShiftRegister"; } /** * Note: Controller decorates the device. Used for adding * special controllers (i.e. PCA9685) **/ if (this.settings.controller) { controller = typeof this.settings.controller === "string" ? Controllers[this.settings.controller] : this.settings.controller; Board.Controller.call(this, Controllers, options); } // current just wraps a Sensor if (this.settings.current) { this.settings.current.board = this.board; this.current = new Sensor(this.settings.current); } // Create a "state" entry for privately // storing the state of the motor state = { isOn: false, currentSpeed: typeof this.settings.speed !== "undefined" ? this.settings.speed : 128, braking: false, enabled: false }; priv.set(this, state); Object.defineProperties(this, { // Calculated, read-only motor on/off state // true|false isOn: { get: function() { return state.isOn; } }, currentSpeed: { get: function() { return state.currentSpeed; } }, braking: { get: function() { return state.braking; } }, enabled: { get: function() { return state.enabled; } } }); // We need to store and initialize the state of the dir pin(s) this.direction = { value: 1 }; if (this.initialize) { this.initialize(options); } this.validatePins(); this.enable(); this.dir(this.direction); } } Motor.prototype.initialize = function() { this.io.pinMode(this.pins.pwm, this.io.MODES.PWM); ["dir", "cdir", "brake", "enable"].forEach(pin => { if (typeof this.pins[pin] !== "undefined") { this.io.pinMode(this.pins[pin], this.io.MODES.OUTPUT); } }); }; Motor.prototype.setPin = function(pin, value) { this.io.digitalWrite(pin, value); }; Motor.prototype.setPWM = function(pin, value) { this.io.analogWrite(pin, Fn.map(value, 0, 255, 0, this.board.RESOLUTION.PWM)); }; Motor.prototype.speed = function(options) { var state = priv.get(this); if (typeof options === "undefined") { return state.currentSpeed; } else { if (typeof options === "number") { options = { speed: options }; } options.speed = Board.constrain(options.speed, 0, 255); options.saveSpeed = typeof options.saveSpeed !== "undefined" ? options.saveSpeed : true; if (options.speed < this.threshold) { options.speed = 0; } state.isOn = options.speed === 0 ? false : true; if (options.saveSpeed) { state.currentSpeed = options.speed; } if (options.braking) { state.braking = true; } if (this.invertPWM && this.direction.value === 1) { options.speed ^= 0xff; } this.setPWM(this.pins.pwm, options.speed); return this; } }; // start a motor - essentially just switch it on like a normal motor Motor.prototype.start = function(speed) { // Send a signal to turn on the motor and run at given speed in whatever // direction is currently set. if (this.pins.brake && this.braking) { this.setPin(this.pins.brake, 0); } // get current speed if nothing provided. speed = typeof speed !== "undefined" ? speed : this.speed(); this.speed({ speed: speed, braking: false }); // "start" event is fired when the motor is started if (speed > 0) { process.nextTick(() => this.emit("start")); } return this; }; Motor.prototype.stop = function() { this.speed({ speed: 0, saveSpeed: false }); process.nextTick(() => this.emit("stop")); return this; }; Motor.prototype.brake = function(duration) { if (typeof this.pins.brake === "undefined") { if (this.board.io.name !== "Mock") { console.log("Non-braking motor type"); } this.stop(); } else { this.setPin(this.pins.brake, 1); this.setPin(this.pins.dir, 1); this.speed({ speed: 255, saveSpeed: false, braking: true }); process.nextTick(() => this.emit("brake")); if (duration) { var motor = this; this.board.wait(duration, function() { motor.resume(); }); } } return this; }; Motor.prototype.release = function() { this.resume(); process.nextTick(() => this.emit("release")); return this; }; Motor.prototype.resume = function() { var speed = this.speed(); this.dir(this.direction); this.start(speed); return this; }; Motor.prototype.enable = function() { var state = priv.get(this); if (typeof this.pins.enable !== "undefined" && !state.enabled) { this.setPin(this.pins.enable, 1); state.enabled = true; } }; Motor.prototype.disable = function() { var state = priv.get(this); if (typeof this.pins.enable !== "undefined" && state.enabled) { this.setPin(this.pins.enable, 0); state.enabled = false; } }; // Check for required pins Motor.prototype.validatePins = function() { if (typeof this.pins.pwm === "undefined") { throw new Error("PWM pin must be defined"); } if (typeof this.pins.dir === "undefined" && this.settings.device !== "NONDIRECTIONAL") { throw new Error("DIR pin must be defined"); } if (this.settings.device === "CDIR" && typeof this.pins.cdir === "undefined") { throw new Error("CDIR pin must be defined for three wire motors"); } }; [ /** * forward Turn the Motor in its forward direction * fwd Turn the Motor in its forward direction * * @param {Number} 0-255, 0 is stopped, 255 is fastest * @return {Object} this */ { name: "forward", abbr: "fwd", value: 1 }, /** * reverse Turn the Motor in its reverse direction * rev Turn the Motor in its reverse direction * * @param {Number} 0-255, 0 is stopped, 255 is fastest * @return {Object} this */ { name: "reverse", abbr: "rev", value: 0 } ].forEach(dir => { Motor.prototype[dir.name] = Motor.prototype[dir.abbr] = function(speed) { this.dir(dir); this.start(speed); return this; }; }); Motor.SHIELD_CONFIGS = { ADAFRUIT_V1: { M1: { pins: { pwm: 11 }, register: { data: 8, clock: 4, latch: 12 }, bits: { a: 2, b: 3 } }, M2: { pins: { pwm: 3 }, register: { data: 8, clock: 4, latch: 12 }, bits: { a: 1, b: 4 } }, M3: { pins: { pwm: 6 }, register: { data: 8, clock: 4, latch: 12 }, bits: { a: 5, b: 7 } }, M4: { pins: { pwm: 5 }, register: { data: 8, clock: 4, latch: 12 }, bits: { a: 0, b: 6 } } }, ADAFRUIT_V2: { M1: { pins: { pwm: 8, dir: 9, cdir: 10 }, address: 0x60, controller: "PCA9685" }, M2: { pins: { pwm: 13, dir: 12, cdir: 11 }, address: 0x60, controller: "PCA9685" }, M3: { pins: { pwm: 2, dir: 3, cdir: 4 }, address: 0x60, controller: "PCA9685" }, M4: { pins: { pwm: 7, dir: 6, cdir: 5 }, address: 0x60, controller: "PCA9685" } }, SEEED_STUDIO: { A: { pins: { pwm: 9, dir: 8, cdir: 11 } }, B: { pins: { pwm: 10, dir: 12, cdir: 13 } } }, FREETRONICS_HBRIDGE: { A: { pins: { pwm: 6, dir: 4, cdir: 7 } }, B: { pins: { pwm: 5, dir: 3, cdir: 2 } } }, ARDUINO_MOTOR_SHIELD_R3_1: { A: { pins: { pwm: 3, dir: 12 } }, B: { pins: { pwm: 11, dir: 13 } } }, ARDUINO_MOTOR_SHIELD_R3_2: { A: { pins: { pwm: 3, dir: 12, brake: 9 } }, B: { pins: { pwm: 11, dir: 13, brake: 8 } } }, ARDUINO_MOTOR_SHIELD_R3_3: { A: { pins: { pwm: 3, dir: 12, brake: 9, current: "A0" } }, B: { pins: { pwm: 11, dir: 13, brake: 8, current: "A1" } } }, DF_ROBOT: { A: { pins: { pwm: 6, dir: 7 } }, B: { pins: { pwm: 5, dir: 4 } } }, NKC_ELECTRONICS_KIT: { A: { pins: { pwm: 9, dir: 12 } }, B: { pins: { pwm: 10, dir: 13 } } }, RUGGED_CIRCUITS: { A: { pins: { pwm: 3, dir: 12 } }, B: { pins: { pwm: 11, dir: 13 } } }, SPARKFUN_ARDUMOTO: { A: { pins: { pwm: 3, dir: 12 } }, B: { pins: { pwm: 11, dir: 13 } } }, POLOLU_DRV8835_SHIELD: { M1: { pins: { pwm: 9, dir: 7 } }, M2: { pins: { pwm: 10, dir: 8 } } }, POLOLU_VNH5019_SHIELD: { M1: { pins: { pwm: 9, dir: 2, cdir: 4, enable: 6 } }, M2: { pins: { pwm: 10, dir: 7, cdir: 8, enable: 12 } } }, MICRO_MAGICIAN_V2: { A: { pins: { pwm: 6, dir: 8 }, invertPWM: true }, B: { pins: { pwm: 5, dir: 7 }, invertPWM: true } }, SPARKFUN_LUDUS: { A: { pins: { pwm: 3, dir: 4, cdir: 5 } }, B: { pins: { pwm: 6, dir: 7, cdir: 8 } } }, SPARKFUN_DUAL_HBRIDGE_EDISON_BLOCK: { A: { pins: { pwm: 20, dir: 33, cdir: 46, enable: 47 } }, B: { pins: { pwm: 14, dir: 48, cdir: 36, enable: 47 } } }, PICAR_V: { A: { controller: "PCA9685_Hybrid", pins: { pwm: 4, dir: "GPIO17" } }, B: { controller: "PCA9685_Hybrid", pins: { pwm: 5, dir: "GPIO27" } } } }; /** * Motors() * new Motors() */ class Motors extends Collection { constructor(numsOrObjects) { super(numsOrObjects); } get type() { return Motor; } } /* * Motors, forward(speed)/fwd(speed) * * eg. array.forward(speed); * Motors, reverse(speed)/rev(speed) * * eg. array.reverse(speed); * Motors, start(speed) * * eg. array.start(speed); * Motors, stop() * * eg. array.stop(); * Motors, brake() * * eg. array.brake(); * Motors, release() * * eg. array.release(); */ Collection.installMethodForwarding( Motors.prototype, Motor.prototype ); Motor.Collection = Motors; /* istanbul ignore else */ if (!!process.env.IS_TEST_MODE) { Motor.Controllers = Controllers; Motor.purge = function() { priv.clear(); registers.clear(); }; } module.exports = Motor;