ledstripe-fork
Version:
control WS2801 and LPD8806 LED stripes via SPI
872 lines (733 loc) • 29 kB
JavaScript
var global = Function("return this;")();
/*!
* Ender: open module JavaScript framework (client-lib)
* copyright Dustin Diaz & Jacob Thornton 2011 (@ded @fat)
* http://ender.no.de
* License MIT
*/
!function (context) {
// a global object for node.js module compatiblity
// ============================================
context['global'] = context
// Implements simple module system
// losely based on CommonJS Modules spec v1.1.1
// ============================================
var modules = {}
, old = context.$
function require (identifier) {
// modules can be required from ender's build system, or found on the window
var module = modules[identifier] || window[identifier]
if (!module) throw new Error("Requested module '" + identifier + "' has not been defined.")
return module
}
function provide (name, what) {
return (modules[name] = what)
}
context['provide'] = provide
context['require'] = require
function aug(o, o2) {
for (var k in o2) k != 'noConflict' && k != '_VERSION' && (o[k] = o2[k])
return o
}
function boosh(s, r, els) {
// string || node || nodelist || window
if (typeof s == 'string' || s.nodeName || (s.length && 'item' in s) || s == window) {
els = ender._select(s, r)
els.selector = s
} else els = isFinite(s.length) ? s : [s]
return aug(els, boosh)
}
function ender(s, r) {
return boosh(s, r)
}
aug(ender, {
_VERSION: '0.3.6'
, fn: boosh // for easy compat to jQuery plugins
, ender: function (o, chain) {
aug(chain ? boosh : ender, o)
}
, _select: function (s, r) {
return (r || document).querySelectorAll(s)
}
})
aug(boosh, {
forEach: function (fn, scope, i) {
// opt out of native forEach so we can intentionally call our own scope
// defaulting to the current item and be able to return self
for (i = 0, l = this.length; i < l; ++i) i in this && fn.call(scope || this[i], this[i], i, this)
// return self for chaining
return this
},
$: ender // handy reference to self
})
ender.noConflict = function () {
context.$ = old
return this
}
if (typeof module !== 'undefined' && module.exports) module.exports = ender
// use subscript notation as extern for Closure compilation
context['ender'] = context['$'] = context['ender'] || ender
}(this);
// pakmanager:bindings
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
/**
* Module dependencies.
*/
var fs = require('fs')
, path = require('path')
, join = path.join
, dirname = path.dirname
, exists = fs.existsSync || path.existsSync
, defaults = {
arrow: process.env.NODE_BINDINGS_ARROW || ' → '
, compiled: process.env.NODE_BINDINGS_COMPILED_DIR || 'compiled'
, platform: process.platform
, arch: process.arch
, version: process.versions.node
, bindings: 'bindings.node'
, try: [
// node-gyp's linked version in the "build" dir
[ 'module_root', 'build', 'bindings' ]
// node-waf and gyp_addon (a.k.a node-gyp)
, [ 'module_root', 'build', 'Debug', 'bindings' ]
, [ 'module_root', 'build', 'Release', 'bindings' ]
// Debug files, for development (legacy behavior, remove for node v0.9)
, [ 'module_root', 'out', 'Debug', 'bindings' ]
, [ 'module_root', 'Debug', 'bindings' ]
// Release files, but manually compiled (legacy behavior, remove for node v0.9)
, [ 'module_root', 'out', 'Release', 'bindings' ]
, [ 'module_root', 'Release', 'bindings' ]
// Legacy from node-waf, node <= 0.4.x
, [ 'module_root', 'build', 'default', 'bindings' ]
// Production "Release" buildtype binary (meh...)
, [ 'module_root', 'compiled', 'version', 'platform', 'arch', 'bindings' ]
]
}
/**
* The main `bindings()` function loads the compiled bindings for a given module.
* It uses V8's Error API to determine the parent filename that this function is
* being invoked from, which is then used to find the root directory.
*/
function bindings (opts) {
// Argument surgery
if (typeof opts == 'string') {
opts = { bindings: opts }
} else if (!opts) {
opts = {}
}
opts.__proto__ = defaults
// Get the module root
if (!opts.module_root) {
opts.module_root = exports.getRoot(exports.getFileName())
}
// Ensure the given bindings name ends with .node
if (path.extname(opts.bindings) != '.node') {
opts.bindings += '.node'
}
var tries = []
, i = 0
, l = opts.try.length
, n
, b
, err
for (; i<l; i++) {
n = join.apply(null, opts.try[i].map(function (p) {
return opts[p] || p
}))
tries.push(n)
try {
b = opts.path ? require.resolve(n) : require(n)
if (!opts.path) {
b.path = n
}
return b
} catch (e) {
if (!/not find/i.test(e.message)) {
throw e
}
}
}
err = new Error('Could not locate the bindings file. Tried:\n'
+ tries.map(function (a) { return opts.arrow + a }).join('\n'))
err.tries = tries
throw err
}
module.exports = exports = bindings
/**
* Gets the filename of the JavaScript file that invokes this function.
* Used to help find the root directory of a module.
* Optionally accepts an filename argument to skip when searching for the invoking filename
*/
exports.getFileName = function getFileName (calling_file) {
var origPST = Error.prepareStackTrace
, origSTL = Error.stackTraceLimit
, dummy = {}
, fileName
Error.stackTraceLimit = 10
Error.prepareStackTrace = function (e, st) {
for (var i=0, l=st.length; i<l; i++) {
fileName = st[i].getFileName()
if (fileName !== __filename) {
if (calling_file) {
if (fileName !== calling_file) {
return
}
} else {
return
}
}
}
}
// run the 'prepareStackTrace' function above
Error.captureStackTrace(dummy)
dummy.stack
// cleanup
Error.prepareStackTrace = origPST
Error.stackTraceLimit = origSTL
return fileName
}
/**
* Gets the root directory of a module, given an arbitrary filename
* somewhere in the module tree. The "root directory" is the directory
* containing the `package.json` file.
*
* In: /home/nate/node-native-module/lib/index.js
* Out: /home/nate/node-native-module
*/
exports.getRoot = function getRoot (file) {
var dir = dirname(file)
, prev
while (true) {
if (dir === '.') {
// Avoids an infinite loop in rare cases, like the REPL
dir = process.cwd()
}
if (exists(join(dir, 'package.json')) || exists(join(dir, 'node_modules'))) {
// Found the 'package.json' file or 'node_modules' dir; we're done
return dir
}
if (prev === dir) {
// Got to the top
throw new Error('Could not find module root given file: "' + file
+ '". Do you have a `package.json` file? ')
}
// Try the parent dir next
prev = dir
dir = join(dir, '..')
}
}
provide("bindings", module.exports);
}(global));
// pakmanager:nan
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
console.log(require('path').relative('.', __dirname));
provide("nan", module.exports);
}(global));
// pakmanager:fs
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
console.log("I'm `fs` modules");
provide("fs", module.exports);
}(global));
// pakmanager:microtime
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
var binding = require('bindings')('microtime.node')
exports.now = binding.now
exports.nowDouble = binding.nowDouble
exports.nowStruct = binding.nowStruct
provide("microtime", module.exports);
}(global));
// pakmanager:nanotimer
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
function NanoTimer(log){
var version = process.version;
var major = version.split('.')[0];
major = major.split('v')[1];
var minor = version.split('.')[1];
if ((major == 0) && (minor < 10)){
console.log('Error: Please update to the latest version of node! This library requires 0.10.x or later');
process.exit(0);
}
//Time reference variables
this.intervalT1 = null;
this.timeOutT1 = null;
this.intervalCount = 1;
//Deferred reference indicator variables. Indicate whether the timer used/will use the deferred call. ie - delay/interval > 25ms
this.deferredInterval = false;
this.deferredTimeout = false;
//Deferred reference variables. Used to clear the native js timeOut calls
this.deferredTimeoutRef = null;
this.deferredIntervalRef = null;
//Callback reference variables. Used to be able to still successfully call callbacks when timeouts or intervals are cleared.
this.timeoutCallbackRef = null;
this.intervalCallbackRef = null;
//Immediate reference variables. Used to clear functions scheduled with setImmediate from running in the event timeout/interval is cleared.
this.timeoutImmediateRef = null;
this.intervalImmediateRef = null;
this.intervalErrorChecked = false;
this.intervalType = "";
if(log){
this.logging = true;
}
}
NanoTimer.prototype.time = function(task, args, format, callback){
//Asynchronous task
if(callback){
var t1 = process.hrtime();
if(args){
args.push(function(){
var time = process.hrtime(t1);
if(format == 's'){
callback(time[0] + time[1]/1000000000);
} else if (format == 'm'){
callback(time[0]/1000 + time[1]/1000000);
} else if (format == 'u'){
callback(time[0]/1000000 + time[1]/1000);
} else if (format == 'n'){
callback(time[0]/1000000000 + time[1]);
} else {
callback(time);
}
});
task.apply(null, args);
} else {
task(function(){
var time = process.hrtime(t1);
if(format == 's'){
callback(time[0] + time[1]/1000000000);
} else if (format == 'm'){
callback(time[0]/1000 + time[1]/1000000);
} else if (format == 'u'){
callback(time[0]/1000000 + time[1]/1000);
} else if (format == 'n'){
callback(time[0]/1000000000 + time[1]);
} else {
callback(time);
}
});
}
//Synchronous task
} else {
var t1 = process.hrtime();
if(args){
task.apply(null, args);
} else{
task();
}
var t2 = process.hrtime(t1);
if(format == 's'){
return t2[0] + t2[1]/1000000000;
} else if (format == 'm'){
return t2[0]/1000 + t2[1]/1000000;
} else if (format == 'u'){
return t2[0]/1000000 + t2[1]/1000;
} else if (format == 'n'){
return t2[0]/1000000000 + t2[1];
} else {
return process.hrtime(t1);
}
}
};
NanoTimer.prototype.setInterval = function(task, args, interval, callback){
if(!this.intervalErrorChecked){
//Task error handling
if(!task){
console.log("A task function must be specified to setInterval");
process.exit(1);
} else {
if(typeof(task) != "function"){
console.log("Task argument to setInterval must be a function reference");
process.exit(1);
}
}
//Interval error handling
if(!interval){
console.log("An interval argument must be specified");
process.exit(1);
} else {
if(typeof(interval) != "string"){
console.log("Interval argument to setInterval must be a string specified as an integer followed by 's' for seconds, 'm' for milli, 'u' for micro, and 'n' for nanoseconds. Ex. 2u");
process.exit(1);
}
}
//This ref is used if deferred timeout is cleared, so the callback can still be accessed
if(callback){
if(typeof(callback) != "function"){
console.log("Callback argument to setInterval must be a function reference");
process.exit(1);
} else {
this.intervalCallbackRef = callback;
}
}
this.intervalType = interval[interval.length-1];
if(this.intervalType == 's'){
this.intervalTime = interval.slice(0, interval.length-1) * 1000000000;
} else if(this.intervalType == 'm'){
this.intervalTime = interval.slice(0, interval.length-1) * 1000000;
} else if(this.intervalType == 'u'){
this.intervalTime = interval.slice(0, interval.length-1) * 1000;
} else if(this.intervalType == 'n'){
this.intervalTime = interval.slice(0, interval.length-1);
} else {
console.log('Error with argument: ' + interval + ': Incorrect interval format. Format is an integer followed by "s" for seconds, "m" for milli, "u" for micro, and "n" for nanoseconds. Ex. 2u');
process.exit(1);
}
this.intervalErrorChecked = true;
}
//Avoid dereferencing inside of function objects later
//Must be performed on every execution
var thisTimer = this;
if(this.intervalTime > 0){
//Check and set constant t1 value.
if(this.intervalT1 == null){
this.intervalT1 = process.hrtime();
}
//Check for overflow. Every 8,000,000 seconds (92.6 days), this will overflow
//and the reference time T1 will be re-acquired. This is the only case in which error will
//propagate.
if(this.intervalTime*this.intervalCount > 8000000000000000){
this.intervalT1 = process.hrtime();
this.intervalCount = 1;
}
//Get comparison time
this.difArray = process.hrtime(this.intervalT1);
this.difTime = (this.difArray[0] * 1000000000) + this.difArray[1];
//If updated time < expected time, continue
//Otherwise, run task and update counter
if(this.difTime < (this.intervalTime*this.intervalCount)){
//Can potentially defer to less accurate setTimeout if intervaltime > 25ms
if(this.intervalTime > 25000000){
if(this.deferredInterval == false){
this.deferredInterval = true;
msDelay = (this.intervalTime - 25000000) / 1000000.0;
this.deferredIntervalRef = setTimeout(function(){thisTimer.setInterval(task, args, interval, callback);}, msDelay);
} else {
this.deferredIntervalRef = null;
this.intervalImmediateRef = setImmediate(function(){thisTimer.setInterval(task, args, interval, callback);});
}
} else {
this.intervalImmediateRef = setImmediate(function(){thisTimer.setInterval(task, args, interval, callback);});
}
} else {
this.intervalImmediateRef = null;
if(this.logging){
console.log('nanotimer log: ' + 'cycle time at - ' + this.difTime);
}
if(args){
task.apply(null, args);
} else {
task();
}
//Check if the intervalT1 is still not NULL. If it is, that means the task cleared the interval so it should not run again.
if(this.intervalT1){
this.intervalCount++;
this.deferredInterval = false;
this.intervalImmediateRef = setImmediate(function(){thisTimer.setInterval(task, args, interval, callback);});
}
}
//If interval = 0, run as fast as possible.
} else {
//Check and set constant t1 value.
if(this.intervalT1 == null){
this.intervalT1 = process.hrtime();
this.intervalContinue = true;
}
if(this.intervalContinue == true) {
if(args){
task.apply(null, args);
} else {
task();
}
setImmediate(function(){thisTimer.setInterval(task, args, interval, callback);});
} else {
this.intervalT1 = null;
this.intervalCount = 1;
callback();
}
}
};
NanoTimer.prototype.setTimeout = function(task, args, delay, callback){
//Task error handling
if(!task){
console.log("A task function must be specified to setTimeout");
process.exit(1);
} else {
if(typeof(task) != "function"){
console.log("Task argument to setTimeout must be a function reference");
process.exit(1);
}
}
//Delay error handling
if(!delay){
console.log("A delay argument must be specified");
process.exit(1);
} else {
if(typeof(delay) != "string"){
console.log("Delay argument to setTimeout must be a string specified as an integer followed by 's' for seconds, 'm' for milli, 'u' for micro, and 'n' for nanoseconds. Ex. 2u");
process.exit(1);
}
}
//This ref is used if deferred timeout is cleared, so the callback can still be accessed
if(callback){
if(typeof(callback) != "function"){
console.log("Callback argument to setTimeout must be a function reference");
process.exit(1);
} else {
this.timeoutCallbackRef = callback;
}
}
//Avoid dereferencing
var thisTimer = this;
var delayType = delay[delay.length-1];
if(delayType == 's'){
var delayTime = delay.slice(0, delay.length-1) * 1000000000;
} else if(delayType == 'm'){
var delayTime = delay.slice(0, delay.length-1) * 1000000;
} else if(delayType == 'u'){
var delayTime = delay.slice(0, delay.length-1) * 1000;
} else if(delayType == 'n'){
var delayTime = delay.slice(0, delay.length-1);
} else {
console.log('Error with argument: ' + delay + ': Incorrect delay format. Format is an integer followed by "s" for seconds, "m" for milli, "u" for micro, and "n" for nanoseconds. Ex. 2u');
process.exit(1);
}
//Set marker
if(this.timeOutT1 == null){
this.timeOutT1 = process.hrtime();
}
var difArray = process.hrtime(this.timeOutT1);
var difTime = (difArray[0] * 1000000000) + difArray[1];
if(difTime < delayTime){
//Can potentially defer to less accurate setTimeout if delayTime > 25ms
if(delayTime > 25000000){
if(this.deferredTimeout == false){
this.deferredTimeout = true;
msDelay = (delayTime - 25000000) / 1000000.0;
this.deferredTimeoutRef = setTimeout(function(){thisTimer.setTimeout(task, args, delay, callback);}, msDelay);
} else {
this.deferredTimeoutRef = null;
this.timeoutImmediateRef = setImmediate(function(){thisTimer.setTimeout(task, args, delay, callback);});
}
} else {
this.timeoutImmediateRef = setImmediate(function(){thisTimer.setTimeout(task, args, delay, callback);});
}
} else {
this.timeoutImmediateRef = null;
this.timeOutT1 = null;
this.deferredTimeout = false;
if(this.logging == true){
console.log('nanotimer log: ' + 'actual wait - ' + difTime);
}
if(args){
task.apply(null, args);
} else{
task();
}
if(callback){
var data = {'waitTime':difTime};
callback(data);
}
}
};
NanoTimer.prototype.clearInterval = function(){
if(this.deferredIntervalRef){
clearTimeout(this.deferredIntervalRef);
this.deferredInterval = false;
}
if(this.intervalImmediateRef){
clearImmediate(this.intervalImmediateRef);
}
this.intervalT1 = null;
this.intervalCount = 1;
this.intervalErrorChecked = false;
if(this.intervalCallbackRef){
this.intervalCallbackRef();
}
};
NanoTimer.prototype.clearTimeout = function(){
if(this.deferredTimeoutRef){
clearTimeout(this.deferredTimeoutRef);
var difArray = process.hrtime(this.timeOutT1);
var difTime = (difArray[0] * 1000000000) + difArray[1];
this.deferredTimeout = false;
}
if(this.timeoutImmediateRef){
clearImmediate(this.timeoutImmediateRef);
}
this.timeOutT1 = null;
if(this.timeoutCallbackRef){
var data = {'waitTime':difTime};
this.timeoutCallbackRef(data);
}
};
module.exports = NanoTimer;
provide("nanotimer", module.exports);
}(global));
// pakmanager:ledstripe-fork
(function (context) {
var module = { exports: {} }, exports = module.exports
, $ = require("ender")
;
var fs = require('fs'),
microtime = require('microtime'),
nanotimer = require('nanotimer')
function LedStripe(){
this.spiDevice = '/dev/spidev0.0';
this.numLEDs = 23;
this.spiFd = null; //filedescriptor for spidevice
this.gamma = 2.5;
this.gammatable = new Array(256);
this.bytePerPixel = 3; //RGB
this.rowResetTime = 1000; // number of us CLK has to be pulled low (=no writes) for frame reset
// manual of WS2801 says 500 is enough, however we need at least 1000
this.lastWriteTime = microtime.now()-this.rowResetTime-1; //last time something was written to SPI
//required for save WS2801 reset
this.sendRgbBuf = null; //function for writing to stripe, depends on stripe type
}
LedStripe.prototype = {
/*
* connect to SPI port
*/
connect: function(numLEDs,stripeType,spiDevice, gamma){
// sanity check for params
if ((numLEDs !== parseInt(numLEDs)) || (numLEDs<1)) {
console.error("invalid param for number of LEDs, plz use integer >0");
return false;
}
if ((stripeType != 'WS2801') && (stripeType != 'LPD8806')){
console.error("invalid param for stripe type, only WS2801 and LPD8806 are suported");
return false;
}
if (spiDevice) this.spiDevice = spiDevice;
// connect synchronously
try{
this.spiFd = fs.openSync(this.spiDevice, 'w');
} catch (err) {
console.error("error opening SPI device "+this.spiDevice, err);
return false;
}
this.sendRgbBuf = (stripeType == 'WS2801') ? this.sendRgbBufWS2801 : this.sendRgbBufLPD8806;
this.numLEDs = numLEDs;
this.gamma = gamma ? gamma : 2.5; //set gamma correction value
// compute gamma correction table
for (var i=0; i<256; i++)
this.gammatable[i] = Math.round(255*Math.pow(i/255, this.gamma));
//console.log("gammatable" + this.gammatable);
},
/*
* disconnect from SPI port
*/
disconnect : function(){
if (this.spiFd) fs.closeSync(this.spiFd);
},
sendRgbBufLPD8806 : function(buffer){
var bufSize = this.numLEDs * this.bytePerPixel;
if (buffer.length != bufSize) {
console.log ("buffer length (" + buffer.lenght +" byte) does not match LED stripe size ("+
this.numLEDs + " LEDs x " + this.bytePerPixel + " colors)");
return;
} // end if (buffer.length != bufSize)
if (this.spiFd) {
var numLeadingZeros = Math.ceil(this.numLEDs / 32); //number of zeros to "reset" LPD8806 stripe
// mind the last zero byte for latching the last blue LED
var aBuf = new Buffer (numLeadingZeros + bufSize + 1);
// prime the stripe with zeros
for (var i=0; i<numLeadingZeros; i++){
aBuf[i] =0x00;
};
// transform color values
for (var i=0; i<(bufSize); i+=3){
var r = (this.gammatable[buffer[i+0]]>>1)+0x80;
var g = (this.gammatable[buffer[i+1]]>>1)+0x80;
var b = (this.gammatable[buffer[i+2]]>>1)+0x80;
aBuf[i+numLeadingZeros+0]=g;
aBuf[i+numLeadingZeros+1]=r;
aBuf[i+numLeadingZeros+2]=b;
};
// trailing zero
aBuf[bufSize+numLeadingZeros] = 0x00;
fs.writeSync(this.spiFd, aBuf, 0, aBuf.length, null);
} //end if (this.spiFd)
}, // end sendRgbBufLPD8806
/*
* send buffer with RGB values to LPD 8806 stripe
*/
sendRgbBufWS2801 : function(buffer){
// checking if enough time passed for resetting stripe
if (microtime.now() > (this.lastWriteTime + this.rowResetTime)){
// yes, its o.k., lets write
// but first do gamma correction
var adjustedBuffer = new Buffer(buffer.length);
for (var i=0; i < buffer.length; i++){
adjustedBuffer[i]=this.gammatable[buffer[i]];
}
fs.writeSync(this.spiFd, adjustedBuffer, 0, buffer.length, null);
this.lastWriteTime = microtime.now();
return true;
}
console.log('writing to fast, data dropped');
return false;
}, // end sendRgbBufWS2801
/*
* fill whole stripe with one color
*/
fill : function(r,g,b){
if (this.spiFd) {
var bufSize = this.numLEDs * this.bytePerPixel;
var aBuf = new Buffer(bufSize);
for (var i=0; i<(bufSize); i+=3){
aBuf[i+0]=r;
aBuf[i+1]=g;
aBuf[i+2]=b;
}
this.sendRgbBuf(aBuf);
}
}, //end fill
/*
* play an animation from RGB buffer
* - buffersize must be a multiple of one frame
* - delay between frames is given in microtimers format,
* e.g. 10m for 10 milliseconds
*/
animate : function(buffer,frameDelay, callback){
var row = 0;
var rows = buffer.length/(this.numLEDs*this.bytePerPixel);
if (rows != Math.ceil(rows)) {
console.log("buffer size is not a multiple of frame size");
return false;
}
var myTimer = new nanotimer();
console.log("Writing " + rows + " rows for " + this.numLEDs + " LEDs with delay " + frameDelay);
myTimer.setInterval(function(){
if (row>=rows){
myTimer.clearInterval();
if (callback)
callback();
} else {
this.sendRgbBuf(buffer.slice(row * this.numLEDs * this.bytePerPixel, (row + 1) * this.numLEDs * this.bytePerPixel));
row++;
}
}.bind(this), null, frameDelay, function(err) {
if(err) {
//error
}
});
} //end animate
}
module.exports = new LedStripe();
provide("ledstripe-fork", module.exports);
}(global));