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!

393 lines (334 loc) 9.82 kB
const Board = require("./board"); const Fn = require("./fn"); const Collection = require("./mixins/collection"); const Withinable = require("./mixins/withinable"); // Sensor instance private data const priv = new Map(); // 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) const sorted = input.sort((a, b) => a - b); const len = sorted.length; const 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} options Options: pin, freq, range */ class Sensor extends Withinable { constructor(options) { super(); // Defaults to 10-bit resolution let resolution = 0x3FF; let raw = null; let last = -1; const samples = []; Board.Component.call( this, options = Board.Options(options) ); if (!options.type) { options.type = "analog"; } if (this.io.RESOLUTION && (this.io.RESOLUTION.ADC && (this.io.RESOLUTION.ADC !== resolution))) { resolution = this.io.RESOLUTION.ADC; } // Set the pin to ANALOG (INPUT) mode this.mode = options.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 const state = { enabled: typeof options.enabled === "undefined" ? true : options.enabled, booleanBarrier: options.type === "digital" ? 0 : null, intervalId: null, scale: null, value: 0, median: 0, freq: options.freq || 25, previousFreq: options.freq || 25, }; // Put a reference where the prototype methods defined in this file have access priv.set(this, state); // Sensor instance properties this.range = options.range || [0, resolution]; this.limit = options.limit || null; this.threshold = options.threshold === undefined ? 1 : options.threshold; this.isScaled = false; this.io[`${options.type}Read`](this.pin, data => { raw = data; // Only append to the samples when noise filtering can/will be used if (options.type !== "digital") { samples.push(raw); } }); // 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. const eventProcessing = () => { let err; let boundary; err = null; // For digital sensors, skip the analog // noise filtering provided below. if (options.type === "digital") { this.emit("data", raw); /* istanbul ignore else */ 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); } const roundMedian = Math.round(state.median); this.emit("data", roundMedian); // 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", roundMedian); // 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, value: roundMedian }); this.emit(`limit:${boundary}`, roundMedian); } } // Reset samples samples.length = 0; }; // ./function eventProcessing() Object.defineProperties(this, { raw: { get() { return raw; } }, analog: { get() { if (options.type === "digital") { return raw; } return raw === null ? 0 : Fn.map(this.raw, 0, resolution, 0, 255) | 0; }, }, constrained: { get() { if (options.type === "digital") { return raw; } return raw === null ? 0 : Fn.constrain(this.raw, 0, 255); } }, boolean: { get() { const state = priv.get(this); let booleanBarrier = state.booleanBarrier; const scale = state.scale || [0, resolution]; if (booleanBarrier === null) { booleanBarrier = scale[0] + (scale[1] - scale[0]) / 2; } return this.value > booleanBarrier; } }, scaled: { get() { let mapped; let constrain; if (state.scale && raw !== null) { if (options.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() { return state.freq; }, set(newFreq) { state.freq = newFreq; if (state.intervalId) { clearInterval(state.intervalId); } if (state.freq !== null) { state.intervalId = setInterval(eventProcessing, newFreq); } } }, value: { get() { if (state.scale) { this.isScaled = true; return this.scaled; } return raw; } }, resolution: { get() { return resolution; } } }); /* istanbul ignore else */ if (!!process.env.IS_TEST_MODE) { Object.defineProperties(this, { state: { get() { 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; } } /** * enable Enable a disabled sensor. * * @return {Object} instance * */ enable() { const state = priv.get(this); /* istanbul ignore else */ if (!state.enabled) { this.freq = state.freq || state.previousFreq; } return this; } /** * disable Disable an enabled sensor. * * @return {Object} instance * */ disable() { const state = priv.get(this); /* istanbul ignore else */ 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 * */ scale(low, high) { this.isScaled = true; priv.get(this).scale = Array.isArray(low) ? low : [low, high]; return this; } /** * scaleTo Scales value to integer representation * @param {Number} low An array containing a lower and upper bound * * @param {Number} low A number to use as a lower bound * @param {Number} high A number to use as an upper bound * @return {Number} The scaled value */ scaleTo(low, high) { const scale = Array.isArray(low) ? low : [low, high]; return Fn.map(this.raw, 0, this.resolution, scale[0], scale[1]); } /** * fscaleTo Scales value to single precision float representation * @param {Number} low An array containing a lower and upper bound * * @param {Number} low A number to use as a lower bound * @param {Number} high A number to use as an upper bound * @return {Number} The scaled value */ fscaleTo(low, high) { const scale = Array.isArray(low) ? low : [low, high]; return Fn.fmap(this.raw, 0, this.resolution, scale[0], scale[1]); } /** * booleanAt Set a midpoint barrier value used to calculate returned value of * .boolean property. * * @param {Number} barrier * @return {Object} instance * */ booleanAt(barrier) { priv.get(this).booleanBarrier = barrier; return this; } } /** * Sensors() * new Sensors() * * Constructs an Array-like instance of all servos */ class Sensors extends Collection.Emitter { constructor(numsOrObjects) { super(numsOrObjects); } get type() { return Sensor; } } Collection.installMethodForwarding( Sensors.prototype, Sensor.prototype ); // Assign Sensors Collection class as static "method" of Sensor. Sensor.Collection = Sensors; /* istanbul ignore else */ if (!!process.env.IS_TEST_MODE) { Sensor.purge = () => { priv.clear(); }; } module.exports = Sensor;