1 | const Board = require("./board");
|
2 | const Emitter = require("./mixins/emitter");
|
3 | const Fn = require("./fn");
|
4 | const { scale, toFixed, uint16 } = Fn;
|
5 | const priv = new Map();
|
6 |
|
7 | const aliases = {
|
8 | down: ["down", "press", "tap", "impact", "hit", "touch"],
|
9 | up: ["up", "release"],
|
10 | hold: ["hold"]
|
11 | };
|
12 |
|
13 | function flatten(array) {
|
14 | return array.flat ?
|
15 | array.flat() :
|
16 | array.reduce((accum, val) => accum.concat(val), []);
|
17 | }
|
18 |
|
19 | function flatKeys(options) {
|
20 | let keys = [];
|
21 |
|
22 | if (options.keys && Array.isArray(options.keys)) {
|
23 | keys = options.keys.slice();
|
24 |
|
25 | if (keys.every(Array.isArray)) {
|
26 | keys = flatten(keys);
|
27 | }
|
28 | }
|
29 |
|
30 | return keys;
|
31 | }
|
32 |
|
33 | const Controllers = {
|
34 | MPR121: {
|
35 | ADDRESSES: {
|
36 | value: [0x5A, 0x5B, 0x5C, 0x5D]
|
37 | },
|
38 | REGISTER: {
|
39 | value: require("./definitions/mpr121.js")
|
40 | },
|
41 | initialize: {
|
42 | value(options, callback) {
|
43 | const { Drivers } = require("./sip");
|
44 | const address = Drivers.addressResolver(this, options);
|
45 | const state = priv.get(this);
|
46 | const keyMap = this.REGISTER.MAPS[options.controller].KEYS;
|
47 | const targets = this.REGISTER.MAPS[options.controller].TARGETS;
|
48 | const mapping = Object.keys(keyMap).reduce((accum, index) => {
|
49 | accum[index] = keyMap[index];
|
50 | return accum;
|
51 | }, []);
|
52 | let keys = flatKeys(options);
|
53 |
|
54 | const length = mapping.length;
|
55 |
|
56 | this.io.i2cConfig(options);
|
57 |
|
58 | this.io.i2cWrite(address, this.REGISTER.MPR121_SOFTRESET, 0x63);
|
59 |
|
60 | this.io.i2cWrite(address, this.REGISTER.MHD_RISING, 0x01);
|
61 | this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_RISING, 0x01);
|
62 | this.io.i2cWrite(address, this.REGISTER.NCL_RISING, 0x00);
|
63 | this.io.i2cWrite(address, this.REGISTER.FDL_RISING, 0x00);
|
64 |
|
65 | this.io.i2cWrite(address, this.REGISTER.MHD_FALLING, 0x01);
|
66 | this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_FALLING, 0x01);
|
67 | this.io.i2cWrite(address, this.REGISTER.NCL_FALLING, 0xFF);
|
68 | this.io.i2cWrite(address, this.REGISTER.FDL_FALLING, 0x02);
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | this.sensitivity = {
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | press: Array(12).fill(0.95),
|
88 | release: Array(12).fill(0.975),
|
89 |
|
90 |
|
91 |
|
92 | };
|
93 |
|
94 |
|
95 |
|
96 | if (options.keys && options.controller === "MPR121_SHIELD") {
|
97 | keys = keys.reverse();
|
98 | }
|
99 |
|
100 | if (options.sensitivity) {
|
101 | if (Array.isArray(options.sensitivity)) {
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | options.sensitivity.forEach(function({press, release}, index) {
|
115 | if (typeof press !== "undefined") {
|
116 | this.sensitivity.press[index] = press;
|
117 | }
|
118 |
|
119 | if (typeof release !== "undefined") {
|
120 | this.sensitivity.release[index] = release;
|
121 | }
|
122 | }, this);
|
123 | } else {
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | if (typeof options.sensitivity.press !== "undefined") {
|
135 | this.sensitivity.press.fill(options.sensitivity.press);
|
136 | }
|
137 |
|
138 | if (typeof options.sensitivity.release !== "undefined") {
|
139 | this.sensitivity.release.fill(options.sensitivity.release);
|
140 | }
|
141 | }
|
142 | }
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | for (let i = 0; i < 12; i++) {
|
154 | this.io.i2cWrite(
|
155 | address,
|
156 | this.REGISTER.ELE0_TOUCH_THRESHOLD + (i << 1),
|
157 | scale(toFixed(1 - this.sensitivity.press[i], 3), 0, 1, 0, 255)
|
158 | );
|
159 | this.io.i2cWrite(
|
160 | address,
|
161 | this.REGISTER.ELE0_RELEASE_THRESHOLD + (i << 1),
|
162 | scale(toFixed(1 - this.sensitivity.release[i], 3), 0, 1, 0, 255)
|
163 | );
|
164 | }
|
165 |
|
166 | this.io.i2cWrite(address, this.REGISTER.FILTER_CONFIG, 0x13);
|
167 | this.io.i2cWrite(address, this.REGISTER.AFE_CONFIGURATION, 0x80);
|
168 |
|
169 | this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_CONTROL_0, 0x8F);
|
170 | this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_USL, 0xE4);
|
171 | this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_LSL, 0x94);
|
172 | this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_TARGET_LEVEL, 0xCD);
|
173 |
|
174 | this.io.i2cWrite(address, this.REGISTER.ELECTRODE_CONFIG, 0xCC);
|
175 |
|
176 | if (!keys.length) {
|
177 | keys = Array.from(Object.assign({}, keyMap, {
|
178 | length
|
179 | }));
|
180 | }
|
181 |
|
182 | state.length = length;
|
183 | state.touches = touches(length);
|
184 | state.keys = keys;
|
185 | state.mapping = mapping;
|
186 | state.targets = targets;
|
187 | state.isMultitouch = true;
|
188 |
|
189 | this.io.i2cRead(address, 0x00, 2, bytes => callback(uint16(bytes[1], bytes[0])));
|
190 | }
|
191 | },
|
192 | toAlias: {
|
193 | value(index) {
|
194 | const state = priv.get(this);
|
195 | return state.keys[index];
|
196 | }
|
197 | },
|
198 | toIndices: {
|
199 | value(raw) {
|
200 | const state = priv.get(this);
|
201 | const indices = [];
|
202 | for (let i = 0; i < 12; i++) {
|
203 | if (raw & (1 << i)) {
|
204 | indices.push(state.targets[raw & (1 << i)]);
|
205 | }
|
206 | }
|
207 | return indices;
|
208 | }
|
209 | },
|
210 | },
|
211 |
|
212 |
|
213 | VKEY: {
|
214 | initialize: {
|
215 | value(options, callback) {
|
216 | const state = priv.get(this);
|
217 | const aref = options.aref || this.io.aref || 5;
|
218 | const use5V = Fn.inRange(aref, 4.5, 5.5);
|
219 | const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
220 | let keys = flatKeys(options);
|
221 | let length = 0;
|
222 |
|
223 | if (!keys.length) {
|
224 | keys = mapping;
|
225 | }
|
226 |
|
227 | state.scale = [
|
228 | use5V ? 17 : 26,
|
229 | use5V ? 40 : 58,
|
230 | use5V ? 496 : 721,
|
231 | ];
|
232 |
|
233 | length = mapping.length;
|
234 | state.length = length;
|
235 | state.touches = touches(length);
|
236 | state.mapping = mapping;
|
237 | state.keys = keys;
|
238 | state.isMultitouch = false;
|
239 |
|
240 | this.io.pinMode(this.pin, this.io.MODES.ANALOG);
|
241 | this.io.analogRead(this.pin, adc => callback(adc));
|
242 | },
|
243 | },
|
244 | toAlias: {
|
245 | value(index) {
|
246 | const state = priv.get(this);
|
247 | return state.keys[index];
|
248 | }
|
249 | },
|
250 | toIndices: {
|
251 | value(raw) {
|
252 | const state = priv.get(this);
|
253 | const length = state.length;
|
254 | const low = state.scale[0];
|
255 | const step = state.scale[1];
|
256 | const high = state.scale[2];
|
257 |
|
258 | if (raw < low || raw > high) {
|
259 | return [];
|
260 | }
|
261 |
|
262 | return [(length - ((raw - low) / step)) | 0];
|
263 | }
|
264 | }
|
265 | },
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | ANALOG: {
|
275 | initialize: {
|
276 | value(options, callback) {
|
277 |
|
278 | let keys = flatKeys(options);
|
279 | let mapping = [];
|
280 | let length = 0;
|
281 |
|
282 | if (options.length && !keys.length) {
|
283 | keys = Array.from({
|
284 | length: options.length
|
285 | }, (_, key) => key);
|
286 | }
|
287 |
|
288 | if (!keys.length) {
|
289 | throw new Error(
|
290 | "Missing `keys`. Analog Keypad requires either a numeric `length` or a `keys` array."
|
291 | );
|
292 | }
|
293 |
|
294 | mapping = keys;
|
295 | length = mapping.length;
|
296 |
|
297 | const state = priv.get(this);
|
298 |
|
299 | const total = length + 1;
|
300 | const vrange = Math.round(1023 / total);
|
301 | const ranges = Array.from({
|
302 | length: total
|
303 | }, (_, index) => {
|
304 | const start = vrange * index;
|
305 | return Array.from({
|
306 | length: vrange - 1
|
307 | }, (_, index) => start + index);
|
308 | });
|
309 |
|
310 | state.length = length;
|
311 | state.ranges = ranges;
|
312 | state.touches = touches(length);
|
313 | state.mapping = mapping;
|
314 | state.keys = keys;
|
315 | state.isMultitouch = true;
|
316 |
|
317 | this.io.pinMode(this.pin, this.io.MODES.ANALOG);
|
318 | this.io.analogRead(this.pin, adc => callback(adc));
|
319 | }
|
320 | },
|
321 | toAlias: {
|
322 | value(index) {
|
323 | const state = priv.get(this);
|
324 | return state.keys[index];
|
325 | }
|
326 | },
|
327 | toIndices: {
|
328 | value(raw) {
|
329 | const state = priv.get(this);
|
330 | const ranges = state.ranges;
|
331 | let index = ranges.findIndex(range => range.includes(raw));
|
332 |
|
333 | if (index === state.length) {
|
334 | index--;
|
335 | }
|
336 |
|
337 | if (index < 0) {
|
338 | return [];
|
339 | }
|
340 |
|
341 | return [index];
|
342 | }
|
343 | }
|
344 | },
|
345 | AT42QT1070: {
|
346 | ADDRESSES: {
|
347 | value: [0x1B]
|
348 | },
|
349 | REGISTER: {
|
350 | value: {
|
351 | READ: 0x03
|
352 | }
|
353 | },
|
354 | initialize: {
|
355 | value(options, callback) {
|
356 | const { Drivers } = require("./sip");
|
357 | const address = Drivers.addressResolver(this, options);
|
358 | const state = priv.get(this);
|
359 | const mapping = [0, 1, 2, 3, 4, 5, 6];
|
360 | let keys = flatKeys(options);
|
361 | let length = 0;
|
362 |
|
363 | if (!keys.length) {
|
364 | keys = mapping;
|
365 | }
|
366 |
|
367 | length = mapping.length;
|
368 |
|
369 | state.length = length;
|
370 | state.touches = touches(length);
|
371 | state.mapping = mapping;
|
372 | state.keys = keys;
|
373 | state.isMultitouch = true;
|
374 |
|
375 | this.io.i2cConfig(options);
|
376 | this.io.i2cRead(address, this.REGISTER.READ, 1, data => callback(data[0]));
|
377 | }
|
378 | },
|
379 | toAlias: {
|
380 | value(index) {
|
381 | const state = priv.get(this);
|
382 | return state.keys[index];
|
383 | }
|
384 | },
|
385 | toIndices: {
|
386 | value(raw) {
|
387 | const indices = [];
|
388 | for (let i = 0; i < 7; i++) {
|
389 | if (raw & (1 << i)) {
|
390 | indices.push(i);
|
391 | }
|
392 | }
|
393 | return indices;
|
394 | }
|
395 | }
|
396 | },
|
397 |
|
398 | "3X4_I2C_NANO_BACKPACK": {
|
399 | ADDRESSES: {
|
400 | value: [0x0A, 0x0B, 0x0C, 0x0D]
|
401 | },
|
402 | initialize: {
|
403 | value(options, callback) {
|
404 | const { Drivers } = require("./sip");
|
405 | const address = Drivers.addressResolver(this, options);
|
406 | const state = priv.get(this);
|
407 | const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
|
408 | let keys = flatKeys(options);
|
409 | let length = 0;
|
410 |
|
411 | if (!keys.length) {
|
412 | keys = mapping;
|
413 | }
|
414 |
|
415 | length = mapping.length;
|
416 |
|
417 | state.length = length;
|
418 | state.touches = touches(length);
|
419 | state.mapping = mapping;
|
420 | state.keys = keys;
|
421 | state.isMultitouch = true;
|
422 |
|
423 |
|
424 | this.io.i2cConfig(options);
|
425 | this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
|
426 | }
|
427 | },
|
428 | toAlias: {
|
429 | value(index) {
|
430 | const state = priv.get(this);
|
431 | return state.keys[index];
|
432 | }
|
433 | },
|
434 | toIndices: {
|
435 | value(raw) {
|
436 | const state = priv.get(this);
|
437 | const indices = [];
|
438 | for (let i = 0; i < state.length; i++) {
|
439 | if (raw & (1 << i)) {
|
440 | indices.push(i);
|
441 | }
|
442 | }
|
443 | return indices;
|
444 | }
|
445 | }
|
446 | },
|
447 | "4X4_I2C_NANO_BACKPACK": {
|
448 | ADDRESSES: {
|
449 | value: [0x0A, 0x0B, 0x0C, 0x0D]
|
450 | },
|
451 | initialize: {
|
452 | value(options, callback) {
|
453 | const { Drivers } = require("./sip");
|
454 | const address = Drivers.addressResolver(this, options);
|
455 | const state = priv.get(this);
|
456 | let keys = flatKeys(options);
|
457 | const mapping = [1, 2, 3, "A", 4, 5, 6, "B", 7, 8, 9, "C", "*", 0, "#", "D"];
|
458 | let length = 0;
|
459 |
|
460 | if (!keys.length) {
|
461 | keys = mapping;
|
462 | }
|
463 |
|
464 | length = mapping.length;
|
465 |
|
466 | state.length = length;
|
467 | state.touches = touches(length);
|
468 | state.mapping = mapping;
|
469 | state.keys = keys;
|
470 | state.isMultitouch = true;
|
471 |
|
472 |
|
473 | this.io.i2cConfig(options);
|
474 | this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
|
475 | }
|
476 | },
|
477 | toAlias: {
|
478 | value(index) {
|
479 | return priv.get(this).keys[index];
|
480 | }
|
481 | },
|
482 | toIndices: {
|
483 | value(raw) {
|
484 | const state = priv.get(this);
|
485 | const indices = [];
|
486 | for (let i = 0; i < state.length; i++) {
|
487 | if (raw & (1 << i)) {
|
488 | indices.push(i);
|
489 | }
|
490 | }
|
491 | return indices;
|
492 | }
|
493 | }
|
494 | },
|
495 | SX1509: {
|
496 | ADDRESSES: {
|
497 | value: [0x0A, 0x0B, 0x0C, 0x0D]
|
498 | },
|
499 | REGISTER: {
|
500 | value: {
|
501 | PULLUP: 0x03,
|
502 | OPEN_DRAIN: 0x05,
|
503 | DIR: 0x07,
|
504 | DIR_B: 0x0E,
|
505 | DIR_A: 0x0F,
|
506 |
|
507 |
|
508 | },
|
509 | },
|
510 | initialize: {
|
511 | value(options, callback) {
|
512 | const { Drivers } = require("./sip");
|
513 | const address = Drivers.addressResolver(this, options);
|
514 | const state = priv.get(this);
|
515 | let keys = flatKeys(options);
|
516 | const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
|
517 | let length = 0;
|
518 |
|
519 | if (!keys.length) {
|
520 | keys = mapping;
|
521 | }
|
522 |
|
523 | length = mapping.length;
|
524 |
|
525 | state.length = length;
|
526 | state.touches = touches(length);
|
527 | state.mapping = mapping;
|
528 | state.keys = keys;
|
529 | state.isMultitouch = true;
|
530 |
|
531 |
|
532 | this.io.i2cConfig(options);
|
533 |
|
534 | this.io.i2cWriteReg(address, this.REGISTER.DIR, 0xF0);
|
535 | this.io.i2cWriteReg(address, this.REGISTER.OPEN_DRAIN, 0x0F);
|
536 | this.io.i2cWriteReg(address, this.REGISTER.PULLUP, 0xF0);
|
537 |
|
538 | this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
|
539 | }
|
540 | },
|
541 | toAlias: {
|
542 | value(index) {
|
543 | const state = priv.get(this);
|
544 | return state.keys[index];
|
545 | }
|
546 | },
|
547 | toIndices: {
|
548 | value(raw) {
|
549 | const state = priv.get(this);
|
550 | const indices = [];
|
551 | for (let i = 0; i < state.length; i++) {
|
552 | if (raw & (1 << i)) {
|
553 | indices.push(i);
|
554 | }
|
555 | }
|
556 | return indices;
|
557 | }
|
558 | }
|
559 | },
|
560 | };
|
561 |
|
562 |
|
563 | Controllers.MPR121QR2 = Controllers.MPR121;
|
564 | Controllers.MPR121QR2_SHIELD = Controllers.MPR121;
|
565 | Controllers.MPR121_KEYPAD = Controllers.MPR121;
|
566 | Controllers.MPR121_SHIELD = Controllers.MPR121;
|
567 | Controllers.QTOUCH = Controllers.AT42QT1070;
|
568 | Controllers.DEFAULT = Controllers.ANALOG;
|
569 |
|
570 |
|
571 | function touches(length) {
|
572 | return Array.from({ length }, () => ({
|
573 | timeout: null,
|
574 | value: 0
|
575 | }));
|
576 | }
|
577 |
|
578 | class Keypad extends Emitter {
|
579 | constructor(options) {
|
580 | super();
|
581 |
|
582 |
|
583 | Board.Component.call(
|
584 | this, options = Board.Options(options)
|
585 | );
|
586 |
|
587 | let raw = null;
|
588 | const state = {
|
589 | touches: null,
|
590 | timeout: null,
|
591 | length: null,
|
592 | keys: null,
|
593 | mapping: null,
|
594 | holdtime: null,
|
595 | };
|
596 |
|
597 | const trigger = Fn.debounce(function(type, value) {
|
598 | const event = {
|
599 | type,
|
600 | which: value,
|
601 | timestamp: Date.now()
|
602 | };
|
603 | aliases[type].forEach(function(type) {
|
604 | this.emit(type, event);
|
605 | }, this);
|
606 |
|
607 | this.emit("change", Object.assign({}, event));
|
608 | }, 5);
|
609 |
|
610 |
|
611 | Board.Controller.call(this, Controllers, options);
|
612 |
|
613 | state.holdtime = options.holdtime ? options.holdtime : 500;
|
614 |
|
615 | priv.set(this, state);
|
616 |
|
617 | if (typeof this.initialize === "function") {
|
618 | this.initialize(options, data => {
|
619 |
|
620 | raw = data;
|
621 |
|
622 | const now = Date.now();
|
623 | const indices = this.toIndices(data);
|
624 | const kLength = state.length;
|
625 |
|
626 | const lists = {
|
627 | down: [],
|
628 | hold: [],
|
629 | up: [],
|
630 | };
|
631 |
|
632 | let target = null;
|
633 | let alias = null;
|
634 |
|
635 | for (let k = 0; k < kLength; k++) {
|
636 | alias = this.toAlias(k);
|
637 |
|
638 | if (indices.includes(k)) {
|
639 | if (state.touches[k].value === 0) {
|
640 |
|
641 | state.touches[k].timeout = now + state.holdtime;
|
642 | lists.down.push(alias);
|
643 |
|
644 | } else if (state.touches[k].value === 1) {
|
645 | if (state.touches[k].timeout !== null && now > state.touches[k].timeout) {
|
646 | state.touches[k].timeout = now + state.holdtime;
|
647 | lists.hold.push(alias);
|
648 | }
|
649 | }
|
650 |
|
651 | state.touches[k].value = 1;
|
652 | } else {
|
653 | if (state.touches[k].value === 1) {
|
654 | state.touches[k].timeout = null;
|
655 | lists.up.push(alias);
|
656 | }
|
657 | state.touches[k].value = 0;
|
658 | }
|
659 | target = null;
|
660 | alias = null;
|
661 | }
|
662 |
|
663 | Object.keys(lists).forEach(function(key) {
|
664 | const list = lists[key];
|
665 |
|
666 | if (list.length) {
|
667 | trigger.call(this, key, list);
|
668 | }
|
669 | }, this);
|
670 | });
|
671 | }
|
672 |
|
673 | Object.defineProperties(this, {
|
674 | isMultitouch: {
|
675 | get() {
|
676 | return state.isMultitouch;
|
677 | }
|
678 | },
|
679 | value: {
|
680 | get() {
|
681 | return raw;
|
682 | }
|
683 | },
|
684 | });
|
685 | }
|
686 | }
|
687 |
|
688 |
|
689 | if (!!process.env.IS_TEST_MODE) {
|
690 | Keypad.Controllers = Controllers;
|
691 | Keypad.purge = () => {
|
692 | priv.clear();
|
693 | };
|
694 | }
|
695 |
|
696 | module.exports = Keypad;
|