UNPKG

johnny-five

Version:

The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!

409 lines (342 loc) 9.55 kB
var IS_TEST_MODE = !!process.env.IS_TEST_MODE; var Board = require("./board"); var Fn = require("./fn"); var events = require("events"); var util = require("util"); var Collection = require("./mixins/collection"); var within = require("./mixins/within"); // Sensor instance private data var priv = new Map(); // var aliases = { // change: [ // // Generic sensor value change // "change", // // Slider sensors (alias) // "slide", // // Soft Potentiometer (alias) // "touch", // // Force Sensor (alias) // "force", // // Flex Sensor (alias) // "bend" // ] // }; // To reduce noise in sensor readings, sort collected samples // from high to low and select the value in the center. function median(input) { // faster than default comparitor (even for small n) var sorted = input.sort(function(a, b) { return a - b; }); var len = sorted.length; var 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; } /** * 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 raw = null; var last = -1; var samples = []; 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); // Create a "state" entry for privately // storing the state of the sensor var state = { enabled: typeof opts.enabled === "undefined" ? true : opts.enabled, booleanBarrier: opts.type === "digital" ? 0 : null, intervalId: null, scale: null, value: 0, median: 0, freq: opts.freq || 25, previousFreq: opts.freq || 25, }; // Put a reference where the prototype methods defined in this file have access priv.set(this, state); // 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; this.io[opts.type + "Read"](this.pin, function(data) { raw = data; // Only append to the samples when noise filtering can/will be used if (opts.type !== "digital") { samples.push(raw); } }.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. var eventProcessing = function() { var err, boundary; err = null; // For digital sensors, skip the analog // noise filtering provided below. if (opts.type === "digital") { this.emit("data", raw); if (last !== raw) { this.emit("change", raw); last = raw; } 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 state.median = median(samples); } this.emit("data", state.median); // If the filtered (state.median) value for this interval is at least ± the // configured threshold from last, fire change events if (state.median <= (last - this.threshold) || state.median >= (last + this.threshold)) { this.emit("change", state.median); // Update the instance-local `last` value (only) when a new change event // has been emitted. For comparison in the next interval last = state.median; } if (this.limit) { if (state.median <= this.limit[0]) { boundary = "lower"; } if (state.median >= this.limit[1]) { boundary = "upper"; } if (boundary) { this.emit("limit", { boundary: boundary, value: state.median }); this.emit("limit:" + boundary, state.median); } } // Reset samples samples.length = 0; }.bind(this); // ./function eventProcessing() Object.defineProperties(this, { raw: { get: function() { return raw; } }, analog: { get: function() { if (opts.type === "digital") { return raw; } return raw === null ? 0 : Fn.map(this.raw, 0, 1023, 0, 255) | 0; } }, constrained: { get: function() { if (opts.type === "digital") { return raw; } return raw === null ? 0 : Fn.constrain(this.raw, 0, 255); } }, boolean: { get: function() { var state = priv.get(this); var booleanBarrier = state.booleanBarrier; var scale = state.scale || [0, 1023]; if (booleanBarrier === null) { booleanBarrier = scale[0] + (scale[1] - scale[0]) / 2; } return this.value > booleanBarrier; } }, scaled: { get: function() { var mapped, constrain; if (state.scale && raw !== null) { if (opts.type === "digital") { // Value is either 0 or 1, use as an index // to return the scaled value. return state.scale[raw]; } mapped = Fn.fmap(raw, this.range[0], this.range[1], state.scale[0], state.scale[1]); constrain = Fn.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 (state.intervalId) { clearInterval(state.intervalId); } if (state.freq !== null) { state.intervalId = setInterval(eventProcessing, newFreq); } } }, value: { get: function() { if (state.scale) { this.isScaled = true; return this.scaled; } return raw; } } }); 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 // and only if the sensor is not `enabled: false` if (state.enabled) { 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 * */ Object.assign(Sensor.prototype, within); /** * enable Enable a disabled sensor. * * @return {Object} instance * */ Sensor.prototype.enable = function() { var state = priv.get(this); if (!state.enabled) { this.freq = state.freq || state.previousFreq; } return this; }; /** * disable Disable an enabled sensor. * * @return {Object} instance * */ Sensor.prototype.disable = function() { var state = priv.get(this); if (state.enabled) { state.enabled = false; state.previousFreq = state.freq; this.freq = null; } 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 = function(low, high) { var scale = Array.isArray(low) ? low : [low, high]; return Fn.map(this.raw, 0, 1023, scale[0], scale[1]); }; Sensor.prototype.fscaleTo = function(low, high) { var scale = Array.isArray(low) ? low : [low, high]; return Fn.fmap(this.raw, 0, 1023, scale[0], scale[1]); }; /** * 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; }; /** * Sensors() * new Sensors() * * Constructs an Array-like instance of all servos */ function Sensors(numsOrObjects) { if (!(this instanceof Sensors)) { return new Sensors(numsOrObjects); } Object.defineProperty(this, "type", { value: Sensor }); Collection.Emitter.call(this, numsOrObjects); } Sensors.prototype = Object.create(Collection.Emitter.prototype, { constructor: { value: Sensors } }); Collection.installMethodForwarding( Sensors.prototype, Sensor.prototype ); // Assign Sensors Collection class as static "method" of Sensor. Sensor.Collection = Sensors; /* istanbul ignore else */ if (IS_TEST_MODE) { Sensor.purge = function() { priv.clear(); }; } module.exports = Sensor;