1 | const Board = require("./board");
|
2 | const Collection = require("./mixins/collection");
|
3 | const Fn = require("./fn");
|
4 | const Withinable = require("./mixins/within");
|
5 |
|
6 |
|
7 | const priv = new Map();
|
8 |
|
9 |
|
10 |
|
11 | function median(input) {
|
12 |
|
13 | const sorted = input.sort((a, b) => a - b);
|
14 | const len = sorted.length;
|
15 | const half = Math.floor(len / 2);
|
16 |
|
17 |
|
18 |
|
19 | return len % 2 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
|
20 | }
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | class Sensor extends Withinable {
|
32 | constructor(options) {
|
33 |
|
34 | super();
|
35 |
|
36 |
|
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 |
|
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 |
|
64 |
|
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 |
|
76 | priv.set(this, state);
|
77 |
|
78 |
|
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 |
|
88 | if (options.type !== "digital") {
|
89 | samples.push(raw);
|
90 | }
|
91 | });
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | const eventProcessing = () => {
|
98 | let err;
|
99 | let boundary;
|
100 |
|
101 | err = null;
|
102 |
|
103 |
|
104 |
|
105 | if (options.type === "digital") {
|
106 | this.emit("data", raw);
|
107 |
|
108 |
|
109 | if (last !== raw) {
|
110 | this.emit("change", raw);
|
111 | last = raw;
|
112 | }
|
113 | return;
|
114 | }
|
115 |
|
116 |
|
117 | if (samples.length > 0) {
|
118 |
|
119 | state.median = median(samples);
|
120 | }
|
121 |
|
122 | const roundMedian = Math.round(state.median);
|
123 |
|
124 | this.emit("data", roundMedian);
|
125 |
|
126 |
|
127 |
|
128 | if (state.median <= (last - this.threshold) || state.median >= (last + this.threshold)) {
|
129 | this.emit("change", roundMedian);
|
130 |
|
131 |
|
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 |
|
153 | samples.length = 0;
|
154 | };
|
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 |
|
204 |
|
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 |
|
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 |
|
260 |
|
261 | if (state.enabled) {
|
262 | this.freq = state.freq;
|
263 | }
|
264 | }
|
265 |
|
266 | |
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 | enable() {
|
273 | const state = priv.get(this);
|
274 |
|
275 |
|
276 | if (!state.enabled) {
|
277 | this.freq = state.freq || state.previousFreq;
|
278 | }
|
279 |
|
280 | return this;
|
281 | }
|
282 |
|
283 | |
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 | disable() {
|
290 | const state = priv.get(this);
|
291 |
|
292 |
|
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 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
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 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
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 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
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 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 | booleanAt(barrier) {
|
357 | priv.get(this).booleanBarrier = barrier;
|
358 | return this;
|
359 | }
|
360 | }
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 | class Sensors extends Collection.Emitter {
|
369 | constructor(numsOrObjects) {
|
370 | super(numsOrObjects);
|
371 | }
|
372 | get type() {
|
373 | return Sensor;
|
374 | }
|
375 | }
|
376 |
|
377 | Collection.installMethodForwarding(
|
378 | Sensors.prototype, Sensor.prototype
|
379 | );
|
380 |
|
381 |
|
382 | Sensor.Collection = Sensors;
|
383 |
|
384 |
|
385 | if (!!process.env.IS_TEST_MODE) {
|
386 | Sensor.purge = () => {
|
387 | priv.clear();
|
388 | };
|
389 | }
|
390 |
|
391 |
|
392 | module.exports = Sensor;
|