UNPKG

johnny-five-electron

Version:

Temporary fork to support Electron (to be deprecated)

1,163 lines (953 loc) 28 kB
require("es6-shim"); require("array-includes").shim(); var IS_TEST_MODE = !!process.env.IS_TEST_MODE; var Emitter = require("events").EventEmitter; var util = require("util"); // var os = require("os"); var chalk = require("chalk"); var _ = require("lodash"); var Collection = require("../lib/mixins/collection"); var __ = require("../lib/fn.js"); var Repl = require("../lib/repl.js"); var Options = require("../lib/board.options.js"); var Pins = require("../lib/board.pins.js"); //var temporal = require("temporal"); //var IO; // Environment Setup var boards = []; var rport = /usb|acm|^com/i; // TODO: // // At some point we should figure out a way to // make execution-on-board environments uniformally // detected and reported. // // var isGalileo = (function() { // var release = os.release(); // return release.includes("yocto") || // release.includes("edison"); // })(); // var isOnBoard = isGalileo; // if (isOnBoard) { // if (isGalileo) { // IO = require("galileo-io"); // } // } /** * Process Codes * SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process * SIGINT 2 Term Interrupt from keyboard * SIGQUIT 3 Core Quit from keyboard * SIGILL 4 Core Illegal Instruction * SIGABRT 6 Core Abort signal from abort(3) * SIGFPE 8 Core Floating point exception * SIGKILL 9 Term Kill signal * SIGSEGV 11 Core Invalid memory reference * SIGPIPE 13 Term Broken pipe: write to pipe with no readers * SIGALRM 14 Term Timer signal from alarm(2) * SIGTERM 15 Term Termination signal * * * * http://www.slac.stanford.edu/BFROOT/www/Computing/Environment/Tools/Batch/exitcode.html * */ var Serial = { used: [], attempts: [], detect: function(callback) { var serialport = IS_TEST_MODE ? require("../test/util/mock-serial") : require("serialport-electron"); // Request a list of available ports, from // the result set, filter for valid paths // via known path pattern match. serialport.list(function(err, result) { // serialport.list() will never result in an error. // On failure, an empty array is returned. (#768) var ports, length; ports = result.filter(function(val) { var available = true; // Match only ports that Arduino cares about // ttyUSB#, cu.usbmodem#, COM# if (!rport.test(val.comName)) { available = false; } // Don't allow already used/encountered usb device paths if (Serial.used.includes(val.comName)) { available = false; } return available; }).map(function(val) { return val.comName; }); length = ports.length; // If no ports are detected... if (!length) { if (IS_TEST_MODE && this.abort) { return; } // Create an attempt counter if (!Serial.attempts[Serial.used.length]) { Serial.attempts[Serial.used.length] = 0; // Log notification... this.info("Board", "Looking for connected device"); } // Set the attempt number Serial.attempts[Serial.used.length]++; // Retry Serial connection Serial.detect.call(this, callback); return; } this.info( "Device(s)", chalk.grey(ports) ); // Get the first available device path from the list of // detected ports callback.call(this, ports[0]); }.bind(this)); }, connect: function(portOrPath, callback) { var IO = IS_TEST_MODE ? require("../test/util/mock-firmata") : require("firmata-electron").Board; var err, io, isConnected, path, type; if (typeof portOrPath === "object" && portOrPath.path) { // // Board({ port: SerialPort Object }) // path = portOrPath.path; this.info( (portOrPath.transport || "SerialPort"), chalk.grey(path) ); } else { // // Board({ port: path String }) // // Board() // ie. auto-detected // path = portOrPath; } // Add the usb device path to the list of device paths that // are currently in use - this is used by the filter function // above to remove any device paths that we've already encountered // or used to avoid blindly attempting to reconnect on them. Serial.used.push(path); try { io = new IO(portOrPath, function(error) { if (error !== undefined) { err = error; } callback.call(this, err, err ? "error" : "ready", io); }.bind(this)); // Extend io instance with special expandos used // by Johny-Five for the IO Plugin system. io.name = "Firmata"; io.defaultLed = 13; io.port = path; // Made this far, safely connected isConnected = true; } catch (error) { err = error; } if (err) { err = err.message || err; } // Determine the type of event that will be passed on to // the board emitter in the callback passed to Serial.detect(...) type = isConnected ? "connect" : "error"; // Execute "connect" callback callback.call(this, err, type, io); } }; /** * Board * @constructor * * @param {Object} opts */ function Board(opts) { if (!(this instanceof Board)) { return new Board(opts); } // Ensure opts is an object opts = opts || {}; // Used to define the board instance's own // properties in the REPL's scope. var replContext = {}; // It's feasible that an IO-Plugin may emit // "connect" and "ready" events out of order. // This is used to enforce the order, by // postponing the "ready" event if the IO-Plugin // hasn't emitted a "connect" event. Once // the "connect" event is emitted, the // postponement is lifted and the board may // proceed with emitting the events in the // correct order. var isPostponed = false; // Initialize this Board instance with // param specified properties. _.assign(this, opts); this.timer = null; this.isConnected = false; // Easily track state of hardware this.isReady = false; // Initialize instance property to reference io board this.io = this.io || null; // Registry of components this.register = []; // Pins, Addr (alt Pin name), Addresses this.occupied = []; // Registry of drivers by address (i.e. I2C Controllers) this.Drivers = {}; // Identify for connect hardware cache if (!this.id) { this.id = __.uid(); } // If no debug flag, default to true if (typeof this.debug === "undefined") { this.debug = true; } // If no repl flag, default to true if (typeof this.repl === "undefined") { this.repl = true; } // If no sigint flag, default to true if (typeof this.sigint === "undefined") { this.sigint = true; } // Specially processed pin capabilities object // assigned when physical board has reported // "ready" via Firmata or IO-Plugin. this.pins = null; // Create a Repl instance and store as // instance property of this io/board. // This will reduce the amount of boilerplate // code required to _always_ have a Repl // session available. // // If a sesssion exists, use it // (instead of creating a new session) // if (this.repl) { if (Repl.ref) { replContext[this.id] = this; Repl.ref.on("ready", function() { Repl.ref.inject(replContext); }); this.repl = Repl.ref; } else { replContext[this.id] = replContext.board = this; this.repl = new Repl(replContext); } } if (opts.io) { // If you already have a connected io instance this.io = opts.io; this.isReady = opts.io.isReady; this.transport = this.io.transport || null; this.port = this.io.name; this.pins = Board.Pins(this); } else { // if (isOnBoard) { // this.io = new IO(); // this.port = this.io.name; // } else { if (this.port && opts.port) { Serial.connect.call(this, this.port, broadcast); } else { // TODO: refactor to do the path lookups // as soon as this file is required. Serial.detect.call(this, function(path) { Serial.connect.call(this, path, broadcast); }); } // } } // Either an IO instance was provided or isOnBoard is true if (!opts.port && this.io !== null) { this.info( "Device(s)", chalk.grey(this.io.name || "unknown") ); ["connect", "ready"].forEach(function(type) { this.io.once(type, function() { // Since connection and readiness happen asynchronously, // it's actually possible for Johnny-Five to receive the // events out of order and that should be ok. if (type === "ready" && !this.isConnected) { isPostponed = true; } else { // Will emit the "connect" and "ready" events // if received in order. If out of order, this // will only emit the "connect" event. The // "ready" event will be handled in the next // condition's consequent. broadcast.call(this, null, type, this.io); } if (type === "connect" && isPostponed) { broadcast.call(this, null, "ready", this.io); } }.bind(this)); if (this.io.isReady) { // If the IO instance is reached "ready" // state, queue tick tasks to emit the // "connect" and "ready" events process.nextTick(function() { this.io.emit(type); }.bind(this)); } }, this); // Bubble "string" events from IO layer this.io.on("string", function(data) { this.emit("string", data); }.bind(this)); } // Cache instance to allow access from module constructors boards.push(this); } function broadcast(err, type, io) { // Assign found io to instance if (!this.io) { this.io = io; } if (type === "error") { if (err && err.message) { console.log(err.message.red); } } if (type === "connect") { this.isConnected = true; this.port = io.port || io.name; this.info( "Connected", chalk.grey(this.port) ); // 10 Second timeout... // // If "ready" hasn't fired and cleared the timer within // 10 seconds of the connect event, then it's likely // there is an issue with the device or firmware. if (!IS_TEST_MODE) { this.timer = setTimeout(function() { this.error( "Device or Firmware Error", "A timeout occurred while connecting to the Board. \n\n" + "Please check that you've properly flashed the board with the correct firmware.\n" + "See: https://github.com/rwaldron/johnny-five/wiki/Getting-Started#trouble-shooting" ); this.emit("error", new Error("A timeout occurred while connecting to the Board.")); }.bind(this), 1e4); } } if (type === "ready") { if (this.timer) { clearTimeout(this.timer); } // Update instance `ready` flag this.isReady = true; this.pins = Board.Pins(this); this.MODES = this.io.MODES; // In multi-board mode, block the REPL from // activation. This will be started directly // by the Board.Array constructor. // // In single-board mode, the REPL will not // be blocked at all. // // If the user program has not disabled the // REPL, initialize it. if (this.repl) { this.repl.initialize(this.emit.bind(this, "ready")); } if (io.name !== "Mock" && this.sigint) { process.on("SIGINT", function() { this.warn("Board", "Closing."); process.exit(0); }.bind(this)); } // Bubble "string" events from IO layer io.on("string", function(data) { this.emit("string", data); }.bind(this)); } // If there is a REPL... if (this.repl) { // "ready" will be emitted once repl.initialize // is complete, so the only event that needs to // be propagated here is the "connect" event. if (type === "connect") { this.emit(type, err); } } else { // The REPL is disabled, propagate all events this.emit(type, err); } } // Inherit event api util.inherits(Board, Emitter); /** * Pass through methods */ [ "digitalWrite", "analogWrite", "servoWrite", "sendI2CWriteRequest", "analogRead", "digitalRead", "sendI2CReadRequest", "pinMode", "queryPinState", "sendI2CConfig", "stepperStep", "stepperConfig", "servoConfig", "i2cConfig", "i2cWrite", "i2cWriteReg", "i2cRead", "i2cReadOnce", ].forEach(function(method) { Board.prototype[method] = function() { this.io[method].apply(this.io, arguments); return this; }; }); Board.prototype.serialize = function(filter) { var blacklist = this.serialize.blacklist; var special = this.serialize.special; return JSON.stringify( this.register.map(function(device) { return Object.getOwnPropertyNames(device).reduce(function(data, prop) { var value = device[prop]; if (!blacklist.includes(prop) && typeof value !== "function") { data[prop] = special[prop] ? special[prop](value) : value; if (filter) { data[prop] = filter(prop, data[prop], device); } } return data; }, {}); }, this) ); }; Board.prototype.serialize.blacklist = [ "board", "io", "_events" ]; Board.prototype.samplingInterval = function(ms) { if (this.io.setSamplingInterval) { this.io.setSamplingInterval(ms); } else { console.log("This IO plugin does not implement an interval adjustment method"); } return this; }; Board.prototype.serialize.special = { mode: function(value) { return ["INPUT", "OUTPUT", "ANALOG", "PWM", "SERVO"][value] || "unknown"; } }; /** * shiftOut * */ Board.prototype.shiftOut = function(dataPin, clockPin, isBigEndian, value) { var mask, write; write = function(value, mask) { this.digitalWrite(clockPin, this.io.LOW); this.digitalWrite( dataPin, this.io[value & mask ? "HIGH" : "LOW"] ); this.digitalWrite(clockPin, this.io.HIGH); }.bind(this); if (arguments.length === 3) { value = arguments[2]; isBigEndian = true; } if (isBigEndian) { for (mask = 128; mask > 0; mask = mask >> 1) { write(value, mask); } } else { for (mask = 0; mask < 128; mask = mask << 1) { write(value, mask); } } }; var logging = { specials: [ "error", "fail", "warn", "info", ], colors: { log: "white", error: "red", fail: "inverse", warn: "yellow", info: "cyan" } }; Board.prototype.log = function( /* type, klass, message [, long description] */ ) { var args = [].slice.call(arguments); // If this was a direct call to `log(...)`, make sure // there is a correct "type" to emit below. if (!logging.specials.includes(args[0])) { args.unshift("log"); } var type = args.shift(); var klass = args.shift(); var message = args.shift(); var color = logging.colors[type]; var now = Date.now(); var event = { type: type, timestamp: now, class: klass, message: "", data: null, }; if (typeof args[args.length - 1] === "object") { event.data = args.pop(); } message += " " + args.join(", "); event.message = message.trim(); if (this.debug) { console.log([ // Timestamp chalk.grey(now), // Module, color matches type of log chalk.magenta(klass), // Details chalk[color](message), // Miscellaneous args args.join(", ") ].join(" ")); } this.emit(type, event); this.emit("message", event); }; // Make shortcuts to all logging methods logging.specials.forEach(function(type) { Board.prototype[type] = function() { var args = [].slice.call(arguments); args.unshift(type); this.log.apply(this, args); }; }); /** * delay, loop, queue * * Pass through methods to temporal */ /* [ "delay", "loop", "queue" ].forEach(function( method ) { Board.prototype[ method ] = function( time, callback ) { temporal[ method ]( time, callback ); return this; }; }); // Alias wait to delay to match existing Johnny-five API Board.prototype.wait = Board.prototype.delay; */ // -----THIS IS A TEMPORARY FIX UNTIL THE ISSUES WITH TEMPORAL ARE RESOLVED----- // Aliasing. // (temporary, while ironing out API details) // The idea is to match existing hardware programming apis // or simply find the words that are most intuitive. // Eventually, there should be a queuing process // for all new callbacks added // // TODO: Repalce with temporal or compulsive API Board.prototype.wait = function(time, callback) { setTimeout(callback.bind(this), time); return this; }; Board.prototype.loop = function(time, callback) { setInterval(callback.bind(this), time); return this; }; // ---------- // Static API // ---------- // Board.map( val, fromLow, fromHigh, toLow, toHigh ) // // Re-maps a number from one range to another. // Based on arduino map() Board.map = __.map; Board.fmap = __.fmap; // Board.constrain( val, lower, upper ) // // Constrains a number to be within a range. // Based on arduino constrain() Board.constrain = __.constrain; // Board.range( upper ) // Board.range( lower, upper ) // Board.range( lower, upper, tick ) // // Returns a new array range // Board.range = __.range; // Board.range.prefixed( prefix, upper ) // Board.range.prefixed( prefix, lower, upper ) // Board.range.prefixed( prefix, lower, upper, tick ) // // Returns a new array range, each value prefixed // Board.range.prefixed = __.range.prefixed; // Board.uid() // // Returns a reasonably unique id string // Board.uid = __.uid; // Board.mount() // Board.mount( index ) // Board.mount( object ) // // Return hardware instance, based on type of param: // @param {arg} // object, user specified // number/index, specified in cache // none, defaults to first in cache // // Notes: // Used to reduce the amount of boilerplate // code required in any given module or program, by // giving the developer the option of omitting an // explicit Board reference in a module // constructor's options Board.mount = function(arg) { var index = typeof arg === "number" && arg, hardware; // board was explicitly provided if (arg && arg.board) { return arg.board; } // index specified, attempt to return // hardware instance. Return null if not // found or not available if (index) { hardware = boards[index]; return hardware && hardware || null; } // If no arg specified and hardware instances // exist in the cache if (boards.length) { return boards[0]; } // No mountable hardware return null; }; /** * Board.Component * * Initialize a new device instance * * Board.Component is a |this| sensitive constructor, * and must be called as: * * Board.Component.call( this, opts ); * * * * TODO: Migrate all constructors to use this * to avoid boilerplate */ Board.Component = function(opts, componentOpts) { if (typeof opts === "undefined") { opts = {}; } if (typeof componentOpts === "undefined") { componentOpts = {}; } // Board specific properties this.board = Board.mount(opts); this.io = this.board.io; // Component/Module instance properties this.id = opts.id || Board.uid(); var originalPins; if (typeof opts.pin === "number" || typeof opts.pin === "string") { originalPins = [opts.pin]; } else { if (Array.isArray(opts.pins)) { originalPins = opts.pins.slice(); } else { if (typeof opts.pins === "object" && opts.pins !== null) { var pinset = opts.pins || opts.pin; originalPins = []; for (var p in pinset) { originalPins.push(pinset[p]); } } } } componentOpts = Board.Component.initialization(componentOpts); if (componentOpts.normalizePin) { opts = Board.Pins.normalize(opts, this.board); } var requesting = []; if (typeof opts.pins !== "undefined") { this.pins = opts.pins || []; if (Array.isArray(this.pins)) { requesting = requesting.concat( this.pins.map(function(pin) { return { value: pin, type: "pin" }; }) ); } else { requesting = requesting.concat( Object.keys(this.pins).map(function(key) { return { value: this.pins[key], type: "pin" }; }, this) ); } } if (typeof opts.pin !== "undefined") { this.pin = opts.pin; requesting.push({ value: this.pin, type: "pin" }); } if (typeof opts.emitter !== "undefined") { this.emitter = opts.emitter; requesting.push({ value: this.emitter, type: "emitter" }); } // TODO: Kill this. if (typeof opts.addr !== "undefined") { this.addr = opts.addr; requesting.push({ value: this.addr, type: "addr" }); } if (typeof opts.address !== "undefined" && requesting.length) { this.address = opts.address; requesting.forEach(function(request) { request.address = this.address; }, this); } if (typeof opts.controller !== "undefined" && requesting.length) { this.controller = opts.controller; requesting.forEach(function(request) { request.controller = this.controller; }, this); } if (componentOpts.requestPin) { // With the pins being requested for use by this component, // compare with the list of pins that are already known to be // in use by other components. If any are known to be in use, // produce a warning for the user. requesting.forEach(function(request, index) { var hasController = typeof request.controller !== "undefined"; var hasAddress = typeof request.address !== "undefined"; var isOccupied = false; var message = ""; request.value = originalPins[index]; if (this.board.occupied.length) { isOccupied = this.board.occupied.some(function(occupied) { var isPinOccupied = request.value === occupied.value && request.type === occupied.type; if (typeof occupied.controller !== "undefined") { if (hasController) { return isPinOccupied && (request.controller === occupied.controller); } return false; } if (typeof occupied.address !== "undefined") { if (hasAddress) { return isPinOccupied && (request.address === occupied.address); } return false; } return isPinOccupied; }); } if (isOccupied) { message = request.type + ": " + request.value; if (hasController) { message += ", controller: " + request.controller; } if (hasAddress) { message += ", address: " + request.address; } this.board.warn("Component", message + " is already in use"); } else { this.board.occupied.push(request); } }, this); } this.board.register.push(this); }; Board.Component.initialization = function(opts) { var defaults = { requestPin: true, normalizePin: true }; return Object.assign({}, defaults, opts); }; Board.Device = function(opts) { Board.Component.call(this, opts); }; /** * Pin Capability Signature Mapping */ Board.Pins = Pins; Board.Options = Options; // Define a user-safe, unwritable hardware cache access Object.defineProperty(Board, "cache", { get: function() { return boards; } }); /** * Board event constructor. * opts: * type - event type. eg: "read", "change", "up" etc. * target - the instance for which the event fired. * 0..* other properties */ Board.Event = function(opts) { if (!(this instanceof Board.Event)) { return new Board.Event(opts); } opts = opts || {}; // default event is read this.type = opts.type || "read"; // actual target instance this.target = opts.target || null; // Initialize this Board instance with // param specified properties. _.assign(this, opts); }; /** * Boards or Board.Array; Used when the program must connect to * more then one board. * * @memberof Board * * @param {Array} ports List of port objects { id: ..., port: ... } * List of id strings (initialized in order) * * @return {Boards} board object references */ function Boards(opts) { if (!(this instanceof Boards)) { return new Boards(opts); } var ports; // new Boards([ ...Array of board opts ]) if (Array.isArray(opts)) { ports = opts.slice(); opts = { ports: ports, }; } // new Boards({ ports: [ ...Array of board opts ], .... }) if (!Array.isArray(opts) && typeof opts === "object" && opts.ports !== undefined) { ports = opts.ports; } // new Boards(non-Array?) // new Boards({ ports: non-Array? }) if (!Array.isArray(ports)) { throw new Error("Expected ports to be an array"); } if (typeof opts.debug === "undefined") { opts.debug = true; } if (typeof opts.repl === "undefined") { opts.repl = true; } var initialized = {}; var noRepl = ports.some(function(port) { return port.repl === false; }); var noDebug = ports.some(function(port) { return port.debug === false; }); this.length = ports.length; this.debug = opts.debug; this.repl = opts.repl; // If any of the port definitions have // explicitly shut off debug output, bubble up // to the Boards instance if (noDebug) { this.debug = false; } // If any of the port definitions have // explicitly shut off the repl, bubble up // to the Boards instance if (noRepl) { this.repl = false; } var expecteds = ports.map(function(port, index) { var portOpts; if (typeof port === "string") { portOpts = { id: port }; } else { portOpts = port; } // Shut off per-board repl instance creation portOpts.repl = false; this[index] = initialized[portOpts.id] = new Board(portOpts); // "error" event is not async, register immediately this[index].on("error", function(error) { this.emit("error", error); }.bind(this)); return new Promise(function(resolve) { this[index].on("ready", function() { resolve(initialized[portOpts.id]); }); }.bind(this)); }, this); Promise.all(expecteds).then(function(boards) { Object.assign(this, boards); this.each(function(board) { board.info("Board ID: ", chalk.green(board.id)); }); // If the Boards instance requires a REPL, // make sure it's created before calling "ready" if (this.repl) { this.repl = new Repl( Object.assign({}, initialized, { board: this }) ); this.repl.initialize(function() { this.emit("ready", initialized); }.bind(this)); } else { // Otherwise, call ready immediately this.emit("ready", initialized); } }.bind(this)); } util.inherits(Boards, Emitter); Object.assign(Boards.prototype, Collection.prototype); Boards.prototype.log = Board.prototype.log; logging.specials.forEach(function(type) { Boards.prototype[type] = function() { var args = [].slice.call(arguments); args.unshift(type); this.log.apply(this, args); }; }); if (IS_TEST_MODE) { Board.__spy = { Serial: Serial }; Board.purge = function() { Board.Pins.normalize.clear(); Repl.isActive = false; Repl.isBlocked = true; Repl.ref = null; boards.length = 0; }; } Board.Array = Boards; module.exports = Board; // References: // http://arduino.cc/en/Main/arduinoBoardUno