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,120 lines (889 loc) 25.2 kB
var Board = require("./board"); var Pin = require("./pin"); var lcdCharacters = require("./lcd-chars"); var RGB = require("./led/rgb"); const priv = new Map(); /** * This atrocity is unfortunately necessary. * If any other approach can be found, patches * will gratefully be accepted. */ function sleepus(usDelay) { const startTime = process.hrtime(); let deltaTime; let usWaited = 0; while (usDelay > usWaited) { deltaTime = process.hrtime(startTime); usWaited = (deltaTime[0] * 1E9 + deltaTime[1]) / 1000; } } /** * This atrocity is unfortunately necessary. * If any other approach can be found, patches * will gratefully be accepted. */ function sleep(ms) { sleepus(ms * 1000); } // 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. // class Expander { constructor(address, io) { this.address = address; this.mask = 0xFF; this.memory = 0x00; this.io = io; } pinMode(pin, dir) { if (dir === 0x01) { this.mask &= ~(1 << pin); } else { this.mask |= 1 << pin; } } portMode(dir) { this.mask = dir === 0x00 ? 0xFF : 0x00; } portWrite(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. const 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, } }; const Controllers = { JHD1313M1: { REGISTER: { value: REGISTER.DEFAULT, }, CHARS: { value: lcdCharacters.DEFAULT, }, initialize: { value(options) { // LCD: 0x3E // RGB: 0x62 this.address = { lcd: options.address || 0x3E, rgb: 0x62 }; options.address = this.address; this.io.i2cConfig(options); this.lines = options.lines || 2; this.rows = options.rows || 2; this.cols = options.cols || 16; this.dots = options.dots || "5x8"; const display = this.REGISTER.DISPLAY | this.REGISTER.DISPLAYON | this.REGISTER.CURSOROFF | this.REGISTER.BLINKOFF; const state = { 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: { const 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.bgOn(); if (options.color) { this.bgColor(options.color); } else { this.bgColor("black"); } }, }, clear: { value() { return this.command(this.REGISTER.CLEAR); } }, setCursor: { value(col, row) { return this.command(row === 0 ? col | 0x80 : col | 0xc0); } }, autoscroll: { value() { const state = priv.get(this); state.display = this.REGISTER.ENTRYLEFT | this.REGISTER.ENTRYSHIFTINCREMENT; this.command(this.REGISTER.ENTRY | state.display); return this; } }, bgColor: { value(red, green, blue) { const rgb = RGB.ToRGB(red, green, blue); const address = this.address.rgb; this.io.i2cWrite(address, [0x00, 0]); this.io.i2cWrite(address, [0x01, 0]); // TRY THIS IN ONE CALL! this.io.i2cWrite(address, [0x04, rgb.red]); this.io.i2cWrite(address, [0x03, rgb.green]); this.io.i2cWrite(address, [0x02, rgb.blue]); return this; } }, bgOn: { value() { this.io.i2cWrite(this.address.rgb, [this.REGISTER.BACKLIGHT_ON, 0xAA]); return this; } }, bgOff: { value() { this.io.i2cWrite(this.address.rgb, [this.REGISTER.BACKLIGHT_ON, 0x00]); return this; } }, command: { value(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(value) { return this.writeBits(this.REGISTER.DATA, value); } }, writeBits: { value(mode, value) { this.io.i2cWrite(this.address.lcd, [mode, value]); return this; } }, hilo: { value(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(options) { this.bitMode = options.bitMode || 4; this.lines = options.lines || 2; this.rows = options.rows || 2; this.cols = options.cols || 16; this.dots = options.dots || "5x8"; if (!options.address) { options.address = ["PCF8574A", "PCF8574AT"].includes(options.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(options); this.address = { lcd: options.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); const backlight = options.backlight || { polarity: 0, pin: 3 }; backlight.pin = typeof backlight.pin === "undefined" ? 3 : backlight.pin; backlight.polarity = typeof backlight.polarity === "undefined" ? 0 : backlight.polarity; const dimensions = this.REGISTER.BITMODE[this.bitMode] | this.REGISTER.LINE[this.lines] | this.REGISTER.DOTS[this.dots]; const display = this.REGISTER.DISPLAY | this.REGISTER.DISPLAYON | this.REGISTER.CURSOROFF | this.REGISTER.BLINKOFF; const entry = this.REGISTER.ENTRYLEFT | this.REGISTER.ENTRYSHIFTDECREMENT; const state = { 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); let 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() { this.command(this.REGISTER.CLEAR); sleep(2); return this; } }, backlight: { value(value) { const state = priv.get(this); let 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(name, charMap) { const state = priv.get(this); let 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 (let i = 0; i < 8; i++) { this.command(this.REGISTER.DATA, charMap[i]); sleep(1); } } state.characters[name] = address; return address; } }, noBacklight: { value() { this.backlight(0); } }, on: { value() { const state = priv.get(this); state.display |= this.REGISTER.DISPLAYON; this.command(this.REGISTER.DISPLAY | state.display); return this; } }, off: { value() { const state = priv.get(this); state.display &= ~this.REGISTER.DISPLAYON; this.command(this.REGISTER.DISPLAY | state.display); return this; } }, hilo: { value(callback) { callback.call(this); } }, command: { value(mode, value) { if (arguments.length === 1) { value = mode; mode = this.REGISTER.COMMAND; } this.send(mode, value); return this; } }, send: { writable: true, value(mode, value) { this.writeBits(mode, value >> 4); this.writeBits(mode, value & 0x0F); return this; } }, writeBits: { writable: true, value(mode, value) { const state = priv.get(this); let pinMapValue = 0; for (let 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(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(options) { this.bitMode = options.bitMode || 4; this.lines = options.lines || 2; this.rows = options.rows || 2; this.cols = options.cols || 16; this.dots = options.dots || "5x8"; if (Array.isArray(options.pins)) { this.pins = { rs: options.pins[0], en: options.pins[1], // TODO: Move to device map profile data: [ options.pins[5], options.pins[4], options.pins[3], options.pins[2] ] }; } else { this.pins = options.pins; } const display = this.REGISTER.DISPLAY | this.REGISTER.DISPLAYON; const state = { display, characters: {}, index: this.REGISTER.MEMORYLIMIT - 1, backlight: { polarity: 1, pin: null, value: null } }; priv.set(this, state); options.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 (options.backlight) { if (typeof options.backlight === "number") { const temp = options.backlight; options.backlight = { pin: temp }; } if (options.backlight.pin) { state.backlight.pin = new Pin({ pin: options.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.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(mode, value) { const state = priv.get(this); let pinMapValue = 0; for (let 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; } }, }); Controllers.DEFAULT = Controllers.PARALLEL; /** * LCD * @param {[type]} options [description] */ class LCD { constructor(options) { Board.Component.call( this, options = Board.Options(options) ); Board.Controller.call(this, Controllers, options); this.ctype = options.controller; if (this.initialize) { this.initialize(options); } Object.defineProperties(this, { characters: { get() { return Object.assign({}, priv.get(this).characters); }, }, }); } command(mode, value) { if (typeof value === "undefined") { value = mode; mode = 0x80; } if (this.bitMode === 4) { this.send(value >> 4); } this.send(value); return this; } send(value) { let pin = 0; let 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++; } // At VCC = 3.3V, the minimum enable pulse width is specified as 450 // nanoseconds on page 49 of the HD44780 datasheet. // We therefore wait for 1 microsecond here to ensure that fast IO plugins // like Pi-IO generate an enable pulse that's wide enough. this.io.digitalWrite(this.pins.en, this.io.LOW); this.io.digitalWrite(this.pins.en, this.io.HIGH); sleepus(1); this.io.digitalWrite(this.pins.en, this.io.LOW); // The execution time for the vast majority of instructions is at least // 37 microseconds. See datasheet pages 24 and 25. // It's important to wait 37 microseconds here to prevent fast IO plugins // like Pi-IO from executing the next instruction before the current // instruction has completed. sleepus(37); return this; } hilo(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); } print(message, options) { let state; let dontProcessSpecials; let hasCharacters; let processed; message = `${message}`; options = options || {}; state = priv.get(this); dontProcessSpecials = options.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, (match, name) => { const 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; } write(charCode) { this.hilo.call(this, function() { this.command(this.REGISTER.DATA, charCode); }); return this; } clear() { this.command(this.REGISTER.CLEAR); sleep(2); return this; } home() { this.command(this.REGISTER.HOME); sleep(2); return this; } setCursor(col, row) { const rowOffsets = [0x00, 0x40, 0x14, 0x54]; this.command(this.REGISTER.SETDDRAMADDR | (col + rowOffsets[row])); return this; } backlight(highOrLow) { const 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; } noBacklight() { const 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); } on() { const state = priv.get(this); state.display |= this.REGISTER.DISPLAYON; this.command(state.display); return this; } off() { const state = priv.get(this); state.display &= ~this.REGISTER.DISPLAYON; this.command(state.display); return this; } cursor(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); } const state = priv.get(this); state.display |= this.REGISTER.CURSORON; this.command(state.display); return this; } noCursor() { const state = priv.get(this); state.display &= ~this.REGISTER.CURSORON; this.command(state.display); return this; } blink() { const state = priv.get(this); state.display |= this.REGISTER.BLINKON; this.command(state.display); return this; } noBlink() { const state = priv.get(this); state.display &= ~this.REGISTER.BLINKON; this.command(state.display); return this; } autoscroll() { const state = priv.get(this); state.display |= this.REGISTER.ENTRYSHIFTINCREMENT; this.command(this.REGISTER.ENTRY | state.display); return this; } noAutoscroll() { const state = priv.get(this); state.display &= ~this.REGISTER.ENTRYSHIFTINCREMENT; this.command(this.REGISTER.ENTRY | state.display); return this; } createChar(name, charMap) { // Ensure location is never above 7 const state = priv.get(this); let 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 (let i = 0; i < 8; i++) { this.command(this.REGISTER.DATA, charMap[i]); } }); // Fill in address state.characters[name] = address; return address; } useChar(name) { const state = priv.get(this); if (typeof state.characters[name] === "undefined") { // Create the character in LCD memory and const newCharIndex = this.createChar(name, this.CHARS[name]); // If character's index already used, remove this character in current LCD character map // because it's not in LCD memory anymore. for (const oldName in state.characters) { if (name !== oldName && state.characters[oldName] === newCharIndex) { delete state.characters[oldName]; break; } } // Add character to current LCD character map state.characters[name] = newCharIndex; } return this; } } const RE_SPECIALS = /:(\w+):/g; /** * TODO: burst() scrollDisplayLeft() scrollDisplayRight() leftToRight() rightToLeft() */ LCD.POSITIVE = 0; LCD.NEGATIVE = 1; LCD.Characters = lcdCharacters; module.exports = LCD;