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!
618 lines (532 loc) • 16.5 kB
JavaScript
var Emitter = require("events").EventEmitter;
var util = require("util");
var Board = require("./board");
var Fn = require("./fn");
var scale = Fn.scale;
var toFixed = Fn.toFixed;
var uint16 = Fn.uint16;
var priv = new Map();
var aliases = {
down: ["down", "press", "tap", "impact", "hit", "touch"],
up: ["up", "release"],
hold: ["hold"]
};
function flatKeys(opts) {
var keys = [];
if (opts.keys && Array.isArray(opts.keys)) {
keys = opts.keys.slice();
if (keys.every(Array.isArray)) {
keys = keys.reduce(function(accum, row) {
return accum.concat(row);
}, []);
}
}
return keys;
}
// TODO:
//
// Provide a mechanism for explicitly naming aliases for buttons
//
//
var Controllers = {
MPR121: {
ADDRESSES: {
value: [0x5A, 0x5B, 0x5C, 0x5D]
},
REGISTER: {
value: require("./definitions/mpr121.js")
},
initialize: {
value: function(opts, dataHandler) {
var state = priv.get(this);
var address = opts.address || this.ADDRESSES[0];
var keys = flatKeys(opts);
var keyMap = this.REGISTER.MAPS[opts.controller].KEYS;
var targets = this.REGISTER.MAPS[opts.controller].TARGETS;
var mapping = Object.keys(keyMap).reduce(function(accum, index) {
accum[index] = keyMap[index];
return accum;
}, []);
var length = mapping.length;
opts.address = address;
this.io.i2cConfig(opts);
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);
// https://www.sparkfun.com/datasheets/Components/MPR121.pdf
//
// p. 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 (opts.keys && opts.controller === "MPR121_SHIELD") {
keys = keys.reverse();
}
if (opts.sensitivity) {
if (Array.isArray(opts.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, },
// ...
// ],
// });
//
opts.sensitivity.forEach(function(sensitivity, index) {
if (typeof sensitivity.press !== "undefined") {
this.sensitivity.press[index] = sensitivity.press;
}
if (typeof sensitivity.release !== "undefined") {
this.sensitivity.release[index] = sensitivity.release;
}
}, this);
} else {
// Initialized as:
//
// new five.Keypad({
// controller: "MPR121",
// sensitivity: {
// press: 0-1,
// release: 0-1,
// },
// });
//
if (typeof opts.sensitivity.press !== "undefined") {
this.sensitivity.press.fill(opts.sensitivity.press);
}
if (typeof opts.sensitivity.release !== "undefined") {
this.sensitivity.release.fill(opts.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 (var 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: 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, function(bytes) {
dataHandler(uint16(bytes[1], bytes[0]));
});
}
},
toAlias: {
value: function(index) {
var state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value: function(raw) {
var state = priv.get(this);
var indices = [];
for (var 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: function(opts, dataHandler) {
var state = priv.get(this);
var aref = opts.aref || this.io.aref || 5;
var use5V = Fn.inRange(aref, 4.5, 5.5);
var keys = flatKeys(opts);
var mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
var 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, function(adc) {
dataHandler(adc);
}.bind(this));
},
},
toAlias: {
value: function(index) {
var state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value: function(raw) {
var state = priv.get(this);
var length = state.length;
var low = state.scale[0];
var step = state.scale[1];
var 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: function(opts, dataHandler) {
var keys = flatKeys(opts);
var mapping = [];
var length = 0;
if (opts.length && !keys.length) {
keys = Array.from({
length: opts.length
}, function(_, key) {
return 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;
var state = priv.get(this);
// keys + Idle state == length + 1
var total = length + 1;
var vrange = Math.round(1023 / total);
var ranges = Array.from({
length: total
}, function(_, index) {
var start = vrange * index;
return Array.from({
length: vrange - 1
}, function(_, index) {
return 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, function(adc) {
dataHandler(adc);
});
}
},
toAlias: {
value: function(index) {
var state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value: function(raw) {
var state = priv.get(this);
var ranges = state.ranges;
var index = ranges.findIndex(function(range) {
return range.includes(raw);
});
if (index === state.length) {
index--;
}
if (index < 0) {
return [];
}
return [index];
}
}
},
AT42QT1070: {
ADDRESSES: {
value: [0x1B]
},
REGISTER: {
value: {
READ: 0x03
}
},
initialize: {
value: function(opts, dataHandler) {
var state = priv.get(this);
var address = opts.address || this.ADDRESSES[0];
var keys = flatKeys(opts);
var mapping = [0, 1, 2, 3, 4, 5, 6];
var 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(opts);
this.io.i2cRead(address, this.REGISTER.READ, 1, function(data) {
dataHandler(data[0]);
});
}
},
toAlias: {
value: function(index) {
var state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value: function(raw) {
var indices = [];
for (var 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: function(opts, dataHandler) {
var state = priv.get(this);
var address = opts.address || this.ADDRESSES[0];
var keys = flatKeys(opts);
var mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
var 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;
opts.address = address;
this.io.i2cConfig(opts);
this.io.i2cRead(address, 2, function(bytes) {
dataHandler(uint16(bytes[0], bytes[1]));
});
}
},
toAlias: {
value: function(index) {
var state = priv.get(this);
return state.keys[index];
}
},
toIndices: {
value: function(raw) {
var state = priv.get(this);
var indices = [];
for (var 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;
function touches(length) {
return Array.from({
length: length
}, function() {
return {
timeout: null,
value: 0,
};
});
}
function Keypad(opts) {
if (!(this instanceof Keypad)) {
return new Keypad(opts);
}
// Initialize a Device instance on a Board
Board.Component.call(
this, opts = Board.Options(opts)
);
var raw = null;
var controller = null;
var state = {
touches: null,
timeout: null,
length: null,
keys: null,
mapping: null,
holdtime: null,
};
var trigger = Fn.debounce(function(type, value) {
var event = {
type: type,
which: value,
timestamp: Date.now()
};
aliases[type].forEach(function(type) {
this.emit(type, event);
}, this);
this.emit("change", Object.assign({}, event));
}, 5);
if (opts.controller && typeof opts.controller === "string") {
controller = Controllers[opts.controller.toUpperCase()];
} else {
controller = opts.controller;
}
if (controller == null) {
controller = Controllers.ANALOG;
}
Board.Controller.call(this, controller, opts);
state.holdtime = opts.holdtime ? opts.holdtime : 500;
priv.set(this, state);
if (typeof this.initialize === "function") {
this.initialize(opts, function(data) {
raw = data;
var now = Date.now();
var indices = this.toIndices(data);
var kLength = state.length;
var lists = {
down: [],
hold: [],
up: [],
};
var target = null;
var alias = null;
for (var 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) {
var list = lists[key];
if (list.length) {
trigger.call(this, key, list);
}
}, this);
}.bind(this));
}
Object.defineProperties(this, {
isMultitouch: {
get: function() {
return state.isMultitouch;
}
},
value: {
get: function() {
return raw;
}
},
});
}
util.inherits(Keypad, Emitter);
module.exports = Keypad;