ocoge/local_modules/ssd1306/oled.js

669 lines
19 KiB
JavaScript
Raw Permalink Normal View History

var Oled = function (rg, opts) {
this.HEIGHT = opts.height || 32;
this.WIDTH = opts.width || 128;
this.ADDRESS = opts.address || 0x3C;
this.BUS = opts.bus || 1;
this.PROTOCOL = 'I2C';
this.LINESPACING = typeof opts.linespacing !== 'undefined' ? opts.linespacing : 1;
this.LETTERSPACING = typeof opts.letterspacing !== 'undefined' ? opts.letterspacing : 1;
// create command buffers
this.DISPLAY_OFF = 0xAE;
this.DISPLAY_ON = 0xAF;
this.SET_DISPLAY_CLOCK_DIV = 0xD5;
this.SET_MULTIPLEX = 0xA8;
this.SET_DISPLAY_OFFSET = 0xD3;
this.SET_START_LINE = 0x00;
this.CHARGE_PUMP = 0x8D;
this.EXTERNAL_VCC = false;
this.MEMORY_MODE = 0x20;
this.SEG_REMAP = 0xA1; // using 0xA0 will flip screen
this.COM_SCAN_DEC = 0xC8;
this.COM_SCAN_INC = 0xC0;
this.SET_COM_PINS = 0xDA;
this.SET_CONTRAST = 0x81;
this.SET_PRECHARGE = 0xd9;
this.SET_VCOM_DETECT = 0xDB;
this.DISPLAY_ALL_ON_RESUME = 0xA4;
this.NORMAL_DISPLAY = 0xA6;
this.COLUMN_ADDR = 0x21;
this.PAGE_ADDR = 0x22;
this.INVERT_DISPLAY = 0xA7;
this.ACTIVATE_SCROLL = 0x2F;
this.DEACTIVATE_SCROLL = 0x2E;
this.SET_VERTICAL_SCROLL_AREA = 0xA3;
this.RIGHT_HORIZONTAL_SCROLL = 0x26;
this.LEFT_HORIZONTAL_SCROLL = 0x27;
this.VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29;
this.VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A;
this.cursor_x = 0;
this.cursor_y = 0;
// new blank buffer
this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8);
this.buffer.fill(0x00);
this.dirtyBytes = [];
var config = {
'128x32': {
'multiplex': 0x1F,
'compins': 0x02,
'coloffset': 0
},
'128x64': {
'multiplex': 0x3F,
'compins': 0x12,
'coloffset': 0
},
'96x16': {
'multiplex': 0x0F,
'compins': 0x2,
'coloffset': 0,
}
};
// Setup i2c
this.rg = rg;
this.rg.rgpio_sbc_sync();
this.i2c_hand = this.rg.i2c_open_sync(this.BUS, this.ADDRESS);
// console.log(`this.i2c_hand = ${this.i2c_hand}`);
var screenSize = this.WIDTH + 'x' + this.HEIGHT;
this.screenConfig = config[screenSize];
this._initialise();
}
Oled.prototype._initialise = function () {
// sequence of bytes to initialise with
var initSeq = [
this.DISPLAY_OFF,
this.SET_DISPLAY_CLOCK_DIV, 0x80,
this.SET_MULTIPLEX, this.screenConfig.multiplex, // set the last value dynamically based on screen size requirement
this.SET_DISPLAY_OFFSET, 0x00, // sets offset pro to 0
this.SET_START_LINE,
this.CHARGE_PUMP, 0x14, // charge pump val
this.MEMORY_MODE, 0x00, // 0x0 act like ks0108
this.SEG_REMAP, // screen orientation
this.COM_SCAN_DEC, // screen orientation change to INC to flip
this.SET_COM_PINS, this.screenConfig.compins, // com pins val sets dynamically to match each screen size requirement
this.SET_CONTRAST, 0x8F, // contrast val
this.SET_PRECHARGE, 0xF1, // precharge val
this.SET_VCOM_DETECT, 0x40, // vcom detect
this.DISPLAY_ALL_ON_RESUME,
this.NORMAL_DISPLAY,
this.DISPLAY_ON
];
var i, initSeqLen = initSeq.length;
// write init seq commands
for (i = 0; i < initSeqLen; i++) {
this._transfer('cmd', initSeq[i]);
}
}
// writes both commands and data buffers to this device
Oled.prototype._transfer = function (type, val, fn) {
var control;
if (type === 'data') {
control = 0x40;
} else if (type === 'cmd') {
control = 0x00;
} else {
return;
}
var bufferForSend;
bufferForSend = Buffer.from([control, val])
// send control and actual val
this.rg.i2c_write_device_sync(this.i2c_hand, bufferForSend, 2);
if (fn) {
fn();
}
}
// read a byte from the oled
Oled.prototype._readI2C = function (fn) {
var data = this.rg.i2c_read_byte_sync(this.i2c_hand);
fn(data);
}
// sometimes the oled gets a bit busy with lots of bytes.
// Read the response byte to see if this is the case
Oled.prototype._waitUntilReady = function (callback) {
var done,
oled = this;
function tick(callback) {
oled._readI2C(function (byte) {
// read the busy byte in the response
busy = byte >> 7 & 1;
if (!busy) {
// if not busy, it's ready for callback
callback();
} else {
setTimeout(function () { tick(callback) }, 0);
}
});
};
setTimeout(function () { tick(callback) }, 0);
}
// set starting position of a text string on the oled
Oled.prototype.setCursor = function (x, y) {
this.cursor_x = x;
this.cursor_y = y;
}
// write text to the oled
Oled.prototype.writeString = function (font, size, string, color, wrap, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
var wordArr = string.split(' '),
len = wordArr.length,
// start x offset at cursor pos
offset = this.cursor_x,
padding = 0;
// loop through words
for (var w = 0; w < len; w += 1) {
// put the word space back in for all in between words or empty words
if (w < len - 1 || !wordArr[w].length) {
wordArr[w] += ' ';
}
var stringArr = wordArr[w].split(''),
slen = stringArr.length,
compare = (font.width * size * slen) + (size * (len - 1));
// wrap words if necessary
if (wrap && len > 1 && w > 0 && (offset >= (this.WIDTH - compare))) {
offset = 0;
this.cursor_y += (font.height * size) + this.LINESPACING;
this.setCursor(offset, this.cursor_y);
}
// loop through the array of each char to draw
for (var i = 0; i < slen; i += 1) {
if (stringArr[i] === '\n') {
offset = 0;
this.cursor_y += (font.height * size) + this.LINESPACING;
this.setCursor(offset, this.cursor_y);
}
else {
// look up the position of the char, pull out the buffer slice
var charBuf = this._findCharBuf(font, stringArr[i]);
// read the bits in the bytes that make up the char
var charBytes = this._readCharBytes(charBuf, font.height);
// draw the entire character
this._drawChar(charBytes, font.height, size, false);
// calc new x position for the next char, add a touch of padding too if it's a non space char
//padding = (stringArr[i] === ' ') ? 0 : this.LETTERSPACING;
offset += (font.width * size) + this.LETTERSPACING;// padding;
// wrap letters if necessary
if (wrap && (offset >= (this.WIDTH - font.width - this.LETTERSPACING))) {
offset = 0;
this.cursor_y += (font.height * size) + this.LINESPACING;
}
// set the 'cursor' for the next char to be drawn, then loop again for next char
this.setCursor(offset, this.cursor_y);
}
}
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// draw an individual character to the screen
Oled.prototype._drawChar = function (byteArray, charHeight, size, sync) {
// take your positions...
var x = this.cursor_x,
y = this.cursor_y;
// loop through the byte array containing the hexes for the char
for (var i = 0; i < byteArray.length; i += 1) {
for (var j = 0; j < charHeight; j += 1) {
// pull color out
var color = byteArray[i][j],
xpos, ypos;
// standard font size
if (size === 1) {
xpos = x + i;
ypos = y + j;
this.drawPixel([xpos, ypos, color], false);
} else {
// MATH! Calculating pixel size multiplier to primitively scale the font
xpos = x + (i * size);
ypos = y + (j * size);
this.fillRect(xpos, ypos, size, size, color, false);
}
}
}
}
// get character bytes from the supplied font object in order to send to framebuffer
Oled.prototype._readCharBytes = function (byteArray, charHeight) {
var bitArr = [],
bitCharArr = [];
// loop through each byte supplied for a char
for (var i = 0; i < byteArray.length; i += 1) {
// set current byte
var byte = byteArray[i];
// read each byte
for (var j = 0; j < charHeight; j += 1) {
// shift bits right until all are read
var bit = byte >> j & 1;
bitArr.push(bit);
}
// push to array containing flattened bit sequence
bitCharArr.push(bitArr);
// clear bits for next byte
bitArr = [];
}
return bitCharArr;
}
// find where the character exists within the font object
Oled.prototype._findCharBuf = function (font, c) {
// use the lookup array as a ref to find where the current char bytes start
var cBufPos = font.lookup.indexOf(c) * font.width;
// slice just the current char's bytes out of the fontData array and return
var cBuf = font.fontData.slice(cBufPos, cBufPos + font.width);
return cBuf;
}
// send the entire framebuffer to the oled
Oled.prototype.update = function () {
// wait for oled to be ready
this._waitUntilReady(function () {
// set the start and endbyte locations for oled display update
var displaySeq = [
this.COLUMN_ADDR,
this.screenConfig.coloffset,
this.screenConfig.coloffset + this.WIDTH - 1, // column start and end address
this.PAGE_ADDR, 0, (this.HEIGHT / 8) - 1 // page start and end address
];
var displaySeqLen = displaySeq.length,
bufferLen = this.buffer.length,
i, v;
// send intro seq
for (i = 0; i < displaySeqLen; i += 1) {
this._transfer('cmd', displaySeq[i]);
}
// write buffer data
var bufferToSend = Buffer.concat([Buffer.from([0x40]), this.buffer]);
this.rg.i2c_write_device_sync(this.i2c_hand, bufferToSend, bufferToSend.length);
}.bind(this));
}
// send dim display command to oled
Oled.prototype.dimDisplay = function (bool) {
var contrast;
if (bool) {
contrast = 0; // Dimmed display
} else {
contrast = 0xCF; // Bright display
}
this._transfer('cmd', this.SET_CONTRAST);
this._transfer('cmd', contrast);
}
// turn oled off
Oled.prototype.turnOffDisplay = function () {
this._transfer('cmd', this.DISPLAY_OFF);
}
// turn oled on
Oled.prototype.turnOnDisplay = function () {
this._transfer('cmd', this.DISPLAY_ON);
}
// clear all pixels currently on the display
Oled.prototype.clearDisplay = function (sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
// write off pixels
this.buffer.fill(0x00);
if (immed) {
this.update();
}
}
// invert pixels on oled
Oled.prototype.invertDisplay = function (bool) {
if (bool) {
this._transfer('cmd', this.INVERT_DISPLAY); // inverted
} else {
this._transfer('cmd', this.NORMAL_DISPLAY); // non inverted
}
}
// draw an RGBA image at the specified coordinates
Oled.prototype.drawRGBAImage = function (image, dx, dy, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
// translate image data to buffer
var x, y, dataIndex, buffIndex, buffByte, bit, pixelByte;
var dyp = this.WIDTH * Math.floor(dy / 8); // calc once
var dxyp = dyp + dx;
for (x = 0; x < image.width; x++) {
var dxx = dx + x;
if (dxx < 0 || dxx >= this.WIDTH) {
// negative, off the screen
continue;
}
// start buffer index for image column
buffIndex = x + dxyp;
buffByte = this.buffer[buffIndex];
for (y = 0; y < image.height; y++) {
var dyy = dy + y; // calc once
if (dyy < 0 || dyy >= this.HEIGHT) {
// negative, off the screen
continue;
}
var dyyp = Math.floor(dyy / 8); // calc once
// check if start of buffer page
if (!(dyy % 8)) {
// check if we need to save previous byte
if ((x || y) && buffByte !== this.buffer[buffIndex]) {
// save current byte and get next buffer byte
this.buffer[buffIndex] = buffByte;
this.dirtyBytes.push(buffIndex);
}
// new buffer page
buffIndex = dx + x + this.WIDTH * dyyp;
buffByte = this.buffer[buffIndex];
}
// process pixel into buffer byte
dataIndex = (image.width * y + x) << 2; // 4 bytes per pixel (RGBA)
if (!image.data[dataIndex + 3]) {
// transparent, continue to next pixel
continue;
}
pixelByte = 0x01 << (dyy - 8 * dyyp);
bit = image.data[dataIndex] || image.data[dataIndex + 1] || image.data[dataIndex + 2];
if (bit) {
buffByte |= pixelByte;
}
else {
buffByte &= ~pixelByte;
}
}
if ((x || y) && buffByte !== this.buffer[buffIndex]) {
// save current byte
this.buffer[buffIndex] = buffByte;
this.dirtyBytes.push(buffIndex);
}
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// draw an image pixel array on the screen
Oled.prototype.drawBitmap = function (pixels, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
var x, y,
pixelArray = [];
for (var i = 0; i < pixels.length; i++) {
x = Math.floor(i % this.WIDTH);
y = Math.floor(i / this.WIDTH);
this.drawPixel([x, y, pixels[i]], false);
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// draw one or many pixels on oled
Oled.prototype.drawPixel = function (pixels, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
// handle lazy single pixel case
if (typeof pixels[0] !== 'object') pixels = [pixels];
pixels.forEach(function (el) {
// return if the pixel is out of range
var x = el[0], y = el[1], color = el[2];
if (x >= this.WIDTH || y >= this.HEIGHT) return;
// thanks, Martin Richards.
// I wanna can this, this tool is for devs who get 0 indexes
//x -= 1; y -=1;
var byte = 0,
page = Math.floor(y / 8),
pageShift = 0x01 << (y - 8 * page);
// is the pixel on the first row of the page?
(page == 0) ? byte = x : byte = x + (this.WIDTH * page);
// colors! Well, monochrome.
if (color === 'BLACK' || color === 0) {
this.buffer[byte] &= ~pageShift;
}
if (color === 'WHITE' || color > 0) {
this.buffer[byte] |= pageShift;
}
// push byte to dirty if not already there
if (this.dirtyBytes.indexOf(byte) === -1) {
this.dirtyBytes.push(byte);
}
}, this);
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// looks at dirty bytes, and sends the updated bytes to the display
Oled.prototype._updateDirtyBytes = function (byteArray) {
var blen = byteArray.length, i,
displaySeq = [];
// check to see if this will even save time
if (blen > (this.buffer.length / 7)) {
// just call regular update at this stage, saves on bytes sent
this.update();
// now that all bytes are synced, reset dirty state
this.dirtyBytes = [];
} else {
this._waitUntilReady(function () {
// iterate through dirty bytes
for (var i = 0; i < blen; i += 1) {
var byte = byteArray[i];
var page = Math.floor(byte / this.WIDTH);
var col = Math.floor(byte % this.WIDTH);
var displaySeq = [
this.COLUMN_ADDR, col, col, // column start and end address
this.PAGE_ADDR, page, page // page start and end address
];
var displaySeqLen = displaySeq.length, v;
// send intro seq
for (v = 0; v < displaySeqLen; v += 1) {
this._transfer('cmd', displaySeq[v]);
}
// send byte, then move on to next byte
this._transfer('data', this.buffer[byte]);
this.buffer[byte];
}
}.bind(this));
}
// now that all bytes are synced, reset dirty state
this.dirtyBytes = [];
}
// using Bresenham's line algorithm
Oled.prototype.drawLine = function (x0, y0, x1, y1, color, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1,
dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1,
err = (dx > dy ? dx : -dy) / 2;
while (true) {
this.drawPixel([x0, y0, color], false);
if (x0 === x1 && y0 === y1) break;
var e2 = err;
if (e2 > -dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// draw a filled rectangle on the oled
Oled.prototype.fillRect = function (x, y, w, h, color, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
// one iteration for each column of the rectangle
for (var i = x; i < x + w; i += 1) {
// draws a vert line
this.drawLine(i, y, i, y + h - 1, color, false);
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
}
// activate scrolling for rows start through stop
Oled.prototype.startScroll = function (dir, start, stop) {
var scrollHeader,
cmdSeq = [];
switch (dir) {
case 'right':
cmdSeq.push(this.RIGHT_HORIZONTAL_SCROLL); break;
case 'left':
cmdSeq.push(this.LEFT_HORIZONTAL_SCROLL); break;
// TODO: left diag and right diag not working yet
case 'left diagonal':
cmdSeq.push(
this.SET_VERTICAL_SCROLL_AREA, 0x00,
this.VERTICAL_AND_LEFT_HORIZONTAL_SCROLL,
this.HEIGHT
);
break;
// TODO: left diag and right diag not working yet
case 'right diagonal':
cmdSeq.push(
this.SET_VERTICAL_SCROLL_AREA, 0x00,
this.VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL,
this.HEIGHT
);
break;
}
this._waitUntilReady(function () {
cmdSeq.push(
0x00, start,
0x00, stop,
// TODO: these need to change when diagonal
0x00, 0xFF,
this.ACTIVATE_SCROLL
);
var i, cmdSeqLen = cmdSeq.length;
for (i = 0; i < cmdSeqLen; i += 1) {
this._transfer('cmd', cmdSeq[i]);
}
}.bind(this));
}
// stop scrolling display contents
Oled.prototype.stopScroll = function () {
this._transfer('cmd', this.DEACTIVATE_SCROLL); // stahp
}
/**
* Draw a circle outline - ported from https://www.npmjs.com/package/oled-ssd1306-i2c/v/1.0.6?activeTab=readme
* This method is ad verbatim translation from the corresponding
* method on the Adafruit GFX library
* https://github.com/adafruit/Adafruit-GFX-Library
*/
Oled.prototype.drawCircle = function (x0, y0, r, color, sync) {
var immed = (typeof sync === 'undefined') ? true : sync;
var f = 1 - r;
var ddF_x = 1;
var ddF_y = -2 * r;
var x = 0;
var y = r;
this.drawPixel(
[[x0, y0 + r, color],
[x0, y0 - r, color],
[x0 + r, y0, color],
[x0 - r, y0, color]],
false
);
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
this.drawPixel(
[[x0 + x, y0 + y, color],
[x0 - x, y0 + y, color],
[x0 + x, y0 - y, color],
[x0 - x, y0 - y, color],
[x0 + y, y0 + x, color],
[x0 - y, y0 + x, color],
[x0 + y, y0 - x, color],
[x0 - y, y0 - x, color]],
false
);
}
if (immed) {
this._updateDirtyBytes(this.dirtyBytes);
}
};
module.exports = Oled;
/*
* This code was forked from baltazor's oled-i2c-bus: https://www.npmjs.com/package/oled-i2c-bus
*/