johnny-five-electron
Version:
Temporary fork to support Electron (to be deprecated)
1,032 lines (816 loc) • 22.6 kB
JavaScript
var Board = require("../lib/board.js"),
Pin = require("../lib/pin.js"),
lcdCharacters = require("../lib/lcd-chars.js"),
converter = require("color-convert")();
var priv = new Map();
/**
* This atrocitity is unfortunately necessary.
* If any other approach can be found, patches
* will gratefully be accepted.
*/
function sleep(ms) {
var start = Date.now();
while (Date.now() < start + ms) {}
}
// TODO: Migrate this to the new codified Expander class.
//
// - add portMode to PCF8574 controller
// - add portWrite to PCF8574 controller
//
//
// TODO: Investigate adding the above methods to
// all expander controllers.
//
function Expander(address, io) {
this.address = address;
this.mask = 0xFF;
this.shadow = 0x00;
this.io = io;
}
Expander.prototype.pinMode = function(pin, dir) {
if (dir === 0x01) {
this.mask &= ~(1 << pin);
} else {
this.mask |= 1 << pin;
}
};
Expander.prototype.portMode = function(dir) {
this.mask = dir === 0x00 ? 0xFF : 0x00;
};
Expander.prototype.write = function(value) {
this.shadow = value & ~(this.mask);
this.io.i2cWrite(this.address, this.shadow);
};
// const-caps throughout serve to indicate the
// "const-ness" of the binding to the reader
// and nothing more.
var OPS = {
DEFAULT: {
SHIFT_LEFT: 0x04,
CLEAR: 0x01,
HOME: 0x02,
ENTRY: 0x04,
DISPLAY: 0x08,
DIMENSIONS: 0x20,
CURSORSHIFT: 0x10,
SETCGRAMADDR: 0x40,
SETDDRAMADDR: 0x80,
// Command And Control
DATA: 0x40,
COMMAND: 0x80,
// flags for display entry mode
ENTRYRIGHT: 0x00,
ENTRYLEFT: 0x02,
ENTRYSHIFTINCREMENT: 0x01,
ENTRYSHIFTDECREMENT: 0x00,
// flags for display on/off control
DISPLAYON: 0x04,
DISPLAYOFF: 0x00,
CURSORON: 0x02,
CURSOROFF: 0x00,
BLINKON: 0x01,
BLINKOFF: 0x00,
// flags for display/cursor shift
DISPLAYMOVE: 0x08,
CURSORMOVE: 0x00,
MOVERIGHT: 0x04,
MOVELEFT: 0x00,
// flags for function set
BITMODE: {
4: 0x00,
8: 0x10,
},
LINE: {
1: 0x00,
2: 0x08
},
DOTS: {
"5x10": 0x04,
"5x8": 0x00
},
// flags for backlight control
BACKLIGHT_ON: 0x08,
BACKLIGHT_OFF: 0x00,
MEMORYLIMIT: 0x08,
// Control
// Enable
EN: 0x04,
// Read/Write
RW: 0x02,
// Register Select
RS: 0x01,
// DATA
D4: 0x04,
D5: 0x05,
D6: 0x06,
D7: 0x07,
}
};
var Controllers = {
JHD1313M1: {
OP: {
value: OPS.DEFAULT,
},
CHARS: {
value: lcdCharacters.DEFAULT,
},
initialize: {
value: function(opts) {
this.io.i2cConfig(opts);
this.lines = opts.lines || 2;
this.rows = opts.rows || 2;
this.cols = opts.cols || 16;
this.dots = opts.dots || "5x8";
// LCD: 0x3E
// RGB: 0x62
this.address = {
lcd: opts.address || 0x3E,
rgb: 0x62
};
var display = this.OP.DISPLAY | this.OP.DISPLAYON | this.OP.CURSOROFF | this.OP.BLINKOFF;
var state = {
display: display,
characters: {},
index: this.OP.MEMORYLIMIT - 1,
backlight: {
polarity: 1,
pin: null,
value: null
}
};
priv.set(this, state);
// Operations within the following labelled block are init-only,
// but _do_ block the process negligible number of milliseconds.
blocking: {
var lines = this.OP.DIMENSIONS | this.OP.LINE[2];
// Copied from Grove Studio lib.
// SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION!
// according to datasheet, we need at least 40ms after
// power rises above 2.7V before sending commands.
// Arduino can turn on way befer 4.5V so we'll wait 50
sleep(50);
this.command(lines);
sleep(5);
this.command(lines);
this.command(lines);
this.command(lines);
sleep(5);
this.command(
this.OP.ENTRY |
this.OP.ENTRYLEFT |
this.OP.ENTRYSHIFTDECREMENT
);
this.on();
this.clear();
this.home();
}
// Backlight initialization
this.io.i2cWrite(this.address.rgb, [ 0, 0 ]);
this.io.i2cWrite(this.address.rgb, [ 1, 0 ]);
this.io.i2cWrite(this.address.rgb, [ 0x08, 0xAA ]);
this.bgColor(opts.color || "white");
},
},
clear: {
value: function() {
return this.command(this.OP.CLEAR);
}
},
setCursor: {
value: function(col, row) {
return this.command(row === 0 ? col | 0x80 : col | 0xc0);
}
},
bgColor: {
value: function(r, g, b) {
var rgb = [r, g, b];
if (arguments.length === 1) {
if (Array.isArray(r)) {
rgb = r;
}
if (typeof r === "string") {
rgb = converter.keyword(r).rgb();
}
}
[0x04, 0x03, 0x02].forEach(function(cmd, i) {
this.io.i2cWrite(this.address.rgb, [ cmd, rgb[i] ]);
}, this);
return this;
}
},
command: {
value: function(mode, value) {
if (arguments.length === 1) {
value = mode;
mode = this.OP.COMMAND;
}
if (mode === this.OP.DATA) {
return this.send(value);
}
return this.writeBits(this.OP.COMMAND, value);
}
},
send: {
value: function(value) {
return this.writeBits(this.OP.DATA, value);
}
},
writeBits: {
value: function(mode, value) {
this.io.i2cWrite(this.address.lcd, [ mode, value ]);
return this;
}
},
hilo: {
value: function(callback) {
callback.call(this);
}
},
},
PCF8574: {
OP: {
value: Object.assign({}, OPS.DEFAULT, {
COMMAND: 0x00,
DATA: 0x01,
BACKLIGHT_ON: 0xFF,
BACKLIGHT_OFF: 0X00
}),
},
CHARS: {
value: lcdCharacters.DEFAULT,
},
initialize: {
value: function(opts) {
this.io.i2cConfig(opts);
this.bitMode = opts.bitMode || 4;
this.lines = opts.lines || 2;
this.rows = opts.rows || 2;
this.cols = opts.cols || 16;
this.dots = opts.dots || "5x8";
if (!opts.address) {
opts.address = ["PCF8574A", "PCF8574AT"].includes(opts.controller) ?
0x3F : 0x27;
/*
| A2 | A1 | A0 | PCF8574(T) | PCF8574A(T) |
|----|----|----|---------|----------|
| L | L | L | 0x20 | 0x38 |
| L | L | H | 0x21 | 0x39 |
| L | H | L | 0x22 | 0x3A |
| L | H | H | 0x23 | 0x3B |
| H | L | L | 0x24 | 0x3C |
| H | L | H | 0x25 | 0x3D |
| H | H | L | 0x26 | 0x3E |
| H | H | H | 0x27 | 0x3F |
TODO: move to API docs
*/
}
this.address = {
lcd: opts.address
};
// Ported from https://bitbucket.org/fmalpartida/new-liquidcrystal
this.expander = new Expander(this.address.lcd, this.io);
this.expander.portMode(this.io.MODES.OUTPUT);
this.expander.write(0);
var backlight = opts.backlight || {
polarity: 0,
pin: 3
};
backlight.pin = typeof backlight.pin === "undefined" ? 3 : backlight.pin;
backlight.polarity = typeof backlight.polarity === "undefined" ? 0 : backlight.polarity;
var dimensions = this.OP.BITMODE[this.bitMode] |
this.OP.LINE[this.lines] |
this.OP.DOTS[this.dots];
var display = this.OP.DISPLAY |
this.OP.DISPLAYON |
this.OP.CURSOROFF |
this.OP.BLINKOFF;
var entry = this.OP.ENTRYLEFT |
this.OP.ENTRYSHIFTDECREMENT;
var state = {
display: display,
characters: {},
index: this.OP.MEMORYLIMIT - 1,
backlight: {
polarity: backlight.polarity,
pinMask: 1 << backlight.pin,
statusMask: 0x00
},
data: [
1 << this.OP.D4,
1 << this.OP.D5,
1 << this.OP.D6,
1 << this.OP.D7
]
};
priv.set(this, state);
// Operations within the following labelled block are init-only,
// but _do_ block the process for negligible number of milliseconds.
blocking: {
//
// Toggle wrte/pulse to reset the LCD component.
//
this.expander.write(0x03 << this.OP.SHIFT_LEFT);
this.pulse(0x03 << this.OP.SHIFT_LEFT);
sleep(4);
this.expander.write(0x03 << this.OP.SHIFT_LEFT);
this.pulse(0x03 << this.OP.SHIFT_LEFT);
sleep(4);
this.expander.write(0x03 << this.OP.SHIFT_LEFT);
this.pulse(0x03 << this.OP.SHIFT_LEFT);
this.expander.write(0x02 << this.OP.SHIFT_LEFT);
this.pulse(0x02 << this.OP.SHIFT_LEFT);
// Initialize the reset component
this.command(this.OP.DIMENSIONS | dimensions);
this.on();
this.clear();
this.command(this.OP.ENTRY | entry);
this.backlight();
}
},
},
clear: {
value: function() {
this.command(this.OP.CLEAR);
sleep(2);
return this;
}
},
backlight: {
value: function(value) {
var state = priv.get(this);
var mask;
value = typeof value === "undefined" ? 255 : value;
if (state.backlight.pinMask !== 0x00) {
if ((state.backlight.polarity === 0 && value > 0) ||
(state.backlight.polarity === 1 && value === 0)) {
mask = 0xFF;
} else {
mask = 0x00;
}
state.backlight.statusMask = state.backlight.pinMask & mask;
this.expander.write(state.backlight.statusMask);
}
return this;
}
},
createChar: {
value: function(name, charMap) {
var state = priv.get(this);
var address;
if (typeof name === "number") {
address = name & 0x07;
} else {
address = state.index;
state.index--;
if (state.index === -1) {
state.index = this.OP.MEMORYLIMIT - 1;
}
}
this.command(this.OP.SETCGRAMADDR | (address << 3));
blocking: {
sleep(1);
for (var i = 0; i < 8; i++) {
this.command(this.OP.DATA, charMap[i]);
sleep(1);
}
}
state.characters[name] = address;
return address;
}
},
noBacklight: {
value: function() {
this.backlight(0);
}
},
hilo: {
value: function(callback) {
callback.call(this);
}
},
command: {
value: function(mode, value) {
if (arguments.length === 1) {
value = mode;
mode = this.OP.COMMAND;
}
this.send(mode, value);
return this;
}
},
send: {
writable: true,
value: function(mode, value) {
this.writeBits(mode, value >> 4);
this.writeBits(mode, value & 0x0F);
return this;
}
},
writeBits: {
writable: true,
value: function(mode, value) {
var state = priv.get(this);
var pinMapValue = 0;
for (var i = 0; i < 4; i++) {
if ((value & 0x01) === 1) {
pinMapValue |= state.data[i];
}
value = (value >> 1);
}
if (mode === this.OP.DATA) {
mode = this.OP.RS;
}
pinMapValue |= mode | state.backlight.statusMask;
this.pulse(pinMapValue);
return this;
}
},
pulse: {
writable: true,
value: function(data) {
this.expander.write(data | this.OP.EN); // En HIGH
this.expander.write(data & ~this.OP.EN); // En LOW
}
}
},
PARALLEL: {
OP: {
value: OPS.DEFAULT,
},
CHARS: {
value: lcdCharacters.DEFAULT,
},
initialize: {
value: function(opts) {
this.bitMode = opts.bitMode || 4;
this.lines = opts.lines || 2;
this.rows = opts.rows || 2;
this.cols = opts.cols || 16;
this.dots = opts.dots || "5x8";
if (Array.isArray(opts.pins)) {
this.pins = {
rs: opts.pins[0],
en: opts.pins[1],
// TODO: Move to device map profile
data: [
opts.pins[5],
opts.pins[4],
opts.pins[3],
opts.pins[2]
]
};
} else {
this.pins = opts.pins;
}
var display = this.OP.DISPLAY | this.OP.DISPLAYON;
var state = {
display: display,
characters: {},
index: this.OP.MEMORYLIMIT - 1,
backlight: {
polarity: 1,
pin: null,
value: null
}
};
priv.set(this, state);
opts.pins.forEach(function(pin) {
this.io.pinMode(pin, 1);
}, this);
this.io.digitalWrite(this.pins.rs, this.io.LOW);
this.io.digitalWrite(this.pins.en, this.io.LOW);
if (opts.backlight) {
if (typeof opts.backlight === "number") {
var temp = opts.backlight;
opts.backlight = {
pin: temp
};
}
if (opts.backlight.pin) {
state.backlight.pin = new Pin({
pin: opts.backlight.pin,
board: this.board
});
state.backlight.pin.high();
}
}
// Operations within the following labelled block are init-only,
// but _do_ block the process negligible number of milliseconds.
blocking: {
// Send 0011 thrice to make sure LCD
// is initialized properly
this.command(0x03);
sleep(4);
this.command(0x03);
sleep(4);
this.command(0x03);
// Switch to 4-bit mode
if (this.bitMode === 4) {
// this.OP.DIMENSIONS |
this.command(0x02);
}
// Set number of lines and dots
// TODO: Move to device map profile
this.command(
this.OP.LINE[this.lines] |
this.OP.DOTS[this.dots]
);
// Clear display and turn it on
this.command(display);
this.clear();
this.home();
}
}
}
}
};
// Alias controllers
Controllers.HD44780 = Controllers.JHD1313M1;
Controllers.LCM1602 = Controllers.LCD1602 = Controllers.LCM1602IIC = Controllers.LCD2004 = Controllers.PCF8574A = Controllers.PCF8574AT = Controllers.PCF8574T = Controllers.PCF8574;
Controllers.MJKDZ = Object.assign({}, Controllers.PCF8574, {
OP: {
value: Object.assign({}, OPS.DEFAULT, {
SHIFT_LEFT: 0x00,
COMMAND: 0x00,
DATA: 0x06,
// Control
// Enable
EN: 0x10,
// Read/Write
RW: 0x05,
// Register Select
RS: 0x06,
D4: 0x00,
D5: 0x01,
D6: 0x02,
D7: 0x03
})
},
writeBits: {
writable: true,
value: function(mode, value) {
var state = priv.get(this);
var pinMapValue = 0;
for (var i = 0; i < 4; i++) {
if ((value & 0x01) === 1) {
pinMapValue |= state.data[i];
}
value = (value >> 1);
}
if (mode === this.OP.DATA) {
mode = (1 << this.OP.RS);
}
pinMapValue |= mode | state.backlight.statusMask;
this.pulse(pinMapValue);
return this;
}
},
});
/**
* LCD
* @param {[type]} opts [description]
*/
function LCD(opts) {
if (!(this instanceof LCD)) {
return new LCD(opts);
}
Board.Component.call(
this, opts = Board.Options(opts)
);
var controller;
if (opts.controller) {
controller = typeof opts.controller === "string" ?
Controllers[opts.controller.toUpperCase()] :
opts.controller;
}
if (!controller) {
controller = Controllers.PARALLEL;
}
Object.defineProperties(this, controller);
this.ctype = opts.controller;
if (this.initialize) {
this.initialize(opts);
}
}
LCD.prototype.command = function(mode, value) {
if (typeof value === "undefined") {
value = mode;
mode = 0x80;
}
if (this.bitMode === 4) {
this.send(value >> 4);
}
this.send(value);
return this;
};
LCD.prototype.send = function(value) {
var pin = 0;
var mask = {
4: 8,
8: 128
}[this.bitMode];
for (; mask > 0; mask = mask >> 1) {
this.io.digitalWrite(
this.pins.data[pin],
this.io[value & mask ? "HIGH" : "LOW"]
);
pin++;
}
["LOW", "HIGH", "LOW"].forEach(function(val) {
this.io.digitalWrite(this.pins.en, this.io[val]);
}, this);
return this;
};
LCD.prototype.hilo = function(callback) {
// RS High for write mode
this.io.digitalWrite(this.pins.rs, this.io.HIGH);
callback.call(this);
// RS Low for command mode
this.io.digitalWrite(this.pins.rs, this.io.LOW);
};
var RE_SPECIALS = /:(\w+):/g;
LCD.prototype.print = function(message, opts) {
var state, dontProcessSpecials, hasCharacters, processed;
message = message + "";
opts = opts || {};
state = priv.get(this);
dontProcessSpecials = opts.dontProcessSpecials || false;
hasCharacters = !dontProcessSpecials && RE_SPECIALS.test(message);
if (message.length === 1) {
this.hilo(function() {
this.command(this.OP.DATA, message.charCodeAt(0));
});
} else {
if (hasCharacters) {
processed = message.replace(RE_SPECIALS, function(match, name) {
var address = state.characters[name];
return typeof address === "number" ? String.fromCharCode(address) : match;
});
this.print(processed, {
dontProcessSpecials: true
});
} else {
this.hilo(function() {
Array.from(message).forEach(function(character) {
this.command(this.OP.DATA, character.charCodeAt(0));
}, this);
// var char;
// while ((char = chars[k++])) {
// this.command(this.OP.DATA, char.charCodeAt(0));
// }
});
}
}
return this;
};
LCD.prototype.write = function(charCode) {
this.hilo.call(this, function() {
this.command(this.OP.DATA, charCode);
});
return this;
};
LCD.prototype.clear = function() {
this.command(this.OP.CLEAR);
sleep(2);
return this;
};
LCD.prototype.home = function() {
this.command(this.OP.HOME);
sleep(2);
return this;
};
LCD.prototype.setCursor = function(col, row) {
var rowOffsets = [0x00, 0x40, 0x14, 0x54];
this.command(this.OP.SETDDRAMADDR | (col + rowOffsets[row]));
return this;
};
LCD.prototype.backlight = function(highOrLow) {
var state = priv.get(this);
highOrLow = typeof highOrLow === "undefined" ? true : false;
if (state.backlight.pin instanceof Pin) {
if (highOrLow) {
state.backlight.pin.high();
} else {
state.backlight.pin.low();
}
}
if (highOrLow) {
state.display |= this.OP.DISPLAYON;
} else {
state.display &= ~this.OP.DISPLAYON;
}
this.command(state.display);
return this;
};
LCD.prototype.noBacklight = function() {
var state = priv.get(this);
if (state.backlight.pin instanceof Pin) {
state.backlight.pin.high();
}
// if (highOrLow) {
// state.display |= this.OP.DISPLAYON;
// } else {
// state.display &= ~this.OP.DISPLAYON;
// }
// this.command(state.display);
return this.backlight(false);
};
LCD.prototype.on = function() {
var state = priv.get(this);
state.display |= this.OP.DISPLAYON;
this.command(state.display);
return this;
};
LCD.prototype.off = function() {
var state = priv.get(this);
state.display &= ~this.OP.DISPLAYON;
this.command(state.display);
return this;
};
LCD.prototype.cursor = function(row, col) {
// When provided with col & row, cursor will behave like setCursor,
// except that it has row and col in the order that most people
// intuitively expect it to be in.
if (typeof col !== "undefined" && typeof row !== "undefined") {
return this.setCursor(col, row);
}
var state = priv.get(this);
state.display |= this.OP.CURSORON;
this.command(state.display);
return this;
};
LCD.prototype.noCursor = function() {
var state = priv.get(this);
state.display &= ~this.OP.CURSORON;
this.command(state.display);
return this;
};
LCD.prototype.blink = function() {
var state = priv.get(this);
state.display |= this.OP.BLINKON;
this.command(state.display);
return this;
};
LCD.prototype.noBlink = function() {
var state = priv.get(this);
state.display &= ~this.OP.BLINKON;
this.command(state.display);
return this;
};
LCD.prototype.autoscroll = function() {
var state = priv.get(this);
state.display |= this.OP.ENTRYSHIFTINCREMENT;
this.command(this.OP.ENTRY | state.display);
return this;
};
LCD.prototype.noAutoscroll = function() {
var state = priv.get(this);
state.display &= ~this.OP.ENTRYSHIFTINCREMENT;
this.command(this.OP.ENTRY | state.display);
return this;
};
LCD.prototype.createChar = function(name, charMap) {
// Ensure location is never above 7
var state = priv.get(this);
var address;
if (typeof name === "number") {
address = name & 0x07;
} else {
address = state.index;
state.index--;
if (state.index === -1) {
state.index = this.OP.MEMORYLIMIT - 1;
}
}
this.command(this.OP.SETCGRAMADDR | (address << 3));
this.hilo(function() {
for (var i = 0; i < 8; i++) {
this.command(this.OP.DATA, charMap[i]);
}
});
// Fill in address
state.characters[name] = address;
return address;
};
LCD.prototype.useChar = function(name) {
var state = priv.get(this);
if (!state.characters[name]) {
// Create the character in LCD memory and
// Add character to current LCD character map
state.characters[name] = this.createChar(name, this.CHARS[name]);
}
return this;
};
/**
*
TODO:
burst()
scrollDisplayLeft()
scrollDisplayRight()
leftToRight()
rightToLeft()
*/
LCD.POSITIVE = 0;
LCD.NEGATIVE = 1;
LCD.Characters = lcdCharacters;
module.exports = LCD;