johnny-five
Version:
Firmata based Arduino Programming Framework.
297 lines (241 loc) • 6.87 kB
JavaScript
var Board = require("../lib/board.js"),
events = require("events"),
util = require("util");
// Sensor instance private data
var priv = new WeakMap(),
aliases = {
change: [
// Generic sensor value change
"change",
// Slider sensors (alias)
"slide",
// Soft Potentiometer (alias)
"touch",
// Force Sensor (alias)
"force",
// Flex Sensor (alias)
"bend"
]
};
/**
* Sensor
* @constructor
*
* @description Generic analog or digital sensor constructor
*
* @param {Object} opts Options: pin, freq, range
*/
function Sensor( opts ) {
if ( !(this instanceof Sensor) ) {
return new Sensor( opts );
}
var value, last, min, max, vranges, samples;
value = null;
last = null;
min = 1023;
max = 0;
samples = [];
// Initialize a Device instance on a Board
Board.Device.call(
this, opts = Board.Options( opts )
);
// Set the pin to ANALOG (INPUT) mode
this.mode = this.firmata.MODES.ANALOG;
this.firmata.pinMode( this.pin, this.mode );
// TODO: remove this for 0.7.0
opts.threshold = opts.sensitivity;
// Sensor instance properties
this.freq = opts.freq || 25;
this.range = opts.range || [ 0, 1023 ];
this.threshold = opts.threshold === undefined ? 1 : opts.threshold;
this.isScaled = false;
// Analog Read event loop
this.firmata.analogRead( this.pin, function( data ) {
// In the first 5 seconds, we will receive min/max
// calibration values which will be used to
// map and scale all sensor readings from this pin
// if ( data > max ) {
// this.range[1] = max = data;
// }
// if ( data < min ) {
// this.range[0] = min = data;
// }
// Update the closed over `value`
value = data;
samples.push( data );
}.bind(this));
// Throttle
setInterval(function() {
var err, median, low, high;
err = null;
// To reduce noise in sensor readings, sort collected samples
// from high to low and select the value in the center.
median = (function( input ) {
var half, len, sorted;
// faster than default comparitor (even for small n)
sorted = input.sort(function( a, b ) {
return a - b;
});
len = sorted.length;
half = Math.floor( len / 2 );
// If the length is odd, return the midpoint m
// If the length is even, return average of m & m + 1
return len % 2 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
}( samples ));
// Emit the continuous "read" event
this.emit( "read", err, median );
// If the median value for this interval is outside last +/- a threshold
// fire a change event
low = last - this.threshold;
high = last + this.threshold;
// Includes all aliases
// Prevent events from firing if the latest value is the same as
// the last value to trigger a change event
if ( median < low || median > high ) {
aliases.change.forEach(function( change ) {
this.emit( change, err, median );
}, this );
}
// Store this media value for comparison
// in next interval
last = median;
// Reset samples
samples.length = 0;
}.bind(this), this.freq );
// Create a "state" entry for privately
// storing the state of the sensor
priv.set( this, {
booleanBarrier: 512,
scale: null,
value: 0
});
Object.defineProperties( this, {
raw: {
get: function() {
return value;
}
},
normalized: {
get: function() {
return value === null ? null :
Board.map( value, this.range[0], this.range[1], 0, 255 );
}
},
constrained: {
get: function() {
return value === null ? null :
Board.constrain( this.normalized, 0, 255 );
}
},
boolean: {
get: function() {
return this.value > priv.get( this ).booleanBarrier ?
true : false;
}
},
scaled: {
get: function() {
var mapped, constrain, scale;
scale = priv.get( this ).scale;
if ( scale && value !== null ) {
mapped = Board.map( value, this.range[0], this.range[1], scale[0], scale[1] );
constrain = Board.constrain( mapped, scale[0], scale[1] );
return constrain;
}
return this.constrained;
}
},
value: {
get: function() {
var state;
state = priv.get( this );
if ( state.scale ) {
this.isScaled = true;
return this.scaled;
}
return value;
}
}
});
}
util.inherits( Sensor, events.EventEmitter );
/**
* pinMode Change the sensor's pinMode on the fly
* @param {Number} mode Sensor pin mode value
* @return {Object} instance
*/
Sensor.prototype.pinMode = function( mode ) {
this.mode = mode;
this.firmata.pinMode( this.pin, mode );
return this;
};
/**
* scale/scaleTo Set a value scaling range
*
* @param {Number} low Lowerbound
* @param {Number} high Upperbound
* @return {Object} instance
*
* @param {Array} [ low, high] Lowerbound
* @return {Object} instance
*
*/
Sensor.prototype.scale = function( low, high ) {
this.isScaled = true;
priv.get( this ).scale = Array.isArray(low) ?
low : [ low, high ];
return this;
};
Sensor.prototype.scaleTo = Sensor.prototype.scale;
/**
* booleanAt Set a midpoint barrier value used to calculate returned value of
* .boolean property.
*
* @param {Number} barrier
* @return {Object} instance
*
*/
Sensor.prototype.booleanAt = function( barrier ) {
priv.get(this).booleanBarrier = barrier;
return this;
};
/**
* EXPERIMENTAL
*
* within When value is within the provided range, execute callback
*
* @param {Number} range Upperbound, converted into an array,
* where 0 is lowerbound
* @param {Function} callback Callback to execute when value falls inside range
* @return {Object} instance
*
*
* @param {Array} range Lower to Upper bounds [ low, high ]
* @param {Function} callback Callback to execute when value falls inside range
* @return {Object} instance
*
*/
Sensor.prototype.within = function( range, callback ) {
var upper;
if ( typeof range === "number" ) {
upper = range;
range = [ 0, upper ];
}
if ( !Array.isArray(range) ) {
this.emit( "error", { message: "range must be an array" } );
return;
}
// Use the continuous read event for high resolution
this.on("read", function() {
if ( this.value >= range[0] && this.value <= range[1] ) {
callback.call( this, null );
}
}.bind(this));
return this;
};
module.exports = Sensor;
// Reference
// http://itp.nyu.edu/physcomp/Labs/Servo
// http://arduinobasics.blogspot.com/2011/05/arduino-uno-flex-sensor-and-leds.html
// TODO:
// Update comments/docs