UNPKG

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