UNPKG

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
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;