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!
1,037 lines (815 loc) • 23.1 kB
JavaScript
var Board = require("./board");
var Pin = require("./pin");
var lcdCharacters = require("./lcd-chars");
var RGB = require("./led/rgb");
var priv = new Map();
/**
* This atrocity is unfortunately necessary.
* If any other approach can be found, patches
* will gratefully be accepted.
*/
function sleep(ms) {
var start = Date.now();
// Wait 1ms longer than requested. For further information see:
// https://github.com/rwaldron/johnny-five/issues/326#issuecomment-230125813
while (Date.now() < start + ms + 1) {}
}
// 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.memory = 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.portWrite = function(value) {
this.memory = value & ~(this.mask);
this.io.i2cWrite(this.address, this.memory);
};
// const-caps throughout serve to indicate the
// "const-ness" of the binding to the reader
// and nothing more.
var REGISTER = {
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: {
REGISTER: {
value: REGISTER.DEFAULT,
},
CHARS: {
value: lcdCharacters.DEFAULT,
},
initialize: {
value: function(opts) {
// LCD: 0x3E
// RGB: 0x62
this.address = {
lcd: opts.address || 0x3E,
rgb: 0x62
};
opts.address = this.address;
this.io.i2cConfig(opts);
this.lines = opts.lines || 2;
this.rows = opts.rows || 2;
this.cols = opts.cols || 16;
this.dots = opts.dots || "5x8";
var display = this.REGISTER.DISPLAY | this.REGISTER.DISPLAYON | this.REGISTER.CURSOROFF | this.REGISTER.BLINKOFF;
var state = {
display: display,
characters: {},
index: this.REGISTER.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.REGISTER.DIMENSIONS | this.REGISTER.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 before 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.REGISTER.ENTRY |
this.REGISTER.ENTRYLEFT |
this.REGISTER.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.REGISTER.CLEAR);
}
},
setCursor: {
value: function(col, row) {
return this.command(row === 0 ? col | 0x80 : col | 0xc0);
}
},
bgColor: {
value: function(red, green, blue) {
var registers = [0x04, 0x03, 0x02];
var rgb = RGB.ToRGB(red, green, blue);
RGB.colors.forEach(function(color, index) {
this.io.i2cWrite(this.address.rgb, [registers[index], rgb[color]]);
}, this);
return this;
}
},
command: {
value: function(mode, value) {
if (arguments.length === 1) {
value = mode;
mode = this.REGISTER.COMMAND;
}
if (mode === this.REGISTER.DATA) {
return this.send(value);
}
return this.writeBits(this.REGISTER.COMMAND, value);
}
},
send: {
value: function(value) {
return this.writeBits(this.REGISTER.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: {
REGISTER: {
value: Object.assign({}, REGISTER.DEFAULT, {
COMMAND: 0x00,
DATA: 0x01,
BACKLIGHT_ON: 0xFF,
BACKLIGHT_OFF: 0X00
}),
},
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 (!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.io.i2cConfig(opts);
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.portWrite(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.REGISTER.BITMODE[this.bitMode] |
this.REGISTER.LINE[this.lines] |
this.REGISTER.DOTS[this.dots];
var display = this.REGISTER.DISPLAY |
this.REGISTER.DISPLAYON |
this.REGISTER.CURSOROFF |
this.REGISTER.BLINKOFF;
var entry = this.REGISTER.ENTRYLEFT |
this.REGISTER.ENTRYSHIFTDECREMENT;
var state = {
display: display,
characters: {},
index: this.REGISTER.MEMORYLIMIT - 1,
backlight: {
polarity: backlight.polarity,
pinMask: 1 << backlight.pin,
statusMask: 0x00
},
data: [
1 << this.REGISTER.D4,
1 << this.REGISTER.D5,
1 << this.REGISTER.D6,
1 << this.REGISTER.D7
]
};
priv.set(this, state);
var toggle = 0x03 << this.REGISTER.SHIFT_LEFT;
// Operations within the following labelled block are init-only,
// but _do_ block the process for negligible number of milliseconds.
blocking: {
//
// Toggle write/pulse to reset the LCD component.
//
this.expander.portWrite(toggle);
this.pulse(toggle);
sleep(4);
this.expander.portWrite(toggle);
this.pulse(toggle);
sleep(4);
this.expander.portWrite(toggle);
this.pulse(toggle);
toggle = 0x02 << this.REGISTER.SHIFT_LEFT;
this.expander.portWrite(toggle);
this.pulse(toggle);
// Initialize the reset component
this.command(this.REGISTER.DIMENSIONS | dimensions);
// Set display details
this.command(state.display);
// Now that the initial display is set,
// overwrite with the "entry" bits
state.display = entry;
this.command(this.REGISTER.ENTRY | state.display);
this.on();
this.clear();
this.backlight();
}
},
},
clear: {
value: function() {
this.command(this.REGISTER.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.portWrite(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.REGISTER.MEMORYLIMIT - 1;
}
}
this.command(this.REGISTER.SETCGRAMADDR | (address << 3));
blocking: {
sleep(1);
for (var i = 0; i < 8; i++) {
this.command(this.REGISTER.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.REGISTER.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.REGISTER.DATA) {
mode = this.REGISTER.RS;
}
pinMapValue |= mode | state.backlight.statusMask;
this.pulse(pinMapValue);
return this;
}
},
pulse: {
writable: true,
value: function(data) {
this.expander.portWrite(data | this.REGISTER.EN); // En HIGH
this.expander.portWrite(data & ~this.REGISTER.EN); // En LOW
}
}
},
PARALLEL: {
REGISTER: {
value: REGISTER.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.REGISTER.DISPLAY | this.REGISTER.DISPLAYON;
var state = {
display: display,
characters: {},
index: this.REGISTER.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 0b00000011 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.REGISTER.DIMENSIONS |
this.command(0x02);
}
// Set number of lines and dots
// TODO: Move to device map profile
this.command(
this.REGISTER.LINE[this.lines] |
this.REGISTER.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, {
REGISTER: {
value: Object.assign({}, REGISTER.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.REGISTER.DATA) {
mode = (1 << this.REGISTER.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 = null;
if (opts.controller) {
controller = typeof opts.controller === "string" ?
Controllers[opts.controller.toUpperCase()] :
opts.controller;
}
if (!controller) {
controller = Controllers.PARALLEL;
}
Board.Controller.call(this, controller, opts);
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.REGISTER.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.REGISTER.DATA, character.charCodeAt(0));
}, this);
});
}
}
return this;
};
LCD.prototype.write = function(charCode) {
this.hilo.call(this, function() {
this.command(this.REGISTER.DATA, charCode);
});
return this;
};
LCD.prototype.clear = function() {
this.command(this.REGISTER.CLEAR);
sleep(2);
return this;
};
LCD.prototype.home = function() {
this.command(this.REGISTER.HOME);
sleep(2);
return this;
};
LCD.prototype.setCursor = function(col, row) {
var rowOffsets = [0x00, 0x40, 0x14, 0x54];
this.command(this.REGISTER.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.REGISTER.DISPLAYON;
} else {
state.display &= ~this.REGISTER.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.REGISTER.DISPLAYON;
// } else {
// state.display &= ~this.REGISTER.DISPLAYON;
// }
// this.command(state.display);
return this.backlight(false);
};
LCD.prototype.on = function() {
var state = priv.get(this);
state.display |= this.REGISTER.DISPLAYON;
this.command(state.display);
return this;
};
LCD.prototype.off = function() {
var state = priv.get(this);
state.display &= ~this.REGISTER.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.REGISTER.CURSORON;
this.command(state.display);
return this;
};
LCD.prototype.noCursor = function() {
var state = priv.get(this);
state.display &= ~this.REGISTER.CURSORON;
this.command(state.display);
return this;
};
LCD.prototype.blink = function() {
var state = priv.get(this);
state.display |= this.REGISTER.BLINKON;
this.command(state.display);
return this;
};
LCD.prototype.noBlink = function() {
var state = priv.get(this);
state.display &= ~this.REGISTER.BLINKON;
this.command(state.display);
return this;
};
LCD.prototype.autoscroll = function() {
var state = priv.get(this);
state.display |= this.REGISTER.ENTRYSHIFTINCREMENT;
this.command(this.REGISTER.ENTRY | state.display);
return this;
};
LCD.prototype.noAutoscroll = function() {
var state = priv.get(this);
state.display &= ~this.REGISTER.ENTRYSHIFTINCREMENT;
this.command(this.REGISTER.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.REGISTER.MEMORYLIMIT - 1;
}
}
this.command(this.REGISTER.SETCGRAMADDR | (address << 3));
this.hilo(function() {
for (var i = 0; i < 8; i++) {
this.command(this.REGISTER.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;