UNPKG

ledstripe-fork

Version:

control WS2801 and LPD8806 LED stripes via SPI

872 lines (733 loc) 29 kB
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));