UNPKG

18.9 kBJavaScriptView Raw
1const Board = require("./board");
2const Emitter = require("./mixins/emitter");
3const Fn = require("./fn");
4const { scale, toFixed, uint16 } = Fn;
5const priv = new Map();
6
7const aliases = {
8 down: ["down", "press", "tap", "impact", "hit", "touch"],
9 up: ["up", "release"],
10 hold: ["hold"]
11};
12
13function flatten(array) {
14 return array.flat ?
15 array.flat() :
16 array.reduce((accum, val) => accum.concat(val), []);
17}
18
19function 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
33const 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 // Page 12
71 // 6. Touch and Release Threshold (0x41~0x5A)
72 // The threshold is defined as a deviation value from the baseline value,
73 // so it remains constant even baseline value changes. Typically the touch
74 // threshold is a little bigger than the release threshold to touch debounce
75 // and hysteresis. The range of the value is 0~255. For typical touch
76 // application, the value can be in range 0x05~0x30 for example. The setting
77 // of the threshold is depended on the actual application. For the operation
78 // details and how to set the threshold refer to application note AN3892 and
79 // MPR121 design guidelines.
80
81 this.sensitivity = {
82 // Inverted map approximately to 8 bit values:
83 //
84 // press: 12
85 // release: 6
86 //
87 press: Array(12).fill(0.95),
88 release: Array(12).fill(0.975),
89 // These defaults as based on the defaults shown
90 // in examples published by Adafruit
91 // https://github.com/adafruit/Adafruit_MPR121/blob/master/Adafruit_MPR121.cpp#L43
92 };
93
94 // If keys were specified for a MPR121_SHIELD (adafruit shield),
95 // then reverse the keys to align with the output of the.
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 // Initialized as:
103 //
104 // new five.Keypad({
105 // controller: "MPR121",
106 // sensitivity: [
107 // { press: 0-1, release: 0-1, },
108 // { press: 0-1, release: 0-1, },
109 // { press: 0-1, release: 0-1, },
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 // Initialized as:
125 //
126 // new five.Keypad({
127 // controller: "MPR121",
128 // sensitivity: {
129 // press: 0-1,
130 // release: 0-1,
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 // The chip expects a LOWER value for a HIGHER sensitivity.
145 // Most people don't think this way, so Johnny-Five aligns with
146 // user/developer intuition, which we assume for this case is:
147 //
148 // "Higher sensitivity value means greater touch sensitivity"
149 //
150 // This means that the value we received needs to be inverted
151 // before it's written to the chip threshold configuration.
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 // https://learn.sparkfun.com/tutorials/vkey-voltage-keypad-hookup-guide
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 // WaveShare AD
268 // - http://www.amazon.com/WaveShare-Accessory-buttons-controlled-keyboard/dp/B00KM6UXVS
269 // - http://www.wvshare.com/product/A_D-Keypad.htm
270 //
271 // TODO: Create docs to show how to create a DIY keypad
272 // that works with this class.
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 // keys + Idle state == length + 1
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 // OPEN_DRAIN_B: 0x0E,
507 // OPEN_DRAIN_A: 0x0F,
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// Otherwise known as...
563Controllers.MPR121QR2 = Controllers.MPR121;
564Controllers.MPR121QR2_SHIELD = Controllers.MPR121;
565Controllers.MPR121_KEYPAD = Controllers.MPR121;
566Controllers.MPR121_SHIELD = Controllers.MPR121;
567Controllers.QTOUCH = Controllers.AT42QT1070;
568Controllers.DEFAULT = Controllers.ANALOG;
569
570
571function touches(length) {
572 return Array.from({ length }, () => ({
573 timeout: null,
574 value: 0
575 }));
576}
577
578class Keypad extends Emitter {
579 constructor(options) {
580 super();
581
582 // Initialize a Device instance on a Board
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/* istanbul ignore else */
689if (!!process.env.IS_TEST_MODE) {
690 Keypad.Controllers = Controllers;
691 Keypad.purge = () => {
692 priv.clear();
693 };
694}
695
696module.exports = Keypad;