UNPKG

johnny-five-electron

Version:

Temporary fork to support Electron (to be deprecated)

330 lines (285 loc) 8.28 kB
var Board = require("../lib/board.js"), events = require("events"), util = require("util"), within = require("./mixins/within"), __ = require("./fn"); // Sensor instance private data var priv = new Map(), aliases = { change: [ // Generic sensor value change "change", // Slider sensors (alias) "slide", // Soft Potentiometer (alias) "touch", // Force Sensor (alias) "force", // Flex Sensor (alias) "bend" ] }, IS_TEST_MODE = !!process.env.IS_TEST_MODE; // To reduce noise in sensor readings, sort collected samples // from high to low and select the value in the center. function arrayMedian(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; } // ./arrayMedian(input) /** * 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, samples, state, median, intervalId, eventProcessing; value = null; min = 1023; max = 0; last = -min; samples = []; intervalId = null; median = NaN; Board.Component.call( this, opts = Board.Options(opts) ); if (!opts.type) { opts.type = "analog"; } // Set the pin to ANALOG (INPUT) mode this.mode = opts.type === "digital" ? this.io.MODES.INPUT : this.io.MODES.ANALOG; this.io.pinMode(this.pin, this.mode); // Sensor instance properties this.range = opts.range || [0, 1023]; this.limit = opts.limit || null; this.threshold = opts.threshold === undefined ? 1 : opts.threshold; this.isScaled = false; // Read event loop this.io[opts.type + "Read"](this.pin, function(data) { // Update the instance-local `value` value. // This is shared by the accessors defined below. value = data; // Only append to the samples when noise filtering can/will be used if (opts.type !== "digital") { samples.push(value); } }.bind(this)); // Throttle // TODO: The event (interval) processing function should be outside of the Sensor // constructor function (with appropriate passed (and bound?) arguments), to // avoid creating a separate copy (of the function) for each Sensor instance. eventProcessing = function() { var err, boundary; err = null; // For digital sensors, skip the analog // noise filtering provided below. if (opts.type === "digital") { this.emit("data", err, value); if (last !== value) { aliases.change.forEach(function(change) { this.emit(change, err, value); }, this); // Update the instance-local `last` value. last = value; } return; } // Keep the previous calculated value if there were no new readings if (samples.length > 0) { // Filter the accumulated sample values to reduce analog reading noise median = arrayMedian(samples); } // @DEPRECATE this.emit("read", err, median); // The "read" event has been deprecated in // favor of a "data" event. this.emit("data", err, median); // If the filtered (median) value for this interval is at least ± the // configured threshold from last, fire change events if (median <= (last - this.threshold) || median >= (last + this.threshold)) { // Include all aliases aliases.change.forEach(function(change) { this.emit(change, err, median); }, this); // Update the instance-local `last` value (only) when a new change event // has been emitted. For comparison in the next interval last = median; } if (this.limit) { if (median <= this.limit[0]) { boundary = "lower"; } if (median >= this.limit[1]) { boundary = "upper"; } if (boundary) { this.emit("limit", err, { boundary: boundary, value: median }); this.emit("limit:" + boundary, err, median); } } // Reset samples samples.length = 0; }.bind(this); // ./function eventProcessing() // Create a "state" entry for privately // storing the state of the sensor state = { booleanBarrier: opts.type === "digital" ? 0 : 512, scale: null, value: 0, freq: opts.freq || 25 }; // Put a reference where the prototype methods defined in this file have access priv.set(this, state); Object.defineProperties(this, { raw: { get: function() { return value; } }, analog: { get: function() { if (opts.type === "digital") { return value; } return value === null ? null : Board.map(this.raw, 0, 1023, 0, 255) | 0; } }, constrained: { get: function() { if (opts.type === "digital") { return value; } return value === null ? null : Board.constrain(this.raw, 0, 255); } }, boolean: { get: function() { return this.value > priv.get(this).booleanBarrier ? true : false; } }, scaled: { get: function() { var mapped, constrain; if (state.scale && value !== null) { if (opts.type === "digital") { // Value is either 0 or 1, use as an index // to return the scaled value. return state.scale[value]; } mapped = Board.fmap(value, this.range[0], this.range[1], state.scale[0], state.scale[1]); constrain = Board.constrain(mapped, state.scale[0], state.scale[1]); return constrain; } return this.constrained; } }, freq: { get: function() { return state.freq; }, set: function(newFreq) { state.freq = newFreq; if (intervalId) { clearInterval(intervalId); } intervalId = setInterval(eventProcessing, newFreq); } }, value: { get: function() { if (state.scale) { this.isScaled = true; return this.scaled; } return value; } } }); if (IS_TEST_MODE) { Object.defineProperties(this, { state: { get: function() { return priv.get(this); } } }); } // Set the freq property only after the get and set functions are defined this.freq = state.freq; } util.inherits(Sensor, events.EventEmitter); /** * 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 * */ __.mixin(Sensor.prototype, within); /** * 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; }; 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