johnny-five
Version:
The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!
697 lines (604 loc) • 18.9 kB
JavaScript
const Board = require("./board");
const Emitter = require("./mixins/emitter");
const Fn = require("./fn");
const { scale, toFixed, uint16 } = Fn;
const priv = new Map();
const aliases = {
down: ["down", "press", "tap", "impact", "hit", "touch"],
up: ["up", "release"],
hold: ["hold"]
};
function flatten(array) {
return array.flat ?
array.flat() :
array.reduce((accum, val) => accum.concat(val), []);
}
function flatKeys(options) {
let keys = [];
if (options.keys && Array.isArray(options.keys)) {
keys = options.keys.slice();
if (keys.every(Array.isArray)) {
keys = flatten(keys);
}
}
return keys;
}
const Controllers = {
MPR121: {
ADDRESSES: {
value: [0x5A, 0x5B, 0x5C, 0x5D]
},
REGISTER: {
value: require("./definitions/mpr121.js")
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
const keyMap = this.REGISTER.MAPS[options.controller].KEYS;
const targets = this.REGISTER.MAPS[options.controller].TARGETS;
const mapping = Object.keys(keyMap).reduce((accum, index) => {
accum[index] = keyMap[index];
return accum;
}, []);
let keys = flatKeys(options);
const length = mapping.length;
this.io.i2cConfig(options);
this.io.i2cWrite(address, this.REGISTER.MPR121_SOFTRESET, 0x63);
this.io.i2cWrite(address, this.REGISTER.MHD_RISING, 0x01);
this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_RISING, 0x01);
this.io.i2cWrite(address, this.REGISTER.NCL_RISING, 0x00);
this.io.i2cWrite(address, this.REGISTER.FDL_RISING, 0x00);
this.io.i2cWrite(address, this.REGISTER.MHD_FALLING, 0x01);
this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_FALLING, 0x01);
this.io.i2cWrite(address, this.REGISTER.NCL_FALLING, 0xFF);
this.io.i2cWrite(address, this.REGISTER.FDL_FALLING, 0x02);
// Page 12
// 6. Touch and Release Threshold (0x41~0x5A)
// The threshold is defined as a deviation value from the baseline value,
// so it remains constant even baseline value changes. Typically the touch
// threshold is a little bigger than the release threshold to touch debounce
// and hysteresis. The range of the value is 0~255. For typical touch
// application, the value can be in range 0x05~0x30 for example. The setting
// of the threshold is depended on the actual application. For the operation
// details and how to set the threshold refer to application note AN3892 and
// MPR121 design guidelines.
this.sensitivity = {
// Inverted map approximately to 8 bit values:
//
// press: 12
// release: 6
//
press: Array(12).fill(0.95),
release: Array(12).fill(0.975),
// These defaults as based on the defaults shown
// in examples published by Adafruit
// https://github.com/adafruit/Adafruit_MPR121/blob/master/Adafruit_MPR121.cpp#L43
};
// If keys were specified for a MPR121_SHIELD (adafruit shield),
// then reverse the keys to align with the output of the.
if (options.keys && options.controller === "MPR121_SHIELD") {
keys = keys.reverse();
}
if (options.sensitivity) {
if (Array.isArray(options.sensitivity)) {
// Initialized as:
//
// new five.Keypad({
// controller: "MPR121",
// sensitivity: [
// { press: 0-1, release: 0-1, },
// { press: 0-1, release: 0-1, },
// { press: 0-1, release: 0-1, },
// ...
// ],
// });
//
options.sensitivity.forEach(function({press, release}, index) {
if (typeof press !== "undefined") {
this.sensitivity.press[index] = press;
}
if (typeof release !== "undefined") {
this.sensitivity.release[index] = release;
}
}, this);
} else {
// Initialized as:
//
// new five.Keypad({
// controller: "MPR121",
// sensitivity: {
// press: 0-1,
// release: 0-1,
// },
// });
//
if (typeof options.sensitivity.press !== "undefined") {
this.sensitivity.press.fill(options.sensitivity.press);
}
if (typeof options.sensitivity.release !== "undefined") {
this.sensitivity.release.fill(options.sensitivity.release);
}
}
}
// The chip expects a LOWER value for a HIGHER sensitivity.
// Most people don't think this way, so Johnny-Five aligns with
// user/developer intuition, which we assume for this case is:
//
// "Higher sensitivity value means greater touch sensitivity"
//
// This means that the value we received needs to be inverted
// before it's written to the chip threshold configuration.
//
for (let i = 0; i < 12; i++) {
this.io.i2cWrite(
address,
this.REGISTER.ELE0_TOUCH_THRESHOLD + (i << 1),
scale(toFixed(1 - this.sensitivity.press[i], 3), 0, 1, 0, 255)
);
this.io.i2cWrite(
address,
this.REGISTER.ELE0_RELEASE_THRESHOLD + (i << 1),
scale(toFixed(1 - this.sensitivity.release[i], 3), 0, 1, 0, 255)
);
}
this.io.i2cWrite(address, this.REGISTER.FILTER_CONFIG, 0x13);
this.io.i2cWrite(address, this.REGISTER.AFE_CONFIGURATION, 0x80);
this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_CONTROL_0, 0x8F);
this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_USL, 0xE4);
this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_LSL, 0x94);
this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_TARGET_LEVEL, 0xCD);
this.io.i2cWrite(address, this.REGISTER.ELECTRODE_CONFIG, 0xCC);
if (!keys.length) {
keys = Array.from(Object.assign({}, keyMap, {
length
}));
}
state.length = length;
state.touches = touches(length);
state.keys = keys;
state.mapping = mapping;
state.targets = targets;
state.isMultitouch = true;
this.io.i2cRead(address, 0x00, 2, bytes => callback(uint16(bytes[1], bytes[0])));
}
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const indices = [];
for (let i = 0; i < 12; i++) {
if (raw & (1 << i)) {
indices.push(state.targets[raw & (1 << i)]);
}
}
return indices;
}
},
},
// https://learn.sparkfun.com/tutorials/vkey-voltage-keypad-hookup-guide
VKEY: {
initialize: {
value(options, callback) {
const state = priv.get(this);
const aref = options.aref || this.io.aref || 5;
const use5V = Fn.inRange(aref, 4.5, 5.5);
const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
let keys = flatKeys(options);
let length = 0;
if (!keys.length) {
keys = mapping;
}
state.scale = [
use5V ? 17 : 26,
use5V ? 40 : 58,
use5V ? 496 : 721,
];
length = mapping.length;
state.length = length;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = false;
this.io.pinMode(this.pin, this.io.MODES.ANALOG);
this.io.analogRead(this.pin, adc => callback(adc));
},
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const length = state.length;
const low = state.scale[0];
const step = state.scale[1];
const high = state.scale[2];
if (raw < low || raw > high) {
return [];
}
return [(length - ((raw - low) / step)) | 0];
}
}
},
// WaveShare AD
// - http://www.amazon.com/WaveShare-Accessory-buttons-controlled-keyboard/dp/B00KM6UXVS
// - http://www.wvshare.com/product/A_D-Keypad.htm
//
// TODO: Create docs to show how to create a DIY keypad
// that works with this class.
//
ANALOG: {
initialize: {
value(options, callback) {
let keys = flatKeys(options);
let mapping = [];
let length = 0;
if (options.length && !keys.length) {
keys = Array.from({
length: options.length
}, (_, key) => key);
}
if (!keys.length) {
throw new Error(
"Missing `keys`. Analog Keypad requires either a numeric `length` or a `keys` array."
);
}
mapping = keys;
length = mapping.length;
const state = priv.get(this);
// keys + Idle state == length + 1
const total = length + 1;
const vrange = Math.round(1023 / total);
const ranges = Array.from({
length: total
}, (_, index) => {
const start = vrange * index;
return Array.from({
length: vrange - 1
}, (_, index) => start + index);
});
state.length = length;
state.ranges = ranges;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = true;
this.io.pinMode(this.pin, this.io.MODES.ANALOG);
this.io.analogRead(this.pin, adc => callback(adc));
}
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const ranges = state.ranges;
let index = ranges.findIndex(range => range.includes(raw));
if (index === state.length) {
index--;
}
if (index < 0) {
return [];
}
return [index];
}
}
},
AT42QT1070: {
ADDRESSES: {
value: [0x1B]
},
REGISTER: {
value: {
READ: 0x03
}
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
const mapping = [0, 1, 2, 3, 4, 5, 6];
let keys = flatKeys(options);
let length = 0;
if (!keys.length) {
keys = mapping;
}
length = mapping.length;
state.length = length;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = true;
this.io.i2cConfig(options);
this.io.i2cRead(address, this.REGISTER.READ, 1, data => callback(data[0]));
}
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const indices = [];
for (let i = 0; i < 7; i++) {
if (raw & (1 << i)) {
indices.push(i);
}
}
return indices;
}
}
},
"3X4_I2C_NANO_BACKPACK": {
ADDRESSES: {
value: [0x0A, 0x0B, 0x0C, 0x0D]
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
let keys = flatKeys(options);
let length = 0;
if (!keys.length) {
keys = mapping;
}
length = mapping.length;
state.length = length;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = true;
this.io.i2cConfig(options);
this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
}
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const indices = [];
for (let i = 0; i < state.length; i++) {
if (raw & (1 << i)) {
indices.push(i);
}
}
return indices;
}
}
},
"4X4_I2C_NANO_BACKPACK": {
ADDRESSES: {
value: [0x0A, 0x0B, 0x0C, 0x0D]
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
let keys = flatKeys(options);
const mapping = [1, 2, 3, "A", 4, 5, 6, "B", 7, 8, 9, "C", "*", 0, "#", "D"];
let length = 0;
if (!keys.length) {
keys = mapping;
}
length = mapping.length;
state.length = length;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = true;
this.io.i2cConfig(options);
this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
}
},
toAlias: {
value(index) {
return priv.get(this).keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const indices = [];
for (let i = 0; i < state.length; i++) {
if (raw & (1 << i)) {
indices.push(i);
}
}
return indices;
}
}
},
SX1509: {
ADDRESSES: {
value: [0x0A, 0x0B, 0x0C, 0x0D]
},
REGISTER: {
value: {
PULLUP: 0x03,
OPEN_DRAIN: 0x05,
DIR: 0x07,
DIR_B: 0x0E,
DIR_A: 0x0F,
// OPEN_DRAIN_B: 0x0E,
// OPEN_DRAIN_A: 0x0F,
},
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
let keys = flatKeys(options);
const mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
let length = 0;
if (!keys.length) {
keys = mapping;
}
length = mapping.length;
state.length = length;
state.touches = touches(length);
state.mapping = mapping;
state.keys = keys;
state.isMultitouch = true;
this.io.i2cConfig(options);
this.io.i2cWriteReg(address, this.REGISTER.DIR, 0xF0);
this.io.i2cWriteReg(address, this.REGISTER.OPEN_DRAIN, 0x0F);
this.io.i2cWriteReg(address, this.REGISTER.PULLUP, 0xF0);
this.io.i2cRead(address, 2, bytes => callback(uint16(bytes[0], bytes[1])));
}
},
toAlias: {
value(index) {
const state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value(raw) {
const state = priv.get(this);
const indices = [];
for (let i = 0; i < state.length; i++) {
if (raw & (1 << i)) {
indices.push(i);
}
}
return indices;
}
}
},
};
// Otherwise known as...
Controllers.MPR121QR2 = Controllers.MPR121;
Controllers.MPR121QR2_SHIELD = Controllers.MPR121;
Controllers.MPR121_KEYPAD = Controllers.MPR121;
Controllers.MPR121_SHIELD = Controllers.MPR121;
Controllers.QTOUCH = Controllers.AT42QT1070;
Controllers.DEFAULT = Controllers.ANALOG;
function touches(length) {
return Array.from({ length }, () => ({
timeout: null,
value: 0
}));
}
class Keypad extends Emitter {
constructor(options) {
super();
// Initialize a Device instance on a Board
Board.Component.call(
this, options = Board.Options(options)
);
let raw = null;
const state = {
touches: null,
timeout: null,
length: null,
keys: null,
mapping: null,
holdtime: null,
};
const trigger = Fn.debounce(function(type, value) {
const event = {
type,
which: value,
timestamp: Date.now()
};
aliases[type].forEach(function(type) {
this.emit(type, event);
}, this);
this.emit("change", Object.assign({}, event));
}, 5);
Board.Controller.call(this, Controllers, options);
state.holdtime = options.holdtime ? options.holdtime : 500;
priv.set(this, state);
if (typeof this.initialize === "function") {
this.initialize(options, data => {
raw = data;
const now = Date.now();
const indices = this.toIndices(data);
const kLength = state.length;
const lists = {
down: [],
hold: [],
up: [],
};
let target = null;
let alias = null;
for (let k = 0; k < kLength; k++) {
alias = this.toAlias(k);
if (indices.includes(k)) {
if (state.touches[k].value === 0) {
state.touches[k].timeout = now + state.holdtime;
lists.down.push(alias);
} else if (state.touches[k].value === 1) {
if (state.touches[k].timeout !== null && now > state.touches[k].timeout) {
state.touches[k].timeout = now + state.holdtime;
lists.hold.push(alias);
}
}
state.touches[k].value = 1;
} else {
if (state.touches[k].value === 1) {
state.touches[k].timeout = null;
lists.up.push(alias);
}
state.touches[k].value = 0;
}
target = null;
alias = null;
}
Object.keys(lists).forEach(function(key) {
const list = lists[key];
if (list.length) {
trigger.call(this, key, list);
}
}, this);
});
}
Object.defineProperties(this, {
isMultitouch: {
get() {
return state.isMultitouch;
}
},
value: {
get() {
return raw;
}
},
});
}
}
/* istanbul ignore else */
if (!!process.env.IS_TEST_MODE) {
Keypad.Controllers = Controllers;
Keypad.purge = () => {
priv.clear();
};
}
module.exports = Keypad;