johnny-five
Version:
Firmata based Arduino Programming Framework.
658 lines (550 loc) • 15.6 kB
JavaScript
var events = require("events"),
util = require("util"),
colors = require("colors"),
Firmata = require("firmata").Board,
_ = require("lodash"),
__ = require("../lib/fn.js"),
Repl = require("../lib/repl.js"),
serialport = require("serialport"),
board,
boards,
rport,
Serial;
boards = [];
rport = /usb|acm|com/i;
/**
* 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
*
*/
Serial = {
used: [],
detect: function( callback ) {
this.info( "Board", "Connecting..." );
// If a |port| was explicitly provided to the Board constructor,
// invoke the detection callback and return immediately
if ( this.port ) {
callback.call( this, this.port );
return;
}
serialport.list(function(err, result) {
var 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 when scanning /dev/, then there is
// nothing left to do and we can safely exit the program
if ( !length ) {
// Alert user that no devices were detected
this.error( "Board", "No USB devices detected" );
// Exit the program by sending SIGABRT
process.exit(3);
// Return (not that it matters, but this is a good way
// to indicate to readers of the code that nothing else
// will happen in this function)
return;
}
// Continue with connection routine attempts
this.info(
"Serial",
"Found possible serial port" + ( length > 1 ? "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( usb, callback ) {
var err, found, connected, eventType;
// 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( usb );
try {
found = new Firmata( usb, function( error ) {
if ( error !== undefined ) {
err = error;
}
// Execute "ready" callback
callback.call( this, err, "ready", found );
}.bind(this));
// Made this far, safely connected
connected = 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(...)
eventType = connected ? "connected" : "error";
// Execute "connected" callback
callback.call( this, err, eventType, found );
}
};
// Board constructor: Firmata Board wrapper
function Board( opts ) {
if ( !(this instanceof Board) ) {
return new Board( opts );
}
// Ensure opts is an object
opts = opts || {};
var inject = {};
// Initialize this Board instance with
// param specified properties.
_.assign( this, opts );
// Easily track state of hardware
this.ready = false;
// Initialize instance property to reference firmata board
this.firmata = null;
// Identify for connected hardware cache
if ( !this.id ) {
this.id = __.uid();
}
// If no debug flag, default to false
// TODO: Remove override
this.debug = true;
if ( !("debug" in this) ) {
this.debug = false;
}
// Create a Repl instance and store as
// instance property of this firmata/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 ( 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 );
}
// Used for testing only
if ( this.mock ) {
this.firmata = new Firmata( this.mock, function() {
// Execute "connected" and "ready" callbacks
this.emit( "connected", null );
this.emit( "ready", null );
this.ready = true;
}.bind(this));
} else {
Serial.detect.call( this, function( port ) {
Serial.connect.call( this, port, function( err, type, board ) {
if ( err ) {
this.error( "Board", err );
} else {
// Assign found board to instance
this.firmata = board;
this.info(
"Board " + ( type === "connected" ? "->" : "<-" ) + " Serialport",
type,
port.grey
);
}
if ( type === "connected" ) {
process.on( "SIGINT", function() {
this.warn( "Board", "Closing: firmata, serialport" );
// On ^c, make sure we close the process after the
// board and serialport are closed. Approx 100ms
// TODO: this sucks, need better solution
setTimeout(function() {
process.exit();
}, 100);
}.bind(this));
}
if ( type === "ready" ) {
// Update instance `ready` flag
this.ready = true;
this.port = port;
// Trigger the repl to start
process.stdin.emit( "data", 1 );
}
// emit connect|ready event
this.emit( type, err );
});
});
}
// Cache instance to allow access from module constructors
boards.push( this );
}
// Inherit event api
util.inherits( Board, events.EventEmitter );
/**
* pinMode, analogWrite, analogRead, digitalWrite, digitalRead
*
* Pass through methods
*/
[ "pinMode", "analogWrite", "analogRead", "digitalWrite", "digitalRead" ].forEach(function( method ) {
Board.prototype[ method ] = function( pin, arg ) {
this.firmata[ method ]( pin, arg );
};
});
/**
* shiftOut
*
*/
Board.prototype.shiftOut = function( dataPin, clockPin, isBigEndian, value ) {
var mask, write;
write = function( value, mask ) {
this.digitalWrite( clockPin, this.firmata.LOW );
this.digitalWrite(
dataPin, this.firmata[ value & mask ? "HIGH" : "LOW" ]
);
this.digitalWrite( clockPin, this.firmata.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 );
};
});
// 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 compulsive API
Board.prototype.wait = function( time, callback ) {
return setTimeout( callback.bind(this), time );
};
Board.prototype.loop = function( time, callback ) {
return setInterval( callback.bind(this), time );
};
// ----------
// 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.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.options = function( arg ) {
var pin,
isArray = Array.isArray(arg),
opts = {};
if ( typeof arg === "number" ||
typeof arg === "string" ||
isArray ) {
// Arrays are on a "pins" property
// String/Numbers are on a "pin" property
opts[ isArray ? "pins" : "pin" ] = arg;
} else {
opts = arg;
}
// Auto-normalize pin values, this reduces boilerplate code
// inside module constructors
if ( opts.pin || opts.pins && opts.pins.length ) {
// When an array of pins is present, attempt to
// normalize them if necessary
if ( opts.pins ) {
opts.pins = opts.pins.map(function( pin ) {
return Board.Pin.normalize(
Board.Pin.translate( pin )
);
});
} else {
opts.pin = Board.Pin.normalize(
Board.Pin.translate( opts.pin )
);
}
}
return opts;
};
/**
* 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 ) {
opts = Board.options( opts );
// Hardware instance properties
this.board = Board.mount( opts );
this.firmata = this.board.firmata;
this.pin = opts.pin || 0;
};
Board.Pin = {
// Require a pin to be a digital PWM pin
isPWM: function( pin ) {
return Board.Pins.pwm[ pin ];
},
isAnalog: function( pin ) {
return !!Board.Pins.analog[ pin ];
},
isDigital: function( pin ) {
return Board.Pins.digital[ pin ];
},
isSerial: function( pin ) {
return Board.Pins.serial[ pin ];
},
translate: function( pin ) {
return Object.keys( Board.Pins.translations ).reduce(function( pin, map ) {
var p = Board.Pins.translations[ map ][ pin ];
return ( p != null && p ) || pin;
}, pin );
},
normalize: function( pin ) {
var p = Board.Pins.analog[ pin ];
return p != null ? p : pin;
}
};
// TODO: Make this this all private?, delegate to
// Board.Pin.is___()
// TODO: Improve/Overhaul all docs
Board.Pins = {
analog: {
A0: 0, //14,
A1: 1, //15,
A2: 2, //16,
A3: 3, //17,
A4: 4, //18,
A5: 5 //19
// A0: 14,
// A1: 15,
// A2: 16,
// A3: 17,
// A4: 18,
// A5: 19
},
// Used for HIGH/LOW input / output
digital: {},
// PWM (8-bit) output with the analogWrite() function.
// [SecretsOfArduinoPWM](http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM)
// Used for Servo (or any PWM hardware)
pwm: {},
// Used to receive / transmit TTL serial data
serial: {
// RX, Receive
0: true,
// TX, Transmit
1: true
},
interrupt: {
2: true,
3: true
},
// These pins support SPI communication using the SPI library.
spi: {
10: true,
11: true,
12: true,
13: true
},
// There is a built-in LED connected to digital pin 13.
// When the pin is HIGH value, the LED is on, when the pin is LOW, it's off.
led: {
13: true
},
// Special kit-centric pin translations
translations: {
// TinkerKit
tinker: {
I0: "A0",
I1: "A1",
I2: "A2",
I3: "A3",
I4: "A4",
I5: "A5",
O0: 11,
O1: 10,
O2: 9,
O3: 6,
O4: 5,
O5: 3,
D13: 13,
D12: 12,
D8: 8,
D7: 7,
D4: 4,
D2: 2
}
}
};
// Add all Digital pins
Board.range( 0, 13 ).forEach(function( val ) {
Board.Pins.digital[ val ] = true;
});
// Add all PWM pins
[ 3, 5, 6, 9, 10, 11, 12, 13 ].forEach(function( val ) {
Board.Pins.pwm[ val ] = true;
});
// 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 );
};
module.exports = Board;
// References:
// http://arduino.cc/en/Main/arduinoBoardUno