johnny-five
Version:
The JavaScript Arduino Programming Framework.
836 lines (676 loc) • 19.1 kB
JavaScript
var IS_TEST_MODE = global.IS_TEST_MODE || false;
var Emitter = require("events").EventEmitter;
var util = require("util");
var os = require("os");
var fs = require("fs");
var colors = require("colors");
var _ = require("lodash");
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;
var port = "";
// 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.contains("yocto") ||
release.contains("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/mock-serial") :
require("serialport");
// Request a list of available ports, from
// the result set, filter for valid paths
// via known path pattern match.
serialport.list(function(err, result) {
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.indexOf(val.comName) > -1) {
available = false;
}
return available;
}).map(function(val) {
return val.comName;
});
length = ports.length;
// If no ports are detected...
if (!length) {
// Create an attempt counter
if (!Serial.attempts[Serial.used.length]) {
Serial.attempts[Serial.used.length] = 0;
// Log notification...
this.info("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)",
ports.toString().grey
);
// 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 = require("firmata").Board;
var err, io, isConnected, path, type;
if (typeof portOrPath === "object" && portOrPath.path) {
//
// Board({ port: SerialPort Object })
//
path = portOrPath.path;
this.info(
"SerialPort",
path.grey
);
} 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, "ready", io);
}.bind(this));
// Extend io instance with special expandos used
// by Johny-Five for the IO Plugin system.
io.name = "Firmata";
io.transport = "Serialport";
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 || {};
var inject, isPostponed;
inject = {};
// 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 devices by pin address
this.register = [];
// Identify for connect hardware cache
if (!this.id) {
this.id = __.uid();
}
// If no debug flag, default to true
if (!("debug" in this)) {
this.debug = true;
}
if (!("repl" in this)) {
this.repl = true;
}
// Specially processed pin capabilities object
// assigned when board is initialized and ready
this.pins = null;
// Human readable name (if one can be detected)
this.type = "";
// 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) {
inject[this.id] = this;
Repl.ref.on("ready", function() {
Repl.ref.inject(inject);
});
this.repl = Repl.ref;
} else {
inject[this.id] = inject.board = this;
this.repl = new Repl(inject);
}
}
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 || "unknown transport";
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)", (this.io.name || "unknown").grey
);
isPostponed = false;
["connect", "ready"].forEach(function(type) {
if (this.io.isReady) {
broadcast.call(this, null, type, this.io);
} else {
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 {
broadcast.call(this, null, type, this.io);
}
if (type === "connect" && isPostponed) {
broadcast.call(this, null, "ready", this.io);
}
}.bind(this));
}
}, this);
}
// Cache instance to allow access from module constructors
boards.push(this);
}
function broadcast(err, type, io) {
if (err) {
this.error("Board", err);
} else {
// Assign found io to instance
if (!this.io) {
this.io = io;
}
}
if (type === "connect") {
this.isConnected = true;
// 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.
this.timer = setTimeout(function() {
this.error(
"Device or Firmware Error",
"A timeout occurred while connecting to the Board. \n" +
"Please check that you've properly flashed the board with the correct firmware."
);
process.exit(15);
}.bind(this), 1e5);
}
if (type === "ready") {
clearTimeout(this.timer);
// Update instance `ready` flag
this.isReady = true;
this.port = io.port || io.name;
this.pins = Board.Pins(this);
this.info(
"Connected",
this.port.grey
);
// In multi-board mode, block the REPL from
// activation. This will be started directly
// by the Board.Array constructor.
if (!Repl.isBlocked) {
process.stdin.emit("data", "1");
}
if (io.name !== "Mock") {
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));
}
// process.on("SIGINT", function() {
// console.log( "exit...." );
// // On ^c, make sure we close the process after the
// // io and serialport are closed. Approx 100ms
// // TODO: this sucks, need better solution
// setTimeout(function() {
// process.exit();
// }, 100);
// }.bind(this));
// emit connect|ready event
this.emit(type, err);
}
// Inherit event api
util.inherits(Board, Emitter);
/**
* pinMode, analogWrite, analogRead, digitalWrite, digitalRead
*
* Pass through methods
*/
[
"pinMode",
"analogWrite", "analogRead",
"digitalWrite", "digitalRead"
].forEach(function(method) {
Board.prototype[method] = function(pin, arg) {
this.io[method](pin, arg);
return this;
};
});
Board.prototype.serialize = function(filter) {
var blacklist, special;
blacklist = this.serialize.blacklist;
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.indexOf(prop) === -1 &&
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);
}
}
};
Board.prototype.log = function( /* type, module, message [, long description] */ ) {
var args = [].slice.call(arguments),
type = args.shift(),
module = args.shift(),
message = args.shift(),
color = Board.prototype.log.types[type];
if (this.debug) {
console.log([
// Timestamp
String(+new Date()).grey,
// Module, color matches type of log
module.magenta,
// Message
message[color],
// Miscellaneous args
args.join(", ")
].join(" "));
}
};
Board.prototype.log.types = {
error: "red",
fail: "orange",
warn: "yellow",
info: "cyan"
};
// Make shortcuts to all logging methods
Object.keys(Board.prototype.log.types).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.Device
*
* Initialize a new device instance
*
* Board.Device is a |this| senstive constructor,
* and must be called as:
*
* Board.Device.call( this, opts );
*
*
*
* TODO: Migrate all constructors to use this
* to avoid boilerplate
*/
Board.Device = function(opts) {
// Board specific properties
this.board = Board.mount(opts);
this.io = this.board.io;
// Device/Module instance properties
this.id = opts.id || null;
// Pin or Pins address(es)
opts = Board.Pins.normalize(opts, this.board);
if (typeof opts.pins !== "undefined") {
this.pins = opts.pins || [];
}
if (typeof opts.pin !== "undefined") {
this.pin = opts.pin || 0;
}
this.board.register.push(this);
};
/**
* 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
*/
Board.Array = function(ports) {
if (!(this instanceof Board.Array)) {
return new Board.Array(ports);
}
if (!Array.isArray(ports)) {
throw new Error("Expected ports to be an array");
}
Array.call(this, ports.length);
this.length = 0;
var initialized, count;
initialized = {};
count = ports.length;
// Block initialization of the program's
// REPL until all boards are ready.
Repl.isBlocked = true;
ports.forEach(function(port, k) {
var opts;
if (typeof port === "string") {
opts = {
id: port
};
} else {
opts = port;
}
this[k] = initialized[opts.id] = new Board(opts);
this[k].on("ready", function() {
this[k].info("Board ID: ", opts.id.green);
if (!--count) {
Repl.isBlocked = false;
process.stdin.emit("data", "1");
this.emit("ready", initialized);
}
}.bind(this));
this.length++;
}, this);
};
util.inherits(Board.Array, Emitter);
Board.Array.prototype.each = Array.prototype.forEach;
if (IS_TEST_MODE) {
Board.__spy = {
Serial: Serial
};
}
module.exports = Board;
// References:
// http://arduino.cc/en/Main/arduinoBoardUno