UNPKG

10.6 kBJavaScriptView Raw
1var Board = require("./board");
2var Fn = require("./fn");
3var events = require("events");
4var util = require("util");
5var Collection = require("./mixins/collection");
6var within = require("./mixins/within");
7
8
9// Sensor instance private data
10var priv = new Map();
11// var aliases = {
12// change: [
13// // Generic sensor value change
14// "change",
15// // Slider sensors (alias)
16// "slide",
17// // Soft Potentiometer (alias)
18// "touch",
19// // Force Sensor (alias)
20// "force",
21// // Flex Sensor (alias)
22// "bend"
23// ]
24// };
25
26// To reduce noise in sensor readings, sort collected samples
27// from high to low and select the value in the center.
28function median(input) {
29 // faster than default comparitor (even for small n)
30 var sorted = input.sort(function(a, b) {
31 return a - b;
32 });
33 var len = sorted.length;
34 var half = Math.floor(len / 2);
35
36 // If the length is odd, return the midpoint m
37 // If the length is even, return average of m & m + 1
38 return len % 2 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
39}
40
41/**
42 * Sensor
43 * @constructor
44 *
45 * @description Generic analog or digital sensor constructor
46 *
47 * @param {Object} opts Options: pin, freq, range
48 */
49
50function Sensor(opts) {
51
52 if (!(this instanceof Sensor)) {
53 return new Sensor(opts);
54 }
55
56 // Defaults to 10-bit resolution
57 var resolution = 0x3FF;
58 var raw = null;
59 var last = -1;
60 var samples = [];
61
62 Board.Component.call(
63 this, opts = Board.Options(opts)
64 );
65
66 if (!opts.type) {
67 opts.type = "analog";
68 }
69
70 if (this.io.RESOLUTION &&
71 (this.io.RESOLUTION.ADC &&
72 (this.io.RESOLUTION.ADC !== resolution))) {
73 resolution = this.io.RESOLUTION.ADC;
74 }
75
76 // Set the pin to ANALOG (INPUT) mode
77 this.mode = opts.type === "digital" ?
78 this.io.MODES.INPUT :
79 this.io.MODES.ANALOG;
80
81 this.io.pinMode(this.pin, this.mode);
82
83 // Create a "state" entry for privately
84 // storing the state of the sensor
85 var state = {
86 enabled: typeof opts.enabled === "undefined" ? true : opts.enabled,
87 booleanBarrier: opts.type === "digital" ? 0 : null,
88 intervalId: null,
89 scale: null,
90 value: 0,
91 median: 0,
92 freq: opts.freq || 25,
93 previousFreq: opts.freq || 25,
94 };
95 // Put a reference where the prototype methods defined in this file have access
96 priv.set(this, state);
97
98 // Sensor instance properties
99 this.range = opts.range || [0, resolution];
100 this.limit = opts.limit || null;
101 this.threshold = opts.threshold === undefined ? 1 : opts.threshold;
102 this.isScaled = false;
103
104 this.io[opts.type + "Read"](this.pin, function(data) {
105 raw = data;
106
107 // Only append to the samples when noise filtering can/will be used
108 if (opts.type !== "digital") {
109 samples.push(raw);
110 }
111 }.bind(this));
112
113 // Throttle
114 // TODO: The event (interval) processing function should be outside of the Sensor
115 // constructor function (with appropriate passed (and bound?) arguments), to
116 // avoid creating a separate copy (of the function) for each Sensor instance.
117 var eventProcessing = function() {
118 var err, boundary;
119
120 err = null;
121
122 // For digital sensors, skip the analog
123 // noise filtering provided below.
124 if (opts.type === "digital") {
125 this.emit("data", raw);
126
127 /* istanbul ignore else */
128 if (last !== raw) {
129 this.emit("change", raw);
130 last = raw;
131 }
132 return;
133 }
134
135 // Keep the previous calculated value if there were no new readings
136 if (samples.length > 0) {
137 // Filter the accumulated sample values to reduce analog reading noise
138 state.median = median(samples);
139 }
140
141 var roundMedian = Math.round(state.median);
142
143 this.emit("data", roundMedian);
144
145 // If the filtered (state.median) value for this interval is at least ± the
146 // configured threshold from last, fire change events
147 if (state.median <= (last - this.threshold) || state.median >= (last + this.threshold)) {
148 this.emit("change", roundMedian);
149 // Update the instance-local `last` value (only) when a new change event
150 // has been emitted. For comparison in the next interval
151 last = state.median;
152 }
153
154 if (this.limit) {
155 if (state.median <= this.limit[0]) {
156 boundary = "lower";
157 }
158 if (state.median >= this.limit[1]) {
159 boundary = "upper";
160 }
161
162 if (boundary) {
163 this.emit("limit", {
164 boundary: boundary,
165 value: roundMedian
166 });
167 this.emit("limit:" + boundary, roundMedian);
168 }
169 }
170
171 // Reset samples
172 samples.length = 0;
173 }.bind(this); // ./function eventProcessing()
174
175
176 Object.defineProperties(this, {
177 raw: {
178 get: function() {
179 return raw;
180 }
181 },
182 analog: {
183 get: function() {
184 if (opts.type === "digital") {
185 return raw;
186 }
187
188 return raw === null ? 0 :
189 Fn.map(this.raw, 0, resolution, 0, 255) | 0;
190 },
191 },
192 constrained: {
193 get: function() {
194 if (opts.type === "digital") {
195 return raw;
196 }
197
198 return raw === null ? 0 :
199 Fn.constrain(this.raw, 0, 255);
200 }
201 },
202 boolean: {
203 get: function() {
204 var state = priv.get(this);
205 var booleanBarrier = state.booleanBarrier;
206 var scale = state.scale || [0, resolution];
207
208 if (booleanBarrier === null) {
209 booleanBarrier = scale[0] + (scale[1] - scale[0]) / 2;
210 }
211
212 return this.value > booleanBarrier;
213 }
214 },
215 scaled: {
216 get: function() {
217 var mapped, constrain;
218
219 if (state.scale && raw !== null) {
220 if (opts.type === "digital") {
221 // Value is either 0 or 1, use as an index
222 // to return the scaled value.
223 return state.scale[raw];
224 }
225
226 mapped = Fn.fmap(raw, this.range[0], this.range[1], state.scale[0], state.scale[1]);
227 constrain = Fn.constrain(mapped, state.scale[0], state.scale[1]);
228
229 return constrain;
230 }
231 return this.constrained;
232 }
233 },
234 freq: {
235 get: function() {
236 return state.freq;
237 },
238 set: function(newFreq) {
239 state.freq = newFreq;
240 if (state.intervalId) {
241 clearInterval(state.intervalId);
242 }
243
244 if (state.freq !== null) {
245 state.intervalId = setInterval(eventProcessing, newFreq);
246 }
247 }
248 },
249 value: {
250 get: function() {
251 if (state.scale) {
252 this.isScaled = true;
253 return this.scaled;
254 }
255
256 return raw;
257 }
258 },
259 resolution: {
260 get: function() {
261 return resolution;
262 }
263 }
264 });
265
266 /* istanbul ignore else */
267 if (!!process.env.IS_TEST_MODE) {
268 Object.defineProperties(this, {
269 state: {
270 get: function() {
271 return priv.get(this);
272 }
273 }
274 });
275 }
276
277 // Set the freq property only after the get and set functions are defined
278 // and only if the sensor is not `enabled: false`
279 if (state.enabled) {
280 this.freq = state.freq;
281 }
282}
283
284util.inherits(Sensor, events.EventEmitter);
285
286/**
287 * EXPERIMENTAL
288 *
289 * within When value is within the provided range, execute callback
290 *
291 * @param {Number} range Upperbound, converted into an array,
292 * where 0 is lowerbound
293 * @param {Function} callback Callback to execute when value falls inside range
294 * @return {Object} instance
295 *
296 *
297 * @param {Array} range Lower to Upper bounds [ low, high ]
298 * @param {Function} callback Callback to execute when value falls inside range
299 * @return {Object} instance
300 *
301 */
302Object.assign(Sensor.prototype, within);
303
304/**
305 * enable Enable a disabled sensor.
306 *
307 * @return {Object} instance
308 *
309 */
310Sensor.prototype.enable = function() {
311 var state = priv.get(this);
312
313 /* istanbul ignore else */
314 if (!state.enabled) {
315 this.freq = state.freq || state.previousFreq;
316 }
317
318 return this;
319};
320
321/**
322 * disable Disable an enabled sensor.
323 *
324 * @return {Object} instance
325 *
326 */
327Sensor.prototype.disable = function() {
328 var state = priv.get(this);
329
330 /* istanbul ignore else */
331 if (state.enabled) {
332 state.enabled = false;
333 state.previousFreq = state.freq;
334 this.freq = null;
335 }
336
337 return this;
338};
339
340/**
341 * scale/scaleTo Set a value scaling range
342 *
343 * @param {Number} low Lowerbound
344 * @param {Number} high Upperbound
345 * @return {Object} instance
346 *
347 * @param {Array} [ low, high] Lowerbound
348 * @return {Object} instance
349 *
350 */
351Sensor.prototype.scale = function(low, high) {
352 this.isScaled = true;
353
354 priv.get(this).scale = Array.isArray(low) ?
355 low : [low, high];
356
357 return this;
358};
359
360/**
361 * scaleTo Scales value to integer representation
362 * @param {Number} low An array containing a lower and upper bound
363 *
364 * @param {Number} low A number to use as a lower bound
365 * @param {Number} high A number to use as an upper bound
366 * @return {Number} The scaled value
367 */
368Sensor.prototype.scaleTo = function(low, high) {
369 var scale = Array.isArray(low) ? low : [low, high];
370 return Fn.map(this.raw, 0, this.resolution, scale[0], scale[1]);
371};
372
373/**
374 * fscaleTo Scales value to single precision float representation
375 * @param {Number} low An array containing a lower and upper bound
376 *
377 * @param {Number} low A number to use as a lower bound
378 * @param {Number} high A number to use as an upper bound
379 * @return {Number} The scaled value
380 */
381Sensor.prototype.fscaleTo = function(low, high) {
382 var scale = Array.isArray(low) ? low : [low, high];
383 return Fn.fmap(this.raw, 0, this.resolution, scale[0], scale[1]);
384};
385
386/**
387 * booleanAt Set a midpoint barrier value used to calculate returned value of
388 * .boolean property.
389 *
390 * @param {Number} barrier
391 * @return {Object} instance
392 *
393 */
394Sensor.prototype.booleanAt = function(barrier) {
395 priv.get(this).booleanBarrier = barrier;
396 return this;
397};
398
399
400
401/**
402 * Sensors()
403 * new Sensors()
404 *
405 * Constructs an Array-like instance of all servos
406 */
407function Sensors(numsOrObjects) {
408 if (!(this instanceof Sensors)) {
409 return new Sensors(numsOrObjects);
410 }
411
412 Object.defineProperty(this, "type", {
413 value: Sensor
414 });
415
416 Collection.Emitter.call(this, numsOrObjects);
417}
418
419util.inherits(Sensors, Collection.Emitter);
420
421
422Collection.installMethodForwarding(
423 Sensors.prototype, Sensor.prototype
424);
425
426// Assign Sensors Collection class as static "method" of Sensor.
427Sensor.Collection = Sensors;
428
429/* istanbul ignore else */
430if (!!process.env.IS_TEST_MODE) {
431 Sensor.purge = function() {
432 priv.clear();
433 };
434}
435
436
437module.exports = Sensor;